KernelDeimos преди 1 година
родител
ревизия
cb81579c53
променени са 100 файла, в които са добавени 12272 реда и са изтрити 0 реда
  1. 4 0
      packages/phoenix/.gitignore
  2. 661 0
      packages/phoenix/LICENSE
  3. 62 0
      packages/phoenix/README.md
  4. 20 0
      packages/phoenix/assets/index.html
  5. 23 0
      packages/phoenix/config/dev.js
  6. 23 0
      packages/phoenix/config/release.js
  7. 1152 0
      packages/phoenix/doc/devlog.md
  8. 77 0
      packages/phoenix/doc/graveyard/keyboard_modifiers.md
  9. 59 0
      packages/phoenix/doc/graveyard/readline.md
  10. 16 0
      packages/phoenix/doc/license_header.txt
  11. 23 0
      packages/phoenix/doc/missing-posix.md
  12. 55 0
      packages/phoenix/doc/parser.md
  13. BIN
      packages/phoenix/doc/readme-gif.gif
  14. 54 0
      packages/phoenix/doc/stash/SymbolParserImpl.js
  15. 26 0
      packages/phoenix/notalicense-license-checker-config.json
  16. 1888 0
      packages/phoenix/package-lock.json
  17. 39 0
      packages/phoenix/package.json
  18. 1 0
      packages/phoenix/packages/contextlink/.gitignore
  19. 45 0
      packages/phoenix/packages/contextlink/context.js
  20. 19 0
      packages/phoenix/packages/contextlink/entry.js
  21. 916 0
      packages/phoenix/packages/contextlink/package-lock.json
  22. 18 0
      packages/phoenix/packages/contextlink/package.json
  23. 48 0
      packages/phoenix/packages/contextlink/test/testcontext.js
  24. 101 0
      packages/phoenix/packages/newparser/exports.js
  25. 29 0
      packages/phoenix/packages/newparser/lib.js
  26. 139 0
      packages/phoenix/packages/newparser/parsers/combinators.js
  27. 46 0
      packages/phoenix/packages/newparser/parsers/terminals.js
  28. 181 0
      packages/phoenix/packages/pty/exports.js
  29. 12 0
      packages/phoenix/packages/pty/package.json
  30. 92 0
      packages/phoenix/packages/strataparse/dsl/ParserBuilder.js
  31. 29 0
      packages/phoenix/packages/strataparse/dsl/ParserRegistry.js
  32. 118 0
      packages/phoenix/packages/strataparse/exports.js
  33. 13 0
      packages/phoenix/packages/strataparse/package.json
  34. 141 0
      packages/phoenix/packages/strataparse/parse.js
  35. 48 0
      packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js
  36. 125 0
      packages/phoenix/packages/strataparse/parse_impls/combinators.js
  37. 62 0
      packages/phoenix/packages/strataparse/parse_impls/literal.js
  38. 45 0
      packages/phoenix/packages/strataparse/parse_impls/whitespace.js
  39. 115 0
      packages/phoenix/packages/strataparse/strata.js
  40. 106 0
      packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js
  41. 58 0
      packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js
  42. 80 0
      packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js
  43. 68 0
      packages/phoenix/packages/strataparse/strata_impls/terminals.js
  44. 49 0
      packages/phoenix/rollup.config.js
  45. 15 0
      packages/phoenix/run.json5
  46. 61 0
      packages/phoenix/src/ansi-shell/ANSIContext.js
  47. 243 0
      packages/phoenix/src/ansi-shell/ANSIShell.js
  48. 57 0
      packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js
  49. 53 0
      packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js
  50. 38 0
      packages/phoenix/src/ansi-shell/decorators/errors.js
  51. 33 0
      packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js
  52. 44 0
      packages/phoenix/src/ansi-shell/ioutil/MemReader.js
  53. 70 0
      packages/phoenix/src/ansi-shell/ioutil/MemWriter.js
  54. 35 0
      packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js
  55. 29 0
      packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js
  56. 25 0
      packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js
  57. 26 0
      packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js
  58. 64 0
      packages/phoenix/src/ansi-shell/ioutil/SignalReader.js
  59. 80 0
      packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js
  60. 40 0
      packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js
  61. 54 0
      packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js
  62. 52 0
      packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js
  63. 25 0
      packages/phoenix/src/ansi-shell/parsing/brainstorming.js
  64. 222 0
      packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js
  65. 441 0
      packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js
  66. 54 0
      packages/phoenix/src/ansi-shell/pipeline/Coupler.js
  67. 43 0
      packages/phoenix/src/ansi-shell/pipeline/Pipe.js
  68. 407 0
      packages/phoenix/src/ansi-shell/pipeline/Pipeline.js
  69. 45 0
      packages/phoenix/src/ansi-shell/pipeline/iowrappers.js
  70. 80 0
      packages/phoenix/src/ansi-shell/readline/history.js
  71. 362 0
      packages/phoenix/src/ansi-shell/readline/readline.js
  72. 107 0
      packages/phoenix/src/ansi-shell/readline/readtoken.js
  73. 135 0
      packages/phoenix/src/ansi-shell/readline/rl_comprehend.js
  74. 212 0
      packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js
  75. 34 0
      packages/phoenix/src/ansi-shell/readline/rl_words.js
  76. 22 0
      packages/phoenix/src/ansi-shell/signals.js
  77. 68 0
      packages/phoenix/src/context/context.js
  78. 70 0
      packages/phoenix/src/main_cli.js
  79. 79 0
      packages/phoenix/src/main_puter.js
  80. 144 0
      packages/phoenix/src/meta/versions.js
  81. 143 0
      packages/phoenix/src/platform/PosixError.js
  82. 20 0
      packages/phoenix/src/platform/node/env.js
  83. 219 0
      packages/phoenix/src/platform/node/filesystem.js
  84. 74 0
      packages/phoenix/src/pty/NodeStdioPTT.js
  85. 75 0
      packages/phoenix/src/pty/XDocumentPTT.js
  86. 39 0
      packages/phoenix/src/puter-shell/completers/command_completer.js
  87. 49 0
      packages/phoenix/src/puter-shell/completers/file_completer.js
  88. 57 0
      packages/phoenix/src/puter-shell/completers/option_completer.js
  89. 106 0
      packages/phoenix/src/puter-shell/coreutils/__exports__.js
  90. 87 0
      packages/phoenix/src/puter-shell/coreutils/ai.js
  91. 81 0
      packages/phoenix/src/puter-shell/coreutils/basename.js
  92. 60 0
      packages/phoenix/src/puter-shell/coreutils/cat.js
  93. 48 0
      packages/phoenix/src/puter-shell/coreutils/cd.js
  94. 52 0
      packages/phoenix/src/puter-shell/coreutils/changelog.js
  95. 31 0
      packages/phoenix/src/puter-shell/coreutils/clear.js
  96. 320 0
      packages/phoenix/src/puter-shell/coreutils/concept-parser.js
  97. 162 0
      packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js
  98. 24 0
      packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js
  99. 134 0
      packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js
  100. 36 0
      packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js

+ 4 - 0
packages/phoenix/.gitignore

@@ -0,0 +1,4 @@
+node_modules/
+dist/
+release/
+*.tar

+ 661 - 0
packages/phoenix/LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ 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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 <https://www.gnu.org/licenses/>.
+
+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
+<https://www.gnu.org/licenses/>.

+ 62 - 0
packages/phoenix/README.md

@@ -0,0 +1,62 @@
+# Important notice
+
+This repository is being moved to [the monorepo](https://github.com/HeyPuter/puter).
+
+<hr />
+
+<h2 align="center">Phoenix</h2>
+<h3 align="center">Puter's pure-javascript shell</h3>
+<h3 align="center"><img alt="" src="./doc/readme-gif.gif"></h3>
+<hr>
+
+`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)
+

+ 20 - 0
packages/phoenix/assets/index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Document</title>
+    <link rel="stylesheet" href="xterm.css">
+    <link rel="stylesheet" href="normalize.css">
+    <link rel="stylesheet" href="style.css">
+    <script src="__SDK_URL__"></script>
+    <script src="config.js"></script>
+    <script src="bundle.js"></script>
+    <script>
+        document.addEventListener('DOMContentLoaded', async () => {
+            main_shell();
+        });
+    </script>
+</head>
+</html>

+ 23 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+globalThis.__CONFIG__ = {
+    "origin": "https://puter.local:8080",
+    "shell.href": "https://puter.local:8081",
+    "sdk_url": "http://puter.localhost:4100/sdk/puter.js",
+};

+ 23 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+globalThis.__CONFIG__ = {
+    origin: location.origin,
+    'shell.href': location.origin + '/puter-shell/',
+    sdk_url: 'https://puter.com/puter.js/v2',
+};

+ 1152 - 0
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.

+ 77 - 0
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.

+ 59 - 0
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;
+    }
+```

+ 16 - 0
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 <https://www.gnu.org/licenses/>.

+ 23 - 0
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.

+ 55 - 0
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.

BIN
packages/phoenix/doc/readme-gif.gif


+ 54 - 0
packages/phoenix/doc/stash/SymbolParserImpl.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 <https://www.gnu.org/licenses/>.
+ */
+// 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 };
+    }
+}

+ 26 - 0
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"
+}

+ 1888 - 0
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"
+    }
+  }
+}

+ 39 - 0
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"
+  ]
+}

+ 1 - 0
packages/phoenix/packages/contextlink/.gitignore

@@ -0,0 +1 @@
+node_modules/

+ 45 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 19 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+export { Context } from "./context.js";

+ 916 - 0
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"
+      }
+    }
+  }
+}

+ 18 - 0
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": ""
+}

+ 48 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+
+    })
+});

+ 101 - 0
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,
+    }
+}

+ 29 - 0
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`); }
+}

+ 139 - 0
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 };
+    }
+}

+ 46 - 0
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 };
+    }
+}

+ 181 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 12 - 0
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"
+}

+ 92 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+    }
+}

+ 29 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+export class ParserRegistry {
+    constructor () {
+        this.parsers_ = {};
+    }
+    register (id, parser) {
+        this.parsers_[id] = parser;
+    }
+    get parsers () {
+        return this.parsers_;
+    }
+}

+ 118 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 13 - 0
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"
+}
+

+ 141 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 48 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 };
+    }
+}

+ 125 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 };
+    }
+}

+ 62 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 };
+    }
+}

+ 45 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 };
+    }
+}

+ 115 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+        }
+    }
+}

+ 106 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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' };
+    }
+}

+ 58 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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' };
+    }
+}

+ 80 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 };
+        }
+    }
+}

+ 68 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+    }
+}

+ 49 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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' }
+            ]
+        }),
+    ]
+}

+ 15 - 0
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: '.'
+        },
+    ],
+}

+ 61 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+};

+ 243 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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');
+        });
+    }
+}

+ 57 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+/**
+ * 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)
+        );
+    }
+}

+ 53 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 38 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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'
+                );
+            }
+        }
+    }
+}

+ 33 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+    }
+}

+ 44 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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
+        };
+    }
+}

+ 70 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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());
+    }
+}

+ 35 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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();
+        }
+    }
+}

+ 29 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+import { ProxyWriter } from "./ProxyWriter.js";
+
+export class NullifyWriter extends ProxyWriter {
+    async write (item) {
+        // NOOP
+    }
+
+    async close () {
+        await this.delegate.close();
+    }
+}

+ 25 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+export class ProxyReader {
+    constructor ({ delegate }) {
+        this.delegate = delegate;
+    }
+
+    read (...a) { return this.delegate.read(...a); }
+}

+ 26 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+export class ProxyWriter {
+    constructor ({ delegate }) {
+        this.delegate = delegate;
+    }
+
+    write (...a) { return this.delegate.write(...a); }
+    close (...a) { return this.delegate.close(...a); }
+}

+ 64 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 };
+    }
+}

+ 80 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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' };
+        }
+    }
+}

+ 40 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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',
+    '"': '"',
+    "'": "'",
+};

+ 54 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 52 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 };
+    }
+}

+ 25 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+const seq = [
+    { $: 'symbol', text: 'command' },
+    { $: 'string.dquote' },
+    { $: 'string.segment', text: '-' },
+    { $: 'op.cmd-subst' },
+    { $: 'op.close' },
+];

+ 222 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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()
+    )
+};

+ 441 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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());
+    }
+}

+ 54 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+            }
+        }
+    }
+}

+ 43 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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();
+    }
+}

+ 407 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 45 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+    }
+}

+ 80 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+    }
+}

+ 362 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 107 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+// [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;
+};

+ 135 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+// 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,
+    };
+};

+ 212 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+/*
+## 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);
+    },
+};

+ 34 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+}

+ 22 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+export const signals = Object.freeze({
+    SIGINT: 2,
+    SIGQUIT: 3,
+});

+ 68 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 70 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);

+ 79 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 }),
+        }),
+    }));
+};

+ 144 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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',
+        ]
+    }
+];

+ 143 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+    }
+}

+ 20 - 0
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];
+        }
+    }
+}

+ 219 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 });
+        }
+    });
+};

+ 74 - 0
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);
+        }
+    }
+}

+ 75 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+        }
+    }
+}

+ 39 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 49 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 57 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+}

+ 106 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+// 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,
+};

+ 87 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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');
+    }
+}

+ 81 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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 <slash> characters, string shall be set to a single <slash> character.
+            //    In this case, skip steps 4 to 6.
+            if (/^\/+$/.test(string)) {
+                string = '/';
+            } else {
+                // 4. If there are any trailing <slash> characters in string, they shall be removed.
+                string = string.replace(/\/+$/, '');
+
+                // 5. If there are any <slash> characters remaining in string, the prefix of string up to and including
+                //    the last <slash> 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');
+    }
+};

+ 60 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+        }
+    }
+}

+ 48 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+    }
+};

+ 52 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+        }
+    }
+};
+

+ 31 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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');
+    }
+};

+ 320 - 0
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');
+        }
+    }
+}

+ 162 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+/*
+    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;
+};

+ 24 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+export class Exit extends Error {
+    constructor (code) {
+        super(`exit ${code}`);
+        this.code = code;
+    }
+}

+ 134 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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');
+        }
+    }
+}

+ 36 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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;
+}

Някои файлове не бяха показани, защото твърде много файлове са промени