From cb81579c53437f8a54872422e9cd1b6c48b8d674 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 12 Apr 2024 20:53:44 -0400 Subject: [PATCH] Copy over phoenix --- packages/phoenix/.gitignore | 4 + packages/phoenix/LICENSE | 661 ++++++ packages/phoenix/README.md | 62 + packages/phoenix/assets/index.html | 20 + packages/phoenix/config/dev.js | 23 + packages/phoenix/config/release.js | 23 + packages/phoenix/doc/devlog.md | 1152 ++++++++++ .../doc/graveyard/keyboard_modifiers.md | 77 + packages/phoenix/doc/graveyard/readline.md | 59 + packages/phoenix/doc/license_header.txt | 16 + packages/phoenix/doc/missing-posix.md | 23 + packages/phoenix/doc/parser.md | 55 + packages/phoenix/doc/readme-gif.gif | Bin 0 -> 260359 bytes .../phoenix/doc/stash/SymbolParserImpl.js | 54 + .../notalicense-license-checker-config.json | 26 + packages/phoenix/package-lock.json | 1888 +++++++++++++++++ packages/phoenix/package.json | 39 + .../phoenix/packages/contextlink/.gitignore | 1 + .../phoenix/packages/contextlink/context.js | 45 + .../phoenix/packages/contextlink/entry.js | 19 + .../packages/contextlink/package-lock.json | 916 ++++++++ .../phoenix/packages/contextlink/package.json | 18 + .../packages/contextlink/test/testcontext.js | 48 + .../phoenix/packages/newparser/exports.js | 101 + packages/phoenix/packages/newparser/lib.js | 29 + .../packages/newparser/parsers/combinators.js | 139 ++ .../packages/newparser/parsers/terminals.js | 46 + packages/phoenix/packages/pty/exports.js | 181 ++ packages/phoenix/packages/pty/package.json | 12 + .../packages/strataparse/dsl/ParserBuilder.js | 92 + .../strataparse/dsl/ParserRegistry.js | 29 + .../phoenix/packages/strataparse/exports.js | 118 ++ .../phoenix/packages/strataparse/package.json | 13 + .../phoenix/packages/strataparse/parse.js | 141 ++ .../parse_impls/StrUntilParserImpl.js | 48 + .../strataparse/parse_impls/combinators.js | 125 ++ .../strataparse/parse_impls/literal.js | 62 + .../strataparse/parse_impls/whitespace.js | 45 + .../phoenix/packages/strataparse/strata.js | 115 + .../ContextSwitchingPStratumImpl.js | 106 + .../FirstRecognizedPStratumImpl.js | 58 + .../MergeWhitespacePStratumImpl.js | 80 + .../strataparse/strata_impls/terminals.js | 68 + packages/phoenix/rollup.config.js | 49 + packages/phoenix/run.json5 | 15 + .../phoenix/src/ansi-shell/ANSIContext.js | 61 + packages/phoenix/src/ansi-shell/ANSIShell.js | 243 +++ .../src/ansi-shell/ConcreteSyntaxError.js | 57 + .../ansi-shell/arg-parsers/simple-parser.js | 53 + .../src/ansi-shell/decorators/errors.js | 38 + .../src/ansi-shell/ioutil/ByteWriter.js | 33 + .../src/ansi-shell/ioutil/MemReader.js | 44 + .../src/ansi-shell/ioutil/MemWriter.js | 70 + .../src/ansi-shell/ioutil/MultiWriter.js | 35 + .../src/ansi-shell/ioutil/NullifyWriter.js | 29 + .../src/ansi-shell/ioutil/ProxyReader.js | 25 + .../src/ansi-shell/ioutil/ProxyWriter.js | 26 + .../src/ansi-shell/ioutil/SignalReader.js | 64 + .../src/ansi-shell/ioutil/SyncLinesReader.js | 80 + .../src/ansi-shell/parsing/PARSE_CONSTANTS.js | 40 + .../ansi-shell/parsing/PuterShellParser.js | 54 + .../parsing/UnquotedTokenParserImpl.js | 52 + .../src/ansi-shell/parsing/brainstorming.js | 25 + .../parsing/buildParserFirstHalf.js | 222 ++ .../parsing/buildParserSecondHalf.js | 441 ++++ .../src/ansi-shell/pipeline/Coupler.js | 54 + .../phoenix/src/ansi-shell/pipeline/Pipe.js | 43 + .../src/ansi-shell/pipeline/Pipeline.js | 407 ++++ .../src/ansi-shell/pipeline/iowrappers.js | 45 + .../src/ansi-shell/readline/history.js | 80 + .../src/ansi-shell/readline/readline.js | 362 ++++ .../src/ansi-shell/readline/readtoken.js | 107 + .../src/ansi-shell/readline/rl_comprehend.js | 135 ++ .../ansi-shell/readline/rl_csi_handlers.js | 212 ++ .../src/ansi-shell/readline/rl_words.js | 34 + packages/phoenix/src/ansi-shell/signals.js | 22 + packages/phoenix/src/context/context.js | 68 + packages/phoenix/src/main_cli.js | 70 + packages/phoenix/src/main_puter.js | 79 + packages/phoenix/src/meta/versions.js | 144 ++ packages/phoenix/src/platform/PosixError.js | 143 ++ packages/phoenix/src/platform/node/env.js | 20 + .../phoenix/src/platform/node/filesystem.js | 219 ++ packages/phoenix/src/pty/NodeStdioPTT.js | 74 + packages/phoenix/src/pty/XDocumentPTT.js | 75 + .../completers/command_completer.js | 39 + .../puter-shell/completers/file_completer.js | 49 + .../completers/option_completer.js | 57 + .../src/puter-shell/coreutils/__exports__.js | 106 + .../phoenix/src/puter-shell/coreutils/ai.js | 87 + .../src/puter-shell/coreutils/basename.js | 81 + .../phoenix/src/puter-shell/coreutils/cat.js | 60 + .../phoenix/src/puter-shell/coreutils/cd.js | 48 + .../src/puter-shell/coreutils/changelog.js | 52 + .../src/puter-shell/coreutils/clear.js | 31 + .../puter-shell/coreutils/concept-parser.js | 320 +++ .../coreutils/coreutil_lib/echo_escapes.js | 162 ++ .../coreutils/coreutil_lib/exit.js | 24 + .../coreutils/coreutil_lib/help.js | 134 ++ .../coreutils/coreutil_lib/validate.js | 36 + .../phoenix/src/puter-shell/coreutils/cp.js | 68 + .../phoenix/src/puter-shell/coreutils/date.js | 321 +++ .../src/puter-shell/coreutils/dcall.js | 59 + .../src/puter-shell/coreutils/dirname.js | 93 + .../phoenix/src/puter-shell/coreutils/echo.js | 74 + .../phoenix/src/puter-shell/coreutils/env.js | 36 + .../src/puter-shell/coreutils/errno.js | 113 + .../src/puter-shell/coreutils/false.js | 32 + .../phoenix/src/puter-shell/coreutils/grep.js | 162 ++ .../phoenix/src/puter-shell/coreutils/head.js | 73 + .../phoenix/src/puter-shell/coreutils/help.js | 76 + .../phoenix/src/puter-shell/coreutils/jq.js | 76 + .../src/puter-shell/coreutils/login.js | 50 + .../phoenix/src/puter-shell/coreutils/ls.js | 268 +++ .../phoenix/src/puter-shell/coreutils/man.js | 31 + .../src/puter-shell/coreutils/mkdir.js | 65 + .../phoenix/src/puter-shell/coreutils/mv.js | 55 + .../src/puter-shell/coreutils/neofetch.js | 95 + .../src/puter-shell/coreutils/printf.js | 493 +++++ .../src/puter-shell/coreutils/printhist.js | 34 + .../phoenix/src/puter-shell/coreutils/pwd.js | 31 + .../phoenix/src/puter-shell/coreutils/rm.js | 64 + .../src/puter-shell/coreutils/rmdir.js | 52 + .../src/puter-shell/coreutils/sample-data.js | 42 + .../phoenix/src/puter-shell/coreutils/sed.js | 725 +++++++ .../src/puter-shell/coreutils/sleep.js | 44 + .../phoenix/src/puter-shell/coreutils/sort.js | 183 ++ .../phoenix/src/puter-shell/coreutils/tail.js | 87 + .../phoenix/src/puter-shell/coreutils/test.js | 32 + .../src/puter-shell/coreutils/touch.js | 58 + .../phoenix/src/puter-shell/coreutils/true.js | 30 + .../src/puter-shell/coreutils/txt2img.js | 58 + .../src/puter-shell/coreutils/usages.js | 36 + .../phoenix/src/puter-shell/coreutils/wc.js | 184 ++ .../src/puter-shell/coreutils/which.js | 75 + packages/phoenix/src/puter-shell/main.js | 202 ++ .../puter-shell/plugins/ChatHistoryPlugin.js | 86 + .../providers/BuiltinCommandProvider.js | 34 + .../providers/CompositeCommandProvider.js | 45 + .../providers/PathCommandProvider.js | 203 ++ .../providers/PuterAppCommandProvider.js | 74 + .../providers/ScriptCommandProvider.js | 67 + packages/phoenix/src/util/bytes.js | 55 + packages/phoenix/src/util/file.js | 28 + packages/phoenix/src/util/lang.js | 31 + packages/phoenix/src/util/log.js | 34 + packages/phoenix/src/util/path.js | 33 + packages/phoenix/src/util/singleton.js | 19 + packages/phoenix/src/util/statemachine.js | 152 ++ packages/phoenix/src/util/wrap-text.js | 111 + packages/phoenix/test.js | 48 + packages/phoenix/test/coreutils.test.js | 49 + packages/phoenix/test/coreutils/basename.js | 130 ++ packages/phoenix/test/coreutils/date.js | 288 +++ packages/phoenix/test/coreutils/dirname.js | 110 + packages/phoenix/test/coreutils/echo.js | 68 + packages/phoenix/test/coreutils/env.js | 36 + packages/phoenix/test/coreutils/errno.js | 144 ++ packages/phoenix/test/coreutils/false.js | 50 + packages/phoenix/test/coreutils/harness.js | 71 + packages/phoenix/test/coreutils/head.js | 117 + packages/phoenix/test/coreutils/printf.js | 587 +++++ packages/phoenix/test/coreutils/sleep.js | 110 + packages/phoenix/test/coreutils/sort.js | 154 ++ packages/phoenix/test/coreutils/tail.js | 117 + packages/phoenix/test/coreutils/true.js | 44 + packages/phoenix/test/coreutils/wc.js | 122 ++ packages/phoenix/test/readtoken.js | 60 + packages/phoenix/test/test-bytes.js | 36 + .../phoenix/test/test-stateful-processor.js | 107 + packages/phoenix/test/wrap-text.js | 84 + packages/phoenix/tools/build_tar.sh | 21 + packages/phoenix/tools/gen.js | 71 + 173 files changed, 20045 insertions(+) create mode 100644 packages/phoenix/.gitignore create mode 100644 packages/phoenix/LICENSE create mode 100644 packages/phoenix/README.md create mode 100644 packages/phoenix/assets/index.html create mode 100644 packages/phoenix/config/dev.js create mode 100644 packages/phoenix/config/release.js create mode 100644 packages/phoenix/doc/devlog.md create mode 100644 packages/phoenix/doc/graveyard/keyboard_modifiers.md create mode 100644 packages/phoenix/doc/graveyard/readline.md create mode 100644 packages/phoenix/doc/license_header.txt create mode 100644 packages/phoenix/doc/missing-posix.md create mode 100644 packages/phoenix/doc/parser.md create mode 100644 packages/phoenix/doc/readme-gif.gif create mode 100644 packages/phoenix/doc/stash/SymbolParserImpl.js create mode 100644 packages/phoenix/notalicense-license-checker-config.json create mode 100644 packages/phoenix/package-lock.json create mode 100644 packages/phoenix/package.json create mode 100644 packages/phoenix/packages/contextlink/.gitignore create mode 100644 packages/phoenix/packages/contextlink/context.js create mode 100644 packages/phoenix/packages/contextlink/entry.js create mode 100644 packages/phoenix/packages/contextlink/package-lock.json create mode 100644 packages/phoenix/packages/contextlink/package.json create mode 100644 packages/phoenix/packages/contextlink/test/testcontext.js create mode 100644 packages/phoenix/packages/newparser/exports.js create mode 100644 packages/phoenix/packages/newparser/lib.js create mode 100644 packages/phoenix/packages/newparser/parsers/combinators.js create mode 100644 packages/phoenix/packages/newparser/parsers/terminals.js create mode 100644 packages/phoenix/packages/pty/exports.js create mode 100644 packages/phoenix/packages/pty/package.json create mode 100644 packages/phoenix/packages/strataparse/dsl/ParserBuilder.js create mode 100644 packages/phoenix/packages/strataparse/dsl/ParserRegistry.js create mode 100644 packages/phoenix/packages/strataparse/exports.js create mode 100644 packages/phoenix/packages/strataparse/package.json create mode 100644 packages/phoenix/packages/strataparse/parse.js create mode 100644 packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js create mode 100644 packages/phoenix/packages/strataparse/parse_impls/combinators.js create mode 100644 packages/phoenix/packages/strataparse/parse_impls/literal.js create mode 100644 packages/phoenix/packages/strataparse/parse_impls/whitespace.js create mode 100644 packages/phoenix/packages/strataparse/strata.js create mode 100644 packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js create mode 100644 packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js create mode 100644 packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js create mode 100644 packages/phoenix/packages/strataparse/strata_impls/terminals.js create mode 100644 packages/phoenix/rollup.config.js create mode 100644 packages/phoenix/run.json5 create mode 100644 packages/phoenix/src/ansi-shell/ANSIContext.js create mode 100644 packages/phoenix/src/ansi-shell/ANSIShell.js create mode 100644 packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js create mode 100644 packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js create mode 100644 packages/phoenix/src/ansi-shell/decorators/errors.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/MemReader.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/MemWriter.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/SignalReader.js create mode 100644 packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js create mode 100644 packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js create mode 100644 packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js create mode 100644 packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js create mode 100644 packages/phoenix/src/ansi-shell/parsing/brainstorming.js create mode 100644 packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js create mode 100644 packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js create mode 100644 packages/phoenix/src/ansi-shell/pipeline/Coupler.js create mode 100644 packages/phoenix/src/ansi-shell/pipeline/Pipe.js create mode 100644 packages/phoenix/src/ansi-shell/pipeline/Pipeline.js create mode 100644 packages/phoenix/src/ansi-shell/pipeline/iowrappers.js create mode 100644 packages/phoenix/src/ansi-shell/readline/history.js create mode 100644 packages/phoenix/src/ansi-shell/readline/readline.js create mode 100644 packages/phoenix/src/ansi-shell/readline/readtoken.js create mode 100644 packages/phoenix/src/ansi-shell/readline/rl_comprehend.js create mode 100644 packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js create mode 100644 packages/phoenix/src/ansi-shell/readline/rl_words.js create mode 100644 packages/phoenix/src/ansi-shell/signals.js create mode 100644 packages/phoenix/src/context/context.js create mode 100644 packages/phoenix/src/main_cli.js create mode 100644 packages/phoenix/src/main_puter.js create mode 100644 packages/phoenix/src/meta/versions.js create mode 100644 packages/phoenix/src/platform/PosixError.js create mode 100644 packages/phoenix/src/platform/node/env.js create mode 100644 packages/phoenix/src/platform/node/filesystem.js create mode 100644 packages/phoenix/src/pty/NodeStdioPTT.js create mode 100644 packages/phoenix/src/pty/XDocumentPTT.js create mode 100644 packages/phoenix/src/puter-shell/completers/command_completer.js create mode 100644 packages/phoenix/src/puter-shell/completers/file_completer.js create mode 100644 packages/phoenix/src/puter-shell/completers/option_completer.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/__exports__.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/ai.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/basename.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/cat.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/cd.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/changelog.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/clear.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/concept-parser.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/cp.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/date.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/dcall.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/dirname.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/echo.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/env.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/errno.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/false.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/grep.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/head.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/help.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/jq.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/login.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/ls.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/man.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/mkdir.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/mv.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/neofetch.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/printf.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/printhist.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/pwd.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/rm.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/rmdir.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sample-data.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sed.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sleep.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/sort.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/tail.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/test.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/touch.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/true.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/txt2img.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/usages.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/wc.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/which.js create mode 100644 packages/phoenix/src/puter-shell/main.js create mode 100644 packages/phoenix/src/puter-shell/plugins/ChatHistoryPlugin.js create mode 100644 packages/phoenix/src/puter-shell/providers/BuiltinCommandProvider.js create mode 100644 packages/phoenix/src/puter-shell/providers/CompositeCommandProvider.js create mode 100644 packages/phoenix/src/puter-shell/providers/PathCommandProvider.js create mode 100644 packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js create mode 100644 packages/phoenix/src/puter-shell/providers/ScriptCommandProvider.js create mode 100644 packages/phoenix/src/util/bytes.js create mode 100644 packages/phoenix/src/util/file.js create mode 100644 packages/phoenix/src/util/lang.js create mode 100644 packages/phoenix/src/util/log.js create mode 100644 packages/phoenix/src/util/path.js create mode 100644 packages/phoenix/src/util/singleton.js create mode 100644 packages/phoenix/src/util/statemachine.js create mode 100644 packages/phoenix/src/util/wrap-text.js create mode 100644 packages/phoenix/test.js create mode 100644 packages/phoenix/test/coreutils.test.js create mode 100644 packages/phoenix/test/coreutils/basename.js create mode 100644 packages/phoenix/test/coreutils/date.js create mode 100644 packages/phoenix/test/coreutils/dirname.js create mode 100644 packages/phoenix/test/coreutils/echo.js create mode 100644 packages/phoenix/test/coreutils/env.js create mode 100644 packages/phoenix/test/coreutils/errno.js create mode 100644 packages/phoenix/test/coreutils/false.js create mode 100644 packages/phoenix/test/coreutils/harness.js create mode 100644 packages/phoenix/test/coreutils/head.js create mode 100644 packages/phoenix/test/coreutils/printf.js create mode 100644 packages/phoenix/test/coreutils/sleep.js create mode 100644 packages/phoenix/test/coreutils/sort.js create mode 100644 packages/phoenix/test/coreutils/tail.js create mode 100644 packages/phoenix/test/coreutils/true.js create mode 100644 packages/phoenix/test/coreutils/wc.js create mode 100644 packages/phoenix/test/readtoken.js create mode 100644 packages/phoenix/test/test-bytes.js create mode 100644 packages/phoenix/test/test-stateful-processor.js create mode 100644 packages/phoenix/test/wrap-text.js create mode 100755 packages/phoenix/tools/build_tar.sh create mode 100644 packages/phoenix/tools/gen.js diff --git a/packages/phoenix/.gitignore b/packages/phoenix/.gitignore new file mode 100644 index 00000000..e62e8910 --- /dev/null +++ b/packages/phoenix/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +release/ +*.tar diff --git a/packages/phoenix/LICENSE b/packages/phoenix/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/packages/phoenix/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/phoenix/README.md b/packages/phoenix/README.md new file mode 100644 index 00000000..81f5258a --- /dev/null +++ b/packages/phoenix/README.md @@ -0,0 +1,62 @@ +# Important notice + +This repository is being moved to [the monorepo](https://github.com/HeyPuter/puter). + +
+ +

Phoenix

+

Puter's pure-javascript shell

+

+
+ +`phoenix` is a pure-javascript shell built for [puter.com](https://puter.com). +Following the spirit of open-source initiatives we've seen like +[SerenityOS](https://serenityos.org/), +we've built much of the shell's functionality from scratch. +Some interesting portions of this shell include: +- A shell parser which produces a Concrete-Syntax-Tree +- Pipeline constructs built on top of the [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) +- Platform support for Puter + +The shell is a work in progress. The following improvements are considered in-scope: +- Anything specified in [POSIX.1-2017 Chapter 2](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html) +- UX improvements over traditional shells + > examples include: readline syntax highlighting, hex view for binary streams +- Platform support, so `phoenix` can run in more environments + +## Running Phoenix + +### In a Browser + +You can use the [terminal on Puter](https://puter.com/app/terminal), +or run from source by following the instructions provided for +[Puter's terminal emulator](https://github.com/HeyPuter/terminal). + +### Running in Node + +Under node.js Phoenix acts as a shell for your operating system. +This is a work-in-progress and lots of things are not working +yet. If you'd like to try it out you can run `src/main_cli.js`. +Check [this issue](https://github.com/HeyPuter/phoenix/issues/14) +for updated information on our progress. + +## Testing + +You can find our tests in the [test/](./test) directory. +Testing is done with [mocha](https://www.npmjs.com/package/mocha). +Make sure it's installed, then run: + +```sh +npm test +``` + +## What's on the Roadmap? + +We're looking to continue improving the shell and broaden its usefulness. +Here are a few ideas we have for the future: + +- local machine platform support + > See [this issue](https://github.com/HeyPuter/phoenix/issues/14) +- further support for the POSIX Command Language + > Check our list of [missing features](doc/missing-posix.md) + diff --git a/packages/phoenix/assets/index.html b/packages/phoenix/assets/index.html new file mode 100644 index 00000000..9f609ae7 --- /dev/null +++ b/packages/phoenix/assets/index.html @@ -0,0 +1,20 @@ + + + + + + + Document + + + + + + + + + diff --git a/packages/phoenix/config/dev.js b/packages/phoenix/config/dev.js new file mode 100644 index 00000000..01886c81 --- /dev/null +++ b/packages/phoenix/config/dev.js @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +globalThis.__CONFIG__ = { + "origin": "https://puter.local:8080", + "shell.href": "https://puter.local:8081", + "sdk_url": "http://puter.localhost:4100/sdk/puter.js", +}; diff --git a/packages/phoenix/config/release.js b/packages/phoenix/config/release.js new file mode 100644 index 00000000..45e3cbc4 --- /dev/null +++ b/packages/phoenix/config/release.js @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +globalThis.__CONFIG__ = { + origin: location.origin, + 'shell.href': location.origin + '/puter-shell/', + sdk_url: 'https://puter.com/puter.js/v2', +}; diff --git a/packages/phoenix/doc/devlog.md b/packages/phoenix/doc/devlog.md new file mode 100644 index 00000000..59aad3f9 --- /dev/null +++ b/packages/phoenix/doc/devlog.md @@ -0,0 +1,1152 @@ +## 2023-05-05 + +### Iframe Shell Architecture + +Separating the terminal emulator from the shell will make it possible to re-use +Puter's terminal emulator for containers, emulators, and tunnels. + +Puter's shell will follow modern approaches for handling data; this means: +- Commands will typically operate on streams of objects + rather than streams of bytes. +- Rich UI capabilities, such as images, will be possible. + +To use Puter's shell, this terminal emulator will include an adapter shell. +The adapter shell will delegate to the real Puter shell and provide a sensible +interface for an end-user using an ANSI terminal emulator. + +This means the scope of this terminal emulator is to be compatible with +two types of shells: +- Legacy ANSI-compatible shells +- Modern Puter-compatible shells + +To avoid duplicate effort, the ANSI adapter for the Puter shell will be +accessed by the terminal emulator through cross-document messaging, +as though it were any other ANSI shell provided by a third-party service. +This will also keep these things loosely coupled so we can separate the +adapter in the future and allow other terminal emulators to take +advantage of it. + +## 2023-05-06 + +### The Context + +In creating the state processor I made a variable called +`ctx`, representing contextual information for state functions. + +The context has a few properties on it: +- constants +- locals +- vars +- externs + +#### constants + +Constants are immutable values tied to the context. +They can be overriden when a context is constructed but +cannot be overwritten within an instance of the context. + +#### variables + +Variabes are mutable context values which the caller providing +the context might be able to access. + +#### locals + +Locals are the same as varaibles but the state processor +exports them. This might not have been a good idea; +maybe to the user of a context these should appear +to be the same as variables, because the code using a +context doesn't care what the longevity of locals is +vs variables. + +Perhaps locals could be a useful concept for values that +only change under a sub-context, but this is already +true about constants since sub-contexts can override +them. After all, I can't think of a compelling reason +not to allow overridding constants when you're creating +a sub-context. + +#### externs + +Externs are like constants in that they're not mutable to +the code using a context. However, unlike constants they're +not limited to primitive values. They can be objects and +these objects can have side-effects. + +### How to make the context better moving forward? + +#### Composing contexts + +The ability to compose context would be useful. For example +the readline function could have a context that's a composition +of the ANSI context (containing ANSI constantsl maybe also +library functions in the future), an outputter context since +it outputs characters to the terminal, as well as a context +specific to handlers under the readline utility. + +#### Additional reflection +This idea of contexts and compositing contexts is actually +something I've been thinking about for a long time. Contexts +are an essential component in FOAM for example. However, this +idea of separating **constants**, **imports**, and +**side-effect varibles** (that is, variables something else +is able to access), +is not something I thought about until I looked at the source +code for ash (an implementation of `sh`), and considered how +I might make that source code more portable by repreasting +it as language-agnostic data. + +## 2023-05-07 + +### Conclusion of Context Thing from Yesterday + +I just figured something out after re-reading yesterday's +devlog entry. + +While the State Processor needs a separate concept of +variables vs locals, even the state functions don't care +about this distinction. It's only there so certain values +are cleared at each iteration of the state processor. + +This means a context can be composed at each iteration +containing both the instance variables and the transient +variables. + +### When Contexts are Equivalent to Pure Functions + +In pure-functional logic functions do not have side effects. +This means they would never change a value by reference, but +they would return a value. + +When a subcontext is created prior to a function call, this +is equivalent to a pure function under certain conditions: +- the values which may be changed must be explicity stated +- the immediate consequences of updating any value are known + +## 2023-05-08 + +### Sending authorization information to the shell + +Separating the terminal emulator from the shell currenly +means that the terminal is a Puter app and the shell is +a service being used by a Puter app, rather than natively +being a Puter app. + +This may change in the future, but currently it means the +terminal emulator needs to - not because it's the terminal +emulator, but because it's the Puter application - configure +the shell with authorization information. + +There are a few different approaches to this: +- pass query string parameters onto the shell via url +- send a non-binary postMessage with configuration +- send an ANSI escape code followed by a binary-encoded + configuration message +- construct a Form object in javascript and do a POST + request to the target iframe + +The last option seems like it could be a CORS nightmare since +right now I'm testing in a situation where the shell happens +to be under the same domain name as the terminal emulator, but +this may not always be the case. + +Passing query string parameters over means authorization +tokens are inside the DOM. While this is already true +about the parent iframe I'd like to avoid this in case we +find security issues with this approach under different +situations. For example the parent iframe is in a situation +where userselect and the default context menu are disabled, +which may be preventing a user from accidentally putting +sensitive html attributes in their clipboard. + +That leaves the two options for sending a postMessage: +either binary, or a non-binary message. The binary approach +would require finding handling an OSC escape sequence handler +and creating some conventions for how to communicate with +Puter's API using ANSI escape codes. While this might be useful +in the future, it seems more practical to create a higher-level +message protocol first and then eventually create an adapter +for OSC codes in the future if need is found for one. + +So with that, here are window messages between Puter's +ANSI terminal emulator and Puter's ANSI adapter for Puter's +shell: + +#### Ready message + +Sent by shell when it's loaded. + +``` +{ $: 'ready' } +``` + +#### Config message + +Sent by terminal emulator after shell is loaded. + +``` +{ + $: 'config', + ...variables +} +``` + +All `variables` are currently keys from the querystring +but this may change as the authorization mechanism and +available shell features mature. + +## 2023-05-09 + +### Parsing CLI arguments + +Node has a bulit-in utility, but using this would be +unreliable because it's allowed to depend on system-provided +APIs which won't be available in a browser. + +There's +[a polyfill](https://github.com/pkgjs/parseargs/tree/main) +which doesn't appear to depend on any node builtins. +It does not support sub-commands, nor does it generate +helptext, but it's a starting point. + +If each command specifies a parser for CLI arguments, and also +provides configuration in a format specific to that parser, +there are a few advantages: +- easy to migrate away from this polyfill later by creating an + adapter or updating the commands which use it. +- easy to add custom argument processors for commands which + have an interface that isn't strictly adherent to convention. +- auto-complete and help can be generated with knowledge of how + CLI arguments are processed by a particular command. + +## 2023-05-10 + +### Kind of tangential, but synonyms are annoying + +The left side of a UNIX pipe is the +- source, faucet, producer, upstream + +The right side of a UNIX pipe is the +- target, sink, consumer, downstream + +I'm going to go with `source` and `target` for any cases like this +because they have the same number of letters, and I like when similar +lines of code are the same length because it's easier to spot errors. + +## 2023-05-14 + +### Retro: Terminal Architecture + +#### class: PreparedCommand + +A prepared command contains information about a command which will +be invoked within a pipeline, including: +- the command to be invoked +- the arguments for the command (as tokens) +- the context that the command will be run under + +A prepared command is created using the static method +`PreparedCommand.createFromTokens`. It does not have a +context until `setContext` is later called. + +#### class Pipeline + +A pipeline contains PreparedCommand instances which represent the +commands that will be run together in a pipeline. + +A pipeline is created using the static method +`Pipeline.createFromTokens`, which accepts a context under which +the pipeline will be constructed. The pipeline's `execute` method +will also be passed a context when the pipeline should begin running, +and this context can be different. (this is also the context that +will be passed to each `PreparedCommand` instance before each +respective `execute` method is called). + +#### class Pipe + +A pipe is composed of a readable stream and a writable stream. +A respective `reader` and `writer` are exposed as +`out` and `in` respectively. + +The readable stream and writable stream are tied together. + +#### class Coupler + +A coupler aggregates a reader and a writer and begins actively +reading, relaying all items to the writer. + +This behaviour allows a coupler to be used as a way of connecting +two pipes together. + +At the time of writing this, it's used to tie the pipe that is +created after the last command in a pipeline to the writer of the +pseudo terminal target, instead of giving the last command this +writer directly. This allows the command to close its output pipe +without affecting subsequent functionality of the terminal. + +### Behaviour of echo escapes + +#### behaviour of `\\` should be verified +Based on experimentation in Bash: +- `\\` is always seen by echo as `\` + - this means `\\a` and `\a` are the same + +#### difference between `\x` and `\0` + +In echo, `\0` initiates an octal escape while `\x` initiates +a hexadecimal escape. + +However, `\0` without being followed by a valid octal sequence +is considered to be `NULL`, while `\x` will be outputted literally +if not followed with a valid hexadecimal sequence. + +If either of these escapes has at least one valid character +for its respective numeric base, it will be processed with that +value. So, for example, `echo -en "\xag" | hexdump -C` shows +bytes `0A 67`, as does the same with `\x0ag` instead of `\xag`. + +## 2023-05-15 + +### Synchronization bug in Coupler + +[this issue](https://github.com/HeyPuter/dev-ansi-terminal/issues/1) +was caused by an issue where the `Coupler` between a pipeline and +stdout was still writing after the command was completed. +This happens because the listener loop in the Coupler is async and +might defer writing until after the pipeline has returned. + +This was fixed by adding a member to Coupler called `isDone` which +provides a promise that resolves when the Coupler receives the end +of the stream. As a consequence of this it is very important to +ensure that the stream gets closed when commands are finished +executing; right now the `PreparedCommand` class is responsible for +this behaviour, so all commands should be executed via +`PreparedCommand`. + +### tail, and echo output chunking + +Right now `tail` outputs the last two items sent over the stream, +and doesn't care if these items contain line breaks. For this +implementation to work the same as the "real" tail, it must be +asserted that each item over the stream is a separate line. + +Since `ls` is outputting each line on a separate call to +`out.write` it is working correctly with tail, but echo is not. +This could be fixed in `tail` itself, having it check each item +for line breaks while iterating backwards, but I would rather have +metadata on each command specifying how it expects its input +to be chunked so that the shell can accommodate; although this isn't +how it works in "real" bash, it wouldn't affect the behaviour of +shell scripts or input and it's closer to the model of Puter's shell +for JSON-like structured data, which may help with improved +interoperability and better code reuse. + +## 2023-05-22 + +### Catching Up + +There hasn't been much to log in the past few days; most updates +to the terminal have been basic command additions. + +The next step is adding the redirect operators (`<` and `>`), +which should involve some information written in this dev log. + +### Multiple Output Redirect + +In Bash, the redirect operator has precedence over pipes. This is +sensible but also results in some situations where a prompt entry +has dormant pieces, for example two output redirects (only one of +them will be used), or an output redirect and a pipe (the pipe will +receive nothing from stdout of the left-hand process). + +Here's an example with two output redirects: + +``` +some-command > a_file.txt > b_file.txt +``` + +In Puter's ANSI shell we could allow this as a way of splitting +the output. Although, this only really makes sense if stdout will +also be passed through the pipeline instead of consumed by a +redirect, otherwise the behaviour is counterintuitive. + +Maybe for this purpose we can have a couple modes of interpretation, +one where the Puter ANSI Shell behaves how Bash would and another +where it behaves in a more convenient way. Shell scripts with no +hashbang would be interpreted the Bash-like way while shell scripts +with a puter-specific hashbang would be interpreted in this more +convenient way. + +For now I plan to prioritize the way that seems more logical as it +will help keep the logic of the shell less complicated. I think it's +likely that we'll reach full POSIX compatibility via Bash running in +containers or emulators before the Puter ANSI shell itself reaches +full POSIX compatibility, so for this reason it makes sense to +prioritize making the Puter ANSI shell convenient and powerful over +making it behave like Bash. Additionally, we have a unique situation +where we're not so bound to backwards compatibility as is a +distribution of a personal computer operating system, so we should +take advantage of that where we can. + +## 2023-05-23 + +### Adding more coreutils + +- `clear` was very easy; it's just an escape code +- `printenv` was also very easy; most of the effort was already done + +### First steps to handling tab-completion + +#### Getting desired tab-completion behaviour from input state +Tab-completion needs information about the type of command arguments. +Since commands are modelled, it's possible the model of a command can +provide this information. For example a registered command could +implement `getTabCompleterFor(ARG_SPEC)`. + +`ARG_SPEC` would be an identifier for an argument that is understood +by readline. Ex: `{ $: 'positional', pos: 0 }` for the first positional +argument, or `{ $: 'named', name: 'verbose' }` for a named parameter +called `verbose`. + +The command model already has a nested model specifying how arguments +are parsed, so this model could describe the behaviour for a +`getArgSpecFromInputState(input, i)`, where `input` is the +current text in readline's buffer and `i` is the cursor position. +This separates the concern of knowing what parameter the user is +typing in from readline, allowing modelled commands to support tab +completion for arbitrary syntaxes. + +**revision** + +It's better if the command model has just one method which +readline needs to call, ex: `getTabCompleterFromInputState`. +I've left the above explanation as-is however because it's easier +to explain the two halves if its functionality separately. + +### Trigger background readdir call on PWD change + +When working on the FUSE driver for Puter's filesystem I noticed that +tab completion makes a readdir call upon the user pressing tab which +blocks the tab completion behaviour until the call is finished. +While this works fine on local filesystems, it's very confusing on +remote filesystems where the ping delay will - for a moment - make it +look like tab completion isn't working at all. + +Puter's shell can handle this a bit better. Triggering a readdir call +whenever PWD changes will allow tab-completion to have a quicker +response time. However, there's a caveat; the information about what +nodes exist in that directory might be outdated by the time the user +tries to use tab completion. + +My first thought was for "tab twice" to invoke a readdir to get the +most recent result, however this conflicts with pressing tab once to +get the completed path and then pressing tab a second time to get +a list of files within that path. + +My second thougfht is using ctrl + tab. The terminal will need to +provide some indication to the user that they can do this and what +is happening. + +Here are a few thoughts on how to do this with ideal UX: + +- after pressing tab: + - complete the text if possible + - highlight the completed portion in a **bright** color + - a dim colour would convey that the completion wasn't input yet + - display in a **hint bar** the following items: + - `[Ctrl+Tab]: re-complete with recent data` + - `[Ctrl+Enter]: more options` + +### Implementation of background readdir + +The background `readdir` could be invoked in two ways: +- when the current working directory changes +- at a poll interval + +These means the **action** of invoking background readdir needs +to be separate from the method by which it is called. + +Also, results from a previous `readdir` need to be marked invalid +when the current working directory changes. + +There is a possibility that the user might use tab completion before +the first `readdir` is called for a given pwd, which means the method +to get path completions must be async. + +if `readdir` is called because of a pwd change, the poll timer should +be reset so that it's not called again too quickly or at the same +time. + +#### Concern Mapping + +- **PuterANSIShell** + - does not need to be aware of this feature +- **readline** + - needs to trap Tab + - needs to recognize what command is being entered + - needs to delegate tab completion logic to the command's model + - does not need to be aware of how tab completion is implemented +- **readdir action** + - needs WRITE to cached dir lists +- **readdir poll timer** + - needs READ to cached dir lists to check when they were + updated + - needs the path to be polled + +#### Order of implementation + +- First implementation will **not** have **background readdir**. + - Interfaces should be appropriate to implement this after. +- When tab completion is working for paths, then readdir caching + can be implemented. + +## 2023-05-25 + +### Revising the boundary between ANSI view and Puter Shell + +Now there are several coreutil commands and a few key shell +features, so it's a good time to take a look at the architecture +and see if the boundary between the ANSI view and Puter Shell +corresponds to the original intention. + +| Shell | I/O | instructions | +| ------------ | ----- | ------------ | +| ANSI Adapter | TTY | text | +| Puter Shell | JSON | logical tree | + +Note from the above table that the Puter Shell itself should +be "syntax agnostic" - i.e. it needs the ANSI adapter or a +GUI on top of it to be useful at the UI boundary. + +#### Pipelines + +The ANSI view should be concerned with pipe syntax, while +pipeline execution should be a concern of the syntax-agnostic +shell. However, currently the ANSI view is responsible for +both. This is because there is no intermediate format for +parsed pipeline instructions. + +##### to improve +- create intermediate representation of pipelines and redirects + +#### Command IO + +The ANSI shell does IO in terms of either bytes or strings. When +commands output strings instead of bytes, their output is adapted +to the Uint8Array type to prevent commands further in the pipeline +from misbehaving due to an unexpected input type. + +Since pipeline I/O should be handled at the Puter shell, this kind +of adapting will happen at that level also. + +#### to improve +- ANSI view should send full pipeline to Puter Shell +- Puter Shell protocol should be improved so that the + client/view can specify a desired output format + (i.e. streams vs objects) + +### Pipeline IR + +The following is an intermediate representation for pipelines +which separates the concern of the ANSI shell syntax from the +logical behaviour that it respresents. + +```javascript +{ + $: 'pipeline', + nodes: [ + { + $: 'command', + id: 'ls', + positionals: [ + '/ed/Documents' + ] + }, + { + $: 'command', + id: 'tail', + params: { + n: 2 + } + } + ] +} +``` + +The `$` property identifies the type of a particular node. +The space of other properties including the `$` symbol is reserved +for meta information about nodes; for example properties like +`$origin` and `$whitespace` could turn this AST into a +CST. + +For the same of easier explanation here I'm going to coin the +term "Abstract Logic Tree" (ALT) and use it along with the +conventional terms as follows: + +| Abrv | Name | Represents | +| ---- | -------------------- | -------------------- | +| ALT | Abstract Logic Tree | What it does | +| AST | Abstract Syntax Tree | How it was described | +| CST | Concrete Syntax Tree | How it was formatted | + +The pipeline format described above is an AST for the +input that was understood by the ANSI shell adapter. +It could be converted to an ALT if the Puter Shell is +designed to understand pipelines a little differently. + +```javascript +{ + $: 'tail', + subject: { + $: 'list', + subject: { + $: 'filepath', + id: '/ed/Documents' + } + } +} +``` + +This is not final, but shows how the AST for pipeline +syntax can be developed in the ANSI shell adapter without +constraining how the Puter Shell itself works. + +### Syntaxes + +#### Why CST tokenization in a shell would be useful + +There are a lot of decisions to make at every single level +of syntax parsing. For example, consider the following: + +``` +ls | tail -n 2 > "some \"data\".txt" +``` + +Tokens can be interpreted at different levels of detail. +A standard shell tokenizer would likely eliminate information +about escape characters within quoted strings at this point. +For example, right now the Puter ANSI shell adapter takes +after what a standard shell does and goes for the second +option described here: + +``` +[ + 'ls', '|', 'tail', '-n', '2', '>', + // now do we do [","some ", "\\\"", ...], + // or do we do ["some \"data\".txt"] ? +] +``` + +This is great for processing and executing commands because +this information is no longer relevant at that stage. + +However, suppose you wanted to add support for syntax highlighting, +or tell a component responsible for a specific context of tab +completion where the cursor is with respect to the tokenized +information. This is no longer feasible. + +For the latter case, the ANSI shell adapter works around this +issue by only parsing the commandline input up to the cursor +location - meaning the last token will always represent the +input up to the cursor location. The input after is truncated +however, leading to the familiar inconvenient situation seen in +many terminals where tab completion does something illogical with +respect the text after your cursor. + +i.e. the following, with the cursor position represented by `X`: + +``` +echo "hello" > some_Xfile.txt +``` + +will be transformed into the following: + +``` +echo "hello" > some_file.txtXfile.txt +``` + +What would be more helpful: +- terminal bell, because `some_file.txt` is already complete +- `some_other_Xfile.txt` if `some_other_file.txt` exists + +So syntax highlighting and tab completion are two reasons why +the CST is useful. There may be other uses as well that I +haven't thought of. So this seems like a reasonable idea. + +#### Choosing monolithic or composite lexers + +Next step, there are also a lot of decisions to make +about processing the text into tokens. + +For example, we can take after the very feature that make +shells so versatile - pipelines - and apply this concept +to the lexer. + +``` +Level 1 lexer produces: + ls, |, tail, -n, 2, >, ", some , \", data, \", .txt + +Level 2 lexer produces: + ls, |, tail, -n, 2, >, "some \"data\".txt" + +``` + +This creates another decision fork, actually. It raises the +question of how to associate the token "some \"data\".txt" +with the tokens it was composed from at the previous level +or lexing, if this should be done at all, and otherwise if +CST information should be stored with the composite token. + +If lexers provide verbose meta information there might be +a concern about efficiency, however lexers could be +configurable in this respect. Furthermore, lexers could be +defined separately from their implementation and JIT-compiled +based on configuration so you actually get an executable bytecode +which doesn't produce metadata (for when it's not needed). + +While designing JIT-compilable lexer definitions is incredibly +out of scope for now, the knowledge that it's possible justifies +the decision to have lexers produce verbose metadata. + +If the "Level 1 lexer" in the example above stores CST information +in each token, the "Level 2 lexer" can simply let this information +propagate as it stores information about what tokens were composed +to produce a higher-level token. This means concern about +whitespace and formatting is limited to the lowest-level lexer which +makes the rest of the lexer stack much easier to maintain. + +#### An interesting philosophical point about lexers and parsers + +Consider a stack of lexers that builds up to high-level constructs +like "pipeline", "command", "condition", etc. The line between a +parser and a lexer becomes blurry, as this is in fact a bottom-up +parser composed of layers, each of which behaves like a lexer. + +I'm going to call the layers `PStrata` (singular: `PStratum`) +to avoid collision with these concepts. + +### The "Implicit Interface Aggregator" + +Vanilla javascript doesn't have interfaces, which sometimes seems +to make it difficult to have guarantees about type methods an +object will implement, what values they'll be able to handle, etc. + +To solve some of the drawbacks of not having interfaces, I'm going +to use a pattern which Chat GPT just named the +Implicit Interface Aggregator Pattern. + +The idea is simple. Instead of having an interface, you have a class +which acts as the user-facing API, and holds the real implementation +by aggregation. While this doesn't fix everything, it leaves the +doors open for multiple options in the future, such as using +typescript or a modelling framework, without locking either of these +doors too early. Since we're potentially developing on a lot of +low-level concepts, perhaps we'll even have our own technology that +we'd like to use to describe and validate the interfaces of the code +we write at some point in the future. + +This class can +handle concerns such as adapting different types of inputs and +outputs; things which an implementation doesn't need to be concerned +with. Generally this kind of separation of concerns would be done +using an abstract class, but this is an imperfect separation of +concerns because the implementor needs to be aware of the abstract +class. Granted, this isn't usually a big deal, but what if the +abstract class and implementors are compiled separately? It may be +advantageous that implementors don't need to have all the build +dependencies of the abstract class. + +The biggest drawback of this approach is that while the aggregating +class can implement runtime assertions, it doesn't solve the issue +of the lack of build-time assertions, which are able to prevent +type errors from getting to releases entirely. However, it does +leave room to add type definitions for this class and its +implementors (turning it into the facade pattern), or apply model +definitions (or schemas) to the aggregator and the output of a +static analysis to the implmentors (turning it into a model +definition). + +#### Where this will be used + +The first use of this pattern will be `PStratum`. +PStratum is a facade which aggregates a PStratumImplementor using +the pattern described above. + +The following layers will exist for the shell: +- StringPStratum will take a string and provide bytes. +- LowLexPStratum will take bytes and identify all syntax + tokens and whitespace. +- HiLexPStratum will create composite tokens for values + such as string literals +- LogicPStratum will take tokens as input and produce + AST nodes. For example, this is when successive instances + of the `|` (pipe) operator will be converted into + a pipeline construct. + + +### First results from the parser + +It appears that the methods I described above are very effective +for implementing a parser with support for concrete syntax trees. + +By wrapping implementations of `Parser` and `PStratum` in facades +it was possible to provide additional functionality for all +implementations in one place: +- `fork` and `join` is implemented by PStratum; each implementation + does not need to be aware of this feature. +- the `look` function (AKA "peek" behaviour) is implemented by + PStratum as well. +- A PStratum implementation can implement the behaviour to reach + for previous values, but PStratum has a default implementation. + The BytesPStratumImpl overrides this to provide Uint8Arrays instead + of arrays of Number values. +- If parser implementations don't return a value, Parser will + create the ParseResult that represents an unrecognized input. + +It was also possible to add a Parser factory which adds additional +functionality to the sub-parsers that it creates: +- track the tokens each parser gets from the delegate PStratum + and keep a record of what lower-level tokens were composed to + produce higher-level tokens +- track how many tokens each parser has read for CST metadata + +A layer called `MergeWhitespacePStratumImpl` completes this by +reading the source bytes for each token and using it to compute +a line and column number. After this, the overall parser is +capable of starting the start byte, end byte, line number, and +column number for each token, as well as preserve this information +for each composite token created at higher levels. + +The following parser configuration with a hard-coded input was +tested: + +```javascript +sp.add( + new StringPStratumImpl(` + ls | tail -n 2 > "test \\"file\\".txt" + `) +); +sp.add( + new FirstRecognizedPStratumImpl({ + parsers: [ + cstParserFac.create(WhitespaceParserImpl), + cstParserFac.create(LiteralParserImpl, { value: '|' }, { + assign: { $: 'pipe' } + }), + cstParserFac.create(UnquotedTokenParserImpl), + ] + }) +); +sp.add( + new MergeWhitespacePStratumImpl() +) +``` + +Note that the multiline string literal begins with whitespace. +It is therefore expected that each token will start on line 1, +and `ls` will start on column 8. + +The following is the output of the parser: + +```javascript +[ + { + '$': 'symbol', + text: 'ls', + '$cst': { start: 9, end: 11, line: 1, col: 8 }, + '$source': Uint8Array(2) [ 108, 115 ] + }, + { + '$': 'pipe', + text: '|', + '$cst': { start: 12, end: 13, line: 1, col: 11 }, + '$source': Uint8Array(1) [ 124 ] + }, + { + '$': 'symbol', + text: 'tail', + '$cst': { start: 14, end: 18, line: 1, col: 13 }, + '$source': Uint8Array(4) [ 116, 97, 105, 108 ] + }, + { + '$': 'symbol', + text: '-n', + '$cst': { start: 19, end: 21, line: 1, col: 18 }, + '$source': Uint8Array(2) [ 45, 110 ] + }, + { + '$': 'symbol', + text: '2', + '$cst': { start: 22, end: 23, line: 1, col: 21 }, + '$source': Uint8Array(1) [ 50 ] + } +] +``` + +No errors were observed in this output, so I can now continue +adding more layers to the parser to get higher-level +representations of redirects, pipelines, and other syntax +constructs that the shell needs to understand. + +## 2023-05-28 + +### Abstracting away communication layers + +As of now the ANSI shell layer and terminal emulator are separate +from each other. To recap, the ANSI shell layer and object-oriented +shell layer are also separate from each other, but the ANSI shell +layer current holds more functionality than is ideal; most commands +have been implemented at the ANSI shell layer in order to get more +functionality earlier in development. + +Although the ANSI shell layer and object-oriented shell layer are +separate, they are both coupled with the communication layer that's +currently used between them: cross-document messaging. This is ideal +for communication between the terminal emulator and ANSI shell, but +less ideal for that between that ANSI shell and OO shell. The terminal +emulator is a web app and will always be run in a browser environment, +which makes the dependency on cross-document messaging acceptable. +Furthermore it's a small body of code and it can easily be extended +upon to support multiple protocols of communication in the future +rather than just cross-document messaging. The ANSI shell on the other +hand, which currently communications with the OO shell using +cross-document messaging, will not always be run in a browser +environment. It is also completely dependent on the OO shell, so it +would make sense to bundle the OO shell with it in some environments. + +The dependency between the ANSI shell and OO shell is not bidirectional. +The OO shell layer is intended to be useful even without the ANSI shell +layer; for example a GUI for constructing and executing pipelines would +be more elegant built upon the OO shell than the ANSI shell, since there +wouldn't be a layer text processing between two layers of +object-oriented logic. When also considering that in Puter any +alternative layer on top of the OO shell is likely to be built to run +in a browser environment, it makes sense to allow the OO shell to be +communicated with via cross-document messaging. + +The following ASCII diagram describes the communication relationships +between various components described above: + +``` +note: "XD" means cross-document messaging + +[web terminal] + | + (XD) + | + |- (stdio) --- [local terminal] + | +[ANSI Shell] + | + (direct calls / XD) + | + |-- (XD) --- [web power tool] + | + [OO Shell] + +``` + +It should be interpreted as follows: +- OO shell can communicate with a web power tool via + cross-document messaging +- the OO shell and ANSI shell should communicate via + either direct calls (when bundled) or cross-document + messaging (when not bundled together) +- the ANSI shell can be used under a web terminal via + cross-document messaging, or a local terminal via + the standard I/O mechanism of the host operating system. + +## 2023-05-29 + +### Interfacing with structured data + +Right now all the coreutils commands currently implemented output +byte streams. However, allowing commands to output objects instead +solves some problems with traditional shells: +- text processing everywhere + - it's needed to get a desired value from structured data + - commands are often concerned with the formatting of data + rather than the significance of the data + - commands like `awk` are archaic and difficult to use, + but are often necessary +- information which a command had to obtain is often lost + - a good example of this is how `ls` colourizes different + inode types but this information goes away when you pipe + it to a command like `tail` + +#### printing structured data + +Users used to a POSIX system will have some expectations +about the output of commands. Sometimes the way an item +is formatted depends on some input arguments, but does not +change the significance of the item itself. + +A good example of this is the `ls` command. It prints the +names of files. The object equivalent of this would be for +it to output CloudItem objects. Where it gets tricky is +`ls` with no arguments will display just the name, while +`ls -l` will display details about each file such as the +mode, owner, group, size, and date modified. + +##### per-command outputters + +If the definition for the `ls` command included an output +formatter this could work - if ls' standard output is +attached to the PTT instead of another command it would +format the output according to the flags. + +This still isn't ideal though. If `ls` is piped to `tail` +this information would be lost. This differs from the +expected behaviour from posix systems; for example: + +``` +ls -l | tail -n 2 > last_two_lines.txt +``` + +this command would output all the details about the last +two files to the text file, rather than just the names. + +##### composite output objects with formatter + data + +A command outputting objects could also attach a formatter +to each object. This has the advantage that an object can +move through a pipeline and then be formatted at the end, +but it does have a drawback that sometimes the formatter +will be the same for every object, and sending a copy +of the formatter with each object would be redundant. + +##### using a formatter registry + +A transient registry of object formatters, existing for +the lifespan of the pipeline, could contain each unique +formatter that any command in the pipeline produced for +one or more of it's output objects. Each object that it +outputs now just needs to refer to an existing formatter +which solves the problem of redundant information passing +through the pipeline + + +##### keeping it simple + +This idea of a transient registry for unique implementations +of some interface could be useful in a general sense. So, I +think it makes sense to actually implement formatters using +the more redundant behaviour first (formatter is coupled with +each object), and then later create an abstraction for +obtaining the correct formatter for an object so that this +optimization can be implemented separately from this specific +use of the optimization. + +## 2024-02-01 + +### StrataParse and Tokens with Command Substitution + +**note:** this devlog entry was written in pieces as I made +significant changes to the parser, so information near the +beginning is less accurate than information towards the end. + +In the "first half" portion of the terminal parser, which +builds a "lexer"* (*not a pure lexer) for parsing, there +currently exists an implementation of parsing for quoted strings. +I have in the past implemented a quoted string parser at least +two different ways - a state machine parser, and with composable +parsers. The string parser in `buildParserFirstHalf` uses the +second approach. This is what it looks like represented as a +lisp-ish pseudo-code: + +```javascript +sequence( + literal('"') + repeat( + choice( + characters_until('\\' or '"') + sequence( + literal('\\') + choice( + literal('"'), + ...escape_substitutions)))) + literal('"')) +``` + +In a BNF grammar, this might be assigned to a symbol name +like "quoted-string". In `strataparse` this is represented +by having a layer which recognizes the components of a string +(like each sequence of characters between escapes, each escape, +and the closing quotation mark), and then a higher-level layer +which composes those to create a single node representing +the string. + +I really like this approach because the result is a highly +configurable parser that will let you control how much +information is kept as you advance to higher-level layers +(ex: CST instead of AST for tab-completion checks), +and only parse to a certain level if desired +(ex: only "first half" of the parser is used for +tab-completion checks). + +The trouble is the POSIX Shell Command Language allows part of a +token to be a command substitution, which means a stack needs to +be maintianed to track nested states. Implementing this in the +current hand-written parser was very tricky. + +Partway through working on this I took a look at existing +shell syntax parsers for javascript. The results weren't very +promising. None of the available parsers could produce a CST, +which is needed for tab completion and will aid in things +like syntax highlighting in the future. + +Between the modules `shell-parse` and `bash-parser`, the first +was able to parse this syntax while the second threw an error: +``` +echo $TEST"something to $($(echo echo) do)"with-this another-token +``` + +Another issue with existing parsers, which makes me wary of even +using pegjs (what `shell-parse` uses) directly is that the AST +they produce requires a lot of branching in the interpreter. +For example it's not known when parsing a token whether you'll +get a `literal`, or a `concatenation` with an array of "pieces" +which might contain literals. This is a perfectly valid +representation of the syntax considering what I mentioned above +about command substitution, but if there can be an array of +pieces I would rather always have an array of pieces. I'm much +more concerned with the simplicity and performance of the +interpreter than the amount of memory the AST consumes. + +Finally, my "favourite" part: when you run a script in `bash` +it doesn't parse the entire script and then run it; it either +parses just one line or, if the line is a compound command +(a structure like `if; then ...; done`) it parses multiple +lines until it has parsed a valid compound command. This means +any parser that can only parse complete inputs with valid syntax +would need to repeatedly parse (1 line, 2 lines, 3 lines...) +at each line until one of the parses is successful, if we wish +to mimic the behaviour of a real POSIX shell. + +In conclusion, I'm keeping the hand-written parser and +solving command substitution by maintaining state via stacks +in both halves of the parser, and we will absolutely need to +do static analysis and refactoring to simplify the parser some +time in the future. + +## 2024-02-04 + +### Platform Support and Deprecation of separate `puter-shell` repo + +To prepare for releasing the Puter Shell under an open-source license, +it makes sense to move everything that's currently in `puter-shell` into +this repo. The separation of concerns makes sense, but it belongs in +a place called "platform support" inside this repo rather than in +another repo (that was an oversight on my part earlier on). + +This change can be made incrementally as follows: +- Expose an object which implements support for the current platform + to all the commands in coreutils. +- Incrementally update commands as follows: + - add the necessary function(s) to `puter` platform support + - while doing this, use the instance of the Puter SDK owned + by `dev-ansi-terminal` instead of delegating to the + wrapper in the `puter-shell` repo via `postMessage` + - update the command to use the new implementation +- Once all commands are updated, the XDocumentPuterShell class will + be dormant and can safely be removed. diff --git a/packages/phoenix/doc/graveyard/keyboard_modifiers.md b/packages/phoenix/doc/graveyard/keyboard_modifiers.md new file mode 100644 index 00000000..c52e3902 --- /dev/null +++ b/packages/phoenix/doc/graveyard/keyboard_modifiers.md @@ -0,0 +1,77 @@ +## keyboard modifier translation + +Encoding of modifier keys in `xterm` is done following this +table: + encoded | keys pressed + --------|--------------------------- + 2 | Shift + 3 | Alt + 4 | Shift + Alt + 5 | Control + 6 | Shift + Control + 7 | Alt + Control + 8 | Shift + Alt + Control + 9 | Meta + 10 | Meta + Shift + 11 | Meta + Alt + 12 | Meta + Alt + Shift + 13 | Meta + Ctrl + 14 | Meta + Ctrl + Shift + 15 | Meta + Ctrl + Alt + 16 | Meta + Ctrl + Alt + Shift + +This script was used to convert between more useful bit flags +and the xterm encodings of the modifiers: + +```javascript +const modifier_keys = ['shift', 'ctrl', 'alt', 'meta']; +const MODIFIER = {}; +for ( let i=0 ; i < modifier_keys.length ; i++ ) { + MODIFIER[modifier_keys[i].toUpperCase()] = 1 << i; +} + +const pc_modifier_list = [ + MODIFIER.SHIFT, + MODIFIER.ALT, + MODIFIER.CTRL, + MODIFIER.META +]; + +const PC_STYLE_MODIFIER_MAP = {}; + +(() => { + let i = 2; + for ( const mod of pc_modifier_list ) { + const new_entries = { [i++]: mod }; + for ( const key in PC_STYLE_MODIFIER_MAP ) { + new_entries[i++] = mod | PC_STYLE_MODIFIER_MAP[key]; + } + for ( const key in new_entries ) { + PC_STYLE_MODIFIER_MAP[key] = new_entries[key]; + } + } +})(); + +for ( const k in PC_STYLE_MODIFIER_MAP ) { + console.log(`${k} :: ${print(PC_STYLE_MODIFIER_MAP[k])}`); +} +``` + +However, it was eventually determined that the PC-style function +keys, although this is not documented, really do represent bit +flags if you simply subtract 1. + +For example, this situation doesn't look like it can be explained +using bit flags: +- **shift** is `2` +- **ctrl** is `5`, and has two `1` bits +- **shift** + **ctrl** is `6` +- flags don't explain this: `2 | 5 = 7` + +But after subtracting `1` from each value: +- **shift** is `1` +- **ctrl** is `4` +- **shift** + **ctrl** is `5` +- flags work correctly: `1 | 4 = 5` + +This is true for all examples. diff --git a/packages/phoenix/doc/graveyard/readline.md b/packages/phoenix/doc/graveyard/readline.md new file mode 100644 index 00000000..9b463d70 --- /dev/null +++ b/packages/phoenix/doc/graveyard/readline.md @@ -0,0 +1,59 @@ +## method `readline` from `BetterReader` + +This method was meant to be a low-level line reader that simply +terminates at the first line feed character and returns the +input. + +This might be useful for non-visible inputs like passwords, but +for visible inputs it is not practical unless the output stream +provided is decorated with something that can filter undesired +input characters that would move the terminal cursor. + +It's especially not useful for a prompt with history, since the +up arrow should clear the input buffer and replace it with something +else. + +Where this may shine is in a benchmark. The approach here doesn't +explicitly iterate over every byte, so assuming methods like +`.indexOf` and `.subarray` on TypedArray values are efficient this +would be faster than the implementation which is now used. + +```javascript + async readLine (options) { + options = options ?? {}; + + let stringSoFar = ''; + + let lineFeedFound = false; + while ( ! lineFeedFound ) { + let chunk = await this.getChunk_(); + + const iLF = chunk.indexOf(CHAR_LF); + + // do we have a line feed character? + if ( iLF >= 0 ) { + lineFeedFound = true; + + // defer the rest of the chunk until next read + if ( iLF !== chunk.length - 1 ) { + this.chunks_.push(chunk.subarray(iLF + 1)) + } + + // (note): LF is not included in return value or next read + chunk = chunk.subarray(0, iLF); + } + + if ( options.stream ) { + options.stream.write(chunk); + if ( lineFeedFound ) { + options.stream.write(new Uint8Array([CHAR_LF])); + } + } + + const text = new TextDecoder().decode(chunk); + stringSoFar += text; + } + + return stringSoFar; + } +``` \ No newline at end of file diff --git a/packages/phoenix/doc/license_header.txt b/packages/phoenix/doc/license_header.txt new file mode 100644 index 00000000..ec7fa584 --- /dev/null +++ b/packages/phoenix/doc/license_header.txt @@ -0,0 +1,16 @@ +Copyright (C) 2024 Puter Technologies Inc. + +This file is part of Phoenix Shell. + +Phoenix Shell is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/packages/phoenix/doc/missing-posix.md b/packages/phoenix/doc/missing-posix.md new file mode 100644 index 00000000..18e694c4 --- /dev/null +++ b/packages/phoenix/doc/missing-posix.md @@ -0,0 +1,23 @@ +# Missing POSIX Functionality + +### References + +- [POSIX.1-2017 Chapter 2: Shell Command Language](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html) + +### Shell Command Language features known to be missing from `phoenix` + +- Parameter expansion + > This is support for `$variables`, and this is **highest priority**. +- Compound commands + > This is `if`, `case`, `while`, `for`, etc +- Arithmetic expansion +- Alias substitution + +### How to Contribute + +- Check the [README.md file](../README.md) for contributor guidelines. +- Additional features will require updates to + [the parser](phoenix/src/ansi-shell/parsing). + Right now there are repeated concerns between + `buildParserFirstHalf` and `buildParserSecondHalf` which need to + be factored out. diff --git a/packages/phoenix/doc/parser.md b/packages/phoenix/doc/parser.md new file mode 100644 index 00000000..eb3ca2ee --- /dev/null +++ b/packages/phoenix/doc/parser.md @@ -0,0 +1,55 @@ +# Puter Terminal Parser + +## The `strataparse` package + +The `strataparse` package makes it possible to build parser in distinct +layers that we call "strata" (each one called a "stratum"). Rather then +distinguish between a "lexer" and "parser", we can instead have an +arbitrary number of layers that use different approaches to processing +or parsing. + +Each stratum implements the method `next (api)`. The `api` object is +provided by strataparse as the bridge between which the strata +interact. Typically, it's used to call `api.delegate` to get a reference +to the lower-level parser. Terminal strata like `StringPStratumImpl`, don't +do this. The `next` method returns the next value in an object of the +form `{ done: true/false, value: ... }`, matching the typical interface +for iterators within this source code. When `done` is true, `value` +can be a message (such as an error) indicating why parsing halted. + +## PuterShellParser + +At the time of writing this, the PuterShellParser class builds a parser +with 4 strata, listed here from bottom up: + +### buildParserFirstHalf (the "lexer half") + +[source code](../src/ansi-shell/parsing/buildParserFirstHalf.js) + +- A "FirstRecognized" strata which behaves like a lexer. It converts + characters like `|` to AST nodes like `{ $: 'op.pipe' }`. + AST nodes use the key `$` to identify the type and can have other + arbitrary values. +- A "MergeWhitespace" strata which is provided by `strataparse`. + It converts whitespace to a `{ $: 'whitespace' }` AST node, and + adds a property called `$cst` to all nodes from the delegate + (the "lexer") as well as these whitespace nodes. This effectively + transforms the AST nodes from before into CST nodes, providing + information about whitespace, line numbers, and column numbers + in a way subsequent layers can digest. + (note that these will still be referred to as "AST nodes throughout + this documentation). + +[source code](../src/ansi-shell/parsing/buildParserSecondHalf.js) + +### buildParserSecondHalf (the "parser half") +- "ReducePrimitives" creates higher-level AST nodes from some of the + AST nodes provided by the "previous"(lower/"lexer half") step. + At the time of writing it's specifically just to deal with strings, + reducing multiple `{ $: 'string.segment' }` and `{ $: 'string.escape }` + nodes into a `{ $: 'string' }` node. +- "ShellConstructs" creates higher-level nodes to model the behaviour + of the shell. For example, a sequence of tokens including + `{ $: 'op.pipe' }` nodes will be composed into a new `{ $: 'pipeline' }` + node. The pipeline node contains an array called `components` which + contains the tokens in between pipe operators. diff --git a/packages/phoenix/doc/readme-gif.gif b/packages/phoenix/doc/readme-gif.gif new file mode 100644 index 0000000000000000000000000000000000000000..d70c4eeb5ff3308475612abc6eab35ed24ec6373 GIT binary patch literal 260359 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

if`6=z$KR;MEqXbt=9HqS{7HafiZ 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

BH!Bi{M0?8S5K}ZWw+f)07m|T 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%0D6%Nee<56EC;Zd@6@gN`+9yDz5HQJ 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

FSKgTrv}9I*Zu1u{xwGM#9!O;Izhcr`~u#bD&u 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

*h1x>|d1VgvVSrm60I7)#5< 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. + */ +// Here for safe-keeping - wasn't correct for shell tokens but +// it will be needed later for variable assignments + +export class SymbolParserImpl { + static meta = { + inputs: 'bytes', + outputs: 'node' + } + static data = { + rexp0: /[A-Za-z_]/, + rexpN: /[A-Za-z0-9_]/, + } + parse (lexer) { + let { done, value } = lexer.look(); + if ( done ) return; + + const { rexp0, rexpN } = this.constructor.data; + + value = String.fromCharCode(value); + if ( ! rexp0.test(value) ) return; + + let text = '' + value; + lexer.next(); + + for ( ;; ) { + ({ done, value } = lexer.look()); + if ( done ) break; + value = String.fromCharCode(value); + if ( ! rexpN.test(value) ) break; + text += value; + lexer.next(); + } + + return { $: 'symbol', text }; + } +} diff --git a/packages/phoenix/notalicense-license-checker-config.json b/packages/phoenix/notalicense-license-checker-config.json new file mode 100644 index 00000000..b43365a4 --- /dev/null +++ b/packages/phoenix/notalicense-license-checker-config.json @@ -0,0 +1,26 @@ +{ + "ignore": ["**/!(*.js|*.css)"], + "license": "doc/license_header.txt", + "licenseFormats": { + "js": { + "prepend": "/*", + "append": " */", + "eachLine": { + "prepend": " * " + } + }, + "dotfile|^Dockerfile": { + "eachLine": { + "prepend": "# " + } + }, + "css": { + "prepend": "/*", + "append": " */", + "eachLine": { + "prepend": " * " + } + } + }, + "trailingWhitespace": "TRIM" +} \ No newline at end of file diff --git a/packages/phoenix/package-lock.json b/packages/phoenix/package-lock.json new file mode 100644 index 00000000..1f1d6840 --- /dev/null +++ b/packages/phoenix/package-lock.json @@ -0,0 +1,1888 @@ +{ + "name": "dev-ansi-terminal", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dev-ansi-terminal", + "version": "0.0.0", + "license": "AGPL-3.0-only", + "workspaces": [ + "packages/pty", + "packages/strataparse", + "packages/contextlink" + ], + "dependencies": { + "@pkgjs/parseargs": "^0.11.0", + "capture-console": "^1.0.2", + "chronokinesis": "^6.0.0", + "cli-columns": "^4.0.0", + "columnify": "^1.6.0", + "fs-mode-to-string": "^0.0.2", + "json-query": "^2.2.2", + "node-pty": "^1.0.0", + "path-browserify": "^1.0.1", + "sinon": "^17.0.1", + "xterm": "^5.1.0", + "xterm-addon-fit": "^0.7.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-replace": "^5.0.2", + "mocha": "^10.2.0", + "rollup": "^3.21.4", + "rollup-plugin-copy": "^3.4.0" + } + }, + "../dev-contextlink": { + "name": "@heyputer/contextlink", + "version": "0.0.0", + "extraneous": true, + "license": "UNLICENSED", + "devDependencies": { + "mocha": "^10.2.0" + } + }, + "../dev-hitide": { + "name": "hitide", + "version": "0.0.0", + "extraneous": true, + "license": "UNLICENSED", + "devDependencies": { + "mocha": "^10.2.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", + "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.2.tgz", + "integrity": "sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz", + "integrity": "sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.2.tgz", + "integrity": "sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", + "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argle": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/argle/-/argle-1.1.2.tgz", + "integrity": "sha512-2sQZC5HeeSH9cQEwnZZhmHiKfvJkQ6ncpf8zl9Hv629aiMUsOw8jzYqOhpaMleQGzpQ7avCwrwyqSW1f4t7v0Q==", + "dependencies": { + "lodash.isfunction": "^3.0.8", + "lodash.isnumber": "^3.0.3" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/capture-console": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/capture-console/-/capture-console-1.0.2.tgz", + "integrity": "sha512-vQNTSFr0cmHAYXXG3KG7ZJQn0XxC3K2di/wUZVb6yII6gqSN/10Egd3vV4XqJ00yCRNHy2wkN4uWHE+rJstDrw==", + "dependencies": { + "argle": "~1.1.1", + "lodash.isfunction": "~3.0.8", + "randomstring": "^1.3.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chronokinesis": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/chronokinesis/-/chronokinesis-6.0.0.tgz", + "integrity": "sha512-NxGxNuzROLws2VVvSj9r1qrq0JK0AwR44FNk+sGfPZlG5EW3viz6z2elg6ZwE2YFCn6+Qg3sPqkfIYLyZ0wAtQ==" + }, + "node_modules/cli-columns": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-columns/-/cli-columns-4.0.0.tgz", + "integrity": "sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ==", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/columnify": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz", + "integrity": "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/contextlink": { + "resolved": "packages/contextlink", + "link": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dev-pty": { + "resolved": "packages/pty", + "link": true + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-mode-to-string": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/fs-mode-to-string/-/fs-mode-to-string-0.0.2.tgz", + "integrity": "sha512-8Pik0/TZnN1uuEO5TdmDoXkjTNA98BUD1uM3RWepPXDLAO9tbmiluyu+fVwWX7C4sKKxDX+64rWNwtNwDJA3Yg==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz", + "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", + "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-query": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json-query/-/json-query-2.2.2.tgz", + "integrity": "sha512-y+IcVZSdqNmS4fO8t1uZF6RMMs0xh3SrTjJr9bp1X3+v0Q13+7Cyv12dSmKwDswp/H427BVtpkLWhGxYu3ZWRA==", + "engines": { + "node": "*" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==" + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.17.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomstring": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.3.0.tgz", + "integrity": "sha512-gY7aQ4i1BgwZ8I1Op4YseITAyiDiajeZOPQUbIq9TPGPhUm5FX59izIaOpmKbME1nmnEiABf28d9K2VSii6BBg==", + "dependencies": { + "randombytes": "2.0.3" + }, + "bin": { + "randomstring": "bin/randomstring" + }, + "engines": { + "node": "*" + } + }, + "node_modules/randomstring/node_modules/randombytes": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.3.tgz", + "integrity": "sha512-lDVjxQQFoCG1jcrP06LNo2lbWp4QTShEXnhActFBwYuHprllQV6VUpwreApsYqCgD+N1mHoqJ/BI/4eV4R2GYg==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.4.tgz", + "integrity": "sha512-N5LxpvDolOm9ueiCp4NfB80omMDqb45ShtsQw2+OT3f11uJ197dv703NZvznYHP6RWR85wfxanXurXKG3ux2GQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-copy": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz", + "integrity": "sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==", + "dev": true, + "dependencies": { + "@types/fs-extra": "^8.0.1", + "colorette": "^1.1.0", + "fs-extra": "^8.1.0", + "globby": "10.0.1", + "is-plain-object": "^3.0.0" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strataparse": { + "resolved": "packages/strataparse", + "link": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xterm": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.1.0.tgz", + "integrity": "sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ==" + }, + "node_modules/xterm-addon-fit": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz", + "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/contextlink": { + "version": "0.0.0", + "license": "AGPL-3.0-only", + "devDependencies": { + "mocha": "^10.2.0" + } + }, + "packages/pty": { + "name": "dev-pty", + "version": "0.0.0", + "license": "AGPL-3.0-only" + }, + "packages/strataparse": { + "version": "0.0.0", + "license": "AGPL-3.0-only" + } + } +} diff --git a/packages/phoenix/package.json b/packages/phoenix/package.json new file mode 100644 index 00000000..de2ea901 --- /dev/null +++ b/packages/phoenix/package.json @@ -0,0 +1,39 @@ +{ + "name": "dev-ansi-terminal", + "version": "0.0.0", + "description": "ANSI Terminal for Puter", + "main": "exports.js", + "scripts": { + "test": "mocha ./test" + }, + "author": "Puter Technologies Inc.", + "license": "AGPL-3.0-only", + "type": "module", + "devDependencies": { + "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-replace": "^5.0.2", + "mocha": "^10.2.0", + "rollup": "^3.21.4", + "rollup-plugin-copy": "^3.4.0" + }, + "dependencies": { + "@pkgjs/parseargs": "^0.11.0", + "capture-console": "^1.0.2", + "chronokinesis": "^6.0.0", + "cli-columns": "^4.0.0", + "columnify": "^1.6.0", + "fs-mode-to-string": "^0.0.2", + "json-query": "^2.2.2", + "node-pty": "^1.0.0", + "path-browserify": "^1.0.1", + "sinon": "^17.0.1", + "xterm": "^5.1.0", + "xterm-addon-fit": "^0.7.0" + }, + "workspaces": [ + "packages/pty", + "packages/strataparse", + "packages/contextlink" + ] +} diff --git a/packages/phoenix/packages/contextlink/.gitignore b/packages/phoenix/packages/contextlink/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/packages/phoenix/packages/contextlink/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/packages/phoenix/packages/contextlink/context.js b/packages/phoenix/packages/contextlink/context.js new file mode 100644 index 00000000..6cbf3149 --- /dev/null +++ b/packages/phoenix/packages/contextlink/context.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class Context { + constructor (values) { + for ( const k in values ) this[k] = values[k]; + } + sub (newValues) { + if ( newValues === undefined ) newValues = {}; + const sub = Object.create(this); + + const alreadyApplied = {}; + for ( const k in sub ) { + if ( sub[k] instanceof Context ) { + const newValuesForK = + newValues.hasOwnProperty(k) + ? newValues[k] : undefined; + sub[k] = sub[k].sub(newValuesForK); + alreadyApplied[k] = true; + } + } + + for ( const k in newValues ) { + if ( alreadyApplied[k] ) continue; + sub[k] = newValues[k]; + } + + return sub; + } +} diff --git a/packages/phoenix/packages/contextlink/entry.js b/packages/phoenix/packages/contextlink/entry.js new file mode 100644 index 00000000..1103462a --- /dev/null +++ b/packages/phoenix/packages/contextlink/entry.js @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export { Context } from "./context.js"; diff --git a/packages/phoenix/packages/contextlink/package-lock.json b/packages/phoenix/packages/contextlink/package-lock.json new file mode 100644 index 00000000..a6aca0eb --- /dev/null +++ b/packages/phoenix/packages/contextlink/package-lock.json @@ -0,0 +1,916 @@ +{ + "name": "dev-contextlink", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dev-contextlink", + "version": "0.0.0", + "license": "UNLICENSED", + "devDependencies": { + "mocha": "^10.2.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/phoenix/packages/contextlink/package.json b/packages/phoenix/packages/contextlink/package.json new file mode 100644 index 00000000..6d27795d --- /dev/null +++ b/packages/phoenix/packages/contextlink/package.json @@ -0,0 +1,18 @@ +{ + "name": "contextlink", + "version": "0.0.0", + "main": "entry.js", + "type": "module", + "scripts": { + "test": "npx mocha" + }, + "author": "Puter Technologies Inc.", + "license": "AGPL-3.0-only", + "devDependencies": { + "mocha": "^10.2.0" + }, + "directories": { + "test": "test" + }, + "description": "" +} diff --git a/packages/phoenix/packages/contextlink/test/testcontext.js b/packages/phoenix/packages/contextlink/test/testcontext.js new file mode 100644 index 00000000..040b4fd4 --- /dev/null +++ b/packages/phoenix/packages/contextlink/test/testcontext.js @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { Context } from "../context.js"; + +describe('context', () => { + it ('works', () => { + const ctx = new Context({ a: 1 }); + const subCtx = ctx.sub({ b: 2 }); + + assert.equal(ctx.a, 1); + assert.equal(ctx.b, undefined); + assert.equal(subCtx.a, 1); + assert.equal(subCtx.b, 2); + }), + it ('doesn\'t mangle inner-contexts', () => { + const ctx = new Context({ + plainObject: { a: 1, b: 2, c: 3 }, + contextObject: new Context({ i: 4, j: 5, k: 6 }), + }); + const subCtx = ctx.sub({ + plainObject: { a: 101 }, + contextObject: { i: 104 }, + }); + assert.equal(subCtx.plainObject.a, 101); + assert.equal(subCtx.plainObject.b, undefined); + + assert.equal(subCtx.contextObject.i, 104); + assert.equal(subCtx.contextObject.j, 5); + + }) +}); diff --git a/packages/phoenix/packages/newparser/exports.js b/packages/phoenix/packages/newparser/exports.js new file mode 100644 index 00000000..dfab2185 --- /dev/null +++ b/packages/phoenix/packages/newparser/exports.js @@ -0,0 +1,101 @@ +import { adapt_parser, INVALID, Parser, UNRECOGNIZED, VALUE } from './lib.js'; +import { Discard, FirstMatch, None, Optional, Repeat, Sequence } from './parsers/combinators.js'; +import { Literal, StringOf } from './parsers/terminals.js'; + +class Symbol extends Parser { + _create(symbolName) { + this.symbolName = symbolName; + } + + _parse (stream) { + const parser = this.symbol_registry[this.symbolName]; + if ( ! parser ) { + throw new Error(`No symbol defined named '${this.symbolName}'`); + } + const subStream = stream.fork(); + const result = parser.parse(subStream); + console.log(`Result of parsing symbol('${this.symbolName}'):`, result); + if ( result.status === UNRECOGNIZED ) { + return UNRECOGNIZED; + } + if ( result.status === INVALID ) { + return { status: INVALID, value: result }; + } + stream.join(subStream); + result.$ = this.symbolName; + return result; + } +} + +class ParserWithAction { + #parser; + #action; + + constructor(parser, action) { + this.#parser = adapt_parser(parser); + this.#action = action; + } + + parse (stream) { + const parsed = this.#parser.parse(stream); + if (parsed.status === VALUE) { + parsed.value = this.#action(parsed.value); + } + return parsed; + } +} + +export class GrammarContext { + constructor (parsers) { + // Object of { parser_name: Parser, ... } + this.parsers = parsers; + } + + sub (more_parsers) { + return new GrammarContext({...this.parsers, ...more_parsers}); + } + + define_parser (grammar, actions) { + const symbol_registry = {}; + const api = {}; + + for (const [name, parserCls] of Object.entries(this.parsers)) { + api[name] = (...params) => { + const result = new parserCls(); + result._create(...params); + result.set_symbol_registry(symbol_registry); + return result; + }; + } + + for (const [name, builder] of Object.entries(grammar)) { + if (actions[name]) { + symbol_registry[name] = new ParserWithAction(builder(api), actions[name]); + } else { + symbol_registry[name] = builder(api); + } + } + + return (stream, entry_symbol) => { + const entry_parser = symbol_registry[entry_symbol]; + if (!entry_parser) { + throw new Error(`Entry symbol '${entry_symbol}' not found in grammar.`); + } + return entry_parser.parse(stream); + }; + } +} + +export const standard_parsers = () => { + return { + discard: Discard, + firstMatch: FirstMatch, + literal: Literal, + none: None, + optional: Optional, + repeat: Repeat, + sequence: Sequence, + stringOf: StringOf, + symbol: Symbol, + } +} diff --git a/packages/phoenix/packages/newparser/lib.js b/packages/phoenix/packages/newparser/lib.js new file mode 100644 index 00000000..89f185e0 --- /dev/null +++ b/packages/phoenix/packages/newparser/lib.js @@ -0,0 +1,29 @@ +export const adapt_parser = v => v; + +export const UNRECOGNIZED = Symbol('unrecognized'); +export const INVALID = Symbol('invalid'); +export const VALUE = Symbol('value'); + +export class Parser { + result (o) { + if (o.value && o.value.$discard) { + delete o.value; + } + return o; + } + + parse (stream) { + let result = this._parse(stream); + if ( typeof result !== 'object' ) { + result = { status: result }; + } + return this.result(result); + } + + set_symbol_registry (symbol_registry) { + this.symbol_registry = symbol_registry; + } + + _create () { throw new Error(`${this.constructor.name}._create() not implemented`); } + _parse (stream) { throw new Error(`${this.constructor.name}._parse() not implemented`); } +} diff --git a/packages/phoenix/packages/newparser/parsers/combinators.js b/packages/phoenix/packages/newparser/parsers/combinators.js new file mode 100644 index 00000000..54b23c05 --- /dev/null +++ b/packages/phoenix/packages/newparser/parsers/combinators.js @@ -0,0 +1,139 @@ +import { INVALID, UNRECOGNIZED, VALUE, adapt_parser, Parser } from '../lib.js'; + +export class Discard extends Parser { + _create (parser) { + this.parser = adapt_parser(parser); + } + + _parse (stream) { + const subStream = stream.fork(); + const result = this.parser.parse(subStream); + if ( result.status === UNRECOGNIZED ) { + return UNRECOGNIZED; + } + if ( result.status === INVALID ) { + return result; + } + stream.join(subStream); + return { status: VALUE, $: 'none', $discard: true, value: result }; + } +} + +export class FirstMatch extends Parser { + _create (...parsers) { + this.parsers = parsers.map(adapt_parser); + } + + _parse (stream) { + for ( const parser of this.parsers ) { + const subStream = stream.fork(); + const result = parser.parse(subStream); + if ( result.status === UNRECOGNIZED ) { + continue; + } + if ( result.status === INVALID ) { + return result; + } + stream.join(subStream); + return result; + } + + return UNRECOGNIZED; + } +} + +export class None extends Parser { + _create () {} + + _parse (stream) { + return { status: VALUE, $: 'none', $discard: true }; + } +} + +export class Optional extends Parser { + _create (parser) { + this.parser = adapt_parser(parser); + } + + _parse (stream) { + const subStream = stream.fork(); + const result = this.parser.parse(subStream); + if ( result.status === VALUE ) { + stream.join(subStream); + return result; + } + return { status: VALUE, $: 'none', $discard: true }; + } +} + +export class Repeat extends Parser { + _create (value_parser, separator_parser, { trailing = false } = {}) { + this.value_parser = adapt_parser(value_parser); + this.separator_parser = adapt_parser(separator_parser); + this.trailing = trailing; + } + + _parse (stream) { + const results = []; + for ( ;; ) { + const subStream = stream.fork(); + + // Value + const result = this.value_parser.parse(subStream); + if ( result.status === UNRECOGNIZED ) { + break; + } + if ( result.status === INVALID ) { + return { status: INVALID, value: result }; + } + stream.join(subStream); + if ( ! result.$discard ) results.push(result); + + // Separator + if ( ! this.separator_parser ) { + continue; + } + const separatorResult = this.separator_parser.parse(subStream); + if ( separatorResult.status === UNRECOGNIZED ) { + break; + } + if ( separatorResult.status === INVALID ) { + return { status: INVALID, value: separatorResult }; + } + stream.join(subStream); + if ( ! result.$discard ) results.push(separatorResult); + + // TODO: Detect trailing separator and reject it if trailing==false + } + + if ( results.length === 0 ) { + return UNRECOGNIZED; + } + + return { status: VALUE, value: results }; + } +} + +export class Sequence extends Parser { + _create (...parsers) { + this.parsers = parsers.map(adapt_parser); + } + + _parse (stream) { + const results = []; + for ( const parser of this.parsers ) { + const subStream = stream.fork(); + const result = parser.parse(subStream); + if ( result.status === UNRECOGNIZED ) { + return UNRECOGNIZED; + } + if ( result.status === INVALID ) { + return { status: INVALID, value: result }; + } + stream.join(subStream); + if ( ! result.$discard ) results.push(result); + } + + return { status: VALUE, value: results }; + } +} diff --git a/packages/phoenix/packages/newparser/parsers/terminals.js b/packages/phoenix/packages/newparser/parsers/terminals.js new file mode 100644 index 00000000..4a82cd14 --- /dev/null +++ b/packages/phoenix/packages/newparser/parsers/terminals.js @@ -0,0 +1,46 @@ +import { Parser, UNRECOGNIZED, VALUE } from '../lib.js'; + +export class Literal extends Parser { + _create (value) { + this.value = value; + } + + _parse (stream) { + const subStream = stream.fork(); + for ( let i=0 ; i < this.value.length ; i++ ) { + let { done, value } = subStream.next(); + if ( done ) return UNRECOGNIZED; + if ( this.value[i] !== value ) return UNRECOGNIZED; + } + + stream.join(subStream); + return { status: VALUE, $: 'literal', value: this.value }; + } +} + +export class StringOf extends Parser { + _create (values) { + this.values = values; + } + + _parse (stream) { + const subStream = stream.fork(); + let text = ''; + + while (true) { + let { done, value } = subStream.look(); + if ( done ) break; + if ( ! this.values.includes(value) ) break; + + subStream.next(); + text += value; + } + + if (text.length === 0) { + return UNRECOGNIZED; + } + + stream.join(subStream); + return { status: VALUE, $: 'stringOf', value: text }; + } +} \ No newline at end of file diff --git a/packages/phoenix/packages/pty/exports.js b/packages/phoenix/packages/pty/exports.js new file mode 100644 index 00000000..bbab56ef --- /dev/null +++ b/packages/phoenix/packages/pty/exports.js @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const encoder = new TextEncoder(); + +const CHAR_LF = '\n'.charCodeAt(0); +const CHAR_CR = '\r'.charCodeAt(0); + +export class BetterReader { + constructor ({ delegate }) { + this.delegate = delegate; + this.chunks_ = []; + } + + async read (opt_buffer) { + if ( ! opt_buffer && this.chunks_.length === 0 ) { + return await this.delegate.read(); + } + + const chunk = await this.getChunk_(); + + if ( ! opt_buffer ) { + return chunk; + } + + this.chunks_.push(chunk); + + while ( this.getTotalBytesReady_() < opt_buffer.length ) { + this.chunks_.push(await this.getChunk_()) + } + + // TODO: need to handle EOT condition in this loop + let offset = 0; + for (;;) { + let item = this.chunks_.shift(); + if ( item === undefined ) { + throw new Error('calculation is wrong') + } + if ( offset + item.length > opt_buffer.length ) { + const diff = opt_buffer.length - offset; + this.chunks_.unshift(item.subarray(diff)); + item = item.subarray(0, diff); + } + opt_buffer.set(item, offset); + offset += item.length; + + if ( offset == opt_buffer.length ) break; + } + + // return opt_buffer.length; + } + + async getChunk_() { + if ( this.chunks_.length === 0 ) { + const { value } = await this.delegate.read(); + return value; + } + + const len = this.getTotalBytesReady_(); + const merged = new Uint8Array(len); + let offset = 0; + for ( const item of this.chunks_ ) { + merged.set(item, offset); + offset += item.length; + } + + this.chunks_ = []; + + return merged; + } + + getTotalBytesReady_ () { + return this.chunks_.reduce((sum, chunk) => sum + chunk.length, 0); + } +} + +/** + * PTT: pseudo-terminal target; called "slave" in POSIX + */ +export class PTT { + constructor(pty) { + this.readableStream = new ReadableStream({ + start: controller => { + this.readController = controller; + } + }); + this.writableStream = new WritableStream({ + start: controller => { + this.writeController = controller; + }, + write: chunk => { + if (typeof chunk === 'string') { + chunk = encoder.encode(chunk); + } + if ( pty.outputModeflags?.outputNLCR ) { + chunk = pty.LF_to_CRLF(chunk); + } + pty.readController.enqueue(chunk); + } + }); + this.out = this.writableStream.getWriter(); + this.in = this.readableStream.getReader(); + } +} + +/** + * PTY: pseudo-terminal + * + * This implements the PTY device driver. + */ +export class PTY { + constructor () { + this.outputModeflags = { + outputNLCR: true + }; + this.readableStream = new ReadableStream({ + start: controller => { + this.readController = controller; + } + }); + this.writableStream = new WritableStream({ + start: controller => { + this.writeController = controller; + }, + write: chunk => { + if ( typeof chunk === 'string' ) { + chunk = encoder.encode(chunk); + } + for ( const target of this.targets ) { + target.readController.enqueue(chunk); + } + } + }); + this.out = this.writableStream.getWriter(); + this.in = this.readableStream.getReader(); + this.targets = []; + } + + getPTT () { + const target = new PTT(this); + this.targets.push(target); + return target; + } + + LF_to_CRLF (input) { + let lfCount = 0; + for (let i = 0; i < input.length; i++) { + if (input[i] === 0x0A) { + lfCount++; + } + } + + const output = new Uint8Array(input.length + lfCount); + + let outputIndex = 0; + for (let i = 0; i < input.length; i++) { + // If LF is encountered, insert CR (0x0D) before LF (0x0A) + if (input[i] === 0x0A) { + output[outputIndex++] = 0x0D; + } + output[outputIndex++] = input[i]; + } + + return output; + } +} diff --git a/packages/phoenix/packages/pty/package.json b/packages/phoenix/packages/pty/package.json new file mode 100644 index 00000000..cc5352b8 --- /dev/null +++ b/packages/phoenix/packages/pty/package.json @@ -0,0 +1,12 @@ +{ + "name": "dev-pty", + "version": "0.0.0", + "description": "", + "main": "exports.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Puter Technologies Inc.", + "license": "AGPL-3.0-only" +} diff --git a/packages/phoenix/packages/strataparse/dsl/ParserBuilder.js b/packages/phoenix/packages/strataparse/dsl/ParserBuilder.js new file mode 100644 index 00000000..0936f973 --- /dev/null +++ b/packages/phoenix/packages/strataparse/dsl/ParserBuilder.js @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { SingleParserFactory } from "../parse.js"; + +export class ParserConfigDSL extends SingleParserFactory { + constructor (parserFactory, cls) { + super(); + this.parserFactory = parserFactory; + this.cls_ = cls; + this.parseParams_ = {}; + this.grammarParams_ = { + assign: {}, + }; + } + + parseParams (obj) { + Object.assign(this.parseParams_, obj); + return this; + } + + assign (obj) { + Object.assign(this.grammarParams_.assign, obj); + return this; + } + + create () { + return this.parserFactory.create( + this.cls_, this.parseParams_, this.grammarParams_, + ); + } +} + +export class ParserBuilder { + constructor ({ + parserFactory, + parserRegistry, + }) { + this.parserFactory = parserFactory; + this.parserRegistry = parserRegistry; + this.parserAPI_ = null; + } + + get parserAPI () { + if ( this.parserAPI_ ) return this.parserAPI_; + + const parserAPI = {}; + + const parsers = this.parserRegistry.parsers; + for ( const parserId in parsers ) { + const parserCls = parsers[parserId]; + parserAPI[parserId] = + this.createParserFunction(parserCls); + } + + return this.parserAPI_ = parserAPI; + } + + createParserFunction (parserCls) { + if ( parserCls.hasOwnProperty('createFunction') ) { + return parserCls.createFunction({ + parserFactory: this.parserFactory + }); + } + + return params => { + const configDSL = new ParserConfigDSL(parserCls) + configDSL.parseParams(params); + return configDSL; + }; + } + + def (def) { + const a = this.parserAPI; + return def(a); + } +} \ No newline at end of file diff --git a/packages/phoenix/packages/strataparse/dsl/ParserRegistry.js b/packages/phoenix/packages/strataparse/dsl/ParserRegistry.js new file mode 100644 index 00000000..87e5f7b4 --- /dev/null +++ b/packages/phoenix/packages/strataparse/dsl/ParserRegistry.js @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class ParserRegistry { + constructor () { + this.parsers_ = {}; + } + register (id, parser) { + this.parsers_[id] = parser; + } + get parsers () { + return this.parsers_; + } +} diff --git a/packages/phoenix/packages/strataparse/exports.js b/packages/phoenix/packages/strataparse/exports.js new file mode 100644 index 00000000..928f2902 --- /dev/null +++ b/packages/phoenix/packages/strataparse/exports.js @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ParserRegistry } from './dsl/ParserRegistry.js'; +import { PStratum } from './strata.js'; + +export { + Parser, + ParseResult, + ParserFactory, +} from './parse.js'; + +import WhitespaceParserImpl from './parse_impls/whitespace.js'; +import LiteralParserImpl from './parse_impls/literal.js'; +import StrUntilParserImpl from './parse_impls/StrUntilParserImpl.js'; + +import { + SequenceParserImpl, + ChoiceParserImpl, + RepeatParserImpl, + NoneParserImpl, +} from './parse_impls/combinators.js'; + +export { + WhitespaceParserImpl, + LiteralParserImpl, + SequenceParserImpl, + ChoiceParserImpl, + RepeatParserImpl, + StrUntilParserImpl, +} + +export { + PStratum, + TerminalPStratumImplType, + DelegatingPStratumImplType, +} from './strata.js'; + +export { + BytesPStratumImpl, + StringPStratumImpl +} from './strata_impls/terminals.js'; + +export { + default as FirstRecognizedPStratumImpl, +} from './strata_impls/FirstRecognizedPStratumImpl.js'; + +export { + default as ContextSwitchingPStratumImpl, +} from './strata_impls/ContextSwitchingPStratumImpl.js'; + +export { ParserBuilder } from './dsl/ParserBuilder.js'; + +export class StrataParseFacade { + static getDefaultParserRegistry() { + const r = new ParserRegistry(); + r.register('sequence', SequenceParserImpl); + r.register('choice', ChoiceParserImpl); + r.register('repeat', RepeatParserImpl); + r.register('literal', LiteralParserImpl); + r.register('none', NoneParserImpl); + + return r; + } +} + +export class StrataParser { + constructor () { + this.strata = []; + this.error = null; + } + add (stratum) { + if ( ! ( stratum instanceof PStratum ) ) { + stratum = new PStratum(stratum); + } + + // TODO: verify that terminals don't delegate + // TODO: verify the delegating strata delegate + if ( this.strata.length > 0 ) { + const delegate = this.strata[this.strata.length - 1]; + stratum.setDelegate(delegate); + } + + this.strata.push(stratum); + } + next () { + return this.strata[this.strata.length - 1].next(); + } + parse () { + let done, value; + const result = []; + for ( ;; ) { + ({ done, value } = + this.strata[this.strata.length - 1].next()); + if ( done ) break + result.push(value); + } + if ( value ) { + this.error = value; + } + return result; + } +} diff --git a/packages/phoenix/packages/strataparse/package.json b/packages/phoenix/packages/strataparse/package.json new file mode 100644 index 00000000..794dc239 --- /dev/null +++ b/packages/phoenix/packages/strataparse/package.json @@ -0,0 +1,13 @@ +{ + "name": "strataparse", + "version": "0.0.0", + "description": "", + "main": "exports.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Puter Technologies Inc.", + "license": "AGPL-3.0-only" +} + diff --git a/packages/phoenix/packages/strataparse/parse.js b/packages/phoenix/packages/strataparse/parse.js new file mode 100644 index 00000000..14663335 --- /dev/null +++ b/packages/phoenix/packages/strataparse/parse.js @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class Parser { + constructor ({ + impl, + assign, + }) { + this.impl = impl; + this.assign = assign ?? {}; + } + parse (lexer) { + const unadaptedResult = this.impl.parse(lexer); + const pr = unadaptedResult instanceof ParseResult + ? unadaptedResult : new ParseResult(unadaptedResult); + if ( pr.status === ParseResult.VALUE ) { + pr.value = { + ...pr.value, + ...this.assign, + }; + } + return pr; + } +} + +export class ParseResult { + static UNRECOGNIZED = { name: 'unrecognized' }; + static VALUE = { name: 'value' }; + static INVALID = { name: 'invalid' }; + constructor (value, opt_status) { + if ( + value === ParseResult.UNRECOGNIZED || + value === ParseResult.INVALID + ) { + this.status = value; + return; + } + this.status = opt_status ?? ( + value === undefined + ? ParseResult.UNRECOGNIZED + : ParseResult.VALUE + ); + this.value = value; + } +} + +class ConcreteSyntaxParserDecorator { + constructor (delegate) { + this.delegate = delegate; + } + parse (lexer, ...a) { + const start = lexer.seqNo; + const result = this.delegate.parse(lexer, ...a); + if ( result.status === ParseResult.VALUE ) { + const end = lexer.seqNo; + result.value.$cst = { start, end }; + } + return result; + } +} + +class RememberSourceParserDecorator { + constructor (delegate) { + this.delegate = delegate; + } + parse (lexer, ...a) { + const start = lexer.seqNo; + const result = this.delegate.parse(lexer, ...a); + if ( result.status === ParseResult.VALUE ) { + const end = lexer.seqNo; + result.value.$source = lexer.reach(start, end); + } + return result; + } +} + +export class ParserFactory { + constructor () { + this.concrete = false; + this.rememberSource = false; + } + decorate (obj) { + if ( this.concrete ) { + obj = new ConcreteSyntaxParserDecorator(obj); + } + if ( this.rememberSource ) { + obj = new RememberSourceParserDecorator(obj); + } + + return obj; + } + create (cls, parserParams, resultParams) { + parserParams = parserParams ?? {}; + + resultParams = resultParams ?? {}; + resultParams.assign = resultParams.assign ?? {}; + + const impl = new cls(parserParams); + const parser = new Parser({ + impl, + assign: resultParams.assign + }); + + // return parser; + return this.decorate(parser); + } +} + +export class SingleParserFactory { + create () { + throw new Error('abstract create() must be implemented'); + } +} + +export class AcceptParserUtil { + static adapt (parser) { + if ( parser === undefined ) return undefined; + if ( parser instanceof SingleParserFactory ) { + parser = parser.create(); + } + if ( ! (parser instanceof Parser) ) { + parser = new Parser({ impl: parser }); + } + return parser; + } +} \ No newline at end of file diff --git a/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js b/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js new file mode 100644 index 00000000..4ca5b81a --- /dev/null +++ b/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default class StrUntilParserImpl { + constructor ({ stopChars }) { + this.stopChars = stopChars; + } + parse (lexer) { + let text = ''; + for ( ;; ) { + console.log('B') + let { done, value } = lexer.look(); + + if ( done ) break; + + // TODO: doing this strictly one byte at a time + // doesn't allow multi-byte stop characters + if ( typeof value === 'number' ) value = + String.fromCharCode(value); + + if ( this.stopChars.includes(value) ) break; + + text += value; + lexer.next(); + } + + if ( text.length === 0 ) return; + + console.log('test?', text) + + return { $: 'until', text }; + } +} diff --git a/packages/phoenix/packages/strataparse/parse_impls/combinators.js b/packages/phoenix/packages/strataparse/parse_impls/combinators.js new file mode 100644 index 00000000..6d6b5697 --- /dev/null +++ b/packages/phoenix/packages/strataparse/parse_impls/combinators.js @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ParserConfigDSL } from "../dsl/ParserBuilder.js"; +import { AcceptParserUtil, Parser, ParseResult } from "../parse.js"; + +export class SequenceParserImpl { + static createFunction ({ parserFactory }) { + return (...parsers) => { + const conf = new ParserConfigDSL(parserFactory, this); + conf.parseParams({ parsers }); + return conf; + }; + } + constructor ({ parsers }) { + this.parsers = parsers.map(AcceptParserUtil.adapt); + } + parse (lexer) { + const results = []; + for ( const parser of this.parsers ) { + const subLexer = lexer.fork(); + const result = parser.parse(subLexer); + if ( result.status === ParseResult.UNRECOGNIZED ) { + return; + } + if ( result.status === ParseResult.INVALID ) { + // TODO: this is wrong + return { done: true, value: result }; + } + lexer.join(subLexer); + results.push(result.value); + } + + return { $: 'sequence', results }; + } +} + +export class ChoiceParserImpl { + static createFunction ({ parserFactory }) { + return (...parsers) => { + const conf = new ParserConfigDSL(parserFactory, this); + conf.parseParams({ parsers }); + return conf; + }; + } + constructor ({ parsers }) { + this.parsers = parsers.map(AcceptParserUtil.adapt); + } + parse (lexer) { + for ( const parser of this.parsers ) { + const subLexer = lexer.fork(); + const result = parser.parse(subLexer); + if ( result.status === ParseResult.UNRECOGNIZED ) { + continue; + } + if ( result.status === ParseResult.INVALID ) { + // TODO: this is wrong + return { done: true, value: result }; + } + lexer.join(subLexer); + return result.value; + } + + return; + } +} + +export class RepeatParserImpl { + static createFunction ({ parserFactory }) { + return (delegate) => { + const conf = new ParserConfigDSL(parserFactory, this); + conf.parseParams({ delegate }); + return conf; + }; + } + constructor ({ delegate }) { + delegate = AcceptParserUtil.adapt(delegate); + this.delegate = delegate; + } + + parse (lexer) { + const results = []; + for ( ;; ) { + const subLexer = lexer.fork(); + const result = this.delegate.parse(subLexer); + if ( result.status === ParseResult.UNRECOGNIZED ) { + break; + } + if ( result.status === ParseResult.INVALID ) { + return { done: true, value: result }; + } + lexer.join(subLexer); + results.push(result.value); + } + + return { $: 'repeat', results }; + } +} + +export class NoneParserImpl { + static createFunction ({ parserFactory }) { + return () => { + const conf = new ParserConfigDSL(parserFactory, this); + return conf; + }; + } + parse () { + return { $: 'none', $discard: true }; + } +} diff --git a/packages/phoenix/packages/strataparse/parse_impls/literal.js b/packages/phoenix/packages/strataparse/parse_impls/literal.js new file mode 100644 index 00000000..4b6319cf --- /dev/null +++ b/packages/phoenix/packages/strataparse/parse_impls/literal.js @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ParserConfigDSL } from "../dsl/ParserBuilder.js"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +export default class LiteralParserImpl { + static meta = { + inputs: 'bytes', + outputs: 'node' + } + static createFunction ({ parserFactory }) { + return (value) => { + const conf = new ParserConfigDSL(parserFactory, this); + conf.parseParams({ value }); + return conf; + }; + } + constructor ({ value }) { + // adapt value + if ( typeof value === 'string' ) { + value = encoder.encode(value); + } + + if ( value.length === 0 ) { + throw new Error( + 'tried to construct a LiteralParser with an ' + + 'empty value, which could cause infinite ' + + 'iteration' + ); + } + + this.value = value; + } + parse (lexer) { + for ( let i=0 ; i < this.value.length ; i++ ) { + let { done, value } = lexer.next(); + if ( done ) return; + if ( this.value[i] !== value ) return; + } + + const text = decoder.decode(this.value); + return { $: 'literal', text }; + } +} diff --git a/packages/phoenix/packages/strataparse/parse_impls/whitespace.js b/packages/phoenix/packages/strataparse/parse_impls/whitespace.js new file mode 100644 index 00000000..82bcb1cb --- /dev/null +++ b/packages/phoenix/packages/strataparse/parse_impls/whitespace.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default class WhitespaceParserImpl { + static meta = { + inputs: 'bytes', + outputs: 'node' + } + static data = { + whitespaceCharCodes: ' \r\t'.split('') + .map(chr => chr.charCodeAt(0)) + } + parse (lexer) { + const { whitespaceCharCodes } = this.constructor.data; + + let text = ''; + + for ( ;; ) { + const { done, value } = lexer.look(); + if ( done ) break; + if ( ! whitespaceCharCodes.includes(value) ) break; + text += String.fromCharCode(value); + lexer.next(); + } + + if ( text.length === 0 ) return; + + return { $: 'whitespace', text }; + } +} diff --git a/packages/phoenix/packages/strataparse/strata.js b/packages/phoenix/packages/strataparse/strata.js new file mode 100644 index 00000000..73dc7055 --- /dev/null +++ b/packages/phoenix/packages/strataparse/strata.js @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class DelegatingPStratumImplAPI { + constructor (facade) { + this.facade = facade; + } + get delegate () { + return this.facade.delegate; + } +} + +export class DelegatingPStratumImplType { + constructor (facade) { + this.facade = facade; + } + getImplAPI () { + return new DelegatingPStratumImplAPI(this.facade); + } +} + +export class TerminalPStratumImplType { + getImplAPI () { + return {}; + } +} + +export class PStratum { + constructor (impl) { + this.impl = impl; + + const implTypeClass = this.impl.constructor.TYPE + ?? DelegatingPStratumImplType; + + this.implType = new implTypeClass(this); + this.api = this.implType.getImplAPI(); + + this.lookValue = null; + this.seqNo = 0; + + this.history = []; + // TODO: make this configurable + this.historyOn = ! this.impl.reach; + } + + setDelegate (delegate) { + this.delegate = delegate; + } + + look () { + if ( this.looking ) { + return this.lookValue; + } + this.looking = true; + this.lookValue = this.impl.next(this.api); + return this.lookValue; + } + + next () { + this.seqNo++; + let toReturn; + if ( this.looking ) { + this.looking = false; + toReturn = this.lookValue; + } else { + toReturn = this.impl.next(this.api); + } + this.history.push(toReturn.value); + return toReturn; + } + + fork () { + const forkImpl = this.impl.fork(this.api); + const fork = new PStratum(forkImpl); + // DRY: sync state + fork.looking = this.looking; + fork.lookValue = this.lookValue; + fork.seqNo = this.seqNo; + fork.history = [...this.history]; + return fork; + } + + join (friend) { + // DRY: sync state + this.looking = friend.looking; + this.lookValue = friend.lookValue; + this.seqNo = friend.seqNo; + this.history = friend.history; + this.impl.join(this.api, friend.impl); + } + + reach (start, end) { + if ( this.impl.reach ) { + return this.impl.reach(this.api, start, end) + } + if ( this.historyOn ) { + return this.history.slice(start, end); + } + } +} diff --git a/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js b/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js new file mode 100644 index 00000000..68205931 --- /dev/null +++ b/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { AcceptParserUtil, ParseResult, Parser } from "../parse.js"; + +export default class ContextSwitchingPStratumImpl { + constructor ({ contexts, entry }) { + this.contexts = { ...contexts }; + for ( const key in this.contexts ) { + console.log('parsers?', this.contexts[key]); + const new_array = []; + for ( const parser of this.contexts[key] ) { + if ( parser.hasOwnProperty('transition') ) { + new_array.push({ + ...parser, + parser: AcceptParserUtil.adapt(parser.parser), + }) + } else { + new_array.push(AcceptParserUtil.adapt(parser)); + } + } + this.contexts[key] = new_array; + } + this.stack = [{ + context_name: entry, + }]; + this.valid = true; + + this.lastvalue = null; + } + get stack_top () { + console.log('stack top?', this.stack[this.stack.length - 1]) + return this.stack[this.stack.length - 1]; + } + get current_context () { + return this.contexts[this.stack_top.context_name]; + } + next (api) { + if ( ! this.valid ) return { done: true }; + const lexer = api.delegate; + + const context = this.current_context; + console.log('context?', context); + for ( const spec of context ) { + { + const { done, value } = lexer.look(); + this.anti_cycle_i = value === this.lastvalue ? (this.anti_cycle_i || 0) + 1 : 0; + if ( this.anti_cycle_i > 30 ) { + throw new Error('infinite loop'); + } + this.lastvalue = value; + console.log('last value?', value, done); + if ( done ) return { done }; + } + + let parser, transition, peek; + if ( spec.hasOwnProperty('parser') ) { + ({ parser, transition, peek } = spec); + } else { + parser = spec; + } + + const subLexer = lexer.fork(); + // console.log('spec?', spec); + const result = parser.parse(subLexer); + if ( result.status === ParseResult.UNRECOGNIZED ) { + continue; + } + if ( result.status === ParseResult.INVALID ) { + return { done: true, value: result }; + } + console.log('RESULT', result, spec) + if ( ! peek ) lexer.join(subLexer); + + if ( transition ) { + console.log('GOT A TRANSITION') + if ( transition.pop ) this.stack.pop(); + if ( transition.to ) this.stack.push({ + context_name: transition.to, + }); + } + + if ( result.value.$discard || peek ) return this.next(api); + + console.log('PROVIDING VALUE', result.value); + return { done: false, value: result.value }; + } + + return { done: true, value: 'ran out of parsers' }; + } +} diff --git a/packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js b/packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js new file mode 100644 index 00000000..cc3c645c --- /dev/null +++ b/packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { AcceptParserUtil, ParseResult, Parser } from "../parse.js"; + +export default class FirstRecognizedPStratumImpl { + static meta = { + description: ` + Implements a layer of top-down parsing by + iterating over parsers for higher-level constructs + and returning the first recognized value that was + produced from lower-level constructs. + ` + } + constructor ({ parsers }) { + this.parsers = parsers.map(AcceptParserUtil.adapt); + this.valid = true; + } + next (api) { + if ( ! this.valid ) return { done: true }; + const lexer = api.delegate; + + for ( const parser of this.parsers ) { + { + const { done } = lexer.look(); + if ( done ) return { done }; + } + + const subLexer = lexer.fork(); + const result = parser.parse(subLexer); + if ( result.status === ParseResult.UNRECOGNIZED ) { + continue; + } + if ( result.status === ParseResult.INVALID ) { + return { done: true, value: result }; + } + lexer.join(subLexer); + return { done: false, value: result.value }; + } + + return { done: true, value: 'ran out of parsers' }; + } +} diff --git a/packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js b/packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js new file mode 100644 index 00000000..f699557b --- /dev/null +++ b/packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const decoder = new TextDecoder(); + +export class MergeWhitespacePStratumImpl { + static meta = { + inputs: 'node', + outputs: 'node', + } + constructor (tabWidth) { + this.tabWidth = tabWidth ?? 1; + this.line = 0; + this.col = 0; + } + countChar (c) { + if ( c === '\n' ) { + this.line++; + this.col = 0; + return; + } + if ( c === '\t' ) { + this.col += this.tabWidth; + return; + } + if ( c === '\r' ) return; + this.col++; + } + next (api) { + const lexer = api.delegate; + + for ( ;; ) { + const { value, done } = lexer.next(); + if ( done ) return { value, done }; + + if ( value.$ === 'whitespace' ) { + for ( const c of value.text ) { + this.countChar(c); + } + return { value, done: false }; + // continue; + } + + value.$cst = { + ...(value.$cst ?? {}), + line: this.line, + col: this.col, + }; + + if ( value.hasOwnProperty('$source') ) { + let source = value.$source; + if ( source instanceof Uint8Array ) { + source = decoder.decode(source); + } + for ( let c of source ) { + this.countChar(c); + } + } else { + console.warn('source missing; can\'t count position'); + } + + return { value, done: false }; + } + } +} diff --git a/packages/phoenix/packages/strataparse/strata_impls/terminals.js b/packages/phoenix/packages/strataparse/strata_impls/terminals.js new file mode 100644 index 00000000..eb3445c6 --- /dev/null +++ b/packages/phoenix/packages/strataparse/strata_impls/terminals.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { TerminalPStratumImplType } from "../strata.js"; + +export class BytesPStratumImpl { + static TYPE = TerminalPStratumImplType + + constructor (bytes, opt_i) { + this.bytes = bytes; + this.i = opt_i ?? 0; + } + next () { + if ( this.i === this.bytes.length ) { + return { done: true, value: undefined }; + } + + const i = this.i++; + return { done: false, value: this.bytes[i] }; + } + fork () { + return new BytesPStratumImpl(this.bytes, this.i); + } + join (api, forked) { + this.i = forked.i; + } + reach (api, start, end) { + return this.bytes.slice(start, end); + } +} + +export class StringPStratumImpl { + static TYPE = TerminalPStratumImplType + + constructor (str) { + const encoder = new TextEncoder(); + const bytes = encoder.encode(str); + this.delegate = new BytesPStratumImpl(bytes); + } + // DRY: proxy methods + next (...a) { + return this.delegate.next(...a); + } + fork (...a) { + return this.delegate.fork(...a); + } + join (...a) { + return this.delegate.join(...a); + } + reach (...a) { + return this.delegate.reach(...a); + } +} diff --git a/packages/phoenix/rollup.config.js b/packages/phoenix/rollup.config.js new file mode 100644 index 00000000..ab826f38 --- /dev/null +++ b/packages/phoenix/rollup.config.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { nodeResolve } from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs'; +import copy from 'rollup-plugin-copy'; + +const configFile = process.env.CONFIG_FILE ?? 'config/dev.js'; +await import(`./${configFile}`); + +export default { + input: "src/main_puter.js", + output: { + file: "dist/bundle.js", + format: "iife" + }, + plugins: [ + nodeResolve(), + commonjs(), + copy({ + targets: [ + { + src: 'assets/index.html', + dest: 'dist', + transform: (contents, name) => { + return contents.toString().replace('__SDK_URL__', globalThis.__CONFIG__.sdk_url); + } + }, + { src: 'assets/shell.html', dest: 'dist' }, + { src: configFile, dest: 'dist', rename: 'config.js' } + ] + }), + ] +} diff --git a/packages/phoenix/run.json5 b/packages/phoenix/run.json5 new file mode 100644 index 00000000..a7f23ac7 --- /dev/null +++ b/packages/phoenix/run.json5 @@ -0,0 +1,15 @@ +{ + services: [ + { + name: 'shell.http', + pwd: './dist', + // command: 'npx http-server -p 8080 -S -C "{cert}" -K "{key}"', + command: 'npx http-server -p 8080', + }, + { + name: 'shell.rollup', + command: 'npx rollup -c rollup.config.js --watch', + pwd: '.' + }, + ], +} diff --git a/packages/phoenix/src/ansi-shell/ANSIContext.js b/packages/phoenix/src/ansi-shell/ANSIContext.js new file mode 100644 index 00000000..cc7283a1 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ANSIContext.js @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Context } from "../context/context.js"; + +const modifiers = ['shift', 'alt', 'ctrl', 'meta']; + +const keyboardModifierBits = {}; +for ( let i=0 ; i < modifiers.length ; i++ ) { + const key = `KEYBOARD_BIT_${modifiers[i].toUpperCase()}`; + keyboardModifierBits[key] = 1 << i; +} + +export const ANSIContext = new Context({ + constants: { + CHAR_LF: '\n'.charCodeAt(0), + CHAR_CR: '\r'.charCodeAt(0), + CHAR_TAB: '\t'.charCodeAt(0), + CHAR_CSI: '['.charCodeAt(0), + CHAR_OSC: ']'.charCodeAt(0), + CHAR_ETX: 0x03, + CHAR_EOT: 0x04, + CHAR_ESC: 0x1B, + CHAR_DEL: 0x7F, + CHAR_BEL: 0x07, + CHAR_FF: 0x0C, + CSI_F_0: 0x40, + CSI_F_E: 0x7F, + ...keyboardModifierBits + } +}); + +export const getActiveModifiersFromXTerm = (n) => { + // decrement explained in doc/graveyard/keyboard_modifiers.md + n--; + + const active = {}; + + for ( let i=0 ; i < modifiers.length ; i++ ) { + if ( n & 1 << i ) { + active[modifiers[i]] = true; + } + } + + return active; +}; diff --git a/packages/phoenix/src/ansi-shell/ANSIShell.js b/packages/phoenix/src/ansi-shell/ANSIShell.js new file mode 100644 index 00000000..0eb151e8 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ANSIShell.js @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ConcreteSyntaxError } from "./ConcreteSyntaxError.js"; +import { MultiWriter } from "./ioutil/MultiWriter.js"; +import { Coupler } from "./pipeline/Coupler.js"; +import { Pipe } from "./pipeline/Pipe.js"; +import { Pipeline } from "./pipeline/Pipeline.js"; + +export class ANSIShell extends EventTarget { + constructor (ctx) { + super(); + + this.ctx = ctx; + this.variables_ = {}; + this.config = ctx.externs.config; + + this.debugFeatures = {}; + + const self = this; + this.variables = new Proxy(this.variables_, { + get (target, k) { + return Reflect.get(target, k); + }, + set (target, k, v) { + const oldval = target[k]; + const retval = Reflect.set(target, k, v); + self.dispatchEvent(new CustomEvent('shell-var-change', { + key: k, + oldValue: oldval, + newValue: target[k], + })) + return retval; + } + }) + + this.addEventListener('signal.window-resize', evt => { + this.variables.size = evt.detail; + }) + + this.env = {}; + + this.initializeReasonableDefaults(); + } + + export_ (k, v) { + if ( typeof v === 'function' ) { + Object.defineProperty(this.env, k, { + enumerable: true, + get: v + }) + return; + } + this.env[k] = v; + } + + initializeReasonableDefaults() { + const { env } = this.ctx.platform; + const home = env.get('HOME'); + const user = env.get('USER'); + this.variables.pwd = home; + this.variables.home = home; + this.variables.user = user; + + this.variables.host = env.get('HOSTNAME'); + + // Computed values + Object.defineProperty(this.env, 'PWD', { + enumerable: true, + get: () => this.variables.pwd, + set: v => this.variables.pwd = v + }) + Object.defineProperty(this.env, 'ROWS', { + enumerable: true, + get: () => this.variables.size?.rows ?? 0 + }) + Object.defineProperty(this.env, 'COLS', { + enumerable: true, + get: () => this.variables.size?.cols ?? 0 + }) + + this.export_('LANG', 'en_US.UTF-8'); + this.export_('PS1', '[\\u@puter.com \\w]\\$ '); + + for ( const k in env.getEnv() ) { + console.log('setting', k, env.get(k)); + this.export_(k, env.get(k)); + } + + // Default values + this.export_('HOME', () => this.variables.home); + this.export_('USER', () => this.variables.user); + this.export_('TERM', 'xterm-256color'); + this.export_('TERM_PROGRAM', 'puter-ansi'); + // TODO: determine how localization will affect this + // TODO: add TERM_PROGRAM_VERSION + // TODO: add OLDPWD + } + + async doPromptIteration() { + if ( globalThis.force_eot && this.ctx.platform.name === 'node' ) { + process.exit(0); + } + const { readline } = this.ctx.externs; + // DRY: created the same way in runPipeline + const executionCtx = this.ctx.sub({ + vars: this.variables, + env: this.env, + locals: { + pwd: this.variables.pwd, + } + }); + this.ctx.externs.echo.off(); + const input = await readline( + this.expandPromptString(this.env.PS1), + executionCtx, + ); + this.ctx.externs.echo.on(); + + if ( input.trim() === '' ) { + this.ctx.externs.out.write(''); + return; + } + + // Specially-processed inputs for debug features + if ( input.startsWith('%%%') ) { + this.ctx.externs.out.write('%%%: interpreting as debug instruction\n'); + const [prefix, flag, onOff] = input.split(' '); + const isOn = onOff === 'on' ? true : false; + this.ctx.externs.out.write( + `%%%: Setting ${JSON.stringify(flag)} to ` + + (isOn ? 'ON' : 'OFF') + '\n' + ) + this.debugFeatures[flag] = isOn; + return; // don't run as a pipeline + } + + // TODO: catch here, but errors need to be more structured first + try { + await this.runPipeline(input); + } catch (e) { + if ( e instanceof ConcreteSyntaxError ) { + const here = e.print_here(input); + this.ctx.externs.out.write(here + '\n'); + } + this.ctx.externs.out.write('error: ' + e.message + '\n'); + console.log(e); + return; + } + } + + readtoken (str) { + return this.ctx.externs.parser.parseLineForProcessing(str); + } + + async runPipeline (cmdOrTokens) { + const tokens = typeof cmdOrTokens === 'string' + ? (() => { + // TODO: move to doPromptIter with better error objects + try { + return this.readtoken(cmdOrTokens) + } catch (e) { + this.ctx.externs.out.write('error: ' + + e.message + '\n'); + return; + } + })() + : cmdOrTokens ; + + if ( tokens.length === 0 ) return; + + if ( tokens.length > 1 ) { + // TODO: as exception instead, and more descriptive + this.ctx.externs.out.write( + "something went wrong...\n" + ); + return; + } + + let ast = tokens[0]; + + // Left the code below here (commented) because I think it's + // interesting; the AST now always has a pipeline at the top + // level after recent changes to the parser. + + // // wrap an individual command in a pipeline + // // TODO: should this be done here, or elsewhere? + // if ( ast.$ === 'command' ) { + // ast = { + // $: 'pipeline', + // components: [ast] + // }; + // } + + if ( this.debugFeatures['show-ast'] ) { + this.ctx.externs.out.write( + JSON.stringify(tokens, undefined, ' ') + '\n' + ); + return; + } + + const executionCtx = this.ctx.sub({ + vars: this.variables, + env: this.env, + locals: { + pwd: this.variables.pwd, + } + }); + + const pipeline = await Pipeline.createFromAST(executionCtx, ast); + + await pipeline.execute(executionCtx); + } + + expandPromptString (str) { + str = str.replace('\\u', this.variables.user); + str = str.replace('\\w', this.variables.pwd); + str = str.replace('\\h', this.variables.host); + str = str.replace('\\$', '$'); + return str; + } + + async outputANSI (ctx) { + await ctx.iterate(async item => { + ctx.externs.out.write(item.name + '\n'); + }); + } +} diff --git a/packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js b/packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js new file mode 100644 index 00000000..7ffbfd5c --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/** + * An error for which the location it occurred within the input is known. + */ +export class ConcreteSyntaxError extends Error { + constructor(message, cst_location) { + super(message); + this.cst_location = cst_location; + } + + /** + * Prints the location of the error in the input. + * + * Example output: + * + * ``` + * 1: echo $($(echo zxcv)) + * ^^^^^^^^^^^ + * ``` + * + * @param {*} input + */ + print_here (input) { + const lines = input.split('\n'); + const line = lines[this.cst_location.line]; + const str_line_number = String(this.cst_location.line + 1) + ': '; + const n_spaces = + str_line_number.length + + this.cst_location.start; + const n_arrows = Math.max( + this.cst_location.end - this.cst_location.start, + 1 + ); + + return ( + str_line_number + line + '\n' + + ' '.repeat(n_spaces) + '^'.repeat(n_arrows) + ); + } +} diff --git a/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js b/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js new file mode 100644 index 00000000..c8f4832a --- /dev/null +++ b/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { parseArgs } from '@pkgjs/parseargs'; +import { DEFAULT_OPTIONS } from '../../puter-shell/coreutils/coreutil_lib/help.js'; + +export default { + name: 'simple-parser', + async process (ctx, spec) { + console.log({ + ...spec, + args: ctx.locals.args + }); + + // Insert standard options + spec.options = Object.assign(spec.options || {}, DEFAULT_OPTIONS); + + let result; + try { + if ( ! ctx.locals.args ) debugger; + result = parseArgs({ ...spec, args: ctx.locals.args }); + } catch (e) { + await ctx.externs.out.write( + '\x1B[31;1m' + + 'error parsing arguments: ' + + e.message + '\x1B[0m\n'); + ctx.cmdExecState.valid = false; + return; + } + + if (result.values.help) { + ctx.cmdExecState.printHelpAndExit = true; + } + + ctx.locals.values = result.values; + ctx.locals.positionals = result.positionals; + } +} diff --git a/packages/phoenix/src/ansi-shell/decorators/errors.js b/packages/phoenix/src/ansi-shell/decorators/errors.js new file mode 100644 index 00000000..85eabb3e --- /dev/null +++ b/packages/phoenix/src/ansi-shell/decorators/errors.js @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'errors', + decorate (fn, { command, ctx }) { + return async (...a) => { + try { + await fn(...a); + } catch (e) { + console.log('GOT IT HERE'); + // message without "Error:" + let message = e.message; + if (message.startsWith('Error: ')) { + message = message.slice(7); + } + ctx.externs.err.write( + '\x1B[31;1m' + command.name + ': ' + message + '\x1B[0m\n' + ); + } + } + } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js b/packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js new file mode 100644 index 00000000..d2e0655a --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ProxyWriter } from "./ProxyWriter.js"; + +const encoder = new TextEncoder(); + +export class ByteWriter extends ProxyWriter { + async write (item) { + if ( typeof item === 'string' ) { + item = encoder.encode(item); + } + if ( item instanceof Blob ) { + item = new Uint8Array(await item.arrayBuffer()); + } + await this.delegate.write(item); + } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/MemReader.js b/packages/phoenix/src/ansi-shell/ioutil/MemReader.js new file mode 100644 index 00000000..53d711ae --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/MemReader.js @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class MemReader { + constructor (data) { + this.data = data; + this.pos = 0; + } + async read (opt_buffer) { + if ( this.pos >= this.data.length ) { + return { done: true }; + } + + if ( ! opt_buffer ) { + this.pos = this.data.length; + return { value: this.data, done: false }; + } + + const toReturn = this.data.slice( + this.pos, + Math.min(this.pos + opt_buffer.length, this.data.length), + ); + + return { + value: opt_buffer, + size: toReturn.length + }; + } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/MemWriter.js b/packages/phoenix/src/ansi-shell/ioutil/MemWriter.js new file mode 100644 index 00000000..8e091f0d --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/MemWriter.js @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const encoder = new TextEncoder(); + +export class MemWriter { + constructor () { + this.items = []; + } + async write (item) { + this.items.push(item); + } + async close () {} + + getAsUint8Array() { + const uint8arrays = []; + for ( let item of this.items ) { + if ( typeof item === 'string' ) { + item = encoder.encode(item); + } + + if ( ! ( item instanceof Uint8Array ) ) { + throw new Error('could not convert to Uint8Array'); + } + + uint8arrays.push(item); + } + + const outputUint8Array = new Uint8Array( + uint8arrays.reduce((sum, item) => sum + item.length, 0) + ); + + let pos = 0; + for ( const item of uint8arrays ) { + outputUint8Array.set(item, pos); + pos += item.length; + } + + return outputUint8Array; + } + + getAsBlob () { + // If there is just one item and it's a blob, return it + if ( this.items.length === 1 && this.items[0] instanceof Blob ) { + return this.items[0]; + } + + const uint8array = this.getAsUint8Array(); + return new Blob([uint8array]); + } + + getAsString () { + return new TextDecoder().decode(this.getAsUint8Array()); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js b/packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js new file mode 100644 index 00000000..9b3be271 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class MultiWriter { + constructor ({ delegates }) { + this.delegates = delegates; + } + + async write (item) { + for ( const delegate of this.delegates ) { + await delegate.write(item); + } + } + + async close () { + for ( const delegate of this.delegates ) { + await delegate.close(); + } + } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js b/packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js new file mode 100644 index 00000000..3980aeff --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ProxyWriter } from "./ProxyWriter.js"; + +export class NullifyWriter extends ProxyWriter { + async write (item) { + // NOOP + } + + async close () { + await this.delegate.close(); + } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js b/packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js new file mode 100644 index 00000000..9b2e6bfe --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class ProxyReader { + constructor ({ delegate }) { + this.delegate = delegate; + } + + read (...a) { return this.delegate.read(...a); } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js b/packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js new file mode 100644 index 00000000..cd6b4dbd --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class ProxyWriter { + constructor ({ delegate }) { + this.delegate = delegate; + } + + write (...a) { return this.delegate.write(...a); } + close (...a) { return this.delegate.close(...a); } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js b/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js new file mode 100644 index 00000000..59d69a0a --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ANSIContext } from "../ANSIContext.js"; +import { signals } from "../signals.js"; +import { ProxyReader } from "./ProxyReader.js"; + +const encoder = new TextEncoder(); + +export class SignalReader extends ProxyReader { + constructor ({ sig, ...kv }, ...a) { + super({ ...kv }, ...a); + this.sig = sig; + } + + async read (opt_buffer) { + const mapping = [ + [ANSIContext.constants.CHAR_ETX, signals.SIGINT], + [ANSIContext.constants.CHAR_EOT, signals.SIGQUIT], + ]; + + let { value, done } = await this.delegate.read(opt_buffer); + + if ( value === undefined ) { + return { value, done }; + } + + const tmp_value = value; + + if ( ! tmp_value instanceof Uint8Array ) { + tmp_value = encoder.encode(value); + } + + // show hex for debugging + // console.log(value.split('').map(c => c.charCodeAt(0).toString(16)).join(' ')); + console.log('value??', value) + + for ( const [key, signal] of mapping ) { + if ( tmp_value.includes(key) ) { + // this.sig.emit(signal); + // if ( signal === signals.SIGQUIT ) { + return { done: true }; + // } + } + } + + return { value, done }; + } +} diff --git a/packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js b/packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js new file mode 100644 index 00000000..be89d509 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ProxyReader } from "./ProxyReader.js"; + +const decoder = new TextDecoder(); + +export class SyncLinesReader extends ProxyReader { + constructor (...a) { + super(...a); + this.lines = []; + this.fragment = ''; + } + async read (opt_buffer) { + if ( opt_buffer ) { + // Line sync contradicts buffered reads + return await this.delegate.read(opt_buffer); + } + + return await this.readNextLine_(); + } + async readNextLine_ () { + if ( this.lines.length > 0 ) { + return { value: this.lines.shift() }; + } + + for ( ;; ) { + // CHECK: this might read once more after done; is that ok? + let { value, done } = await this.delegate.read(); + + if ( value instanceof Uint8Array ) { + value = decoder.decode(value); + } + + if ( done ) { + if ( this.fragment.length === 0 ) { + return { value, done }; + } + + value = this.fragment; + this.fragment = ''; + return { value }; + } + + if ( ! value.match(/\n|\r|\r\n/) ) { + this.fragment += value; + continue; + } + + // Guaranteed to be 2 items, because value includes a newline + const lines = value.split(/\n|\r|\r\n/); + + // The first line continues from the existing fragment + const firstLine = this.fragment + lines.shift(); + // The last line is incomplete, and goes on the fragment + this.fragment = lines.pop(); + + // Any lines between are enqueued for subsequent reads, + // and they include a line-feed character. + this.lines.push(...lines.map(txt => txt + '\n')); + + return { value: firstLine + '\n' }; + } + } +} diff --git a/packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js b/packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js new file mode 100644 index 00000000..bb4769cd --- /dev/null +++ b/packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const PARSE_CONSTANTS = { + list_ws: [' ', '\n', '\t'], + list_quot: [`"`, `'`], +}; + +PARSE_CONSTANTS.list_stoptoken = [ + '|','>','<','&','\\','#',';','(',')', + ...PARSE_CONSTANTS.list_ws, + ...PARSE_CONSTANTS.list_quot, +] + +PARSE_CONSTANTS.escapeSubstitutions = { + '\\': '\\', + '/': '/', + b: '\b', + f: '\f', + n: '\n', + r: '\r', + t: '\t', + '"': '"', + "'": "'", +}; \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js b/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js new file mode 100644 index 00000000..b38ffa6d --- /dev/null +++ b/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { StrataParser, StringPStratumImpl } from "strataparse"; +import { buildParserFirstHalf } from "./buildParserFirstHalf.js"; +import { buildParserSecondHalf } from "./buildParserSecondHalf.js"; + +export class PuterShellParser { + constructor () { + { + } + } + parseLineForSyntax () {} + parseLineForProcessing (input) { + const sp = new StrataParser(); + sp.add(new StringPStratumImpl(input)); + // TODO: optimize by re-using this parser + // buildParserFirstHalf(sp, "interpreting"); + buildParserFirstHalf(sp, "syntaxHighlighting"); + buildParserSecondHalf(sp); + const result = sp.parse(); + if ( sp.error ) { + throw new Error(sp.error); + } + console.log('PARSER RESULT', result); + return result; + } + parseScript (input) { + const sp = new StrataParser(); + sp.add(new StringPStratumImpl(input)); + buildParserFirstHalf(sp, "syntaxHighlighting"); + buildParserSecondHalf(sp, { multiline: true }); + const result = sp.parse(); + if ( sp.error ) { + throw new Error(sp.error); + } + return result; + } +} diff --git a/packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js b/packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js new file mode 100644 index 00000000..f6916088 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const list_ws = [' ', '\n', '\t']; +const list_quot = [`"`, `'`]; +const list_stoptoken = [ + '|','>','<','&','\\','#',';','(',')', + ...list_ws, + ...list_quot +]; + +export class UnquotedTokenParserImpl { + static meta = { + inputs: 'bytes', + outputs: 'node' + } + static data = { + excludes: list_stoptoken + } + parse (lexer) { + const { excludes } = this.constructor.data; + let text = ''; + + for ( ;; ) { + const { done, value } = lexer.look(); + if ( done ) break; + const str = String.fromCharCode(value); + if ( excludes.includes(str) ) break; + text += str; + lexer.next(); + } + + if ( text.length === 0 ) return; + + return { $: 'symbol', text }; + } +} diff --git a/packages/phoenix/src/ansi-shell/parsing/brainstorming.js b/packages/phoenix/src/ansi-shell/parsing/brainstorming.js new file mode 100644 index 00000000..0e16cdca --- /dev/null +++ b/packages/phoenix/src/ansi-shell/parsing/brainstorming.js @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const seq = [ + { $: 'symbol', text: 'command' }, + { $: 'string.dquote' }, + { $: 'string.segment', text: '-' }, + { $: 'op.cmd-subst' }, + { $: 'op.close' }, +]; \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js b/packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js new file mode 100644 index 00000000..141f632f --- /dev/null +++ b/packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { FirstRecognizedPStratumImpl, ParserBuilder, ParserFactory, StrUntilParserImpl, StrataParseFacade, WhitespaceParserImpl } from "strataparse"; +import { UnquotedTokenParserImpl } from "./UnquotedTokenParserImpl.js"; +import { PARSE_CONSTANTS } from "./PARSE_CONSTANTS.js"; +import { MergeWhitespacePStratumImpl } from "strataparse/strata_impls/MergeWhitespacePStratumImpl.js"; +import ContextSwitchingPStratumImpl from "strataparse/strata_impls/ContextSwitchingPStratumImpl.js"; + +const parserConfigProfiles = { + syntaxHighlighting: { cst: true }, + interpreting: { cst: false } +}; + +const list_ws = [' ', '\n', '\t']; +const list_quot = [`"`, `'`]; +const list_stoptoken = [ + '|','>','<','&','\\','#',';','(',')', + ...list_ws, + ...list_quot +]; + +export const buildParserFirstHalf = (sp, profile) => { + const options = profile ? parserConfigProfiles[profile] + : { cst: false }; + + const parserFactory = new ParserFactory(); + if ( options.cst ) { + parserFactory.concrete = true; + parserFactory.rememberSource = true; + } + + const parserRegistry = StrataParseFacade.getDefaultParserRegistry(); + + const parserBuilder = new ParserBuilder({ + parserFactory, + parserRegistry, + }); + + + // TODO: unquoted tokens will actually need to be parsed in + // segments to because `$(echo "la")h` works in sh + const buildStringParserDef = quote => { + return a => a.sequence( + a.literal(quote), + a.repeat(a.choice( + // TODO: replace this with proper parser + parserFactory.create(StrUntilParserImpl, { + stopChars: ['\\', quote], + }, { assign: { $: 'string.segment' } }), + a.sequence( + a.literal('\\'), + a.choice( + a.literal(quote), + ...Object.keys( + PARSE_CONSTANTS.escapeSubstitutions + ).map(chr => a.literal(chr)) + // TODO: \u[4],\x[2],\0[3] + ) + ).assign({ $: 'string.escape' }) + )), + a.literal(quote), + ).assign({ $: 'string' }) + }; + + + const buildStringContext = quote => [ + parserFactory.create(StrUntilParserImpl, { + stopChars: ['\\', "$", quote], + }, { assign: { $: 'string.segment' } }), + parserBuilder.def(a => a.sequence( + a.literal('\\'), + a.choice( + a.literal(quote), + ...Object.keys( + PARSE_CONSTANTS.escapeSubstitutions + ).map(chr => a.literal(chr)) + // TODO: \u[4],\x[2],\0[3] + ) + ).assign({ $: 'string.escape' })), + { + parser: parserBuilder.def(a => a.literal(quote).assign({ $: 'string.close' })), + transition: { pop: true } + }, + { + parser: parserBuilder.def(a => { + return a.literal('$(').assign({ $: 'op.cmd-subst' }) + }), + transition: { + to: 'command', + } + }, + ]; + + // sp.add( + // new FirstRecognizedPStratumImpl({ + // parsers: [ + // parserFactory.create(WhitespaceParserImpl), + // parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })), + // parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })), + // parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })), + // parserBuilder.def(a => a.literal('$((').assign({ $: 'op.arithmetic' })), + // parserBuilder.def(a => a.literal('$(').assign({ $: 'op.cmd-subst' })), + // parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })), + // parserFactory.create(StrUntilParserImpl, { + // stopChars: list_stoptoken, + // }, { assign: { $: 'symbol' } }), + // // parserFactory.create(UnquotedTokenParserImpl), + // parserBuilder.def(buildStringParserDef('"')), + // parserBuilder.def(buildStringParserDef(`'`)), + // ] + // }) + // ) + + sp.add( + new ContextSwitchingPStratumImpl({ + entry: 'command', + contexts: { + command: [ + parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })), + parserFactory.create(WhitespaceParserImpl), + parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })), + parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })), + parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })), + { + parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })), + transition: { + pop: true, + } + }, + { + parser: parserBuilder.def(a => a.literal('"').assign({ $: 'string.dquote' })), + transition: { + to: 'string.dquote', + } + }, + { + parser: parserBuilder.def(a => a.literal(`'`).assign({ $: 'string.squote' })), + transition: { + to: 'string.squote', + } + }, + { + parser: parserBuilder.def(a => a.none()), + transition: { + to: 'symbol', + } + }, + ], + 'string.dquote': buildStringContext('"'), + 'string.squote': buildStringContext(`'`), + symbol: [ + parserFactory.create(StrUntilParserImpl, { + stopChars: [...list_stoptoken, '$'], + }, { assign: { $: 'symbol' } }), + { + // TODO: redundant definition to the one in 'command' + parser: + parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })), + transition: { pop: true } + }, + { + parser: parserFactory.create(WhitespaceParserImpl), + transition: { pop: true } + }, + { + peek: true, + parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })), + transition: { pop: true } + }, + { + parser: parserBuilder.def(a => { + return a.literal('$(').assign({ $: 'op.cmd-subst' }) + }), + transition: { + to: 'command', + } + }, + { + parser: parserBuilder.def(a => a.none()), + transition: { pop: true } + }, + { + parser: parserBuilder.def(a => a.choice( + ...list_stoptoken.map(chr => a.literal(chr)) + )), + transition: { pop: true } + } + ], + }, + wrappers: { + 'string.dquote': { + $: 'string', + quote: '"', + }, + 'string.squote': { + $: 'string', + quote: `'`, + }, + }, + }) + ) + + sp.add( + new MergeWhitespacePStratumImpl() + ) +}; \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js b/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js new file mode 100644 index 00000000..4bd4d82a --- /dev/null +++ b/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ParserBuilder, ParserFactory, StrataParseFacade } from "strataparse" + +import { PARSE_CONSTANTS } from "./PARSE_CONSTANTS.js"; +const escapeSubstitutions = PARSE_CONSTANTS.escapeSubstitutions; + +const splitTokens = (items, delimPredicate) => { + const result = []; + { + let buffer = []; + // single pass to split by pipe token + for ( let i=0 ; i < items.length ; i++ ) { + if ( delimPredicate(items[i]) ) { + result.push(buffer); + buffer = []; + continue; + } + + buffer.push(items[i]); + } + + if ( buffer.length !== 0 ) { + result.push(buffer); + } + } + return result; +}; + +class ReducePrimitivesPStratumImpl { + next (api) { + const lexer = api.delegate; + + let { value, done } = lexer.next(); + + if ( value.$ === 'string' ) { + const [lQuote, contents, rQuote] = value.results; + let text = ''; + for ( const item of contents.results ) { + if ( item.$ === 'string.segment' ) { + // console.log('segment?', item.text) + text += item.text; + continue; + } + if ( item.$ === 'string.escape' ) { + const [escChar, escValue] = item.results; + if ( escValue.$ === 'literal' ) { + text += escapeSubstitutions[escValue.text]; + } // else + if ( escValue.$ === 'sequence' ) { + // TODO: \u[4],\x[2],\0[3] + } + } + } + + value.text = text; + delete value.results; + } + + return { value, done }; + } +} + +class ShellConstructsPStratumImpl { + static states = [ + { + name: 'pipeline', + enter ({ node }) { + node.$ = 'pipeline'; + node.commands = []; + }, + exit ({ node }) { + console.log('!!!!!',this.stack_top.node) + if ( this.stack_top?.node?.$ === 'script' ) { + this.stack_top.node.statements.push(node); + } + if ( this.stack_top?.node?.$ === 'string' ) { + this.stack_top.node.components.push(node); + } + }, + next ({ value, lexer }) { + if ( value.$ === 'op.line-terminator' ) { + console.log('the stack??', this.stack) + this.pop(); + return; + } + if ( value.$ === 'op.close' ) { + if ( this.stack.length === 1 ) { + throw new Error('unexpected close'); + } + lexer.next(); + this.pop(); + return; + } + if ( value.$ === 'op.pipe' ) { + lexer.next(); + } + this.push('command'); + } + }, + { + name: 'command', + enter ({ node }) { + node.$ = 'command'; + node.tokens = []; + node.inputRedirects = []; + node.outputRedirects = []; + }, + next ({ value, lexer }) { + if ( value.$ === 'op.line-terminator' ) { + this.pop(); + return; + } + if ( value.$ === 'whitespace' ) { + lexer.next(); + return; + } + if ( value.$ === 'op.close' ) { + this.pop(); + return; + } + if ( value.$ === 'op.pipe' ) { + this.pop(); + return; + } + if ( value.$ === 'op.redirect' ) { + this.push('redirect', { direction: value.direction }); + lexer.next(); + return; + } + this.push('token'); + }, + exit ({ node }) { + this.stack_top.node.commands.push(node); + } + }, + { + name: 'redirect', + enter ({ node }) { + node.$ = 'redirect'; + node.tokens = []; + }, + exit ({ node }) { + const { direction } = node; + const arry = direction === 'in' ? + this.stack_top.node.inputRedirects : + this.stack_top.node.outputRedirects; + arry.push(node.tokens[0]); + }, + next ({ node, value, lexer }) { + if ( node.tokens.length === 1 ) { + this.pop(); + return; + } + if ( value.$ === 'whitespace' ) { + lexer.next(); + return; + } + if ( value.$ === 'op.close' ) { + throw new Error('unexpected close'); + } + this.push('token'); + } + }, + { + name: 'token', + enter ({ node }) { + node.$ = 'token'; + node.components = []; + }, + exit ({ node }) { + this.stack_top.node.tokens.push(node); + }, + next ({ value, lexer }) { + if ( value.$ === 'op.line-terminator' ) { + console.log('well, got here') + this.pop(); + return; + } + if ( value.$ === 'string.dquote' ) { + this.push('string', { quote: '"' }); + lexer.next(); + return; + } + if ( value.$ === 'string.squote' ) { + this.push('string', { quote: "'" }); + lexer.next(); + return; + } + if ( + value.$ === 'whitespace' || + value.$ === 'op.close' + ) { + this.pop(); + return; + } + this.push('string', { quote: null }); + } + }, + { + name: 'string', + enter ({ node }) { + node.$ = 'string'; + node.components = []; + }, + exit ({ node }) { + this.stack_top.node.components.push(...node.components); + }, + next ({ node, value, lexer }) { + console.log('WHAT THO', node) + if ( value.$ === 'op.line-terminator' && node.quote === null ) { + console.log('well, got here') + this.pop(); + return; + } + if ( value.$ === 'string.close' && node.quote !== null ) { + lexer.next(); + this.pop(); + return; + } + if ( + node.quote === null && ( + value.$ === 'whitespace' || + value.$ === 'op.close' + ) + ) { + this.pop(); + return; + } + if ( value.$ === 'op.cmd-subst' ) { + this.push('pipeline'); + lexer.next(); + return; + } + node.components.push(value); + lexer.next(); + } + }, + ]; + + constructor () { + this.states = this.constructor.states; + this.buffer = []; + this.stack = []; + this.done_ = false; + + this._init(); + } + + _init () { + this.push('pipeline'); + } + + get stack_top () { + return this.stack[this.stack.length - 1]; + } + + push (state_name, node) { + const state = this.states.find(s => s.name === state_name); + if ( ! node ) node = {}; + this.stack.push({ state, node }); + state.enter && state.enter.call(this, { node }); + } + + pop () { + const { state, node } = this.stack.pop(); + state.exit && state.exit.call(this, { node }); + } + + chstate (state) { + this.stack_top.state = state; + } + + next (api) { + if ( this.done_ ) return { done: true }; + + const lexer = api.delegate; + + console.log('THE NODE', this.stack[0].node); + // return { done: true, value: { $: 'test' } }; + + for ( let i=0 ; i < 500 ; i++ ) { + const { done, value } = lexer.look(); + + if ( done ) { + while ( this.stack.length > 1 ) { + this.pop(); + } + break; + } + + const { state, node } = this.stack_top; + console.log('value?', value, done) + console.log('state?', state.name); + + state.next.call(this, { lexer, value, node, state }); + + // if ( done ) break; + } + + console.log('THE NODE', this.stack[0]); + + this.done_ = true; + return { done: false, value: this.stack[0].node }; + } + + // old method; not used anymore + consolidateTokens (tokens) { + const types = tokens.map(token => token.$); + + if ( tokens.length === 0 ) { + throw new Error('expected some tokens'); + } + + if ( types.includes('op.pipe') ) { + const components = + splitTokens(tokens, t => t.$ === 'op.pipe') + .map(tokens => this.consolidateTokens(tokens)); + + return { $: 'pipeline', components }; + } + + // const command = tokens.shift(); + const args = []; + const outputRedirects = []; + const inputRedirects = []; + + const states = { + STATE_NORMAL: {}, + STATE_REDIRECT: { + direction: null + }, + }; + const stack = []; + let dest = args; + let state = states.STATE_NORMAL; + for ( const token of tokens ) { + if ( state === states.STATE_REDIRECT ) { + const arry = state.direction === 'out' ? + outputRedirects : inputRedirects; + arry.push({ + // TODO: get string value only + path: token, + }) + state = states.STATE_NORMAL; + continue; + } + if ( token.$ === 'op.redirect' ) { + state = states.STATE_REDIRECT; + state.direction = token.direction; + continue; + } + if ( token.$ === 'op.cmd-subst' ) { + const new_dest = []; + dest = new_dest; + stack.push({ + $: 'command-substitution', + tokens: new_dest, + }); + continue; + } + if ( token.$ === 'op.close' ) { + const sub = stack.pop(); + dest = stack.length === 0 ? args : stack[stack.length-1].tokens; + const cmd_node = this.consolidateTokens(sub.tokens); + dest.push(cmd_node); + continue; + } + dest.push(token); + } + + const command = args.shift(); + + return { + $: 'command', + command, + args, + inputRedirects, + outputRedirects, + }; + } +} + +class MultilinePStratumImpl extends ShellConstructsPStratumImpl { + static states = [ + { + name: 'script', + enter ({ node }) { + node.$ = 'script'; + node.statements = []; + }, + next ({ value, lexer }) { + if ( value.$ === 'op.line-terminator' ) { + lexer.next(); + return; + } + + this.push('pipeline'); + } + }, + ...ShellConstructsPStratumImpl.states, + ]; + + _init () { + this.push('script'); + } +} + +export const buildParserSecondHalf = (sp, { multiline } = {}) => { + const parserFactory = new ParserFactory(); + const parserRegistry = StrataParseFacade.getDefaultParserRegistry(); + + const parserBuilder = new ParserBuilder( + parserFactory, + parserRegistry, + ); + + // sp.add(new ReducePrimitivesPStratumImpl()); + if ( multiline ) { + console.log('USING MULTILINE'); + sp.add(new MultilinePStratumImpl()); + } else { + sp.add(new ShellConstructsPStratumImpl()); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/pipeline/Coupler.js b/packages/phoenix/src/ansi-shell/pipeline/Coupler.js new file mode 100644 index 00000000..43ffe423 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/pipeline/Coupler.js @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class Coupler { + static description = ` + Connects a read stream to a write stream. + Does not close the write stream when the read stream is closed. + ` + + constructor (source, target) { + this.source = source; + this.target = target; + this.on_ = true; + this.isDone = new Promise(rslv => { + this.resolveIsDone = rslv; + }) + this.listenLoop_(); + } + + off () { this.on_ = false; } + on () { this.on_ = true; } + + async listenLoop_ () { + this.active = true; + for (;;) { + const { value, done } = await this.source.read(); + if ( done ) { + this.source = null; + this.target = null; + this.active = false; + this.resolveIsDone(); + break; + } + if ( this.on_ ) { + await this.target.write(value); + } + } + } +} diff --git a/packages/phoenix/src/ansi-shell/pipeline/Pipe.js b/packages/phoenix/src/ansi-shell/pipeline/Pipe.js new file mode 100644 index 00000000..59c9c46f --- /dev/null +++ b/packages/phoenix/src/ansi-shell/pipeline/Pipe.js @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class Pipe { + constructor () { + this.readableStream = new ReadableStream({ + start: controller => { + this.readController = controller; + }, + close: () => { + this.writableController.close(); + } + }); + this.writableStream = new WritableStream({ + start: controller => { + this.writableController = controller; + }, + write: item => { + this.readController.enqueue(item); + }, + close: () => { + this.readController.close(); + } + }); + this.in = this.writableStream.getWriter(); + this.out = this.readableStream.getReader(); + } +} diff --git a/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js b/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js new file mode 100644 index 00000000..2ad93864 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { SyncLinesReader } from "../ioutil/SyncLinesReader.js"; +import { TOKENS } from "../readline/readtoken.js"; +import { ByteWriter } from "../ioutil/ByteWriter.js"; +import { Coupler } from "./Coupler.js"; +import { CommandStdinDecorator } from "./iowrappers.js"; +import { Pipe } from "./Pipe.js"; +import { MemReader } from "../ioutil/MemReader.js"; +import { MemWriter } from "../ioutil/MemWriter.js"; +import { MultiWriter } from "../ioutil/MultiWriter.js"; +import { NullifyWriter } from "../ioutil/NullifyWriter.js"; +import { ConcreteSyntaxError } from "../ConcreteSyntaxError.js"; +import { SignalReader } from "../ioutil/SignalReader.js"; +import { Exit } from "../../puter-shell/coreutils/coreutil_lib/exit.js"; +import { resolveRelativePath } from '../../util/path.js'; +import { printUsage } from '../../puter-shell/coreutils/coreutil_lib/help.js'; + +class Token { + static createFromAST (ctx, ast) { + if ( ast.$ !== 'token' ) { + throw new Error('expected token node'); + } + + console.log('ast has cst?', + ast, + ast.components?.[0]?.$cst + ) + + return new Token(ast); + } + constructor (ast) { + this.ast = ast; + this.$cst = ast.components?.[0]?.$cst; + } + maybeStaticallyResolve (ctx) { + // If the only components are of type 'symbol' and 'string.segment' + // then we can statically resolve the value of the token. + + console.log('checking viability of static resolve', this.ast) + + const isStatic = this.ast.components.every(c => { + return c.$ === 'symbol' || c.$ === 'string.segment'; + }); + + if ( ! isStatic ) return; + + console.log('doing static thing', this.ast) + + // TODO: Variables can also be statically resolved, I think... + let value = ''; + for ( const component of this.ast.components ) { + console.log('component', component); + value += component.text; + } + + return value; + } + + async resolve (ctx) { + let value = ''; + for ( const component of this.ast.components ) { + if ( component.$ === 'string.segment' || component.$ === 'symbol' ) { + value += component.text; + continue; + } + if ( component.$ === 'pipeline' ) { + const pipeline = await Pipeline.createFromAST(ctx, component); + const memWriter = new MemWriter(); + const cmdCtx = { externs: { out: memWriter } } + const subCtx = ctx.sub(cmdCtx); + await pipeline.execute(subCtx); + value += memWriter.getAsString().trimEnd(); + continue; + } + } + // const name_subst = await PreparedCommand.createFromAST(this.ctx, command); + // const memWriter = new MemWriter(); + // const cmdCtx = { externs: { out: memWriter } } + // const ctx = this.ctx.sub(cmdCtx); + // name_subst.setContext(ctx); + // await name_subst.execute(); + // const cmd = memWriter.getAsString().trimEnd(); + return value; + } +} + +export class PreparedCommand { + static async createFromAST (ctx, ast) { + if ( ast.$ !== 'command' ) { + throw new Error('expected command node'); + } + + ast = { ...ast }; + const command_token = Token.createFromAST(ctx, ast.tokens.shift()); + + + // TODO: check that node for command name is of a + // supported type - maybe use adapt pattern + console.log('ast?', ast); + const cmd = command_token.maybeStaticallyResolve(ctx); + + const { commands } = ctx.registries; + const { commandProvider } = ctx.externs; + + const command = cmd + ? await commandProvider.lookup(cmd, { ctx }) + : command_token; + + if ( command === undefined ) { + console.log('command token?', command_token); + throw new ConcreteSyntaxError( + `no command: ${JSON.stringify(cmd)}`, + command_token.$cst, + ); + throw new Error('no command: ' + JSON.stringify(cmd)); + } + + // TODO: test this + console.log('ast?', ast); + const inputRedirect = ast.inputRedirects.length > 0 ? (() => { + const token = Token.createFromAST(ctx, ast.inputRedirects[0]); + return token.maybeStaticallyResolve(ctx) ?? token; + })() : null; + // TODO: test this + const outputRedirects = ast.outputRedirects.map(rdirNode => { + const token = Token.createFromAST(ctx, rdirNode); + return token.maybeStaticallyResolve(ctx) ?? token; + }); + + return new PreparedCommand({ + command, + args: ast.tokens.map(node => Token.createFromAST(ctx, node)), + // args: ast.args.map(node => node.text), + inputRedirect, + outputRedirects, + }); + } + + constructor ({ command, args, inputRedirect, outputRedirects }) { + this.command = command; + this.args = args; + this.inputRedirect = inputRedirect; + this.outputRedirects = outputRedirects; + } + + setContext (ctx) { + this.ctx = ctx; + } + + async execute () { + let { command, args } = this; + + // If we have an AST node of type `command` it means we + // need to run that command to get the name of the + // command to run. + if ( command instanceof Token ) { + const cmd = await command.resolve(this.ctx); + console.log('RUNNING CMD?', cmd) + const { commandProvider } = this.ctx.externs; + command = await commandProvider.lookup(cmd, { ctx: this.ctx }); + if ( command === undefined ) { + throw new Error('no command: ' + JSON.stringify(cmd)); + } + } + + args = await Promise.all(args.map(async node => { + if ( node instanceof Token ) { + return await node.resolve(this.ctx); + } + + return node.text; + })); + + const { argparsers } = this.ctx.registries; + const { decorators } = this.ctx.registries; + + let in_ = this.ctx.externs.in_; + if ( this.inputRedirect ) { + const { filesystem } = this.ctx.platform; + const dest_path = this.inputRedirect instanceof Token + ? await this.inputRedirect.resolve(this.ctx) + : this.inputRedirect; + const response = await filesystem.read( + resolveRelativePath(this.ctx.vars, dest_path)); + in_ = new MemReader(response); + } + + // simple naive implementation for now + const sig = { + listeners_: [], + emit (signal) { + for ( const listener of this.listeners_ ) { + listener(signal); + } + }, + on (listener) { + this.listeners_.push(listener); + } + }; + + in_ = new SignalReader({ delegate: in_, sig }); + + if ( command.input?.syncLines ) { + in_ = new SyncLinesReader({ delegate: in_ }); + } + in_ = new CommandStdinDecorator(in_); + + let out = this.ctx.externs.out; + const outputMemWriters = []; + if ( this.outputRedirects.length > 0 ) { + for ( let i=0 ; i < this.outputRedirects.length ; i++ ) { + outputMemWriters.push(new MemWriter()); + } + out = new NullifyWriter({ delegate: out }); + out = new MultiWriter({ + delegates: [...outputMemWriters, out], + }); + } + + const ctx = this.ctx.sub({ + externs: { + in_, + out, + sig, + }, + cmdExecState: { + valid: true, + printHelpAndExit: false, + }, + locals: { + command, + args, + outputIsRedirected: this.outputRedirects.length > 0, + } + }); + + if ( command.args ) { + const argProcessorId = command.args.$; + const argProcessor = argparsers[argProcessorId]; + const spec = { ...command.args }; + delete spec.$; + await argProcessor.process(ctx, spec); + } + + if ( ! ctx.cmdExecState.valid ) { + ctx.locals.exit = -1; + await ctx.externs.out.close(); + return; + } + + if ( ctx.cmdExecState.printHelpAndExit ) { + ctx.locals.exit = 0; + await printUsage(command, ctx.externs.out, ctx.vars); + await ctx.externs.out.close(); + return; + } + + let execute = command.execute.bind(command); + if ( command.decorators ) { + for ( const decoratorId in command.decorators ) { + const params = command.decorators[decoratorId]; + const decorator = decorators[decoratorId]; + execute = decorator.decorate(execute, { + command, params, ctx + }); + } + } + + // FIXME: This is really sketchy... + // `await execute(ctx);` should automatically throw any promise rejections, + // but for some reason Node crashes first, unless we set this handler, + // EVEN IF IT DOES NOTHING. I also can't find a place to safely remove it, + // so apologies if it makes debugging promises harder. + if (ctx.platform.name === 'node') { + const rejectionCatcher = (reason, promise) => { + }; + process.on('unhandledRejection', rejectionCatcher); + } + + let exit_code = 0; + try { + await execute(ctx); + } catch (e) { + if ( e instanceof Exit ) { + exit_code = e.code; + } else if ( e.code ) { + await ctx.externs.err.write( + '\x1B[31;1m' + + command.name + ': ' + + e.message + '\x1B[0m\n' + ); + } else { + await ctx.externs.err.write( + '\x1B[31;1m' + + command.name + ': ' + + e.toString() + '\x1B[0m\n' + ); + ctx.locals.exit = -1; + } + } + + // ctx.externs.in?.close?.(); + // ctx.externs.out?.close?.(); + await ctx.externs.out.close(); + + // TODO: need write command from puter-shell before this can be done + for ( let i=0 ; i < this.outputRedirects.length ; i++ ) { + console.log('output redirect??', this.outputRedirects[i]); + const { filesystem } = this.ctx.platform; + const outputRedirect = this.outputRedirects[i]; + const dest_path = outputRedirect instanceof Token + ? await outputRedirect.resolve(this.ctx) + : outputRedirect; + const path = resolveRelativePath(ctx.vars, dest_path); + console.log('it should work?', { + path, + outputMemWriters, + }) + // TODO: error handling here + + await filesystem.write(path, outputMemWriters[i].getAsBlob()); + } + + console.log('OUTPUT WRITERS', outputMemWriters); + } +} + +export class Pipeline { + static async createFromAST (ctx, ast) { + if ( ast.$ !== 'pipeline' ) { + throw new Error('expected pipeline node'); + } + + const preparedCommands = []; + + for ( const cmdNode of ast.commands ) { + const command = await PreparedCommand.createFromAST(ctx, cmdNode); + preparedCommands.push(command); + } + + return new Pipeline({ preparedCommands }); + } + constructor ({ preparedCommands }) { + this.preparedCommands = preparedCommands; + } + async execute (ctx) { + const preparedCommands = this.preparedCommands; + + let nextIn = ctx.externs.in; + let lastPipe = null; + + // TOOD: this will eventually defer piping of certain + // sub-pipelines to the Puter Shell. + + for ( let i=0 ; i < preparedCommands.length ; i++ ) { + const command = preparedCommands[i]; + + // if ( command.command.input?.syncLines ) { + // nextIn = new SyncLinesReader({ delegate: nextIn }); + // } + + const cmdCtx = { externs: { in_: nextIn } }; + + const pipe = new Pipe(); + lastPipe = pipe; + let cmdOut = pipe.in; + cmdOut = new ByteWriter({ delegate: cmdOut }); + cmdCtx.externs.out = cmdOut; + cmdCtx.externs.commandProvider = ctx.externs.commandProvider; + nextIn = pipe.out; + + // TODO: need to consider redirect from out to err + cmdCtx.externs.err = ctx.externs.out; + command.setContext(ctx.sub(cmdCtx)); + } + + + const coupler = new Coupler(lastPipe.out, ctx.externs.out); + + const commandPromises = []; + for ( let i = preparedCommands.length - 1 ; i >= 0 ; i-- ) { + const command = preparedCommands[i]; + commandPromises.push(command.execute()); + } + await Promise.all(commandPromises); + console.log('PIPELINE DONE'); + + await coupler.isDone; + } +} \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/pipeline/iowrappers.js b/packages/phoenix/src/ansi-shell/pipeline/iowrappers.js new file mode 100644 index 00000000..fc4cf49c --- /dev/null +++ b/packages/phoenix/src/ansi-shell/pipeline/iowrappers.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class CommandStdinDecorator { + constructor (rs) { + this.rs = rs; + } + async read (...a) { + return await this.rs.read(...a); + } + + // utility methods + async collect () { + const items = []; + for (;;) { + const { value, done } = await this.rs.read(); + if ( done ) return items; + items.push(value); + } + } +} + +export class CommandStdoutDecorator { + constructor (delegate) { + this.delegate = delegate; + } + async write (...a) { + return await this.delegate.write(...a); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/readline/history.js b/packages/phoenix/src/ansi-shell/readline/history.js new file mode 100644 index 00000000..f04b3a36 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/readline/history.js @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class HistoryManager { + constructor({ enableLogging = false } = {}) { + this.items = []; + this.index_ = 0; + this.listeners_ = {}; + this.enableLogging_ = enableLogging; + } + + log(...a) { + // TODO: Command line option for configuring logging + if ( this.enableLogging_ ) { + console.log('[HistoryManager]', ...a); + } + } + + get index() { + return this.index_; + } + + set index(v) { + this.log('setting index', v); + this.index_ = v; + } + + get() { + return this.items[this.index]; + } + + // Save, overwriting the current history item + save(data, { opt_debug } = {}) { + this.log('saving', data, 'at', this.index, + ...(opt_debug ? [ 'from', opt_debug ] : [])); + this.items[this.index] = data; + + if (this.listeners_.hasOwnProperty('add')) { + for (const listener of this.listeners_.add) { + listener(data); + } + } + } + + append(data) { + if ( + this.items.length !== 0 && + this.index !== this.items.length + ) { + this.log('POP'); + // remove last item + this.items.pop(); + } + this.index = this.items.length; + this.save(data, { opt_debug: 'append' }); + this.index++; + } + + on(topic, listener) { + if (!this.listeners_.hasOwnProperty(topic)) { + this.listeners_[topic] = []; + } + this.listeners_[topic].push(listener); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/readline/readline.js b/packages/phoenix/src/ansi-shell/readline/readline.js new file mode 100644 index 00000000..e7f816d2 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/readline/readline.js @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Context } from '../../context/context.js'; +import { CommandCompleter } from '../../puter-shell/completers/command_completer.js'; +import { FileCompleter } from '../../puter-shell/completers/file_completer.js'; +import { OptionCompleter } from '../../puter-shell/completers/option_completer.js'; +import { Uint8List } from '../../util/bytes.js'; +import { StatefulProcessorBuilder } from '../../util/statemachine.js'; +import { ANSIContext } from '../ANSIContext.js'; +import { readline_comprehend } from './rl_comprehend.js'; +import { CSI_HANDLERS } from './rl_csi_handlers.js'; +import { HistoryManager } from './history.js'; + +const decoder = new TextDecoder(); + +const cc = chr => chr.charCodeAt(0); + +const ReadlineProcessorBuilder = builder => builder + // TODO: import these constants from a package + .installContext(ANSIContext) + .installContext(new Context({ + variables: { + result: { value: '' }, + cursor: { value: 0 }, + }, + // TODO: dormant configuration; waiting on ContextSignature + imports: { + out: {}, + in_: {}, + history: {} + } + })) + .variable('result', { getDefaultValue: () => '' }) + .variable('cursor', { getDefaultValue: () => 0 }) + .external('out', { required: true }) + .external('in_', { required: true }) + .external('history', { required: true }) + .external('prompt', { required: true }) + .external('commandCtx', { required: true }) + .beforeAll('get-byte', async ctx => { + const { locals, externs } = ctx; + + const byteBuffer = new Uint8Array(1); + await externs.in_.read(byteBuffer); + locals.byteBuffer = byteBuffer; + locals.byte = byteBuffer[0]; + }) + .state('start', async ctx => { + const { consts, vars, externs, locals } = ctx; + + if ( locals.byte === consts.CHAR_LF || locals.byte === consts.CHAR_CR ) { + externs.out.write('\n'); + ctx.setState('end'); + return; + } + + if ( locals.byte === consts.CHAR_ETX ) { + externs.out.write('^C\n'); + // Exit if input line is empty + // FIXME: Check for 'process' is so we only do this on Node. How should we handle exiting in Puter terminal? + if ( typeof process !== 'undefined' && ctx.vars.result.length === 0 ) { + process.exit(1); + return; + } + // Otherwise clear it + ctx.vars.result = ''; + ctx.setState('end'); + return; + } + + if ( locals.byte === consts.CHAR_EOT ) { + externs.out.write('^D\n'); + ctx.vars.result = ''; + ctx.setState('end'); + return; + } + + if ( locals.byte === consts.CHAR_FF ) { + externs.out.write('\x1B[H\x1B[2J'); + externs.out.write(externs.prompt); + externs.out.write(vars.result); + const invCurPos = vars.result.length - vars.cursor; + console.log(invCurPos) + if ( invCurPos !== 0 ) { + externs.out.write(`\x1B[${invCurPos}D`); + } + return; + } + + if ( locals.byte === consts.CHAR_TAB ) { + const inputState = readline_comprehend(ctx.sub({ + params: { + input: vars.result, + cursor: vars.cursor + } + })); + // NEXT: get tab completer for input state + console.log('input state', inputState); + + let completer = null; + if ( inputState.$ === 'redirect' ) { + completer = new FileCompleter(); + } + + if ( inputState.$ === 'command' ) { + if ( inputState.tokens.length === 1 ) { + // Match first token against command names + completer = new CommandCompleter(); + } else if ( inputState.input.startsWith('--') ) { + // Match `--*` against option names, if they exist + completer = new OptionCompleter(); + } else { + // Match everything else against file names + completer = new FileCompleter(); + } + } + + if ( completer === null ) return; + + const completions = await completer.getCompletions( + externs.commandCtx, + inputState, + ); + + const applyCompletion = txt => { + const p1 = vars.result.slice(0, vars.cursor); + const p2 = vars.result.slice(vars.cursor); + console.log({ p1, p2 }); + vars.result = p1 + txt + p2; + vars.cursor += txt.length; + externs.out.write(txt); + }; + + if ( completions.length === 0 ) return; + + if ( completions.length === 1 ) { + applyCompletion(completions[0]); + } + + if ( completions.length > 1 ) { + let inCommon = ''; + for ( let i=0 ; true ; i++ ) { + if ( ! completions.every(completion => { + return completion.length > i; + }) ) break; + + let matches = true; + + const chrFirst = completions[0][i]; + for ( let ci=1 ; ci < completions.length ; ci++ ) { + const chrOther = completions[ci][i]; + if ( chrFirst !== chrOther ) { + matches = false; + break; + } + } + + if ( ! matches ) break; + inCommon += chrFirst; + } + + if ( inCommon.length > 0 ) { + applyCompletion(inCommon); + } + } + return; + } + + if ( locals.byte === consts.CHAR_ESC ) { + ctx.setState('ESC'); + return; + } + + // (note): DEL is actually the backspace key + // [explained here](https://en.wikipedia.org/wiki/Backspace#Common_use) + // TOOD: very similar to delete in CSI_HANDLERS; how can this be unified? + if ( locals.byte === consts.CHAR_DEL ) { + // can't backspace at beginning of line + if ( vars.cursor === 0 ) return; + + vars.result = vars.result.slice(0, vars.cursor - 1) + + vars.result.slice(vars.cursor) + + vars.cursor--; + + // TODO: maybe wrap these CSI codes in a library + const backspaceSequence = new Uint8Array([ + // consts.CHAR_ESC, consts.CHAR_CSI, cc('s'), // save cur + consts.CHAR_ESC, consts.CHAR_CSI, cc('D'), // left + consts.CHAR_ESC, consts.CHAR_CSI, cc('P'), + // consts.CHAR_ESC, consts.CHAR_CSI, cc('u'), // restore cur + // consts.CHAR_ESC, consts.CHAR_CSI, cc('D'), // left + ]); + + externs.out.write(backspaceSequence); + return; + } + + const part = decoder.decode(locals.byteBuffer); + + if ( vars.cursor === vars.result.length ) { + // output + externs.out.write(locals.byteBuffer); + // update buffer + vars.result = vars.result + part; + // update cursor + vars.cursor += part.length; + } else { + // output + const insertSequence = new Uint8Array([ + consts.CHAR_ESC, + consts.CHAR_CSI, + '@'.charCodeAt(0), + ...locals.byteBuffer + ]); + externs.out.write(insertSequence); + // update buffer + vars.result = + vars.result.slice(0, vars.cursor) + + part + + vars.result.slice(vars.cursor) + // update cursor + vars.cursor += part.length; + } + }) + .onTransitionTo('ESC-CSI', async ctx => { + ctx.vars.controlSequence = new Uint8List(); + }) + .state('ESC', async ctx => { + const { consts, vars, externs, locals } = ctx; + + if ( locals.byte === consts.CHAR_ESC ) { + externs.out.write(consts.CHAR_ESC); + ctx.setState('start'); + return; + } + + if ( locals.byte === ctx.consts.CHAR_CSI ) { + ctx.setState('ESC-CSI'); + return; + } + if ( locals.byte === ctx.consts.CHAR_OSC ) { + ctx.setState('ESC-OSC'); + return; + } + }) + .state('ESC-CSI', async ctx => { + const { consts, locals, vars } = ctx; + + if ( + locals.byte >= consts.CSI_F_0 && + locals.byte < consts.CSI_F_E + ) { + ctx.trigger('ESC-CSI.post'); + ctx.setState('start'); + return; + } + + vars.controlSequence.append(locals.byte); + }) + .state('ESC-OSC', async ctx => { + const { consts, locals, vars } = ctx; + + // TODO: ESC\ can also end an OSC sequence according + // to sources, but this has not been implemented + // because it would add another state. + // This should be implemented when there's a + // simpler solution ("peek" & "scan" functionality) + if ( + locals.byte === 0x07 + ) { + // ctx.trigger('ESC-OSC.post'); + ctx.setState('start'); + return; + } + + vars.controlSequence.append(locals.byte); + }) + .action('ESC-CSI.post', async ctx => { + const { vars, externs, locals } = ctx; + + const finalByte = locals.byte; + const controlSequence = vars.controlSequence.toArray(); + + // Log.log('controlSequence', finalByte, controlSequence); + + if ( ! CSI_HANDLERS.hasOwnProperty(finalByte) ) { + return; + } + + ctx.locals.controlSequence = controlSequence; + ctx.locals.doWrite = false; + CSI_HANDLERS[finalByte](ctx); + + if ( ctx.locals.doWrite ) { + externs.out.write(new Uint8Array([ + ctx.consts.CHAR_ESC, + ctx.consts.CHAR_CSI, + ...controlSequence, + finalByte + ])) + } + }) + .build(); + +const ReadlineProcessor = ReadlineProcessorBuilder( + new StatefulProcessorBuilder() +); + +class Readline { + constructor (params) { + this.internal_ = {}; + for ( const k in params ) this.internal_[k] = params[k]; + + this.history = new HistoryManager(); + } + + async readline (prompt, commandCtx) { + const out = this.internal_.out; + const in_ = this.internal_.in; + + await out.write(prompt); + + const { + result + } = await ReadlineProcessor.run({ + prompt, + out, in_, + history: this.history, + commandCtx, + }); + + if ( result.trim() !== '' ) { + this.history.append(result); + } + + return result; + } +} + +export default class ReadlineLib { + static create(params) { + const rl = new Readline(params); + return rl; + } +} diff --git a/packages/phoenix/src/ansi-shell/readline/readtoken.js b/packages/phoenix/src/ansi-shell/readline/readtoken.js new file mode 100644 index 00000000..63fbc138 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/readline/readtoken.js @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +// [reference impl](https://github.com/brgl/busybox/blob/master/shell/ash.c) + +const list_ws = [' ', '\n', '\t']; +const list_recorded_tokens = [ + '|','>','<','&',';','(',')', +]; +const list_stoptoken = [ + '|','>','<','&','\\','#',';','(',')', + ...list_ws +]; + +export const TOKENS = {}; +for ( const k of list_recorded_tokens ) { + TOKENS[k] = {}; +} + +export const readtoken = str => { + let state = null; + let buffer = ''; + let quoteType = ''; + const tokens = []; + + const actions = { + endToken: () => { + tokens.push(buffer); + buffer = ''; + } + }; + + const states = { + start: i => { + if ( list_ws.includes(str[i]) ) { + return; + } + if ( str[i] === '#' ) return str.length; + if ( list_recorded_tokens.includes(str[i]) ) { + tokens.push(TOKENS[str[i]]); + return; + } + if ( str[i] === '"' || str[i] === "'" ) { + state = states.quote; + quoteType = str[i]; + return; + } + state = states.text; + return i; // prevent increment + }, + text: i => { + if ( str[i] === '"' || str[i] === "'" ) { + state = states.quote; + quoteType = str[i]; + return; + } + if ( list_stoptoken.includes(str[i]) ) { + state = states.start; + actions.endToken(); + return i; // prevent increment + } + buffer += str[i]; + }, + quote: i => { + if ( str[i] === '\\' ) { + state = states.quote_esc; + return; + } + if ( str[i] === quoteType ) { + state = states.text; + return; + } + buffer += str[i]; + }, + quote_esc: i => { + if ( str[i] !== quoteType ) { + buffer += '\\'; + } + buffer += str[i]; + state = states.quote; + } + }; + state = states.start; + for ( let i=0 ; i < str.length ; ) { + let newI = state(i); + i = newI !== undefined ? newI : i+1; + } + + if ( buffer !== '' ) actions.endToken(); + + return tokens; +}; \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/readline/rl_comprehend.js b/packages/phoenix/src/ansi-shell/readline/rl_comprehend.js new file mode 100644 index 00000000..12ab9a23 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/readline/rl_comprehend.js @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +// This function comprehends the readline input and returns something +// called a "readline input state" - this includes any information needed + +import { readtoken, TOKENS } from "./readtoken.js"; + +// TODO: update to use syntax parser + +// REMINDER: input state will be sent to readline first, +// then readline will use the input state to determine +// what component to ask for tab completion + +// to perform autocomplete functions +export const readline_comprehend = (ctx) => { + const { input, cursor } = ctx.params; + + // TODO: CST for input tokens might be a good idea + // for now, tokens up to the current cursor position + // will be considered. + + const relevantInput = input.slice(0, cursor); + + const endsWithWhitespace = (() => { + const lastChar = relevantInput[relevantInput.length - 1]; + return lastChar === ' ' || + lastChar === '\t' || + lastChar === '\r' || + lastChar === '\n' + })(); + + let tokens = readtoken(relevantInput); + let tokensStart = 0; + + // We now go backwards through the tokens, looking for: + // - a redirect token immediately to the left + // - a pipe token to the left + + if ( tokens.length === 0 ) return { $: 'empty' }; + + // Remove tokens for previous commands + for ( let i=tokens.length ; i >= 0 ; i-- ) { + const token = tokens[i]; + const isCommandSeparator = + token === TOKENS['|'] || + token === TOKENS[';'] ; + if ( isCommandSeparator ) { + tokens = tokens.slice(i + 1); + break; + } + } + + // Check if current input is for a redirect operator + const resultIfRedirectOperator = (() => { + if ( tokens.length < 1 ) return; + + const lastToken = tokens[tokens.length - 1]; + if ( + lastToken === TOKENS['<'] || + lastToken === TOKENS['>'] + ) { + return { + $: 'redirect' + }; + } + + if ( tokens.length < 2 ) return; + if ( endsWithWhitespace ) return; + + const secondFromLastToken = tokens[tokens.length - 2]; + if ( + secondFromLastToken === TOKENS['<'] || + secondFromLastToken === TOKENS['>'] + ) { + return { + $: 'redirect', + input: lastToken + }; + } + + })(); + + if ( resultIfRedirectOperator ) return resultIfRedirectOperator; + + if ( tokens.length === 0 ) { + return { $: 'empty' }; + } + + // If the first token is not a command name, then + // this input is not considered comprehensible + if ( typeof tokens[0] !== 'string' ) { + return { + $: 'unrecognized' + }; + } + + // DRY: command arguments are parsed by readline + const argTokens = []; + for ( let i=0 ; i < tokens.length ; i++ ) { + if ( + tokens[i] === TOKENS['<'] || + tokens[i] === TOKENS['>'] + ) { + // skip this token and the next one + i++; continue; + } + + argTokens.push(tokens[i]); + } + + return { + $: 'command', + id: tokens[0], + tokens: argTokens, + input: endsWithWhitespace ? + '' : argTokens[argTokens.length - 1], + endsWithWhitespace, + }; +}; diff --git a/packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js b/packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js new file mode 100644 index 00000000..1128929b --- /dev/null +++ b/packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/* +## this source file +- maps: CSI (Control Sequence Introducer) sequences +- to: expected functionality in the context of readline + +## relevant articles +- [ECMA-48](https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf) +- [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code) +*/ + +import { ANSIContext, getActiveModifiersFromXTerm } from "../ANSIContext.js"; +import { findNextWord } from "./rl_words.js"; + +// TODO: potentially include metadata in handlers + +// --- util --- +const cc = chr => chr.charCodeAt(0); + +const CHAR_DEL = 127; +const CHAR_ESC = 0x1B; + +const { consts } = ANSIContext; + +// --- convenience function decorators --- +const CSI_INT_ARG = delegate => ctx => { + const controlSequence = ctx.locals.controlSequence; + + let str = new TextDecoder().decode(controlSequence); + + // Detection of modifier keys like ctrl and shift + if ( str.includes(';') ) { + const parts = str.split(';'); + str = parts[0]; + const modsStr = parts[parts.length - 1]; + let modN = Number.parseInt(modsStr); + const mods = getActiveModifiersFromXTerm(modN); + for ( const k in mods ) ctx.locals[k] = mods[k]; + } + + let num = str === '' ? 1 : Number.parseInt(str); + if ( Number.isNaN(num) ) num = 0; + + ctx.locals.num = num; + + return delegate(ctx); +}; + +// --- PC-Style Function Key handles (see `~` final byte in CSI_HANDLERS) --- +export const PC_FN_HANDLERS = { + // delete key + 3: ctx => { + const { vars } = ctx; + const deleteSequence = new Uint8Array([ + consts.CHAR_ESC, consts.CHAR_CSI, cc('P') + ]); + vars.result = vars.result.slice(0, vars.cursor) + + vars.result.slice(vars.cursor + 1); + ctx.externs.out.write(deleteSequence); + } +}; + +const save_history = ctx => { + const { history } = ctx.externs; + history.save(ctx.vars.result); +}; + +const correct_cursor = (ctx, oldCursor) => { + // TODO: make this work differently if oldCursor is not defined + + const amount = ctx.vars.cursor - oldCursor; + ctx.vars.cursor = ctx.vars.result.length; + const L = amount < 0 ? 'D' : 'C'; + if ( amount === 0 ) return; + const moveSequence = new Uint8Array([ + consts.CHAR_ESC, consts.CHAR_CSI, + ...(new TextEncoder().encode('' + Math.abs(amount))), + cc(L) + ]); + ctx.externs.out.write(moveSequence); +}; + +const home = ctx => { + const amount = ctx.vars.cursor; + ctx.vars.cursor = 0; + const moveSequence = new Uint8Array([ + consts.CHAR_ESC, consts.CHAR_CSI, + ...(new TextEncoder().encode('' + amount)), + cc('D') + ]); + if ( amount !== 0 ) ctx.externs.out.write(moveSequence); +}; + +const select_current_history = ctx => { + const { history } = ctx.externs; + home(ctx); + ctx.vars.result = history.get(); + ctx.vars.cursor = ctx.vars.result.length; + const clearToEndSequence = new Uint8Array([ + consts.CHAR_ESC, consts.CHAR_CSI, + ...(new TextEncoder().encode('0')), + cc('K') + ]); + ctx.externs.out.write(clearToEndSequence); + ctx.externs.out.write(history.get()); +}; + +// --- CSI handlers: this is the last definition in this file --- +export const CSI_HANDLERS = { + [cc('A')]: CSI_INT_ARG(ctx => { + save_history(ctx); + const { history } = ctx.externs; + + if ( history.index === 0 ) return; + + history.index--; + select_current_history(ctx); + }), + [cc('B')]: CSI_INT_ARG(ctx => { + save_history(ctx); + const { history } = ctx.externs; + + if ( history.index === history.items.length - 1 ) return; + + history.index++; + select_current_history(ctx); + }), + // cursor back + [cc('D')]: CSI_INT_ARG(ctx => { + if ( ctx.vars.cursor === 0 ) { + return; + } + if ( ctx.locals.ctrl ) { + // TODO: temporary inaccurate implementation + const txt = ctx.vars.result; + const ind = findNextWord(txt, ctx.vars.cursor, true); + const diff = ctx.vars.cursor - ind; + ctx.vars.cursor = ind; + const moveSequence = new Uint8Array([ + consts.CHAR_ESC, consts.CHAR_CSI, + ...(new TextEncoder().encode('' + diff)), + cc('D') + ]); + ctx.externs.out.write(moveSequence); + return; + } + ctx.vars.cursor -= ctx.locals.num; + ctx.locals.doWrite = true; + }), + // cursor forward + [cc('C')]: CSI_INT_ARG(ctx => { + if ( ctx.vars.cursor >= ctx.vars.result.length ) { + return; + } + if ( ctx.locals.ctrl ) { + // TODO: temporary inaccurate implementation + const txt = ctx.vars.result; + const ind = findNextWord(txt, ctx.vars.cursor); + const diff = ind - ctx.vars.cursor; + ctx.vars.cursor = ind; + const moveSequence = new Uint8Array([ + consts.CHAR_ESC, consts.CHAR_CSI, + ...(new TextEncoder().encode('' + diff)), + cc('C') + ]); + ctx.externs.out.write(moveSequence); + return; + } + ctx.vars.cursor += ctx.locals.num; + ctx.locals.doWrite = true; + }), + // PC-Style Function Keys + [cc('~')]: CSI_INT_ARG(ctx => { + if ( ! PC_FN_HANDLERS.hasOwnProperty(ctx.locals.num) ) { + console.error(`unrecognized PC Function: ${ctx.locals.num}`); + return; + } + PC_FN_HANDLERS[ctx.locals.num](ctx); + }), + // Home + [cc('H')]: ctx => { + home(ctx); + }, + // End + [cc('F')]: ctx => { + const amount = ctx.vars.result.length - ctx.vars.cursor; + ctx.vars.cursor = ctx.vars.result.length; + const moveSequence = new Uint8Array([ + consts.CHAR_ESC, consts.CHAR_CSI, + ...(new TextEncoder().encode('' + amount)), + cc('C') + ]); + if ( amount !== 0 ) ctx.externs.out.write(moveSequence); + }, +}; diff --git a/packages/phoenix/src/ansi-shell/readline/rl_words.js b/packages/phoenix/src/ansi-shell/readline/rl_words.js new file mode 100644 index 00000000..10469852 --- /dev/null +++ b/packages/phoenix/src/ansi-shell/readline/rl_words.js @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const findNextWord = (str, from, reverse) => { + let stage = 0; + let incr = reverse ? -1 : 1; + const cond = reverse ? i => i > 0 : i => i < str.length; + if ( reverse && from !== 0 ) from--; + for ( let i=from ; cond(i) ; i += incr ) { + if ( stage === 0 ) { + if ( str[i] !== ' ' ) stage++; + continue; + } + if ( stage === 1 ) { + if ( str[i] === ' ' ) return reverse ? i + 1 : i; + } + } + return reverse ? 0 : str.length; +} diff --git a/packages/phoenix/src/ansi-shell/signals.js b/packages/phoenix/src/ansi-shell/signals.js new file mode 100644 index 00000000..b81253fe --- /dev/null +++ b/packages/phoenix/src/ansi-shell/signals.js @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const signals = Object.freeze({ + SIGINT: 2, + SIGQUIT: 3, +}); diff --git a/packages/phoenix/src/context/context.js b/packages/phoenix/src/context/context.js new file mode 100644 index 00000000..aaf3868b --- /dev/null +++ b/packages/phoenix/src/context/context.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class AbstractContext { + get constants () { + return this.instance_.constants; + } + get consts () { + return this.constants; + } + get variables () { + return this.instance_.valuesAccessor; + } + get vars () { + return this.variables; + } +} + +// export class SubContext extends AbstractContext { +// constructor ({ parent, changes }) { +// for ( const k in parent.spec ) +// } +// } + +export class Context extends AbstractContext { + constructor (spec) { + super(); + this.spec = { ...spec }; + + this.instance_ = {}; + + if ( ! spec.constants ) spec.constants = {}; + + const constants = {}; + for ( const k in this.spec.constants ) { + Object.defineProperty(constants, k, { + value: this.spec.constants[k], + enumerable: true + }) + } + this.instance_.constants = constants; + + // const values = {}; + // for ( const k in this.spec.variables ) { + // Object.defineProperty(values, k, { + // value: this.spec.variables[k], + // enumerable: true, + // writable: true + // }); + // } + // this.instance_.values = values; + } +} diff --git a/packages/phoenix/src/main_cli.js b/packages/phoenix/src/main_cli.js new file mode 100644 index 00000000..9f3106c3 --- /dev/null +++ b/packages/phoenix/src/main_cli.js @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Context } from 'contextlink'; +import { launchPuterShell } from './puter-shell/main.js'; +import { NodeStdioPTT } from './pty/NodeStdioPTT.js'; +import { CreateFilesystemProvider } from './platform/node/filesystem.js'; +import { CreateEnvProvider } from './platform/node/env.js'; +import { parseArgs } from '@pkgjs/parseargs'; +import capcon from 'capture-console'; +import fs from 'fs'; + +const { values } = parseArgs({ + options: { + 'log': { + type: 'string', + } + }, + args: process.argv.slice(2), +}); +const logFile = await (async () => { + if (!values.log) + return; + return await fs.promises.open(values.log, 'w'); +})(); + + +// Capture console.foo() output and either send it to the log file, or to nowhere. +for (const [name, oldMethod] of Object.entries(console)) { + console[name] = async (...args) => { + let result; + const stdio = capcon.interceptStdio(() => { + result = oldMethod(...args); + }); + + if (logFile) { + await logFile.write(stdio.stdout); + await logFile.write(stdio.stderr); + } + + return result; + }; +} + +const ctx = new Context({ + ptt: new NodeStdioPTT(), + config: {}, + platform: new Context({ + name: 'node', + filesystem: CreateFilesystemProvider(), + env: CreateEnvProvider(), + }), +}); + +await launchPuterShell(ctx); diff --git a/packages/phoenix/src/main_puter.js b/packages/phoenix/src/main_puter.js new file mode 100644 index 00000000..a7449c45 --- /dev/null +++ b/packages/phoenix/src/main_puter.js @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Context } from 'contextlink'; +import { launchPuterShell } from './puter-shell/main.js'; +import { CreateFilesystemProvider } from './platform/puter/filesystem.js'; +import { CreateDriversProvider } from './platform/puter/drivers.js'; +import { XDocumentPTT } from './pty/XDocumentPTT.js'; +import { CreateEnvProvider } from './platform/puter/env.js'; + +window.main_shell = async () => { + const config = {}; + + let resolveConfigured = null; + const configured_ = new Promise(rslv => { + resolveConfigured = rslv; + }); + + const terminal = puter.ui.parentApp(); + if (!terminal) { + console.error('Phoenix cannot run without a parent Terminal. Exiting...'); + puter.exit(); + return; + } + terminal.on('message', message => { + if (message.$ === 'config') { + const configValues = { ...message }; + delete configValues.$; + for ( const k in configValues ) { + config[k] = configValues[k]; + } + resolveConfigured(); + } + }); + terminal.on('close', () => { + console.log('Terminal closed; exiting Phoenix...'); + puter.exit(); + }); + + // FIXME: on terminal close, close ourselves + + terminal.postMessage({ $: 'ready' }); + + await configured_; + + const puterSDK = globalThis.puter; + if ( config['puter.auth.token'] ) { + await puterSDK.setAuthToken(config['puter.auth.token']); + } + await puterSDK.setAPIOrigin(config['puter.api_origin']); + + const ptt = new XDocumentPTT(terminal); + await launchPuterShell(new Context({ + ptt, + config, puterSDK, + externs: new Context({ puterSDK }), + platform: new Context({ + name: 'puter', + filesystem: CreateFilesystemProvider({ puterSDK }), + drivers: CreateDriversProvider({ puterSDK }), + env: CreateEnvProvider({ config }), + }), + })); +}; diff --git a/packages/phoenix/src/meta/versions.js b/packages/phoenix/src/meta/versions.js new file mode 100644 index 00000000..cd83eacf --- /dev/null +++ b/packages/phoenix/src/meta/versions.js @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const SHELL_VERSIONS = [ + { + v: '0.2.4', + changes: [ + 'more completers for tab-completion', + 'help updates', + '"which" command added', + '"date" command added', + 'improvements when running under node.js', + ] + }, + { + v: '0.2.3', + changes: [ + '"printf" command added', + '"help" command updated', + '"errno" command added', + 'POSIX error code associations added', + ] + }, + { + v: '0.2.2', + changes: [ + 'wc works with BLOB inputs', + '"~" path resolution fixed', + '"head" command added', + '"tail" command updated', + '"ls" symlink support improved', + '"sort" command added', + 'Testing improved', + '"cd" with no arguments works', + 'Filesystem errors are more consistent', + '"help" output improved', + '"pwd" argument processing updated' + + ] + }, + { + v: '0.2.1', + changes: [ + 'commands: true, false', + 'commands: basename, dirname', + 'more node.js support', + 'wc command', + 'sleep command', + 'improved coreutils documentation', + 'updates to existing coreutils', + 'readline fixes', + ] + }, + { + v: '0.2.0', + changes: [ + 'brand change: Phoenix Shell', + 'open-sourced under AGPL-3.0', + 'new commands: ai, txt2img, jq, and more', + 'added login command', + 'coreutils updates', + 'added command substitution', + 'parser improvements', + ] + }, + { + v: '0.1.10', + changes: [ + 'new input parser', + 'add pwd command', + ] + }, + { + v: '0.1.9', + changes: [ + 'add help command', + 'add changelog command', + 'add ioctl messages for window size', + 'add env.ROWS and env.COLS', + ] + }, + { + v: '0.1.8', + changes: [ + 'add neofetch command', + 'add simple tab completion', + ] + }, + { + v: '0.1.7', + changes: [ + 'add clear and printenv', + ] + }, + { + v: '0.1.6', + changes: [ + 'add redirect syntax', + ], + }, + { + v: '0.1.5', + changes: [ + 'add cp command', + ], + }, + { + v: '0.1.4', + changes: [ + 'improve error handling', + ], + }, + { + v: '0.1.3', + changes: [ + 'fixes for existing commands', + 'mv added', + 'cat added', + 'readline history (transient) added', + ] + }, + { + v: '0.1.2', + changes: [ + 'add echo', + 'fix synchronization of pipe coupler', + ] + } +]; diff --git a/packages/phoenix/src/platform/PosixError.js b/packages/phoenix/src/platform/PosixError.js new file mode 100644 index 00000000..0fc136a5 --- /dev/null +++ b/packages/phoenix/src/platform/PosixError.js @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const ErrorCodes = { + EACCES: Symbol.for('EACCES'), + EADDRINUSE: Symbol.for('EADDRINUSE'), + ECONNREFUSED: Symbol.for('ECONNREFUSED'), + ECONNRESET: Symbol.for('ECONNRESET'), + EEXIST: Symbol.for('EEXIST'), + EFBIG: Symbol.for('EFBIG'), + EINVAL: Symbol.for('EINVAL'), + EIO: Symbol.for('EIO'), + EISDIR: Symbol.for('EISDIR'), + EMFILE: Symbol.for('EMFILE'), + ENOENT: Symbol.for('ENOENT'), + ENOSPC: Symbol.for('ENOSPC'), + ENOTDIR: Symbol.for('ENOTDIR'), + ENOTEMPTY: Symbol.for('ENOTEMPTY'), + EPERM: Symbol.for('EPERM'), + EPIPE: Symbol.for('EPIPE'), + ETIMEDOUT: Symbol.for('ETIMEDOUT'), +}; + +// Codes taken from `errno` on Linux. +export const ErrorMetadata = new Map([ + [ErrorCodes.EPERM, { code: 1, description: 'Operation not permitted' }], + [ErrorCodes.ENOENT, { code: 2, description: 'File or directory not found' }], + [ErrorCodes.EIO, { code: 5, description: 'IO error' }], + [ErrorCodes.EACCES, { code: 13, description: 'Permission denied' }], + [ErrorCodes.EEXIST, { code: 17, description: 'File already exists' }], + [ErrorCodes.ENOTDIR, { code: 20, description: 'Is not a directory' }], + [ErrorCodes.EISDIR, { code: 21, description: 'Is a directory' }], + [ErrorCodes.EINVAL, { code: 22, description: 'Argument invalid' }], + [ErrorCodes.EMFILE, { code: 24, description: 'Too many open files' }], + [ErrorCodes.EFBIG, { code: 27, description: 'File too big' }], + [ErrorCodes.ENOSPC, { code: 28, description: 'Device out of space' }], + [ErrorCodes.EPIPE, { code: 32, description: 'Pipe broken' }], + [ErrorCodes.ENOTEMPTY, { code: 39, description: 'Directory is not empty' }], + [ErrorCodes.EADDRINUSE, { code: 98, description: 'Address already in use' }], + [ErrorCodes.ECONNRESET, { code: 104, description: 'Connection reset'}], + [ErrorCodes.ETIMEDOUT, { code: 110, description: 'Connection timed out' }], + [ErrorCodes.ECONNREFUSED, { code: 111, description: 'Connection refused' }], +]); + +export const errorFromIntegerCode = (code) => { + for (const [errorCode, metadata] of ErrorMetadata) { + if (metadata.code === code) { + return errorCode; + } + } + return undefined; +}; + +export class PosixError extends Error { + // posixErrorCode can be either a string, or one of the ErrorCodes above. + // If message is undefined, a default message will be used. + constructor(posixErrorCode, message) { + let posixCode; + if (typeof posixErrorCode === 'symbol') { + if (ErrorCodes[Symbol.keyFor(posixErrorCode)] !== posixErrorCode) { + throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`); + } + posixCode = posixErrorCode; + } else { + const code = ErrorCodes[posixErrorCode]; + if (!code) throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`); + posixCode = code; + } + + super(message ?? ErrorMetadata.get(posixCode).description); + this.posixCode = posixCode; + } + + // + // Helpers for constructing a PosixError when you don't already have an error message. + // + static AccessNotPermitted({ message, path } = {}) { + return new PosixError(ErrorCodes.EACCES, message ?? (path ? `Access not permitted to: '${path}'` : undefined)); + } + static AddressInUse({ message, address } = {}) { + return new PosixError(ErrorCodes.EADDRINUSE, message ?? (address ? `Address '${address}' in use` : undefined)); + } + static ConnectionRefused({ message } = {}) { + return new PosixError(ErrorCodes.ECONNREFUSED, message); + } + static ConnectionReset({ message } = {}) { + return new PosixError(ErrorCodes.ECONNRESET, message); + } + static PathAlreadyExists({ message, path } = {}) { + return new PosixError(ErrorCodes.EEXIST, message ?? (path ? `Path already exists: '${path}'` : undefined)); + } + static FileTooLarge({ message } = {}) { + return new PosixError(ErrorCodes.EFBIG, message); + } + static InvalidArgument({ message } = {}) { + return new PosixError(ErrorCodes.EINVAL, message); + } + static IO({ message } = {}) { + return new PosixError(ErrorCodes.EIO, message); + } + static IsDirectory({ message, path } = {}) { + return new PosixError(ErrorCodes.EISDIR, message ?? (path ? `Path is directory: '${path}'` : undefined)); + } + static TooManyOpenFiles({ message } = {}) { + return new PosixError(ErrorCodes.EMFILE, message); + } + static DoesNotExist({ message, path } = {}) { + return new PosixError(ErrorCodes.ENOENT, message ?? (path ? `Path not found: '${path}'` : undefined)); + } + static NotEnoughSpace({ message } = {}) { + return new PosixError(ErrorCodes.ENOSPC, message); + } + static IsNotDirectory({ message, path } = {}) { + return new PosixError(ErrorCodes.ENOTDIR, message ?? (path ? `Path is not a directory: '${path}'` : undefined)); + } + static DirectoryIsNotEmpty({ message, path } = {}) { + return new PosixError(ErrorCodes.ENOTEMPTY, message ?? (path ?`Directory is not empty: '${path}'` : undefined)); + } + static OperationNotPermitted({ message } = {}) { + return new PosixError(ErrorCodes.EPERM, message); + } + static BrokenPipe({ message } = {}) { + return new PosixError(ErrorCodes.EPIPE, message); + } + static TimedOut({ message } = {}) { + return new PosixError(ErrorCodes.ETIMEDOUT, message); + } +} diff --git a/packages/phoenix/src/platform/node/env.js b/packages/phoenix/src/platform/node/env.js new file mode 100644 index 00000000..58f31614 --- /dev/null +++ b/packages/phoenix/src/platform/node/env.js @@ -0,0 +1,20 @@ +import os from 'os'; + +export const CreateEnvProvider = () => { + return { + getEnv: () => { + let env = process.env; + if ( ! env.PS1 ) { + env.PS1 = `[\\u@\\h \\w]\\$ `; + } + if ( ! env.HOSTNAME ) { + env.HOSTNAME = os.hostname(); + } + return env; + }, + + get (k) { + return this.getEnv()[k]; + } + } +} diff --git a/packages/phoenix/src/platform/node/filesystem.js b/packages/phoenix/src/platform/node/filesystem.js new file mode 100644 index 00000000..85b9e84c --- /dev/null +++ b/packages/phoenix/src/platform/node/filesystem.js @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import fs from 'fs'; +import path_ from 'path'; + +import modeString from 'fs-mode-to-string'; +import { ErrorCodes, PosixError } from '../PosixError.js'; + +function convertNodeError(e) { + switch (e.code) { + case 'EACCES': return new PosixError(ErrorCodes.EACCES, e.message); + case 'EADDRINUSE': return new PosixError(ErrorCodes.EADDRINUSE, e.message); + case 'ECONNREFUSED': return new PosixError(ErrorCodes.ECONNREFUSED, e.message); + case 'ECONNRESET': return new PosixError(ErrorCodes.ECONNRESET, e.message); + case 'EEXIST': return new PosixError(ErrorCodes.EEXIST, e.message); + case 'EIO': return new PosixError(ErrorCodes.EIO, e.message); + case 'EISDIR': return new PosixError(ErrorCodes.EISDIR, e.message); + case 'EMFILE': return new PosixError(ErrorCodes.EMFILE, e.message); + case 'ENOENT': return new PosixError(ErrorCodes.ENOENT, e.message); + case 'ENOTDIR': return new PosixError(ErrorCodes.ENOTDIR, e.message); + case 'ENOTEMPTY': return new PosixError(ErrorCodes.ENOTEMPTY, e.message); + // ENOTFOUND is Node-specific. ECONNREFUSED is similar enough. + case 'ENOTFOUND': return new PosixError(ErrorCodes.ECONNREFUSED, e.message); + case 'EPERM': return new PosixError(ErrorCodes.EPERM, e.message); + case 'EPIPE': return new PosixError(ErrorCodes.EPIPE, e.message); + case 'ETIMEDOUT': return new PosixError(ErrorCodes.ETIMEDOUT, e.message); + } + // Some other kind of error + return e; +} + +// DRY: Almost the same as puter/filesystem.js +function wrapAPIs(apis) { + for (const method in apis) { + if (typeof apis[method] !== 'function') { + continue; + } + const original = apis[method]; + apis[method] = async (...args) => { + try { + return await original(...args); + } catch (e) { + throw convertNodeError(e); + } + }; + } + return apis; +} + +export const CreateFilesystemProvider = () => { + return wrapAPIs({ + capabilities: { + 'readdir.posix-mode': true, + }, + readdir: async (path) => { + const names = await fs.promises.readdir(path); + + const items = []; + + const users = {}; + const groups = {}; + + for ( const name of names ) { + const filePath = path_.join(path, name); + const stat = await fs.promises.lstat(filePath); + + items.push({ + name, + is_dir: stat.isDirectory(), + is_symlink: stat.isSymbolicLink(), + symlink_path: stat.isSymbolicLink() ? await fs.promises.readlink(filePath) : null, + size: stat.size, + modified: stat.mtimeMs / 1000, + created: stat.ctimeMs / 1000, + accessed: stat.atimeMs / 1000, + mode: stat.mode, + mode_human_readable: modeString(stat.mode), + uid: stat.uid, + gid: stat.gid, + }); + } + + return items; + }, + stat: async (path) => { + const stat = await fs.promises.lstat(path); + const fullPath = await fs.promises.realpath(path); + const parsedPath = path_.parse(fullPath); + // TODO: Fill in more of these? + return { + id: stat.ino, + associated_app_id: null, + public_token: null, + file_request_token: null, + uid: stat.uid, + parent_id: null, + parent_uid: null, + is_dir: stat.isDirectory(), + is_public: null, + is_shortcut: null, + is_symlink: stat.isSymbolicLink(), + symlink_path: stat.isSymbolicLink() ? await fs.promises.readlink(path) : null, + sort_by: null, + sort_order: null, + immutable: null, + name: parsedPath.base, + path: fullPath, + dirname: parsedPath.dir, + dirpath: parsedPath.dir, + metadata: null, + modified: stat.mtime, + created: stat.birthtime, + accessed: stat.atime, + size: stat.size, + layout: null, + owner: null, + type: null, + is_empty: await (async (stat) => { + if (!stat.isDirectory()) + return null; + const children = await fs.promises.readdir(path); + return children.length === 0; + })(stat), + }; + }, + mkdir: async (path, options = { createMissingParents: false }) => { + const createMissingParents = options['createMissingParents'] || false; + return await fs.promises.mkdir(path, { recursive: createMissingParents }); + }, + read: async (path) => { + return await fs.promises.readFile(path); + }, + write: async (path, data) => { + if (data instanceof Blob) { + return await fs.promises.writeFile(path, data.stream()); + } + return await fs.promises.writeFile(path, data); + }, + rm: async (path, options = { recursive: false }) => { + const recursive = options['recursive'] || false; + const stat = await fs.promises.stat(path); + + if ( stat.isDirectory() && ! recursive ) { + throw PosixError.IsDirectory({ path }); + } + + return await fs.promises.rm(path, { recursive }); + }, + rmdir: async (path) => { + const stat = await fs.promises.stat(path); + + if ( !stat.isDirectory() ) { + throw PosixError.IsNotDirectory({ path }); + } + + return await fs.promises.rmdir(path); + }, + move: async (oldPath, newPath) => { + let destStat = null; + try { + destStat = await fs.promises.stat(newPath); + } catch (e) { + if ( e.code !== 'ENOENT' ) throw e; + } + + // fs.promises.rename() expects the new path to include the filename. + // So, if newPath is a directory, append the old filename to it to produce the target path and name. + if ( destStat && destStat.isDirectory() ) { + if ( ! newPath.endsWith('/') ) newPath += '/'; + newPath += path_.basename(oldPath); + } + + return await fs.promises.rename(oldPath, newPath); + }, + copy: async (oldPath, newPath) => { + const srcStat = await fs.promises.stat(oldPath); + const srcIsDir = srcStat.isDirectory(); + + let destStat = null; + try { + destStat = await fs.promises.stat(newPath); + } catch (e) { + if ( e.code !== 'ENOENT' ) throw e; + } + const destIsDir = destStat && destStat.isDirectory(); + + // fs.promises.cp() is experimental, but does everything we want. Maybe implement this manually if needed. + + // `dir -> file`: invalid + if ( srcIsDir && destStat && ! destStat.isDirectory() ) { + throw new PosixError(ErrorCodes.ENOTDIR, 'Cannot copy a directory into a file'); + } + + // `file -> dir`: fs.promises.cp() expects the new path to include the filename. + if ( ! srcIsDir && destIsDir ) { + if ( ! newPath.endsWith('/') ) newPath += '/'; + newPath += path_.basename(oldPath); + } + + return await fs.promises.cp(oldPath, newPath, { recursive: srcIsDir }); + } + }); +}; diff --git a/packages/phoenix/src/pty/NodeStdioPTT.js b/packages/phoenix/src/pty/NodeStdioPTT.js new file mode 100644 index 00000000..58cd947c --- /dev/null +++ b/packages/phoenix/src/pty/NodeStdioPTT.js @@ -0,0 +1,74 @@ +import { ReadableStream, WritableStream } from 'stream/web'; +import { signals } from "../ansi-shell/signals.js"; + +const writestream_node_to_web = node_stream => { + return node_stream; + // return new WritableStream({ + // write: chunk => { + // node_stream.write(chunk); + // } + // }); +}; + +export class NodeStdioPTT { + constructor() { + // this.in = process.stdin; + // this.out = process.stdout; + // this.err = process.stderr; + + // this.in = ReadableStream.from(process.stdin).getReader(); + + let readController; + const readableStream = new ReadableStream({ + start: controller => { + readController = controller; + } + }); + this.in = readableStream.getReader(); + process.stdin.setRawMode(true); + process.stdin.on('data', chunk => { + const input = new Uint8Array(chunk); + readController.enqueue(input); + }); + + this.out = writestream_node_to_web(process.stdout); + this.err = writestream_node_to_web(process.stderr); + + this.ioctl_listeners = {}; + + process.stdout.on('resize', () => { + this.emit('ioctl.set', { + data: { + windowSize: { + rows: process.stdout.rows, + cols: process.stdout.columns, + } + } + }); + }); + + process.stdin.on('end', () => { + globalThis.force_eot = true; + readController.enqueue(new Uint8Array([4])); + }); + } + + on (name, listener) { + if ( ! this.ioctl_listeners.hasOwnProperty(name) ) { + this.ioctl_listeners[name] = []; + } + this.ioctl_listeners[name].push(listener); + + // Hack: Pretend the window got resized, so that listeners get notified of the current size. + if (name === 'ioctl.set') { + process.stdout.emit('resize'); + } + } + + emit (name, evt) { + if ( ! this.ioctl_listeners.hasOwnProperty(name) ) return; + for ( const listener of this.ioctl_listeners[name] ) { + listener(evt); + } + } +} diff --git a/packages/phoenix/src/pty/XDocumentPTT.js b/packages/phoenix/src/pty/XDocumentPTT.js new file mode 100644 index 00000000..13ea657a --- /dev/null +++ b/packages/phoenix/src/pty/XDocumentPTT.js @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { BetterReader } from "dev-pty"; + +const encoder = new TextEncoder(); + +export class XDocumentPTT { + constructor(terminalConnection) { + this.ioctl_listeners = {}; + + this.readableStream = new ReadableStream({ + start: controller => { + this.readController = controller; + } + }); + this.writableStream = new WritableStream({ + start: controller => { + this.writeController = controller; + }, + write: chunk => { + if (typeof chunk === 'string') { + chunk = encoder.encode(chunk); + } + terminalConnection.postMessage({ + $: 'output', + data: chunk, + }); + } + }); + this.out = this.writableStream.getWriter(); + this.in = this.readableStream.getReader(); + this.in = new BetterReader({ delegate: this.in }); + + terminalConnection.on('message', message => { + if (message.$ === 'ioctl.set') { + this.emit('ioctl.set', message); + return; + } + if (message.$ === 'input') { + this.readController.enqueue(message.data); + return; + } + }); + } + + on (name, listener) { + if ( ! this.ioctl_listeners.hasOwnProperty(name) ) { + this.ioctl_listeners[name] = []; + } + this.ioctl_listeners[name].push(listener); + } + + emit (name, evt) { + if ( ! this.ioctl_listeners.hasOwnProperty(name) ) return; + for ( const listener of this.ioctl_listeners[name] ) { + listener(evt); + } + } +} diff --git a/packages/phoenix/src/puter-shell/completers/command_completer.js b/packages/phoenix/src/puter-shell/completers/command_completer.js new file mode 100644 index 00000000..6994ddab --- /dev/null +++ b/packages/phoenix/src/puter-shell/completers/command_completer.js @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class CommandCompleter { + async getCompletions (ctx, inputState) { + const { builtins } = ctx.registries; + const query = inputState.input; + + if ( query === '' ) { + return []; + } + + const completions = []; + + // TODO: Match executable names as well as builtins + for ( const commandName of Object.keys(builtins) ) { + if ( commandName.startsWith(query) ) { + completions.push(commandName.slice(query.length)); + } + } + + return completions; + } +} diff --git a/packages/phoenix/src/puter-shell/completers/file_completer.js b/packages/phoenix/src/puter-shell/completers/file_completer.js new file mode 100644 index 00000000..ea5df34c --- /dev/null +++ b/packages/phoenix/src/puter-shell/completers/file_completer.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import path_ from "path-browserify"; +import { resolveRelativePath } from '../../util/path.js'; + +export class FileCompleter { + async getCompletions (ctx, inputState) { + const { filesystem } = ctx.platform; + + if ( inputState.input === '' ) { + return []; + } + + let path = resolveRelativePath(ctx.vars, inputState.input); + let dir = path_.dirname(path); + let base = path_.basename(path); + + const completions = []; + + const result = await filesystem.readdir(dir); + if ( result === undefined ) { + return []; + } + + for ( const item of result ) { + if ( item.name.startsWith(base) ) { + completions.push(item.name.slice(base.length)); + } + } + + return completions; + } +} diff --git a/packages/phoenix/src/puter-shell/completers/option_completer.js b/packages/phoenix/src/puter-shell/completers/option_completer.js new file mode 100644 index 00000000..922f2bad --- /dev/null +++ b/packages/phoenix/src/puter-shell/completers/option_completer.js @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { DEFAULT_OPTIONS } from '../coreutils/coreutil_lib/help.js'; + +export class OptionCompleter { + async getCompletions (ctx, inputState) { + const { builtins } = ctx.registries; + const query = inputState.input; + + if ( query === '' ) { + return []; + } + + // TODO: Query the command through the providers system. + // Or, we could include the command in the context that's given to completers? + const command = builtins[inputState.tokens[0]]; + if ( ! command ) { + return []; + } + + const completions = []; + + const processOptions = (options) => { + for ( const optionName of Object.keys(options) ) { + const prefixedOptionName = `--${optionName}`; + if ( prefixedOptionName.startsWith(query) ) { + completions.push(prefixedOptionName.slice(query.length)); + } + } + }; + + // TODO: Only check these for builtins! + processOptions(DEFAULT_OPTIONS); + + if ( command.args?.options ) { + processOptions(command.args.options); + } + + return completions; + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/__exports__.js b/packages/phoenix/src/puter-shell/coreutils/__exports__.js new file mode 100644 index 00000000..2ff3c9d2 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/__exports__.js @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +// Generated by /tools/gen.js +import module_ai from './ai.js' +import module_basename from './basename.js' +import module_cat from './cat.js' +import module_cd from './cd.js' +import module_changelog from './changelog.js' +import module_clear from './clear.js' +import module_concept_parser from './concept-parser.js' +import module_cp from './cp.js' +import module_date from './date.js' +import module_dcall from './dcall.js' +import module_dirname from './dirname.js' +import module_echo from './echo.js' +import module_env from './env.js' +import module_errno from './errno.js' +import module_false from './false.js' +import module_grep from './grep.js' +import module_head from './head.js' +import module_help from './help.js' +import module_jq from './jq.js' +import module_login from './login.js' +import module_ls from './ls.js' +import module_man from './man.js' +import module_mkdir from './mkdir.js' +import module_mv from './mv.js' +import module_neofetch from './neofetch.js' +import module_printf from './printf.js' +import module_printhist from './printhist.js' +import module_pwd from './pwd.js' +import module_rm from './rm.js' +import module_rmdir from './rmdir.js' +import module_sample_data from './sample-data.js' +import module_sed from './sed.js' +import module_sleep from './sleep.js' +import module_sort from './sort.js' +import module_tail from './tail.js' +import module_test from './test.js' +import module_touch from './touch.js' +import module_true from './true.js' +import module_txt2img from './txt2img.js' +import module_usages from './usages.js' +import module_wc from './wc.js' +import module_which from './which.js' + +export default { + "ai": module_ai, + "basename": module_basename, + "cat": module_cat, + "cd": module_cd, + "changelog": module_changelog, + "clear": module_clear, + "concept-parser": module_concept_parser, + "cp": module_cp, + "date": module_date, + "dcall": module_dcall, + "dirname": module_dirname, + "echo": module_echo, + "env": module_env, + "errno": module_errno, + "false": module_false, + "grep": module_grep, + "head": module_head, + "help": module_help, + "jq": module_jq, + "login": module_login, + "ls": module_ls, + "man": module_man, + "mkdir": module_mkdir, + "mv": module_mv, + "neofetch": module_neofetch, + "printf": module_printf, + "printhist": module_printhist, + "pwd": module_pwd, + "rm": module_rm, + "rmdir": module_rmdir, + "sample-data": module_sample_data, + "sed": module_sed, + "sleep": module_sleep, + "sort": module_sort, + "tail": module_tail, + "test": module_test, + "touch": module_touch, + "true": module_true, + "txt2img": module_txt2img, + "usages": module_usages, + "wc": module_wc, + "which": module_which, +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/ai.js b/packages/phoenix/src/puter-shell/coreutils/ai.js new file mode 100644 index 00000000..23e2bbb2 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/ai.js @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'ai', + usage: 'ai PROMPT', + description: 'Send PROMPT to Puter\'s AI chatbot, and print its response.', + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { positionals } = ctx.locals; + const [ prompt ] = positionals; + + if ( ! prompt ) { + await ctx.externs.err.write('ai: missing prompt\n'); + throw new Exit(1); + } + if ( positionals.length > 1 ) { + await ctx.externs.err.write('ai: prompt must be wrapped in quotes\n'); + throw new Exit(1); + } + + const { drivers } = ctx.platform; + const { chatHistory } = ctx.plugins; + + let a_interface, a_method, a_args; + + a_interface = 'puter-chat-completion'; + a_method = 'complete'; + a_args = { + messages: [ + ...chatHistory.get_messages(), + { + role: 'user', + content: prompt, + } + ], + }; + + console.log('THESE ARE THE MESSAGES', a_args.messages); + + const result = await drivers.call({ + interface: a_interface, + method: a_method, + args: a_args, + }); + + const resobj = JSON.parse(await result.text(), null, 2); + + if ( resobj.success !== true ) { + await ctx.externs.err.write('request failed\n'); + await ctx.externs.err.write(resobj); + return; + } + + const message = resobj?.result?.message?.content; + + if ( ! message ) { + await ctx.externs.err.write('message not found in response\n'); + await ctx.externs.err.write(result); + return; + } + + chatHistory.add_message(resobj?.result?.message); + + await ctx.externs.out.write(message + '\n'); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/basename.js b/packages/phoenix/src/puter-shell/coreutils/basename.js new file mode 100644 index 00000000..f5e78602 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/basename.js @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'basename', + usage: 'basename PATH [SUFFIX]', + description: 'Print PATH without leading directory segments.\n\n' + + 'If SUFFIX is provided, it is removed from the end of the result.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + let string = ctx.locals.positionals[0]; + const suffix = ctx.locals.positionals[1]; + + if (string === undefined) { + await ctx.externs.err.write('basename: Missing path argument\n'); + throw new Exit(1); + } + if (ctx.locals.positionals.length > 2) { + await ctx.externs.err.write('basename: Too many arguments, expected 1 or 2\n'); + throw new Exit(1); + } + + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html + + // 1. If string is a null string, it is unspecified whether the resulting string is '.' or a null string. + // In either case, skip steps 2 through 6. + if (string === '') { + string = '.'; + } else { + // 2. If string is "//", it is implementation-defined whether steps 3 to 6 are skipped or processed. + // NOTE: We process it normally. + + // 3. If string consists entirely of characters, string shall be set to a single character. + // In this case, skip steps 4 to 6. + if (/^\/+$/.test(string)) { + string = '/'; + } else { + // 4. If there are any trailing characters in string, they shall be removed. + string = string.replace(/\/+$/, ''); + + // 5. If there are any characters remaining in string, the prefix of string up to and including + // the last character in string shall be removed. + const lastSlashIndex = string.lastIndexOf('/'); + if (lastSlashIndex !== -1) { + string = string.substring(lastSlashIndex + 1); + } + + // 6. If the suffix operand is present, is not identical to the characters remaining in string, and is + // identical to a suffix of the characters remaining in string, the suffix suffix shall be removed + // from string. Otherwise, string is not modified by this step. It shall not be considered an error + // if suffix is not found in string. + if (suffix !== undefined && suffix !== string && string.endsWith(suffix)) { + string = string.substring(0, string.length - suffix.length); + } + } + } + + // The resulting string shall be written to standard output. + await ctx.externs.out.write(string + '\n'); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/cat.js b/packages/phoenix/src/puter-shell/coreutils/cat.js new file mode 100644 index 00000000..f1e2941f --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/cat.js @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { resolveRelativePath } from '../../util/path.js'; + +export default { + name: 'cat', + usage: 'cat [FILE...]', + description: 'Concatenate the FILE(s) and print the result.\n\n' + + 'If no FILE is given, or a FILE is `-`, read the standard input.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + input: { + syncLines: true, + }, + output: 'text', + execute: async ctx => { + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + const paths = [...positionals]; + if ( paths.length < 1 ) paths.push('-'); + + for ( const relPath of paths ) { + if ( relPath === '-' ) { + let line, done; + const next_line = async () => { + ({ value: line, done } = await ctx.externs.in_.read()); + console.log('CAT LOOP', { line, done }); + } + for ( await next_line() ; ! done ; await next_line() ) { + await ctx.externs.out.write(line); + } + continue; + } + const absPath = resolveRelativePath(ctx.vars, relPath); + + const result = await filesystem.read(absPath); + + await ctx.externs.out.write(result); + } + } +} \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/cd.js b/packages/phoenix/src/puter-shell/coreutils/cd.js new file mode 100644 index 00000000..7352d39c --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/cd.js @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; +import { resolveRelativePath } from '../../util/path.js'; + +export default { + name: 'cd', + usage: 'cd PATH', + description: 'Change the current directory to PATH.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + // ctx.params to access processed args + // ctx.args to access raw args + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + let [ target ] = positionals; + target = resolveRelativePath(ctx.vars, target); + + const result = await filesystem.readdir(target); + + if ( result.$ === 'error' ) { + await ctx.externs.err.write('cd: error: ' + result.message + '\n'); + throw new Exit(1); + } + + ctx.vars.pwd = target; + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/changelog.js b/packages/phoenix/src/puter-shell/coreutils/changelog.js new file mode 100644 index 00000000..027ec4c9 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/changelog.js @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { SHELL_VERSIONS } from "../../meta/versions.js"; + +async function printVersion(ctx, version) { + await ctx.externs.out.write(`\x1B[35;1m[v${version.v}]\x1B[0m\n`); + for ( const change of version.changes ) { + await ctx.externs.out.write(`\x1B[32;1m+\x1B[0m ${change}\n`); + } +} + +export default { + name: 'changelog', + description: 'Print the changelog for the Phoenix Shell, ordered oldest to newest.', + args: { + $: 'simple-parser', + allowPositionals: false, + options: { + latest: { + description: 'Print only the changes for the most recent version', + type: 'boolean' + } + } + }, + execute: async ctx => { + if (ctx.locals.values.latest) { + await printVersion(ctx, SHELL_VERSIONS[0]); + return; + } + + for ( const version of SHELL_VERSIONS.toReversed() ) { + await printVersion(ctx, version); + } + } +}; + diff --git a/packages/phoenix/src/puter-shell/coreutils/clear.js b/packages/phoenix/src/puter-shell/coreutils/clear.js new file mode 100644 index 00000000..d38bf876 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/clear.js @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'clear', + usage: 'clear', + description: 'Clear the terminal output.', + args: { + // TODO: add 'none-parser' + $: 'simple-parser', + allowPositionals: false + }, + execute: async ctx => { + await ctx.externs.out.write('\x1B[H\x1B[2J'); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/concept-parser.js b/packages/phoenix/src/puter-shell/coreutils/concept-parser.js new file mode 100644 index 00000000..31888220 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/concept-parser.js @@ -0,0 +1,320 @@ +import { GrammarContext, standard_parsers } from '../../../packages/newparser/exports.js'; +import { Parser, UNRECOGNIZED, VALUE } from '../../../packages/newparser/lib.js'; + +class NumberParser extends Parser { + static data = { + startDigit: /[1-9]/, + digit: /[0-9]/, + } + _parse (stream) { + const subStream = stream.fork(); + + const { startDigit, digit } = this.constructor.data; + + let { done, value } = subStream.look(); + if ( done ) return UNRECOGNIZED; + let text = ''; + + // Returns true if there is a next character + const consume = () => { + text += value; + subStream.next(); + ({ done, value } = subStream.look()); + + return !done; + }; + + // Returns the number of consumed characters + const consumeDigitSequence = () => { + let consumed = 0; + while (!done && digit.test(value)) { + consumed++; + consume(); + } + return consumed; + }; + + // Sign + if ( value === '-' ) { + if ( !consume() ) return UNRECOGNIZED; + } + + // Digits + if (value === '0') { + if ( !consume() ) return UNRECOGNIZED; + } else if (startDigit.test(value)) { + if (consumeDigitSequence() === 0) return UNRECOGNIZED; + } else { + return UNRECOGNIZED; + } + + // Decimal + digits + if (value === '.') { + if ( !consume() ) return UNRECOGNIZED; + if (consumeDigitSequence() === 0) return UNRECOGNIZED; + } + + // Exponent + if (value === 'e' || value === 'E') { + if ( !consume() ) return UNRECOGNIZED; + + if (value === '+' || value === '-') { + if ( !consume() ) return UNRECOGNIZED; + } + if (consumeDigitSequence() === 0) return UNRECOGNIZED; + } + + if ( text.length === 0 ) return UNRECOGNIZED; + stream.join(subStream); + return { status: VALUE, $: 'number', value: Number.parseFloat(text) }; + } +} + +class StringParser extends Parser { + static data = { + escapes: { + '"': '"', + '\\': '\\', + '/': '/', + 'b': String.fromCharCode(8), + 'f': String.fromCharCode(0x0C), + '\n': '\n', + '\r': '\r', + '\t': '\t', + }, + hexDigit: /[0-9A-Fa-f]/, + } + _parse (stream) { + const { escapes, hexDigit } = this.constructor.data; + + const subStream = stream.fork(); + let { done, value } = subStream.look(); + if ( done ) return UNRECOGNIZED; + + let text = ''; + + // Returns true if there is a next character + const next = () => { + subStream.next(); + ({ done, value } = subStream.look()); + return !done; + }; + + // Opening " + if (value === '"') { + if (!next()) return UNRECOGNIZED; + } else { + return UNRECOGNIZED; + } + + let insideString = true; + while (insideString) { + if (value === '"') + break; + + // Escape sequences + if (value === '\\') { + if (!next()) return UNRECOGNIZED; + const escape = escapes[value]; + if (escape) { + text += escape; + if (!next()) return UNRECOGNIZED; + continue; + } + + if (value === 'u') { + if (!next()) return UNRECOGNIZED; + + // Consume 4 hex digits, and decode as a unicode codepoint + let hexString = ''; + while (!done && hexString.length < 4) { + if (hexDigit.test(value)) { + hexString += value; + if (!next()) return UNRECOGNIZED; + continue; + } + // Less than 4 hex digits read + return UNRECOGNIZED; + } + let codepoint = Number.parseInt(hexString, 16); + text += String.fromCodePoint(codepoint); + continue; + } + + // Otherwise, it's an invalid escape sequence + return UNRECOGNIZED; + } + + // Anything else is valid string content + text += value; + if (!next()) return UNRECOGNIZED; + } + + // Closing " + if (value === '"') { + next(); + } else { + return UNRECOGNIZED; + } + + if ( text.length === 0 ) return UNRECOGNIZED; + stream.join(subStream); + return { status: VALUE, $: 'string', value: text }; + } +} + +class StringStream { + constructor (str, startIndex = 0) { + this.str = str; + this.i = startIndex; + } + + value_at (index) { + if ( index >= this.str.length ) { + return { done: true, value: undefined }; + } + + return { done: false, value: this.str[index] }; + } + + look () { + return this.value_at(this.i); + } + + next () { + const result = this.value_at(this.i); + this.i++; + return result; + } + + fork () { + return new StringStream(this.str, this.i); + } + + join (forked) { + this.i = forked.i; + } +} + +export default { + name: 'concept-parser', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + const { in_, out, err } = ctx.externs; + await out.write("STARTING CONCEPT PARSER\n"); + const grammar_context = new GrammarContext(standard_parsers()); + await out.write("Constructed a grammar context\n"); + + const parser = grammar_context.define_parser({ + element: a => a.sequence( + a.symbol('whitespace'), + a.symbol('value'), + a.symbol('whitespace'), + ), + value: a => a.firstMatch( + a.symbol('object'), + a.symbol('array'), + a.symbol('string'), + a.symbol('number'), + a.symbol('true'), + a.symbol('false'), + a.symbol('null'), + ), + array: a => a.sequence( + a.literal('['), + a.symbol('whitespace'), + a.optional( + a.repeat( + a.symbol('element'), + a.literal(','), + { trailing: true }, + ), + ), + a.symbol('whitespace'), + a.literal(']'), + ), + member: a => a.sequence( + a.symbol('whitespace'), + a.symbol('string'), + a.symbol('whitespace'), + a.literal(':'), + a.symbol('whitespace'), + a.symbol('value'), + a.symbol('whitespace'), + ), + object: a => a.sequence( + a.literal('{'), + a.symbol('whitespace'), + a.optional( + a.repeat( + a.symbol('member'), + a.literal(','), + { trailing: true }, + ), + ), + a.symbol('whitespace'), + a.literal('}'), + ), + true: a => a.literal('true'), + false: a => a.literal('false'), + null: a => a.literal('null'), + number: a => new NumberParser(), + string: a => new StringParser(), + whitespace: a => a.optional( + a.stringOf(' \r\n\t'.split('')), + ), + }, { + element: it => it[0].value, + value: it => it, + array: it => { + // A parsed array contains 3 values: `[`, the entries array, and `]`, so we only care about index 1. + // If it's less than 3, there were no entries. + if (it.length < 3) return []; + return (it[1].value || []) + .filter(it => it.$ !== 'literal') + .map(it => it.value); + }, + member: it => { + // A parsed member contains 3 values: a name, `:`, and a value. + const [ name_part, colon, value_part ] = it; + return { name: name_part.value, value: value_part.value }; + }, + object: it => { + console.log('OBJECT!!!!'); + console.log(it[1]); + // A parsed object contains 3 values: `{`, the members array, and `}`, so we only care about index 1. + // If it's less than 3, there were no members. + if (it.length < 3) return {}; + const result = {}; + // FIXME: This is all wrong!!! + (it[1].value || []) + .filter(it => it.$ === 'member') + .forEach(it => { + result[it.name] = it.value; + }); + return result; + }, + true: _ => true, + false: _ => false, + null: _ => null, + number: it => it, + string: it => it, + whitespace: _ => {}, + }); + + // TODO: What do we want our streams to be like? + const input = ctx.locals.positionals.shift(); + const stream = new StringStream(input); + try { + const result = parser(stream, 'element'); + console.log('Parsed something!', result); + await out.write('Parsed: `' + JSON.stringify(result, undefined, 2) + '`\n'); + } catch (e) { + await err.write(`Error while parsing: ${e.toString()}\n`); + await err.write(e.stack + '\n'); + } + } +} \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js new file mode 100644 index 00000000..c084d723 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +/* + Echo Escapes Implementations + ---------------------------- + + This documentation describes how functions in this file + should be implemented. + + SITUATION + The function is passed an object called `fns` containing + functions to interact with the caller. + + It can be assumped that the called has already advanced + a "text cursor" just past the first character identifying + the escape sequence. For example, for escape sequence `\a` + the text cursor will be positioned immediately after `a`. + + INPUTS + function: peek() + returns the character at the position of the text cursor + + function: advance(n=1) + advances the text cursor `n` bytes forward + + function: markIgnored + informs the caller that the escape sequence should be + treated as literal text + + function: output + commands the caller to write a string + + function: outputETX + informs the caller that this is the end of text; + \c is Ctrl+C is ETX +*/ + +// TODO: get these values from a common place +const NUL = String.fromCharCode(1); +const BEL = String.fromCharCode(7); +const BS = String.fromCharCode(8); +const VT = String.fromCharCode(0x0B); +const FF = String.fromCharCode(0x0C); +const ESC = String.fromCharCode(0x1B); + +const HEX_REGEX = /^[A-Fa-f0-9]/; +const OCT_REGEX = /^[0-7]/; +const maybeGetHex = chr => { + let hexchars = ''; + if ( chr.match(HEX_REGEX) ) { + // + } +}; + +const echo_escapes = { + 'a': caller => caller.output(BEL), + 'b': caller => caller.output(BS), + 'c': caller => caller.outputETX(), + 'e': caller => caller.output(ESC), + 'f': caller => caller.output(FF), + 'n': caller => caller.output('\n'), + 'r': caller => caller.output('\r'), + 't': caller => caller.output('\t'), + 'v': caller => caller.output(VT), + 'x': caller => { + let hexchars = ''; + while ( caller.peek().match(HEX_REGEX) ) { + hexchars += caller.peek(); + caller.advance(); + + if ( hexchars.length === 2 ) break; + } + if ( hexchars.length === 0 ) { + caller.markIgnored(); + return; + } + caller.output(String.fromCharCode(Number.parseInt(hexchars, 16))); + }, + '0': caller => { + let octchars = ''; + while ( caller.peek().match(OCT_REGEX) ) { + octchars += caller.peek(); + caller.advance(); + + if ( octchars.length === 3 ) break; + } + if ( octchars.length === 0 ) { + caller.output(NUL); + return; + } + caller.output(String.fromCharCode(Number.parseInt(hexchars, 8))); + }, + '\\': caller => caller.output('\\'), +}; + +export const processEscapes = str => { + let output = ''; + + let state = null; + const states = {}; + states.STATE_ESCAPE = i => { + state = states.STATE_NORMAL; + + let ignored = false; + + const chr = str[i]; + i++; + const apiToCaller = { + advance: n => { + n = n ?? 1; + i += n; + }, + peek: () => str[i], + output: text => output += text, + markIgnored: () => ignored = true, + outputETX: () => { + state = states.STATE_ETX; + } + }; + echo_escapes[chr](apiToCaller); + + if ( ignored ) { + output += '\\' + str[i]; + return; + } + + return i; + }; + states.STATE_NORMAL = i => { + console.log('str@i', str[i]); + if ( str[i] === '\\' ) { + console.log('escape state?'); + state = states.STATE_ESCAPE; + return; + } + output += str[i]; + }; + states.STATE_ETX = () => str.length; + state = states.STATE_NORMAL; + + for ( let i=0 ; i < str.length ; ) { + i = state(i) ?? i+1; + } + + return output; +}; \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js new file mode 100644 index 00000000..1d7b419b --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class Exit extends Error { + constructor (code) { + super(`exit ${code}`); + this.code = code; + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js new file mode 100644 index 00000000..fbf16f4e --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { wrapText } from '../../../util/wrap-text.js'; + +const TAB_SIZE = 8; + +export const DEFAULT_OPTIONS = { + help: { + description: 'Display this help text, and exit', + type: 'boolean', + }, +}; + +export const printUsage = async (command, out, vars) => { + const { name, usage, description, args, helpSections } = command; + const options = Object.create(DEFAULT_OPTIONS); + Object.assign(options, args.options); + + const heading = async text => { + await out.write(`\x1B[34;1m${text}:\x1B[0m\n`); + }; + const colorOption = text => { + return `\x1B[92m${text}\x1B[0m`; + }; + const colorOptionArgument = text => { + return `\x1B[91m${text}\x1B[0m`; + }; + const wrap = text => { + return wrapText(text, vars.size.cols).join('\n') + '\n'; + } + + await heading('Usage'); + if (!usage) { + let output = name; + if (options) { + output += ' [OPTIONS]'; + } + if (args.allowPositionals) { + output += ' INPUTS...'; + } + await out.write(` ${output}\n\n`); + } else if (typeof usage === 'string') { + await out.write(` ${usage}\n\n`); + } else { + for (const line of usage) { + await out.write(` ${line}\n`); + } + await out.write('\n'); + } + + if (description) { + await out.write(wrap(description)); + await out.write(`\n`); + } + + if (options) { + await heading('Options'); + + for (const optionName in options) { + let optionText = ' '; + let indentSize = optionText.length; + const option = options[optionName]; + if (option.short) { + optionText += colorOption('-' + option.short) + ', '; + indentSize += `-${option.short}, `.length; + } else { + optionText += ` `; + indentSize += ` `.length; + } + optionText += colorOption(`--${optionName}`); + indentSize += `--${optionName}`.length; + if (option.type !== 'boolean') { + const valueName = option.valueName || 'VALUE'; + optionText += `=${colorOptionArgument(valueName)}`; + indentSize += `=${valueName}`.length; + } + if (option.description) { + const indentSizeIncludingTab = (size) => { + return (Math.floor(size / TAB_SIZE) + 1) * TAB_SIZE + 1; + }; + + // Wrap the description based on the terminal width, with each line indented. + let remainingWidth = vars.size.cols - indentSizeIncludingTab(indentSize); + let skipIndentOnFirstLine = true; + + // If there's not enough room after a very long option name, start on the next line. + if (remainingWidth < 30) { + optionText += '\n'; + indentSize = 8; + remainingWidth = vars.size.cols - indentSizeIncludingTab(indentSize); + skipIndentOnFirstLine = false; + } + + const wrappedDescriptionLines = wrapText(option.description, remainingWidth); + for (const line of wrappedDescriptionLines) { + if (skipIndentOnFirstLine) { + skipIndentOnFirstLine = false; + } else { + optionText += ' '.repeat(indentSize); + } + optionText += `\t ${line}\n`; + } + } else { + optionText += '\n'; + } + await out.write(optionText); + } + await out.write('\n'); + } + + if (helpSections) { + for (const [title, contents] of Object.entries(helpSections)) { + await heading(title); + await out.write(wrap(contents)); + await out.write('\n\n'); + } + } +} \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js new file mode 100644 index 00000000..807f748b --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const validate_string = (str, meta) => { + if ( str === undefined ) { + if ( ! meta.allow_empty ) { + throw new Error(`${meta?.name} is required`); + } + return ''; + } + + if ( typeof str !== 'string' ) { + throw new Error(`${meta?.name} must be a string`); + } + + if ( ! meta.allow_empty && str.length === 0 ) { + throw new Error(`${meta?.name} must not be empty`); + } + + return str; +} diff --git a/packages/phoenix/src/puter-shell/coreutils/cp.js b/packages/phoenix/src/puter-shell/coreutils/cp.js new file mode 100644 index 00000000..032f7ecc --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/cp.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from "./coreutil_lib/exit.js"; +import { resolveRelativePath } from '../../util/path.js'; + +export default { + name: 'cp', + usage: ['cp [OPTIONS] SOURCE DESTINATION', 'cp [OPTIONS] SOURCE... DIRECTORY'], + description: 'Copy the SOURCE to DESTINATION, or multiple SOURCE(s) to DIRECTORY.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + recursive: { + description: 'Copy directories recursively', + type: 'boolean', + short: 'R' + } + } + }, + execute: async ctx => { + const { positionals, values } = ctx.locals; + const { out, err } = ctx.externs; + const { filesystem } = ctx.platform; + + if ( positionals.length < 1 ) { + await err.write('cp: missing file operand\n'); + throw new Exit(1); + } + + const srcRelPath = positionals.shift(); + + if ( positionals.length < 1 ) { + const aft = positionals[0]; + await err.write(`cp: missing destination file operand after '${aft}'\n`); + throw new Exit(1); + } + + const dstRelPath = positionals.shift(); + + const srcAbsPath = resolveRelativePath(ctx.vars, srcRelPath); + let dstAbsPath = resolveRelativePath(ctx.vars, dstRelPath); + + const srcStat = await filesystem.stat(srcAbsPath); + if ( srcStat && srcStat.is_dir && ! values.recursive ) { + await err.write(`cp: -R not specified; skipping directory '${srcRelPath}'\n`); + throw new Exit(1); + } + + await filesystem.copy(srcAbsPath, dstAbsPath); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/date.js b/packages/phoenix/src/puter-shell/coreutils/date.js new file mode 100644 index 00000000..f694544f --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/date.js @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +// "When no formatting operand is specified, the output in the POSIX locale shall be equivalent to specifying:" +const DEFAULT_FORMAT = '+%a %b %e %H:%M:%S %Z %Y'; + +function padStart(number, length, padChar) { + let string = number.toString(); + if ( string.length >= length ) { + return string; + } + + return padChar.repeat(length - string.length) + string; +} + +function highlight(text) { + return `\x1B[92m${text}\x1B[0m`; +} + +export default { + name: 'date', + usage: 'date [OPTIONS] [+FORMAT]', + description: 'Print the system date and time\n\n' + + 'If FORMAT is provided, it controls the date format used.', + helpSections: { + 'Format Sequences': 'The following format sequences are understood:\n\n' + + ` ${highlight('%a')} Weekday name, abbreviated.\n` + + ` ${highlight('%A')} Weekday name\n` + + ` ${highlight('%b')} Month name, abbreviated\n` + + ` ${highlight('%B')} Month name\n` + + ` ${highlight('%c')} Default date and time representation\n` + + ` ${highlight('%C')} Century, 2 digits padded with '0'\n` + + ` ${highlight('%d')} Day of the month, 2 digits padded with '0'\n` + + ` ${highlight('%D')} Date in the format mm/dd/yy\n` + + ` ${highlight('%e')} Day of the month, 2 characters padded with leading spaces\n` + + ` ${highlight('%h')} Same as ${highlight('%b')}\n` + + ` ${highlight('%H')} Hour (24-hour clock), 2 digits padded with '0'\n` + + ` ${highlight('%I')} Hour (12-hour clock), 2 digits padded with '0'\n` + + // ` ${highlight('%j')} TODO: Day of the year, 3 digits padded with '0'\n` + + ` ${highlight('%m')} Month, 2 digits padded with '0', with January = 01\n` + + ` ${highlight('%M')} Minutes, 2 digits padded with '0'\n` + + ` ${highlight('%n')} A newline character\n` + + ` ${highlight('%p')} AM or PM\n` + + ` ${highlight('%r')} Time (12-hour clock) with AM/PM, as 'HH:MM:SS AM/PM'\n` + + ` ${highlight('%S')} Seconds, 2 digits padded with '0'\n` + + ` ${highlight('%t')} A tab character\n` + + ` ${highlight('%T')} Time (24-hour clock), as 'HH:MM:SS'\n` + + ` ${highlight('%u')} Weekday as a number, with Monday = 1 and Sunday = 7\n` + + // ` ${highlight('%U')} TODO: Week of the year (Sunday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Sunday shall be considered to be in week 0.\n` + + // ` ${highlight('%V')} TODO: Week of the year (Monday as the first day of the week) as a decimal number [01,53]. If the week containing January 1 has four or more days in the new year, then it shall be considered week 1; otherwise, it shall be the last week of the previous year, and the next week shall be week 1.\n` + + ` ${highlight('%w')} Weekday as a number, with Sunday = 0\n` + + // ` ${highlight('%W')} TODO: Week of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday shall be considered to be in week 0.\n` + + ` ${highlight('%x')} Default date representation\n` + + ` ${highlight('%X')} Default time representation\n` + + ` ${highlight('%y')} Year within century, 2 digits padded with '0'\n` + + ` ${highlight('%Y')} Year\n` + + ` ${highlight('%Z')} Timezone name, if it can be determined\n` + + ` ${highlight('%%')} A percent sign\n` + }, + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + utc: { + description: 'Operate in UTC instead of the local timezone', + type: 'boolean', + short: 'u', + default: false, + } + } + }, + execute: async ctx => { + const { out, err } = ctx.externs; + const { positionals, values } = ctx.locals; + + if ( positionals.length > 1 ) { + await err.write('date: Too many arguments\n'); + throw new Exit(1); + } + + let format = positionals.shift() ?? DEFAULT_FORMAT; + + if ( ! format.startsWith('+') ) { + await err.write('date: Format does not begin with `+`\n'); + throw new Exit(1); + } + format = format.substring(1); + + // TODO: Should we use the server time instead? Maybe put that behind an option. + const date = new Date(); + const locale = 'en-US'; // TODO: POSIX: Pull this from the user's settings. + const timeZone = values.utc ? 'UTC' : undefined; + + let output = ''; + for (let i = 0; i < format.length; i++) { + let char = format[i]; + if ( char === '%' ) { + char = format[++i]; + switch (char) { + // "Locale's abbreviated weekday name." + case 'a': { + output += date.toLocaleDateString(locale, { timeZone: timeZone, weekday: 'short' }); + break; + } + + // "Locale's full weekday name." + case 'A': { + output += date.toLocaleDateString(locale, { timeZone: timeZone, weekday: 'long' }); + break; + } + + // "Locale's abbreviated month name." + case 'b': + // "A synonym for %b." + case 'h': { + output += date.toLocaleDateString(locale, { timeZone: timeZone, month: 'short' }); + break; + } + + // "Locale's full month name." + case 'B': { + output += date.toLocaleDateString(locale, { timeZone: timeZone, month: 'long' }); + break; + } + + // "Locale's appropriate date and time representation." + case 'c': { + output += date.toLocaleString(locale, { timeZone: timeZone }); + break; + } + + // "Century (a year divided by 100 and truncated to an integer) as a decimal number [00,99]." + case 'C': { + output += Math.trunc(date.getFullYear() / 100); + break; + } + + // "Day of the month as a decimal number [01,31]." + case 'd': { + output += padStart(date.getDate(), 2, '0'); + break; + } + + // "Date in the format mm/dd/yy." + case 'D': { + const month = padStart(date.getMonth() + 1, 2, '0'); + const day = padStart(date.getDate(), 2, '0'); + const year = padStart(date.getFullYear() % 100, 2, '0'); + output += `${month}/${day}/${year}`; + break; + } + + // "Day of the month as a decimal number [1,31] in a two-digit field with leading + // character fill." + case 'e': { + output += padStart(date.getDate(), 2, ' '); + break; + } + + // "Hour (24-hour clock) as a decimal number [00,23]." + case 'H': { + output += padStart(date.getHours(), 2, '0'); + break; + } + + // "Hour (12-hour clock) as a decimal number [01,12]." + case 'I': { + output += padStart((date.getHours() % 12) || 12, 2, '0'); + break; + } + + // TODO: "Day of the year as a decimal number [001,366]." + case 'j': break; + + // "Month as a decimal number [01,12]." + case 'm': { + // getMonth() starts at 0 for January + output += padStart(date.getMonth() + 1, 2, '0'); + break; + } + + // "Minute as a decimal number [00,59]." + case 'M': { + output += padStart(date.getMinutes(), 2, '0'); + break; + } + + // "A ." + case 'n': output += '\n'; break; + + // "Locale's equivalent of either AM or PM." + case 'p': { + // TODO: We should access this from the locale. + output += date.getHours() < 12 ? 'AM' : 'PM'; + break; + } + + // "12-hour clock time [01,12] using the AM/PM notation; in the POSIX locale, this shall be + // equivalent to %I : %M : %S %p." + case 'r': { + const rawHours = date.getHours(); + const hours = padStart((rawHours % 12) || 12, 2, '0'); + // TODO: We should access this from the locale. + const am_pm = rawHours < 12 ? 'AM' : 'PM'; + const minutes = padStart(date.getMinutes(), 2, '0'); + const seconds = padStart(date.getSeconds(), 2, '0'); + output += `${hours}:${minutes}:${seconds} ${am_pm}`; + break; + } + + // "Seconds as a decimal number [00,60]." + case 'S': { + output += padStart(date.getSeconds(), 2, '0'); + break; + } + + // "A ." + case 't': output += '\t'; break; + + // "24-hour clock time [00,23] in the format HH:MM:SS." + case 'T': { + const hours = padStart(date.getHours(), 2, '0'); + const minutes = padStart(date.getMinutes(), 2, '0'); + const seconds = padStart(date.getSeconds(), 2, '0'); + output += `${hours}:${minutes}:${seconds}`; + break; + } + + // "Weekday as a decimal number [1,7] (1=Monday)." + case 'u': { + // getDay() returns 0 for Sunday + output += date.getDay() || 7; + break; + } + + // TODO: "Week of the year (Sunday as the first day of the week) as a decimal number [00,53]. + // All days in a new year preceding the first Sunday shall be considered to be in week 0." + case 'U': break; + + // TODO: "Week of the year (Monday as the first day of the week) as a decimal number [01,53]. + // If the week containing January 1 has four or more days in the new year, then it shall be + // considered week 1; otherwise, it shall be the last week of the previous year, and the next + // week shall be week 1." + case 'V': break; + + // "Weekday as a decimal number [0,6] (0=Sunday)." + case 'w': { + output += date.getDay(); + break; + } + + // TODO: "Week of the year (Monday as the first day of the week) as a decimal number [00,53]. + // All days in a new year preceding the first Monday shall be considered to be in week 0." + case 'W': break; + + // "Locale's appropriate date representation." + case 'x': { + output += date.toLocaleDateString(locale, { timeZone: timeZone }); + break; + } + + // "Locale's appropriate time representation." + case 'X': { + output += date.toLocaleTimeString(locale, { timeZone: timeZone }); + break; + } + + // "Year within century [00,99]." + case 'y': { + output += date.getFullYear() % 100; + break; + } + + // "Year with century as a decimal number." + case 'Y': { + output += date.getFullYear(); + break; + } + + // "Timezone name, or no characters if no timezone is determinable." + case 'Z': { + const parts = new Intl.DateTimeFormat(locale, { timeZone: timeZone, timeZoneName: 'short' }).formatToParts(date); + output += parts.find(it => it.type === 'timeZoneName').value; + break; + } + + // "A character." + case '%': output += '%'; break; + + // We reached the end of the string, just output the %. + case undefined: output += '%'; break; + + // If nothing matched, just output the input verbatim + default: output += '%' + char; break; + } + continue; + } + output += char; + } + output += '\n'; + + await out.write(output); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/dcall.js b/packages/phoenix/src/puter-shell/coreutils/dcall.js new file mode 100644 index 00000000..dad4bdf4 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/dcall.js @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'driver-call', + usage: 'driver-call METHOD [JSON]', + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { positionals } = ctx.locals; + const [ method, json ] = positionals; + + const { drivers } = ctx.platform; + + let a_interface, a_method, a_args; + if ( method === 'test' ) { + // a_interface = 'puter-kvstore'; + // a_method = 'get'; + // a_args = { key: 'something' }; + a_interface = 'puter-image-generation', + a_method = 'generate'; + a_args = { + prompt: 'a blue cat', + }; + } else { + [a_interface, a_method] = method.split(':'); + try { + a_args = JSON.parse(json); + } catch (e) { + a_args = {}; + } + } + + const result = await drivers.call({ + interface: a_interface, + method: a_method, + args: a_args, + }); + + await ctx.externs.out.write(result); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/dirname.js b/packages/phoenix/src/puter-shell/coreutils/dirname.js new file mode 100644 index 00000000..c0f52d51 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/dirname.js @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'dirname', + usage: 'dirname PATH', + description: 'Print PATH without its final segment.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + let string = ctx.locals.positionals[0]; + const removeTrailingSlashes = (input) => { + return input.replace(/\/+$/, ''); + } + + if (string === undefined) { + await ctx.externs.err.write('dirname: Missing path argument\n'); + throw new Exit(1); + } + if (ctx.locals.positionals.length > 1) { + await ctx.externs.err.write('dirname: Too many arguments, expected 1\n'); + throw new Exit(1); + } + + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/dirname.html + let skipToAfterStep8 = false; + + // 1. If string is //, skip steps 2 to 5. + if (string !== '//') { + // 2. If string consists entirely of characters, string shall be set to a single character. + // In this case, skip steps 3 to 8. + if (string === '/'.repeat(string.length)) { + string = '/'; + skipToAfterStep8 = true; + } else { + // 3. If there are any trailing characters in string, they shall be removed. + string = removeTrailingSlashes(string); + + // 4. If there are no characters remaining in string, string shall be set to a single character. + // In this case, skip steps 5 to 8. + if (string.indexOf('/') === -1) { + string = '.'; + skipToAfterStep8 = true; + } + + // 5. If there are any trailing non- characters in string, they shall be removed. + else { + const lastSlashIndex = string.lastIndexOf('/'); + if (lastSlashIndex === -1) { + string = ''; + } else { + string = string.substring(0, lastSlashIndex); + } + } + } + } + + if (!skipToAfterStep8) { + // 6. If the remaining string is //, it is implementation-defined whether steps 7 and 8 are skipped or processed. + // NOTE: We process it normally. + + // 7. If there are any trailing characters in string, they shall be removed. + string = removeTrailingSlashes(string); + + // 8. If the remaining string is empty, string shall be set to a single character. + if (string.length === 0) { + string = '/'; + } + } + + // The resulting string shall be written to standard output. + await ctx.externs.out.write(string + '\n'); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/echo.js b/packages/phoenix/src/puter-shell/coreutils/echo.js new file mode 100644 index 00000000..d29509a5 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/echo.js @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { processEscapes } from "./coreutil_lib/echo_escapes.js"; + +export default { + name: 'echo', + usage: 'echo [OPTIONS] INPUTS...', + description: 'Print the inputs to standard output.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + 'no-newline': { + description: 'Do not print a trailing newline', + type: 'boolean', + short: 'n' + }, + 'enable-escapes': { + description: 'Interpret backslash escape sequences', + type: 'boolean', + short: 'e' + }, + 'disable-escapes': { + description: 'Disable interpreting backslash escape sequences', + type: 'boolean', + short: 'E' + } + } + }, + execute: async ctx => { + const { positionals, values } = ctx.locals; + + let output = ''; + let notFirst = false; + for ( const positional of positionals ) { + if ( notFirst ) { + output += ' '; + } else notFirst = true; + output += positional; + } + + if ( ! values.n ) { + output += '\n'; + } + + if ( values.e && ! values.E ) { + console.log('processing'); + output = processEscapes(output); + } + + const lines = output.split('\n'); + for ( let i=0 ; i < lines.length ; i++ ) { + const line = lines[i]; + const isLast = i === lines.length - 1; + await ctx.externs.out.write(line + (isLast ? '' : '\n')); + } + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/env.js b/packages/phoenix/src/puter-shell/coreutils/env.js new file mode 100644 index 00000000..6f9eb4ab --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/env.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'env', + usage: 'env', + description: 'Print environment variables, one per line, as NAME=VALUE.', + args: { + // TODO: add 'none-parser' + $: 'simple-parser', + allowPositionals: false + }, + execute: async ctx => { + const env = ctx.env; + const out = ctx.externs.out; + + for ( const k in env ) { + await out.write(`${k}=${env[k]}\n`); + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/errno.js b/packages/phoenix/src/puter-shell/coreutils/errno.js new file mode 100644 index 00000000..dd36d8dd --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/errno.js @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ErrorCodes, ErrorMetadata, errorFromIntegerCode } from '../../platform/PosixError.js'; +import { Exit } from './coreutil_lib/exit.js'; + +const maxErrorNameLength = Object.keys(ErrorCodes) + .reduce((longest, name) => Math.max(longest, name.length), 0); +const maxNumberLength = 3; + +async function printSingleErrno(errorCode, out) { + const metadata = ErrorMetadata.get(errorCode); + const paddedName = errorCode.description + ' '.repeat(maxErrorNameLength - errorCode.description.length); + const code = metadata.code.toString(); + const paddedCode = ' '.repeat(maxNumberLength - code.length) + code; + await out.write(`${paddedName} ${paddedCode} ${metadata.description}\n`); +} + +export default { + name: 'errno', + usage: 'errno [OPTIONS] [NAME-OR-CODE...]', + description: 'Look up and describe errno codes.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + list: { + description: 'List all errno values', + type: 'boolean', + short: 'l' + }, + search: { + description: 'Search for errors whose descriptions contain NAME-OR-CODEs, case-insensitively', + type: 'boolean', + short: 's' + } + } + }, + execute: async ctx => { + const { err, out } = ctx.externs; + const { positionals, values } = ctx.locals; + + if (values.search) { + for (const [errorCode, metadata] of ErrorMetadata) { + const description = metadata.description.toLowerCase(); + let matches = true; + for (const nameOrCode of positionals) { + if (! description.includes(nameOrCode.toLowerCase())) { + matches = false; + break; + } + } + if (matches) { + await printSingleErrno(errorCode, out); + } + } + return; + } + + if (values.list) { + for (const errorCode of ErrorMetadata.keys()) { + await printSingleErrno(errorCode, out); + } + return; + } + + let failedToMatchSomething = false; + const fail = async (nameOrCode) => { + await err.write(`ERROR: Not understood: ${nameOrCode}\n`); + failedToMatchSomething = true; + }; + + for (const nameOrCode of positionals) { + let errorCode = ErrorCodes[nameOrCode.toUpperCase()]; + if (errorCode) { + await printSingleErrno(errorCode, out); + continue; + } + + const code = Number.parseInt(nameOrCode); + if (!isFinite(code)) { + await fail(nameOrCode); + continue; + } + errorCode = errorFromIntegerCode(code); + if (errorCode) { + await printSingleErrno(errorCode, out); + continue; + } + + await fail(nameOrCode); + } + + if (failedToMatchSomething) { + throw new Exit(1); + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/false.js b/packages/phoenix/src/puter-shell/coreutils/false.js new file mode 100644 index 00000000..d888e53a --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/false.js @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'false', + usage: 'false', + description: 'Do nothing, and return a failure code.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + throw new Exit(1); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/grep.js b/packages/phoenix/src/puter-shell/coreutils/grep.js new file mode 100644 index 00000000..29351e1c --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/grep.js @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { resolveRelativePath } from '../../util/path.js'; + +const lxor = (a, b) => a ? !b : b; + +import path_ from "path-browserify"; + +export default { + name: 'grep', + usage: 'grep [OPTIONS] PATTERN FILE...', + description: 'Search FILE(s) for PATTERN, and print any matches.', + input: { + syncLines: true, + }, + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + 'ignore-case': { + description: 'Match the pattern case-insensitively', + type: 'boolean', + short: 'i' + }, + 'invert-match': { + description: 'Print lines that do not match the pattern', + type: 'boolean', + short: 'v' + }, + 'line-number': { + description: 'Print the line number before each result', + type: 'boolean', + short: 'n' + }, + recursive: { + description: 'Recursively search in directories', + type: 'boolean', + short: 'r' + } + }, + }, + output: 'text', + execute: async ctx => { + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + const [ pattern, ...files ] = positionals; + + const do_grep_dir = async ( path ) => { + const entries = await filesystem.readdir(path); + + for ( const entry of entries ) { + const entryPath = path_.join(path, entry.name); + + if ( entry.type === 'directory' ) { + if ( values.recursive ) { + await do_grep_dir(entryPath); + } + } else { + await do_grep_file(entryPath); + } + } + } + + const do_grep_line = async ( line ) => { + if ( line.endsWith('\n') ) line = line.slice(0, -1); + const re = new RegExp( + pattern, + values['ignore-case'] ? 'i' : '' + ); + + console.log( + 'Attempting to match line', + line, + 'with pattern', + pattern, + 'and re', + re, + 'and parameters', + values + ); + + if ( lxor(values['invert-match'], re.test(line)) ) { + const lineNumber = values['line-number'] ? i + 1 : ''; + const lineToPrint = + lineNumber ? lineNumber + ':' : '' + + line; + + console.log(`LINE{${lineToPrint}}`); + await ctx.externs.out.write(lineToPrint + '\n'); + } + } + + const do_grep_lines = async ( lines ) => { + for ( let i=0 ; i < lines.length ; i++ ) { + const line = lines[i]; + + await do_grep_line(line); + } + } + + const do_grep_file = async ( path ) => { + console.log('about to read path', path); + const data_blob = await filesystem.read(path); + const data_string = await data_blob.text(); + + const lines = data_string.split('\n'); + + await do_grep_lines(lines); + } + + + + if ( files.length === 0 ) { + if ( values.recursive ) { + files.push('.'); + } else { + files.push('-'); + } + } + + console.log('FILES', files); + + for ( let file of files ) { + if ( file === '-' ) { + for ( ;; ) { + const { value, done } = await ctx.externs.in_.read(); + if ( done ) break; + await do_grep_line(value); + } + } else { + file = resolveRelativePath(ctx.vars, file); + const stat = await filesystem.stat(file); + if ( stat.is_dir ) { + if ( values.recursive ) { + await do_grep_dir(file); + } else { + await ctx.externs.err.write('grep: ' + file + ': Is a directory\n'); + } + } else { + await do_grep_file(file); + } + } + } + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/head.js b/packages/phoenix/src/puter-shell/coreutils/head.js new file mode 100644 index 00000000..e96d8570 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/head.js @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; +import { fileLines } from '../../util/file.js'; + +export default { + name: 'head', + usage: 'head [OPTIONS] [FILE]', + description: 'Read a file and print the first lines to standard output.\n\n' + + 'Defaults to 10 lines unless --lines is given. ' + + 'If no FILE is provided, or FILE is `-`, read standard input.', + input: { + syncLines: true + }, + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + lines: { + description: 'Print the last COUNT lines', + type: 'string', + short: 'n', + valueName: 'COUNT', + } + } + }, + execute: async ctx => { + const { out, err } = ctx.externs; + const { positionals, values } = ctx.locals; + + if (positionals.length > 1) { + // TODO: Support multiple files (this is POSIX) + await err.write('head: Only one FILE parameter is allowed\n'); + throw new Exit(1); + } + const relPath = positionals[0] || '-'; + + let lineCount = 10; + + if (values.lines) { + const parsedLineCount = Number.parseFloat(values.lines); + if (isNaN(parsedLineCount) || ! Number.isInteger(parsedLineCount) || parsedLineCount < 1) { + await err.write(`head: Invalid number of lines '${values.lines}'\n`); + throw new Exit(1); + } + lineCount = parsedLineCount; + } + + let processedLineCount = 0; + for await (const line of fileLines(ctx, relPath)) { + await out.write(line); + processedLineCount++; + if (processedLineCount >= lineCount) + break; + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/help.js b/packages/phoenix/src/puter-shell/coreutils/help.js new file mode 100644 index 00000000..817074c0 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/help.js @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +// TODO: fetch help information from command registry + +import { printUsage } from "./coreutil_lib/help.js"; +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'help', + usage: ['help', 'help COMMAND'], + description: 'Print help information for a specific command, or list available commands.\n\n' + + 'If COMMAND is provided, print the documentation for that command. ' + + 'Otherwise, list all the commands that are available.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + const { positionals } = ctx.locals; + const { builtins } = ctx.registries; + + const { out, err } = ctx.externs; + + if (positionals.length > 1) { + await err.write('help: Too many arguments, expected 0 or 1\n'); + throw new Exit(1); + } + + if (positionals.length === 1) { + const commandName = positionals[0]; + const command = builtins[commandName]; + if (!command) { + await err.write(`help: No builtin found named '${commandName}'\n`); + throw new Exit(1); + } + await printUsage(command, out, ctx.vars); + return; + } + + const heading = txt => { + out.write(`\x1B[34;1m~ ${txt} ~\x1B[0m\n`); + }; + + heading('available commands'); + out.write('Use \x1B[34;1mhelp COMMAND-NAME\x1B[0m for more information\n'); + for ( const k in builtins ) { + out.write(' - ' + k + '\n'); + } + out.write('\n'); + heading('available features'); + out.write(' - pipes; ex: ls | tail -n 2\n') + out.write(' - redirects; ex: ls > some_file.txt\n') + out.write(' - simple tab completion\n') + out.write(' - in-memory command history\n') + out.write('\n'); + heading('what\'s coming up?'); + out.write(' - keep watching for \x1B[34;1mmore\x1B[0m (est: v0.1.11)\n') + // out.write(' - \x1B[34;1mcurl\x1B[0m up with your favorite terminal (est: TBA)\n') + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/jq.js b/packages/phoenix/src/puter-shell/coreutils/jq.js new file mode 100644 index 00000000..ab3a641d --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/jq.js @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import jsonQuery from 'json-query'; +import { signals } from '../../ansi-shell/signals.js'; +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'jq', + usage: 'jq FILTER [FILE...]', + description: 'Process JSON input FILE(s) according to FILTER.\n\n' + + 'Reads from standard input if no FILE is provided.', + input: { + syncLines: true, + }, + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { externs } = ctx; + const { sdkv2 } = externs; + + const { positionals } = ctx.locals; + const [query] = positionals; + + // Read one line at a time + const { in_, out, err } = ctx.externs; + + let rslv_sigint; + const p_int = new Promise(rslv => rslv_sigint = rslv); + ctx.externs.sig.on((signal) => { + if ( signal === signals.SIGINT ) { + rslv_sigint({ is_sigint: true }); + } + }); + + + let line, done; + const next_line = async () => { + let is_sigint = false; + ({ value: line, done, is_sigint } = await Promise.race([ + p_int, in_.read(), + ])); + if ( is_sigint ) { + throw new Exit(130); + } + // ({ value: line, done } = await in_.read()); + } + for ( await next_line() ; ! done ; await next_line() ) { + let data; try { + data = JSON.parse(line); + } catch (e) { + await err.write('Error: ' + e.message + '\n'); + continue; + } + const result = jsonQuery(query, { data }); + await out.write(JSON.stringify(result.value) + '\n'); + } + } +} \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/login.js b/packages/phoenix/src/puter-shell/coreutils/login.js new file mode 100644 index 00000000..e5117cfa --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/login.js @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'login', + usage: 'login', + description: 'Log in to a Puter.com account.', + args: { + $: 'simple-parser', + allowPositionals: false, + }, + execute: async ctx => { + // ctx.params to access processed args + // ctx.args to access raw args + const { positionals, values } = ctx.locals; + const { puterSDK } = ctx.externs; + + console.log('this is athe puter sdk', puterSDK); + + if ( puterSDK.APIOrigin === undefined ) { + await ctx.externs.err.write('login: API origin not set\n'); + throw new Exit(1); + } + + const res = await puterSDK.auth.signIn(); + + ctx.vars.user = res?.username; + ctx.vars.home = '/' + res?.username; + ctx.vars.pwd = '/' + res?.username + `/AppData/` + puterSDK.appID; + + return res?.username; + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/ls.js b/packages/phoenix/src/puter-shell/coreutils/ls.js new file mode 100644 index 00000000..c30d8fbe --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/ls.js @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import columnify from "columnify"; +import cli_columns from "cli-columns"; +import { resolveRelativePath } from '../../util/path.js'; + +// formatLsTimestamp(): written by AI +function formatLsTimestamp(unixTimestamp) { + const date = new Date(unixTimestamp * 1000); // Convert Unix timestamp to JavaScript Date + const now = new Date(); + + const optionsCurrentYear = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }; + const optionsPreviousYear = { month: 'short', day: 'numeric', year: 'numeric' }; + + // Check if the year of the date is the same as the current year + if (date.getFullYear() === now.getFullYear()) { + // Format for current year + return date.toLocaleString('en-US', optionsCurrentYear) + .replace(',', ''); // Remove comma from time); + } else { + // Format for previous year + return date.toLocaleString('en-US', optionsPreviousYear) + .replace(',', ''); // Remove comma from time); + } +} + +const B_to_human_readable = B => { + const KiB = B / 1024; + const MiB = KiB / 1024; + const GiB = MiB / 1024; + const TiB = GiB / 1024; + if ( TiB > 1 ) { + return `${TiB.toFixed(3)} TiB`; + } else if ( GiB > 1 ) { + return `${GiB.toFixed(3)} GiB`; + } else if ( MiB > 1 ) { + return `${MiB.toFixed(3)} MiB`; + } else { + return `${KiB.toFixed(3)} KiB`; + } +} + +export default { + name: 'ls', + usage: 'ls [OPTIONS] [PATH...]', + description: 'List directory contents.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + all: { + description: 'List all entries, including those starting with `.`', + type: 'boolean', + short: 'a' + }, + long: { + description: 'List entries in long format, as a table', + type: 'boolean', + short: 'l' + }, + 'human-readable': { + description: 'Print sizes in a human readable format (eg 12MiB, 3GiB), instead of byte counts', + type: 'boolean', + short: 'h' + }, + time: { + description: 'Specify which time to display, one of atime (access time), ctime (creation time), or mtime (modification time)', + type: 'string' + }, + S: { + description: 'Sort the results', + type: 'boolean', + }, + t: { + description: 'Sort by time, newest first. See --time', + type: 'boolean', + }, + reverse: { + description: 'Reverse the sort direction', + type: 'boolean', + short: 'r', + }, + } + }, + execute: async ctx => { + console.log('ls context', ctx); + console.log('env.COLS', ctx.env.COLS); + // ctx.params to access processed args + // ctx.args to access raw args + const { positionals, values, pwd } = ctx.locals; + const { filesystem } = ctx.platform; + + const paths = positionals.length < 1 + ? [pwd] : positionals ; + + const showHeadings = paths.length > 1 ? async ({ i, path }) => { + if ( i !== 0 ) await ctx.externs.out.write('\n'); + await ctx.externs.out.write(path + ':\n'); + } : () => {}; + + for ( let i=0 ; i < paths.length ; i++ ) { + let path = paths[i]; + await showHeadings({ i, path }); + path = resolveRelativePath(ctx.vars, path); + let result = await filesystem.readdir(path); + console.log('ls items', result); + + if ( ! values.all ) { + result = result.filter(item => !item.name.startsWith('.')); + } + + const reverse_sort = values.reverse; + const decsort = (delegate) => { + if ( ! reverse_sort ) return delegate; + return (a, b) => -delegate(a, b); + }; + + const time_properties = { + mtime: 'modified', + ctime: 'created', + atime: 'accessed', + }; + + if ( values.t ) { + const timeprop = time_properties[values.time || 'mtime']; + result = result.sort(decsort((a, b) => { + return b[timeprop] - a[timeprop]; + })); + } + + if ( values.S ) { + result = result.sort(decsort((a, b) => { + if ( a.is_dir && !b.is_dir ) return 1; + if ( !a.is_dir && b.is_dir ) return -1; + return b.size - a.size; + })); + } + + // const write_item = values.long + // ? item => { + // let line = ''; + // line += item.is_dir ? 'd' : item.is_symlink ? 'l' : '-'; + // line += ' '; + // line += item.is_dir ? 'N/A' : item.size; + // line += ' '; + // line += item.name; + // return line; + // } + // : item => item.name + // + const icons = { + // d: '📁', + // l: '🔗', + }; + + const colors = { + 'd-': 'blue', + 'ds': 'magenta', + 'l-': 'cyan', + }; + + const col_to_ansi = { + blue: '34', + cyan: '36', + green: '32', + magenta: '35', + }; + + const col = (type, text) => { + if ( ! colors[type] ) return text; + return `\x1b[${col_to_ansi[colors[type]]};1m${text}\x1b[0m`; + } + + + const POSIX = filesystem.capabilities['readdir.posix-mode']; + + const simpleTypeForItem = (item) => { + return (item.is_dir ? 'd' : item.is_symlink ? 'l' : '-') + + ( (item.subdomains && item.subdomains.length) ? 's' : '-' ); + }; + + if ( values.long ) { + const time = values.time || 'mtime'; + const items = result.map(item => { + const ts = item[time_properties[time]]; + const www = + (!item.subdomains) ? 'N/A' : + (!item.subdomains.length) ? '---' : + item.subdomains[0].address + ( + item.subdomains.length > 1 + ? ` +${item.subdomains.length - 1}` + : '' + ) + const type = simpleTypeForItem(item); + const mode = POSIX ? item.mode_human_readable : null; + + let size = item.size; + if ( values['human-readable'] ) { + size = B_to_human_readable(size); + } + if ( item.is_dir && ! POSIX ) size = 'N/A'; + return { + ...item, + user: item.uid, + group: item.gid, + mode, + type: icons[type] || type, + name: col(type, item.name), + www: www, + size: size, + [time_properties[time]]: formatLsTimestamp(ts), + }; + }); + const text = columnify(items, { + columns: [ + POSIX ? 'mode' : 'type', + 'name', + ...(POSIX ? ['user', 'group'] : []), + ...(filesystem.capabilities['readdir.www'] ? ['www'] : []), + 'size', time_properties[time], + ], + maxLineWidth: ctx.env.COLS, + config: { + // json: { + // maxWidth: 20, + // } + } + }); + const lines = text.split('\n'); + for ( const line of lines ) { + await ctx.externs.out.write(line + '\n'); + } + continue; + } + + console.log('what is', cli_columns); + + const names = result.map(item => { + return col(simpleTypeForItem(item), item.name); + }); + const text = cli_columns(names, { + width: ctx.env.COLS, + }) + + const lines = text.split('\n'); + + for ( const line of lines ) { + await ctx.externs.out.write(line + '\n'); + } + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/man.js b/packages/phoenix/src/puter-shell/coreutils/man.js new file mode 100644 index 00000000..2c16c1de --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/man.js @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'man', + usage: 'man', + description: 'Stub command. Please use `help` instead.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + await ctx.externs.out.write('`\x1B[34;1mman\x1B[0m` is not supported. ' + + 'Please use `\x1B[34;1mhelp COMMAND\x1B[0m` for documentation.\n'); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/mkdir.js b/packages/phoenix/src/puter-shell/coreutils/mkdir.js new file mode 100644 index 00000000..f14d32e6 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/mkdir.js @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { validate_string } from "./coreutil_lib/validate.js"; +import { EMPTY } from "../../util/singleton.js"; +import { Exit } from './coreutil_lib/exit.js'; +import { resolveRelativePath } from '../../util/path.js'; + +// DRY: very similar to `cd` +export default { + name: 'mkdir', + usage: 'mkdir [OPTIONS] PATH', + description: 'Create a directory at PATH.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + parents: { + description: 'Create parent directories if they do not exist. Do not treat existing directories as an error', + type: 'boolean', + short: 'p' + } + } + }, + decorators: { errors: EMPTY }, + execute: async ctx => { + // ctx.params to access processed args + // ctx.args to access raw args + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + let [ target ] = positionals; + + try { + validate_string(target, { name: 'path' }); + } catch (e) { + await ctx.externs.err.write(`mkdir: ${e.message}\n`); + throw new Exit(1); + } + + target = resolveRelativePath(ctx.vars, target); + + const result = await filesystem.mkdir(target, { createMissingParents: values.parents }); + + if ( result && result.$ === 'error' ) { + await ctx.externs.err.write(`mkdir: ${result.message}\n`); + throw new Exit(1); + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/mv.js b/packages/phoenix/src/puter-shell/coreutils/mv.js new file mode 100644 index 00000000..bf05b50e --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/mv.js @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; +import { resolveRelativePath } from '../../util/path.js'; + +export default { + name: 'mv', + usage: 'mv SOURCE DESTINATION', + description: 'Move SOURCE file or directory to DESTINATION.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + const { positionals } = ctx.locals; + const { out, err } = ctx.externs; + const { filesystem } = ctx.platform; + + if ( positionals.length < 1 ) { + await err.write('mv: missing file operand\n'); + throw new Exit(1); + } + + const srcRelPath = positionals.shift(); + + if ( positionals.length < 1 ) { + const aft = positionals[0]; + await err.write(`mv: missing destination file operand after '${aft}'\n`); + throw new Exit(1); + } + + const dstRelPath = positionals.shift(); + + const srcAbsPath = resolveRelativePath(ctx.vars, srcRelPath); + let dstAbsPath = resolveRelativePath(ctx.vars, dstRelPath); + + await filesystem.move(srcAbsPath, dstAbsPath); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/neofetch.js b/packages/phoenix/src/puter-shell/coreutils/neofetch.js new file mode 100644 index 00000000..2906b842 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/neofetch.js @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { SHELL_VERSIONS } from "../../meta/versions.js"; + +const logo = ` + ▄████▄ ▄▄█████▄_ + _▄███▀▀██▀ ▐██▀¬ '▀█▄ + ╓██└ ▐█▀ ▀█ + (█▀ █▌ ██ + █▌ ▄██▀▀▀██▄ ▀██▄ + ██ ▐█ ██ + █▌ ▐█ + ▀█▄_ ▄█▀ + '▀▀████ █████ ████▌ ██▀▀ + ▐█ ██ █▌ + ▐█ ██ █▌ + _▄_▄██\` ██ '▀█▄_▄_ + ╒█▀▀▀█▌ ██ ▐█▀\`▀█▄ + ▐█▄_▄█▌ ╓████▄ ▐█▄_▄█▌ + ▀▀▀\` █▌ ▐█ ▀▀▀\` + '▀███▀ +`.slice(1); + +export default { + name: 'neofetch', + usage: 'neofetch', + description: 'Print information about the system.', + execute: async ctx => { + const cols = [17,18,19,26,27].reverse(); + const C25 = n => `\x1B[38;5;${n}m`; + const B25 = n => `\x1B[48;5;${n}m`; + const COL = C25(27); + const END = "\x1B[0m"; + const lines = logo.split('\n').map(line => { + while ( line.length < 40 ) line += ' '; + return line; + }); + + for ( let i=0 ; i < lines.length ; i++ ) { + let ind = Math.floor(i / 5); + const col = cols[ind]; + lines[i] = `\x1B[38;5;${col}m` + lines[i] + END; + } + + { + const org = lines[9]; + lines[9] = org.slice(0, 34) + C25(cols[2]) + org.slice(34); + } + { + let org = lines[10]; + org = org.slice(10); + lines[10] = C25(cols[1]) + org.slice(0, 12) + + C25(cols[2]) + org.slice(12); + } + + lines[0] += COL + ctx.env.USER + END + '@' + + COL + 'puter.com' + END; + lines[1] += '-----------------'; + lines[2] += COL + 'OS' + END + ': Puter' + lines[3] += COL + 'Shell' + END + ': Puter Shell v' + SHELL_VERSIONS[0].v + lines[4] += COL + 'Window' + END + `: ${ctx.env.COLS}x${ctx.env.ROWS}` + lines[5] += COL + 'Commands' + END + `: ${Object.keys(ctx.registries.builtins).length}` + + const colors = [[],[]]; + for ( let i=0 ; i < 16 ; i++ ) { + let ri = i < 8 ? 14 : 15; + let esc = i < 9 + ? `\x1B[3${i}m\x1B[4${i}m` + : C25(i)+B25(i) ; + lines[ri] += esc + ' '; + } + lines[14] += '\x1B[0m'; + lines[15] += '\x1B[0m'; + + for ( const line of lines ) { + await ctx.externs.out.write(line + '\n'); + } + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/printf.js b/packages/phoenix/src/puter-shell/coreutils/printf.js new file mode 100644 index 00000000..0a99d87d --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/printf.js @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +// TODO: get these values from a common place +// DRY: Copied from echo_escapes.js +const BEL = String.fromCharCode(7); +const BS = String.fromCharCode(8); +const VT = String.fromCharCode(0x0B); +const FF = String.fromCharCode(0x0C); + +function parseFormat(input, startOffset) { + let i = startOffset; + + if (input[i] !== '%') { + throw new Error('Called parseFormat() without a format specifier!'); + } + i++; + + const result = { + flags: { + leftJustify: false, + prefixWithSign: false, + prefixWithSpaceIfWithoutSign: false, + alternativeForm: false, + padWithLeadingZeroes: false, + }, + fieldWidth: null, + precision: null, + conversionSpecifier: null, + + newOffset: startOffset, + }; + + // Output a single % for '%%' or '%' followed by the end of the input. + if (input[i] === '%') { + i++; + result.conversionSpecifier = '%'; + result.newOffset = i; + return result; + } + + const consumeInteger = () => { + const startIndex = i; + while (input[i] >= '0' && input[i] <= '9') { + i++; + } + if (startIndex === i) { + return null; + } + + const integerString = input.substring(startIndex, i); + return Number.parseInt(integerString, 10); + }; + + // Flags + const possibleFlags = '-+ #0'; + while (possibleFlags.includes(input[i])) { + switch (input[i]) { + case '-': result.flags.leftJustify = true; break; + case '+': result.flags.prefixWithSign = true; break; + case ' ': result.flags.prefixWithSpaceIfWithoutSign = true; break; + case '#': result.flags.alternativeForm = true; break; + case '0': result.flags.padWithLeadingZeroes = true; break; + } + i++; + } + + // Field width + result.fieldWidth = consumeInteger(); + + // Precision + if (input[i] === '.') { + i++; + result.precision = consumeInteger() || 0; + } + + // Conversion specifier + const possibleConversionSpecifiers = 'cdeEfFgGiousxX'; + if (possibleConversionSpecifiers.includes(input[i])) { + result.conversionSpecifier = input[i]; + i++; + } else { + throw new Error(`Invalid conversion specifier '${input.substring(startOffset, i + 1)}'`); + } + + result.newOffset = i; + return result; +} + +function formatOutput(parsedFormat, remainingArguments) { + const { flags, fieldWidth, precision, conversionSpecifier } = parsedFormat; + + const padAndAlignString = (input) => { + if (!fieldWidth || input.length >= fieldWidth) { + return input; + } + + const padding = ' '.repeat(fieldWidth - input.length); + return flags.leftJustify ? (input + padding) : (padding + input); + }; + + const formatInteger = (integer, specifier) => { + const unsigned = 'ouxX'.includes(specifier); + const radix = (() => { + switch (specifier) { + case 'o': return 8; + case 'x': + case 'X': return 16; + default: return 10; + } + })(); + + // POSIX doesn't specify what we should do to format a negative number as %u. + // Common behavior seems to be bit-casting it to unsigned. + if (unsigned && integer < 0) { + integer = integer >>> 0; + } + + let digits = Math.abs(integer).toString(radix); + if (specifier === 'o' && flags.alternativeForm && digits[0] !== '0') { + // "For the o conversion specifier, it shall increase the precision to force the first digit of the result to be a zero." + // (Where 'it' is the alternative form flag.) + digits = '0' + digits; + } + const signOrPrefix = (() => { + if (flags.alternativeForm) { + if (specifier === 'x') return '0x'; + if (specifier === 'X') return '0X'; + } + if (unsigned) return ''; + if (integer < 0) return '-'; + if (flags.prefixWithSign) return '+'; + if (flags.prefixWithSpaceIfWithoutSign) return ' '; + return ''; + })(); + + // Expand digits with 0s, up to `precision` characters. + // "The default precision shall be 1." + const usedPrecision = precision ?? 1; + // Special case: "The result of converting a zero value with a precision of 0 shall be no characters." + if (usedPrecision === 0 && integer === 0) { + digits = ''; + } else if (digits.length < precision) { + digits = '0'.repeat(precision - digits.length) + digits; + } + + // Pad up to `fieldWidth` with spaces or 0s. + const width = signOrPrefix.length + digits.length; + let output = signOrPrefix + digits; + if (width < fieldWidth) { + if (flags.leftJustify) { + output = signOrPrefix + digits + ' '.repeat(fieldWidth - width); + } else if (precision === null && flags.padWithLeadingZeroes) { + // "For d, i , o, u, x, and X conversion specifiers, if a precision is specified, the '0' flag shall be ignored." + output = signOrPrefix + '0'.repeat(fieldWidth - width) + digits; + } else { + output = ' '.repeat(fieldWidth - width) + signOrPrefix + digits; + } + } + + if (specifier === specifier.toUpperCase()) { + output = output.toUpperCase(); + } + + return output; + }; + + const formatFloat = (float, specifier) => { + if (float === undefined) float = 0; + + const sign = (() => { + if (float < 0) return '-'; + if (flags.prefixWithSign) return '+'; + if (flags.prefixWithSpaceIfWithoutSign) return ' '; + return ''; + })(); + const floatString = (() => { + // NaN and Infinity are the same regardless of representation + if (!isFinite(float)) { + return float.toString(); + } + + const formatExponential = (mantissaString, exponent) => { + // #: "For [...] e, E, [...] conversion specifiers, the result shall always contain a radix character, + // even if no digits follow the radix character." + if (flags.alternativeForm && !mantissaString.includes('.')) { + mantissaString += '.'; + } + + // "The exponent shall always contain at least two digits." + const exponentOutput = (() => { + if (exponent <= -10 || exponent >= 10) return exponent.toString(); + if (exponent < 0) return '-0' + Math.abs(exponent).toString(); + return '+0' + Math.abs(exponent).toString(); + })(); + return mantissaString + 'e' + exponentOutput; + }; + + switch (specifier) { + // TODO: %a and %A, floats in hexadecimal + case 'e': + case 'E': { + // "When the precision is missing, six digits shall be written after the radix character" + const usedPrecision = precision ?? 6; + // We unfortunately can't fully rely on toExponential() because printf has different formatting rules. + const [mantissaString, exponentString] = Math.abs(float).toExponential(usedPrecision).split('e'); + const exponent = Number.parseInt(exponentString); + return formatExponential(mantissaString, exponent); + } + case 'f': + case 'F': { + // "If the precision is omitted from the argument, six digits shall be written after the radix character" + const usedPrecision = precision ?? 6; + const result = Math.abs(float).toFixed(usedPrecision); + if (flags.alternativeForm && usedPrecision === 0) { + // #: "For [...] f, F, [...] conversion specifiers, the result shall always contain a radix character, + // even if no digits follow the radix character." + return result + '.'; + } + return result; + } + case 'g': + case 'G': { + // Default isn't specified in the spec, but 6 matches behavior of other implementations. + const usedPrecision = precision ?? 6; + + // "The style used depends on the value converted: style e (or E) shall be used only if the exponent + // resulting from the conversion is less than -4 or greater than or equal to the precision." + // We add a digit of precision to make sure we don't break things when rounding later. + const [mantissaString, exponentString] = Math.abs(float).toExponential(usedPrecision + 1).split('e'); + const mantissa = Number.parseFloat(mantissaString); + const exponent = Number.parseInt(exponentString); + + // Unfortunately, `float.toPrecision()` doesn't use the same rules as printf to decide whether to + // use decimal or exponential representation, so we have to construct the output ourselves. + const usingExponential = exponent > usedPrecision || exponent < -4; + if (usingExponential) { + const decimalDigits = Math.max(0, usedPrecision - (mantissa < 1 ? 0 : 1)); + // "Trailing zeros are removed from the result." + let mantissaOutput = mantissa.toFixed(decimalDigits) + .replace(/\.0+/, ''); + return formatExponential(mantissaOutput, exponent); + } + + // Decimal representation + const result = Math.abs(float).toPrecision(usedPrecision); + if (flags.alternativeForm && usedPrecision === 0) { + // #: "For [...] g, and G conversion specifiers, the result shall always contain a radix character, + // even if no digits follow the radix character." + return result + '.'; + } + // Trailing zeros are removed from the result. + return result.replace(/\.0+/, ''); + } + default: throw new Error(`Invalid float specifier '${specifier}'`); + } + })(); + + // Pad up to `fieldWidth` with spaces or 0s. + const width = sign.length + floatString.length; + let output = sign + floatString; + if (width < fieldWidth) { + if (flags.leftJustify) { + output = sign + floatString + ' '.repeat(fieldWidth - width); + } else if (flags.padWithLeadingZeroes && isFinite(float)) { + output = sign + '0'.repeat(fieldWidth - width) + floatString; + } else { + output = ' '.repeat(fieldWidth - width) + sign + floatString; + } + } + + if (specifier === specifier.toUpperCase()) { + output = output.toUpperCase(); + } else { + output = output.toLowerCase(); + } + + return output; + }; + + switch (conversionSpecifier) { + // TODO: a,A: Float in hexadecimal format + // TODO: b: binary data with escapes + // TODO: Any other common options that are not in the posix spec + + // Integers + case 'd': + case 'i': + case 'o': + case 'u': + case 'x': + case 'X': { + return formatInteger(Number.parseInt(remainingArguments.shift()) || 0, conversionSpecifier); + } + + // Floating point numbers + case 'e': + case 'E': + case 'f': + case 'F': + case 'g': + case 'G': { + return formatFloat(Number.parseFloat(remainingArguments.shift()), conversionSpecifier); + } + + // Single character + case 'c': { + const argument = remainingArguments.shift() || ''; + // It's unspecified whether an empty string produces a null byte or nothing. + // We'll go with nothing for now. + return padAndAlignString(argument[0] || ''); + } + + // String + case 's': { + let argument = remainingArguments.shift() || ''; + if (precision && precision < argument.length) { + argument = argument.substring(0, precision); + } + return padAndAlignString(argument); + } + + // Percent sign + case '%': return '%'; + } +} + +function highlight(text) { + return `\x1B[92m${text}\x1B[0m`; +} + +// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html +export default { + name: 'printf', + usage: 'printf FORMAT [ARGUMENT...]', + description: 'Write a formatted string to standard output.\n\n' + + 'The output is determined by FORMAT, with any escape sequences replaced, and any format strings applied to the following ARGUMENTs.\n\n' + + 'FORMAT is written repeatedly until all ARGUMENTs are consumed. If FORMAT does not consume any ARGUMENTs, it is only written once.', + helpSections: { + 'Escape Sequences': 'The following escape sequences are understood:\n\n' + + ` ${highlight('\\\\')} A literal \\\n` + + ` ${highlight('\\a')} Terminal BELL\n` + + ` ${highlight('\\b')} Backspace\n` + + ` ${highlight('\\f')} Form-feed\n` + + ` ${highlight('\\n')} Newline\n` + + ` ${highlight('\\r')} Carriage return\n` + + ` ${highlight('\\t')} Horizontal tab\n` + + ` ${highlight('\\v')} Vertical tab\n` + + ` ${highlight('\\###')} A byte with the octal value of ### (between 1 and 3 digits)`, + 'Format Strings': 'Format strings behave like C printf. ' + + 'A format string is, in order: a `%`, zero or more flags, a width, a precision, and a conversion specifier. ' + + 'All except the initial `%` and the conversion specifier are optional.\n\n' + + 'Flags:\n\n' + + ` ${highlight('-')} Left-justify the result\n` + + ` ${highlight('+')} For numeric types, always include a sign character\n` + + ` ${highlight('\' \'')} ${highlight('(space)')} For numeric types, include a space where the sign would go for positive numbers. Overridden by ${highlight('+')}.\n`+ + ` ${highlight('#')} Use alternative form, depending on the conversion:\n` + + ` ${highlight('o')} Ensure result is always prefixed with a '0'\n` + + ` ${highlight('x,X')} Prefix result with '0x' or '0X' respectively\n` + + ` ${highlight('e,E,f,F,g,G')} Always include a decimal point. For ${highlight('g,G')}, also keep trailing 0s\n\n` + + 'Width:\n\n' + + 'A number, for how many characters the result should occupy.\n\n' + + 'Precision:\n\n' + + 'A \'.\' followed optionally by a number. If no number is specified, it is taken as 0. Effect depends on the conversion:\n\n' + + ` ${highlight('d,i,o,u,x,X')} Determines the minimum number of digits\n` + + ` ${highlight('e,E,f,F')} Determines the number of digits after the decimal point\n\n` + + ` ${highlight('g,G')} Determines the number of significant figures\n\n` + + ` ${highlight('s')} Determines the maximum number of characters to be printed\n\n` + + 'Conversion specifiers:\n\n' + + ` ${highlight('%')} A literal '%'\n` + + ` ${highlight('s')} ARGUMENT as a string\n` + + ` ${highlight('c')} The first character of ARGUMENT as a string\n` + + ` ${highlight('d,i')} ARGUMENT as a number, formatted as a signed decimal integer\n` + + ` ${highlight('u')} ARGUMENT as a number, formatted as an unsigned decimal integer\n` + + ` ${highlight('o')} ARGUMENT as a number, formatted as an unsigned octal integer\n` + + ` ${highlight('x,X')} ARGUMENT as a number, formatted as an unsigned hexadecimal integer, in lower or uppercase respectively\n` + + ` ${highlight('e,E')} ARGUMENT as a number, formatted as a float in exponential notation, in lower or uppercase respectively\n` + + ` ${highlight('f,F')} ARGUMENT as a number, formatted as a float in decimal notation, in lower or uppercase respectively\n` + + ` ${highlight('g,G')} ARGUMENT as a number, formatted as a float in either decimal or exponential notation, in lower or uppercase respectively`, + }, + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + const { out, err } = ctx.externs; + const { positionals } = ctx.locals; + const [ format, ...remainingArguments ] = ctx.locals.positionals; + + if (positionals.length === 0) { + await err.write('printf: Missing format argument\n'); + throw new Exit(1); + } + + // We process the format as many times as needed to consume all of remainingArguments, but always at least once. + do { + const previousRemainingArgumentCount = remainingArguments.length; + let output = ''; + + for (let i = 0; i < format.length; ++i) { + let char = format[i]; + // Escape sequences + if (char === '\\') { + char = format[++i]; + switch (char) { + case undefined: { + // We reached the end of the string, just output the slash. + output += '\\'; + break; + } + case '\\': output += '\\'; break; + case 'a': output += BEL; break; + case 'b': output += BS; break; + case 'f': output += FF; break; + case 'n': output += '\n'; break; + case 'r': output += '\r'; break; + case 't': output += '\t'; break; + case 'v': output += VT; break; + default: { + // 1 to 3-digit octal number + if (char >= '0' && char <= '9') { + const digitsStartI = i; + if (format[i+1] >= '0' && format[i+1] <= '9') { + i++; + if (format[i+1] >= '0' && format[i+1] <= '9') { + i++; + } + } + + const octalString = format.substring(digitsStartI, i + 1); + const octalValue = Number.parseInt(octalString, 8); + output += String.fromCodePoint(octalValue); + break; + } + + // Unrecognized, so just output the sequence verbatim. + output += '\\' + char; + break; + } + } + continue; + } + + // Conversion specifiers + if (char === '%') { + // Parse the conversion specifier + let parsedFormat; + try { + parsedFormat = parseFormat(format, i); + } catch (e) { + await err.write(`printf: ${e.message}\n`); + throw new Exit(1); + } + i = parsedFormat.newOffset - 1; // -1 because we're about to increment i in the loop header + + // Output the result + output += formatOutput(parsedFormat, remainingArguments); + continue; + } + + // Everything else is copied directly. + // TODO: Append these to the output in batches, for performance? + output += char; + } + + await out.write(output); + + // "If the format operand contains no conversion specifications and argument operands are present, the results are unspecified." + // We handle this by printing it once and stopping. + if (remainingArguments.length === previousRemainingArgumentCount) { + break; + } + } while (remainingArguments.length > 0); + + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/printhist.js b/packages/phoenix/src/puter-shell/coreutils/printhist.js new file mode 100644 index 00000000..d740fcc6 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/printhist.js @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'printhist', + usage: 'printhist', + description: 'Print shell history.', + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { historyManager } = ctx.externs; + console.log('test????', ctx); + for ( const item of historyManager.items ) { + await ctx.externs.out.write(item + '\n'); + } + } +} \ No newline at end of file diff --git a/packages/phoenix/src/puter-shell/coreutils/pwd.js b/packages/phoenix/src/puter-shell/coreutils/pwd.js new file mode 100644 index 00000000..1fd5907f --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/pwd.js @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +export default { + name: 'pwd', + usage: 'pwd', + description: 'Print the current working directory.', + args: { + $: 'simple-parser', + allowPositionals: false, + }, + execute: async ctx => { + await ctx.externs.out.write(ctx.vars.pwd + '\n'); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/rm.js b/packages/phoenix/src/puter-shell/coreutils/rm.js new file mode 100644 index 00000000..358e41b5 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/rm.js @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { resolveRelativePath } from '../../util/path.js'; + +// TODO: add logic to check if directory is empty +// TODO: add check for `--dir` +// TODO: allow multiple paths + +// DRY: very similar to `cd` +export default { + name: 'rm', + usage: 'rm [OPTIONS] PATH', + description: 'Remove the file or directory at PATH.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + dir: { + description: 'Remove empty directories', + type: 'boolean', + short: 'd' + }, + recursive: { + description: 'Recursively remove directories and their contents', + type: 'boolean', + short: 'r' + }, + force: { + description: 'Ignore non-existent paths, and never prompt', + type: 'boolean', + short: 'f' + } + } + }, + execute: async ctx => { + // ctx.params to access processed args + // ctx.args to access raw args + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + let [ target ] = positionals; + target = resolveRelativePath(ctx.vars, target); + + await filesystem.rm(target, { recursive: values.recursive }) + } +}; + + diff --git a/packages/phoenix/src/puter-shell/coreutils/rmdir.js b/packages/phoenix/src/puter-shell/coreutils/rmdir.js new file mode 100644 index 00000000..d03048f0 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/rmdir.js @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { resolveRelativePath } from '../../util/path.js'; + +// TODO: add logic to check if directory is empty +// TODO: add check for `--dir` +// TODO: allow multiple paths + +// DRY: very similar to `cd` +export default { + name: 'rmdir', + usage: 'rmdir [OPTIONS] DIRECTORY', + description: 'Remove the DIRECTORY if it is empty.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + parents: { + description: 'Also remove empty parent directories', + type: 'boolean', + short: 'p' + } + } + }, + execute: async ctx => { + // ctx.params to access processed args + // ctx.args to access raw args + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + let [ target ] = positionals; + target = resolveRelativePath(ctx.vars, target); + + await filesystem.rmdir(target); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/sample-data.js b/packages/phoenix/src/puter-shell/coreutils/sample-data.js new file mode 100644 index 00000000..039b6900 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sample-data.js @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'sample-data', + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { positionals } = ctx.locals; + const [ what ] = positionals; + + if ( what === 'blob' ) { + // Hello world blob + const blob = new Blob([ 'Hello, world!' ]); + console.log('before writing'); + await ctx.externs.out.write(blob); + console.log('after writing'); + return; + } + + console.log('before writing'); + await ctx.externs.out.write('Hello, World!\n'); + console.log('after writing'); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/sed.js b/packages/phoenix/src/puter-shell/coreutils/sed.js new file mode 100644 index 00000000..99d46e9a --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sed.js @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; +import { fileLines } from '../../util/file.js'; + +function makeIndent(size) { + return ' '.repeat(size); +} + +// Either a line number or a regex +class Address { + constructor(value) { + this.value = value; + } + + matches(lineNumber, line) { + if (this.value instanceof RegExp) { + return this.value.test(line); + } + return this.value === lineNumber; + } + + isLineNumberBefore(lineNumber) { + return (typeof this.value === 'number') && this.value < lineNumber; + } + + dump(indent) { + if (this.value instanceof RegExp) { + return `${makeIndent(indent)}REGEX: ${this.value}\n`; + } + return `${makeIndent(indent)}LINE: ${this.value}\n`; + } +} + +class AddressRange { + // Three kinds of AddressRange: + // - Empty (includes everything) + // - Single (matches individual line) + // - Range (matches lines between start and end, inclusive) + constructor({ start, end, inverted = false } = {}) { + this.start = start; + this.end = end; + this.inverted = inverted; + this.insideRange = false; + this.leaveRangeNextLine = false; + } + + updateMatchState(lineNumber, line) { + // Only ranges have a state to update + if (!(this.start && this.end)) { + return; + } + + // Reset our state each time we start a new file. + if (lineNumber === 1) { + this.insideRange = false; + this.leaveRangeNextLine = false; + } + + // Leave the range if the previous line matched the end. + if (this.leaveRangeNextLine) { + this.insideRange = false; + this.leaveRangeNextLine = false; + } + + if (this.insideRange) { + // We're inside the range, does this line end it? + // If the end address is a line number in the past, yes, immediately. + if (this.end.isLineNumberBefore(lineNumber)) { + this.insideRange = false; + return; + } + // If the line matches the end address, include it but leave the range on the next line. + this.leaveRangeNextLine = this.end.matches(lineNumber, line); + } else { + // Does this line start the range? + this.insideRange = this.start.matches(lineNumber, line); + } + } + + matches(lineNumber, line) { + const invertIfNeeded = (value) => { + return this.inverted ? !value : value; + }; + + // Empty - matches all lines + if (!this.start) { + return invertIfNeeded(true); + } + + // Range + if (this.end) { + return invertIfNeeded(this.insideRange); + } + + // Single + return invertIfNeeded(this.start.matches(lineNumber, line)); + } + + dump(indent) { + const inverted = this.inverted ? `${makeIndent(indent+1)}(INVERTED)\n` : ''; + + if (!this.start) { + return `${makeIndent(indent)}ADDRESS RANGE (EMPTY)\n` + + inverted; + } + + if (this.end) { + return `${makeIndent(indent)}ADDRESS RANGE (RANGE):\n` + + inverted + + this.start.dump(indent+1) + + this.end.dump(indent+1); + } + + return `${makeIndent(indent)}ADDRESS RANGE (SINGLE):\n` + + this.start.dump(indent+1) + + inverted; + } +} + +const JumpLocation = { + None: Symbol('None'), + EndOfCycle: Symbol('EndOfCycle'), + StartOfCycle: Symbol('StartOfCycle'), + Label: Symbol('Label'), + Quit: Symbol('Quit'), + QuitSilent: Symbol('QuitSilent'), +}; + +class Command { + constructor(addressRange) { + this.addressRange = addressRange ?? new AddressRange(); + } + + updateMatchState(context) { + this.addressRange.updateMatchState(context.lineNumber, context.patternSpace); + } + + async runCommand(context) { + if (this.addressRange.matches(context.lineNumber, context.patternSpace)) { + return await this.run(context); + } + return JumpLocation.None; + } + + async run(context) { + throw new Error('run() not implemented for ' + this.constructor.name); + } + + dump(indent) { + throw new Error('dump() not implemented for ' + this.constructor.name); + } +} + +// '{}' - Group other commands +class GroupCommand extends Command { + constructor(addressRange, subCommands) { + super(addressRange); + this.subCommands = subCommands; + } + + updateMatchState(context) { + super.updateMatchState(context); + for (const command of this.subCommands) { + command.updateMatchState(context); + } + } + + async run(context) { + for (const command of this.subCommands) { + const result = await command.runCommand(context); + if (result !== JumpLocation.None) { + return result; + } + } + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}GROUP:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CHILDREN:\n` + + this.subCommands.map(command => command.dump(indent+2)).join(''); + } +} + +// '=' - Output line number +class LineNumberCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + await context.out.write(`${context.lineNumber}\n`); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}LINE-NUMBER:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'a' - Append text +class AppendTextCommand extends Command { + constructor(addressRange, text) { + super(addressRange); + this.text = text; + } + + async run(context) { + context.queuedOutput += this.text + '\n'; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}APPEND-TEXT:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; + } +} + +// 'c' - Replace line with text +class ReplaceCommand extends Command { + constructor(addressRange, text) { + super(addressRange); + this.text = text; + } + + async run(context) { + context.patternSpace = ''; + // Output if we're either a 0-address range, 1-address range, or 2-address on the last line. + if (this.addressRange.leaveRangeNextLine || !this.addressRange.end) { + await context.out.write(this.text + '\n'); + } + return JumpLocation.EndOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}REPLACE-TEXT:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; + } +} + +// 'd' - Delete pattern +class DeleteCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace = ''; + return JumpLocation.EndOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}DELETE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'D' - Delete first line of pattern +class DeleteLineCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + const [ firstLine, rest ] = context.patternSpace.split('\n', 2); + context.patternSpace = rest ?? ''; + if (rest === undefined) { + return JumpLocation.EndOfCycle; + } + return JumpLocation.StartOfCycle; + } + + dump(indent) { + return `${makeIndent(indent)}DELETE-LINE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'g' - Get the held line into the pattern +class GetCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace = context.holdSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}GET-HELD:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'G' - Get the held line and append it to the pattern +class GetAppendCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace += '\n' + context.holdSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}GET-HELD-APPEND:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'h' - Hold the pattern +class HoldCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.holdSpace = context.patternSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}HOLD:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'H' - Hold append the pattern +class HoldAppendCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.holdSpace += '\n' + context.patternSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}HOLD-APPEND:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'i' - Insert text +class InsertTextCommand extends Command { + constructor(addressRange, text) { + super(addressRange); + this.text = text; + } + + async run(context) { + await context.out.write(this.text + '\n'); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}INSERT-TEXT:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`; + } +} + +// 'l' - Print pattern in debug format +class DebugPrintCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + let output = ''; + for (const c of context.patternSpace) { + if (c < ' ') { + const charCode = c.charCodeAt(0); + switch (charCode) { + case 0x07: output += '\\a'; break; + case 0x08: output += '\\b'; break; + case 0x0C: output += '\\f'; break; + case 0x0A: output += '$\n'; break; + case 0x0D: output += '\\r'; break; + case 0x09: output += '\\t'; break; + case 0x0B: output += '\\v'; break; + default: { + const octal = charCode.toString(8); + output += '\\' + '0'.repeat(3 - octal.length) + octal; + } + } + } else if (c === '\\') { + output += '\\\\'; + } else { + output += c; + } + } + await context.out.write(output); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}DEBUG-PRINT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'p' - Print pattern +class PrintCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + await context.out.write(context.patternSpace); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}PRINT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'P' - Print first line of pattern +class PrintLineCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + const firstLine = context.patternSpace.split('\n', 2)[0]; + await context.out.write(firstLine); + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}PRINT-LINE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'q' - Quit +class QuitCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + return JumpLocation.Quit; + } + + dump(indent) { + return `${makeIndent(indent)}QUIT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'Q' - Quit, suppressing the default output +class QuitSilentCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + return JumpLocation.QuitSilent; + } + + dump(indent) { + return `${makeIndent(indent)}QUIT-SILENT:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'x' - Exchange hold and pattern +class ExchangeCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + const oldPattern = context.patternSpace; + context.patternSpace = context.holdSpace; + context.holdSpace = oldPattern; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}EXCHANGE:\n` + + this.addressRange.dump(indent+1); + } +} + +// 'y' - Transliterate characters +class TransliterateCommand extends Command { + constructor(addressRange, inputCharacters, replacementCharacters) { + super(addressRange); + this.inputCharacters = inputCharacters; + this.replacementCharacters = replacementCharacters; + + if (inputCharacters.length !== replacementCharacters.length) { + throw new Error('inputCharacters and replacementCharacters must be the same length!'); + } + } + + async run(context) { + let newPatternSpace = ''; + for (let i = 0; i < context.patternSpace.length; ++i) { + const char = context.patternSpace[i]; + const replacementIndex = this.inputCharacters.indexOf(char); + if (replacementIndex !== -1) { + newPatternSpace += this.replacementCharacters[replacementIndex]; + continue; + } + newPatternSpace += char; + } + context.patternSpace = newPatternSpace; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}TRANSLITERATE:\n` + + this.addressRange.dump(indent+1) + + `${makeIndent(indent+1)}FROM '${this.inputCharacters}'\n` + + `${makeIndent(indent+1)}TO '${this.replacementCharacters}'\n`; + } +} + +// 'z' - Zap, delete the pattern without ending cycle +class ZapCommand extends Command { + constructor(addressRange) { + super(addressRange); + } + + async run(context) { + context.patternSpace = ''; + return JumpLocation.None; + } + + dump(indent) { + return `${makeIndent(indent)}ZAP:\n` + + this.addressRange.dump(indent+1); + } +} + +const CycleResult = { + Continue: Symbol('Continue'), + Quit: Symbol('Quit'), + QuitSilent: Symbol('QuitSilent'), +}; + +class Script { + constructor(commands) { + this.commands = commands; + } + + async runCycle(context) { + for (let i = 0; i < this.commands.length; i++) { + const command = this.commands[i]; + command.updateMatchState(context); + const result = await command.runCommand(context); + switch (result) { + case JumpLocation.Label: + // TODO: Implement labels + break; + case JumpLocation.Quit: + return CycleResult.Quit; + case JumpLocation.QuitSilent: + return CycleResult.QuitSilent; + case JumpLocation.StartOfCycle: + i = -1; // To start at 0 after the loop increment. + continue; + case JumpLocation.EndOfCycle: + return CycleResult.Continue; + case JumpLocation.None: + continue; + } + } + } + + dump() { + return `SCRIPT:\n` + + this.commands.map(command => command.dump(1)).join(''); + } +} + +function parseScript(scriptString) { + const commands = []; + + // Generate a hard-coded script for now. + // TODO: Actually parse input! + + commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef')); + // commands.push(new ZapCommand(new AddressRange({start: new Address(1), end: new Address(10)}))); + // commands.push(new HoldAppendCommand(new AddressRange({start: new Address(1), end: new Address(10)}))); + // commands.push(new GetCommand(new AddressRange({start: new Address(11)}))); + // commands.push(new DebugPrintCommand(new AddressRange())); + + // commands.push(new ReplaceCommand(new AddressRange({start: new Address(3), end: new Address(30)}), "LOL")); + + // commands.push(new GroupCommand(new AddressRange({ start: new Address(5), end: new Address(10) }), [ + // // new LineNumberCommand(), + // // new TextCommand(new AddressRange({ start: new Address(8) }), "Well hello friends! :^)"), + // new QuitCommand(new AddressRange({ start: new Address(8) })), + // new NoopCommand(new AddressRange()), + // new PrintCommand(new AddressRange({ start: new Address(2), end: new Address(14) })), + // ])); + + // commands.push(new LineNumberCommand(new AddressRange({ start: new Address(5), end: new Address(10) }))); + // commands.push(new PrintCommand()); + // commands.push(new NoopCommand()); + // commands.push(new PrintCommand()); + + return new Script(commands); +} + +export default { + name: 'sed', + usage: 'sed [OPTIONS] [SCRIPT] FILE...', + description: 'Filter and transform text, line by line.\n\n' + + 'Treats the first positional argument as the SCRIPT if no -e options are provided. ' + + 'If a FILE is `-`, read standard input.', + input: { + syncLines: true + }, + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + expression: { + description: 'Specify an additional script to execute. May be specified multiple times.', + type: 'string', + short: 'e', + multiple: true, + default: [], + }, + quiet: { + description: 'Suppress default printing of selected lines.', + type: 'boolean', + short: 'n', + default: false, + }, + } + }, + execute: async ctx => { + const { out, err } = ctx.externs; + const { positionals, values } = ctx.locals; + + if (positionals.length < 1) { + await err.write('sed: No inputs given\n'); + throw new Exit(1); + } + + // "If any -e or -f options are specified, the script of editing commands shall initially be empty. The commands + // specified by each -e or -f option shall be added to the script in the order specified. When each addition is + // made, if the previous addition (if any) was from a -e option, a shall be inserted before the new + // addition. The resulting script shall have the same properties as the script operand, described in the + // OPERANDS section." + // TODO: -f loads scripts from a file + let scriptString = ''; + if (values.expression.length > 0) { + scriptString = values.expression.join('\n'); + } else { + scriptString = positionals.shift(); + } + + const script = parseScript(scriptString); + await out.write(script.dump()); + + const context = { + out: out, + patternSpace: '', + holdSpace: '\n', + lineNumber: 1, + queuedOutput: '', + } + + // All remaining positionals are file paths to process. + for (const relPath of positionals) { + context.lineNumber = 1; + for await (const line of fileLines(ctx, relPath)) { + context.patternSpace = line.replace(/\n$/, ''); + const result = await script.runCycle(context); + switch (result) { + case CycleResult.Quit: { + if (!values.quiet) { + await out.write(context.patternSpace + '\n'); + } + return; + } + case CycleResult.QuitSilent: { + return; + } + } + if (!values.quiet) { + await out.write(context.patternSpace + '\n'); + } + if (context.queuedOutput) { + await out.write(context.queuedOutput + '\n'); + context.queuedOutput = ''; + } + context.lineNumber++; + } + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/sleep.js b/packages/phoenix/src/puter-shell/coreutils/sleep.js new file mode 100644 index 00000000..9023053f --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sleep.js @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'sleep', + usage: 'sleep TIME', + description: 'Pause for at least TIME seconds, where TIME is a positive number.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + const { positionals } = ctx.locals; + if (positionals.length !== 1) { + await ctx.externs.err.write('sleep: Exactly one TIME parameter is required'); + throw new Exit(1); + } + + let time = Number.parseFloat(positionals[0]); + if (isNaN(time) || time < 0) { + await ctx.externs.err.write('sleep: Invalid TIME parameter; must be a positive number'); + throw new Exit(1); + } + + await new Promise(r => setTimeout(r, time * 1000)); + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/sort.js b/packages/phoenix/src/puter-shell/coreutils/sort.js new file mode 100644 index 00000000..3f725a82 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/sort.js @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { resolveRelativePath } from '../../util/path.js'; + +export default { + name: 'sort', + usage: 'sort [FILE...]', + description: 'Sort the combined lines from the files provided, and output them.\n\n' + + 'If no FILE is specified, or FILE is `-`, read standard input.', + input: { + syncLines: true + }, + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + 'dictionary-order': { + description: 'Only consider alphanumeric characters and whitespace', + type: 'boolean', + short: 'd' + }, + 'ignore-case': { + description: 'Sort case-insensitively', + type: 'boolean', + short: 'f' + }, + 'ignore-nonprinting': { + description: 'Only consider printable characters', + type: 'boolean', + short: 'i' + }, + output: { + description: 'Output to this file, instead of standard output', + type: 'string', + short: 'o' + }, + unique: { + description: 'Remove duplicates of previous lines', + type: 'boolean', + short: 'u' + }, + reverse: { + description: 'Sort in reverse order', + type: 'boolean', + short: 'r' + }, + } + }, + execute: async ctx => { + const { in_, out, err } = ctx.externs; + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + let relPaths = [...positionals]; + if (relPaths.length === 0) { + relPaths.push('-'); + } + + const lines = []; + + for (const relPath of relPaths) { + if (relPath === '-') { + lines.push(...await in_.collect()); + } else { + const absPath = resolveRelativePath(ctx.vars, relPath); + const fileData = await filesystem.read(absPath); + // DRY: Similar logic in wc and tail + if (fileData instanceof Blob) { + const arrayBuffer = await fileData.arrayBuffer(); + const fileText = new TextDecoder().decode(arrayBuffer); + lines.push(...fileText.split(/\n|\r|\r\n/).map(it => it + '\n')); + } else if (typeof fileData === 'string') { + lines.push(...fileData.split(/\n|\r|\r\n/).map(it => it + '\n')); + } else { + // ArrayBuffer or TypedArray + const fileText = new TextDecoder().decode(fileData); + lines.push(...fileText.split(/\n|\r|\r\n/).map(it => it + '\n')); + } + } + } + + const compareStrings = (a,b) => { + let aIndex = 0; + let bIndex = 0; + + const skipIgnored = (string, index) => { + if (values['dictionary-order'] && values['ignore-nonprinting']) { + // Combining --dictionary-order and --ignore-nonprinting is unspecified. + // We'll treat that as "must be alphanumeric only". + while (index < string.length && ! /[a-zA-Z0-9]/.test(string[index])) { + index++; + } + return index; + } + if (values['dictionary-order']) { + // Only consider whitespace and alphanumeric characters + while (index < string.length && ! /[a-zA-Z0-9\s]/.test(string[index])) { + index++; + } + return index; + } + if (values['ignore-nonprinting']) { + // Only consider printing characters + // So, ignore anything below an ascii space, inclusive. TODO: detect unicode control characters too? + while (index < string.length && string[index] <= ' ') { + index++; + } + return index; + } + + return index; + }; + + aIndex = skipIgnored(a, aIndex); + bIndex = skipIgnored(b, bIndex); + while (aIndex < a.length && bIndex < b.length) { + // POSIX: Sorting should be locale-dependent + let comparedCharA = a[aIndex]; + let comparedCharB = b[bIndex]; + if (values['ignore-case']) { + comparedCharA = comparedCharA.toUpperCase(); + comparedCharB = comparedCharB.toUpperCase(); + } + + if (comparedCharA !== comparedCharB) { + if (values.reverse) { + return comparedCharA < comparedCharB ? 1 : -1; + } + return comparedCharA < comparedCharB ? -1 : 1; + } + + aIndex++; + bIndex++; + aIndex = skipIgnored(a, aIndex); + bIndex = skipIgnored(b, bIndex); + } + + // If we got here, we reached the end of one of the strings. + // If we reached the end of both, they're equal. Otherwise, return whichever ended. + if (aIndex >= a.length) { + if (bIndex >= b.length) { + return 0; + } + return -1; + } + return 1; + }; + + lines.sort(compareStrings); + + let resultLines = lines; + if (values.unique) { + resultLines = lines.filter((value, index, array) => { + return !index || compareStrings(value, array[index - 1]) !== 0; + }); + } + + if (values.output) { + const outputPath = resolveRelativePath(ctx.vars, values.output); + await filesystem.write(outputPath, resultLines.join('')); + } else { + for (const line of resultLines) { + await out.write(line); + } + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/tail.js b/packages/phoenix/src/puter-shell/coreutils/tail.js new file mode 100644 index 00000000..f93c5482 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/tail.js @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; +import { fileLines } from '../../util/file.js'; + +export default { + name: 'tail', + usage: 'tail [OPTIONS] [FILE]', + description: 'Read a file and print the last lines to standard output.\n\n' + + 'Defaults to 10 lines unless --lines is given. ' + + 'If no FILE is provided, or FILE is `-`, read standard input.', + input: { + syncLines: true + }, + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + lines: { + description: 'Print the last COUNT lines', + type: 'string', + short: 'n', + valueName: 'COUNT', + } + } + }, + execute: async ctx => { + const { out, err } = ctx.externs; + const { positionals, values } = ctx.locals; + + if (positionals.length > 1) { + // TODO: Support multiple files (this is an extension to POSIX, but available in the GNU tail) + await err.write('tail: Only one FILE parameter is allowed\n'); + throw new Exit(1); + } + const relPath = positionals[0] || '-'; + + let lineCount = 10; + + if (values.lines) { + const parsedLineCount = Number.parseFloat(values.lines); + if (isNaN(parsedLineCount) || ! Number.isInteger(parsedLineCount) || parsedLineCount < 1) { + await err.write(`tail: Invalid number of lines '${values.lines}'\n`); + throw new Exit(1); + } + lineCount = parsedLineCount; + } + + let lines = []; + for await (const line of fileLines(ctx, relPath)) { + lines.push(line); + // We keep lineCount+1 lines, to account for a possible trailing blank line. + if (lines.length > lineCount + 1) { + lines.shift(); + } + } + + // Ignore trailing blank line + if ( lines.length > 0 && lines[lines.length - 1] === '\n') { + lines.pop(); + } + // Now we remove the extra line if it's there. + if ( lines.length > lineCount ) { + lines.shift(); + } + + for ( const line of lines ) { + await out.write(line); + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/test.js b/packages/phoenix/src/puter-shell/coreutils/test.js new file mode 100644 index 00000000..baf0e19b --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/test.js @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'test', + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { historyManager } = ctx.externs; + const { chatHistory } = ctx.plugins; + + + console.log('test????', chatHistory.get_messages()); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/touch.js b/packages/phoenix/src/puter-shell/coreutils/touch.js new file mode 100644 index 00000000..8f15f13c --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/touch.js @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; +import { resolveRelativePath } from '../../util/path.js'; +import { ErrorCodes } from '../../platform/PosixError.js'; + +export default { + name: 'touch', + usage: 'touch FILE...', + description: 'Mark the FILE(s) as accessed and modified at the current time, creating them if they do not exist.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + const { positionals } = ctx.locals; + const { filesystem } = ctx.platform; + + if ( positionals.length === 0 ) { + await ctx.externs.err.write('touch: missing file operand\n'); + throw new Exit(1); + } + + for ( let i=0 ; i < positionals.length ; i++ ) { + const path = resolveRelativePath(ctx.vars, positionals[i]); + + let stat = null; + try { + stat = await filesystem.stat(path); + } catch (e) { + if (e.posixCode !== ErrorCodes.ENOENT) { + await ctx.externs.err.write(`touch: ${e.message}\n`); + throw new Exit(1); + } + } + + if ( stat ) continue; + + await filesystem.write(path, ''); + } + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/true.js b/packages/phoenix/src/puter-shell/coreutils/true.js new file mode 100644 index 00000000..fa2ddce9 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/true.js @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'true', + usage: 'true', + description: 'Do nothing, and return a success code.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + return; + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/txt2img.js b/packages/phoenix/src/puter-shell/coreutils/txt2img.js new file mode 100644 index 00000000..bc432d24 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/txt2img.js @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'txt2img', + usage: 'txt2img PROMPT', + description: 'Send PROMPT to an image-drawing AI, and print the result to standard output.', + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { positionals } = ctx.locals; + const [ prompt ] = positionals; + + if ( ! prompt ) { + await ctx.externs.err.write('txt2img: missing prompt\n'); + throw new Exit(1); + } + if ( positionals.length > 1 ) { + await ctx.externs.err.write('txt2img: prompt must be wrapped in quotes\n'); + throw new Exit(1); + } + + const { drivers } = ctx.platform; + + let a_interface, a_method, a_args; + + a_interface = 'puter-image-generation'; + a_method = 'generate'; + a_args = { prompt }; + + const result = await drivers.call({ + interface: a_interface, + method: a_method, + args: a_args, + }); + + await ctx.externs.out.write(result); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/usages.js b/packages/phoenix/src/puter-shell/coreutils/usages.js new file mode 100644 index 00000000..a04c39e9 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/usages.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export default { + name: 'usages', + usage: 'usages', + description: 'Print usage statistics, formatted as JSON.', + args: { + $: 'simple-parser', + allowPositionals: true, + }, + execute: async ctx => { + const { positionals } = ctx.locals; + + const { drivers } = ctx.platform; + + const result = await drivers.usage(); + + await ctx.externs.out.write(JSON.stringify(result, undefined, 2)); + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/wc.js b/packages/phoenix/src/puter-shell/coreutils/wc.js new file mode 100644 index 00000000..1924a4d3 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/wc.js @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { resolveRelativePath } from '../../util/path.js'; +import { fileLines } from '../../util/file.js'; + +const TAB_SIZE = 8; + +export default { + name: 'wc', + usage: 'wc [OPTIONS] [FILE...]', + description: 'Count newlines, words, and bytes in each specified FILE, and print them in a table.\n\n' + + 'If no FILE is specified, or FILE is `-`, read standard input. ' + + 'If more than one FILE is specified, also print a line for the totals.\n\n' + + 'The outputs are always printed in the order: newlines, words, characters, bytes, maximum line length, followed by the file name. ' + + 'If no options are given to output specific counts, the default is `-lwc`.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + bytes: { + description: 'Output the number of bytes in each file', + type: 'boolean', + short: 'c' + }, + chars: { + description: 'Output the number of characters in each file', + type: 'boolean', + short: 'm' + }, + lines: { + description: 'Output the number of newlines in each file', + type: 'boolean', + short: 'l' + }, + 'max-line-length': { + description: 'Output the maximum line length in each file. Tabs are expanded to the nearest multiple of 8', + type: 'boolean', + short: 'L' + }, + words: { + description: 'Output the number of words in each file. A word is a sequence of non-whitespace characters', + type: 'boolean', + short: 'w' + }, + } + }, + execute: async ctx => { + const { positionals, values } = ctx.locals; + const { filesystem } = ctx.platform; + + const paths = [...positionals]; + // "If no input file operands are specified, no name shall be written and no characters preceding the + // pathname shall be written." + // For convenience, we add '-' to paths, but make a note not to output the filename. + let emptyStdinPath = false; + if (paths.length < 1) { + emptyStdinPath = true; + paths.push('-'); + } + + let { bytes: printBytes, chars: printChars, lines: printNewlines, 'max-line-length': printMaxLineLengths, words: printWords } = values; + const anyOutputOptionsSpecified = printBytes || printChars || printNewlines || printMaxLineLengths || printWords; + if (!anyOutputOptionsSpecified) { + printBytes = true; + printNewlines = true; + printWords = true; + } + + let perFile = []; + let newlinesWidth = 1; + let wordsWidth = 1; + let charsWidth = 1; + let bytesWidth = 1; + let maxLineLengthWidth = 1; + + for (const relPath of paths) { + let counts = { + filename: relPath, + newlines: 0, + words: 0, + chars: 0, + bytes: 0, + maxLineLength: 0, + }; + + let inWord = false; + let currentLineLength = 0; + + for await (const line of fileLines(ctx, relPath)) { + counts.chars += line.length; + if (printBytes) { + const byteInput = new TextEncoder().encode(line); + counts.bytes += byteInput.length; + } + + for (const char of line) { + // "The wc utility shall consider a word to be a non-zero-length string of characters delimited by white space." + if (/\s/.test(char)) { + if (char === '\r' || char === '\n') { + counts.newlines++; + counts.maxLineLength = Math.max(counts.maxLineLength, currentLineLength); + currentLineLength = 0; + } else if (char === '\t') { + currentLineLength = (Math.floor(currentLineLength / TAB_SIZE) + 1) * TAB_SIZE; + } else { + currentLineLength++; + } + inWord = false; + continue; + } + currentLineLength++; + if (!inWord) { + counts.words++; + inWord = true; + } + } + } + + counts.maxLineLength = Math.max(counts.maxLineLength, currentLineLength); + + newlinesWidth = Math.max(newlinesWidth, counts.newlines.toString().length); + wordsWidth = Math.max(wordsWidth, counts.words.toString().length); + charsWidth = Math.max(charsWidth, counts.chars.toString().length); + bytesWidth = Math.max(bytesWidth, counts.bytes.toString().length); + maxLineLengthWidth = Math.max(maxLineLengthWidth, counts.maxLineLength.toString().length); + perFile.push(counts); + } + + let printCounts = async (count) => { + let output = ''; + const append = (string) => { + if (output.length !== 0) output += ' '; + output += string; + }; + + if (printNewlines) append(count.newlines.toString().padStart(newlinesWidth, ' ')); + if (printWords) append(count.words.toString().padStart(wordsWidth, ' ')); + if (printChars) append(count.chars.toString().padStart(charsWidth, ' ')); + if (printBytes) append(count.bytes.toString().padStart(bytesWidth, ' ')); + if (printMaxLineLengths) append(count.maxLineLength.toString().padStart(maxLineLengthWidth, ' ')); + // The only time emptyStdinPath is true, is if we had no file paths given as arguments. That means only one + // input (stdin), so this won't be called to print a "totals" line. + if (!emptyStdinPath) append(count.filename); + output += '\n'; + await ctx.externs.out.write(output); + } + + let totalCounts = { + filename: 'total', // POSIX: This is locale-dependent + newlines: 0, + words: 0, + chars: 0, + bytes: 0, + maxLineLength: 0, + }; + for (const count of perFile) { + totalCounts.newlines += count.newlines; + totalCounts.words += count.words; + totalCounts.chars += count.chars; + totalCounts.bytes += count.bytes; + totalCounts.maxLineLength = Math.max(totalCounts.maxLineLength, count.maxLineLength); + await printCounts(count); + } + if (perFile.length > 1) { + await printCounts(totalCounts); + } + } +}; diff --git a/packages/phoenix/src/puter-shell/coreutils/which.js b/packages/phoenix/src/puter-shell/coreutils/which.js new file mode 100644 index 00000000..6871acdc --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/which.js @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'which', + usage: 'which COMMAND...', + description: 'Look up each COMMAND, and return the path name of its executable.\n\n' + + 'Returns 1 if any COMMAND is not found, otherwise returns 0.', + args: { + $: 'simple-parser', + allowPositionals: true, + options: { + 'all': { + description: 'Return all matching path names of each COMMAND, not just the first', + type: 'boolean', + short: 'a', + }, + }, + }, + execute: async ctx => { + const { out, err, commandProvider } = ctx.externs; + const { positionals, values } = ctx.locals; + + let anyCommandsNotFound = false; + + const printPath = async ( commandName, command ) => { + if (command.path) { + await out.write(`${command.path}\n`); + } else { + await out.write(`${commandName}: shell built-in command\n`); + } + }; + + for ( const commandName of positionals ) { + const result = values.all + ? await commandProvider.lookupAll(commandName, { ctx }) + : await commandProvider.lookup(commandName, { ctx }); + + if ( ! result ) { + anyCommandsNotFound = true; + await err.write(`${commandName} not found\n`); + continue; + } + + if ( values.all ) { + for ( const command of result ) { + await printPath(commandName, command); + } + } else { + await printPath(commandName, result); + } + } + + if ( anyCommandsNotFound ) { + throw new Exit(1); + } + } +}; diff --git a/packages/phoenix/src/puter-shell/main.js b/packages/phoenix/src/puter-shell/main.js new file mode 100644 index 00000000..f0e85f6d --- /dev/null +++ b/packages/phoenix/src/puter-shell/main.js @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import builtins from './coreutils/__exports__.js'; +import ReadlineLib from "../ansi-shell/readline/readline.js"; + +// TODO: auto-gen argument parser registry from files +import SimpleArgParser from "../ansi-shell/arg-parsers/simple-parser.js"; +import ErrorsDecorator from "../ansi-shell/decorators/errors.js"; +import { ANSIShell } from "../ansi-shell/ANSIShell.js"; +import { Context } from "contextlink"; +import { SHELL_VERSIONS } from "../meta/versions.js"; +import { PuterShellParser } from "../ansi-shell/parsing/PuterShellParser.js"; +import { BuiltinCommandProvider } from "./providers/BuiltinCommandProvider.js"; +import { CreateChatHistoryPlugin } from './plugins/ChatHistoryPlugin.js'; +import { Pipe } from '../ansi-shell/pipeline/Pipe.js'; +import { Coupler } from '../ansi-shell/pipeline/Coupler.js'; +import { BetterReader } from 'dev-pty'; +import { MultiWriter } from '../ansi-shell/ioutil/MultiWriter.js'; +import { CompositeCommandProvider } from './providers/CompositeCommandProvider.js'; +import { ScriptCommandProvider } from './providers/ScriptCommandProvider.js'; +import { PuterAppCommandProvider } from './providers/PuterAppCommandProvider.js'; + +const argparser_registry = { + [SimpleArgParser.name]: SimpleArgParser +}; + +const decorator_registry = { + [ErrorsDecorator.name]: ErrorsDecorator +}; + +const GH_LINK = { + 'terminal': 'https://github.com/HeyPuter/terminal', + 'phoenix': 'https://github.com/HeyPuter/phoenix', +}; + +export const launchPuterShell = async (ctx) => { + const config = ctx.config; + const ptt = ctx.ptt; + const puterShell = ctx.puterShell; + + // Need to replace `in` with something we can write to + const real_pipe = new Pipe(); + const echo_pipe = new Pipe(); + const out_writer = new MultiWriter({ + delegates: [ + echo_pipe.in, + real_pipe.in, + ] + }) + new Coupler(ptt.in, out_writer); + const echo = new Coupler(echo_pipe.out, ptt.out); + const stdin = new BetterReader({ delegate: real_pipe.out }); + echo.off(); + + const readline = ReadlineLib.create({ + in: stdin, + out: ptt.out + }); + + const sdkv2 = globalThis.puter; + if ( ctx.platform.name !== 'node' ) { + await sdkv2.setAuthToken(config['puter.auth.token']); + await sdkv2.setAPIOrigin(config['puter.api_origin']); + } + + // PathCommandProvider is only compatible with node.js for now + // HACK: The import path is split to fool rollup into not including it. + const { PathCommandProvider } = (ctx.platform.name === 'node') + ? await import('./providers/' + 'PathCommandProvider.js') + : { PathCommandProvider: null }; + + const commandProvider = new CompositeCommandProvider([ + new BuiltinCommandProvider(), + // PathCommandProvider is only compatible with node.js for now + ...(ctx.platform.name === 'node' ? [new PathCommandProvider()] : []), + // PuterAppCommandProvider is only usable on Puter + ...(ctx.platform.name === 'puter' ? [new PuterAppCommandProvider()] : []), + new ScriptCommandProvider(), + ]); + + ctx = ctx.sub({ + externs: new Context({ + config, puterShell, + readline: readline.readline.bind(readline), + in: stdin, + out: ptt.out, + echo, + parser: new PuterShellParser(), + commandProvider, + sdkv2, + historyManager: readline.history, + }), + registries: new Context({ + argparsers: argparser_registry, + decorators: decorator_registry, + // While we use the BuiltinCommandProvider to provide the + // functionality of command lookup, we still need a registry + // of builtins to support the `help` command. + builtins, + }), + plugins: new Context(), + locals: new Context(), + }); + + { + const name = "chatHistory"; + const p = CreateChatHistoryPlugin(ctx); + ctx.plugins[name] = new Context(p.expose); + p.init(); + } + + const ansiShell = new ANSIShell(ctx); + + // TODO: move ioctl to PTY + ptt.on('ioctl.set', evt => { + ansiShell.dispatchEvent(new CustomEvent('signal.window-resize', { + detail: { + ...evt.windowSize + } + })); + }); + + const fire = (text) => { + // Define fire-like colors (ANSI 256-color codes) + const fireColors = [202, 208, 166]; + + // Split the text into an array of characters + const chars = text.split(''); + + // Apply a fire-like color to each character + const fireText = chars.map(char => { + // Select a random fire color for each character + const colorCode = fireColors[Math.floor(Math.random() * fireColors.length)]; + // Return the character wrapped in the ANSI escape code for the selected color + return `\x1b[38;5;${colorCode}m${char}\x1b[0m`; + }).join(''); + + return fireText; + } + + const blue = (text) => { + return `\x1b[38:5:27;1m${text}\x1b[0m`; + } + + const mklink = (url, text) => { + return `\x1b]8;;${url}\x07${text || url}\x1b]8;;\x07` + }; + + ctx.externs.out.write( + `${fire('Phoenix Shell')} [v${SHELL_VERSIONS[0].v}]\n` + + `⛷ try typing \x1B[34;1mhelp\x1B[0m or ` + + `\x1B[34;1mchangelog\x1B[0m to get started.\n` + + '\n' + + `${ + mklink(GH_LINK['phoenix'], fire('This shell')) + } and ${ + mklink(GH_LINK['terminal'], blue('Puter\'s Terminal Emulator')) + } are free software:\n` + + // `- ${fire('phoenix')}: ` + mklink(GH_LINK['phoenix'], fire(GH_LINK['phoenix'])) + '\n' + + // `- ${blue('terminal')}: ` + mklink(GH_LINK['terminal'], blue(GH_LINK['terminal'])) + '\n' + + `- ${'phoenix'}: ` + mklink(GH_LINK['phoenix']) + '\n' + + `- ${'terminal'}: ` + mklink(GH_LINK['terminal']) + '\n' + + // `🔗 ${mklink('https://puter.com', 'puter.com')} ` + + '' + // `🔗 ${mklink('https://puter.com', 'puter.com')} ` + + ); + + if ( ! config.hasOwnProperty('puter.auth.token') ) { + ctx.externs.out.write('\n'); + ctx.externs.out.write( + `\x1B[33;1m⚠\x1B[0m` + + `\x1B[31;1m` + + ' You are not running this terminal or shell within puter.com\n' + + `\x1B[0m` + + 'Use of the shell outside of puter.com is still experimental.\n' + + 'You must enter the command \x1B[34;1m`login`\x1B[0m to access most functionality.\n' + + '' + ); + } + + ctx.externs.out.write('\n'); + + for ( ;; ) { + await ansiShell.doPromptIteration(); + } +}; diff --git a/packages/phoenix/src/puter-shell/plugins/ChatHistoryPlugin.js b/packages/phoenix/src/puter-shell/plugins/ChatHistoryPlugin.js new file mode 100644 index 00000000..8d9df329 --- /dev/null +++ b/packages/phoenix/src/puter-shell/plugins/ChatHistoryPlugin.js @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const CreateChatHistoryPlugin = ctx => { + const messages = [ + { + role: 'system', + content: + 'You are running inside the Puter terminal via the `ai` command. Refer to yourself as Puter Terminal AI.', + }, + { + role: 'system', + content: + // note: this really doesn't work at all; GPT is effectively incapable of following this instruction. + 'You can provide commands to the user by prefixing a line in your response with %%%. The user will then be able to run the command by accepting confirmation.', + }, + { + role: 'system', + content: + 'If the user asks you about commands they have run, read them from system messages; if you don\'t see any, just let them know.', + }, + { + role: 'system', + content: + 'If the user asks what commands are available, tell them you don\'t yet have the ability to list commands but the `help` command is available for this purpose.' + }, + { + role: 'system', + content: + [ + 'FAQ, in case the user asks (rephrase these answers in character as Puter Terminal AI):', + 'Q: What is the command language?', + 'A: A subset of the POSIX Command Language, commonly known as the shell language.', + 'Q: Is this POSIX compliant?', + 'A: Our goal is to eventually be POSIX compliant, but support for most syntax is currently incomplete.', + 'Q: Is this a real shell?', + 'A: Yes, this is a real shell. You can interact with Puter\'s filesystem and drivers.', + 'Q: What is Puter?', + 'A: Puter is an operating system on the cloud, accessible from your browser. It is designed to be a platform for running applications and services with tools and interfaces you\'re already familiar with.', + 'Q: Is Puter a real operating system?', + 'A: Puter has a filesystem, manages cloud resources, and provides online services we call "drivers". It is the higher-level equivalent of a traditional operating system.', + ].join(' ') + }, + ]; + return { + expose: { + add_message (a_message) { + messages.push(a_message); + }, + get_messages () { + return [...messages]; + }, + }, + init () { + const history = ctx.externs.historyManager; + history.on('add', (input) => { + // To the best of our ability, we want to ignore invocations + // of the "ai" command itself. This won't always work because + // the history manager can't resolve command substitutions. + if ( input.startsWith('ai ') ) return; + + messages.push({ + role: 'system', + content: + `The user entered a command in the terminal: ` + + input + }); + }); + } + }; +}; diff --git a/packages/phoenix/src/puter-shell/providers/BuiltinCommandProvider.js b/packages/phoenix/src/puter-shell/providers/BuiltinCommandProvider.js new file mode 100644 index 00000000..efd86382 --- /dev/null +++ b/packages/phoenix/src/puter-shell/providers/BuiltinCommandProvider.js @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import builtins from '../coreutils/__exports__.js'; + +export class BuiltinCommandProvider { + async lookup (id) { + return builtins[id]; + } + + // Only a single builtin can match a given name + async lookupAll (...a) { + const result = await this.lookup(...a); + if ( result ) { + return [ result ]; + } + return undefined; + } +} diff --git a/packages/phoenix/src/puter-shell/providers/CompositeCommandProvider.js b/packages/phoenix/src/puter-shell/providers/CompositeCommandProvider.js new file mode 100644 index 00000000..58cf5f27 --- /dev/null +++ b/packages/phoenix/src/puter-shell/providers/CompositeCommandProvider.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class CompositeCommandProvider { + constructor (providers) { + this.providers = providers; + } + + async lookup (...a) { + for (const provider of this.providers) { + const command = await provider.lookup(...a); + if (command) { + return command; + } + } + } + + async lookupAll (...a) { + const results = []; + for (const provider of this.providers) { + const commands = await provider.lookupAll(...a); + if ( commands ) { + results.push(...commands); + } + } + + if ( results.length === 0 ) return undefined; + return results; + } +} diff --git a/packages/phoenix/src/puter-shell/providers/PathCommandProvider.js b/packages/phoenix/src/puter-shell/providers/PathCommandProvider.js new file mode 100644 index 00000000..bf84a052 --- /dev/null +++ b/packages/phoenix/src/puter-shell/providers/PathCommandProvider.js @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import path_ from "path-browserify"; +import child_process from "node:child_process"; +import stream from "node:stream"; +import { signals } from '../../ansi-shell/signals.js'; +import { Exit } from '../coreutils/coreutil_lib/exit.js'; +import pty from 'node-pty'; + +function spawn_process(ctx, executablePath) { + console.log(`Spawning ${executablePath} as a child process`); + const child = child_process.spawn(executablePath, ctx.locals.args, { + stdio: ['pipe', 'pipe', 'pipe'], + cwd: ctx.vars.pwd, + }); + + const in_ = new stream.PassThrough(); + const out = new stream.PassThrough(); + const err = new stream.PassThrough(); + + in_.on('data', (chunk) => { + child.stdin.write(chunk); + }); + out.on('data', (chunk) => { + ctx.externs.out.write(chunk); + }); + err.on('data', (chunk) => { + ctx.externs.err.write(chunk); + }); + + const fn_err = label => err => { + console.log(`ERR(${label})`, err); + }; + in_.on('error', fn_err('in_')); + out.on('error', fn_err('out')); + err.on('error', fn_err('err')); + child.stdin.on('error', fn_err('stdin')); + child.stdout.on('error', fn_err('stdout')); + child.stderr.on('error', fn_err('stderr')); + + child.stdout.pipe(out); + child.stderr.pipe(err); + + child.on('error', (err) => { + console.error(`Error running path executable '${executablePath}':`, err); + }); + + const sigint_promise = new Promise((resolve, reject) => { + ctx.externs.sig.on((signal) => { + if ( signal === signals.SIGINT ) { + reject(new Exit(130)); + } + }); + }); + + const exit_promise = new Promise((resolve, reject) => { + child.on('exit', (code) => { + ctx.externs.out.write(`Exited with code ${code}\n`); + if (code === 0) { + resolve({ done: true }); + } else { + reject(new Exit(code)); + } + }); + }); + + // Repeatedly copy data from stdin to the child, while it's running. + let data, done; + const next_data = async () => { + // FIXME: This waits for one more read() after we finish. + ({ value: data, done } = await Promise.race([ + exit_promise, sigint_promise, ctx.externs.in_.read(), + ])); + if ( data ) { + in_.write(data); + if ( ! done ) setTimeout(next_data, 0); + } + } + setTimeout(next_data, 0); + + return Promise.race([ exit_promise, sigint_promise ]); +} + +function spawn_pty(ctx, executablePath) { + console.log(`Spawning ${executablePath} as a pty`); + const child = pty.spawn(executablePath, ctx.locals.args, { + name: 'xterm-color', + rows: ctx.env.ROWS, + cols: ctx.env.COLS, + cwd: ctx.vars.pwd, + env: ctx.env + }); + child.onData(chunk => { + ctx.externs.out.write(chunk); + }); + + const sigint_promise = new Promise((resolve, reject) => { + ctx.externs.sig.on((signal) => { + if ( signal === signals.SIGINT ) { + child.kill('SIGINT'); // FIXME: Docs say this will throw when used on Windows + reject(new Exit(130)); + } + }); + }); + + const exit_promise = new Promise((resolve, reject) => { + child.onExit(({code, signal}) => { + ctx.externs.out.write(`Exited with code ${code || 0} and signal ${signal || 0}\n`); + if ( signal ) { + reject(new Exit(1)); + } else if ( code ) { + reject(new Exit(code)); + } else { + resolve({ done: true }); + } + }); + }); + + // Repeatedly copy data from stdin to the child, while it's running. + let data, done; + const next_data = async () => { + // FIXME: This waits for one more read() after we finish. + ({ value: data, done } = await Promise.race([ + exit_promise, sigint_promise, ctx.externs.in_.read(), + ])); + if ( data ) { + child.write(data); + if ( ! done ) setTimeout(next_data, 0); + } + } + setTimeout(next_data, 0); + + return Promise.race([ exit_promise, sigint_promise ]); +} + +function makeCommand(id, executablePath) { + return { + name: id, + path: executablePath, + async execute(ctx) { + // TODO: spawn_pty() does a lot of things better than spawn_process(), but can't handle output redirection. + // At some point, we'll need to implement more ioctls within spawn_process() and then remove spawn_pty(), + // but for now, the best experience is to use spawn_pty() unless we need the redirection. + if (ctx.locals.outputIsRedirected) { + return spawn_process(ctx, executablePath); + } + return spawn_pty(ctx, executablePath); + } + }; +} + +async function findCommandsInPath(id, ctx, firstOnly) { + const PATH = ctx.env['PATH']; + if (!PATH) + return; + const pathDirectories = PATH.split(':'); + + const results = []; + + for (const dir of pathDirectories) { + const executablePath = path_.resolve(dir, id); + let stat; + try { + stat = await ctx.platform.filesystem.stat(executablePath); + } catch (e) { + // Stat failed -> file does not exist + continue; + } + // TODO: Detect if the file is executable, and ignore it if not. + const command = makeCommand(id, executablePath); + + if ( firstOnly ) return command; + results.push(command); + } + + return results.length > 0 ? results : undefined; +} + +export class PathCommandProvider { + async lookup (id, { ctx }) { + return findCommandsInPath(id, ctx, true); + } + + async lookupAll(id, { ctx }) { + return findCommandsInPath(id, ctx, false); + } +} diff --git a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js new file mode 100644 index 00000000..44bf6cbb --- /dev/null +++ b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +const BUILT_IN_APPS = [ + 'explorer', +]; + +export class PuterAppCommandProvider { + + async lookup (id) { + // Built-in apps will not be returned by the fetch query below, so we handle them separately. + if (BUILT_IN_APPS.includes(id)) { + return { + name: id, + path: 'Built-in Puter app', + // TODO: Parameters and options? + async execute(ctx) { + const args = {}; // TODO: Passed-in parameters and options would go here + // NOTE: No await here, because launchApp() currently only resolves for Puter SDK apps. + puter.ui.launchApp(id, args); + } + }; + } + + const request = await fetch(`${puter.APIOrigin}/drivers/call`, { + "headers": { + "Content-Type": "application/json", + "Authorization": `Bearer ${puter.authToken}`, + }, + "body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }), + "method": "POST", + }); + + const { success, result } = await request.json(); + + if (!success) return; + + const { name, index_url } = result; + return { + name, + path: index_url, + // TODO: Parameters and options? + async execute(ctx) { + const args = {}; // TODO: Passed-in parameters and options would go here + // NOTE: No await here, yet, because launchApp() currently only resolves for Puter SDK apps. + puter.ui.launchApp(name, args); + } + }; + } + + // Only a single Puter app can match a given name + async lookupAll (...a) { + const result = await this.lookup(...a); + if ( result ) { + return [ result ]; + } + return undefined; + } +} diff --git a/packages/phoenix/src/puter-shell/providers/ScriptCommandProvider.js b/packages/phoenix/src/puter-shell/providers/ScriptCommandProvider.js new file mode 100644 index 00000000..66be2041 --- /dev/null +++ b/packages/phoenix/src/puter-shell/providers/ScriptCommandProvider.js @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import path_ from "path-browserify"; +import { Pipeline } from "../../ansi-shell/pipeline/Pipeline.js"; +import { resolveRelativePath } from '../../util/path.js'; + +export class ScriptCommandProvider { + async lookup (id, { ctx }) { + const { filesystem } = ctx.platform; + + const is_path = id.match(/^[.\/]/); + if ( ! is_path ) return undefined; + + const absPath = resolveRelativePath(ctx.vars, id); + try { + await filesystem.stat(absPath); + // TODO: More rigorous check that it's an executable text file + } catch (e) { + return undefined; + } + + return { + path: id, + async execute (ctx) { + const script_blob = await filesystem.read(absPath); + const script_text = await script_blob.text(); + + console.log('result though?', script_text); + + // note: it's still called `parseLineForProcessing` but + // it has since been extended to parse the entire file + const ast = ctx.externs.parser.parseScript(script_text); + const statements = ast[0].statements; + + for (const stmt of statements) { + const pipeline = await Pipeline.createFromAST(ctx, stmt); + await pipeline.execute(ctx); + } + } + }; + } + + // Only a single script can match a given path + async lookupAll (...a) { + const result = await this.lookup(...a); + if ( result ) { + return [ result ]; + } + return undefined; + } +} \ No newline at end of file diff --git a/packages/phoenix/src/util/bytes.js b/packages/phoenix/src/util/bytes.js new file mode 100644 index 00000000..cee745c9 --- /dev/null +++ b/packages/phoenix/src/util/bytes.js @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class Uint8List { + constructor (initialSize) { + initialSize = initialSize || 2; + + this.array = new Uint8Array(initialSize); + this.size = 0; + } + + get capacity () { + return this.array.length; + } + + append (chunk) { + if ( typeof chunk === 'number' ) { + chunk = new Uint8Array([chunk]); + } + + const sizeNeeded = this.size + chunk.length; + let newCapacity = this.capacity; + while ( sizeNeeded > newCapacity ) { + newCapacity *= 2; + } + + if ( newCapacity !== this.capacity ) { + const newArray = new Uint8Array(newCapacity); + newArray.set(this.array, 0); + this.array = newArray; + } + + this.array.set(chunk, this.size); + this.size += chunk.length; + } + + toArray () { + return this.array.subarray(0, this.size); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/util/file.js b/packages/phoenix/src/util/file.js new file mode 100644 index 00000000..a29045f2 --- /dev/null +++ b/packages/phoenix/src/util/file.js @@ -0,0 +1,28 @@ +import { resolveRelativePath } from './path.js'; + +// Iterate the given file, one line at a time. +// TODO: Make this read one line at a time, instead of all at once. +export async function* fileLines(ctx, relPath, options = { dashIsStdin: true }) { + let lines = []; + if (options.dashIsStdin && relPath === '-') { + lines = await ctx.externs.in_.collect(); + } else { + const absPath = resolveRelativePath(ctx.vars, relPath); + const fileData = await ctx.platform.filesystem.read(absPath); + if (fileData instanceof Blob) { + const arrayBuffer = await fileData.arrayBuffer(); + const fileText = new TextDecoder().decode(arrayBuffer); + lines = fileText.split(/\n|\r|\r\n/).map(it => it + '\n'); + } else if (typeof fileData === 'string') { + lines = fileData.split(/\n|\r|\r\n/).map(it => it + '\n'); + } else { + // ArrayBuffer or TypedArray + const fileText = new TextDecoder().decode(fileData); + lines = fileText.split(/\n|\r|\r\n/).map(it => it + '\n'); + } + } + + for (const line of lines) { + yield line; + } +} \ No newline at end of file diff --git a/packages/phoenix/src/util/lang.js b/packages/phoenix/src/util/lang.js new file mode 100644 index 00000000..dea8aa06 --- /dev/null +++ b/packages/phoenix/src/util/lang.js @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const disallowAccessToUndefined = (obj) => { + return new Proxy(obj, { + get (target, prop, receiver) { + if ( ! target.hasOwnProperty(prop) ) { + throw new Error( + `disallowed access to undefined property` + + `: ${JSON.stringify(prop)}.` + ); + } + return Reflect.get(target, prop, receiver); + } + }) +} \ No newline at end of file diff --git a/packages/phoenix/src/util/log.js b/packages/phoenix/src/util/log.js new file mode 100644 index 00000000..f68f772a --- /dev/null +++ b/packages/phoenix/src/util/log.js @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export class Log { + static log (...items) { + items = items.map(this.toString); + console.log(...items); + } + + static toString (item) { + if ( item instanceof Uint8Array ) { + return [...item] + .map(x => x.toString(16).padStart(2, '0')) + .join(' '); + } + + return item; + } +} diff --git a/packages/phoenix/src/util/path.js b/packages/phoenix/src/util/path.js new file mode 100644 index 00000000..bd418be0 --- /dev/null +++ b/packages/phoenix/src/util/path.js @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import path_ from "path-browserify"; + +export const resolveRelativePath = (vars, relativePath) => { + if (!relativePath) { + // If relativePath is undefined, return home directory + return vars.home; + } + if ( relativePath.startsWith('/') ) { + return relativePath; + } + if ( relativePath.startsWith('~') ) { + return path_.resolve(vars.home, '.' + relativePath.slice(1)); + } + return path_.resolve(vars.pwd, relativePath); +}; diff --git a/packages/phoenix/src/util/singleton.js b/packages/phoenix/src/util/singleton.js new file mode 100644 index 00000000..4bcbf85d --- /dev/null +++ b/packages/phoenix/src/util/singleton.js @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export const EMPTY = Object.freeze({}); diff --git a/packages/phoenix/src/util/statemachine.js b/packages/phoenix/src/util/statemachine.js new file mode 100644 index 00000000..ca4a77ee --- /dev/null +++ b/packages/phoenix/src/util/statemachine.js @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { disallowAccessToUndefined } from "./lang.js"; +import { Context } from "contextlink"; + +export class StatefulProcessor { + constructor (params) { + for ( const k in params ) this[k] = params[k]; + + let lastState = null; + } + async run (imports) { + this.state = 'start'; + imports = imports ?? {}; + const externals = {}; + for ( const k in this.externals ) { + if ( this.externals[k].required && ! imports[k] ) { + throw new Error(`missing required external: ${k}`); + } + if ( ! imports[k] ) continue; + externals[k] = imports[k]; + } + + const ctx = new Context({ + consts: disallowAccessToUndefined(this.constants), + externs: externals, + vars: this.createVariables_(), + setState: this.setState_.bind(this) + }); + + for ( ;; ) { + if ( this.state === 'end' ) break; + + await this.iter_(ctx); + } + + return ctx.vars; + } + setState_ (newState) { + this.state = newState; + } + async iter_ (runContext) { + const ctx = runContext.sub({ + locals: {} + }); + + ctx.trigger = name => { + return this.actions[name](ctx); + } + if ( this.state !== this.lastState ) { + this.lastState = this.state; + if ( this.transitions.hasOwnProperty(this.state) ) { + for ( const handler of this.transitions[this.state] ) { + await handler(ctx); + } + } + } + + for ( const beforeAll of this.beforeAlls ) { + await beforeAll.handler(ctx); + } + + await this.states[this.state](ctx); + } + createVariables_ () { + const o = {}; + for ( const k in this.variables ) { + if ( this.variables[k].getDefaultValue ) { + o[k] = this.variables[k].getDefaultValue(); + } + } + return o; + } +} + +export class StatefulProcessorBuilder { + static COMMON_1 = [ + 'variable', 'external', 'state', 'action' + ] + + constructor () { + this.constants = {}; + this.beforeAlls = []; + this.transitions = {}; + + for ( const facet of this.constructor.COMMON_1 ) { + this[facet + 's'] = {}; + this[facet] = function (name, value) { + this[facet + 's'][name] = value; + return this; + } + } + } + + installContext (context) { + for ( const k in context.constants ) { + this.constant(k, context.constants[k]); + } + return this; + } + + constant (name, value) { + Object.defineProperty(this.constants, name, { + value + }); + return this; + } + + beforeAll (name, handler) { + this.beforeAlls.push({ + name, handler + }); + return this; + } + + onTransitionTo (name, handler) { + if ( ! this.transitions.hasOwnProperty(name) ) { + this.transitions[name] = []; + } + this.transitions[name].push(handler); + return this; + } + + build () { + const params = {}; + for ( const facet of this.constructor.COMMON_1 ) { + params[facet + 's'] = this[facet + 's']; + } + return new StatefulProcessor({ + ...params, + constants: this.constants, + beforeAlls: this.beforeAlls, + transitions: this.transitions, + }); + } +} \ No newline at end of file diff --git a/packages/phoenix/src/util/wrap-text.js b/packages/phoenix/src/util/wrap-text.js new file mode 100644 index 00000000..478f1e76 --- /dev/null +++ b/packages/phoenix/src/util/wrap-text.js @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export function lengthIgnoringEscapes(text) { + const escape = '\x1b'; + // There are a lot of different ones, but we only use graphics-mode ones, so only parse those for now. + // TODO: Parse other escape sequences as needed. + // Format is: ESC, '[', DIGIT, 0 or more characters, and then 'm' + const escapeSequenceRegex = /^\x1B\[\d.*?m/; + + let length = 0; + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (char === escape) { + // Consume an ANSI escape sequence + const match = text.substring(i).match(escapeSequenceRegex); + if (match) { + i += match[0].length - 1; + } + continue; + } + length++; + } + return length; +} + +// TODO: Ensure this works with multi-byte characters (UTF-8) +export const wrapText = (text, width) => { + const whitespaceChars = ' \t'.split(''); + const isWhitespace = c => { + return whitespaceChars.includes(c); + }; + + // If width was invalid, just return the original text as a failsafe. + if (typeof width !== 'number' || width < 1) + return [text]; + + const lines = []; + let currentLine = ''; + const splitWordIfTooLong = (word) => { + while (lengthIgnoringEscapes(word) > width) { + lines.push(word.substring(0, width - 1) + '-'); + word = word.substring(width - 1); + } + + currentLine = word; + }; + + for (let i = 0; i < text.length; i++) { + const char = text.charAt(i); + // Handle special characters + if (char === '\n') { + lines.push(currentLine.trimEnd()); + currentLine = ''; + // Don't skip whitespace after a newline, to allow for indentation. + continue; + } + // TODO: Handle \t? + if (/\S/.test(char)) { + // Grab next word + let word = char; + while ((i+1) < text.length && /\S/.test(text[i + 1])) { + word += text[i+1]; + i++; + } + if (lengthIgnoringEscapes(currentLine) === 0) { + splitWordIfTooLong(word); + continue; + } + if ((lengthIgnoringEscapes(currentLine) + lengthIgnoringEscapes(word)) > width) { + // Next line + lines.push(currentLine.trimEnd()); + splitWordIfTooLong(word); + continue; + } + currentLine += word; + continue; + } + + currentLine += char; + if (lengthIgnoringEscapes(currentLine) >= width) { + lines.push(currentLine.trimEnd()); + currentLine = ''; + // Skip whitespace at end of line. + while (isWhitespace(text[i + 1])) { + i++; + } + continue; + } + } + if (currentLine.length >= 0) { // Not lengthIgnoringEscapes! + lines.push(currentLine); + } + + return lines; +}; \ No newline at end of file diff --git a/packages/phoenix/test.js b/packages/phoenix/test.js new file mode 100644 index 00000000..2414e287 --- /dev/null +++ b/packages/phoenix/test.js @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { + StringPStratumImpl, + StrataParser, + ParserFactory, +} from 'strataparse'; +import { buildParserFirstHalf } from './src/ansi-shell/parsing/buildParserFirstHalf.js'; +import { buildParserSecondHalf } from './src/ansi-shell/parsing/buildParserSecondHalf.js'; + + +const sp = new StrataParser(); + +const cstParserFac = new ParserFactory() +cstParserFac.concrete = true; +cstParserFac.rememberSource = true; + +sp.add( + new StringPStratumImpl(` + ls | tail -n 2 "ab" > "te\\"st" + `) +); + +// buildParserFirstHalf(sp, 'syntaxHighlighting'); +buildParserFirstHalf(sp, 'interpreting'); +buildParserSecondHalf(sp); + +const result = sp.parse(); +console.log(result && JSON.stringify(result, undefined, ' ')); +if ( sp.error ) { + console.log('has error:', sp.error); +} diff --git a/packages/phoenix/test/coreutils.test.js b/packages/phoenix/test/coreutils.test.js new file mode 100644 index 00000000..004b7677 --- /dev/null +++ b/packages/phoenix/test/coreutils.test.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { runBasenameTests } from "./coreutils/basename.js"; +import { runDateTests } from "./coreutils/date.js"; +import { runDirnameTests } from "./coreutils/dirname.js"; +import { runEchoTests } from "./coreutils/echo.js"; +import { runEnvTests } from "./coreutils/env.js"; +import { runErrnoTests } from './coreutils/errno.js'; +import { runFalseTests } from "./coreutils/false.js"; +import { runHeadTests } from "./coreutils/head.js"; +import { runPrintfTests } from './coreutils/printf.js'; +import { runSleepTests } from "./coreutils/sleep.js"; +import { runSortTests } from "./coreutils/sort.js"; +import { runTailTests } from "./coreutils/tail.js"; +import { runTrueTests } from "./coreutils/true.js"; +import { runWcTests } from "./coreutils/wc.js"; + +describe('coreutils', function () { + runBasenameTests(); + runDateTests(); + runDirnameTests(); + runEchoTests(); + runEnvTests(); + runErrnoTests(); + runFalseTests(); + runHeadTests(); + runPrintfTests(); + runSleepTests(); + runSortTests(); + runTailTests(); + runTrueTests(); + runWcTests(); +}); diff --git a/packages/phoenix/test/coreutils/basename.js b/packages/phoenix/test/coreutils/basename.js new file mode 100644 index 00000000..888e1299 --- /dev/null +++ b/packages/phoenix/test/coreutils/basename.js @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runBasenameTests = () => { + describe('basename', function () { + it('expects at least 1 argument', async () => { + let ctx = MakeTestContext(builtins.basename, {}); + let hadError = false; + try { + await builtins.basename.execute(ctx); + } catch (e) { + hadError = true; + } + if (!hadError) { + assert.fail('should fail when given 0 arguments'); + } + assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout'); + // Output to stderr is allowed but not required. + }); + it('expects at most 2 arguments', async () => { + let ctx = MakeTestContext(builtins.basename, {positionals: ['a', 'b', 'c']}); + let hadError = false; + try { + await builtins.basename.execute(ctx); + } catch (e) { + hadError = true; + } + if (!hadError) { + assert.fail('should fail when given 3 arguments'); + } + assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout'); + // Output to stderr is allowed but not required. + }); + + const testCases = [ + { + description: '"foo.txt" produces "foo.txt"', + input: ['foo.txt'], + expectedStdout: 'foo.txt\n' + }, + { + description: '"./foo.txt" produces "foo.txt"', + input: ['./foo.txt'], + expectedStdout: 'foo.txt\n' + }, + { + description: '"/a/b/c/foo.txt" produces "foo.txt"', + input: ['/a/b/c/foo.txt'], + expectedStdout: 'foo.txt\n' + }, + { + description: 'two slashes produces "/"', + input: ['//'], + expectedStdout: '/\n' + }, + { + description: 'a series of slashes produces "/"', + input: ['/////'], + expectedStdout: '/\n' + }, + { + description: 'empty string produces "/"', + input: [''], + expectedStdout: '.\n' + }, + { + description: 'trailing slashes are trimmed', + input: ['foo.txt/'], + expectedStdout: 'foo.txt\n' + }, + { + description: 'suffix is removed from simple filename', + input: ['foo.txt', '.txt'], + expectedStdout: 'foo\n' + }, + { + description: 'suffix is removed from path', + input: ['/a/b/c/foo.txt', '.txt'], + expectedStdout: 'foo\n' + }, + { + description: 'suffix is removed only once', + input: ['/a/b/c/foo.txt.txt.txt', '.txt'], + expectedStdout: 'foo.txt.txt\n' + }, + { + description: 'suffix is ignored if not found in the input', + input: ['/a/b/c/foo.txt', '.png'], + expectedStdout: 'foo.txt\n' + }, + { + description: 'suffix is removed even if input has a trailing slash', + input: ['/a/b/c/foo.txt/', '.txt'], + expectedStdout: 'foo\n' + }, + ]; + for (const {description, input, expectedStdout} of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.basename, {positionals: input}); + try { + const result = await builtins.basename.execute(ctx); + assert.equal(result, undefined, 'should exit successfully, returning nothing'); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/date.js b/packages/phoenix/test/coreutils/date.js new file mode 100644 index 00000000..efc800ea --- /dev/null +++ b/packages/phoenix/test/coreutils/date.js @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import * as ck from 'chronokinesis'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runDateTests = () => { + describe('date', function () { + beforeEach(() => { + ck.freeze(); + ck.timezone('UTC', '2024-03-07 13:05:07'); + }); + afterEach(() => { + ck.reset(); + }); + + const testCases = [ + { + description: 'outputs the date and time in a standard format when no format parameter is given', + input: [ ], + options: { utc: true }, + expectedStdout: 'Thu Mar 7 13:05:07 UTC 2024\n', + expectedStderr: '', + }, + { + description: 'outputs the format verbatim if no format sequences are included', + input: [ '+hello' ], + options: { utc: true }, + expectedStdout: 'hello\n', + expectedStderr: '', + }, + { + description: '%a outputs abbreviated weekday name', + input: [ '+%a' ], + options: { utc: true }, + expectedStdout: 'Thu\n', + expectedStderr: '', + }, + { + description: '%A outputs full weekday name', + input: [ '+%A' ], + options: { utc: true }, + expectedStdout: 'Thursday\n', + expectedStderr: '', + }, + { + description: '%b outputs abbreviated month name', + input: [ '+%b' ], + options: { utc: true }, + expectedStdout: 'Mar\n', + expectedStderr: '', + }, + { + description: '%B outputs full month name', + input: [ '+%B' ], + options: { utc: true }, + expectedStdout: 'March\n', + expectedStderr: '', + }, + { + description: '%c outputs full date and time', + input: [ '+%c' ], + options: { utc: true }, + expectedStdout: '3/7/2024, 1:05:07 PM\n', + expectedStderr: '', + }, + { + description: '%C outputs century as 2 digits', + input: [ '+%C' ], + options: { utc: true }, + expectedStdout: '20\n', + expectedStderr: '', + }, + { + description: '%d outputs day of the month as 2 digits', + input: [ '+%d' ], + options: { utc: true }, + expectedStdout: '07\n', + expectedStderr: '', + }, + { + description: '%D outputs date as mm/dd/yy', + input: [ '+%D' ], + options: { utc: true }, + expectedStdout: '03/07/24\n', + expectedStderr: '', + }, + { + description: '%e outputs day of the month as 2 characters padded with a leading space', + input: [ '+%e' ], + options: { utc: true }, + expectedStdout: ' 7\n', + expectedStderr: '', + }, + { + description: '%H outputs the 24-hour clock hour, as 2 digits', + input: [ '+%H' ], + options: { utc: true }, + expectedStdout: '13\n', + expectedStderr: '', + }, + { + description: '%h outputs the same as %b', + input: [ '+%h' ], + options: { utc: true }, + expectedStdout: 'Mar\n', + expectedStderr: '', + }, + { + description: '%I outputs the 12-hour clock hour, as 2 digits', + input: [ '+%I' ], + options: { utc: true }, + expectedStdout: '01\n', + expectedStderr: '', + }, + // TODO: %j outputs the day of the year as a 3-digit number, starting at 001. + { + description: '%m outputs the month, as 2 digits, with January as 01', + input: [ '+%m' ], + options: { utc: true }, + expectedStdout: '03\n', + expectedStderr: '', + }, + { + description: '%M outputs the minute, as 2 digits', + input: [ '+%M' ], + options: { utc: true }, + expectedStdout: '05\n', + expectedStderr: '', + }, + { + description: '%n outputs a newline character', + input: [ '+%n' ], + options: { utc: true }, + expectedStdout: '\n\n', + expectedStderr: '', + }, + { + description: '%p outputs AM or PM', + input: [ '+%p' ], + options: { utc: true }, + expectedStdout: 'PM\n', + expectedStderr: '', + }, + { + description: '%r outputs the 12-hour clock time', + input: [ '+%r' ], + options: { utc: true }, + expectedStdout: '01:05:07 PM\n', + expectedStderr: '', + }, + { + description: '%S outputs seconds, as 2 digits', + input: [ '+%S' ], + options: { utc: true }, + expectedStdout: '07\n', + expectedStderr: '', + }, + { + description: '%t outputs a tab character', + input: [ '+%t' ], + options: { utc: true }, + expectedStdout: '\t\n', + expectedStderr: '', + }, + { + description: '%T outputs the 24-hour clock time', + input: [ '+%T' ], + options: { utc: true }, + expectedStdout: '13:05:07\n', + expectedStderr: '', + }, + { + description: '%u outputs the week day as a number, with Monday = 1 and Sunday = 7', + input: [ '+%u' ], + options: { utc: true }, + expectedStdout: '4\n', + expectedStderr: '', + }, + // TODO: %U outputs the week of the year, as 2 digits, with weeks starting on Sunday, and the first being week 00 + // TODO: %V outputs the week of the year, as 2 digits, with weeks starting on Monday, and the first being week 01 + { + description: '%w outputs the week day as a number, with Sunday = 0 and Saturday = 6', + input: [ '+%w' ], + options: { utc: true }, + expectedStdout: '4\n', + expectedStderr: '', + }, + // TODO: %W outputs the week of the year, as 2 digits,, with weeks starting on Monday, and the first being week 00 + { + description: '%x outputs a local date representation', + input: [ '+%x' ], + options: { utc: true }, + expectedStdout: '3/7/2024\n', + expectedStderr: '', + }, + { + description: '%X outputs a local time representation', + input: [ '+%X' ], + options: { utc: true }, + expectedStdout: '1:05:07 PM\n', + expectedStderr: '', + }, + { + description: '%y outputs the year within a century, as 2 digits', + input: [ '+%y' ], + options: { utc: true }, + expectedStdout: '24\n', + expectedStderr: '', + }, + { + description: '%Y outputs the year', + input: [ '+%Y' ], + options: { utc: true }, + expectedStdout: '2024\n', + expectedStderr: '', + }, + { + description: '%Z outputs the timezone name', + input: [ '+%Z' ], + options: { utc: true }, + expectedStdout: 'UTC\n', + expectedStderr: '', + }, + { + description: '%% outputs a percent sign', + input: [ '+%%' ], + options: { utc: true }, + expectedStdout: '%\n', + expectedStderr: '', + }, + { + description: 'multiple format sequences can be included at once', + input: [ '+%B is month %m' ], + options: { utc: true }, + expectedStdout: 'March is month 03\n', + expectedStderr: '', + }, + { + description: 'unrecognized formats are output verbatim', + input: [ '+%4%L hello' ], + options: { utc: true }, + expectedStdout: '%4%L hello\n', + expectedStderr: '', + }, + ]; + + for (const { description, input, options, expectedStdout, expectedStderr, expectedFail } of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.date, { positionals: input, values: options }); + let hadError = false; + try { + const result = await builtins.date.execute(ctx); + if (!expectedFail) { + assert.equal(result, undefined, 'should exit successfully, returning nothing'); + } + } catch (e) { + hadError = true; + if (!expectedFail) { + assert.fail(e); + } + } + if (expectedFail && !hadError) { + assert.fail('should have returned an error code'); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr'); + }); + } + }); +}; diff --git a/packages/phoenix/test/coreutils/dirname.js b/packages/phoenix/test/coreutils/dirname.js new file mode 100644 index 00000000..a263b9d7 --- /dev/null +++ b/packages/phoenix/test/coreutils/dirname.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runDirnameTests = () => { + describe('dirname', function () { + it('expects at least 1 argument', async () => { + let ctx = MakeTestContext(builtins.dirname, {}); + let hadError = false; + try { + await builtins.dirname.execute(ctx); + } catch (e) { + hadError = true; + } + if (!hadError) { + assert.fail('should fail when given 0 arguments'); + } + assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout'); + // Output to stderr is allowed but not required. + }); + it('expects at most 1 argument', async () => { + let ctx = MakeTestContext(builtins.dirname, {positionals: ['a', 'b']}); + let hadError = false; + try { + await builtins.dirname.execute(ctx); + } catch (e) { + hadError = true; + } + if (!hadError) { + assert.fail('should fail when given 2 or more arguments'); + } + assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout'); + // Output to stderr is allowed but not required. + }); + + const testCases = [ + { + description: '"foo.txt" produces "."', + input: 'foo.txt', + expectedStdout: '.\n' + }, + { + description: '"./foo.txt" produces "."', + input: './foo.txt', + expectedStdout: '.\n' + }, + { + description: '"/a/b/c/foo.txt" produces "/a/b/c"', + input: '/a/b/c/foo.txt', + expectedStdout: '/a/b/c\n' + }, + { + description: '"a/b/c/foo.txt" produces "a/b/c"', + input: 'a/b/c/foo.txt', + expectedStdout: 'a/b/c\n' + }, + { + description: 'two slashes produces "/"', + input: '//', + expectedStdout: '/\n' + }, + { + description: 'a series of slashes produces "/"', + input: '/////', + expectedStdout: '/\n' + }, + { + description: 'empty string produces "/"', + input: '', + expectedStdout: '/\n' + }, + { + description: 'trailing slashes are trimmed', + input: 'a/b/c////foo//', + expectedStdout: 'a/b/c\n' + }, + ]; + for (const {description, input, expectedStdout} of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.dirname, {positionals: [input]}); + try { + const result = await builtins.dirname.execute(ctx); + assert.equal(result, undefined, 'should exit successfully, returning nothing'); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/echo.js b/packages/phoenix/test/coreutils/echo.js new file mode 100644 index 00000000..099f762c --- /dev/null +++ b/packages/phoenix/test/coreutils/echo.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runEchoTests = () => { + describe('echo', function () { + const testCases = [ + { + description: 'empty input prints a newline', + input: [], + options: {}, + expectedStdout: '\n' + }, + { + description: 'single input is printed', + input: ['hello'], + options: {}, + expectedStdout: 'hello\n' + }, + { + description: 'multiple inputs are printed, separated by spaces', + input: ['hello', 'world'], + options: {}, + expectedStdout: 'hello world\n' + }, + { + description: '-n suppresses newlines', + input: ['hello', 'world'], + options: { + n: true + }, + expectedStdout: 'hello world' + }, + // TODO: Test the `-e` option for interpreting backslash escapes. + ]; + for (const {description, input, options, expectedStdout} of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.echo, {positionals: input, values: options}); + try { + const result = await builtins.echo.execute(ctx); + assert.equal(result, undefined, 'should exit successfully, returning nothing'); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/env.js b/packages/phoenix/test/coreutils/env.js new file mode 100644 index 00000000..5cb30a54 --- /dev/null +++ b/packages/phoenix/test/coreutils/env.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runEnvTests = () => { + describe('env', function () { + it('should return a non-zero exit code, and output the env variables', async function () { + let ctx = MakeTestContext(builtins.env, { env: {'a': '1', 'b': '2' } }); + try { + await builtins.env.execute(ctx); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, 'a=1\nb=2\n', 'env should output the env variables, one per line'); + assert.equal(ctx.externs.err.output, '', 'env should not write to stderr'); + }); + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/errno.js b/packages/phoenix/test/coreutils/errno.js new file mode 100644 index 00000000..fbb63b53 --- /dev/null +++ b/packages/phoenix/test/coreutils/errno.js @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; +import { ErrorCodes, ErrorMetadata } from '../../src/platform/PosixError.js'; + +export const runErrnoTests = () => { + describe('errno', function () { + + const testCases = [ + { + description: 'exits normally if nothing is passed in', + input: [ ], + values: {}, + expectedStdout: '', + expectedStderr: '', + expectedFail: false, + }, + { + description: 'can search by number', + input: [ ErrorMetadata.get(ErrorCodes.EFBIG).code.toString() ], + values: {}, + expectedStdout: 'EFBIG 27 File too big\n', + expectedStderr: '', + expectedFail: false, + }, + { + description: 'can search by number', + input: [ ErrorCodes.EIO.description ], + values: {}, + expectedStdout: 'EIO 5 IO error\n', + expectedStderr: '', + expectedFail: false, + }, + { + description: 'prints an error message and returns a code > 0 if an error is not found', + input: [ 'NOT-A-REAL-ERROR' ], + values: {}, + expectedStdout: '', + expectedStderr: 'ERROR: Not understood: NOT-A-REAL-ERROR\n', + expectedFail: true, + }, + { + description: 'accepts multiple arguments and prints each', + input: [ ErrorMetadata.get(ErrorCodes.ENOENT).code.toString(), 'NOT-A-REAL-ERROR', ErrorCodes.EPIPE.description ], + values: {}, + expectedStdout: + 'ENOENT 2 File or directory not found\n' + + 'EPIPE 32 Pipe broken\n', + expectedStderr: 'ERROR: Not understood: NOT-A-REAL-ERROR\n', + expectedFail: true, + }, + { + description: 'searches descriptions if --search is provided', + input: [ 'directory' ], + values: { search: true }, + expectedStdout: + 'ENOENT 2 File or directory not found\n' + + 'ENOTDIR 20 Is not a directory\n' + + 'EISDIR 21 Is a directory\n' + + 'ENOTEMPTY 39 Directory is not empty\n', + expectedStderr: '', + expectedFail: false, + }, + { + description: 'lists all errors if --list is provided, ignoring parameters', + input: [ 'directory' ], + values: { list: true }, + expectedStdout: + 'EPERM 1 Operation not permitted\n' + + 'ENOENT 2 File or directory not found\n' + + 'EIO 5 IO error\n' + + 'EACCES 13 Permission denied\n' + + 'EEXIST 17 File already exists\n' + + 'ENOTDIR 20 Is not a directory\n' + + 'EISDIR 21 Is a directory\n' + + 'EINVAL 22 Argument invalid\n' + + 'EMFILE 24 Too many open files\n' + + 'EFBIG 27 File too big\n' + + 'ENOSPC 28 Device out of space\n' + + 'EPIPE 32 Pipe broken\n' + + 'ENOTEMPTY 39 Directory is not empty\n' + + 'EADDRINUSE 98 Address already in use\n' + + 'ECONNRESET 104 Connection reset\n' + + 'ETIMEDOUT 110 Connection timed out\n' + + 'ECONNREFUSED 111 Connection refused\n', + expectedStderr: '', + expectedFail: false, + }, + { + description: '--search overrides --list', + input: [ 'directory' ], + values: { list: true, search: true }, + expectedStdout: + 'ENOENT 2 File or directory not found\n' + + 'ENOTDIR 20 Is not a directory\n' + + 'EISDIR 21 Is a directory\n' + + 'ENOTEMPTY 39 Directory is not empty\n', + expectedStderr: '', + expectedFail: false, + }, + ]; + + for (const { description, input, values, expectedStdout, expectedStderr, expectedFail } of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.errno, { positionals: input, values }); + let hadError = false; + try { + const result = await builtins.errno.execute(ctx); + if (!expectedFail) { + assert.equal(result, undefined, 'should exit successfully, returning nothing'); + } + } catch (e) { + hadError = true; + if (!expectedFail) { + assert.fail(e); + } + } + if (expectedFail && !hadError) { + assert.fail('should have returned an error code'); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/false.js b/packages/phoenix/test/coreutils/false.js new file mode 100644 index 00000000..bb88d31e --- /dev/null +++ b/packages/phoenix/test/coreutils/false.js @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; +import { Exit } from "../../src/puter-shell/coreutils/coreutil_lib/exit.js"; + +async function testFalse(options) { + let ctx = MakeTestContext(builtins.false, options); + let hadError = false; + try { + await builtins.false.execute(ctx); + } catch (e) { + assert(e instanceof Exit); + assert.notEqual(e.code, 0, 'returned exit code 0, meaning success'); + hadError = true; + } + if (!hadError) { + assert.fail('didn\'t return an exit code'); + } + assert.equal(ctx.externs.out.output, '', 'false should not write to stdout'); + assert.equal(ctx.externs.err.output, '', 'false should not write to stderr'); +} + +export const runFalseTests = () => { + describe('false', function () { + it('should return a non-zero exit code, with no output', async function () { + await testFalse({}); + }); + it('should allow, but ignore, positional arguments', async function () { + await testFalse({positionals: ['foo', 'bar', 'baz']}); + }); + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/harness.js b/packages/phoenix/test/coreutils/harness.js new file mode 100644 index 00000000..339b9b9d --- /dev/null +++ b/packages/phoenix/test/coreutils/harness.js @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Context } from "contextlink"; +import { SyncLinesReader } from '../../src/ansi-shell/ioutil/SyncLinesReader.js'; +import { CommandStdinDecorator } from '../../src/ansi-shell/pipeline/iowrappers.js'; + +export class WritableStringStream extends WritableStream { + constructor() { + super({ + write: (chunk) => { + if (this.output_ === undefined) + this.output_ = ""; + this.output_ += chunk; + } + }); + } + + write(chunk) { + if (!this.writer_) + this.writer_ = this.getWriter(); + return this.writer_.write(chunk); + } + + get output() { return this.output_ || ""; } +} + +// TODO: Flesh this out as needed. +export const MakeTestContext = (command, { positionals = [], values = {}, stdinInputs = [], env = {} }) => { + + let in_ = ReadableStream.from(stdinInputs).getReader(); + if (command.input?.syncLines) { + in_ = new SyncLinesReader({ delegate: in_ }); + } + in_ = new CommandStdinDecorator(in_); + + return new Context({ + cmdExecState: { valid: true }, + externs: new Context({ + in_, + out: new WritableStringStream(), + err: new WritableStringStream(), + sig: null, + }), + locals: new Context({ + args: [], + command, + positionals, + values, + }), + platform: new Context({}), + plugins: new Context({}), + registries: new Context({}), + env: env, + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/head.js b/packages/phoenix/test/coreutils/head.js new file mode 100644 index 00000000..fb34034f --- /dev/null +++ b/packages/phoenix/test/coreutils/head.js @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runHeadTests = () => { + describe('head', function () { + // Too many parameters + // Bad -n + const failureCases = [ + { + description: 'expects at most 1 argument', + options: {}, + positionals: ['1', '2'], + }, + { + description: 'expects --lines, if set, to be a number', + options: { lines: 'frog' }, + positionals: ['-'], + }, + { + description: 'expects --lines, if set, to be an integer', + options: { lines: '1.75' }, + positionals: ['-'], + }, + { + description: 'expects --lines, if set, to be positive', + options: { lines: '-3' }, + positionals: ['-'], + }, + { + description: 'expects --lines, if set, to not be 0', + options: { lines: '0' }, + positionals: ['-'], + }, + ]; + for (const { description, options, positionals } of failureCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.head, { positionals, values: options }); + let hadError = false; + try { + await builtins.head.execute(ctx); + } catch (e) { + hadError = true; + } + if (!hadError) { + assert.fail('didn\'t return an error code'); + } + assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout'); + // Output to stderr is allowed but not required. + }); + } + + const alphabet = 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\n'; + const testCases = [ + { + description: 'reads from stdin if no parameter is given', + options: {}, + positionals: [], + stdin: alphabet, + expectedStdout: 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n', + }, + { + description: 'reads from stdin if parameter is `-`', + options: {}, + positionals: ['-'], + stdin: alphabet, + expectedStdout: 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n', + }, + { + description: '--lines/-n specifies how many lines to write', + options: { lines: 5 }, + positionals: ['-'], + stdin: alphabet, + expectedStdout: 'a\nb\nc\nd\ne\n', + }, + { + description: 'when --lines/-n is greater than the number of lines, write everything', + options: { lines: 500 }, + positionals: ['-'], + stdin: alphabet, + expectedStdout: alphabet, + }, + // TODO: Test with files once the harness supports that. + ]; + for (const { description, options, positionals, stdin, expectedStdout } of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.head, { positionals, values: options, stdinInputs: [stdin] }); + try { + const result = await builtins.head.execute(ctx); + assert.equal(result, undefined); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, '', 'sleep should not write to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/printf.js b/packages/phoenix/test/coreutils/printf.js new file mode 100644 index 00000000..04c6c6a9 --- /dev/null +++ b/packages/phoenix/test/coreutils/printf.js @@ -0,0 +1,587 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runPrintfTests = () => { + describe('printf', function () { + const testCases = [ + { + description: 'outputs format verbatim if no operands were given', + input: [ 'hello' ], + expectedStdout: 'hello', + expectedStderr: '', + }, + { + description: 'outputs octal escape sequences', + input: [ '\\0\\41\\041' ], + expectedStdout: '\0!!', + expectedStderr: '', + }, + { + description: 'outputs a trailing backslash as itself', + input: [ '\\' ], + expectedStdout: '\\', + expectedStderr: '', + }, + { + description: 'outputs unrecognized escape sequences as themselves', + input: [ '\\z\\@\\#' ], + expectedStdout: '\\z\\@\\#', + expectedStderr: '', + }, + { + description: 'outputs escape sequences', + input: [ '\\a\\b\\f\\n\\r\\t\\v' ], + expectedStdout: '\x07\x08\x0C\n\r\t\x0B', + expectedStderr: '', + }, + { + description: 'rejects empty format specifier', + input: [ '%' ], + expectedStdout: '', + expectedStderr: 'printf: Invalid conversion specifier \'%\'\n', + expectedFail: true, + }, + { + description: 'outputs `%%` as `%`', + input: [ '%%' ], + expectedStdout: '%', + expectedStderr: '', + }, + + // + // %c: Character + // + { + description: 'outputs single characters for `%c`', + input: [ '%c', 'hello', '123' ], + expectedStdout: 'h1', + expectedStderr: '', + }, + { + description: 'outputs single characters for `%c`', + input: [ '%c', 'hello', '123' ], + expectedStdout: 'h1', + expectedStderr: '', + }, + { + description: 'supports padding and alignment for `%c`', + input: [ '"%-12c" "%12c"', 'hello', '123' ], + expectedStdout: '"h " " 1"', + expectedStderr: '', + }, + + // + // %s: String + // + { + description: 'outputs whole value as string for `%s`', + input: [ '%s', 'hello', '123' ], + expectedStdout: 'hello123', + expectedStderr: '', + }, + { + description: 'supports padding and alignment for `%s`', + input: [ '"%-12s" "%12s"', 'hello', '123' ], + expectedStdout: '"hello " " 123"', + expectedStderr: '', + }, + { + description: 'supports precision for `%s`', + input: [ '%.4s\n', 'hello', '123' ], + expectedStdout: 'hell\n123\n', + expectedStderr: '', + }, + + // + // %d and %i: Signed decimal integer + // + { + description: 'outputs a signed decimal integer for `%d` or `%i`', + input: [ '%d %i\n', '13', '13', '-127', '-127' ], + expectedStdout: '13 13\n-127 -127\n', + expectedStderr: '', + }, + { + description: 'supports padding for `%d` and `%i`', + input: [ '"%5d" "%05i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '" 13" "00013"\n" -127" "-0127"\n', + expectedStderr: '', + }, + { + description: 'supports alignment for `%d` and `%i`', + input: [ '"%-5d" "%0-5i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '"13 " "13 "\n"-127 " "-127 "\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag for `%d` and `%i`', + input: [ '"%+5d" "%+05i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '" +13" "+0013"\n" -127" "-0127"\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag with alignment for `%d` and `%i`', + input: [ '"%+-5d" "%+-05i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '"+13 " "+13 "\n"-127 " "-127 "\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag for `%d` and `%i`', + input: [ '"% 5d" "% 05i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '" 13" " 0013"\n" -127" "-0127"\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag with alignment for `%d` and `%i`', + input: [ '"% -5d" "% -05i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '" 13 " " 13 "\n"-127 " "-127 "\n', + expectedStderr: '', + }, + { + description: '`+` flag overrides ` ` for `%d` and `%i`', + input: [ '"%+ -5d" "%+ 05i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '"+13 " "+0013"\n"-127 " "-0127"\n', + expectedStderr: '', + }, + { + description: 'supports precision for `%d` and `%i`', + input: [ '"%.5d" "%0.5i"\n', '13', '13', '-127', '-127' ], + expectedStdout: '"00013" "00013"\n"-00127" "-00127"\n', + expectedStderr: '', + }, + { + description: '0 precision for `%d` and `%i`', + input: [ '"%.d" "%.0i"\n', '13', '13', '-127', '-127', '0', '0' ], + expectedStdout: '"13" "13"\n"-127" "-127"\n"" ""\n', + expectedStderr: '', + }, + + // + // %u: Unsigned decimal integer + // + { + description: 'outputs an unsigned decimal integer for `%u`', + input: [ '%u\n', '13', '0', '-127' ], + expectedStdout: '13\n0\n4294967169\n', + expectedStderr: '', + }, + { + description: 'supports padding for `%u`', + input: [ '"%5u" "%05u"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '" 13" "00013"\n" 0" "00000"\n"4294967169" "4294967169"\n', + expectedStderr: '', + }, + { + description: 'supports alignment for `%u`', + input: [ '"%-5u" "%0-5u"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"13 " "13 "\n"0 " "0 "\n"4294967169" "4294967169"\n', + expectedStderr: '', + }, + { + description: 'supports precision for `%u`', + input: [ '"%.5u" "%0.5u"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"00013" "00013"\n"00000" "00000"\n"4294967169" "4294967169"\n', + expectedStderr: '', + }, + { + description: 'ignores `+` and ` ` flags for `%u`', + input: [ '"%+u" "% u"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"13" "13"\n"0" "0"\n"4294967169" "4294967169"\n', + expectedStderr: '', + }, + { + description: '0 precision for `%u`', + input: [ '"%.u" "%.0u"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"13" "13"\n"" ""\n"4294967169" "4294967169"\n', + expectedStderr: '', + }, + + // + // %o: Unsigned octal integer + // + { + description: 'outputs an unsigned octal integer for `%o`', + input: [ '%o\n', '13', '0', '-127' ], + expectedStdout: '15\n0\n37777777601\n', + expectedStderr: '', + }, + { + description: 'supports padding for `%o`', + input: [ '"%5o" "%05o"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '" 15" "00015"\n" 0" "00000"\n"37777777601" "37777777601"\n', + expectedStderr: '', + }, + { + description: 'supports alignment for `%o`', + input: [ '"%-5o" "%0-5o"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"15 " "15 "\n"0 " "0 "\n"37777777601" "37777777601"\n', + expectedStderr: '', + }, + { + description: 'supports precision for `%o`', + input: [ '"%.5o" "%0.5o"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"00015" "00015"\n"00000" "00000"\n"37777777601" "37777777601"\n', + expectedStderr: '', + }, + { + description: 'ignores `+` and ` ` flags for `%o`', + input: [ '"%+o" "% o"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"15" "15"\n"0" "0"\n"37777777601" "37777777601"\n', + expectedStderr: '', + }, + { + description: '0 precision for `%o`', + input: [ '"%.o" "%.0o"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"15" "15"\n"" ""\n"37777777601" "37777777601"\n', + expectedStderr: '', + }, + { + description: 'ensures a starting `0` when using the `#` flag for `%o`', + input: [ '"%#o" "%#0o"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"015" "015"\n"0" "0"\n"037777777601" "037777777601"\n', + expectedStderr: '', + }, + + // + // %x and %X: Unsigned hexadecimal integer + // + { + description: 'outputs an unsigned hexadecimal integer for `%x` and `%X`', + input: [ '"%x" "%X"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"d" "D"\n"0" "0"\n"ffffff81" "FFFFFF81"\n', + expectedStderr: '', + }, + { + description: 'supports padding for `%x` and `%X`', + input: [ '"%5x" "%05X"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '" d" "0000D"\n" 0" "00000"\n"ffffff81" "FFFFFF81"\n', + expectedStderr: '', + }, + { + description: 'supports alignment for `%x` and `%X`', + input: [ '"%-5x" "%0-5X"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"d " "D "\n"0 " "0 "\n"ffffff81" "FFFFFF81"\n', + expectedStderr: '', + }, + { + description: 'supports precision for `%x` and `%X`', + input: [ '"%.5x" "%0.5X"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"0000d" "0000D"\n"00000" "00000"\n"ffffff81" "FFFFFF81"\n', + expectedStderr: '', + }, + { + description: 'ignores `+` and ` ` flags for `%x` and `%X`', + input: [ '"%+x" "% X"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"d" "D"\n"0" "0"\n"ffffff81" "FFFFFF81"\n', + expectedStderr: '', + }, + { + description: '0 precision for `%x` and `%X`', + input: [ '"%.x" "%.0X"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"d" "D"\n"" ""\n"ffffff81" "FFFFFF81"\n', + expectedStderr: '', + }, + { + description: 'ensures a starting `0x` or `0X` when using the `#` flag for `%x` and `%X`', + input: [ '"%#x" "%#0X"\n', '13', '13', '0', '0', '-127', '-127' ], + expectedStdout: '"0xd" "0XD"\n"0x0" "0X0"\n"0xffffff81" "0XFFFFFF81"\n', + expectedStderr: '', + }, + + // + // %f and %F: Floating point, decimal notation + // + { + description: 'outputs a floating point number in decimal notation for `%f` and `%F`', + input: [ '"%f" "%F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13.000000" "13.000000"\n"-12345.678900" "-12345.678900"\n"0.000010" "0.000010"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'supports padding for `%f` and `%F`', + input: [ '"%12f" "%012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13.000000" "00013.000000"\n"-12345.678900" "-12345.678900"\n" 0.000010" "00000.000010"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + { + description: 'supports padding and alignment for `%f` and `%F`', + input: [ '"%-12f" "%-012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13.000000 " "13.000000 "\n"-12345.678900" "-12345.678900"\n"0.000010 " "0.000010 "\n' + + '"infinity " "INFINITY "\n"nan " "NAN "\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag for `%f` and `%F`', + input: [ '"%+12f" "%+012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" +13.000000" "+0013.000000"\n"-12345.678900" "-12345.678900"\n" +0.000010" "+0000.000010"\n' + + '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag with alignment for `%f` and `%F`', + input: [ '"%+-12f" "%+-012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"+13.000000 " "+13.000000 "\n"-12345.678900" "-12345.678900"\n"+0.000010 " "+0.000010 "\n' + + '"+infinity " "+INFINITY "\n"+nan " "+NAN "\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag for `%f` and `%F`', + input: [ '"% 12f" "% 012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13.000000" " 0013.000000"\n"-12345.678900" "-12345.678900"\n" 0.000010" " 0000.000010"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag with alignment for `%f` and `%F`', + input: [ '"% -12f" "% -012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13.000000 " " 13.000000 "\n"-12345.678900" "-12345.678900"\n" 0.000010 " " 0.000010 "\n' + + '" infinity " " INFINITY "\n" nan " " NAN "\n', + expectedStderr: '', + }, + { + description: '`+` flag overrides ` ` for `%f` and `%F`', + input: [ '"% +12f" "% +012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" +13.000000" "+0013.000000"\n"-12345.678900" "-12345.678900"\n" +0.000010" "+0000.000010"\n' + + '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n', + expectedStderr: '', + }, + { + description: 'supports precision for `%f` and `%F`', + input: [ '"%.3f" "%0.3F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13.000" "13.000"\n"-12345.679" "-12345.679"\n"0.000" "0.000"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'zero precision removes decimal point for `%f` and `%F`', + input: [ '"%.0f" "%0.0F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13" "13"\n"-12346" "-12346"\n"0" "0"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'zero precision with `#` flag forces a decimal point for `%f` and `%F`', + input: [ '"%#.0f" "%0#.0F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13." "13."\n"-12346." "-12346."\n"0." "0."\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'supports width and precision for `%f` and `%F`', + input: [ '"%12.3f" "%012.3F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13.000" "00000013.000"\n" -12345.679" "-0012345.679"\n" 0.000" "00000000.000"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + + // + // %e and %E: Floating point, exponential notation + // + { + description: 'outputs a floating point number in exponential notation for `%e` and `%E`', + input: [ '"%e" "%E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"1.300000e+01" "1.300000E+01"\n"-1.234568e+04" "-1.234568E+04"\n"1.000000e-05" "1.000000E-05"\n' + + '"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'supports padding for `%e` and `%E`', + input: [ '"%15e" "%015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 1.300000e+01" "0001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" 1.000000e-05" "0001.000000E-05"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + { + description: 'supports padding and alignment for `%e` and `%E`', + input: [ '"%-15e" "%-015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"1.300000e+01 " "1.300000E+01 "\n"-1.234568e+04 " "-1.234568E+04 "\n"1.000000e-05 " "1.000000E-05 "\n' + + '"infinity " "INFINITY "\n"nan " "NAN "\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag for `%e` and `%E`', + input: [ '"%+15e" "%+015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" +1.300000e+01" "+001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" +1.000000e-05" "+001.000000E-05"\n' + + '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag with alignment for `%e` and `%E`', + input: [ '"%+-15e" "%+-015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"+1.300000e+01 " "+1.300000E+01 "\n"-1.234568e+04 " "-1.234568E+04 "\n"+1.000000e-05 " "+1.000000E-05 "\n' + + '"+infinity " "+INFINITY "\n"+nan " "+NAN "\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag for `%e` and `%E`', + input: [ '"% 15e" "% 015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 1.300000e+01" " 001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" 1.000000e-05" " 001.000000E-05"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag with alignment for `%e` and `%E`', + input: [ '"% -15e" "% -015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 1.300000e+01 " " 1.300000E+01 "\n"-1.234568e+04 " "-1.234568E+04 "\n" 1.000000e-05 " " 1.000000E-05 "\n' + + '" infinity " " INFINITY "\n" nan " " NAN "\n', + expectedStderr: '', + }, + { + description: '`+` flag overrides ` ` for `%e` and `%E`', + input: [ '"% +15e" "% +015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" +1.300000e+01" "+001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" +1.000000e-05" "+001.000000E-05"\n' + + '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n', + expectedStderr: '', + }, + { + description: 'supports precision for `%e` and `%E`', + input: [ '"%.3e" "%0.3E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"1.300e+01" "1.300E+01"\n"-1.235e+04" "-1.235E+04"\n"1.000e-05" "1.000E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'zero precision removes decimal point for `%e` and `%E`', + input: [ '"%.0e" "%0.0E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"1e+01" "1E+01"\n"-1e+04" "-1E+04"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'zero precision with `#` flag forces a decimal point for `%e` and `%E`', + input: [ '"%#.0e" "%0#.0E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"1.e+01" "1.E+01"\n"-1.e+04" "-1.E+04"\n"1.e-05" "1.E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'supports width and precision for `%e` and `%E`', + input: [ '"%15.3e" "%015.3E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 1.300e+01" "0000001.300E+01"\n" -1.235e+04" "-000001.235E+04"\n" 1.000e-05" "0000001.000E-05"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + + // + // %g and %G: Floating point, set number of significant digits, may be decimal or exponential notation + // + { + description: 'outputs a floating point number for `%g` and `%G`', + input: [ '"%g" "%G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13" "13"\n"-12345.7" "-12345.7"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'supports padding for `%g` and `%G`', + input: [ '"%12g" "%012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13" "000000000013"\n" -12345.7" "-000012345.7"\n" 1e-05" "00000001E-05"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + { + description: 'supports padding and alignment for `%g` and `%G`', + input: [ '"%-12g" "%-012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13 " "13 "\n"-12345.7 " "-12345.7 "\n"1e-05 " "1E-05 "\n' + + '"infinity " "INFINITY "\n"nan " "NAN "\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag for `%g` and `%G`', + input: [ '"%+12g" "%+012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" +13" "+00000000013"\n" -12345.7" "-000012345.7"\n" +1e-05" "+0000001E-05"\n' + + '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n', + expectedStderr: '', + }, + { + description: 'supports `+` flag with alignment for `%g` and `%G`', + input: [ '"%+-12g" "%+-012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"+13 " "+13 "\n"-12345.7 " "-12345.7 "\n"+1e-05 " "+1E-05 "\n' + + '"+infinity " "+INFINITY "\n"+nan " "+NAN "\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag for `%g` and `%G`', + input: [ '"% 12g" "% 012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13" " 00000000013"\n" -12345.7" "-000012345.7"\n" 1e-05" " 0000001E-05"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + { + description: 'supports ` ` flag with alignment for `%g` and `%G`', + input: [ '"% -12g" "% -012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13 " " 13 "\n"-12345.7 " "-12345.7 "\n" 1e-05 " " 1E-05 "\n' + + '" infinity " " INFINITY "\n" nan " " NAN "\n', + expectedStderr: '', + }, + { + description: '`+` flag overrides ` ` for `%g` and `%G`', + input: [ '"% +12g" "% +012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" +13" "+00000000013"\n" -12345.7" "-000012345.7"\n" +1e-05" "+0000001E-05"\n' + + '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n', + expectedStderr: '', + }, + { + description: 'supports precision for `%g` and `%G`', + input: [ '"%.3g" "%0.3G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"13" "13"\n"-1.23e+04" "-1.23E+04"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'zero precision removes decimal point for `%g` and `%G`', + input: [ '"%.0g" "%0.0G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"1e+01" "1E+01"\n"-1e+04" "-1E+04"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'zero precision with `#` flag forces a decimal point for `%g` and `%G`', + input: [ '"%#.0g" "%0#.0G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '"1.e+01" "1.E+01"\n"-1.e+04" "-1.E+04"\n"1.e-05" "1.E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n', + expectedStderr: '', + }, + { + description: 'supports width and precision for `%g` and `%G`', + input: [ '"%12.3g" "%012.3G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ], + expectedStdout: '" 13" "000000000013"\n" -1.23e+04" "-0001.23E+04"\n" 1e-05" "00000001E-05"\n' + + '" infinity" " INFINITY"\n" nan" " NAN"\n', + expectedStderr: '', + }, + ]; + + for (const { description, input, expectedStdout, expectedStderr, expectedFail } of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.printf, { positionals: input }); + let hadError = false; + try { + const result = await builtins.printf.execute(ctx); + if (!expectedFail) { + assert.equal(result, undefined, 'should exit successfully, returning nothing'); + } + } catch (e) { + hadError = true; + if (!expectedFail) { + assert.fail(e); + } + } + if (expectedFail && !hadError) { + assert.fail('should have returned an error code'); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr'); + }); + } + }); +}; diff --git a/packages/phoenix/test/coreutils/sleep.js b/packages/phoenix/test/coreutils/sleep.js new file mode 100644 index 00000000..039dbd8c --- /dev/null +++ b/packages/phoenix/test/coreutils/sleep.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import sinon from 'sinon'; +import { MakeTestContext } from './harness.js'; +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runSleepTests = () => { + describe('sleep', function () { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + afterEach(() => { + clock.restore(); + }); + + const failureCases = [ + { + description: 'expects at least 1 argument', + positionals: [], + }, + { + description: 'expects at most 1 argument', + positionals: ['1', '2'], + }, + { + description: 'expects its argument to be a number', + positionals: ['frog'], + }, + { + description: 'expects its argument to be positive', + positionals: ['-1'], + }, + ]; + for (const { description, positionals } of failureCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.sleep, { positionals }); + let hadError = false; + try { + await builtins.sleep.execute(ctx); + } catch (e) { + hadError = true; + } + if (!hadError) { + assert.fail('didn\'t return an error code'); + } + assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout'); + // Output to stderr is allowed but not required. + }); + } + + const testCases = [ + { + description: 'sleep 0.5', + positionals: ['0.5'], + durationS: 0.5, + }, + { + description: 'sleep 1', + positionals: ['1'], + durationS: 1, + }, + { + description: 'sleep 1.5', + positionals: ['1.5'], + durationS: 1.5, + }, + { + description: 'sleep 27', + positionals: ['27'], + durationS: 27, + }, + ]; + for (const { description, positionals, durationS } of testCases) { + it(description, async () => { + const durationMs = durationS * 1000; + let ctx = MakeTestContext(builtins.sleep, { positionals }); + const startTimeMs = performance.now(); + let endTimeMs; + builtins.sleep.execute(ctx) + .then(() => { endTimeMs = performance.now(); }) + .catch((e) => { assert.fail(e); }); + await clock.tickAsync(durationMs - 5); + assert.ok(endTimeMs === undefined, `sleep took less than ${durationS}s, took ${(endTimeMs - startTimeMs) / 1000}s`); + await clock.tickAsync(10); + assert.ok(endTimeMs !== undefined, `sleep took more than ${durationS}s, not done after ${(durationS + 0.005)}s`); + + assert.equal(ctx.externs.out.output, '', 'sleep should not write to stdout'); + assert.equal(ctx.externs.err.output, '', 'sleep should not write to stderr'); + }); + } + }); +}; \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/sort.js b/packages/phoenix/test/coreutils/sort.js new file mode 100644 index 00000000..98302935 --- /dev/null +++ b/packages/phoenix/test/coreutils/sort.js @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runSortTests = () => { + describe('sort', function () { + const testCases = [ + { + description: 'reads from stdin if no parameter is given', + options: {}, + positionals: [], + stdin: 'a\nb\nc\n', + expectedStdout: 'a\nb\nc\n', + expectedStderr: '', + }, + { + description: 'reads from stdin if parameter is `-`', + options: {}, + positionals: ['-'], + stdin: 'a\nb\nc\n', + expectedStdout: 'a\nb\nc\n', + expectedStderr: '', + }, + { + description: 'sorts the output by byte value by default', + options: {}, + positionals: ['-'], + stdin: 'awesome\nCOOL\nAmazing\n!\ncold\n123\n', + expectedStdout: '!\n123\nAmazing\nCOOL\nawesome\ncold\n', + expectedStderr: '', + }, + { + description: 'keeps duplicates by default', + options: {}, + positionals: ['-'], + stdin: 'a\na\na\n', + expectedStdout: 'a\na\na\n', + expectedStderr: '', + }, + { + description: 'removes duplicates when -u/--unique is specified', + options: { unique: true }, + positionals: ['-'], + stdin: 'a\nd\na\nb\nc\nc\nb\na\n', + expectedStdout: 'a\nb\nc\nd\n', + expectedStderr: '', + }, + { + description: 'reverses the order when -r/--reverse is specified', + options: { reverse: true }, + positionals: ['-'], + stdin: 'a\nd\na\nb\nc\nc\nb\na\n', + expectedStdout: 'd\nc\nc\nb\nb\na\na\na\n', + expectedStderr: '', + }, + { + description: 'supports --reverse and --unique together', + options: { reverse: true, unique: true }, + positionals: ['-'], + stdin: 'a\nd\na\nb\nc\nc\nb\na\n', + expectedStdout: 'd\nc\nb\na\n', + expectedStderr: '', + }, + { + description: 'sorts case-insensitively when -f/--ignore-case is specified', + options: { 'ignore-case': true }, + positionals: ['-'], + stdin: 'b\nB\nA\na\n', + expectedStdout: 'A\na\nb\nB\n', + expectedStderr: '', + }, + { + description: 'supports --ignore-case and --unique together', + options: { 'ignore-case': true, unique: true }, + positionals: ['-'], + stdin: 'b\nB\nA\na\n', + expectedStdout: 'A\nb\n', + expectedStderr: '', + }, + { + description: 'considers only printing characters when -i/--ignore-nonprinting is specified', + options: { 'ignore-nonprinting': true }, + positionals: ['-'], + stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n', + expectedStdout: '*-*-*z\n=======a=======\n????b\n?a\na\n\0\0\0\0b\n hello\n', + expectedStderr: '', + }, + { + description: 'supports --ignore-nonprinting and --unique together', + options: { 'ignore-nonprinting': true, unique: true }, + positionals: ['-'], + stdin: '\0\0c\n\0b\nA\na\n\0a\n', + expectedStdout: 'A\na\n\0b\n\0\0c\n', + expectedStderr: '', + }, + { + description: 'considers only alphanumeric and whitespace characters when -d/--dictionary-order is specified', + options: { 'dictionary-order': true }, + positionals: ['-'], + stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n', + expectedStdout: ' hello\na\n?a\n=======a=======\n????b\n\0\0\0\0b\n*-*-*z\n', + expectedStderr: '', + }, + { + description: 'supports --dictionary-order and --unique together', + options: { 'dictionary-order': true, unique: true }, + positionals: ['-'], + stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n', + expectedStdout: ' hello\na\n????b\n*-*-*z\n', + expectedStderr: '', + }, + { + description: 'supports --dictionary-order and --ignore-nonprinting together', + options: { 'dictionary-order': true, 'ignore-nonprinting': true }, + positionals: ['-'], + stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n', + expectedStdout: 'a\n?a\n=======a=======\n????b\n\0\0\0\0b\n hello\n*-*-*z\n', + expectedStderr: '', + }, + // TODO: Test with files once the harness supports that. + ]; + for (const { description, options, positionals, stdin, expectedStdout, expectedStderr } of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.sort, { positionals, values: options, stdinInputs: [stdin] }); + try { + const result = await builtins.sort.execute(ctx); + assert.equal(result, undefined); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/tail.js b/packages/phoenix/test/coreutils/tail.js new file mode 100644 index 00000000..93cd28f0 --- /dev/null +++ b/packages/phoenix/test/coreutils/tail.js @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runTailTests = () => { + describe('tail', function () { + // Too many parameters + // Bad -n + const failureCases = [ + { + description: 'expects at most 1 argument', + options: {}, + positionals: ['1', '2'], + }, + { + description: 'expects --lines, if set, to be a number', + options: { lines: 'frog' }, + positionals: ['-'], + }, + { + description: 'expects --lines, if set, to be an integer', + options: { lines: '1.75' }, + positionals: ['-'], + }, + { + description: 'expects --lines, if set, to be positive', + options: { lines: '-3' }, + positionals: ['-'], + }, + { + description: 'expects --lines, if set, to not be 0', + options: { lines: '0' }, + positionals: ['-'], + }, + ]; + for (const { description, options, positionals } of failureCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.tail, { positionals, values: options }); + let hadError = false; + try { + await builtins.tail.execute(ctx); + } catch (e) { + hadError = true; + } + if (!hadError) { + assert.fail('didn\'t return an error code'); + } + assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout'); + // Output to stderr is allowed but not required. + }); + } + + const alphabet = 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\n'; + const testCases = [ + { + description: 'reads from stdin if no parameter is given', + options: {}, + positionals: [], + stdin: alphabet, + expectedStdout: 'q\nr\ns\nt\nu\nv\nw\nx\ny\nz\n', + }, + { + description: 'reads from stdin if parameter is `-`', + options: {}, + positionals: ['-'], + stdin: alphabet, + expectedStdout: 'q\nr\ns\nt\nu\nv\nw\nx\ny\nz\n', + }, + { + description: '--lines/-n specifies how many lines to write', + options: { lines: 5 }, + positionals: ['-'], + stdin: alphabet, + expectedStdout: 'v\nw\nx\ny\nz\n', + }, + { + description: 'when --lines/-n is greater than the number of lines, write everything', + options: { lines: 500 }, + positionals: ['-'], + stdin: alphabet, + expectedStdout: alphabet, + }, + // TODO: Test with files once the harness supports that. + ]; + for (const { description, options, positionals, stdin, expectedStdout } of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.tail, { positionals, values: options, stdinInputs: [stdin] }); + try { + const result = await builtins.tail.execute(ctx); + assert.equal(result, undefined); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, '', 'sleep should not write to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/true.js b/packages/phoenix/test/coreutils/true.js new file mode 100644 index 00000000..7ef5dfe8 --- /dev/null +++ b/packages/phoenix/test/coreutils/true.js @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +async function testTrue(options) { + let ctx = MakeTestContext(builtins.true, options); + try { + const result = await builtins.true.execute(ctx); + assert.equal(result, undefined); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, '', 'true should not write to stdout'); + assert.equal(ctx.externs.err.output, '', 'true should not write to stderr'); +} + +export const runTrueTests = () => { + describe('true', function () { + it('should execute successfully with no output', async function () { + await testTrue({}); + }); + it('should allow, but ignore, positional arguments', async function () { + await testTrue({positionals: ['foo', 'bar', 'baz']}); + }); + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/coreutils/wc.js b/packages/phoenix/test/coreutils/wc.js new file mode 100644 index 00000000..16fa45bb --- /dev/null +++ b/packages/phoenix/test/coreutils/wc.js @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { MakeTestContext } from './harness.js' +import builtins from '../../src/puter-shell/coreutils/__exports__.js'; + +export const runWcTests = () => { + describe('wc', function () { + const testCases = [ + { + description: 'can read from stdin when given `-`', + positionals: ['-'], + stdin: 'Well hello friends!', + expectedStdout: '0 3 19 -\n', + }, + { + description: 'reads from stdin when given no arguments', + positionals: [], + stdin: 'Well hello friends!', + expectedStdout: '0 3 19\n', + }, + { + description: 'handles empty stdin', + positionals: ['-'], + stdin: '', + expectedStdout: '0 0 0 -\n', + }, + { + description: 'counts newlines', + positionals: ['-'], + stdin: 'Well\nhello\nfriends!\n', + expectedStdout: '3 3 20 -\n', + }, + { + description: '-lwc produces the default output', + options: { bytes: true, lines: true, words: true }, + positionals: ['-'], + stdin: 'Well\nhello\nfriends!\n', + expectedStdout: '3 3 20 -\n', + }, + { + description: '-l outputs only lines', + options: { lines: true }, + positionals: ['-'], + stdin: 'Well\nhello\nmy friends!\n', + expectedStdout: '3 -\n', + }, + { + description: '-w outputs only words', + options: { words: true }, + positionals: ['-'], + stdin: 'Well\nhello\nmy friends!\n', + expectedStdout: '4 -\n', + }, + { + description: '-c outputs only bytes', + options: { bytes: true }, + positionals: ['-'], + stdin: '🖥️ Well\nhello\nmy friends!\n', + expectedStdout: '31 -\n', + }, + { + description: '-m outputs only characters', + options: { chars: true }, + positionals: ['-'], + stdin: '🖥️ Well\nhello\nmy friends!\n', + expectedStdout: '27 -\n', + }, + { + description: '-L outputs the maximum line length', + options: { 'max-line-length': true }, + positionals: ['-'], + stdin: '🖥️ Well\nhello\nmy friends!\n', + expectedStdout: '11 -\n', + }, + { + description: '-L treats tabs as jumping to the next multiple of 8 columns', + options: { 'max-line-length': true }, + positionals: ['-'], + stdin: 'hi\tmum\t!\n', + expectedStdout: '17 -\n', + }, + { + description: '-lwmcL outputs everything', + options: { bytes: true, chars: true, lines: true, 'max-line-length': true, words: true }, + positionals: ['-'], + stdin: '🖥️ Well\nhello\nmy friends!\n', + expectedStdout: '3 5 27 31 11 -\n', + }, + // TODO: Test with files once the harness supports that. + ]; + for (const { description, options, positionals, stdin, expectedStdout } of testCases) { + it(description, async () => { + let ctx = MakeTestContext(builtins.wc, { positionals, values: options, stdinInputs: [stdin] }); + try { + const result = await builtins.wc.execute(ctx); + assert.equal(result, undefined, 'should exit successfully, returning nothing'); + } catch (e) { + assert.fail(e); + } + assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout'); + assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr'); + }); + } + }); +} \ No newline at end of file diff --git a/packages/phoenix/test/readtoken.js b/packages/phoenix/test/readtoken.js new file mode 100644 index 00000000..549e3589 --- /dev/null +++ b/packages/phoenix/test/readtoken.js @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { readtoken, TOKENS } from '../src/ansi-shell/readline/readtoken.js'; + +describe('readtoken', () => { + const tcases = [ + { + desc: 'should accept unquoted string', + input: 'asdf', + expected: ['asdf'] + }, + { + desc: 'should accept leading spaces', + input: ' asdf', + expected: ['asdf'] + }, + { + desc: 'should accept trailing spaces', + input: 'asdf ', + expected: ['asdf'] + }, + { + desc: 'should expected quoted string', + input: '"asdf"', + expected: ['asdf'] + }, + { + desc: 'should recognize pipe with no whitespace', + input: 'asdf|zxcv', + expected: ['asdf', TOKENS['|'], 'zxcv'] + }, + { + desc: 'mixed quoted and unquoted should work', + input: '"asdf" zxcv', + expected: ['asdf', 'zxcv'] + }, + ]; + for ( const { desc, input, expected } of tcases ) { + it(desc, () => { + assert.deepEqual(readtoken(input), expected) + }); + } +}) \ No newline at end of file diff --git a/packages/phoenix/test/test-bytes.js b/packages/phoenix/test/test-bytes.js new file mode 100644 index 00000000..ba583762 --- /dev/null +++ b/packages/phoenix/test/test-bytes.js @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { Uint8List } from '../src/util/bytes.js'; + +describe('bytes', () => { + describe('Uint8List', () => { + it ('should satisfy: 5 bytes of input', () => { + const list = new Uint8List(); + for ( let i = 0 ; i < 5 ; i++ ) { + list.append(i); + } + const array = list.toArray(); + assert.equal(array.length, 5); + for ( let i = 0 ; i < 5 ; i++ ) { + assert.equal(array[i], i); + } + }) + }) +}) \ No newline at end of file diff --git a/packages/phoenix/test/test-stateful-processor.js b/packages/phoenix/test/test-stateful-processor.js new file mode 100644 index 00000000..8e6ecd5a --- /dev/null +++ b/packages/phoenix/test/test-stateful-processor.js @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; + +import { StatefulProcessorBuilder } from '../src/util/statemachine.js'; + +describe('StatefulProcessor', async () => { + it ('should satisfy: simple example', async () => { + const messages = []; + const processor = new StatefulProcessorBuilder() + .state('start', async ctx => { + messages.push('start'); + ctx.setState('intermediate'); + }) + .state('intermediate', async ctx => { + messages.push('intermediate'); + ctx.setState('end'); + }) + .build(); + await processor.run(); + assert.deepEqual(messages, ['start', 'intermediate']); + }); + it ('should handle transition', async () => { + const messages = []; + const processor = new StatefulProcessorBuilder() + .state('start', async ctx => { + messages.push('start'); + ctx.setState('intermediate'); + }) + .onTransitionTo('intermediate', ctx => { + messages.push('transition'); + ctx.locals.test1 = true; + }) + .state('intermediate', async ctx => { + messages.push('intermediate'); + assert.equal(ctx.locals.test1, true); + ctx.setState('end'); + }) + .build(); + await processor.run(); + assert.deepEqual(messages, [ + 'start', 'transition', 'intermediate' + ]); + }); + it ('should handle beforeAll', async () => { + const messages = []; + const processor = new StatefulProcessorBuilder() + .state('start', async ctx => { + messages.push('start'); + assert.equal(ctx.locals.test2, 'undefined_a'); + ctx.setState('intermediate'); + }) + .beforeAll('example-hook', async ctx => { + messages.push('before'); + ctx.locals.test2 += '_a'; + }) + .state('intermediate', async ctx => { + messages.push('intermediate'); + assert.equal(ctx.locals.test2, 'undefined_a'); + ctx.setState('end'); + }) + .build(); + await processor.run(); + assert.deepEqual(messages, [ + 'before', 'start', 'before', 'intermediate' + ]); + }); + it ('should fail when export is missing', async () => { + const messages = []; + const processor = new StatefulProcessorBuilder() + .external('test3', { required: true }) + .state('start', async ctx => { + ctx.setState('end'); + }) + .build(); + await assert.rejects(processor.run()); + }); + it ('should succeed when export is provided', async () => { + const messages = []; + const processor = new StatefulProcessorBuilder() + .external('test3', { required: true }) + .state('start', async ctx => { + messages.push(ctx.externs.test3) + ctx.setState('end'); + }) + .build(); + await processor.run({ test3: 'test4' }); + assert.deepEqual(messages, ['test4']); + }); +}) + diff --git a/packages/phoenix/test/wrap-text.js b/packages/phoenix/test/wrap-text.js new file mode 100644 index 00000000..3ee7a7bd --- /dev/null +++ b/packages/phoenix/test/wrap-text.js @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import assert from 'assert'; +import { lengthIgnoringEscapes, wrapText } from '../src/util/wrap-text.js'; + +describe('wrapText', () => { + const testCases = [ + { + description: 'should wrap text', + input: 'Well, hello friends! How are you today?', + width: 12, + output: ['Well, hello', 'friends! How', 'are you', 'today?'], + }, + { + description: 'should break too-long words onto multiple lines', + input: 'Antidisestablishmentarianism.', + width: 20, + output: ['Antidisestablishmen-', 'tarianism.'], + }, + { + description: 'should break too-long words onto multiple lines', + input: 'Antidisestablishmentarianism.', + width: 10, + output: ['Antidises-', 'tablishme-', 'ntarianis-', 'm.'], + }, + { + description: 'should break too-long words when there is already text on the line', + input: 'The longest word I can think of is antidisestablishmentarianism.', + width: 20, + output: ['The longest word I', 'can think of is', 'antidisestablishmen-', 'tarianism.'], + }, + { + description: 'should return the original text if the width is invalid', + input: 'Well, hello friends!', + width: 0, + output: ['Well, hello friends!'], + }, + { + description: 'should maintain existing newlines', + input: 'Well\nhello\n\nfriends!', + width: 20, + output: ['Well', 'hello', '', 'friends!'], + }, + { + description: 'should maintain indentation after newlines', + input: 'Well\n hello\n\nfriends!', + width: 20, + output: ['Well', ' hello', '', 'friends!'], + }, + { + description: 'should ignore ansi escape sequences', + input: '\x1B[34;1mWell this is some text with ansi escape sequences\x1B[0m', + width: 20, + output: ['\x1B[34;1mWell this is some', 'text with ansi', 'escape sequences\x1B[0m'], + }, + ]; + for (const { description, input, width, output } of testCases) { + it (description, () => { + const result = wrapText(input, width); + for (const line of result) { + if (typeof width === 'number' && width > 0) { + assert.ok(lengthIgnoringEscapes(line) <= width, `Line is too long: '${line}'`); + } + } + assert.equal('|' + result.join('|\n|') + '|', '|' + output.join('|\n|') + '|'); + }); + } +}) \ No newline at end of file diff --git a/packages/phoenix/tools/build_tar.sh b/packages/phoenix/tools/build_tar.sh new file mode 100755 index 00000000..c064787b --- /dev/null +++ b/packages/phoenix/tools/build_tar.sh @@ -0,0 +1,21 @@ +if [ $(basename "$(pwd)") != "phoenix" ]; then + echo "This should be run in the dev-ansi-termial repo" + exit 1 +fi + +export CONFIG_FILE='config/release.js' +npx rollup -c rollup.config.js + +if [ -d ./release ]; then + rm -rf ./release/* +fi + +mkdir -p release +mkdir -p release/puter-shell + +cp -r ./dist/* ./release + +cd ../dev-puter-shell +npx rollup -c rollup.config.js +cp -r ./dist/* ../phoenix/release/puter-shell +cd - diff --git a/packages/phoenix/tools/gen.js b/packages/phoenix/tools/gen.js new file mode 100644 index 00000000..031d6932 --- /dev/null +++ b/packages/phoenix/tools/gen.js @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +// Script that generates some of the javascript files +import fs from 'fs'; +import path from 'path'; + +const [directory] = process.argv.slice(2); +const target = path.resolve(process.cwd(), directory); +const outputFile = path.resolve(target, '__exports__.js'); + +const files = fs.readdirSync(target); + +let output = ''; +const line = str => { + output += str + '\n'; +} + +const toVar = name => { + name = name.replace(/-/g, '_'); + return 'module_' + name; +} + +const licenseLines = fs.readFileSync('../doc/license_header.txt', {encoding: 'utf8'}).split('\n'); +licenseLines.pop(); // Remove trailing empty line +line('/*'); +for (const licenseLine of licenseLines) { + if (licenseLine.length === 0) { + line(' *'); + } else { + line(` * ${licenseLine}`); + } +} +line(' */'); +line('// Generated by /tools/gen.js'); + +for ( const file of files ) { + if ( ! file.endsWith('.js') ) continue; + const name = path.parse(file).name; + if ( name === '__exports__' ) continue; + line(`import ${toVar(name)} from './${file}'`); +} + +line(''); +line('export default {'); + +for ( const file of files ) { + if ( ! file.endsWith('.js') ) continue; + const name = path.parse(file).name; + if ( name === '__exports__' ) continue; + line(` ${JSON.stringify(name)}: ${toVar(name)},`); +} + +line('};'); + +fs.writeFileSync(outputFile, output);