diff --git a/packages/phoenix/.gitignore b/packages/phoenix/.gitignore
new file mode 100644
index 00000000..e62e8910
--- /dev/null
+++ b/packages/phoenix/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+dist/
+release/
+*.tar
diff --git a/packages/phoenix/LICENSE b/packages/phoenix/LICENSE
new file mode 100644
index 00000000..0ad25db4
--- /dev/null
+++ b/packages/phoenix/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/packages/phoenix/README.md b/packages/phoenix/README.md
new file mode 100644
index 00000000..81f5258a
--- /dev/null
+++ b/packages/phoenix/README.md
@@ -0,0 +1,62 @@
+# Important notice
+
+This repository is being moved to [the monorepo](https://github.com/HeyPuter/puter).
+
+
+
+
Phoenix
+
Puter's pure-javascript shell
+
+
+
+`phoenix` is a pure-javascript shell built for [puter.com](https://puter.com).
+Following the spirit of open-source initiatives we've seen like
+[SerenityOS](https://serenityos.org/),
+we've built much of the shell's functionality from scratch.
+Some interesting portions of this shell include:
+- A shell parser which produces a Concrete-Syntax-Tree
+- Pipeline constructs built on top of the [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
+- Platform support for Puter
+
+The shell is a work in progress. The following improvements are considered in-scope:
+- Anything specified in [POSIX.1-2017 Chapter 2](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html)
+- UX improvements over traditional shells
+ > examples include: readline syntax highlighting, hex view for binary streams
+- Platform support, so `phoenix` can run in more environments
+
+## Running Phoenix
+
+### In a Browser
+
+You can use the [terminal on Puter](https://puter.com/app/terminal),
+or run from source by following the instructions provided for
+[Puter's terminal emulator](https://github.com/HeyPuter/terminal).
+
+### Running in Node
+
+Under node.js Phoenix acts as a shell for your operating system.
+This is a work-in-progress and lots of things are not working
+yet. If you'd like to try it out you can run `src/main_cli.js`.
+Check [this issue](https://github.com/HeyPuter/phoenix/issues/14)
+for updated information on our progress.
+
+## Testing
+
+You can find our tests in the [test/](./test) directory.
+Testing is done with [mocha](https://www.npmjs.com/package/mocha).
+Make sure it's installed, then run:
+
+```sh
+npm test
+```
+
+## What's on the Roadmap?
+
+We're looking to continue improving the shell and broaden its usefulness.
+Here are a few ideas we have for the future:
+
+- local machine platform support
+ > See [this issue](https://github.com/HeyPuter/phoenix/issues/14)
+- further support for the POSIX Command Language
+ > Check our list of [missing features](doc/missing-posix.md)
+
diff --git a/packages/phoenix/assets/index.html b/packages/phoenix/assets/index.html
new file mode 100644
index 00000000..9f609ae7
--- /dev/null
+++ b/packages/phoenix/assets/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+
diff --git a/packages/phoenix/config/dev.js b/packages/phoenix/config/dev.js
new file mode 100644
index 00000000..01886c81
--- /dev/null
+++ b/packages/phoenix/config/dev.js
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+globalThis.__CONFIG__ = {
+ "origin": "https://puter.local:8080",
+ "shell.href": "https://puter.local:8081",
+ "sdk_url": "http://puter.localhost:4100/sdk/puter.js",
+};
diff --git a/packages/phoenix/config/release.js b/packages/phoenix/config/release.js
new file mode 100644
index 00000000..45e3cbc4
--- /dev/null
+++ b/packages/phoenix/config/release.js
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+globalThis.__CONFIG__ = {
+ origin: location.origin,
+ 'shell.href': location.origin + '/puter-shell/',
+ sdk_url: 'https://puter.com/puter.js/v2',
+};
diff --git a/packages/phoenix/doc/devlog.md b/packages/phoenix/doc/devlog.md
new file mode 100644
index 00000000..59aad3f9
--- /dev/null
+++ b/packages/phoenix/doc/devlog.md
@@ -0,0 +1,1152 @@
+## 2023-05-05
+
+### Iframe Shell Architecture
+
+Separating the terminal emulator from the shell will make it possible to re-use
+Puter's terminal emulator for containers, emulators, and tunnels.
+
+Puter's shell will follow modern approaches for handling data; this means:
+- Commands will typically operate on streams of objects
+ rather than streams of bytes.
+- Rich UI capabilities, such as images, will be possible.
+
+To use Puter's shell, this terminal emulator will include an adapter shell.
+The adapter shell will delegate to the real Puter shell and provide a sensible
+interface for an end-user using an ANSI terminal emulator.
+
+This means the scope of this terminal emulator is to be compatible with
+two types of shells:
+- Legacy ANSI-compatible shells
+- Modern Puter-compatible shells
+
+To avoid duplicate effort, the ANSI adapter for the Puter shell will be
+accessed by the terminal emulator through cross-document messaging,
+as though it were any other ANSI shell provided by a third-party service.
+This will also keep these things loosely coupled so we can separate the
+adapter in the future and allow other terminal emulators to take
+advantage of it.
+
+## 2023-05-06
+
+### The Context
+
+In creating the state processor I made a variable called
+`ctx`, representing contextual information for state functions.
+
+The context has a few properties on it:
+- constants
+- locals
+- vars
+- externs
+
+#### constants
+
+Constants are immutable values tied to the context.
+They can be overriden when a context is constructed but
+cannot be overwritten within an instance of the context.
+
+#### variables
+
+Variabes are mutable context values which the caller providing
+the context might be able to access.
+
+#### locals
+
+Locals are the same as varaibles but the state processor
+exports them. This might not have been a good idea;
+maybe to the user of a context these should appear
+to be the same as variables, because the code using a
+context doesn't care what the longevity of locals is
+vs variables.
+
+Perhaps locals could be a useful concept for values that
+only change under a sub-context, but this is already
+true about constants since sub-contexts can override
+them. After all, I can't think of a compelling reason
+not to allow overridding constants when you're creating
+a sub-context.
+
+#### externs
+
+Externs are like constants in that they're not mutable to
+the code using a context. However, unlike constants they're
+not limited to primitive values. They can be objects and
+these objects can have side-effects.
+
+### How to make the context better moving forward?
+
+#### Composing contexts
+
+The ability to compose context would be useful. For example
+the readline function could have a context that's a composition
+of the ANSI context (containing ANSI constantsl maybe also
+library functions in the future), an outputter context since
+it outputs characters to the terminal, as well as a context
+specific to handlers under the readline utility.
+
+#### Additional reflection
+This idea of contexts and compositing contexts is actually
+something I've been thinking about for a long time. Contexts
+are an essential component in FOAM for example. However, this
+idea of separating **constants**, **imports**, and
+**side-effect varibles** (that is, variables something else
+is able to access),
+is not something I thought about until I looked at the source
+code for ash (an implementation of `sh`), and considered how
+I might make that source code more portable by repreasting
+it as language-agnostic data.
+
+## 2023-05-07
+
+### Conclusion of Context Thing from Yesterday
+
+I just figured something out after re-reading yesterday's
+devlog entry.
+
+While the State Processor needs a separate concept of
+variables vs locals, even the state functions don't care
+about this distinction. It's only there so certain values
+are cleared at each iteration of the state processor.
+
+This means a context can be composed at each iteration
+containing both the instance variables and the transient
+variables.
+
+### When Contexts are Equivalent to Pure Functions
+
+In pure-functional logic functions do not have side effects.
+This means they would never change a value by reference, but
+they would return a value.
+
+When a subcontext is created prior to a function call, this
+is equivalent to a pure function under certain conditions:
+- the values which may be changed must be explicity stated
+- the immediate consequences of updating any value are known
+
+## 2023-05-08
+
+### Sending authorization information to the shell
+
+Separating the terminal emulator from the shell currenly
+means that the terminal is a Puter app and the shell is
+a service being used by a Puter app, rather than natively
+being a Puter app.
+
+This may change in the future, but currently it means the
+terminal emulator needs to - not because it's the terminal
+emulator, but because it's the Puter application - configure
+the shell with authorization information.
+
+There are a few different approaches to this:
+- pass query string parameters onto the shell via url
+- send a non-binary postMessage with configuration
+- send an ANSI escape code followed by a binary-encoded
+ configuration message
+- construct a Form object in javascript and do a POST
+ request to the target iframe
+
+The last option seems like it could be a CORS nightmare since
+right now I'm testing in a situation where the shell happens
+to be under the same domain name as the terminal emulator, but
+this may not always be the case.
+
+Passing query string parameters over means authorization
+tokens are inside the DOM. While this is already true
+about the parent iframe I'd like to avoid this in case we
+find security issues with this approach under different
+situations. For example the parent iframe is in a situation
+where userselect and the default context menu are disabled,
+which may be preventing a user from accidentally putting
+sensitive html attributes in their clipboard.
+
+That leaves the two options for sending a postMessage:
+either binary, or a non-binary message. The binary approach
+would require finding handling an OSC escape sequence handler
+and creating some conventions for how to communicate with
+Puter's API using ANSI escape codes. While this might be useful
+in the future, it seems more practical to create a higher-level
+message protocol first and then eventually create an adapter
+for OSC codes in the future if need is found for one.
+
+So with that, here are window messages between Puter's
+ANSI terminal emulator and Puter's ANSI adapter for Puter's
+shell:
+
+#### Ready message
+
+Sent by shell when it's loaded.
+
+```
+{ $: 'ready' }
+```
+
+#### Config message
+
+Sent by terminal emulator after shell is loaded.
+
+```
+{
+ $: 'config',
+ ...variables
+}
+```
+
+All `variables` are currently keys from the querystring
+but this may change as the authorization mechanism and
+available shell features mature.
+
+## 2023-05-09
+
+### Parsing CLI arguments
+
+Node has a bulit-in utility, but using this would be
+unreliable because it's allowed to depend on system-provided
+APIs which won't be available in a browser.
+
+There's
+[a polyfill](https://github.com/pkgjs/parseargs/tree/main)
+which doesn't appear to depend on any node builtins.
+It does not support sub-commands, nor does it generate
+helptext, but it's a starting point.
+
+If each command specifies a parser for CLI arguments, and also
+provides configuration in a format specific to that parser,
+there are a few advantages:
+- easy to migrate away from this polyfill later by creating an
+ adapter or updating the commands which use it.
+- easy to add custom argument processors for commands which
+ have an interface that isn't strictly adherent to convention.
+- auto-complete and help can be generated with knowledge of how
+ CLI arguments are processed by a particular command.
+
+## 2023-05-10
+
+### Kind of tangential, but synonyms are annoying
+
+The left side of a UNIX pipe is the
+- source, faucet, producer, upstream
+
+The right side of a UNIX pipe is the
+- target, sink, consumer, downstream
+
+I'm going to go with `source` and `target` for any cases like this
+because they have the same number of letters, and I like when similar
+lines of code are the same length because it's easier to spot errors.
+
+## 2023-05-14
+
+### Retro: Terminal Architecture
+
+#### class: PreparedCommand
+
+A prepared command contains information about a command which will
+be invoked within a pipeline, including:
+- the command to be invoked
+- the arguments for the command (as tokens)
+- the context that the command will be run under
+
+A prepared command is created using the static method
+`PreparedCommand.createFromTokens`. It does not have a
+context until `setContext` is later called.
+
+#### class Pipeline
+
+A pipeline contains PreparedCommand instances which represent the
+commands that will be run together in a pipeline.
+
+A pipeline is created using the static method
+`Pipeline.createFromTokens`, which accepts a context under which
+the pipeline will be constructed. The pipeline's `execute` method
+will also be passed a context when the pipeline should begin running,
+and this context can be different. (this is also the context that
+will be passed to each `PreparedCommand` instance before each
+respective `execute` method is called).
+
+#### class Pipe
+
+A pipe is composed of a readable stream and a writable stream.
+A respective `reader` and `writer` are exposed as
+`out` and `in` respectively.
+
+The readable stream and writable stream are tied together.
+
+#### class Coupler
+
+A coupler aggregates a reader and a writer and begins actively
+reading, relaying all items to the writer.
+
+This behaviour allows a coupler to be used as a way of connecting
+two pipes together.
+
+At the time of writing this, it's used to tie the pipe that is
+created after the last command in a pipeline to the writer of the
+pseudo terminal target, instead of giving the last command this
+writer directly. This allows the command to close its output pipe
+without affecting subsequent functionality of the terminal.
+
+### Behaviour of echo escapes
+
+#### behaviour of `\\` should be verified
+Based on experimentation in Bash:
+- `\\` is always seen by echo as `\`
+ - this means `\\a` and `\a` are the same
+
+#### difference between `\x` and `\0`
+
+In echo, `\0` initiates an octal escape while `\x` initiates
+a hexadecimal escape.
+
+However, `\0` without being followed by a valid octal sequence
+is considered to be `NULL`, while `\x` will be outputted literally
+if not followed with a valid hexadecimal sequence.
+
+If either of these escapes has at least one valid character
+for its respective numeric base, it will be processed with that
+value. So, for example, `echo -en "\xag" | hexdump -C` shows
+bytes `0A 67`, as does the same with `\x0ag` instead of `\xag`.
+
+## 2023-05-15
+
+### Synchronization bug in Coupler
+
+[this issue](https://github.com/HeyPuter/dev-ansi-terminal/issues/1)
+was caused by an issue where the `Coupler` between a pipeline and
+stdout was still writing after the command was completed.
+This happens because the listener loop in the Coupler is async and
+might defer writing until after the pipeline has returned.
+
+This was fixed by adding a member to Coupler called `isDone` which
+provides a promise that resolves when the Coupler receives the end
+of the stream. As a consequence of this it is very important to
+ensure that the stream gets closed when commands are finished
+executing; right now the `PreparedCommand` class is responsible for
+this behaviour, so all commands should be executed via
+`PreparedCommand`.
+
+### tail, and echo output chunking
+
+Right now `tail` outputs the last two items sent over the stream,
+and doesn't care if these items contain line breaks. For this
+implementation to work the same as the "real" tail, it must be
+asserted that each item over the stream is a separate line.
+
+Since `ls` is outputting each line on a separate call to
+`out.write` it is working correctly with tail, but echo is not.
+This could be fixed in `tail` itself, having it check each item
+for line breaks while iterating backwards, but I would rather have
+metadata on each command specifying how it expects its input
+to be chunked so that the shell can accommodate; although this isn't
+how it works in "real" bash, it wouldn't affect the behaviour of
+shell scripts or input and it's closer to the model of Puter's shell
+for JSON-like structured data, which may help with improved
+interoperability and better code reuse.
+
+## 2023-05-22
+
+### Catching Up
+
+There hasn't been much to log in the past few days; most updates
+to the terminal have been basic command additions.
+
+The next step is adding the redirect operators (`<` and `>`),
+which should involve some information written in this dev log.
+
+### Multiple Output Redirect
+
+In Bash, the redirect operator has precedence over pipes. This is
+sensible but also results in some situations where a prompt entry
+has dormant pieces, for example two output redirects (only one of
+them will be used), or an output redirect and a pipe (the pipe will
+receive nothing from stdout of the left-hand process).
+
+Here's an example with two output redirects:
+
+```
+some-command > a_file.txt > b_file.txt
+```
+
+In Puter's ANSI shell we could allow this as a way of splitting
+the output. Although, this only really makes sense if stdout will
+also be passed through the pipeline instead of consumed by a
+redirect, otherwise the behaviour is counterintuitive.
+
+Maybe for this purpose we can have a couple modes of interpretation,
+one where the Puter ANSI Shell behaves how Bash would and another
+where it behaves in a more convenient way. Shell scripts with no
+hashbang would be interpreted the Bash-like way while shell scripts
+with a puter-specific hashbang would be interpreted in this more
+convenient way.
+
+For now I plan to prioritize the way that seems more logical as it
+will help keep the logic of the shell less complicated. I think it's
+likely that we'll reach full POSIX compatibility via Bash running in
+containers or emulators before the Puter ANSI shell itself reaches
+full POSIX compatibility, so for this reason it makes sense to
+prioritize making the Puter ANSI shell convenient and powerful over
+making it behave like Bash. Additionally, we have a unique situation
+where we're not so bound to backwards compatibility as is a
+distribution of a personal computer operating system, so we should
+take advantage of that where we can.
+
+## 2023-05-23
+
+### Adding more coreutils
+
+- `clear` was very easy; it's just an escape code
+- `printenv` was also very easy; most of the effort was already done
+
+### First steps to handling tab-completion
+
+#### Getting desired tab-completion behaviour from input state
+Tab-completion needs information about the type of command arguments.
+Since commands are modelled, it's possible the model of a command can
+provide this information. For example a registered command could
+implement `getTabCompleterFor(ARG_SPEC)`.
+
+`ARG_SPEC` would be an identifier for an argument that is understood
+by readline. Ex: `{ $: 'positional', pos: 0 }` for the first positional
+argument, or `{ $: 'named', name: 'verbose' }` for a named parameter
+called `verbose`.
+
+The command model already has a nested model specifying how arguments
+are parsed, so this model could describe the behaviour for a
+`getArgSpecFromInputState(input, i)`, where `input` is the
+current text in readline's buffer and `i` is the cursor position.
+This separates the concern of knowing what parameter the user is
+typing in from readline, allowing modelled commands to support tab
+completion for arbitrary syntaxes.
+
+**revision**
+
+It's better if the command model has just one method which
+readline needs to call, ex: `getTabCompleterFromInputState`.
+I've left the above explanation as-is however because it's easier
+to explain the two halves if its functionality separately.
+
+### Trigger background readdir call on PWD change
+
+When working on the FUSE driver for Puter's filesystem I noticed that
+tab completion makes a readdir call upon the user pressing tab which
+blocks the tab completion behaviour until the call is finished.
+While this works fine on local filesystems, it's very confusing on
+remote filesystems where the ping delay will - for a moment - make it
+look like tab completion isn't working at all.
+
+Puter's shell can handle this a bit better. Triggering a readdir call
+whenever PWD changes will allow tab-completion to have a quicker
+response time. However, there's a caveat; the information about what
+nodes exist in that directory might be outdated by the time the user
+tries to use tab completion.
+
+My first thought was for "tab twice" to invoke a readdir to get the
+most recent result, however this conflicts with pressing tab once to
+get the completed path and then pressing tab a second time to get
+a list of files within that path.
+
+My second thougfht is using ctrl + tab. The terminal will need to
+provide some indication to the user that they can do this and what
+is happening.
+
+Here are a few thoughts on how to do this with ideal UX:
+
+- after pressing tab:
+ - complete the text if possible
+ - highlight the completed portion in a **bright** color
+ - a dim colour would convey that the completion wasn't input yet
+ - display in a **hint bar** the following items:
+ - `[Ctrl+Tab]: re-complete with recent data`
+ - `[Ctrl+Enter]: more options`
+
+### Implementation of background readdir
+
+The background `readdir` could be invoked in two ways:
+- when the current working directory changes
+- at a poll interval
+
+These means the **action** of invoking background readdir needs
+to be separate from the method by which it is called.
+
+Also, results from a previous `readdir` need to be marked invalid
+when the current working directory changes.
+
+There is a possibility that the user might use tab completion before
+the first `readdir` is called for a given pwd, which means the method
+to get path completions must be async.
+
+if `readdir` is called because of a pwd change, the poll timer should
+be reset so that it's not called again too quickly or at the same
+time.
+
+#### Concern Mapping
+
+- **PuterANSIShell**
+ - does not need to be aware of this feature
+- **readline**
+ - needs to trap Tab
+ - needs to recognize what command is being entered
+ - needs to delegate tab completion logic to the command's model
+ - does not need to be aware of how tab completion is implemented
+- **readdir action**
+ - needs WRITE to cached dir lists
+- **readdir poll timer**
+ - needs READ to cached dir lists to check when they were
+ updated
+ - needs the path to be polled
+
+#### Order of implementation
+
+- First implementation will **not** have **background readdir**.
+ - Interfaces should be appropriate to implement this after.
+- When tab completion is working for paths, then readdir caching
+ can be implemented.
+
+## 2023-05-25
+
+### Revising the boundary between ANSI view and Puter Shell
+
+Now there are several coreutil commands and a few key shell
+features, so it's a good time to take a look at the architecture
+and see if the boundary between the ANSI view and Puter Shell
+corresponds to the original intention.
+
+| Shell | I/O | instructions |
+| ------------ | ----- | ------------ |
+| ANSI Adapter | TTY | text |
+| Puter Shell | JSON | logical tree |
+
+Note from the above table that the Puter Shell itself should
+be "syntax agnostic" - i.e. it needs the ANSI adapter or a
+GUI on top of it to be useful at the UI boundary.
+
+#### Pipelines
+
+The ANSI view should be concerned with pipe syntax, while
+pipeline execution should be a concern of the syntax-agnostic
+shell. However, currently the ANSI view is responsible for
+both. This is because there is no intermediate format for
+parsed pipeline instructions.
+
+##### to improve
+- create intermediate representation of pipelines and redirects
+
+#### Command IO
+
+The ANSI shell does IO in terms of either bytes or strings. When
+commands output strings instead of bytes, their output is adapted
+to the Uint8Array type to prevent commands further in the pipeline
+from misbehaving due to an unexpected input type.
+
+Since pipeline I/O should be handled at the Puter shell, this kind
+of adapting will happen at that level also.
+
+#### to improve
+- ANSI view should send full pipeline to Puter Shell
+- Puter Shell protocol should be improved so that the
+ client/view can specify a desired output format
+ (i.e. streams vs objects)
+
+### Pipeline IR
+
+The following is an intermediate representation for pipelines
+which separates the concern of the ANSI shell syntax from the
+logical behaviour that it respresents.
+
+```javascript
+{
+ $: 'pipeline',
+ nodes: [
+ {
+ $: 'command',
+ id: 'ls',
+ positionals: [
+ '/ed/Documents'
+ ]
+ },
+ {
+ $: 'command',
+ id: 'tail',
+ params: {
+ n: 2
+ }
+ }
+ ]
+}
+```
+
+The `$` property identifies the type of a particular node.
+The space of other properties including the `$` symbol is reserved
+for meta information about nodes; for example properties like
+`$origin` and `$whitespace` could turn this AST into a
+CST.
+
+For the same of easier explanation here I'm going to coin the
+term "Abstract Logic Tree" (ALT) and use it along with the
+conventional terms as follows:
+
+| Abrv | Name | Represents |
+| ---- | -------------------- | -------------------- |
+| ALT | Abstract Logic Tree | What it does |
+| AST | Abstract Syntax Tree | How it was described |
+| CST | Concrete Syntax Tree | How it was formatted |
+
+The pipeline format described above is an AST for the
+input that was understood by the ANSI shell adapter.
+It could be converted to an ALT if the Puter Shell is
+designed to understand pipelines a little differently.
+
+```javascript
+{
+ $: 'tail',
+ subject: {
+ $: 'list',
+ subject: {
+ $: 'filepath',
+ id: '/ed/Documents'
+ }
+ }
+}
+```
+
+This is not final, but shows how the AST for pipeline
+syntax can be developed in the ANSI shell adapter without
+constraining how the Puter Shell itself works.
+
+### Syntaxes
+
+#### Why CST tokenization in a shell would be useful
+
+There are a lot of decisions to make at every single level
+of syntax parsing. For example, consider the following:
+
+```
+ls | tail -n 2 > "some \"data\".txt"
+```
+
+Tokens can be interpreted at different levels of detail.
+A standard shell tokenizer would likely eliminate information
+about escape characters within quoted strings at this point.
+For example, right now the Puter ANSI shell adapter takes
+after what a standard shell does and goes for the second
+option described here:
+
+```
+[
+ 'ls', '|', 'tail', '-n', '2', '>',
+ // now do we do [","some ", "\\\"", ...],
+ // or do we do ["some \"data\".txt"] ?
+]
+```
+
+This is great for processing and executing commands because
+this information is no longer relevant at that stage.
+
+However, suppose you wanted to add support for syntax highlighting,
+or tell a component responsible for a specific context of tab
+completion where the cursor is with respect to the tokenized
+information. This is no longer feasible.
+
+For the latter case, the ANSI shell adapter works around this
+issue by only parsing the commandline input up to the cursor
+location - meaning the last token will always represent the
+input up to the cursor location. The input after is truncated
+however, leading to the familiar inconvenient situation seen in
+many terminals where tab completion does something illogical with
+respect the text after your cursor.
+
+i.e. the following, with the cursor position represented by `X`:
+
+```
+echo "hello" > some_Xfile.txt
+```
+
+will be transformed into the following:
+
+```
+echo "hello" > some_file.txtXfile.txt
+```
+
+What would be more helpful:
+- terminal bell, because `some_file.txt` is already complete
+- `some_other_Xfile.txt` if `some_other_file.txt` exists
+
+So syntax highlighting and tab completion are two reasons why
+the CST is useful. There may be other uses as well that I
+haven't thought of. So this seems like a reasonable idea.
+
+#### Choosing monolithic or composite lexers
+
+Next step, there are also a lot of decisions to make
+about processing the text into tokens.
+
+For example, we can take after the very feature that make
+shells so versatile - pipelines - and apply this concept
+to the lexer.
+
+```
+Level 1 lexer produces:
+ ls, |, tail, -n, 2, >, ", some , \", data, \", .txt
+
+Level 2 lexer produces:
+ ls, |, tail, -n, 2, >, "some \"data\".txt"
+
+```
+
+This creates another decision fork, actually. It raises the
+question of how to associate the token "some \"data\".txt"
+with the tokens it was composed from at the previous level
+or lexing, if this should be done at all, and otherwise if
+CST information should be stored with the composite token.
+
+If lexers provide verbose meta information there might be
+a concern about efficiency, however lexers could be
+configurable in this respect. Furthermore, lexers could be
+defined separately from their implementation and JIT-compiled
+based on configuration so you actually get an executable bytecode
+which doesn't produce metadata (for when it's not needed).
+
+While designing JIT-compilable lexer definitions is incredibly
+out of scope for now, the knowledge that it's possible justifies
+the decision to have lexers produce verbose metadata.
+
+If the "Level 1 lexer" in the example above stores CST information
+in each token, the "Level 2 lexer" can simply let this information
+propagate as it stores information about what tokens were composed
+to produce a higher-level token. This means concern about
+whitespace and formatting is limited to the lowest-level lexer which
+makes the rest of the lexer stack much easier to maintain.
+
+#### An interesting philosophical point about lexers and parsers
+
+Consider a stack of lexers that builds up to high-level constructs
+like "pipeline", "command", "condition", etc. The line between a
+parser and a lexer becomes blurry, as this is in fact a bottom-up
+parser composed of layers, each of which behaves like a lexer.
+
+I'm going to call the layers `PStrata` (singular: `PStratum`)
+to avoid collision with these concepts.
+
+### The "Implicit Interface Aggregator"
+
+Vanilla javascript doesn't have interfaces, which sometimes seems
+to make it difficult to have guarantees about type methods an
+object will implement, what values they'll be able to handle, etc.
+
+To solve some of the drawbacks of not having interfaces, I'm going
+to use a pattern which Chat GPT just named the
+Implicit Interface Aggregator Pattern.
+
+The idea is simple. Instead of having an interface, you have a class
+which acts as the user-facing API, and holds the real implementation
+by aggregation. While this doesn't fix everything, it leaves the
+doors open for multiple options in the future, such as using
+typescript or a modelling framework, without locking either of these
+doors too early. Since we're potentially developing on a lot of
+low-level concepts, perhaps we'll even have our own technology that
+we'd like to use to describe and validate the interfaces of the code
+we write at some point in the future.
+
+This class can
+handle concerns such as adapting different types of inputs and
+outputs; things which an implementation doesn't need to be concerned
+with. Generally this kind of separation of concerns would be done
+using an abstract class, but this is an imperfect separation of
+concerns because the implementor needs to be aware of the abstract
+class. Granted, this isn't usually a big deal, but what if the
+abstract class and implementors are compiled separately? It may be
+advantageous that implementors don't need to have all the build
+dependencies of the abstract class.
+
+The biggest drawback of this approach is that while the aggregating
+class can implement runtime assertions, it doesn't solve the issue
+of the lack of build-time assertions, which are able to prevent
+type errors from getting to releases entirely. However, it does
+leave room to add type definitions for this class and its
+implementors (turning it into the facade pattern), or apply model
+definitions (or schemas) to the aggregator and the output of a
+static analysis to the implmentors (turning it into a model
+definition).
+
+#### Where this will be used
+
+The first use of this pattern will be `PStratum`.
+PStratum is a facade which aggregates a PStratumImplementor using
+the pattern described above.
+
+The following layers will exist for the shell:
+- StringPStratum will take a string and provide bytes.
+- LowLexPStratum will take bytes and identify all syntax
+ tokens and whitespace.
+- HiLexPStratum will create composite tokens for values
+ such as string literals
+- LogicPStratum will take tokens as input and produce
+ AST nodes. For example, this is when successive instances
+ of the `|` (pipe) operator will be converted into
+ a pipeline construct.
+
+
+### First results from the parser
+
+It appears that the methods I described above are very effective
+for implementing a parser with support for concrete syntax trees.
+
+By wrapping implementations of `Parser` and `PStratum` in facades
+it was possible to provide additional functionality for all
+implementations in one place:
+- `fork` and `join` is implemented by PStratum; each implementation
+ does not need to be aware of this feature.
+- the `look` function (AKA "peek" behaviour) is implemented by
+ PStratum as well.
+- A PStratum implementation can implement the behaviour to reach
+ for previous values, but PStratum has a default implementation.
+ The BytesPStratumImpl overrides this to provide Uint8Arrays instead
+ of arrays of Number values.
+- If parser implementations don't return a value, Parser will
+ create the ParseResult that represents an unrecognized input.
+
+It was also possible to add a Parser factory which adds additional
+functionality to the sub-parsers that it creates:
+- track the tokens each parser gets from the delegate PStratum
+ and keep a record of what lower-level tokens were composed to
+ produce higher-level tokens
+- track how many tokens each parser has read for CST metadata
+
+A layer called `MergeWhitespacePStratumImpl` completes this by
+reading the source bytes for each token and using it to compute
+a line and column number. After this, the overall parser is
+capable of starting the start byte, end byte, line number, and
+column number for each token, as well as preserve this information
+for each composite token created at higher levels.
+
+The following parser configuration with a hard-coded input was
+tested:
+
+```javascript
+sp.add(
+ new StringPStratumImpl(`
+ ls | tail -n 2 > "test \\"file\\".txt"
+ `)
+);
+sp.add(
+ new FirstRecognizedPStratumImpl({
+ parsers: [
+ cstParserFac.create(WhitespaceParserImpl),
+ cstParserFac.create(LiteralParserImpl, { value: '|' }, {
+ assign: { $: 'pipe' }
+ }),
+ cstParserFac.create(UnquotedTokenParserImpl),
+ ]
+ })
+);
+sp.add(
+ new MergeWhitespacePStratumImpl()
+)
+```
+
+Note that the multiline string literal begins with whitespace.
+It is therefore expected that each token will start on line 1,
+and `ls` will start on column 8.
+
+The following is the output of the parser:
+
+```javascript
+[
+ {
+ '$': 'symbol',
+ text: 'ls',
+ '$cst': { start: 9, end: 11, line: 1, col: 8 },
+ '$source': Uint8Array(2) [ 108, 115 ]
+ },
+ {
+ '$': 'pipe',
+ text: '|',
+ '$cst': { start: 12, end: 13, line: 1, col: 11 },
+ '$source': Uint8Array(1) [ 124 ]
+ },
+ {
+ '$': 'symbol',
+ text: 'tail',
+ '$cst': { start: 14, end: 18, line: 1, col: 13 },
+ '$source': Uint8Array(4) [ 116, 97, 105, 108 ]
+ },
+ {
+ '$': 'symbol',
+ text: '-n',
+ '$cst': { start: 19, end: 21, line: 1, col: 18 },
+ '$source': Uint8Array(2) [ 45, 110 ]
+ },
+ {
+ '$': 'symbol',
+ text: '2',
+ '$cst': { start: 22, end: 23, line: 1, col: 21 },
+ '$source': Uint8Array(1) [ 50 ]
+ }
+]
+```
+
+No errors were observed in this output, so I can now continue
+adding more layers to the parser to get higher-level
+representations of redirects, pipelines, and other syntax
+constructs that the shell needs to understand.
+
+## 2023-05-28
+
+### Abstracting away communication layers
+
+As of now the ANSI shell layer and terminal emulator are separate
+from each other. To recap, the ANSI shell layer and object-oriented
+shell layer are also separate from each other, but the ANSI shell
+layer current holds more functionality than is ideal; most commands
+have been implemented at the ANSI shell layer in order to get more
+functionality earlier in development.
+
+Although the ANSI shell layer and object-oriented shell layer are
+separate, they are both coupled with the communication layer that's
+currently used between them: cross-document messaging. This is ideal
+for communication between the terminal emulator and ANSI shell, but
+less ideal for that between that ANSI shell and OO shell. The terminal
+emulator is a web app and will always be run in a browser environment,
+which makes the dependency on cross-document messaging acceptable.
+Furthermore it's a small body of code and it can easily be extended
+upon to support multiple protocols of communication in the future
+rather than just cross-document messaging. The ANSI shell on the other
+hand, which currently communications with the OO shell using
+cross-document messaging, will not always be run in a browser
+environment. It is also completely dependent on the OO shell, so it
+would make sense to bundle the OO shell with it in some environments.
+
+The dependency between the ANSI shell and OO shell is not bidirectional.
+The OO shell layer is intended to be useful even without the ANSI shell
+layer; for example a GUI for constructing and executing pipelines would
+be more elegant built upon the OO shell than the ANSI shell, since there
+wouldn't be a layer text processing between two layers of
+object-oriented logic. When also considering that in Puter any
+alternative layer on top of the OO shell is likely to be built to run
+in a browser environment, it makes sense to allow the OO shell to be
+communicated with via cross-document messaging.
+
+The following ASCII diagram describes the communication relationships
+between various components described above:
+
+```
+note: "XD" means cross-document messaging
+
+[web terminal]
+ |
+ (XD)
+ |
+ |- (stdio) --- [local terminal]
+ |
+[ANSI Shell]
+ |
+ (direct calls / XD)
+ |
+ |-- (XD) --- [web power tool]
+ |
+ [OO Shell]
+
+```
+
+It should be interpreted as follows:
+- OO shell can communicate with a web power tool via
+ cross-document messaging
+- the OO shell and ANSI shell should communicate via
+ either direct calls (when bundled) or cross-document
+ messaging (when not bundled together)
+- the ANSI shell can be used under a web terminal via
+ cross-document messaging, or a local terminal via
+ the standard I/O mechanism of the host operating system.
+
+## 2023-05-29
+
+### Interfacing with structured data
+
+Right now all the coreutils commands currently implemented output
+byte streams. However, allowing commands to output objects instead
+solves some problems with traditional shells:
+- text processing everywhere
+ - it's needed to get a desired value from structured data
+ - commands are often concerned with the formatting of data
+ rather than the significance of the data
+ - commands like `awk` are archaic and difficult to use,
+ but are often necessary
+- information which a command had to obtain is often lost
+ - a good example of this is how `ls` colourizes different
+ inode types but this information goes away when you pipe
+ it to a command like `tail`
+
+#### printing structured data
+
+Users used to a POSIX system will have some expectations
+about the output of commands. Sometimes the way an item
+is formatted depends on some input arguments, but does not
+change the significance of the item itself.
+
+A good example of this is the `ls` command. It prints the
+names of files. The object equivalent of this would be for
+it to output CloudItem objects. Where it gets tricky is
+`ls` with no arguments will display just the name, while
+`ls -l` will display details about each file such as the
+mode, owner, group, size, and date modified.
+
+##### per-command outputters
+
+If the definition for the `ls` command included an output
+formatter this could work - if ls' standard output is
+attached to the PTT instead of another command it would
+format the output according to the flags.
+
+This still isn't ideal though. If `ls` is piped to `tail`
+this information would be lost. This differs from the
+expected behaviour from posix systems; for example:
+
+```
+ls -l | tail -n 2 > last_two_lines.txt
+```
+
+this command would output all the details about the last
+two files to the text file, rather than just the names.
+
+##### composite output objects with formatter + data
+
+A command outputting objects could also attach a formatter
+to each object. This has the advantage that an object can
+move through a pipeline and then be formatted at the end,
+but it does have a drawback that sometimes the formatter
+will be the same for every object, and sending a copy
+of the formatter with each object would be redundant.
+
+##### using a formatter registry
+
+A transient registry of object formatters, existing for
+the lifespan of the pipeline, could contain each unique
+formatter that any command in the pipeline produced for
+one or more of it's output objects. Each object that it
+outputs now just needs to refer to an existing formatter
+which solves the problem of redundant information passing
+through the pipeline
+
+
+##### keeping it simple
+
+This idea of a transient registry for unique implementations
+of some interface could be useful in a general sense. So, I
+think it makes sense to actually implement formatters using
+the more redundant behaviour first (formatter is coupled with
+each object), and then later create an abstraction for
+obtaining the correct formatter for an object so that this
+optimization can be implemented separately from this specific
+use of the optimization.
+
+## 2024-02-01
+
+### StrataParse and Tokens with Command Substitution
+
+**note:** this devlog entry was written in pieces as I made
+significant changes to the parser, so information near the
+beginning is less accurate than information towards the end.
+
+In the "first half" portion of the terminal parser, which
+builds a "lexer"* (*not a pure lexer) for parsing, there
+currently exists an implementation of parsing for quoted strings.
+I have in the past implemented a quoted string parser at least
+two different ways - a state machine parser, and with composable
+parsers. The string parser in `buildParserFirstHalf` uses the
+second approach. This is what it looks like represented as a
+lisp-ish pseudo-code:
+
+```javascript
+sequence(
+ literal('"')
+ repeat(
+ choice(
+ characters_until('\\' or '"')
+ sequence(
+ literal('\\')
+ choice(
+ literal('"'),
+ ...escape_substitutions))))
+ literal('"'))
+```
+
+In a BNF grammar, this might be assigned to a symbol name
+like "quoted-string". In `strataparse` this is represented
+by having a layer which recognizes the components of a string
+(like each sequence of characters between escapes, each escape,
+and the closing quotation mark), and then a higher-level layer
+which composes those to create a single node representing
+the string.
+
+I really like this approach because the result is a highly
+configurable parser that will let you control how much
+information is kept as you advance to higher-level layers
+(ex: CST instead of AST for tab-completion checks),
+and only parse to a certain level if desired
+(ex: only "first half" of the parser is used for
+tab-completion checks).
+
+The trouble is the POSIX Shell Command Language allows part of a
+token to be a command substitution, which means a stack needs to
+be maintianed to track nested states. Implementing this in the
+current hand-written parser was very tricky.
+
+Partway through working on this I took a look at existing
+shell syntax parsers for javascript. The results weren't very
+promising. None of the available parsers could produce a CST,
+which is needed for tab completion and will aid in things
+like syntax highlighting in the future.
+
+Between the modules `shell-parse` and `bash-parser`, the first
+was able to parse this syntax while the second threw an error:
+```
+echo $TEST"something to $($(echo echo) do)"with-this another-token
+```
+
+Another issue with existing parsers, which makes me wary of even
+using pegjs (what `shell-parse` uses) directly is that the AST
+they produce requires a lot of branching in the interpreter.
+For example it's not known when parsing a token whether you'll
+get a `literal`, or a `concatenation` with an array of "pieces"
+which might contain literals. This is a perfectly valid
+representation of the syntax considering what I mentioned above
+about command substitution, but if there can be an array of
+pieces I would rather always have an array of pieces. I'm much
+more concerned with the simplicity and performance of the
+interpreter than the amount of memory the AST consumes.
+
+Finally, my "favourite" part: when you run a script in `bash`
+it doesn't parse the entire script and then run it; it either
+parses just one line or, if the line is a compound command
+(a structure like `if; then ...; done`) it parses multiple
+lines until it has parsed a valid compound command. This means
+any parser that can only parse complete inputs with valid syntax
+would need to repeatedly parse (1 line, 2 lines, 3 lines...)
+at each line until one of the parses is successful, if we wish
+to mimic the behaviour of a real POSIX shell.
+
+In conclusion, I'm keeping the hand-written parser and
+solving command substitution by maintaining state via stacks
+in both halves of the parser, and we will absolutely need to
+do static analysis and refactoring to simplify the parser some
+time in the future.
+
+## 2024-02-04
+
+### Platform Support and Deprecation of separate `puter-shell` repo
+
+To prepare for releasing the Puter Shell under an open-source license,
+it makes sense to move everything that's currently in `puter-shell` into
+this repo. The separation of concerns makes sense, but it belongs in
+a place called "platform support" inside this repo rather than in
+another repo (that was an oversight on my part earlier on).
+
+This change can be made incrementally as follows:
+- Expose an object which implements support for the current platform
+ to all the commands in coreutils.
+- Incrementally update commands as follows:
+ - add the necessary function(s) to `puter` platform support
+ - while doing this, use the instance of the Puter SDK owned
+ by `dev-ansi-terminal` instead of delegating to the
+ wrapper in the `puter-shell` repo via `postMessage`
+ - update the command to use the new implementation
+- Once all commands are updated, the XDocumentPuterShell class will
+ be dormant and can safely be removed.
diff --git a/packages/phoenix/doc/graveyard/keyboard_modifiers.md b/packages/phoenix/doc/graveyard/keyboard_modifiers.md
new file mode 100644
index 00000000..c52e3902
--- /dev/null
+++ b/packages/phoenix/doc/graveyard/keyboard_modifiers.md
@@ -0,0 +1,77 @@
+## keyboard modifier translation
+
+Encoding of modifier keys in `xterm` is done following this
+table:
+ encoded | keys pressed
+ --------|---------------------------
+ 2 | Shift
+ 3 | Alt
+ 4 | Shift + Alt
+ 5 | Control
+ 6 | Shift + Control
+ 7 | Alt + Control
+ 8 | Shift + Alt + Control
+ 9 | Meta
+ 10 | Meta + Shift
+ 11 | Meta + Alt
+ 12 | Meta + Alt + Shift
+ 13 | Meta + Ctrl
+ 14 | Meta + Ctrl + Shift
+ 15 | Meta + Ctrl + Alt
+ 16 | Meta + Ctrl + Alt + Shift
+
+This script was used to convert between more useful bit flags
+and the xterm encodings of the modifiers:
+
+```javascript
+const modifier_keys = ['shift', 'ctrl', 'alt', 'meta'];
+const MODIFIER = {};
+for ( let i=0 ; i < modifier_keys.length ; i++ ) {
+ MODIFIER[modifier_keys[i].toUpperCase()] = 1 << i;
+}
+
+const pc_modifier_list = [
+ MODIFIER.SHIFT,
+ MODIFIER.ALT,
+ MODIFIER.CTRL,
+ MODIFIER.META
+];
+
+const PC_STYLE_MODIFIER_MAP = {};
+
+(() => {
+ let i = 2;
+ for ( const mod of pc_modifier_list ) {
+ const new_entries = { [i++]: mod };
+ for ( const key in PC_STYLE_MODIFIER_MAP ) {
+ new_entries[i++] = mod | PC_STYLE_MODIFIER_MAP[key];
+ }
+ for ( const key in new_entries ) {
+ PC_STYLE_MODIFIER_MAP[key] = new_entries[key];
+ }
+ }
+})();
+
+for ( const k in PC_STYLE_MODIFIER_MAP ) {
+ console.log(`${k} :: ${print(PC_STYLE_MODIFIER_MAP[k])}`);
+}
+```
+
+However, it was eventually determined that the PC-style function
+keys, although this is not documented, really do represent bit
+flags if you simply subtract 1.
+
+For example, this situation doesn't look like it can be explained
+using bit flags:
+- **shift** is `2`
+- **ctrl** is `5`, and has two `1` bits
+- **shift** + **ctrl** is `6`
+- flags don't explain this: `2 | 5 = 7`
+
+But after subtracting `1` from each value:
+- **shift** is `1`
+- **ctrl** is `4`
+- **shift** + **ctrl** is `5`
+- flags work correctly: `1 | 4 = 5`
+
+This is true for all examples.
diff --git a/packages/phoenix/doc/graveyard/readline.md b/packages/phoenix/doc/graveyard/readline.md
new file mode 100644
index 00000000..9b463d70
--- /dev/null
+++ b/packages/phoenix/doc/graveyard/readline.md
@@ -0,0 +1,59 @@
+## method `readline` from `BetterReader`
+
+This method was meant to be a low-level line reader that simply
+terminates at the first line feed character and returns the
+input.
+
+This might be useful for non-visible inputs like passwords, but
+for visible inputs it is not practical unless the output stream
+provided is decorated with something that can filter undesired
+input characters that would move the terminal cursor.
+
+It's especially not useful for a prompt with history, since the
+up arrow should clear the input buffer and replace it with something
+else.
+
+Where this may shine is in a benchmark. The approach here doesn't
+explicitly iterate over every byte, so assuming methods like
+`.indexOf` and `.subarray` on TypedArray values are efficient this
+would be faster than the implementation which is now used.
+
+```javascript
+ async readLine (options) {
+ options = options ?? {};
+
+ let stringSoFar = '';
+
+ let lineFeedFound = false;
+ while ( ! lineFeedFound ) {
+ let chunk = await this.getChunk_();
+
+ const iLF = chunk.indexOf(CHAR_LF);
+
+ // do we have a line feed character?
+ if ( iLF >= 0 ) {
+ lineFeedFound = true;
+
+ // defer the rest of the chunk until next read
+ if ( iLF !== chunk.length - 1 ) {
+ this.chunks_.push(chunk.subarray(iLF + 1))
+ }
+
+ // (note): LF is not included in return value or next read
+ chunk = chunk.subarray(0, iLF);
+ }
+
+ if ( options.stream ) {
+ options.stream.write(chunk);
+ if ( lineFeedFound ) {
+ options.stream.write(new Uint8Array([CHAR_LF]));
+ }
+ }
+
+ const text = new TextDecoder().decode(chunk);
+ stringSoFar += text;
+ }
+
+ return stringSoFar;
+ }
+```
\ No newline at end of file
diff --git a/packages/phoenix/doc/license_header.txt b/packages/phoenix/doc/license_header.txt
new file mode 100644
index 00000000..ec7fa584
--- /dev/null
+++ b/packages/phoenix/doc/license_header.txt
@@ -0,0 +1,16 @@
+Copyright (C) 2024 Puter Technologies Inc.
+
+This file is part of Phoenix Shell.
+
+Phoenix Shell is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
diff --git a/packages/phoenix/doc/missing-posix.md b/packages/phoenix/doc/missing-posix.md
new file mode 100644
index 00000000..18e694c4
--- /dev/null
+++ b/packages/phoenix/doc/missing-posix.md
@@ -0,0 +1,23 @@
+# Missing POSIX Functionality
+
+### References
+
+- [POSIX.1-2017 Chapter 2: Shell Command Language](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html)
+
+### Shell Command Language features known to be missing from `phoenix`
+
+- Parameter expansion
+ > This is support for `$variables`, and this is **highest priority**.
+- Compound commands
+ > This is `if`, `case`, `while`, `for`, etc
+- Arithmetic expansion
+- Alias substitution
+
+### How to Contribute
+
+- Check the [README.md file](../README.md) for contributor guidelines.
+- Additional features will require updates to
+ [the parser](phoenix/src/ansi-shell/parsing).
+ Right now there are repeated concerns between
+ `buildParserFirstHalf` and `buildParserSecondHalf` which need to
+ be factored out.
diff --git a/packages/phoenix/doc/parser.md b/packages/phoenix/doc/parser.md
new file mode 100644
index 00000000..eb3ca2ee
--- /dev/null
+++ b/packages/phoenix/doc/parser.md
@@ -0,0 +1,55 @@
+# Puter Terminal Parser
+
+## The `strataparse` package
+
+The `strataparse` package makes it possible to build parser in distinct
+layers that we call "strata" (each one called a "stratum"). Rather then
+distinguish between a "lexer" and "parser", we can instead have an
+arbitrary number of layers that use different approaches to processing
+or parsing.
+
+Each stratum implements the method `next (api)`. The `api` object is
+provided by strataparse as the bridge between which the strata
+interact. Typically, it's used to call `api.delegate` to get a reference
+to the lower-level parser. Terminal strata like `StringPStratumImpl`, don't
+do this. The `next` method returns the next value in an object of the
+form `{ done: true/false, value: ... }`, matching the typical interface
+for iterators within this source code. When `done` is true, `value`
+can be a message (such as an error) indicating why parsing halted.
+
+## PuterShellParser
+
+At the time of writing this, the PuterShellParser class builds a parser
+with 4 strata, listed here from bottom up:
+
+### buildParserFirstHalf (the "lexer half")
+
+[source code](../src/ansi-shell/parsing/buildParserFirstHalf.js)
+
+- A "FirstRecognized" strata which behaves like a lexer. It converts
+ characters like `|` to AST nodes like `{ $: 'op.pipe' }`.
+ AST nodes use the key `$` to identify the type and can have other
+ arbitrary values.
+- A "MergeWhitespace" strata which is provided by `strataparse`.
+ It converts whitespace to a `{ $: 'whitespace' }` AST node, and
+ adds a property called `$cst` to all nodes from the delegate
+ (the "lexer") as well as these whitespace nodes. This effectively
+ transforms the AST nodes from before into CST nodes, providing
+ information about whitespace, line numbers, and column numbers
+ in a way subsequent layers can digest.
+ (note that these will still be referred to as "AST nodes throughout
+ this documentation).
+
+[source code](../src/ansi-shell/parsing/buildParserSecondHalf.js)
+
+### buildParserSecondHalf (the "parser half")
+- "ReducePrimitives" creates higher-level AST nodes from some of the
+ AST nodes provided by the "previous"(lower/"lexer half") step.
+ At the time of writing it's specifically just to deal with strings,
+ reducing multiple `{ $: 'string.segment' }` and `{ $: 'string.escape }`
+ nodes into a `{ $: 'string' }` node.
+- "ShellConstructs" creates higher-level nodes to model the behaviour
+ of the shell. For example, a sequence of tokens including
+ `{ $: 'op.pipe' }` nodes will be composed into a new `{ $: 'pipeline' }`
+ node. The pipeline node contains an array called `components` which
+ contains the tokens in between pipe operators.
diff --git a/packages/phoenix/doc/readme-gif.gif b/packages/phoenix/doc/readme-gif.gif
new file mode 100644
index 00000000..d70c4eeb
Binary files /dev/null and b/packages/phoenix/doc/readme-gif.gif differ
diff --git a/packages/phoenix/doc/stash/SymbolParserImpl.js b/packages/phoenix/doc/stash/SymbolParserImpl.js
new file mode 100644
index 00000000..0c6f4b2a
--- /dev/null
+++ b/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 .
+ */
+// Here for safe-keeping - wasn't correct for shell tokens but
+// it will be needed later for variable assignments
+
+export class SymbolParserImpl {
+ static meta = {
+ inputs: 'bytes',
+ outputs: 'node'
+ }
+ static data = {
+ rexp0: /[A-Za-z_]/,
+ rexpN: /[A-Za-z0-9_]/,
+ }
+ parse (lexer) {
+ let { done, value } = lexer.look();
+ if ( done ) return;
+
+ const { rexp0, rexpN } = this.constructor.data;
+
+ value = String.fromCharCode(value);
+ if ( ! rexp0.test(value) ) return;
+
+ let text = '' + value;
+ lexer.next();
+
+ for ( ;; ) {
+ ({ done, value } = lexer.look());
+ if ( done ) break;
+ value = String.fromCharCode(value);
+ if ( ! rexpN.test(value) ) break;
+ text += value;
+ lexer.next();
+ }
+
+ return { $: 'symbol', text };
+ }
+}
diff --git a/packages/phoenix/notalicense-license-checker-config.json b/packages/phoenix/notalicense-license-checker-config.json
new file mode 100644
index 00000000..b43365a4
--- /dev/null
+++ b/packages/phoenix/notalicense-license-checker-config.json
@@ -0,0 +1,26 @@
+{
+ "ignore": ["**/!(*.js|*.css)"],
+ "license": "doc/license_header.txt",
+ "licenseFormats": {
+ "js": {
+ "prepend": "/*",
+ "append": " */",
+ "eachLine": {
+ "prepend": " * "
+ }
+ },
+ "dotfile|^Dockerfile": {
+ "eachLine": {
+ "prepend": "# "
+ }
+ },
+ "css": {
+ "prepend": "/*",
+ "append": " */",
+ "eachLine": {
+ "prepend": " * "
+ }
+ }
+ },
+ "trailingWhitespace": "TRIM"
+}
\ No newline at end of file
diff --git a/packages/phoenix/package-lock.json b/packages/phoenix/package-lock.json
new file mode 100644
index 00000000..1f1d6840
--- /dev/null
+++ b/packages/phoenix/package-lock.json
@@ -0,0 +1,1888 @@
+{
+ "name": "dev-ansi-terminal",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dev-ansi-terminal",
+ "version": "0.0.0",
+ "license": "AGPL-3.0-only",
+ "workspaces": [
+ "packages/pty",
+ "packages/strataparse",
+ "packages/contextlink"
+ ],
+ "dependencies": {
+ "@pkgjs/parseargs": "^0.11.0",
+ "capture-console": "^1.0.2",
+ "chronokinesis": "^6.0.0",
+ "cli-columns": "^4.0.0",
+ "columnify": "^1.6.0",
+ "fs-mode-to-string": "^0.0.2",
+ "json-query": "^2.2.2",
+ "node-pty": "^1.0.0",
+ "path-browserify": "^1.0.1",
+ "sinon": "^17.0.1",
+ "xterm": "^5.1.0",
+ "xterm-addon-fit": "^0.7.0"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^24.1.0",
+ "@rollup/plugin-node-resolve": "^15.0.2",
+ "@rollup/plugin-replace": "^5.0.2",
+ "mocha": "^10.2.0",
+ "rollup": "^3.21.4",
+ "rollup-plugin-copy": "^3.4.0"
+ }
+ },
+ "../dev-contextlink": {
+ "name": "@heyputer/contextlink",
+ "version": "0.0.0",
+ "extraneous": true,
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "mocha": "^10.2.0"
+ }
+ },
+ "../dev-hitide": {
+ "name": "hitide",
+ "version": "0.0.0",
+ "extraneous": true,
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "mocha": "^10.2.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "dev": true
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs": {
+ "version": "24.1.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz",
+ "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.2",
+ "glob": "^8.0.3",
+ "is-reference": "1.2.1",
+ "magic-string": "^0.27.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.68.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@rollup/plugin-node-resolve": {
+ "version": "15.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.2.tgz",
+ "integrity": "sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "@types/resolve": "1.20.2",
+ "deepmerge": "^4.2.2",
+ "is-builtin-module": "^3.2.1",
+ "is-module": "^1.0.0",
+ "resolve": "^1.22.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.78.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-replace": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz",
+ "integrity": "sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "magic-string": "^0.27.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
+ "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "11.2.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
+ "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@sinonjs/samsam": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
+ "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
+ "dependencies": {
+ "@sinonjs/commons": "^2.0.0",
+ "lodash.get": "^4.4.2",
+ "type-detect": "^4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/text-encoding": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
+ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
+ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
+ "dev": true
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.2.tgz",
+ "integrity": "sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
+ "dev": true,
+ "dependencies": {
+ "@types/minimatch": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "18.16.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz",
+ "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==",
+ "dev": true
+ },
+ "node_modules/@types/resolve": {
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
+ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
+ "dev": true
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argle": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/argle/-/argle-1.1.2.tgz",
+ "integrity": "sha512-2sQZC5HeeSH9cQEwnZZhmHiKfvJkQ6ncpf8zl9Hv629aiMUsOw8jzYqOhpaMleQGzpQ7avCwrwyqSW1f4t7v0Q==",
+ "dependencies": {
+ "lodash.isfunction": "^3.0.8",
+ "lodash.isnumber": "^3.0.3"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "node_modules/builtin-modules": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/capture-console": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/capture-console/-/capture-console-1.0.2.tgz",
+ "integrity": "sha512-vQNTSFr0cmHAYXXG3KG7ZJQn0XxC3K2di/wUZVb6yII6gqSN/10Egd3vV4XqJ00yCRNHy2wkN4uWHE+rJstDrw==",
+ "dependencies": {
+ "argle": "~1.1.1",
+ "lodash.isfunction": "~3.0.8",
+ "randomstring": "^1.3.0"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chronokinesis": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/chronokinesis/-/chronokinesis-6.0.0.tgz",
+ "integrity": "sha512-NxGxNuzROLws2VVvSj9r1qrq0JK0AwR44FNk+sGfPZlG5EW3viz6z2elg6ZwE2YFCn6+Qg3sPqkfIYLyZ0wAtQ=="
+ },
+ "node_modules/cli-columns": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-columns/-/cli-columns-4.0.0.tgz",
+ "integrity": "sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ==",
+ "dependencies": {
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/colorette": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
+ "dev": true
+ },
+ "node_modules/columnify": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz",
+ "integrity": "sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==",
+ "dependencies": {
+ "strip-ansi": "^6.0.1",
+ "wcwidth": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/contextlink": {
+ "resolved": "packages/contextlink",
+ "link": true
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/debug/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dev-pty": {
+ "resolved": "packages/pty",
+ "link": true
+ },
+ "node_modules/diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.12",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+ "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs-mode-to-string": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/fs-mode-to-string/-/fs-mode-to-string-0.0.2.tgz",
+ "integrity": "sha512-8Pik0/TZnN1uuEO5TdmDoXkjTNA98BUD1uM3RWepPXDLAO9tbmiluyu+fVwWX7C4sKKxDX+64rWNwtNwDJA3Yg=="
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globby": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.1.tgz",
+ "integrity": "sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==",
+ "dev": true,
+ "dependencies": {
+ "@types/glob": "^7.1.1",
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.0.3",
+ "glob": "^7.1.3",
+ "ignore": "^5.1.1",
+ "merge2": "^1.2.3",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-builtin-module": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+ "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+ "dev": true,
+ "dependencies": {
+ "builtin-modules": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
+ "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
+ "dev": true
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
+ "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-query": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/json-query/-/json-query-2.2.2.tgz",
+ "integrity": "sha512-y+IcVZSdqNmS4fO8t1uZF6RMMs0xh3SrTjJr9bp1X3+v0Q13+7Cyv12dSmKwDswp/H427BVtpkLWhGxYu3ZWRA==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/just-extend": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
+ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw=="
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
+ },
+ "node_modules/lodash.isfunction": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
+ "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
+ "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+ "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.3",
+ "debug": "4.3.4",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.2.0",
+ "he": "1.2.0",
+ "js-yaml": "4.1.0",
+ "log-symbols": "4.1.0",
+ "minimatch": "5.0.1",
+ "ms": "2.1.3",
+ "nanoid": "3.3.3",
+ "serialize-javascript": "6.0.0",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "workerpool": "6.2.1",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mochajs"
+ }
+ },
+ "node_modules/mocha/node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mocha/node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+ "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/nan": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
+ "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+ "dev": true,
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nise": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz",
+ "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0",
+ "@sinonjs/fake-timers": "^11.2.2",
+ "@sinonjs/text-encoding": "^0.7.2",
+ "just-extend": "^6.2.0",
+ "path-to-regexp": "^6.2.1"
+ }
+ },
+ "node_modules/node-pty": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
+ "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "nan": "^2.17.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-to-regexp": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+ "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/randomstring": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.3.0.tgz",
+ "integrity": "sha512-gY7aQ4i1BgwZ8I1Op4YseITAyiDiajeZOPQUbIq9TPGPhUm5FX59izIaOpmKbME1nmnEiABf28d9K2VSii6BBg==",
+ "dependencies": {
+ "randombytes": "2.0.3"
+ },
+ "bin": {
+ "randomstring": "bin/randomstring"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/randomstring/node_modules/randombytes": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.3.tgz",
+ "integrity": "sha512-lDVjxQQFoCG1jcrP06LNo2lbWp4QTShEXnhActFBwYuHprllQV6VUpwreApsYqCgD+N1mHoqJ/BI/4eV4R2GYg=="
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.2",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
+ "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.11.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.4.tgz",
+ "integrity": "sha512-N5LxpvDolOm9ueiCp4NfB80omMDqb45ShtsQw2+OT3f11uJ197dv703NZvznYHP6RWR85wfxanXurXKG3ux2GQ==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup-plugin-copy": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz",
+ "integrity": "sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/fs-extra": "^8.0.1",
+ "colorette": "^1.1.0",
+ "fs-extra": "^8.1.0",
+ "globby": "10.0.1",
+ "is-plain-object": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.3"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+ "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/sinon": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
+ "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0",
+ "@sinonjs/fake-timers": "^11.2.2",
+ "@sinonjs/samsam": "^8.0.0",
+ "diff": "^5.1.0",
+ "nise": "^5.1.5",
+ "supports-color": "^7.2.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sinon"
+ }
+ },
+ "node_modules/sinon/node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/sinon/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strataparse": {
+ "resolved": "packages/strataparse",
+ "link": true
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+ "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/xterm": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.1.0.tgz",
+ "integrity": "sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ=="
+ },
+ "node_modules/xterm-addon-fit": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz",
+ "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==",
+ "peerDependencies": {
+ "xterm": "^5.0.0"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/contextlink": {
+ "version": "0.0.0",
+ "license": "AGPL-3.0-only",
+ "devDependencies": {
+ "mocha": "^10.2.0"
+ }
+ },
+ "packages/pty": {
+ "name": "dev-pty",
+ "version": "0.0.0",
+ "license": "AGPL-3.0-only"
+ },
+ "packages/strataparse": {
+ "version": "0.0.0",
+ "license": "AGPL-3.0-only"
+ }
+ }
+}
diff --git a/packages/phoenix/package.json b/packages/phoenix/package.json
new file mode 100644
index 00000000..de2ea901
--- /dev/null
+++ b/packages/phoenix/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "dev-ansi-terminal",
+ "version": "0.0.0",
+ "description": "ANSI Terminal for Puter",
+ "main": "exports.js",
+ "scripts": {
+ "test": "mocha ./test"
+ },
+ "author": "Puter Technologies Inc.",
+ "license": "AGPL-3.0-only",
+ "type": "module",
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^24.1.0",
+ "@rollup/plugin-node-resolve": "^15.0.2",
+ "@rollup/plugin-replace": "^5.0.2",
+ "mocha": "^10.2.0",
+ "rollup": "^3.21.4",
+ "rollup-plugin-copy": "^3.4.0"
+ },
+ "dependencies": {
+ "@pkgjs/parseargs": "^0.11.0",
+ "capture-console": "^1.0.2",
+ "chronokinesis": "^6.0.0",
+ "cli-columns": "^4.0.0",
+ "columnify": "^1.6.0",
+ "fs-mode-to-string": "^0.0.2",
+ "json-query": "^2.2.2",
+ "node-pty": "^1.0.0",
+ "path-browserify": "^1.0.1",
+ "sinon": "^17.0.1",
+ "xterm": "^5.1.0",
+ "xterm-addon-fit": "^0.7.0"
+ },
+ "workspaces": [
+ "packages/pty",
+ "packages/strataparse",
+ "packages/contextlink"
+ ]
+}
diff --git a/packages/phoenix/packages/contextlink/.gitignore b/packages/phoenix/packages/contextlink/.gitignore
new file mode 100644
index 00000000..c2658d7d
--- /dev/null
+++ b/packages/phoenix/packages/contextlink/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/packages/phoenix/packages/contextlink/context.js b/packages/phoenix/packages/contextlink/context.js
new file mode 100644
index 00000000..6cbf3149
--- /dev/null
+++ b/packages/phoenix/packages/contextlink/context.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class Context {
+ constructor (values) {
+ for ( const k in values ) this[k] = values[k];
+ }
+ sub (newValues) {
+ if ( newValues === undefined ) newValues = {};
+ const sub = Object.create(this);
+
+ const alreadyApplied = {};
+ for ( const k in sub ) {
+ if ( sub[k] instanceof Context ) {
+ const newValuesForK =
+ newValues.hasOwnProperty(k)
+ ? newValues[k] : undefined;
+ sub[k] = sub[k].sub(newValuesForK);
+ alreadyApplied[k] = true;
+ }
+ }
+
+ for ( const k in newValues ) {
+ if ( alreadyApplied[k] ) continue;
+ sub[k] = newValues[k];
+ }
+
+ return sub;
+ }
+}
diff --git a/packages/phoenix/packages/contextlink/entry.js b/packages/phoenix/packages/contextlink/entry.js
new file mode 100644
index 00000000..1103462a
--- /dev/null
+++ b/packages/phoenix/packages/contextlink/entry.js
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export { Context } from "./context.js";
diff --git a/packages/phoenix/packages/contextlink/package-lock.json b/packages/phoenix/packages/contextlink/package-lock.json
new file mode 100644
index 00000000..a6aca0eb
--- /dev/null
+++ b/packages/phoenix/packages/contextlink/package-lock.json
@@ -0,0 +1,916 @@
+{
+ "name": "dev-contextlink",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dev-contextlink",
+ "version": "0.0.0",
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "mocha": "^10.2.0"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/debug/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+ "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+ "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.3",
+ "debug": "4.3.4",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.2.0",
+ "he": "1.2.0",
+ "js-yaml": "4.1.0",
+ "log-symbols": "4.1.0",
+ "minimatch": "5.0.1",
+ "ms": "2.1.3",
+ "nanoid": "3.3.3",
+ "serialize-javascript": "6.0.0",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "workerpool": "6.2.1",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mochajs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+ "dev": true,
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+ "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+ "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/packages/phoenix/packages/contextlink/package.json b/packages/phoenix/packages/contextlink/package.json
new file mode 100644
index 00000000..6d27795d
--- /dev/null
+++ b/packages/phoenix/packages/contextlink/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "contextlink",
+ "version": "0.0.0",
+ "main": "entry.js",
+ "type": "module",
+ "scripts": {
+ "test": "npx mocha"
+ },
+ "author": "Puter Technologies Inc.",
+ "license": "AGPL-3.0-only",
+ "devDependencies": {
+ "mocha": "^10.2.0"
+ },
+ "directories": {
+ "test": "test"
+ },
+ "description": ""
+}
diff --git a/packages/phoenix/packages/contextlink/test/testcontext.js b/packages/phoenix/packages/contextlink/test/testcontext.js
new file mode 100644
index 00000000..040b4fd4
--- /dev/null
+++ b/packages/phoenix/packages/contextlink/test/testcontext.js
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { Context } from "../context.js";
+
+describe('context', () => {
+ it ('works', () => {
+ const ctx = new Context({ a: 1 });
+ const subCtx = ctx.sub({ b: 2 });
+
+ assert.equal(ctx.a, 1);
+ assert.equal(ctx.b, undefined);
+ assert.equal(subCtx.a, 1);
+ assert.equal(subCtx.b, 2);
+ }),
+ it ('doesn\'t mangle inner-contexts', () => {
+ const ctx = new Context({
+ plainObject: { a: 1, b: 2, c: 3 },
+ contextObject: new Context({ i: 4, j: 5, k: 6 }),
+ });
+ const subCtx = ctx.sub({
+ plainObject: { a: 101 },
+ contextObject: { i: 104 },
+ });
+ assert.equal(subCtx.plainObject.a, 101);
+ assert.equal(subCtx.plainObject.b, undefined);
+
+ assert.equal(subCtx.contextObject.i, 104);
+ assert.equal(subCtx.contextObject.j, 5);
+
+ })
+});
diff --git a/packages/phoenix/packages/newparser/exports.js b/packages/phoenix/packages/newparser/exports.js
new file mode 100644
index 00000000..dfab2185
--- /dev/null
+++ b/packages/phoenix/packages/newparser/exports.js
@@ -0,0 +1,101 @@
+import { adapt_parser, INVALID, Parser, UNRECOGNIZED, VALUE } from './lib.js';
+import { Discard, FirstMatch, None, Optional, Repeat, Sequence } from './parsers/combinators.js';
+import { Literal, StringOf } from './parsers/terminals.js';
+
+class Symbol extends Parser {
+ _create(symbolName) {
+ this.symbolName = symbolName;
+ }
+
+ _parse (stream) {
+ const parser = this.symbol_registry[this.symbolName];
+ if ( ! parser ) {
+ throw new Error(`No symbol defined named '${this.symbolName}'`);
+ }
+ const subStream = stream.fork();
+ const result = parser.parse(subStream);
+ console.log(`Result of parsing symbol('${this.symbolName}'):`, result);
+ if ( result.status === UNRECOGNIZED ) {
+ return UNRECOGNIZED;
+ }
+ if ( result.status === INVALID ) {
+ return { status: INVALID, value: result };
+ }
+ stream.join(subStream);
+ result.$ = this.symbolName;
+ return result;
+ }
+}
+
+class ParserWithAction {
+ #parser;
+ #action;
+
+ constructor(parser, action) {
+ this.#parser = adapt_parser(parser);
+ this.#action = action;
+ }
+
+ parse (stream) {
+ const parsed = this.#parser.parse(stream);
+ if (parsed.status === VALUE) {
+ parsed.value = this.#action(parsed.value);
+ }
+ return parsed;
+ }
+}
+
+export class GrammarContext {
+ constructor (parsers) {
+ // Object of { parser_name: Parser, ... }
+ this.parsers = parsers;
+ }
+
+ sub (more_parsers) {
+ return new GrammarContext({...this.parsers, ...more_parsers});
+ }
+
+ define_parser (grammar, actions) {
+ const symbol_registry = {};
+ const api = {};
+
+ for (const [name, parserCls] of Object.entries(this.parsers)) {
+ api[name] = (...params) => {
+ const result = new parserCls();
+ result._create(...params);
+ result.set_symbol_registry(symbol_registry);
+ return result;
+ };
+ }
+
+ for (const [name, builder] of Object.entries(grammar)) {
+ if (actions[name]) {
+ symbol_registry[name] = new ParserWithAction(builder(api), actions[name]);
+ } else {
+ symbol_registry[name] = builder(api);
+ }
+ }
+
+ return (stream, entry_symbol) => {
+ const entry_parser = symbol_registry[entry_symbol];
+ if (!entry_parser) {
+ throw new Error(`Entry symbol '${entry_symbol}' not found in grammar.`);
+ }
+ return entry_parser.parse(stream);
+ };
+ }
+}
+
+export const standard_parsers = () => {
+ return {
+ discard: Discard,
+ firstMatch: FirstMatch,
+ literal: Literal,
+ none: None,
+ optional: Optional,
+ repeat: Repeat,
+ sequence: Sequence,
+ stringOf: StringOf,
+ symbol: Symbol,
+ }
+}
diff --git a/packages/phoenix/packages/newparser/lib.js b/packages/phoenix/packages/newparser/lib.js
new file mode 100644
index 00000000..89f185e0
--- /dev/null
+++ b/packages/phoenix/packages/newparser/lib.js
@@ -0,0 +1,29 @@
+export const adapt_parser = v => v;
+
+export const UNRECOGNIZED = Symbol('unrecognized');
+export const INVALID = Symbol('invalid');
+export const VALUE = Symbol('value');
+
+export class Parser {
+ result (o) {
+ if (o.value && o.value.$discard) {
+ delete o.value;
+ }
+ return o;
+ }
+
+ parse (stream) {
+ let result = this._parse(stream);
+ if ( typeof result !== 'object' ) {
+ result = { status: result };
+ }
+ return this.result(result);
+ }
+
+ set_symbol_registry (symbol_registry) {
+ this.symbol_registry = symbol_registry;
+ }
+
+ _create () { throw new Error(`${this.constructor.name}._create() not implemented`); }
+ _parse (stream) { throw new Error(`${this.constructor.name}._parse() not implemented`); }
+}
diff --git a/packages/phoenix/packages/newparser/parsers/combinators.js b/packages/phoenix/packages/newparser/parsers/combinators.js
new file mode 100644
index 00000000..54b23c05
--- /dev/null
+++ b/packages/phoenix/packages/newparser/parsers/combinators.js
@@ -0,0 +1,139 @@
+import { INVALID, UNRECOGNIZED, VALUE, adapt_parser, Parser } from '../lib.js';
+
+export class Discard extends Parser {
+ _create (parser) {
+ this.parser = adapt_parser(parser);
+ }
+
+ _parse (stream) {
+ const subStream = stream.fork();
+ const result = this.parser.parse(subStream);
+ if ( result.status === UNRECOGNIZED ) {
+ return UNRECOGNIZED;
+ }
+ if ( result.status === INVALID ) {
+ return result;
+ }
+ stream.join(subStream);
+ return { status: VALUE, $: 'none', $discard: true, value: result };
+ }
+}
+
+export class FirstMatch extends Parser {
+ _create (...parsers) {
+ this.parsers = parsers.map(adapt_parser);
+ }
+
+ _parse (stream) {
+ for ( const parser of this.parsers ) {
+ const subStream = stream.fork();
+ const result = parser.parse(subStream);
+ if ( result.status === UNRECOGNIZED ) {
+ continue;
+ }
+ if ( result.status === INVALID ) {
+ return result;
+ }
+ stream.join(subStream);
+ return result;
+ }
+
+ return UNRECOGNIZED;
+ }
+}
+
+export class None extends Parser {
+ _create () {}
+
+ _parse (stream) {
+ return { status: VALUE, $: 'none', $discard: true };
+ }
+}
+
+export class Optional extends Parser {
+ _create (parser) {
+ this.parser = adapt_parser(parser);
+ }
+
+ _parse (stream) {
+ const subStream = stream.fork();
+ const result = this.parser.parse(subStream);
+ if ( result.status === VALUE ) {
+ stream.join(subStream);
+ return result;
+ }
+ return { status: VALUE, $: 'none', $discard: true };
+ }
+}
+
+export class Repeat extends Parser {
+ _create (value_parser, separator_parser, { trailing = false } = {}) {
+ this.value_parser = adapt_parser(value_parser);
+ this.separator_parser = adapt_parser(separator_parser);
+ this.trailing = trailing;
+ }
+
+ _parse (stream) {
+ const results = [];
+ for ( ;; ) {
+ const subStream = stream.fork();
+
+ // Value
+ const result = this.value_parser.parse(subStream);
+ if ( result.status === UNRECOGNIZED ) {
+ break;
+ }
+ if ( result.status === INVALID ) {
+ return { status: INVALID, value: result };
+ }
+ stream.join(subStream);
+ if ( ! result.$discard ) results.push(result);
+
+ // Separator
+ if ( ! this.separator_parser ) {
+ continue;
+ }
+ const separatorResult = this.separator_parser.parse(subStream);
+ if ( separatorResult.status === UNRECOGNIZED ) {
+ break;
+ }
+ if ( separatorResult.status === INVALID ) {
+ return { status: INVALID, value: separatorResult };
+ }
+ stream.join(subStream);
+ if ( ! result.$discard ) results.push(separatorResult);
+
+ // TODO: Detect trailing separator and reject it if trailing==false
+ }
+
+ if ( results.length === 0 ) {
+ return UNRECOGNIZED;
+ }
+
+ return { status: VALUE, value: results };
+ }
+}
+
+export class Sequence extends Parser {
+ _create (...parsers) {
+ this.parsers = parsers.map(adapt_parser);
+ }
+
+ _parse (stream) {
+ const results = [];
+ for ( const parser of this.parsers ) {
+ const subStream = stream.fork();
+ const result = parser.parse(subStream);
+ if ( result.status === UNRECOGNIZED ) {
+ return UNRECOGNIZED;
+ }
+ if ( result.status === INVALID ) {
+ return { status: INVALID, value: result };
+ }
+ stream.join(subStream);
+ if ( ! result.$discard ) results.push(result);
+ }
+
+ return { status: VALUE, value: results };
+ }
+}
diff --git a/packages/phoenix/packages/newparser/parsers/terminals.js b/packages/phoenix/packages/newparser/parsers/terminals.js
new file mode 100644
index 00000000..4a82cd14
--- /dev/null
+++ b/packages/phoenix/packages/newparser/parsers/terminals.js
@@ -0,0 +1,46 @@
+import { Parser, UNRECOGNIZED, VALUE } from '../lib.js';
+
+export class Literal extends Parser {
+ _create (value) {
+ this.value = value;
+ }
+
+ _parse (stream) {
+ const subStream = stream.fork();
+ for ( let i=0 ; i < this.value.length ; i++ ) {
+ let { done, value } = subStream.next();
+ if ( done ) return UNRECOGNIZED;
+ if ( this.value[i] !== value ) return UNRECOGNIZED;
+ }
+
+ stream.join(subStream);
+ return { status: VALUE, $: 'literal', value: this.value };
+ }
+}
+
+export class StringOf extends Parser {
+ _create (values) {
+ this.values = values;
+ }
+
+ _parse (stream) {
+ const subStream = stream.fork();
+ let text = '';
+
+ while (true) {
+ let { done, value } = subStream.look();
+ if ( done ) break;
+ if ( ! this.values.includes(value) ) break;
+
+ subStream.next();
+ text += value;
+ }
+
+ if (text.length === 0) {
+ return UNRECOGNIZED;
+ }
+
+ stream.join(subStream);
+ return { status: VALUE, $: 'stringOf', value: text };
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/packages/pty/exports.js b/packages/phoenix/packages/pty/exports.js
new file mode 100644
index 00000000..bbab56ef
--- /dev/null
+++ b/packages/phoenix/packages/pty/exports.js
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const encoder = new TextEncoder();
+
+const CHAR_LF = '\n'.charCodeAt(0);
+const CHAR_CR = '\r'.charCodeAt(0);
+
+export class BetterReader {
+ constructor ({ delegate }) {
+ this.delegate = delegate;
+ this.chunks_ = [];
+ }
+
+ async read (opt_buffer) {
+ if ( ! opt_buffer && this.chunks_.length === 0 ) {
+ return await this.delegate.read();
+ }
+
+ const chunk = await this.getChunk_();
+
+ if ( ! opt_buffer ) {
+ return chunk;
+ }
+
+ this.chunks_.push(chunk);
+
+ while ( this.getTotalBytesReady_() < opt_buffer.length ) {
+ this.chunks_.push(await this.getChunk_())
+ }
+
+ // TODO: need to handle EOT condition in this loop
+ let offset = 0;
+ for (;;) {
+ let item = this.chunks_.shift();
+ if ( item === undefined ) {
+ throw new Error('calculation is wrong')
+ }
+ if ( offset + item.length > opt_buffer.length ) {
+ const diff = opt_buffer.length - offset;
+ this.chunks_.unshift(item.subarray(diff));
+ item = item.subarray(0, diff);
+ }
+ opt_buffer.set(item, offset);
+ offset += item.length;
+
+ if ( offset == opt_buffer.length ) break;
+ }
+
+ // return opt_buffer.length;
+ }
+
+ async getChunk_() {
+ if ( this.chunks_.length === 0 ) {
+ const { value } = await this.delegate.read();
+ return value;
+ }
+
+ const len = this.getTotalBytesReady_();
+ const merged = new Uint8Array(len);
+ let offset = 0;
+ for ( const item of this.chunks_ ) {
+ merged.set(item, offset);
+ offset += item.length;
+ }
+
+ this.chunks_ = [];
+
+ return merged;
+ }
+
+ getTotalBytesReady_ () {
+ return this.chunks_.reduce((sum, chunk) => sum + chunk.length, 0);
+ }
+}
+
+/**
+ * PTT: pseudo-terminal target; called "slave" in POSIX
+ */
+export class PTT {
+ constructor(pty) {
+ this.readableStream = new ReadableStream({
+ start: controller => {
+ this.readController = controller;
+ }
+ });
+ this.writableStream = new WritableStream({
+ start: controller => {
+ this.writeController = controller;
+ },
+ write: chunk => {
+ if (typeof chunk === 'string') {
+ chunk = encoder.encode(chunk);
+ }
+ if ( pty.outputModeflags?.outputNLCR ) {
+ chunk = pty.LF_to_CRLF(chunk);
+ }
+ pty.readController.enqueue(chunk);
+ }
+ });
+ this.out = this.writableStream.getWriter();
+ this.in = this.readableStream.getReader();
+ }
+}
+
+/**
+ * PTY: pseudo-terminal
+ *
+ * This implements the PTY device driver.
+ */
+export class PTY {
+ constructor () {
+ this.outputModeflags = {
+ outputNLCR: true
+ };
+ this.readableStream = new ReadableStream({
+ start: controller => {
+ this.readController = controller;
+ }
+ });
+ this.writableStream = new WritableStream({
+ start: controller => {
+ this.writeController = controller;
+ },
+ write: chunk => {
+ if ( typeof chunk === 'string' ) {
+ chunk = encoder.encode(chunk);
+ }
+ for ( const target of this.targets ) {
+ target.readController.enqueue(chunk);
+ }
+ }
+ });
+ this.out = this.writableStream.getWriter();
+ this.in = this.readableStream.getReader();
+ this.targets = [];
+ }
+
+ getPTT () {
+ const target = new PTT(this);
+ this.targets.push(target);
+ return target;
+ }
+
+ LF_to_CRLF (input) {
+ let lfCount = 0;
+ for (let i = 0; i < input.length; i++) {
+ if (input[i] === 0x0A) {
+ lfCount++;
+ }
+ }
+
+ const output = new Uint8Array(input.length + lfCount);
+
+ let outputIndex = 0;
+ for (let i = 0; i < input.length; i++) {
+ // If LF is encountered, insert CR (0x0D) before LF (0x0A)
+ if (input[i] === 0x0A) {
+ output[outputIndex++] = 0x0D;
+ }
+ output[outputIndex++] = input[i];
+ }
+
+ return output;
+ }
+}
diff --git a/packages/phoenix/packages/pty/package.json b/packages/phoenix/packages/pty/package.json
new file mode 100644
index 00000000..cc5352b8
--- /dev/null
+++ b/packages/phoenix/packages/pty/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "dev-pty",
+ "version": "0.0.0",
+ "description": "",
+ "main": "exports.js",
+ "type": "module",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Puter Technologies Inc.",
+ "license": "AGPL-3.0-only"
+}
diff --git a/packages/phoenix/packages/strataparse/dsl/ParserBuilder.js b/packages/phoenix/packages/strataparse/dsl/ParserBuilder.js
new file mode 100644
index 00000000..0936f973
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/dsl/ParserBuilder.js
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { SingleParserFactory } from "../parse.js";
+
+export class ParserConfigDSL extends SingleParserFactory {
+ constructor (parserFactory, cls) {
+ super();
+ this.parserFactory = parserFactory;
+ this.cls_ = cls;
+ this.parseParams_ = {};
+ this.grammarParams_ = {
+ assign: {},
+ };
+ }
+
+ parseParams (obj) {
+ Object.assign(this.parseParams_, obj);
+ return this;
+ }
+
+ assign (obj) {
+ Object.assign(this.grammarParams_.assign, obj);
+ return this;
+ }
+
+ create () {
+ return this.parserFactory.create(
+ this.cls_, this.parseParams_, this.grammarParams_,
+ );
+ }
+}
+
+export class ParserBuilder {
+ constructor ({
+ parserFactory,
+ parserRegistry,
+ }) {
+ this.parserFactory = parserFactory;
+ this.parserRegistry = parserRegistry;
+ this.parserAPI_ = null;
+ }
+
+ get parserAPI () {
+ if ( this.parserAPI_ ) return this.parserAPI_;
+
+ const parserAPI = {};
+
+ const parsers = this.parserRegistry.parsers;
+ for ( const parserId in parsers ) {
+ const parserCls = parsers[parserId];
+ parserAPI[parserId] =
+ this.createParserFunction(parserCls);
+ }
+
+ return this.parserAPI_ = parserAPI;
+ }
+
+ createParserFunction (parserCls) {
+ if ( parserCls.hasOwnProperty('createFunction') ) {
+ return parserCls.createFunction({
+ parserFactory: this.parserFactory
+ });
+ }
+
+ return params => {
+ const configDSL = new ParserConfigDSL(parserCls)
+ configDSL.parseParams(params);
+ return configDSL;
+ };
+ }
+
+ def (def) {
+ const a = this.parserAPI;
+ return def(a);
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/packages/strataparse/dsl/ParserRegistry.js b/packages/phoenix/packages/strataparse/dsl/ParserRegistry.js
new file mode 100644
index 00000000..87e5f7b4
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/dsl/ParserRegistry.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class ParserRegistry {
+ constructor () {
+ this.parsers_ = {};
+ }
+ register (id, parser) {
+ this.parsers_[id] = parser;
+ }
+ get parsers () {
+ return this.parsers_;
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/exports.js b/packages/phoenix/packages/strataparse/exports.js
new file mode 100644
index 00000000..928f2902
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/exports.js
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ParserRegistry } from './dsl/ParserRegistry.js';
+import { PStratum } from './strata.js';
+
+export {
+ Parser,
+ ParseResult,
+ ParserFactory,
+} from './parse.js';
+
+import WhitespaceParserImpl from './parse_impls/whitespace.js';
+import LiteralParserImpl from './parse_impls/literal.js';
+import StrUntilParserImpl from './parse_impls/StrUntilParserImpl.js';
+
+import {
+ SequenceParserImpl,
+ ChoiceParserImpl,
+ RepeatParserImpl,
+ NoneParserImpl,
+} from './parse_impls/combinators.js';
+
+export {
+ WhitespaceParserImpl,
+ LiteralParserImpl,
+ SequenceParserImpl,
+ ChoiceParserImpl,
+ RepeatParserImpl,
+ StrUntilParserImpl,
+}
+
+export {
+ PStratum,
+ TerminalPStratumImplType,
+ DelegatingPStratumImplType,
+} from './strata.js';
+
+export {
+ BytesPStratumImpl,
+ StringPStratumImpl
+} from './strata_impls/terminals.js';
+
+export {
+ default as FirstRecognizedPStratumImpl,
+} from './strata_impls/FirstRecognizedPStratumImpl.js';
+
+export {
+ default as ContextSwitchingPStratumImpl,
+} from './strata_impls/ContextSwitchingPStratumImpl.js';
+
+export { ParserBuilder } from './dsl/ParserBuilder.js';
+
+export class StrataParseFacade {
+ static getDefaultParserRegistry() {
+ const r = new ParserRegistry();
+ r.register('sequence', SequenceParserImpl);
+ r.register('choice', ChoiceParserImpl);
+ r.register('repeat', RepeatParserImpl);
+ r.register('literal', LiteralParserImpl);
+ r.register('none', NoneParserImpl);
+
+ return r;
+ }
+}
+
+export class StrataParser {
+ constructor () {
+ this.strata = [];
+ this.error = null;
+ }
+ add (stratum) {
+ if ( ! ( stratum instanceof PStratum ) ) {
+ stratum = new PStratum(stratum);
+ }
+
+ // TODO: verify that terminals don't delegate
+ // TODO: verify the delegating strata delegate
+ if ( this.strata.length > 0 ) {
+ const delegate = this.strata[this.strata.length - 1];
+ stratum.setDelegate(delegate);
+ }
+
+ this.strata.push(stratum);
+ }
+ next () {
+ return this.strata[this.strata.length - 1].next();
+ }
+ parse () {
+ let done, value;
+ const result = [];
+ for ( ;; ) {
+ ({ done, value } =
+ this.strata[this.strata.length - 1].next());
+ if ( done ) break
+ result.push(value);
+ }
+ if ( value ) {
+ this.error = value;
+ }
+ return result;
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/package.json b/packages/phoenix/packages/strataparse/package.json
new file mode 100644
index 00000000..794dc239
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "strataparse",
+ "version": "0.0.0",
+ "description": "",
+ "main": "exports.js",
+ "type": "module",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Puter Technologies Inc.",
+ "license": "AGPL-3.0-only"
+}
+
diff --git a/packages/phoenix/packages/strataparse/parse.js b/packages/phoenix/packages/strataparse/parse.js
new file mode 100644
index 00000000..14663335
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/parse.js
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class Parser {
+ constructor ({
+ impl,
+ assign,
+ }) {
+ this.impl = impl;
+ this.assign = assign ?? {};
+ }
+ parse (lexer) {
+ const unadaptedResult = this.impl.parse(lexer);
+ const pr = unadaptedResult instanceof ParseResult
+ ? unadaptedResult : new ParseResult(unadaptedResult);
+ if ( pr.status === ParseResult.VALUE ) {
+ pr.value = {
+ ...pr.value,
+ ...this.assign,
+ };
+ }
+ return pr;
+ }
+}
+
+export class ParseResult {
+ static UNRECOGNIZED = { name: 'unrecognized' };
+ static VALUE = { name: 'value' };
+ static INVALID = { name: 'invalid' };
+ constructor (value, opt_status) {
+ if (
+ value === ParseResult.UNRECOGNIZED ||
+ value === ParseResult.INVALID
+ ) {
+ this.status = value;
+ return;
+ }
+ this.status = opt_status ?? (
+ value === undefined
+ ? ParseResult.UNRECOGNIZED
+ : ParseResult.VALUE
+ );
+ this.value = value;
+ }
+}
+
+class ConcreteSyntaxParserDecorator {
+ constructor (delegate) {
+ this.delegate = delegate;
+ }
+ parse (lexer, ...a) {
+ const start = lexer.seqNo;
+ const result = this.delegate.parse(lexer, ...a);
+ if ( result.status === ParseResult.VALUE ) {
+ const end = lexer.seqNo;
+ result.value.$cst = { start, end };
+ }
+ return result;
+ }
+}
+
+class RememberSourceParserDecorator {
+ constructor (delegate) {
+ this.delegate = delegate;
+ }
+ parse (lexer, ...a) {
+ const start = lexer.seqNo;
+ const result = this.delegate.parse(lexer, ...a);
+ if ( result.status === ParseResult.VALUE ) {
+ const end = lexer.seqNo;
+ result.value.$source = lexer.reach(start, end);
+ }
+ return result;
+ }
+}
+
+export class ParserFactory {
+ constructor () {
+ this.concrete = false;
+ this.rememberSource = false;
+ }
+ decorate (obj) {
+ if ( this.concrete ) {
+ obj = new ConcreteSyntaxParserDecorator(obj);
+ }
+ if ( this.rememberSource ) {
+ obj = new RememberSourceParserDecorator(obj);
+ }
+
+ return obj;
+ }
+ create (cls, parserParams, resultParams) {
+ parserParams = parserParams ?? {};
+
+ resultParams = resultParams ?? {};
+ resultParams.assign = resultParams.assign ?? {};
+
+ const impl = new cls(parserParams);
+ const parser = new Parser({
+ impl,
+ assign: resultParams.assign
+ });
+
+ // return parser;
+ return this.decorate(parser);
+ }
+}
+
+export class SingleParserFactory {
+ create () {
+ throw new Error('abstract create() must be implemented');
+ }
+}
+
+export class AcceptParserUtil {
+ static adapt (parser) {
+ if ( parser === undefined ) return undefined;
+ if ( parser instanceof SingleParserFactory ) {
+ parser = parser.create();
+ }
+ if ( ! (parser instanceof Parser) ) {
+ parser = new Parser({ impl: parser });
+ }
+ return parser;
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js b/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js
new file mode 100644
index 00000000..4ca5b81a
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default class StrUntilParserImpl {
+ constructor ({ stopChars }) {
+ this.stopChars = stopChars;
+ }
+ parse (lexer) {
+ let text = '';
+ for ( ;; ) {
+ console.log('B')
+ let { done, value } = lexer.look();
+
+ if ( done ) break;
+
+ // TODO: doing this strictly one byte at a time
+ // doesn't allow multi-byte stop characters
+ if ( typeof value === 'number' ) value =
+ String.fromCharCode(value);
+
+ if ( this.stopChars.includes(value) ) break;
+
+ text += value;
+ lexer.next();
+ }
+
+ if ( text.length === 0 ) return;
+
+ console.log('test?', text)
+
+ return { $: 'until', text };
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/parse_impls/combinators.js b/packages/phoenix/packages/strataparse/parse_impls/combinators.js
new file mode 100644
index 00000000..6d6b5697
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/parse_impls/combinators.js
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ParserConfigDSL } from "../dsl/ParserBuilder.js";
+import { AcceptParserUtil, Parser, ParseResult } from "../parse.js";
+
+export class SequenceParserImpl {
+ static createFunction ({ parserFactory }) {
+ return (...parsers) => {
+ const conf = new ParserConfigDSL(parserFactory, this);
+ conf.parseParams({ parsers });
+ return conf;
+ };
+ }
+ constructor ({ parsers }) {
+ this.parsers = parsers.map(AcceptParserUtil.adapt);
+ }
+ parse (lexer) {
+ const results = [];
+ for ( const parser of this.parsers ) {
+ const subLexer = lexer.fork();
+ const result = parser.parse(subLexer);
+ if ( result.status === ParseResult.UNRECOGNIZED ) {
+ return;
+ }
+ if ( result.status === ParseResult.INVALID ) {
+ // TODO: this is wrong
+ return { done: true, value: result };
+ }
+ lexer.join(subLexer);
+ results.push(result.value);
+ }
+
+ return { $: 'sequence', results };
+ }
+}
+
+export class ChoiceParserImpl {
+ static createFunction ({ parserFactory }) {
+ return (...parsers) => {
+ const conf = new ParserConfigDSL(parserFactory, this);
+ conf.parseParams({ parsers });
+ return conf;
+ };
+ }
+ constructor ({ parsers }) {
+ this.parsers = parsers.map(AcceptParserUtil.adapt);
+ }
+ parse (lexer) {
+ for ( const parser of this.parsers ) {
+ const subLexer = lexer.fork();
+ const result = parser.parse(subLexer);
+ if ( result.status === ParseResult.UNRECOGNIZED ) {
+ continue;
+ }
+ if ( result.status === ParseResult.INVALID ) {
+ // TODO: this is wrong
+ return { done: true, value: result };
+ }
+ lexer.join(subLexer);
+ return result.value;
+ }
+
+ return;
+ }
+}
+
+export class RepeatParserImpl {
+ static createFunction ({ parserFactory }) {
+ return (delegate) => {
+ const conf = new ParserConfigDSL(parserFactory, this);
+ conf.parseParams({ delegate });
+ return conf;
+ };
+ }
+ constructor ({ delegate }) {
+ delegate = AcceptParserUtil.adapt(delegate);
+ this.delegate = delegate;
+ }
+
+ parse (lexer) {
+ const results = [];
+ for ( ;; ) {
+ const subLexer = lexer.fork();
+ const result = this.delegate.parse(subLexer);
+ if ( result.status === ParseResult.UNRECOGNIZED ) {
+ break;
+ }
+ if ( result.status === ParseResult.INVALID ) {
+ return { done: true, value: result };
+ }
+ lexer.join(subLexer);
+ results.push(result.value);
+ }
+
+ return { $: 'repeat', results };
+ }
+}
+
+export class NoneParserImpl {
+ static createFunction ({ parserFactory }) {
+ return () => {
+ const conf = new ParserConfigDSL(parserFactory, this);
+ return conf;
+ };
+ }
+ parse () {
+ return { $: 'none', $discard: true };
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/parse_impls/literal.js b/packages/phoenix/packages/strataparse/parse_impls/literal.js
new file mode 100644
index 00000000..4b6319cf
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/parse_impls/literal.js
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ParserConfigDSL } from "../dsl/ParserBuilder.js";
+
+const encoder = new TextEncoder();
+const decoder = new TextDecoder();
+
+export default class LiteralParserImpl {
+ static meta = {
+ inputs: 'bytes',
+ outputs: 'node'
+ }
+ static createFunction ({ parserFactory }) {
+ return (value) => {
+ const conf = new ParserConfigDSL(parserFactory, this);
+ conf.parseParams({ value });
+ return conf;
+ };
+ }
+ constructor ({ value }) {
+ // adapt value
+ if ( typeof value === 'string' ) {
+ value = encoder.encode(value);
+ }
+
+ if ( value.length === 0 ) {
+ throw new Error(
+ 'tried to construct a LiteralParser with an ' +
+ 'empty value, which could cause infinite ' +
+ 'iteration'
+ );
+ }
+
+ this.value = value;
+ }
+ parse (lexer) {
+ for ( let i=0 ; i < this.value.length ; i++ ) {
+ let { done, value } = lexer.next();
+ if ( done ) return;
+ if ( this.value[i] !== value ) return;
+ }
+
+ const text = decoder.decode(this.value);
+ return { $: 'literal', text };
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/parse_impls/whitespace.js b/packages/phoenix/packages/strataparse/parse_impls/whitespace.js
new file mode 100644
index 00000000..82bcb1cb
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/parse_impls/whitespace.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default class WhitespaceParserImpl {
+ static meta = {
+ inputs: 'bytes',
+ outputs: 'node'
+ }
+ static data = {
+ whitespaceCharCodes: ' \r\t'.split('')
+ .map(chr => chr.charCodeAt(0))
+ }
+ parse (lexer) {
+ const { whitespaceCharCodes } = this.constructor.data;
+
+ let text = '';
+
+ for ( ;; ) {
+ const { done, value } = lexer.look();
+ if ( done ) break;
+ if ( ! whitespaceCharCodes.includes(value) ) break;
+ text += String.fromCharCode(value);
+ lexer.next();
+ }
+
+ if ( text.length === 0 ) return;
+
+ return { $: 'whitespace', text };
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/strata.js b/packages/phoenix/packages/strataparse/strata.js
new file mode 100644
index 00000000..73dc7055
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/strata.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class DelegatingPStratumImplAPI {
+ constructor (facade) {
+ this.facade = facade;
+ }
+ get delegate () {
+ return this.facade.delegate;
+ }
+}
+
+export class DelegatingPStratumImplType {
+ constructor (facade) {
+ this.facade = facade;
+ }
+ getImplAPI () {
+ return new DelegatingPStratumImplAPI(this.facade);
+ }
+}
+
+export class TerminalPStratumImplType {
+ getImplAPI () {
+ return {};
+ }
+}
+
+export class PStratum {
+ constructor (impl) {
+ this.impl = impl;
+
+ const implTypeClass = this.impl.constructor.TYPE
+ ?? DelegatingPStratumImplType;
+
+ this.implType = new implTypeClass(this);
+ this.api = this.implType.getImplAPI();
+
+ this.lookValue = null;
+ this.seqNo = 0;
+
+ this.history = [];
+ // TODO: make this configurable
+ this.historyOn = ! this.impl.reach;
+ }
+
+ setDelegate (delegate) {
+ this.delegate = delegate;
+ }
+
+ look () {
+ if ( this.looking ) {
+ return this.lookValue;
+ }
+ this.looking = true;
+ this.lookValue = this.impl.next(this.api);
+ return this.lookValue;
+ }
+
+ next () {
+ this.seqNo++;
+ let toReturn;
+ if ( this.looking ) {
+ this.looking = false;
+ toReturn = this.lookValue;
+ } else {
+ toReturn = this.impl.next(this.api);
+ }
+ this.history.push(toReturn.value);
+ return toReturn;
+ }
+
+ fork () {
+ const forkImpl = this.impl.fork(this.api);
+ const fork = new PStratum(forkImpl);
+ // DRY: sync state
+ fork.looking = this.looking;
+ fork.lookValue = this.lookValue;
+ fork.seqNo = this.seqNo;
+ fork.history = [...this.history];
+ return fork;
+ }
+
+ join (friend) {
+ // DRY: sync state
+ this.looking = friend.looking;
+ this.lookValue = friend.lookValue;
+ this.seqNo = friend.seqNo;
+ this.history = friend.history;
+ this.impl.join(this.api, friend.impl);
+ }
+
+ reach (start, end) {
+ if ( this.impl.reach ) {
+ return this.impl.reach(this.api, start, end)
+ }
+ if ( this.historyOn ) {
+ return this.history.slice(start, end);
+ }
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js b/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js
new file mode 100644
index 00000000..68205931
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { AcceptParserUtil, ParseResult, Parser } from "../parse.js";
+
+export default class ContextSwitchingPStratumImpl {
+ constructor ({ contexts, entry }) {
+ this.contexts = { ...contexts };
+ for ( const key in this.contexts ) {
+ console.log('parsers?', this.contexts[key]);
+ const new_array = [];
+ for ( const parser of this.contexts[key] ) {
+ if ( parser.hasOwnProperty('transition') ) {
+ new_array.push({
+ ...parser,
+ parser: AcceptParserUtil.adapt(parser.parser),
+ })
+ } else {
+ new_array.push(AcceptParserUtil.adapt(parser));
+ }
+ }
+ this.contexts[key] = new_array;
+ }
+ this.stack = [{
+ context_name: entry,
+ }];
+ this.valid = true;
+
+ this.lastvalue = null;
+ }
+ get stack_top () {
+ console.log('stack top?', this.stack[this.stack.length - 1])
+ return this.stack[this.stack.length - 1];
+ }
+ get current_context () {
+ return this.contexts[this.stack_top.context_name];
+ }
+ next (api) {
+ if ( ! this.valid ) return { done: true };
+ const lexer = api.delegate;
+
+ const context = this.current_context;
+ console.log('context?', context);
+ for ( const spec of context ) {
+ {
+ const { done, value } = lexer.look();
+ this.anti_cycle_i = value === this.lastvalue ? (this.anti_cycle_i || 0) + 1 : 0;
+ if ( this.anti_cycle_i > 30 ) {
+ throw new Error('infinite loop');
+ }
+ this.lastvalue = value;
+ console.log('last value?', value, done);
+ if ( done ) return { done };
+ }
+
+ let parser, transition, peek;
+ if ( spec.hasOwnProperty('parser') ) {
+ ({ parser, transition, peek } = spec);
+ } else {
+ parser = spec;
+ }
+
+ const subLexer = lexer.fork();
+ // console.log('spec?', spec);
+ const result = parser.parse(subLexer);
+ if ( result.status === ParseResult.UNRECOGNIZED ) {
+ continue;
+ }
+ if ( result.status === ParseResult.INVALID ) {
+ return { done: true, value: result };
+ }
+ console.log('RESULT', result, spec)
+ if ( ! peek ) lexer.join(subLexer);
+
+ if ( transition ) {
+ console.log('GOT A TRANSITION')
+ if ( transition.pop ) this.stack.pop();
+ if ( transition.to ) this.stack.push({
+ context_name: transition.to,
+ });
+ }
+
+ if ( result.value.$discard || peek ) return this.next(api);
+
+ console.log('PROVIDING VALUE', result.value);
+ return { done: false, value: result.value };
+ }
+
+ return { done: true, value: 'ran out of parsers' };
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js b/packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js
new file mode 100644
index 00000000..cc3c645c
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/strata_impls/FirstRecognizedPStratumImpl.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { AcceptParserUtil, ParseResult, Parser } from "../parse.js";
+
+export default class FirstRecognizedPStratumImpl {
+ static meta = {
+ description: `
+ Implements a layer of top-down parsing by
+ iterating over parsers for higher-level constructs
+ and returning the first recognized value that was
+ produced from lower-level constructs.
+ `
+ }
+ constructor ({ parsers }) {
+ this.parsers = parsers.map(AcceptParserUtil.adapt);
+ this.valid = true;
+ }
+ next (api) {
+ if ( ! this.valid ) return { done: true };
+ const lexer = api.delegate;
+
+ for ( const parser of this.parsers ) {
+ {
+ const { done } = lexer.look();
+ if ( done ) return { done };
+ }
+
+ const subLexer = lexer.fork();
+ const result = parser.parse(subLexer);
+ if ( result.status === ParseResult.UNRECOGNIZED ) {
+ continue;
+ }
+ if ( result.status === ParseResult.INVALID ) {
+ return { done: true, value: result };
+ }
+ lexer.join(subLexer);
+ return { done: false, value: result.value };
+ }
+
+ return { done: true, value: 'ran out of parsers' };
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js b/packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js
new file mode 100644
index 00000000..f699557b
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/strata_impls/MergeWhitespacePStratumImpl.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const decoder = new TextDecoder();
+
+export class MergeWhitespacePStratumImpl {
+ static meta = {
+ inputs: 'node',
+ outputs: 'node',
+ }
+ constructor (tabWidth) {
+ this.tabWidth = tabWidth ?? 1;
+ this.line = 0;
+ this.col = 0;
+ }
+ countChar (c) {
+ if ( c === '\n' ) {
+ this.line++;
+ this.col = 0;
+ return;
+ }
+ if ( c === '\t' ) {
+ this.col += this.tabWidth;
+ return;
+ }
+ if ( c === '\r' ) return;
+ this.col++;
+ }
+ next (api) {
+ const lexer = api.delegate;
+
+ for ( ;; ) {
+ const { value, done } = lexer.next();
+ if ( done ) return { value, done };
+
+ if ( value.$ === 'whitespace' ) {
+ for ( const c of value.text ) {
+ this.countChar(c);
+ }
+ return { value, done: false };
+ // continue;
+ }
+
+ value.$cst = {
+ ...(value.$cst ?? {}),
+ line: this.line,
+ col: this.col,
+ };
+
+ if ( value.hasOwnProperty('$source') ) {
+ let source = value.$source;
+ if ( source instanceof Uint8Array ) {
+ source = decoder.decode(source);
+ }
+ for ( let c of source ) {
+ this.countChar(c);
+ }
+ } else {
+ console.warn('source missing; can\'t count position');
+ }
+
+ return { value, done: false };
+ }
+ }
+}
diff --git a/packages/phoenix/packages/strataparse/strata_impls/terminals.js b/packages/phoenix/packages/strataparse/strata_impls/terminals.js
new file mode 100644
index 00000000..eb3445c6
--- /dev/null
+++ b/packages/phoenix/packages/strataparse/strata_impls/terminals.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { TerminalPStratumImplType } from "../strata.js";
+
+export class BytesPStratumImpl {
+ static TYPE = TerminalPStratumImplType
+
+ constructor (bytes, opt_i) {
+ this.bytes = bytes;
+ this.i = opt_i ?? 0;
+ }
+ next () {
+ if ( this.i === this.bytes.length ) {
+ return { done: true, value: undefined };
+ }
+
+ const i = this.i++;
+ return { done: false, value: this.bytes[i] };
+ }
+ fork () {
+ return new BytesPStratumImpl(this.bytes, this.i);
+ }
+ join (api, forked) {
+ this.i = forked.i;
+ }
+ reach (api, start, end) {
+ return this.bytes.slice(start, end);
+ }
+}
+
+export class StringPStratumImpl {
+ static TYPE = TerminalPStratumImplType
+
+ constructor (str) {
+ const encoder = new TextEncoder();
+ const bytes = encoder.encode(str);
+ this.delegate = new BytesPStratumImpl(bytes);
+ }
+ // DRY: proxy methods
+ next (...a) {
+ return this.delegate.next(...a);
+ }
+ fork (...a) {
+ return this.delegate.fork(...a);
+ }
+ join (...a) {
+ return this.delegate.join(...a);
+ }
+ reach (...a) {
+ return this.delegate.reach(...a);
+ }
+}
diff --git a/packages/phoenix/rollup.config.js b/packages/phoenix/rollup.config.js
new file mode 100644
index 00000000..ab826f38
--- /dev/null
+++ b/packages/phoenix/rollup.config.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { nodeResolve } from '@rollup/plugin-node-resolve'
+import commonjs from '@rollup/plugin-commonjs';
+import copy from 'rollup-plugin-copy';
+
+const configFile = process.env.CONFIG_FILE ?? 'config/dev.js';
+await import(`./${configFile}`);
+
+export default {
+ input: "src/main_puter.js",
+ output: {
+ file: "dist/bundle.js",
+ format: "iife"
+ },
+ plugins: [
+ nodeResolve(),
+ commonjs(),
+ copy({
+ targets: [
+ {
+ src: 'assets/index.html',
+ dest: 'dist',
+ transform: (contents, name) => {
+ return contents.toString().replace('__SDK_URL__', globalThis.__CONFIG__.sdk_url);
+ }
+ },
+ { src: 'assets/shell.html', dest: 'dist' },
+ { src: configFile, dest: 'dist', rename: 'config.js' }
+ ]
+ }),
+ ]
+}
diff --git a/packages/phoenix/run.json5 b/packages/phoenix/run.json5
new file mode 100644
index 00000000..a7f23ac7
--- /dev/null
+++ b/packages/phoenix/run.json5
@@ -0,0 +1,15 @@
+{
+ services: [
+ {
+ name: 'shell.http',
+ pwd: './dist',
+ // command: 'npx http-server -p 8080 -S -C "{cert}" -K "{key}"',
+ command: 'npx http-server -p 8080',
+ },
+ {
+ name: 'shell.rollup',
+ command: 'npx rollup -c rollup.config.js --watch',
+ pwd: '.'
+ },
+ ],
+}
diff --git a/packages/phoenix/src/ansi-shell/ANSIContext.js b/packages/phoenix/src/ansi-shell/ANSIContext.js
new file mode 100644
index 00000000..cc7283a1
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ANSIContext.js
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Context } from "../context/context.js";
+
+const modifiers = ['shift', 'alt', 'ctrl', 'meta'];
+
+const keyboardModifierBits = {};
+for ( let i=0 ; i < modifiers.length ; i++ ) {
+ const key = `KEYBOARD_BIT_${modifiers[i].toUpperCase()}`;
+ keyboardModifierBits[key] = 1 << i;
+}
+
+export const ANSIContext = new Context({
+ constants: {
+ CHAR_LF: '\n'.charCodeAt(0),
+ CHAR_CR: '\r'.charCodeAt(0),
+ CHAR_TAB: '\t'.charCodeAt(0),
+ CHAR_CSI: '['.charCodeAt(0),
+ CHAR_OSC: ']'.charCodeAt(0),
+ CHAR_ETX: 0x03,
+ CHAR_EOT: 0x04,
+ CHAR_ESC: 0x1B,
+ CHAR_DEL: 0x7F,
+ CHAR_BEL: 0x07,
+ CHAR_FF: 0x0C,
+ CSI_F_0: 0x40,
+ CSI_F_E: 0x7F,
+ ...keyboardModifierBits
+ }
+});
+
+export const getActiveModifiersFromXTerm = (n) => {
+ // decrement explained in doc/graveyard/keyboard_modifiers.md
+ n--;
+
+ const active = {};
+
+ for ( let i=0 ; i < modifiers.length ; i++ ) {
+ if ( n & 1 << i ) {
+ active[modifiers[i]] = true;
+ }
+ }
+
+ return active;
+};
diff --git a/packages/phoenix/src/ansi-shell/ANSIShell.js b/packages/phoenix/src/ansi-shell/ANSIShell.js
new file mode 100644
index 00000000..0eb151e8
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ANSIShell.js
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ConcreteSyntaxError } from "./ConcreteSyntaxError.js";
+import { MultiWriter } from "./ioutil/MultiWriter.js";
+import { Coupler } from "./pipeline/Coupler.js";
+import { Pipe } from "./pipeline/Pipe.js";
+import { Pipeline } from "./pipeline/Pipeline.js";
+
+export class ANSIShell extends EventTarget {
+ constructor (ctx) {
+ super();
+
+ this.ctx = ctx;
+ this.variables_ = {};
+ this.config = ctx.externs.config;
+
+ this.debugFeatures = {};
+
+ const self = this;
+ this.variables = new Proxy(this.variables_, {
+ get (target, k) {
+ return Reflect.get(target, k);
+ },
+ set (target, k, v) {
+ const oldval = target[k];
+ const retval = Reflect.set(target, k, v);
+ self.dispatchEvent(new CustomEvent('shell-var-change', {
+ key: k,
+ oldValue: oldval,
+ newValue: target[k],
+ }))
+ return retval;
+ }
+ })
+
+ this.addEventListener('signal.window-resize', evt => {
+ this.variables.size = evt.detail;
+ })
+
+ this.env = {};
+
+ this.initializeReasonableDefaults();
+ }
+
+ export_ (k, v) {
+ if ( typeof v === 'function' ) {
+ Object.defineProperty(this.env, k, {
+ enumerable: true,
+ get: v
+ })
+ return;
+ }
+ this.env[k] = v;
+ }
+
+ initializeReasonableDefaults() {
+ const { env } = this.ctx.platform;
+ const home = env.get('HOME');
+ const user = env.get('USER');
+ this.variables.pwd = home;
+ this.variables.home = home;
+ this.variables.user = user;
+
+ this.variables.host = env.get('HOSTNAME');
+
+ // Computed values
+ Object.defineProperty(this.env, 'PWD', {
+ enumerable: true,
+ get: () => this.variables.pwd,
+ set: v => this.variables.pwd = v
+ })
+ Object.defineProperty(this.env, 'ROWS', {
+ enumerable: true,
+ get: () => this.variables.size?.rows ?? 0
+ })
+ Object.defineProperty(this.env, 'COLS', {
+ enumerable: true,
+ get: () => this.variables.size?.cols ?? 0
+ })
+
+ this.export_('LANG', 'en_US.UTF-8');
+ this.export_('PS1', '[\\u@puter.com \\w]\\$ ');
+
+ for ( const k in env.getEnv() ) {
+ console.log('setting', k, env.get(k));
+ this.export_(k, env.get(k));
+ }
+
+ // Default values
+ this.export_('HOME', () => this.variables.home);
+ this.export_('USER', () => this.variables.user);
+ this.export_('TERM', 'xterm-256color');
+ this.export_('TERM_PROGRAM', 'puter-ansi');
+ // TODO: determine how localization will affect this
+ // TODO: add TERM_PROGRAM_VERSION
+ // TODO: add OLDPWD
+ }
+
+ async doPromptIteration() {
+ if ( globalThis.force_eot && this.ctx.platform.name === 'node' ) {
+ process.exit(0);
+ }
+ const { readline } = this.ctx.externs;
+ // DRY: created the same way in runPipeline
+ const executionCtx = this.ctx.sub({
+ vars: this.variables,
+ env: this.env,
+ locals: {
+ pwd: this.variables.pwd,
+ }
+ });
+ this.ctx.externs.echo.off();
+ const input = await readline(
+ this.expandPromptString(this.env.PS1),
+ executionCtx,
+ );
+ this.ctx.externs.echo.on();
+
+ if ( input.trim() === '' ) {
+ this.ctx.externs.out.write('');
+ return;
+ }
+
+ // Specially-processed inputs for debug features
+ if ( input.startsWith('%%%') ) {
+ this.ctx.externs.out.write('%%%: interpreting as debug instruction\n');
+ const [prefix, flag, onOff] = input.split(' ');
+ const isOn = onOff === 'on' ? true : false;
+ this.ctx.externs.out.write(
+ `%%%: Setting ${JSON.stringify(flag)} to ` +
+ (isOn ? 'ON' : 'OFF') + '\n'
+ )
+ this.debugFeatures[flag] = isOn;
+ return; // don't run as a pipeline
+ }
+
+ // TODO: catch here, but errors need to be more structured first
+ try {
+ await this.runPipeline(input);
+ } catch (e) {
+ if ( e instanceof ConcreteSyntaxError ) {
+ const here = e.print_here(input);
+ this.ctx.externs.out.write(here + '\n');
+ }
+ this.ctx.externs.out.write('error: ' + e.message + '\n');
+ console.log(e);
+ return;
+ }
+ }
+
+ readtoken (str) {
+ return this.ctx.externs.parser.parseLineForProcessing(str);
+ }
+
+ async runPipeline (cmdOrTokens) {
+ const tokens = typeof cmdOrTokens === 'string'
+ ? (() => {
+ // TODO: move to doPromptIter with better error objects
+ try {
+ return this.readtoken(cmdOrTokens)
+ } catch (e) {
+ this.ctx.externs.out.write('error: ' +
+ e.message + '\n');
+ return;
+ }
+ })()
+ : cmdOrTokens ;
+
+ if ( tokens.length === 0 ) return;
+
+ if ( tokens.length > 1 ) {
+ // TODO: as exception instead, and more descriptive
+ this.ctx.externs.out.write(
+ "something went wrong...\n"
+ );
+ return;
+ }
+
+ let ast = tokens[0];
+
+ // Left the code below here (commented) because I think it's
+ // interesting; the AST now always has a pipeline at the top
+ // level after recent changes to the parser.
+
+ // // wrap an individual command in a pipeline
+ // // TODO: should this be done here, or elsewhere?
+ // if ( ast.$ === 'command' ) {
+ // ast = {
+ // $: 'pipeline',
+ // components: [ast]
+ // };
+ // }
+
+ if ( this.debugFeatures['show-ast'] ) {
+ this.ctx.externs.out.write(
+ JSON.stringify(tokens, undefined, ' ') + '\n'
+ );
+ return;
+ }
+
+ const executionCtx = this.ctx.sub({
+ vars: this.variables,
+ env: this.env,
+ locals: {
+ pwd: this.variables.pwd,
+ }
+ });
+
+ const pipeline = await Pipeline.createFromAST(executionCtx, ast);
+
+ await pipeline.execute(executionCtx);
+ }
+
+ expandPromptString (str) {
+ str = str.replace('\\u', this.variables.user);
+ str = str.replace('\\w', this.variables.pwd);
+ str = str.replace('\\h', this.variables.host);
+ str = str.replace('\\$', '$');
+ return str;
+ }
+
+ async outputANSI (ctx) {
+ await ctx.iterate(async item => {
+ ctx.externs.out.write(item.name + '\n');
+ });
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js b/packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js
new file mode 100644
index 00000000..7ffbfd5c
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+/**
+ * An error for which the location it occurred within the input is known.
+ */
+export class ConcreteSyntaxError extends Error {
+ constructor(message, cst_location) {
+ super(message);
+ this.cst_location = cst_location;
+ }
+
+ /**
+ * Prints the location of the error in the input.
+ *
+ * Example output:
+ *
+ * ```
+ * 1: echo $($(echo zxcv))
+ * ^^^^^^^^^^^
+ * ```
+ *
+ * @param {*} input
+ */
+ print_here (input) {
+ const lines = input.split('\n');
+ const line = lines[this.cst_location.line];
+ const str_line_number = String(this.cst_location.line + 1) + ': ';
+ const n_spaces =
+ str_line_number.length +
+ this.cst_location.start;
+ const n_arrows = Math.max(
+ this.cst_location.end - this.cst_location.start,
+ 1
+ );
+
+ return (
+ str_line_number + line + '\n' +
+ ' '.repeat(n_spaces) + '^'.repeat(n_arrows)
+ );
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js b/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js
new file mode 100644
index 00000000..c8f4832a
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { parseArgs } from '@pkgjs/parseargs';
+import { DEFAULT_OPTIONS } from '../../puter-shell/coreutils/coreutil_lib/help.js';
+
+export default {
+ name: 'simple-parser',
+ async process (ctx, spec) {
+ console.log({
+ ...spec,
+ args: ctx.locals.args
+ });
+
+ // Insert standard options
+ spec.options = Object.assign(spec.options || {}, DEFAULT_OPTIONS);
+
+ let result;
+ try {
+ if ( ! ctx.locals.args ) debugger;
+ result = parseArgs({ ...spec, args: ctx.locals.args });
+ } catch (e) {
+ await ctx.externs.out.write(
+ '\x1B[31;1m' +
+ 'error parsing arguments: ' +
+ e.message + '\x1B[0m\n');
+ ctx.cmdExecState.valid = false;
+ return;
+ }
+
+ if (result.values.help) {
+ ctx.cmdExecState.printHelpAndExit = true;
+ }
+
+ ctx.locals.values = result.values;
+ ctx.locals.positionals = result.positionals;
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/decorators/errors.js b/packages/phoenix/src/ansi-shell/decorators/errors.js
new file mode 100644
index 00000000..85eabb3e
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/decorators/errors.js
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'errors',
+ decorate (fn, { command, ctx }) {
+ return async (...a) => {
+ try {
+ await fn(...a);
+ } catch (e) {
+ console.log('GOT IT HERE');
+ // message without "Error:"
+ let message = e.message;
+ if (message.startsWith('Error: ')) {
+ message = message.slice(7);
+ }
+ ctx.externs.err.write(
+ '\x1B[31;1m' + command.name + ': ' + message + '\x1B[0m\n'
+ );
+ }
+ }
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js b/packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js
new file mode 100644
index 00000000..d2e0655a
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ProxyWriter } from "./ProxyWriter.js";
+
+const encoder = new TextEncoder();
+
+export class ByteWriter extends ProxyWriter {
+ async write (item) {
+ if ( typeof item === 'string' ) {
+ item = encoder.encode(item);
+ }
+ if ( item instanceof Blob ) {
+ item = new Uint8Array(await item.arrayBuffer());
+ }
+ await this.delegate.write(item);
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/MemReader.js b/packages/phoenix/src/ansi-shell/ioutil/MemReader.js
new file mode 100644
index 00000000..53d711ae
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/MemReader.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class MemReader {
+ constructor (data) {
+ this.data = data;
+ this.pos = 0;
+ }
+ async read (opt_buffer) {
+ if ( this.pos >= this.data.length ) {
+ return { done: true };
+ }
+
+ if ( ! opt_buffer ) {
+ this.pos = this.data.length;
+ return { value: this.data, done: false };
+ }
+
+ const toReturn = this.data.slice(
+ this.pos,
+ Math.min(this.pos + opt_buffer.length, this.data.length),
+ );
+
+ return {
+ value: opt_buffer,
+ size: toReturn.length
+ };
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/MemWriter.js b/packages/phoenix/src/ansi-shell/ioutil/MemWriter.js
new file mode 100644
index 00000000..8e091f0d
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/MemWriter.js
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const encoder = new TextEncoder();
+
+export class MemWriter {
+ constructor () {
+ this.items = [];
+ }
+ async write (item) {
+ this.items.push(item);
+ }
+ async close () {}
+
+ getAsUint8Array() {
+ const uint8arrays = [];
+ for ( let item of this.items ) {
+ if ( typeof item === 'string' ) {
+ item = encoder.encode(item);
+ }
+
+ if ( ! ( item instanceof Uint8Array ) ) {
+ throw new Error('could not convert to Uint8Array');
+ }
+
+ uint8arrays.push(item);
+ }
+
+ const outputUint8Array = new Uint8Array(
+ uint8arrays.reduce((sum, item) => sum + item.length, 0)
+ );
+
+ let pos = 0;
+ for ( const item of uint8arrays ) {
+ outputUint8Array.set(item, pos);
+ pos += item.length;
+ }
+
+ return outputUint8Array;
+ }
+
+ getAsBlob () {
+ // If there is just one item and it's a blob, return it
+ if ( this.items.length === 1 && this.items[0] instanceof Blob ) {
+ return this.items[0];
+ }
+
+ const uint8array = this.getAsUint8Array();
+ return new Blob([uint8array]);
+ }
+
+ getAsString () {
+ return new TextDecoder().decode(this.getAsUint8Array());
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js b/packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js
new file mode 100644
index 00000000..9b3be271
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class MultiWriter {
+ constructor ({ delegates }) {
+ this.delegates = delegates;
+ }
+
+ async write (item) {
+ for ( const delegate of this.delegates ) {
+ await delegate.write(item);
+ }
+ }
+
+ async close () {
+ for ( const delegate of this.delegates ) {
+ await delegate.close();
+ }
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js b/packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js
new file mode 100644
index 00000000..3980aeff
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ProxyWriter } from "./ProxyWriter.js";
+
+export class NullifyWriter extends ProxyWriter {
+ async write (item) {
+ // NOOP
+ }
+
+ async close () {
+ await this.delegate.close();
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js b/packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js
new file mode 100644
index 00000000..9b2e6bfe
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class ProxyReader {
+ constructor ({ delegate }) {
+ this.delegate = delegate;
+ }
+
+ read (...a) { return this.delegate.read(...a); }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js b/packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js
new file mode 100644
index 00000000..cd6b4dbd
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class ProxyWriter {
+ constructor ({ delegate }) {
+ this.delegate = delegate;
+ }
+
+ write (...a) { return this.delegate.write(...a); }
+ close (...a) { return this.delegate.close(...a); }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js b/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js
new file mode 100644
index 00000000..59d69a0a
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ANSIContext } from "../ANSIContext.js";
+import { signals } from "../signals.js";
+import { ProxyReader } from "./ProxyReader.js";
+
+const encoder = new TextEncoder();
+
+export class SignalReader extends ProxyReader {
+ constructor ({ sig, ...kv }, ...a) {
+ super({ ...kv }, ...a);
+ this.sig = sig;
+ }
+
+ async read (opt_buffer) {
+ const mapping = [
+ [ANSIContext.constants.CHAR_ETX, signals.SIGINT],
+ [ANSIContext.constants.CHAR_EOT, signals.SIGQUIT],
+ ];
+
+ let { value, done } = await this.delegate.read(opt_buffer);
+
+ if ( value === undefined ) {
+ return { value, done };
+ }
+
+ const tmp_value = value;
+
+ if ( ! tmp_value instanceof Uint8Array ) {
+ tmp_value = encoder.encode(value);
+ }
+
+ // show hex for debugging
+ // console.log(value.split('').map(c => c.charCodeAt(0).toString(16)).join(' '));
+ console.log('value??', value)
+
+ for ( const [key, signal] of mapping ) {
+ if ( tmp_value.includes(key) ) {
+ // this.sig.emit(signal);
+ // if ( signal === signals.SIGQUIT ) {
+ return { done: true };
+ // }
+ }
+ }
+
+ return { value, done };
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js b/packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js
new file mode 100644
index 00000000..be89d509
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ProxyReader } from "./ProxyReader.js";
+
+const decoder = new TextDecoder();
+
+export class SyncLinesReader extends ProxyReader {
+ constructor (...a) {
+ super(...a);
+ this.lines = [];
+ this.fragment = '';
+ }
+ async read (opt_buffer) {
+ if ( opt_buffer ) {
+ // Line sync contradicts buffered reads
+ return await this.delegate.read(opt_buffer);
+ }
+
+ return await this.readNextLine_();
+ }
+ async readNextLine_ () {
+ if ( this.lines.length > 0 ) {
+ return { value: this.lines.shift() };
+ }
+
+ for ( ;; ) {
+ // CHECK: this might read once more after done; is that ok?
+ let { value, done } = await this.delegate.read();
+
+ if ( value instanceof Uint8Array ) {
+ value = decoder.decode(value);
+ }
+
+ if ( done ) {
+ if ( this.fragment.length === 0 ) {
+ return { value, done };
+ }
+
+ value = this.fragment;
+ this.fragment = '';
+ return { value };
+ }
+
+ if ( ! value.match(/\n|\r|\r\n/) ) {
+ this.fragment += value;
+ continue;
+ }
+
+ // Guaranteed to be 2 items, because value includes a newline
+ const lines = value.split(/\n|\r|\r\n/);
+
+ // The first line continues from the existing fragment
+ const firstLine = this.fragment + lines.shift();
+ // The last line is incomplete, and goes on the fragment
+ this.fragment = lines.pop();
+
+ // Any lines between are enqueued for subsequent reads,
+ // and they include a line-feed character.
+ this.lines.push(...lines.map(txt => txt + '\n'));
+
+ return { value: firstLine + '\n' };
+ }
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js b/packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js
new file mode 100644
index 00000000..bb4769cd
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const PARSE_CONSTANTS = {
+ list_ws: [' ', '\n', '\t'],
+ list_quot: [`"`, `'`],
+};
+
+PARSE_CONSTANTS.list_stoptoken = [
+ '|','>','<','&','\\','#',';','(',')',
+ ...PARSE_CONSTANTS.list_ws,
+ ...PARSE_CONSTANTS.list_quot,
+]
+
+PARSE_CONSTANTS.escapeSubstitutions = {
+ '\\': '\\',
+ '/': '/',
+ b: '\b',
+ f: '\f',
+ n: '\n',
+ r: '\r',
+ t: '\t',
+ '"': '"',
+ "'": "'",
+};
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js b/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js
new file mode 100644
index 00000000..b38ffa6d
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { StrataParser, StringPStratumImpl } from "strataparse";
+import { buildParserFirstHalf } from "./buildParserFirstHalf.js";
+import { buildParserSecondHalf } from "./buildParserSecondHalf.js";
+
+export class PuterShellParser {
+ constructor () {
+ {
+ }
+ }
+ parseLineForSyntax () {}
+ parseLineForProcessing (input) {
+ const sp = new StrataParser();
+ sp.add(new StringPStratumImpl(input));
+ // TODO: optimize by re-using this parser
+ // buildParserFirstHalf(sp, "interpreting");
+ buildParserFirstHalf(sp, "syntaxHighlighting");
+ buildParserSecondHalf(sp);
+ const result = sp.parse();
+ if ( sp.error ) {
+ throw new Error(sp.error);
+ }
+ console.log('PARSER RESULT', result);
+ return result;
+ }
+ parseScript (input) {
+ const sp = new StrataParser();
+ sp.add(new StringPStratumImpl(input));
+ buildParserFirstHalf(sp, "syntaxHighlighting");
+ buildParserSecondHalf(sp, { multiline: true });
+ const result = sp.parse();
+ if ( sp.error ) {
+ throw new Error(sp.error);
+ }
+ return result;
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js b/packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js
new file mode 100644
index 00000000..f6916088
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/parsing/UnquotedTokenParserImpl.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const list_ws = [' ', '\n', '\t'];
+const list_quot = [`"`, `'`];
+const list_stoptoken = [
+ '|','>','<','&','\\','#',';','(',')',
+ ...list_ws,
+ ...list_quot
+];
+
+export class UnquotedTokenParserImpl {
+ static meta = {
+ inputs: 'bytes',
+ outputs: 'node'
+ }
+ static data = {
+ excludes: list_stoptoken
+ }
+ parse (lexer) {
+ const { excludes } = this.constructor.data;
+ let text = '';
+
+ for ( ;; ) {
+ const { done, value } = lexer.look();
+ if ( done ) break;
+ const str = String.fromCharCode(value);
+ if ( excludes.includes(str) ) break;
+ text += str;
+ lexer.next();
+ }
+
+ if ( text.length === 0 ) return;
+
+ return { $: 'symbol', text };
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/parsing/brainstorming.js b/packages/phoenix/src/ansi-shell/parsing/brainstorming.js
new file mode 100644
index 00000000..0e16cdca
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/parsing/brainstorming.js
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const seq = [
+ { $: 'symbol', text: 'command' },
+ { $: 'string.dquote' },
+ { $: 'string.segment', text: '-' },
+ { $: 'op.cmd-subst' },
+ { $: 'op.close' },
+];
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js b/packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js
new file mode 100644
index 00000000..141f632f
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { FirstRecognizedPStratumImpl, ParserBuilder, ParserFactory, StrUntilParserImpl, StrataParseFacade, WhitespaceParserImpl } from "strataparse";
+import { UnquotedTokenParserImpl } from "./UnquotedTokenParserImpl.js";
+import { PARSE_CONSTANTS } from "./PARSE_CONSTANTS.js";
+import { MergeWhitespacePStratumImpl } from "strataparse/strata_impls/MergeWhitespacePStratumImpl.js";
+import ContextSwitchingPStratumImpl from "strataparse/strata_impls/ContextSwitchingPStratumImpl.js";
+
+const parserConfigProfiles = {
+ syntaxHighlighting: { cst: true },
+ interpreting: { cst: false }
+};
+
+const list_ws = [' ', '\n', '\t'];
+const list_quot = [`"`, `'`];
+const list_stoptoken = [
+ '|','>','<','&','\\','#',';','(',')',
+ ...list_ws,
+ ...list_quot
+];
+
+export const buildParserFirstHalf = (sp, profile) => {
+ const options = profile ? parserConfigProfiles[profile]
+ : { cst: false };
+
+ const parserFactory = new ParserFactory();
+ if ( options.cst ) {
+ parserFactory.concrete = true;
+ parserFactory.rememberSource = true;
+ }
+
+ const parserRegistry = StrataParseFacade.getDefaultParserRegistry();
+
+ const parserBuilder = new ParserBuilder({
+ parserFactory,
+ parserRegistry,
+ });
+
+
+ // TODO: unquoted tokens will actually need to be parsed in
+ // segments to because `$(echo "la")h` works in sh
+ const buildStringParserDef = quote => {
+ return a => a.sequence(
+ a.literal(quote),
+ a.repeat(a.choice(
+ // TODO: replace this with proper parser
+ parserFactory.create(StrUntilParserImpl, {
+ stopChars: ['\\', quote],
+ }, { assign: { $: 'string.segment' } }),
+ a.sequence(
+ a.literal('\\'),
+ a.choice(
+ a.literal(quote),
+ ...Object.keys(
+ PARSE_CONSTANTS.escapeSubstitutions
+ ).map(chr => a.literal(chr))
+ // TODO: \u[4],\x[2],\0[3]
+ )
+ ).assign({ $: 'string.escape' })
+ )),
+ a.literal(quote),
+ ).assign({ $: 'string' })
+ };
+
+
+ const buildStringContext = quote => [
+ parserFactory.create(StrUntilParserImpl, {
+ stopChars: ['\\', "$", quote],
+ }, { assign: { $: 'string.segment' } }),
+ parserBuilder.def(a => a.sequence(
+ a.literal('\\'),
+ a.choice(
+ a.literal(quote),
+ ...Object.keys(
+ PARSE_CONSTANTS.escapeSubstitutions
+ ).map(chr => a.literal(chr))
+ // TODO: \u[4],\x[2],\0[3]
+ )
+ ).assign({ $: 'string.escape' })),
+ {
+ parser: parserBuilder.def(a => a.literal(quote).assign({ $: 'string.close' })),
+ transition: { pop: true }
+ },
+ {
+ parser: parserBuilder.def(a => {
+ return a.literal('$(').assign({ $: 'op.cmd-subst' })
+ }),
+ transition: {
+ to: 'command',
+ }
+ },
+ ];
+
+ // sp.add(
+ // new FirstRecognizedPStratumImpl({
+ // parsers: [
+ // parserFactory.create(WhitespaceParserImpl),
+ // parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })),
+ // parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })),
+ // parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })),
+ // parserBuilder.def(a => a.literal('$((').assign({ $: 'op.arithmetic' })),
+ // parserBuilder.def(a => a.literal('$(').assign({ $: 'op.cmd-subst' })),
+ // parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
+ // parserFactory.create(StrUntilParserImpl, {
+ // stopChars: list_stoptoken,
+ // }, { assign: { $: 'symbol' } }),
+ // // parserFactory.create(UnquotedTokenParserImpl),
+ // parserBuilder.def(buildStringParserDef('"')),
+ // parserBuilder.def(buildStringParserDef(`'`)),
+ // ]
+ // })
+ // )
+
+ sp.add(
+ new ContextSwitchingPStratumImpl({
+ entry: 'command',
+ contexts: {
+ command: [
+ parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })),
+ parserFactory.create(WhitespaceParserImpl),
+ parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })),
+ parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })),
+ parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })),
+ {
+ parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
+ transition: {
+ pop: true,
+ }
+ },
+ {
+ parser: parserBuilder.def(a => a.literal('"').assign({ $: 'string.dquote' })),
+ transition: {
+ to: 'string.dquote',
+ }
+ },
+ {
+ parser: parserBuilder.def(a => a.literal(`'`).assign({ $: 'string.squote' })),
+ transition: {
+ to: 'string.squote',
+ }
+ },
+ {
+ parser: parserBuilder.def(a => a.none()),
+ transition: {
+ to: 'symbol',
+ }
+ },
+ ],
+ 'string.dquote': buildStringContext('"'),
+ 'string.squote': buildStringContext(`'`),
+ symbol: [
+ parserFactory.create(StrUntilParserImpl, {
+ stopChars: [...list_stoptoken, '$'],
+ }, { assign: { $: 'symbol' } }),
+ {
+ // TODO: redundant definition to the one in 'command'
+ parser:
+ parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })),
+ transition: { pop: true }
+ },
+ {
+ parser: parserFactory.create(WhitespaceParserImpl),
+ transition: { pop: true }
+ },
+ {
+ peek: true,
+ parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
+ transition: { pop: true }
+ },
+ {
+ parser: parserBuilder.def(a => {
+ return a.literal('$(').assign({ $: 'op.cmd-subst' })
+ }),
+ transition: {
+ to: 'command',
+ }
+ },
+ {
+ parser: parserBuilder.def(a => a.none()),
+ transition: { pop: true }
+ },
+ {
+ parser: parserBuilder.def(a => a.choice(
+ ...list_stoptoken.map(chr => a.literal(chr))
+ )),
+ transition: { pop: true }
+ }
+ ],
+ },
+ wrappers: {
+ 'string.dquote': {
+ $: 'string',
+ quote: '"',
+ },
+ 'string.squote': {
+ $: 'string',
+ quote: `'`,
+ },
+ },
+ })
+ )
+
+ sp.add(
+ new MergeWhitespacePStratumImpl()
+ )
+};
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js b/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js
new file mode 100644
index 00000000..4bd4d82a
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ParserBuilder, ParserFactory, StrataParseFacade } from "strataparse"
+
+import { PARSE_CONSTANTS } from "./PARSE_CONSTANTS.js";
+const escapeSubstitutions = PARSE_CONSTANTS.escapeSubstitutions;
+
+const splitTokens = (items, delimPredicate) => {
+ const result = [];
+ {
+ let buffer = [];
+ // single pass to split by pipe token
+ for ( let i=0 ; i < items.length ; i++ ) {
+ if ( delimPredicate(items[i]) ) {
+ result.push(buffer);
+ buffer = [];
+ continue;
+ }
+
+ buffer.push(items[i]);
+ }
+
+ if ( buffer.length !== 0 ) {
+ result.push(buffer);
+ }
+ }
+ return result;
+};
+
+class ReducePrimitivesPStratumImpl {
+ next (api) {
+ const lexer = api.delegate;
+
+ let { value, done } = lexer.next();
+
+ if ( value.$ === 'string' ) {
+ const [lQuote, contents, rQuote] = value.results;
+ let text = '';
+ for ( const item of contents.results ) {
+ if ( item.$ === 'string.segment' ) {
+ // console.log('segment?', item.text)
+ text += item.text;
+ continue;
+ }
+ if ( item.$ === 'string.escape' ) {
+ const [escChar, escValue] = item.results;
+ if ( escValue.$ === 'literal' ) {
+ text += escapeSubstitutions[escValue.text];
+ } // else
+ if ( escValue.$ === 'sequence' ) {
+ // TODO: \u[4],\x[2],\0[3]
+ }
+ }
+ }
+
+ value.text = text;
+ delete value.results;
+ }
+
+ return { value, done };
+ }
+}
+
+class ShellConstructsPStratumImpl {
+ static states = [
+ {
+ name: 'pipeline',
+ enter ({ node }) {
+ node.$ = 'pipeline';
+ node.commands = [];
+ },
+ exit ({ node }) {
+ console.log('!!!!!',this.stack_top.node)
+ if ( this.stack_top?.node?.$ === 'script' ) {
+ this.stack_top.node.statements.push(node);
+ }
+ if ( this.stack_top?.node?.$ === 'string' ) {
+ this.stack_top.node.components.push(node);
+ }
+ },
+ next ({ value, lexer }) {
+ if ( value.$ === 'op.line-terminator' ) {
+ console.log('the stack??', this.stack)
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'op.close' ) {
+ if ( this.stack.length === 1 ) {
+ throw new Error('unexpected close');
+ }
+ lexer.next();
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'op.pipe' ) {
+ lexer.next();
+ }
+ this.push('command');
+ }
+ },
+ {
+ name: 'command',
+ enter ({ node }) {
+ node.$ = 'command';
+ node.tokens = [];
+ node.inputRedirects = [];
+ node.outputRedirects = [];
+ },
+ next ({ value, lexer }) {
+ if ( value.$ === 'op.line-terminator' ) {
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'whitespace' ) {
+ lexer.next();
+ return;
+ }
+ if ( value.$ === 'op.close' ) {
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'op.pipe' ) {
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'op.redirect' ) {
+ this.push('redirect', { direction: value.direction });
+ lexer.next();
+ return;
+ }
+ this.push('token');
+ },
+ exit ({ node }) {
+ this.stack_top.node.commands.push(node);
+ }
+ },
+ {
+ name: 'redirect',
+ enter ({ node }) {
+ node.$ = 'redirect';
+ node.tokens = [];
+ },
+ exit ({ node }) {
+ const { direction } = node;
+ const arry = direction === 'in' ?
+ this.stack_top.node.inputRedirects :
+ this.stack_top.node.outputRedirects;
+ arry.push(node.tokens[0]);
+ },
+ next ({ node, value, lexer }) {
+ if ( node.tokens.length === 1 ) {
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'whitespace' ) {
+ lexer.next();
+ return;
+ }
+ if ( value.$ === 'op.close' ) {
+ throw new Error('unexpected close');
+ }
+ this.push('token');
+ }
+ },
+ {
+ name: 'token',
+ enter ({ node }) {
+ node.$ = 'token';
+ node.components = [];
+ },
+ exit ({ node }) {
+ this.stack_top.node.tokens.push(node);
+ },
+ next ({ value, lexer }) {
+ if ( value.$ === 'op.line-terminator' ) {
+ console.log('well, got here')
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'string.dquote' ) {
+ this.push('string', { quote: '"' });
+ lexer.next();
+ return;
+ }
+ if ( value.$ === 'string.squote' ) {
+ this.push('string', { quote: "'" });
+ lexer.next();
+ return;
+ }
+ if (
+ value.$ === 'whitespace' ||
+ value.$ === 'op.close'
+ ) {
+ this.pop();
+ return;
+ }
+ this.push('string', { quote: null });
+ }
+ },
+ {
+ name: 'string',
+ enter ({ node }) {
+ node.$ = 'string';
+ node.components = [];
+ },
+ exit ({ node }) {
+ this.stack_top.node.components.push(...node.components);
+ },
+ next ({ node, value, lexer }) {
+ console.log('WHAT THO', node)
+ if ( value.$ === 'op.line-terminator' && node.quote === null ) {
+ console.log('well, got here')
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'string.close' && node.quote !== null ) {
+ lexer.next();
+ this.pop();
+ return;
+ }
+ if (
+ node.quote === null && (
+ value.$ === 'whitespace' ||
+ value.$ === 'op.close'
+ )
+ ) {
+ this.pop();
+ return;
+ }
+ if ( value.$ === 'op.cmd-subst' ) {
+ this.push('pipeline');
+ lexer.next();
+ return;
+ }
+ node.components.push(value);
+ lexer.next();
+ }
+ },
+ ];
+
+ constructor () {
+ this.states = this.constructor.states;
+ this.buffer = [];
+ this.stack = [];
+ this.done_ = false;
+
+ this._init();
+ }
+
+ _init () {
+ this.push('pipeline');
+ }
+
+ get stack_top () {
+ return this.stack[this.stack.length - 1];
+ }
+
+ push (state_name, node) {
+ const state = this.states.find(s => s.name === state_name);
+ if ( ! node ) node = {};
+ this.stack.push({ state, node });
+ state.enter && state.enter.call(this, { node });
+ }
+
+ pop () {
+ const { state, node } = this.stack.pop();
+ state.exit && state.exit.call(this, { node });
+ }
+
+ chstate (state) {
+ this.stack_top.state = state;
+ }
+
+ next (api) {
+ if ( this.done_ ) return { done: true };
+
+ const lexer = api.delegate;
+
+ console.log('THE NODE', this.stack[0].node);
+ // return { done: true, value: { $: 'test' } };
+
+ for ( let i=0 ; i < 500 ; i++ ) {
+ const { done, value } = lexer.look();
+
+ if ( done ) {
+ while ( this.stack.length > 1 ) {
+ this.pop();
+ }
+ break;
+ }
+
+ const { state, node } = this.stack_top;
+ console.log('value?', value, done)
+ console.log('state?', state.name);
+
+ state.next.call(this, { lexer, value, node, state });
+
+ // if ( done ) break;
+ }
+
+ console.log('THE NODE', this.stack[0]);
+
+ this.done_ = true;
+ return { done: false, value: this.stack[0].node };
+ }
+
+ // old method; not used anymore
+ consolidateTokens (tokens) {
+ const types = tokens.map(token => token.$);
+
+ if ( tokens.length === 0 ) {
+ throw new Error('expected some tokens');
+ }
+
+ if ( types.includes('op.pipe') ) {
+ const components =
+ splitTokens(tokens, t => t.$ === 'op.pipe')
+ .map(tokens => this.consolidateTokens(tokens));
+
+ return { $: 'pipeline', components };
+ }
+
+ // const command = tokens.shift();
+ const args = [];
+ const outputRedirects = [];
+ const inputRedirects = [];
+
+ const states = {
+ STATE_NORMAL: {},
+ STATE_REDIRECT: {
+ direction: null
+ },
+ };
+ const stack = [];
+ let dest = args;
+ let state = states.STATE_NORMAL;
+ for ( const token of tokens ) {
+ if ( state === states.STATE_REDIRECT ) {
+ const arry = state.direction === 'out' ?
+ outputRedirects : inputRedirects;
+ arry.push({
+ // TODO: get string value only
+ path: token,
+ })
+ state = states.STATE_NORMAL;
+ continue;
+ }
+ if ( token.$ === 'op.redirect' ) {
+ state = states.STATE_REDIRECT;
+ state.direction = token.direction;
+ continue;
+ }
+ if ( token.$ === 'op.cmd-subst' ) {
+ const new_dest = [];
+ dest = new_dest;
+ stack.push({
+ $: 'command-substitution',
+ tokens: new_dest,
+ });
+ continue;
+ }
+ if ( token.$ === 'op.close' ) {
+ const sub = stack.pop();
+ dest = stack.length === 0 ? args : stack[stack.length-1].tokens;
+ const cmd_node = this.consolidateTokens(sub.tokens);
+ dest.push(cmd_node);
+ continue;
+ }
+ dest.push(token);
+ }
+
+ const command = args.shift();
+
+ return {
+ $: 'command',
+ command,
+ args,
+ inputRedirects,
+ outputRedirects,
+ };
+ }
+}
+
+class MultilinePStratumImpl extends ShellConstructsPStratumImpl {
+ static states = [
+ {
+ name: 'script',
+ enter ({ node }) {
+ node.$ = 'script';
+ node.statements = [];
+ },
+ next ({ value, lexer }) {
+ if ( value.$ === 'op.line-terminator' ) {
+ lexer.next();
+ return;
+ }
+
+ this.push('pipeline');
+ }
+ },
+ ...ShellConstructsPStratumImpl.states,
+ ];
+
+ _init () {
+ this.push('script');
+ }
+}
+
+export const buildParserSecondHalf = (sp, { multiline } = {}) => {
+ const parserFactory = new ParserFactory();
+ const parserRegistry = StrataParseFacade.getDefaultParserRegistry();
+
+ const parserBuilder = new ParserBuilder(
+ parserFactory,
+ parserRegistry,
+ );
+
+ // sp.add(new ReducePrimitivesPStratumImpl());
+ if ( multiline ) {
+ console.log('USING MULTILINE');
+ sp.add(new MultilinePStratumImpl());
+ } else {
+ sp.add(new ShellConstructsPStratumImpl());
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/pipeline/Coupler.js b/packages/phoenix/src/ansi-shell/pipeline/Coupler.js
new file mode 100644
index 00000000..43ffe423
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/pipeline/Coupler.js
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class Coupler {
+ static description = `
+ Connects a read stream to a write stream.
+ Does not close the write stream when the read stream is closed.
+ `
+
+ constructor (source, target) {
+ this.source = source;
+ this.target = target;
+ this.on_ = true;
+ this.isDone = new Promise(rslv => {
+ this.resolveIsDone = rslv;
+ })
+ this.listenLoop_();
+ }
+
+ off () { this.on_ = false; }
+ on () { this.on_ = true; }
+
+ async listenLoop_ () {
+ this.active = true;
+ for (;;) {
+ const { value, done } = await this.source.read();
+ if ( done ) {
+ this.source = null;
+ this.target = null;
+ this.active = false;
+ this.resolveIsDone();
+ break;
+ }
+ if ( this.on_ ) {
+ await this.target.write(value);
+ }
+ }
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/pipeline/Pipe.js b/packages/phoenix/src/ansi-shell/pipeline/Pipe.js
new file mode 100644
index 00000000..59c9c46f
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/pipeline/Pipe.js
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class Pipe {
+ constructor () {
+ this.readableStream = new ReadableStream({
+ start: controller => {
+ this.readController = controller;
+ },
+ close: () => {
+ this.writableController.close();
+ }
+ });
+ this.writableStream = new WritableStream({
+ start: controller => {
+ this.writableController = controller;
+ },
+ write: item => {
+ this.readController.enqueue(item);
+ },
+ close: () => {
+ this.readController.close();
+ }
+ });
+ this.in = this.writableStream.getWriter();
+ this.out = this.readableStream.getReader();
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js b/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js
new file mode 100644
index 00000000..2ad93864
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { SyncLinesReader } from "../ioutil/SyncLinesReader.js";
+import { TOKENS } from "../readline/readtoken.js";
+import { ByteWriter } from "../ioutil/ByteWriter.js";
+import { Coupler } from "./Coupler.js";
+import { CommandStdinDecorator } from "./iowrappers.js";
+import { Pipe } from "./Pipe.js";
+import { MemReader } from "../ioutil/MemReader.js";
+import { MemWriter } from "../ioutil/MemWriter.js";
+import { MultiWriter } from "../ioutil/MultiWriter.js";
+import { NullifyWriter } from "../ioutil/NullifyWriter.js";
+import { ConcreteSyntaxError } from "../ConcreteSyntaxError.js";
+import { SignalReader } from "../ioutil/SignalReader.js";
+import { Exit } from "../../puter-shell/coreutils/coreutil_lib/exit.js";
+import { resolveRelativePath } from '../../util/path.js';
+import { printUsage } from '../../puter-shell/coreutils/coreutil_lib/help.js';
+
+class Token {
+ static createFromAST (ctx, ast) {
+ if ( ast.$ !== 'token' ) {
+ throw new Error('expected token node');
+ }
+
+ console.log('ast has cst?',
+ ast,
+ ast.components?.[0]?.$cst
+ )
+
+ return new Token(ast);
+ }
+ constructor (ast) {
+ this.ast = ast;
+ this.$cst = ast.components?.[0]?.$cst;
+ }
+ maybeStaticallyResolve (ctx) {
+ // If the only components are of type 'symbol' and 'string.segment'
+ // then we can statically resolve the value of the token.
+
+ console.log('checking viability of static resolve', this.ast)
+
+ const isStatic = this.ast.components.every(c => {
+ return c.$ === 'symbol' || c.$ === 'string.segment';
+ });
+
+ if ( ! isStatic ) return;
+
+ console.log('doing static thing', this.ast)
+
+ // TODO: Variables can also be statically resolved, I think...
+ let value = '';
+ for ( const component of this.ast.components ) {
+ console.log('component', component);
+ value += component.text;
+ }
+
+ return value;
+ }
+
+ async resolve (ctx) {
+ let value = '';
+ for ( const component of this.ast.components ) {
+ if ( component.$ === 'string.segment' || component.$ === 'symbol' ) {
+ value += component.text;
+ continue;
+ }
+ if ( component.$ === 'pipeline' ) {
+ const pipeline = await Pipeline.createFromAST(ctx, component);
+ const memWriter = new MemWriter();
+ const cmdCtx = { externs: { out: memWriter } }
+ const subCtx = ctx.sub(cmdCtx);
+ await pipeline.execute(subCtx);
+ value += memWriter.getAsString().trimEnd();
+ continue;
+ }
+ }
+ // const name_subst = await PreparedCommand.createFromAST(this.ctx, command);
+ // const memWriter = new MemWriter();
+ // const cmdCtx = { externs: { out: memWriter } }
+ // const ctx = this.ctx.sub(cmdCtx);
+ // name_subst.setContext(ctx);
+ // await name_subst.execute();
+ // const cmd = memWriter.getAsString().trimEnd();
+ return value;
+ }
+}
+
+export class PreparedCommand {
+ static async createFromAST (ctx, ast) {
+ if ( ast.$ !== 'command' ) {
+ throw new Error('expected command node');
+ }
+
+ ast = { ...ast };
+ const command_token = Token.createFromAST(ctx, ast.tokens.shift());
+
+
+ // TODO: check that node for command name is of a
+ // supported type - maybe use adapt pattern
+ console.log('ast?', ast);
+ const cmd = command_token.maybeStaticallyResolve(ctx);
+
+ const { commands } = ctx.registries;
+ const { commandProvider } = ctx.externs;
+
+ const command = cmd
+ ? await commandProvider.lookup(cmd, { ctx })
+ : command_token;
+
+ if ( command === undefined ) {
+ console.log('command token?', command_token);
+ throw new ConcreteSyntaxError(
+ `no command: ${JSON.stringify(cmd)}`,
+ command_token.$cst,
+ );
+ throw new Error('no command: ' + JSON.stringify(cmd));
+ }
+
+ // TODO: test this
+ console.log('ast?', ast);
+ const inputRedirect = ast.inputRedirects.length > 0 ? (() => {
+ const token = Token.createFromAST(ctx, ast.inputRedirects[0]);
+ return token.maybeStaticallyResolve(ctx) ?? token;
+ })() : null;
+ // TODO: test this
+ const outputRedirects = ast.outputRedirects.map(rdirNode => {
+ const token = Token.createFromAST(ctx, rdirNode);
+ return token.maybeStaticallyResolve(ctx) ?? token;
+ });
+
+ return new PreparedCommand({
+ command,
+ args: ast.tokens.map(node => Token.createFromAST(ctx, node)),
+ // args: ast.args.map(node => node.text),
+ inputRedirect,
+ outputRedirects,
+ });
+ }
+
+ constructor ({ command, args, inputRedirect, outputRedirects }) {
+ this.command = command;
+ this.args = args;
+ this.inputRedirect = inputRedirect;
+ this.outputRedirects = outputRedirects;
+ }
+
+ setContext (ctx) {
+ this.ctx = ctx;
+ }
+
+ async execute () {
+ let { command, args } = this;
+
+ // If we have an AST node of type `command` it means we
+ // need to run that command to get the name of the
+ // command to run.
+ if ( command instanceof Token ) {
+ const cmd = await command.resolve(this.ctx);
+ console.log('RUNNING CMD?', cmd)
+ const { commandProvider } = this.ctx.externs;
+ command = await commandProvider.lookup(cmd, { ctx: this.ctx });
+ if ( command === undefined ) {
+ throw new Error('no command: ' + JSON.stringify(cmd));
+ }
+ }
+
+ args = await Promise.all(args.map(async node => {
+ if ( node instanceof Token ) {
+ return await node.resolve(this.ctx);
+ }
+
+ return node.text;
+ }));
+
+ const { argparsers } = this.ctx.registries;
+ const { decorators } = this.ctx.registries;
+
+ let in_ = this.ctx.externs.in_;
+ if ( this.inputRedirect ) {
+ const { filesystem } = this.ctx.platform;
+ const dest_path = this.inputRedirect instanceof Token
+ ? await this.inputRedirect.resolve(this.ctx)
+ : this.inputRedirect;
+ const response = await filesystem.read(
+ resolveRelativePath(this.ctx.vars, dest_path));
+ in_ = new MemReader(response);
+ }
+
+ // simple naive implementation for now
+ const sig = {
+ listeners_: [],
+ emit (signal) {
+ for ( const listener of this.listeners_ ) {
+ listener(signal);
+ }
+ },
+ on (listener) {
+ this.listeners_.push(listener);
+ }
+ };
+
+ in_ = new SignalReader({ delegate: in_, sig });
+
+ if ( command.input?.syncLines ) {
+ in_ = new SyncLinesReader({ delegate: in_ });
+ }
+ in_ = new CommandStdinDecorator(in_);
+
+ let out = this.ctx.externs.out;
+ const outputMemWriters = [];
+ if ( this.outputRedirects.length > 0 ) {
+ for ( let i=0 ; i < this.outputRedirects.length ; i++ ) {
+ outputMemWriters.push(new MemWriter());
+ }
+ out = new NullifyWriter({ delegate: out });
+ out = new MultiWriter({
+ delegates: [...outputMemWriters, out],
+ });
+ }
+
+ const ctx = this.ctx.sub({
+ externs: {
+ in_,
+ out,
+ sig,
+ },
+ cmdExecState: {
+ valid: true,
+ printHelpAndExit: false,
+ },
+ locals: {
+ command,
+ args,
+ outputIsRedirected: this.outputRedirects.length > 0,
+ }
+ });
+
+ if ( command.args ) {
+ const argProcessorId = command.args.$;
+ const argProcessor = argparsers[argProcessorId];
+ const spec = { ...command.args };
+ delete spec.$;
+ await argProcessor.process(ctx, spec);
+ }
+
+ if ( ! ctx.cmdExecState.valid ) {
+ ctx.locals.exit = -1;
+ await ctx.externs.out.close();
+ return;
+ }
+
+ if ( ctx.cmdExecState.printHelpAndExit ) {
+ ctx.locals.exit = 0;
+ await printUsage(command, ctx.externs.out, ctx.vars);
+ await ctx.externs.out.close();
+ return;
+ }
+
+ let execute = command.execute.bind(command);
+ if ( command.decorators ) {
+ for ( const decoratorId in command.decorators ) {
+ const params = command.decorators[decoratorId];
+ const decorator = decorators[decoratorId];
+ execute = decorator.decorate(execute, {
+ command, params, ctx
+ });
+ }
+ }
+
+ // FIXME: This is really sketchy...
+ // `await execute(ctx);` should automatically throw any promise rejections,
+ // but for some reason Node crashes first, unless we set this handler,
+ // EVEN IF IT DOES NOTHING. I also can't find a place to safely remove it,
+ // so apologies if it makes debugging promises harder.
+ if (ctx.platform.name === 'node') {
+ const rejectionCatcher = (reason, promise) => {
+ };
+ process.on('unhandledRejection', rejectionCatcher);
+ }
+
+ let exit_code = 0;
+ try {
+ await execute(ctx);
+ } catch (e) {
+ if ( e instanceof Exit ) {
+ exit_code = e.code;
+ } else if ( e.code ) {
+ await ctx.externs.err.write(
+ '\x1B[31;1m' +
+ command.name + ': ' +
+ e.message + '\x1B[0m\n'
+ );
+ } else {
+ await ctx.externs.err.write(
+ '\x1B[31;1m' +
+ command.name + ': ' +
+ e.toString() + '\x1B[0m\n'
+ );
+ ctx.locals.exit = -1;
+ }
+ }
+
+ // ctx.externs.in?.close?.();
+ // ctx.externs.out?.close?.();
+ await ctx.externs.out.close();
+
+ // TODO: need write command from puter-shell before this can be done
+ for ( let i=0 ; i < this.outputRedirects.length ; i++ ) {
+ console.log('output redirect??', this.outputRedirects[i]);
+ const { filesystem } = this.ctx.platform;
+ const outputRedirect = this.outputRedirects[i];
+ const dest_path = outputRedirect instanceof Token
+ ? await outputRedirect.resolve(this.ctx)
+ : outputRedirect;
+ const path = resolveRelativePath(ctx.vars, dest_path);
+ console.log('it should work?', {
+ path,
+ outputMemWriters,
+ })
+ // TODO: error handling here
+
+ await filesystem.write(path, outputMemWriters[i].getAsBlob());
+ }
+
+ console.log('OUTPUT WRITERS', outputMemWriters);
+ }
+}
+
+export class Pipeline {
+ static async createFromAST (ctx, ast) {
+ if ( ast.$ !== 'pipeline' ) {
+ throw new Error('expected pipeline node');
+ }
+
+ const preparedCommands = [];
+
+ for ( const cmdNode of ast.commands ) {
+ const command = await PreparedCommand.createFromAST(ctx, cmdNode);
+ preparedCommands.push(command);
+ }
+
+ return new Pipeline({ preparedCommands });
+ }
+ constructor ({ preparedCommands }) {
+ this.preparedCommands = preparedCommands;
+ }
+ async execute (ctx) {
+ const preparedCommands = this.preparedCommands;
+
+ let nextIn = ctx.externs.in;
+ let lastPipe = null;
+
+ // TOOD: this will eventually defer piping of certain
+ // sub-pipelines to the Puter Shell.
+
+ for ( let i=0 ; i < preparedCommands.length ; i++ ) {
+ const command = preparedCommands[i];
+
+ // if ( command.command.input?.syncLines ) {
+ // nextIn = new SyncLinesReader({ delegate: nextIn });
+ // }
+
+ const cmdCtx = { externs: { in_: nextIn } };
+
+ const pipe = new Pipe();
+ lastPipe = pipe;
+ let cmdOut = pipe.in;
+ cmdOut = new ByteWriter({ delegate: cmdOut });
+ cmdCtx.externs.out = cmdOut;
+ cmdCtx.externs.commandProvider = ctx.externs.commandProvider;
+ nextIn = pipe.out;
+
+ // TODO: need to consider redirect from out to err
+ cmdCtx.externs.err = ctx.externs.out;
+ command.setContext(ctx.sub(cmdCtx));
+ }
+
+
+ const coupler = new Coupler(lastPipe.out, ctx.externs.out);
+
+ const commandPromises = [];
+ for ( let i = preparedCommands.length - 1 ; i >= 0 ; i-- ) {
+ const command = preparedCommands[i];
+ commandPromises.push(command.execute());
+ }
+ await Promise.all(commandPromises);
+ console.log('PIPELINE DONE');
+
+ await coupler.isDone;
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/pipeline/iowrappers.js b/packages/phoenix/src/ansi-shell/pipeline/iowrappers.js
new file mode 100644
index 00000000..fc4cf49c
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/pipeline/iowrappers.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class CommandStdinDecorator {
+ constructor (rs) {
+ this.rs = rs;
+ }
+ async read (...a) {
+ return await this.rs.read(...a);
+ }
+
+ // utility methods
+ async collect () {
+ const items = [];
+ for (;;) {
+ const { value, done } = await this.rs.read();
+ if ( done ) return items;
+ items.push(value);
+ }
+ }
+}
+
+export class CommandStdoutDecorator {
+ constructor (delegate) {
+ this.delegate = delegate;
+ }
+ async write (...a) {
+ return await this.delegate.write(...a);
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/readline/history.js b/packages/phoenix/src/ansi-shell/readline/history.js
new file mode 100644
index 00000000..f04b3a36
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/readline/history.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class HistoryManager {
+ constructor({ enableLogging = false } = {}) {
+ this.items = [];
+ this.index_ = 0;
+ this.listeners_ = {};
+ this.enableLogging_ = enableLogging;
+ }
+
+ log(...a) {
+ // TODO: Command line option for configuring logging
+ if ( this.enableLogging_ ) {
+ console.log('[HistoryManager]', ...a);
+ }
+ }
+
+ get index() {
+ return this.index_;
+ }
+
+ set index(v) {
+ this.log('setting index', v);
+ this.index_ = v;
+ }
+
+ get() {
+ return this.items[this.index];
+ }
+
+ // Save, overwriting the current history item
+ save(data, { opt_debug } = {}) {
+ this.log('saving', data, 'at', this.index,
+ ...(opt_debug ? [ 'from', opt_debug ] : []));
+ this.items[this.index] = data;
+
+ if (this.listeners_.hasOwnProperty('add')) {
+ for (const listener of this.listeners_.add) {
+ listener(data);
+ }
+ }
+ }
+
+ append(data) {
+ if (
+ this.items.length !== 0 &&
+ this.index !== this.items.length
+ ) {
+ this.log('POP');
+ // remove last item
+ this.items.pop();
+ }
+ this.index = this.items.length;
+ this.save(data, { opt_debug: 'append' });
+ this.index++;
+ }
+
+ on(topic, listener) {
+ if (!this.listeners_.hasOwnProperty(topic)) {
+ this.listeners_[topic] = [];
+ }
+ this.listeners_[topic].push(listener);
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/readline/readline.js b/packages/phoenix/src/ansi-shell/readline/readline.js
new file mode 100644
index 00000000..e7f816d2
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/readline/readline.js
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Context } from '../../context/context.js';
+import { CommandCompleter } from '../../puter-shell/completers/command_completer.js';
+import { FileCompleter } from '../../puter-shell/completers/file_completer.js';
+import { OptionCompleter } from '../../puter-shell/completers/option_completer.js';
+import { Uint8List } from '../../util/bytes.js';
+import { StatefulProcessorBuilder } from '../../util/statemachine.js';
+import { ANSIContext } from '../ANSIContext.js';
+import { readline_comprehend } from './rl_comprehend.js';
+import { CSI_HANDLERS } from './rl_csi_handlers.js';
+import { HistoryManager } from './history.js';
+
+const decoder = new TextDecoder();
+
+const cc = chr => chr.charCodeAt(0);
+
+const ReadlineProcessorBuilder = builder => builder
+ // TODO: import these constants from a package
+ .installContext(ANSIContext)
+ .installContext(new Context({
+ variables: {
+ result: { value: '' },
+ cursor: { value: 0 },
+ },
+ // TODO: dormant configuration; waiting on ContextSignature
+ imports: {
+ out: {},
+ in_: {},
+ history: {}
+ }
+ }))
+ .variable('result', { getDefaultValue: () => '' })
+ .variable('cursor', { getDefaultValue: () => 0 })
+ .external('out', { required: true })
+ .external('in_', { required: true })
+ .external('history', { required: true })
+ .external('prompt', { required: true })
+ .external('commandCtx', { required: true })
+ .beforeAll('get-byte', async ctx => {
+ const { locals, externs } = ctx;
+
+ const byteBuffer = new Uint8Array(1);
+ await externs.in_.read(byteBuffer);
+ locals.byteBuffer = byteBuffer;
+ locals.byte = byteBuffer[0];
+ })
+ .state('start', async ctx => {
+ const { consts, vars, externs, locals } = ctx;
+
+ if ( locals.byte === consts.CHAR_LF || locals.byte === consts.CHAR_CR ) {
+ externs.out.write('\n');
+ ctx.setState('end');
+ return;
+ }
+
+ if ( locals.byte === consts.CHAR_ETX ) {
+ externs.out.write('^C\n');
+ // Exit if input line is empty
+ // FIXME: Check for 'process' is so we only do this on Node. How should we handle exiting in Puter terminal?
+ if ( typeof process !== 'undefined' && ctx.vars.result.length === 0 ) {
+ process.exit(1);
+ return;
+ }
+ // Otherwise clear it
+ ctx.vars.result = '';
+ ctx.setState('end');
+ return;
+ }
+
+ if ( locals.byte === consts.CHAR_EOT ) {
+ externs.out.write('^D\n');
+ ctx.vars.result = '';
+ ctx.setState('end');
+ return;
+ }
+
+ if ( locals.byte === consts.CHAR_FF ) {
+ externs.out.write('\x1B[H\x1B[2J');
+ externs.out.write(externs.prompt);
+ externs.out.write(vars.result);
+ const invCurPos = vars.result.length - vars.cursor;
+ console.log(invCurPos)
+ if ( invCurPos !== 0 ) {
+ externs.out.write(`\x1B[${invCurPos}D`);
+ }
+ return;
+ }
+
+ if ( locals.byte === consts.CHAR_TAB ) {
+ const inputState = readline_comprehend(ctx.sub({
+ params: {
+ input: vars.result,
+ cursor: vars.cursor
+ }
+ }));
+ // NEXT: get tab completer for input state
+ console.log('input state', inputState);
+
+ let completer = null;
+ if ( inputState.$ === 'redirect' ) {
+ completer = new FileCompleter();
+ }
+
+ if ( inputState.$ === 'command' ) {
+ if ( inputState.tokens.length === 1 ) {
+ // Match first token against command names
+ completer = new CommandCompleter();
+ } else if ( inputState.input.startsWith('--') ) {
+ // Match `--*` against option names, if they exist
+ completer = new OptionCompleter();
+ } else {
+ // Match everything else against file names
+ completer = new FileCompleter();
+ }
+ }
+
+ if ( completer === null ) return;
+
+ const completions = await completer.getCompletions(
+ externs.commandCtx,
+ inputState,
+ );
+
+ const applyCompletion = txt => {
+ const p1 = vars.result.slice(0, vars.cursor);
+ const p2 = vars.result.slice(vars.cursor);
+ console.log({ p1, p2 });
+ vars.result = p1 + txt + p2;
+ vars.cursor += txt.length;
+ externs.out.write(txt);
+ };
+
+ if ( completions.length === 0 ) return;
+
+ if ( completions.length === 1 ) {
+ applyCompletion(completions[0]);
+ }
+
+ if ( completions.length > 1 ) {
+ let inCommon = '';
+ for ( let i=0 ; true ; i++ ) {
+ if ( ! completions.every(completion => {
+ return completion.length > i;
+ }) ) break;
+
+ let matches = true;
+
+ const chrFirst = completions[0][i];
+ for ( let ci=1 ; ci < completions.length ; ci++ ) {
+ const chrOther = completions[ci][i];
+ if ( chrFirst !== chrOther ) {
+ matches = false;
+ break;
+ }
+ }
+
+ if ( ! matches ) break;
+ inCommon += chrFirst;
+ }
+
+ if ( inCommon.length > 0 ) {
+ applyCompletion(inCommon);
+ }
+ }
+ return;
+ }
+
+ if ( locals.byte === consts.CHAR_ESC ) {
+ ctx.setState('ESC');
+ return;
+ }
+
+ // (note): DEL is actually the backspace key
+ // [explained here](https://en.wikipedia.org/wiki/Backspace#Common_use)
+ // TOOD: very similar to delete in CSI_HANDLERS; how can this be unified?
+ if ( locals.byte === consts.CHAR_DEL ) {
+ // can't backspace at beginning of line
+ if ( vars.cursor === 0 ) return;
+
+ vars.result = vars.result.slice(0, vars.cursor - 1) +
+ vars.result.slice(vars.cursor)
+
+ vars.cursor--;
+
+ // TODO: maybe wrap these CSI codes in a library
+ const backspaceSequence = new Uint8Array([
+ // consts.CHAR_ESC, consts.CHAR_CSI, cc('s'), // save cur
+ consts.CHAR_ESC, consts.CHAR_CSI, cc('D'), // left
+ consts.CHAR_ESC, consts.CHAR_CSI, cc('P'),
+ // consts.CHAR_ESC, consts.CHAR_CSI, cc('u'), // restore cur
+ // consts.CHAR_ESC, consts.CHAR_CSI, cc('D'), // left
+ ]);
+
+ externs.out.write(backspaceSequence);
+ return;
+ }
+
+ const part = decoder.decode(locals.byteBuffer);
+
+ if ( vars.cursor === vars.result.length ) {
+ // output
+ externs.out.write(locals.byteBuffer);
+ // update buffer
+ vars.result = vars.result + part;
+ // update cursor
+ vars.cursor += part.length;
+ } else {
+ // output
+ const insertSequence = new Uint8Array([
+ consts.CHAR_ESC,
+ consts.CHAR_CSI,
+ '@'.charCodeAt(0),
+ ...locals.byteBuffer
+ ]);
+ externs.out.write(insertSequence);
+ // update buffer
+ vars.result =
+ vars.result.slice(0, vars.cursor) +
+ part +
+ vars.result.slice(vars.cursor)
+ // update cursor
+ vars.cursor += part.length;
+ }
+ })
+ .onTransitionTo('ESC-CSI', async ctx => {
+ ctx.vars.controlSequence = new Uint8List();
+ })
+ .state('ESC', async ctx => {
+ const { consts, vars, externs, locals } = ctx;
+
+ if ( locals.byte === consts.CHAR_ESC ) {
+ externs.out.write(consts.CHAR_ESC);
+ ctx.setState('start');
+ return;
+ }
+
+ if ( locals.byte === ctx.consts.CHAR_CSI ) {
+ ctx.setState('ESC-CSI');
+ return;
+ }
+ if ( locals.byte === ctx.consts.CHAR_OSC ) {
+ ctx.setState('ESC-OSC');
+ return;
+ }
+ })
+ .state('ESC-CSI', async ctx => {
+ const { consts, locals, vars } = ctx;
+
+ if (
+ locals.byte >= consts.CSI_F_0 &&
+ locals.byte < consts.CSI_F_E
+ ) {
+ ctx.trigger('ESC-CSI.post');
+ ctx.setState('start');
+ return;
+ }
+
+ vars.controlSequence.append(locals.byte);
+ })
+ .state('ESC-OSC', async ctx => {
+ const { consts, locals, vars } = ctx;
+
+ // TODO: ESC\ can also end an OSC sequence according
+ // to sources, but this has not been implemented
+ // because it would add another state.
+ // This should be implemented when there's a
+ // simpler solution ("peek" & "scan" functionality)
+ if (
+ locals.byte === 0x07
+ ) {
+ // ctx.trigger('ESC-OSC.post');
+ ctx.setState('start');
+ return;
+ }
+
+ vars.controlSequence.append(locals.byte);
+ })
+ .action('ESC-CSI.post', async ctx => {
+ const { vars, externs, locals } = ctx;
+
+ const finalByte = locals.byte;
+ const controlSequence = vars.controlSequence.toArray();
+
+ // Log.log('controlSequence', finalByte, controlSequence);
+
+ if ( ! CSI_HANDLERS.hasOwnProperty(finalByte) ) {
+ return;
+ }
+
+ ctx.locals.controlSequence = controlSequence;
+ ctx.locals.doWrite = false;
+ CSI_HANDLERS[finalByte](ctx);
+
+ if ( ctx.locals.doWrite ) {
+ externs.out.write(new Uint8Array([
+ ctx.consts.CHAR_ESC,
+ ctx.consts.CHAR_CSI,
+ ...controlSequence,
+ finalByte
+ ]))
+ }
+ })
+ .build();
+
+const ReadlineProcessor = ReadlineProcessorBuilder(
+ new StatefulProcessorBuilder()
+);
+
+class Readline {
+ constructor (params) {
+ this.internal_ = {};
+ for ( const k in params ) this.internal_[k] = params[k];
+
+ this.history = new HistoryManager();
+ }
+
+ async readline (prompt, commandCtx) {
+ const out = this.internal_.out;
+ const in_ = this.internal_.in;
+
+ await out.write(prompt);
+
+ const {
+ result
+ } = await ReadlineProcessor.run({
+ prompt,
+ out, in_,
+ history: this.history,
+ commandCtx,
+ });
+
+ if ( result.trim() !== '' ) {
+ this.history.append(result);
+ }
+
+ return result;
+ }
+}
+
+export default class ReadlineLib {
+ static create(params) {
+ const rl = new Readline(params);
+ return rl;
+ }
+}
diff --git a/packages/phoenix/src/ansi-shell/readline/readtoken.js b/packages/phoenix/src/ansi-shell/readline/readtoken.js
new file mode 100644
index 00000000..63fbc138
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/readline/readtoken.js
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+// [reference impl](https://github.com/brgl/busybox/blob/master/shell/ash.c)
+
+const list_ws = [' ', '\n', '\t'];
+const list_recorded_tokens = [
+ '|','>','<','&',';','(',')',
+];
+const list_stoptoken = [
+ '|','>','<','&','\\','#',';','(',')',
+ ...list_ws
+];
+
+export const TOKENS = {};
+for ( const k of list_recorded_tokens ) {
+ TOKENS[k] = {};
+}
+
+export const readtoken = str => {
+ let state = null;
+ let buffer = '';
+ let quoteType = '';
+ const tokens = [];
+
+ const actions = {
+ endToken: () => {
+ tokens.push(buffer);
+ buffer = '';
+ }
+ };
+
+ const states = {
+ start: i => {
+ if ( list_ws.includes(str[i]) ) {
+ return;
+ }
+ if ( str[i] === '#' ) return str.length;
+ if ( list_recorded_tokens.includes(str[i]) ) {
+ tokens.push(TOKENS[str[i]]);
+ return;
+ }
+ if ( str[i] === '"' || str[i] === "'" ) {
+ state = states.quote;
+ quoteType = str[i];
+ return;
+ }
+ state = states.text;
+ return i; // prevent increment
+ },
+ text: i => {
+ if ( str[i] === '"' || str[i] === "'" ) {
+ state = states.quote;
+ quoteType = str[i];
+ return;
+ }
+ if ( list_stoptoken.includes(str[i]) ) {
+ state = states.start;
+ actions.endToken();
+ return i; // prevent increment
+ }
+ buffer += str[i];
+ },
+ quote: i => {
+ if ( str[i] === '\\' ) {
+ state = states.quote_esc;
+ return;
+ }
+ if ( str[i] === quoteType ) {
+ state = states.text;
+ return;
+ }
+ buffer += str[i];
+ },
+ quote_esc: i => {
+ if ( str[i] !== quoteType ) {
+ buffer += '\\';
+ }
+ buffer += str[i];
+ state = states.quote;
+ }
+ };
+ state = states.start;
+ for ( let i=0 ; i < str.length ; ) {
+ let newI = state(i);
+ i = newI !== undefined ? newI : i+1;
+ }
+
+ if ( buffer !== '' ) actions.endToken();
+
+ return tokens;
+};
\ No newline at end of file
diff --git a/packages/phoenix/src/ansi-shell/readline/rl_comprehend.js b/packages/phoenix/src/ansi-shell/readline/rl_comprehend.js
new file mode 100644
index 00000000..12ab9a23
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/readline/rl_comprehend.js
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+// This function comprehends the readline input and returns something
+// called a "readline input state" - this includes any information needed
+
+import { readtoken, TOKENS } from "./readtoken.js";
+
+// TODO: update to use syntax parser
+
+// REMINDER: input state will be sent to readline first,
+// then readline will use the input state to determine
+// what component to ask for tab completion
+
+// to perform autocomplete functions
+export const readline_comprehend = (ctx) => {
+ const { input, cursor } = ctx.params;
+
+ // TODO: CST for input tokens might be a good idea
+ // for now, tokens up to the current cursor position
+ // will be considered.
+
+ const relevantInput = input.slice(0, cursor);
+
+ const endsWithWhitespace = (() => {
+ const lastChar = relevantInput[relevantInput.length - 1];
+ return lastChar === ' ' ||
+ lastChar === '\t' ||
+ lastChar === '\r' ||
+ lastChar === '\n'
+ })();
+
+ let tokens = readtoken(relevantInput);
+ let tokensStart = 0;
+
+ // We now go backwards through the tokens, looking for:
+ // - a redirect token immediately to the left
+ // - a pipe token to the left
+
+ if ( tokens.length === 0 ) return { $: 'empty' };
+
+ // Remove tokens for previous commands
+ for ( let i=tokens.length ; i >= 0 ; i-- ) {
+ const token = tokens[i];
+ const isCommandSeparator =
+ token === TOKENS['|'] ||
+ token === TOKENS[';'] ;
+ if ( isCommandSeparator ) {
+ tokens = tokens.slice(i + 1);
+ break;
+ }
+ }
+
+ // Check if current input is for a redirect operator
+ const resultIfRedirectOperator = (() => {
+ if ( tokens.length < 1 ) return;
+
+ const lastToken = tokens[tokens.length - 1];
+ if (
+ lastToken === TOKENS['<'] ||
+ lastToken === TOKENS['>']
+ ) {
+ return {
+ $: 'redirect'
+ };
+ }
+
+ if ( tokens.length < 2 ) return;
+ if ( endsWithWhitespace ) return;
+
+ const secondFromLastToken = tokens[tokens.length - 2];
+ if (
+ secondFromLastToken === TOKENS['<'] ||
+ secondFromLastToken === TOKENS['>']
+ ) {
+ return {
+ $: 'redirect',
+ input: lastToken
+ };
+ }
+
+ })();
+
+ if ( resultIfRedirectOperator ) return resultIfRedirectOperator;
+
+ if ( tokens.length === 0 ) {
+ return { $: 'empty' };
+ }
+
+ // If the first token is not a command name, then
+ // this input is not considered comprehensible
+ if ( typeof tokens[0] !== 'string' ) {
+ return {
+ $: 'unrecognized'
+ };
+ }
+
+ // DRY: command arguments are parsed by readline
+ const argTokens = [];
+ for ( let i=0 ; i < tokens.length ; i++ ) {
+ if (
+ tokens[i] === TOKENS['<'] ||
+ tokens[i] === TOKENS['>']
+ ) {
+ // skip this token and the next one
+ i++; continue;
+ }
+
+ argTokens.push(tokens[i]);
+ }
+
+ return {
+ $: 'command',
+ id: tokens[0],
+ tokens: argTokens,
+ input: endsWithWhitespace ?
+ '' : argTokens[argTokens.length - 1],
+ endsWithWhitespace,
+ };
+};
diff --git a/packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js b/packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js
new file mode 100644
index 00000000..1128929b
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+/*
+## this source file
+- maps: CSI (Control Sequence Introducer) sequences
+- to: expected functionality in the context of readline
+
+## relevant articles
+- [ECMA-48](https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf)
+- [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code)
+*/
+
+import { ANSIContext, getActiveModifiersFromXTerm } from "../ANSIContext.js";
+import { findNextWord } from "./rl_words.js";
+
+// TODO: potentially include metadata in handlers
+
+// --- util ---
+const cc = chr => chr.charCodeAt(0);
+
+const CHAR_DEL = 127;
+const CHAR_ESC = 0x1B;
+
+const { consts } = ANSIContext;
+
+// --- convenience function decorators ---
+const CSI_INT_ARG = delegate => ctx => {
+ const controlSequence = ctx.locals.controlSequence;
+
+ let str = new TextDecoder().decode(controlSequence);
+
+ // Detection of modifier keys like ctrl and shift
+ if ( str.includes(';') ) {
+ const parts = str.split(';');
+ str = parts[0];
+ const modsStr = parts[parts.length - 1];
+ let modN = Number.parseInt(modsStr);
+ const mods = getActiveModifiersFromXTerm(modN);
+ for ( const k in mods ) ctx.locals[k] = mods[k];
+ }
+
+ let num = str === '' ? 1 : Number.parseInt(str);
+ if ( Number.isNaN(num) ) num = 0;
+
+ ctx.locals.num = num;
+
+ return delegate(ctx);
+};
+
+// --- PC-Style Function Key handles (see `~` final byte in CSI_HANDLERS) ---
+export const PC_FN_HANDLERS = {
+ // delete key
+ 3: ctx => {
+ const { vars } = ctx;
+ const deleteSequence = new Uint8Array([
+ consts.CHAR_ESC, consts.CHAR_CSI, cc('P')
+ ]);
+ vars.result = vars.result.slice(0, vars.cursor) +
+ vars.result.slice(vars.cursor + 1);
+ ctx.externs.out.write(deleteSequence);
+ }
+};
+
+const save_history = ctx => {
+ const { history } = ctx.externs;
+ history.save(ctx.vars.result);
+};
+
+const correct_cursor = (ctx, oldCursor) => {
+ // TODO: make this work differently if oldCursor is not defined
+
+ const amount = ctx.vars.cursor - oldCursor;
+ ctx.vars.cursor = ctx.vars.result.length;
+ const L = amount < 0 ? 'D' : 'C';
+ if ( amount === 0 ) return;
+ const moveSequence = new Uint8Array([
+ consts.CHAR_ESC, consts.CHAR_CSI,
+ ...(new TextEncoder().encode('' + Math.abs(amount))),
+ cc(L)
+ ]);
+ ctx.externs.out.write(moveSequence);
+};
+
+const home = ctx => {
+ const amount = ctx.vars.cursor;
+ ctx.vars.cursor = 0;
+ const moveSequence = new Uint8Array([
+ consts.CHAR_ESC, consts.CHAR_CSI,
+ ...(new TextEncoder().encode('' + amount)),
+ cc('D')
+ ]);
+ if ( amount !== 0 ) ctx.externs.out.write(moveSequence);
+};
+
+const select_current_history = ctx => {
+ const { history } = ctx.externs;
+ home(ctx);
+ ctx.vars.result = history.get();
+ ctx.vars.cursor = ctx.vars.result.length;
+ const clearToEndSequence = new Uint8Array([
+ consts.CHAR_ESC, consts.CHAR_CSI,
+ ...(new TextEncoder().encode('0')),
+ cc('K')
+ ]);
+ ctx.externs.out.write(clearToEndSequence);
+ ctx.externs.out.write(history.get());
+};
+
+// --- CSI handlers: this is the last definition in this file ---
+export const CSI_HANDLERS = {
+ [cc('A')]: CSI_INT_ARG(ctx => {
+ save_history(ctx);
+ const { history } = ctx.externs;
+
+ if ( history.index === 0 ) return;
+
+ history.index--;
+ select_current_history(ctx);
+ }),
+ [cc('B')]: CSI_INT_ARG(ctx => {
+ save_history(ctx);
+ const { history } = ctx.externs;
+
+ if ( history.index === history.items.length - 1 ) return;
+
+ history.index++;
+ select_current_history(ctx);
+ }),
+ // cursor back
+ [cc('D')]: CSI_INT_ARG(ctx => {
+ if ( ctx.vars.cursor === 0 ) {
+ return;
+ }
+ if ( ctx.locals.ctrl ) {
+ // TODO: temporary inaccurate implementation
+ const txt = ctx.vars.result;
+ const ind = findNextWord(txt, ctx.vars.cursor, true);
+ const diff = ctx.vars.cursor - ind;
+ ctx.vars.cursor = ind;
+ const moveSequence = new Uint8Array([
+ consts.CHAR_ESC, consts.CHAR_CSI,
+ ...(new TextEncoder().encode('' + diff)),
+ cc('D')
+ ]);
+ ctx.externs.out.write(moveSequence);
+ return;
+ }
+ ctx.vars.cursor -= ctx.locals.num;
+ ctx.locals.doWrite = true;
+ }),
+ // cursor forward
+ [cc('C')]: CSI_INT_ARG(ctx => {
+ if ( ctx.vars.cursor >= ctx.vars.result.length ) {
+ return;
+ }
+ if ( ctx.locals.ctrl ) {
+ // TODO: temporary inaccurate implementation
+ const txt = ctx.vars.result;
+ const ind = findNextWord(txt, ctx.vars.cursor);
+ const diff = ind - ctx.vars.cursor;
+ ctx.vars.cursor = ind;
+ const moveSequence = new Uint8Array([
+ consts.CHAR_ESC, consts.CHAR_CSI,
+ ...(new TextEncoder().encode('' + diff)),
+ cc('C')
+ ]);
+ ctx.externs.out.write(moveSequence);
+ return;
+ }
+ ctx.vars.cursor += ctx.locals.num;
+ ctx.locals.doWrite = true;
+ }),
+ // PC-Style Function Keys
+ [cc('~')]: CSI_INT_ARG(ctx => {
+ if ( ! PC_FN_HANDLERS.hasOwnProperty(ctx.locals.num) ) {
+ console.error(`unrecognized PC Function: ${ctx.locals.num}`);
+ return;
+ }
+ PC_FN_HANDLERS[ctx.locals.num](ctx);
+ }),
+ // Home
+ [cc('H')]: ctx => {
+ home(ctx);
+ },
+ // End
+ [cc('F')]: ctx => {
+ const amount = ctx.vars.result.length - ctx.vars.cursor;
+ ctx.vars.cursor = ctx.vars.result.length;
+ const moveSequence = new Uint8Array([
+ consts.CHAR_ESC, consts.CHAR_CSI,
+ ...(new TextEncoder().encode('' + amount)),
+ cc('C')
+ ]);
+ if ( amount !== 0 ) ctx.externs.out.write(moveSequence);
+ },
+};
diff --git a/packages/phoenix/src/ansi-shell/readline/rl_words.js b/packages/phoenix/src/ansi-shell/readline/rl_words.js
new file mode 100644
index 00000000..10469852
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/readline/rl_words.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const findNextWord = (str, from, reverse) => {
+ let stage = 0;
+ let incr = reverse ? -1 : 1;
+ const cond = reverse ? i => i > 0 : i => i < str.length;
+ if ( reverse && from !== 0 ) from--;
+ for ( let i=from ; cond(i) ; i += incr ) {
+ if ( stage === 0 ) {
+ if ( str[i] !== ' ' ) stage++;
+ continue;
+ }
+ if ( stage === 1 ) {
+ if ( str[i] === ' ' ) return reverse ? i + 1 : i;
+ }
+ }
+ return reverse ? 0 : str.length;
+}
diff --git a/packages/phoenix/src/ansi-shell/signals.js b/packages/phoenix/src/ansi-shell/signals.js
new file mode 100644
index 00000000..b81253fe
--- /dev/null
+++ b/packages/phoenix/src/ansi-shell/signals.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const signals = Object.freeze({
+ SIGINT: 2,
+ SIGQUIT: 3,
+});
diff --git a/packages/phoenix/src/context/context.js b/packages/phoenix/src/context/context.js
new file mode 100644
index 00000000..aaf3868b
--- /dev/null
+++ b/packages/phoenix/src/context/context.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class AbstractContext {
+ get constants () {
+ return this.instance_.constants;
+ }
+ get consts () {
+ return this.constants;
+ }
+ get variables () {
+ return this.instance_.valuesAccessor;
+ }
+ get vars () {
+ return this.variables;
+ }
+}
+
+// export class SubContext extends AbstractContext {
+// constructor ({ parent, changes }) {
+// for ( const k in parent.spec )
+// }
+// }
+
+export class Context extends AbstractContext {
+ constructor (spec) {
+ super();
+ this.spec = { ...spec };
+
+ this.instance_ = {};
+
+ if ( ! spec.constants ) spec.constants = {};
+
+ const constants = {};
+ for ( const k in this.spec.constants ) {
+ Object.defineProperty(constants, k, {
+ value: this.spec.constants[k],
+ enumerable: true
+ })
+ }
+ this.instance_.constants = constants;
+
+ // const values = {};
+ // for ( const k in this.spec.variables ) {
+ // Object.defineProperty(values, k, {
+ // value: this.spec.variables[k],
+ // enumerable: true,
+ // writable: true
+ // });
+ // }
+ // this.instance_.values = values;
+ }
+}
diff --git a/packages/phoenix/src/main_cli.js b/packages/phoenix/src/main_cli.js
new file mode 100644
index 00000000..9f3106c3
--- /dev/null
+++ b/packages/phoenix/src/main_cli.js
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Context } from 'contextlink';
+import { launchPuterShell } from './puter-shell/main.js';
+import { NodeStdioPTT } from './pty/NodeStdioPTT.js';
+import { CreateFilesystemProvider } from './platform/node/filesystem.js';
+import { CreateEnvProvider } from './platform/node/env.js';
+import { parseArgs } from '@pkgjs/parseargs';
+import capcon from 'capture-console';
+import fs from 'fs';
+
+const { values } = parseArgs({
+ options: {
+ 'log': {
+ type: 'string',
+ }
+ },
+ args: process.argv.slice(2),
+});
+const logFile = await (async () => {
+ if (!values.log)
+ return;
+ return await fs.promises.open(values.log, 'w');
+})();
+
+
+// Capture console.foo() output and either send it to the log file, or to nowhere.
+for (const [name, oldMethod] of Object.entries(console)) {
+ console[name] = async (...args) => {
+ let result;
+ const stdio = capcon.interceptStdio(() => {
+ result = oldMethod(...args);
+ });
+
+ if (logFile) {
+ await logFile.write(stdio.stdout);
+ await logFile.write(stdio.stderr);
+ }
+
+ return result;
+ };
+}
+
+const ctx = new Context({
+ ptt: new NodeStdioPTT(),
+ config: {},
+ platform: new Context({
+ name: 'node',
+ filesystem: CreateFilesystemProvider(),
+ env: CreateEnvProvider(),
+ }),
+});
+
+await launchPuterShell(ctx);
diff --git a/packages/phoenix/src/main_puter.js b/packages/phoenix/src/main_puter.js
new file mode 100644
index 00000000..a7449c45
--- /dev/null
+++ b/packages/phoenix/src/main_puter.js
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Context } from 'contextlink';
+import { launchPuterShell } from './puter-shell/main.js';
+import { CreateFilesystemProvider } from './platform/puter/filesystem.js';
+import { CreateDriversProvider } from './platform/puter/drivers.js';
+import { XDocumentPTT } from './pty/XDocumentPTT.js';
+import { CreateEnvProvider } from './platform/puter/env.js';
+
+window.main_shell = async () => {
+ const config = {};
+
+ let resolveConfigured = null;
+ const configured_ = new Promise(rslv => {
+ resolveConfigured = rslv;
+ });
+
+ const terminal = puter.ui.parentApp();
+ if (!terminal) {
+ console.error('Phoenix cannot run without a parent Terminal. Exiting...');
+ puter.exit();
+ return;
+ }
+ terminal.on('message', message => {
+ if (message.$ === 'config') {
+ const configValues = { ...message };
+ delete configValues.$;
+ for ( const k in configValues ) {
+ config[k] = configValues[k];
+ }
+ resolveConfigured();
+ }
+ });
+ terminal.on('close', () => {
+ console.log('Terminal closed; exiting Phoenix...');
+ puter.exit();
+ });
+
+ // FIXME: on terminal close, close ourselves
+
+ terminal.postMessage({ $: 'ready' });
+
+ await configured_;
+
+ const puterSDK = globalThis.puter;
+ if ( config['puter.auth.token'] ) {
+ await puterSDK.setAuthToken(config['puter.auth.token']);
+ }
+ await puterSDK.setAPIOrigin(config['puter.api_origin']);
+
+ const ptt = new XDocumentPTT(terminal);
+ await launchPuterShell(new Context({
+ ptt,
+ config, puterSDK,
+ externs: new Context({ puterSDK }),
+ platform: new Context({
+ name: 'puter',
+ filesystem: CreateFilesystemProvider({ puterSDK }),
+ drivers: CreateDriversProvider({ puterSDK }),
+ env: CreateEnvProvider({ config }),
+ }),
+ }));
+};
diff --git a/packages/phoenix/src/meta/versions.js b/packages/phoenix/src/meta/versions.js
new file mode 100644
index 00000000..cd83eacf
--- /dev/null
+++ b/packages/phoenix/src/meta/versions.js
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const SHELL_VERSIONS = [
+ {
+ v: '0.2.4',
+ changes: [
+ 'more completers for tab-completion',
+ 'help updates',
+ '"which" command added',
+ '"date" command added',
+ 'improvements when running under node.js',
+ ]
+ },
+ {
+ v: '0.2.3',
+ changes: [
+ '"printf" command added',
+ '"help" command updated',
+ '"errno" command added',
+ 'POSIX error code associations added',
+ ]
+ },
+ {
+ v: '0.2.2',
+ changes: [
+ 'wc works with BLOB inputs',
+ '"~" path resolution fixed',
+ '"head" command added',
+ '"tail" command updated',
+ '"ls" symlink support improved',
+ '"sort" command added',
+ 'Testing improved',
+ '"cd" with no arguments works',
+ 'Filesystem errors are more consistent',
+ '"help" output improved',
+ '"pwd" argument processing updated'
+
+ ]
+ },
+ {
+ v: '0.2.1',
+ changes: [
+ 'commands: true, false',
+ 'commands: basename, dirname',
+ 'more node.js support',
+ 'wc command',
+ 'sleep command',
+ 'improved coreutils documentation',
+ 'updates to existing coreutils',
+ 'readline fixes',
+ ]
+ },
+ {
+ v: '0.2.0',
+ changes: [
+ 'brand change: Phoenix Shell',
+ 'open-sourced under AGPL-3.0',
+ 'new commands: ai, txt2img, jq, and more',
+ 'added login command',
+ 'coreutils updates',
+ 'added command substitution',
+ 'parser improvements',
+ ]
+ },
+ {
+ v: '0.1.10',
+ changes: [
+ 'new input parser',
+ 'add pwd command',
+ ]
+ },
+ {
+ v: '0.1.9',
+ changes: [
+ 'add help command',
+ 'add changelog command',
+ 'add ioctl messages for window size',
+ 'add env.ROWS and env.COLS',
+ ]
+ },
+ {
+ v: '0.1.8',
+ changes: [
+ 'add neofetch command',
+ 'add simple tab completion',
+ ]
+ },
+ {
+ v: '0.1.7',
+ changes: [
+ 'add clear and printenv',
+ ]
+ },
+ {
+ v: '0.1.6',
+ changes: [
+ 'add redirect syntax',
+ ],
+ },
+ {
+ v: '0.1.5',
+ changes: [
+ 'add cp command',
+ ],
+ },
+ {
+ v: '0.1.4',
+ changes: [
+ 'improve error handling',
+ ],
+ },
+ {
+ v: '0.1.3',
+ changes: [
+ 'fixes for existing commands',
+ 'mv added',
+ 'cat added',
+ 'readline history (transient) added',
+ ]
+ },
+ {
+ v: '0.1.2',
+ changes: [
+ 'add echo',
+ 'fix synchronization of pipe coupler',
+ ]
+ }
+];
diff --git a/packages/phoenix/src/platform/PosixError.js b/packages/phoenix/src/platform/PosixError.js
new file mode 100644
index 00000000..0fc136a5
--- /dev/null
+++ b/packages/phoenix/src/platform/PosixError.js
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const ErrorCodes = {
+ EACCES: Symbol.for('EACCES'),
+ EADDRINUSE: Symbol.for('EADDRINUSE'),
+ ECONNREFUSED: Symbol.for('ECONNREFUSED'),
+ ECONNRESET: Symbol.for('ECONNRESET'),
+ EEXIST: Symbol.for('EEXIST'),
+ EFBIG: Symbol.for('EFBIG'),
+ EINVAL: Symbol.for('EINVAL'),
+ EIO: Symbol.for('EIO'),
+ EISDIR: Symbol.for('EISDIR'),
+ EMFILE: Symbol.for('EMFILE'),
+ ENOENT: Symbol.for('ENOENT'),
+ ENOSPC: Symbol.for('ENOSPC'),
+ ENOTDIR: Symbol.for('ENOTDIR'),
+ ENOTEMPTY: Symbol.for('ENOTEMPTY'),
+ EPERM: Symbol.for('EPERM'),
+ EPIPE: Symbol.for('EPIPE'),
+ ETIMEDOUT: Symbol.for('ETIMEDOUT'),
+};
+
+// Codes taken from `errno` on Linux.
+export const ErrorMetadata = new Map([
+ [ErrorCodes.EPERM, { code: 1, description: 'Operation not permitted' }],
+ [ErrorCodes.ENOENT, { code: 2, description: 'File or directory not found' }],
+ [ErrorCodes.EIO, { code: 5, description: 'IO error' }],
+ [ErrorCodes.EACCES, { code: 13, description: 'Permission denied' }],
+ [ErrorCodes.EEXIST, { code: 17, description: 'File already exists' }],
+ [ErrorCodes.ENOTDIR, { code: 20, description: 'Is not a directory' }],
+ [ErrorCodes.EISDIR, { code: 21, description: 'Is a directory' }],
+ [ErrorCodes.EINVAL, { code: 22, description: 'Argument invalid' }],
+ [ErrorCodes.EMFILE, { code: 24, description: 'Too many open files' }],
+ [ErrorCodes.EFBIG, { code: 27, description: 'File too big' }],
+ [ErrorCodes.ENOSPC, { code: 28, description: 'Device out of space' }],
+ [ErrorCodes.EPIPE, { code: 32, description: 'Pipe broken' }],
+ [ErrorCodes.ENOTEMPTY, { code: 39, description: 'Directory is not empty' }],
+ [ErrorCodes.EADDRINUSE, { code: 98, description: 'Address already in use' }],
+ [ErrorCodes.ECONNRESET, { code: 104, description: 'Connection reset'}],
+ [ErrorCodes.ETIMEDOUT, { code: 110, description: 'Connection timed out' }],
+ [ErrorCodes.ECONNREFUSED, { code: 111, description: 'Connection refused' }],
+]);
+
+export const errorFromIntegerCode = (code) => {
+ for (const [errorCode, metadata] of ErrorMetadata) {
+ if (metadata.code === code) {
+ return errorCode;
+ }
+ }
+ return undefined;
+};
+
+export class PosixError extends Error {
+ // posixErrorCode can be either a string, or one of the ErrorCodes above.
+ // If message is undefined, a default message will be used.
+ constructor(posixErrorCode, message) {
+ let posixCode;
+ if (typeof posixErrorCode === 'symbol') {
+ if (ErrorCodes[Symbol.keyFor(posixErrorCode)] !== posixErrorCode) {
+ throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`);
+ }
+ posixCode = posixErrorCode;
+ } else {
+ const code = ErrorCodes[posixErrorCode];
+ if (!code) throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`);
+ posixCode = code;
+ }
+
+ super(message ?? ErrorMetadata.get(posixCode).description);
+ this.posixCode = posixCode;
+ }
+
+ //
+ // Helpers for constructing a PosixError when you don't already have an error message.
+ //
+ static AccessNotPermitted({ message, path } = {}) {
+ return new PosixError(ErrorCodes.EACCES, message ?? (path ? `Access not permitted to: '${path}'` : undefined));
+ }
+ static AddressInUse({ message, address } = {}) {
+ return new PosixError(ErrorCodes.EADDRINUSE, message ?? (address ? `Address '${address}' in use` : undefined));
+ }
+ static ConnectionRefused({ message } = {}) {
+ return new PosixError(ErrorCodes.ECONNREFUSED, message);
+ }
+ static ConnectionReset({ message } = {}) {
+ return new PosixError(ErrorCodes.ECONNRESET, message);
+ }
+ static PathAlreadyExists({ message, path } = {}) {
+ return new PosixError(ErrorCodes.EEXIST, message ?? (path ? `Path already exists: '${path}'` : undefined));
+ }
+ static FileTooLarge({ message } = {}) {
+ return new PosixError(ErrorCodes.EFBIG, message);
+ }
+ static InvalidArgument({ message } = {}) {
+ return new PosixError(ErrorCodes.EINVAL, message);
+ }
+ static IO({ message } = {}) {
+ return new PosixError(ErrorCodes.EIO, message);
+ }
+ static IsDirectory({ message, path } = {}) {
+ return new PosixError(ErrorCodes.EISDIR, message ?? (path ? `Path is directory: '${path}'` : undefined));
+ }
+ static TooManyOpenFiles({ message } = {}) {
+ return new PosixError(ErrorCodes.EMFILE, message);
+ }
+ static DoesNotExist({ message, path } = {}) {
+ return new PosixError(ErrorCodes.ENOENT, message ?? (path ? `Path not found: '${path}'` : undefined));
+ }
+ static NotEnoughSpace({ message } = {}) {
+ return new PosixError(ErrorCodes.ENOSPC, message);
+ }
+ static IsNotDirectory({ message, path } = {}) {
+ return new PosixError(ErrorCodes.ENOTDIR, message ?? (path ? `Path is not a directory: '${path}'` : undefined));
+ }
+ static DirectoryIsNotEmpty({ message, path } = {}) {
+ return new PosixError(ErrorCodes.ENOTEMPTY, message ?? (path ?`Directory is not empty: '${path}'` : undefined));
+ }
+ static OperationNotPermitted({ message } = {}) {
+ return new PosixError(ErrorCodes.EPERM, message);
+ }
+ static BrokenPipe({ message } = {}) {
+ return new PosixError(ErrorCodes.EPIPE, message);
+ }
+ static TimedOut({ message } = {}) {
+ return new PosixError(ErrorCodes.ETIMEDOUT, message);
+ }
+}
diff --git a/packages/phoenix/src/platform/node/env.js b/packages/phoenix/src/platform/node/env.js
new file mode 100644
index 00000000..58f31614
--- /dev/null
+++ b/packages/phoenix/src/platform/node/env.js
@@ -0,0 +1,20 @@
+import os from 'os';
+
+export const CreateEnvProvider = () => {
+ return {
+ getEnv: () => {
+ let env = process.env;
+ if ( ! env.PS1 ) {
+ env.PS1 = `[\\u@\\h \\w]\\$ `;
+ }
+ if ( ! env.HOSTNAME ) {
+ env.HOSTNAME = os.hostname();
+ }
+ return env;
+ },
+
+ get (k) {
+ return this.getEnv()[k];
+ }
+ }
+}
diff --git a/packages/phoenix/src/platform/node/filesystem.js b/packages/phoenix/src/platform/node/filesystem.js
new file mode 100644
index 00000000..85b9e84c
--- /dev/null
+++ b/packages/phoenix/src/platform/node/filesystem.js
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import fs from 'fs';
+import path_ from 'path';
+
+import modeString from 'fs-mode-to-string';
+import { ErrorCodes, PosixError } from '../PosixError.js';
+
+function convertNodeError(e) {
+ switch (e.code) {
+ case 'EACCES': return new PosixError(ErrorCodes.EACCES, e.message);
+ case 'EADDRINUSE': return new PosixError(ErrorCodes.EADDRINUSE, e.message);
+ case 'ECONNREFUSED': return new PosixError(ErrorCodes.ECONNREFUSED, e.message);
+ case 'ECONNRESET': return new PosixError(ErrorCodes.ECONNRESET, e.message);
+ case 'EEXIST': return new PosixError(ErrorCodes.EEXIST, e.message);
+ case 'EIO': return new PosixError(ErrorCodes.EIO, e.message);
+ case 'EISDIR': return new PosixError(ErrorCodes.EISDIR, e.message);
+ case 'EMFILE': return new PosixError(ErrorCodes.EMFILE, e.message);
+ case 'ENOENT': return new PosixError(ErrorCodes.ENOENT, e.message);
+ case 'ENOTDIR': return new PosixError(ErrorCodes.ENOTDIR, e.message);
+ case 'ENOTEMPTY': return new PosixError(ErrorCodes.ENOTEMPTY, e.message);
+ // ENOTFOUND is Node-specific. ECONNREFUSED is similar enough.
+ case 'ENOTFOUND': return new PosixError(ErrorCodes.ECONNREFUSED, e.message);
+ case 'EPERM': return new PosixError(ErrorCodes.EPERM, e.message);
+ case 'EPIPE': return new PosixError(ErrorCodes.EPIPE, e.message);
+ case 'ETIMEDOUT': return new PosixError(ErrorCodes.ETIMEDOUT, e.message);
+ }
+ // Some other kind of error
+ return e;
+}
+
+// DRY: Almost the same as puter/filesystem.js
+function wrapAPIs(apis) {
+ for (const method in apis) {
+ if (typeof apis[method] !== 'function') {
+ continue;
+ }
+ const original = apis[method];
+ apis[method] = async (...args) => {
+ try {
+ return await original(...args);
+ } catch (e) {
+ throw convertNodeError(e);
+ }
+ };
+ }
+ return apis;
+}
+
+export const CreateFilesystemProvider = () => {
+ return wrapAPIs({
+ capabilities: {
+ 'readdir.posix-mode': true,
+ },
+ readdir: async (path) => {
+ const names = await fs.promises.readdir(path);
+
+ const items = [];
+
+ const users = {};
+ const groups = {};
+
+ for ( const name of names ) {
+ const filePath = path_.join(path, name);
+ const stat = await fs.promises.lstat(filePath);
+
+ items.push({
+ name,
+ is_dir: stat.isDirectory(),
+ is_symlink: stat.isSymbolicLink(),
+ symlink_path: stat.isSymbolicLink() ? await fs.promises.readlink(filePath) : null,
+ size: stat.size,
+ modified: stat.mtimeMs / 1000,
+ created: stat.ctimeMs / 1000,
+ accessed: stat.atimeMs / 1000,
+ mode: stat.mode,
+ mode_human_readable: modeString(stat.mode),
+ uid: stat.uid,
+ gid: stat.gid,
+ });
+ }
+
+ return items;
+ },
+ stat: async (path) => {
+ const stat = await fs.promises.lstat(path);
+ const fullPath = await fs.promises.realpath(path);
+ const parsedPath = path_.parse(fullPath);
+ // TODO: Fill in more of these?
+ return {
+ id: stat.ino,
+ associated_app_id: null,
+ public_token: null,
+ file_request_token: null,
+ uid: stat.uid,
+ parent_id: null,
+ parent_uid: null,
+ is_dir: stat.isDirectory(),
+ is_public: null,
+ is_shortcut: null,
+ is_symlink: stat.isSymbolicLink(),
+ symlink_path: stat.isSymbolicLink() ? await fs.promises.readlink(path) : null,
+ sort_by: null,
+ sort_order: null,
+ immutable: null,
+ name: parsedPath.base,
+ path: fullPath,
+ dirname: parsedPath.dir,
+ dirpath: parsedPath.dir,
+ metadata: null,
+ modified: stat.mtime,
+ created: stat.birthtime,
+ accessed: stat.atime,
+ size: stat.size,
+ layout: null,
+ owner: null,
+ type: null,
+ is_empty: await (async (stat) => {
+ if (!stat.isDirectory())
+ return null;
+ const children = await fs.promises.readdir(path);
+ return children.length === 0;
+ })(stat),
+ };
+ },
+ mkdir: async (path, options = { createMissingParents: false }) => {
+ const createMissingParents = options['createMissingParents'] || false;
+ return await fs.promises.mkdir(path, { recursive: createMissingParents });
+ },
+ read: async (path) => {
+ return await fs.promises.readFile(path);
+ },
+ write: async (path, data) => {
+ if (data instanceof Blob) {
+ return await fs.promises.writeFile(path, data.stream());
+ }
+ return await fs.promises.writeFile(path, data);
+ },
+ rm: async (path, options = { recursive: false }) => {
+ const recursive = options['recursive'] || false;
+ const stat = await fs.promises.stat(path);
+
+ if ( stat.isDirectory() && ! recursive ) {
+ throw PosixError.IsDirectory({ path });
+ }
+
+ return await fs.promises.rm(path, { recursive });
+ },
+ rmdir: async (path) => {
+ const stat = await fs.promises.stat(path);
+
+ if ( !stat.isDirectory() ) {
+ throw PosixError.IsNotDirectory({ path });
+ }
+
+ return await fs.promises.rmdir(path);
+ },
+ move: async (oldPath, newPath) => {
+ let destStat = null;
+ try {
+ destStat = await fs.promises.stat(newPath);
+ } catch (e) {
+ if ( e.code !== 'ENOENT' ) throw e;
+ }
+
+ // fs.promises.rename() expects the new path to include the filename.
+ // So, if newPath is a directory, append the old filename to it to produce the target path and name.
+ if ( destStat && destStat.isDirectory() ) {
+ if ( ! newPath.endsWith('/') ) newPath += '/';
+ newPath += path_.basename(oldPath);
+ }
+
+ return await fs.promises.rename(oldPath, newPath);
+ },
+ copy: async (oldPath, newPath) => {
+ const srcStat = await fs.promises.stat(oldPath);
+ const srcIsDir = srcStat.isDirectory();
+
+ let destStat = null;
+ try {
+ destStat = await fs.promises.stat(newPath);
+ } catch (e) {
+ if ( e.code !== 'ENOENT' ) throw e;
+ }
+ const destIsDir = destStat && destStat.isDirectory();
+
+ // fs.promises.cp() is experimental, but does everything we want. Maybe implement this manually if needed.
+
+ // `dir -> file`: invalid
+ if ( srcIsDir && destStat && ! destStat.isDirectory() ) {
+ throw new PosixError(ErrorCodes.ENOTDIR, 'Cannot copy a directory into a file');
+ }
+
+ // `file -> dir`: fs.promises.cp() expects the new path to include the filename.
+ if ( ! srcIsDir && destIsDir ) {
+ if ( ! newPath.endsWith('/') ) newPath += '/';
+ newPath += path_.basename(oldPath);
+ }
+
+ return await fs.promises.cp(oldPath, newPath, { recursive: srcIsDir });
+ }
+ });
+};
diff --git a/packages/phoenix/src/pty/NodeStdioPTT.js b/packages/phoenix/src/pty/NodeStdioPTT.js
new file mode 100644
index 00000000..58cd947c
--- /dev/null
+++ b/packages/phoenix/src/pty/NodeStdioPTT.js
@@ -0,0 +1,74 @@
+import { ReadableStream, WritableStream } from 'stream/web';
+import { signals } from "../ansi-shell/signals.js";
+
+const writestream_node_to_web = node_stream => {
+ return node_stream;
+ // return new WritableStream({
+ // write: chunk => {
+ // node_stream.write(chunk);
+ // }
+ // });
+};
+
+export class NodeStdioPTT {
+ constructor() {
+ // this.in = process.stdin;
+ // this.out = process.stdout;
+ // this.err = process.stderr;
+
+ // this.in = ReadableStream.from(process.stdin).getReader();
+
+ let readController;
+ const readableStream = new ReadableStream({
+ start: controller => {
+ readController = controller;
+ }
+ });
+ this.in = readableStream.getReader();
+ process.stdin.setRawMode(true);
+ process.stdin.on('data', chunk => {
+ const input = new Uint8Array(chunk);
+ readController.enqueue(input);
+ });
+
+ this.out = writestream_node_to_web(process.stdout);
+ this.err = writestream_node_to_web(process.stderr);
+
+ this.ioctl_listeners = {};
+
+ process.stdout.on('resize', () => {
+ this.emit('ioctl.set', {
+ data: {
+ windowSize: {
+ rows: process.stdout.rows,
+ cols: process.stdout.columns,
+ }
+ }
+ });
+ });
+
+ process.stdin.on('end', () => {
+ globalThis.force_eot = true;
+ readController.enqueue(new Uint8Array([4]));
+ });
+ }
+
+ on (name, listener) {
+ if ( ! this.ioctl_listeners.hasOwnProperty(name) ) {
+ this.ioctl_listeners[name] = [];
+ }
+ this.ioctl_listeners[name].push(listener);
+
+ // Hack: Pretend the window got resized, so that listeners get notified of the current size.
+ if (name === 'ioctl.set') {
+ process.stdout.emit('resize');
+ }
+ }
+
+ emit (name, evt) {
+ if ( ! this.ioctl_listeners.hasOwnProperty(name) ) return;
+ for ( const listener of this.ioctl_listeners[name] ) {
+ listener(evt);
+ }
+ }
+}
diff --git a/packages/phoenix/src/pty/XDocumentPTT.js b/packages/phoenix/src/pty/XDocumentPTT.js
new file mode 100644
index 00000000..13ea657a
--- /dev/null
+++ b/packages/phoenix/src/pty/XDocumentPTT.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { BetterReader } from "dev-pty";
+
+const encoder = new TextEncoder();
+
+export class XDocumentPTT {
+ constructor(terminalConnection) {
+ this.ioctl_listeners = {};
+
+ this.readableStream = new ReadableStream({
+ start: controller => {
+ this.readController = controller;
+ }
+ });
+ this.writableStream = new WritableStream({
+ start: controller => {
+ this.writeController = controller;
+ },
+ write: chunk => {
+ if (typeof chunk === 'string') {
+ chunk = encoder.encode(chunk);
+ }
+ terminalConnection.postMessage({
+ $: 'output',
+ data: chunk,
+ });
+ }
+ });
+ this.out = this.writableStream.getWriter();
+ this.in = this.readableStream.getReader();
+ this.in = new BetterReader({ delegate: this.in });
+
+ terminalConnection.on('message', message => {
+ if (message.$ === 'ioctl.set') {
+ this.emit('ioctl.set', message);
+ return;
+ }
+ if (message.$ === 'input') {
+ this.readController.enqueue(message.data);
+ return;
+ }
+ });
+ }
+
+ on (name, listener) {
+ if ( ! this.ioctl_listeners.hasOwnProperty(name) ) {
+ this.ioctl_listeners[name] = [];
+ }
+ this.ioctl_listeners[name].push(listener);
+ }
+
+ emit (name, evt) {
+ if ( ! this.ioctl_listeners.hasOwnProperty(name) ) return;
+ for ( const listener of this.ioctl_listeners[name] ) {
+ listener(evt);
+ }
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/completers/command_completer.js b/packages/phoenix/src/puter-shell/completers/command_completer.js
new file mode 100644
index 00000000..6994ddab
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/completers/command_completer.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class CommandCompleter {
+ async getCompletions (ctx, inputState) {
+ const { builtins } = ctx.registries;
+ const query = inputState.input;
+
+ if ( query === '' ) {
+ return [];
+ }
+
+ const completions = [];
+
+ // TODO: Match executable names as well as builtins
+ for ( const commandName of Object.keys(builtins) ) {
+ if ( commandName.startsWith(query) ) {
+ completions.push(commandName.slice(query.length));
+ }
+ }
+
+ return completions;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/completers/file_completer.js b/packages/phoenix/src/puter-shell/completers/file_completer.js
new file mode 100644
index 00000000..ea5df34c
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/completers/file_completer.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import path_ from "path-browserify";
+import { resolveRelativePath } from '../../util/path.js';
+
+export class FileCompleter {
+ async getCompletions (ctx, inputState) {
+ const { filesystem } = ctx.platform;
+
+ if ( inputState.input === '' ) {
+ return [];
+ }
+
+ let path = resolveRelativePath(ctx.vars, inputState.input);
+ let dir = path_.dirname(path);
+ let base = path_.basename(path);
+
+ const completions = [];
+
+ const result = await filesystem.readdir(dir);
+ if ( result === undefined ) {
+ return [];
+ }
+
+ for ( const item of result ) {
+ if ( item.name.startsWith(base) ) {
+ completions.push(item.name.slice(base.length));
+ }
+ }
+
+ return completions;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/completers/option_completer.js b/packages/phoenix/src/puter-shell/completers/option_completer.js
new file mode 100644
index 00000000..922f2bad
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/completers/option_completer.js
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { DEFAULT_OPTIONS } from '../coreutils/coreutil_lib/help.js';
+
+export class OptionCompleter {
+ async getCompletions (ctx, inputState) {
+ const { builtins } = ctx.registries;
+ const query = inputState.input;
+
+ if ( query === '' ) {
+ return [];
+ }
+
+ // TODO: Query the command through the providers system.
+ // Or, we could include the command in the context that's given to completers?
+ const command = builtins[inputState.tokens[0]];
+ if ( ! command ) {
+ return [];
+ }
+
+ const completions = [];
+
+ const processOptions = (options) => {
+ for ( const optionName of Object.keys(options) ) {
+ const prefixedOptionName = `--${optionName}`;
+ if ( prefixedOptionName.startsWith(query) ) {
+ completions.push(prefixedOptionName.slice(query.length));
+ }
+ }
+ };
+
+ // TODO: Only check these for builtins!
+ processOptions(DEFAULT_OPTIONS);
+
+ if ( command.args?.options ) {
+ processOptions(command.args.options);
+ }
+
+ return completions;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/__exports__.js b/packages/phoenix/src/puter-shell/coreutils/__exports__.js
new file mode 100644
index 00000000..2ff3c9d2
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/__exports__.js
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+// Generated by /tools/gen.js
+import module_ai from './ai.js'
+import module_basename from './basename.js'
+import module_cat from './cat.js'
+import module_cd from './cd.js'
+import module_changelog from './changelog.js'
+import module_clear from './clear.js'
+import module_concept_parser from './concept-parser.js'
+import module_cp from './cp.js'
+import module_date from './date.js'
+import module_dcall from './dcall.js'
+import module_dirname from './dirname.js'
+import module_echo from './echo.js'
+import module_env from './env.js'
+import module_errno from './errno.js'
+import module_false from './false.js'
+import module_grep from './grep.js'
+import module_head from './head.js'
+import module_help from './help.js'
+import module_jq from './jq.js'
+import module_login from './login.js'
+import module_ls from './ls.js'
+import module_man from './man.js'
+import module_mkdir from './mkdir.js'
+import module_mv from './mv.js'
+import module_neofetch from './neofetch.js'
+import module_printf from './printf.js'
+import module_printhist from './printhist.js'
+import module_pwd from './pwd.js'
+import module_rm from './rm.js'
+import module_rmdir from './rmdir.js'
+import module_sample_data from './sample-data.js'
+import module_sed from './sed.js'
+import module_sleep from './sleep.js'
+import module_sort from './sort.js'
+import module_tail from './tail.js'
+import module_test from './test.js'
+import module_touch from './touch.js'
+import module_true from './true.js'
+import module_txt2img from './txt2img.js'
+import module_usages from './usages.js'
+import module_wc from './wc.js'
+import module_which from './which.js'
+
+export default {
+ "ai": module_ai,
+ "basename": module_basename,
+ "cat": module_cat,
+ "cd": module_cd,
+ "changelog": module_changelog,
+ "clear": module_clear,
+ "concept-parser": module_concept_parser,
+ "cp": module_cp,
+ "date": module_date,
+ "dcall": module_dcall,
+ "dirname": module_dirname,
+ "echo": module_echo,
+ "env": module_env,
+ "errno": module_errno,
+ "false": module_false,
+ "grep": module_grep,
+ "head": module_head,
+ "help": module_help,
+ "jq": module_jq,
+ "login": module_login,
+ "ls": module_ls,
+ "man": module_man,
+ "mkdir": module_mkdir,
+ "mv": module_mv,
+ "neofetch": module_neofetch,
+ "printf": module_printf,
+ "printhist": module_printhist,
+ "pwd": module_pwd,
+ "rm": module_rm,
+ "rmdir": module_rmdir,
+ "sample-data": module_sample_data,
+ "sed": module_sed,
+ "sleep": module_sleep,
+ "sort": module_sort,
+ "tail": module_tail,
+ "test": module_test,
+ "touch": module_touch,
+ "true": module_true,
+ "txt2img": module_txt2img,
+ "usages": module_usages,
+ "wc": module_wc,
+ "which": module_which,
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/ai.js b/packages/phoenix/src/puter-shell/coreutils/ai.js
new file mode 100644
index 00000000..23e2bbb2
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/ai.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'ai',
+ usage: 'ai PROMPT',
+ description: 'Send PROMPT to Puter\'s AI chatbot, and print its response.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ const [ prompt ] = positionals;
+
+ if ( ! prompt ) {
+ await ctx.externs.err.write('ai: missing prompt\n');
+ throw new Exit(1);
+ }
+ if ( positionals.length > 1 ) {
+ await ctx.externs.err.write('ai: prompt must be wrapped in quotes\n');
+ throw new Exit(1);
+ }
+
+ const { drivers } = ctx.platform;
+ const { chatHistory } = ctx.plugins;
+
+ let a_interface, a_method, a_args;
+
+ a_interface = 'puter-chat-completion';
+ a_method = 'complete';
+ a_args = {
+ messages: [
+ ...chatHistory.get_messages(),
+ {
+ role: 'user',
+ content: prompt,
+ }
+ ],
+ };
+
+ console.log('THESE ARE THE MESSAGES', a_args.messages);
+
+ const result = await drivers.call({
+ interface: a_interface,
+ method: a_method,
+ args: a_args,
+ });
+
+ const resobj = JSON.parse(await result.text(), null, 2);
+
+ if ( resobj.success !== true ) {
+ await ctx.externs.err.write('request failed\n');
+ await ctx.externs.err.write(resobj);
+ return;
+ }
+
+ const message = resobj?.result?.message?.content;
+
+ if ( ! message ) {
+ await ctx.externs.err.write('message not found in response\n');
+ await ctx.externs.err.write(result);
+ return;
+ }
+
+ chatHistory.add_message(resobj?.result?.message);
+
+ await ctx.externs.out.write(message + '\n');
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/basename.js b/packages/phoenix/src/puter-shell/coreutils/basename.js
new file mode 100644
index 00000000..f5e78602
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/basename.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'basename',
+ usage: 'basename PATH [SUFFIX]',
+ description: 'Print PATH without leading directory segments.\n\n' +
+ 'If SUFFIX is provided, it is removed from the end of the result.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ let string = ctx.locals.positionals[0];
+ const suffix = ctx.locals.positionals[1];
+
+ if (string === undefined) {
+ await ctx.externs.err.write('basename: Missing path argument\n');
+ throw new Exit(1);
+ }
+ if (ctx.locals.positionals.length > 2) {
+ await ctx.externs.err.write('basename: Too many arguments, expected 1 or 2\n');
+ throw new Exit(1);
+ }
+
+ // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html
+
+ // 1. If string is a null string, it is unspecified whether the resulting string is '.' or a null string.
+ // In either case, skip steps 2 through 6.
+ if (string === '') {
+ string = '.';
+ } else {
+ // 2. If string is "//", it is implementation-defined whether steps 3 to 6 are skipped or processed.
+ // NOTE: We process it normally.
+
+ // 3. If string consists entirely of characters, string shall be set to a single character.
+ // In this case, skip steps 4 to 6.
+ if (/^\/+$/.test(string)) {
+ string = '/';
+ } else {
+ // 4. If there are any trailing characters in string, they shall be removed.
+ string = string.replace(/\/+$/, '');
+
+ // 5. If there are any characters remaining in string, the prefix of string up to and including
+ // the last character in string shall be removed.
+ const lastSlashIndex = string.lastIndexOf('/');
+ if (lastSlashIndex !== -1) {
+ string = string.substring(lastSlashIndex + 1);
+ }
+
+ // 6. If the suffix operand is present, is not identical to the characters remaining in string, and is
+ // identical to a suffix of the characters remaining in string, the suffix suffix shall be removed
+ // from string. Otherwise, string is not modified by this step. It shall not be considered an error
+ // if suffix is not found in string.
+ if (suffix !== undefined && suffix !== string && string.endsWith(suffix)) {
+ string = string.substring(0, string.length - suffix.length);
+ }
+ }
+ }
+
+ // The resulting string shall be written to standard output.
+ await ctx.externs.out.write(string + '\n');
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/cat.js b/packages/phoenix/src/puter-shell/coreutils/cat.js
new file mode 100644
index 00000000..f1e2941f
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/cat.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { resolveRelativePath } from '../../util/path.js';
+
+export default {
+ name: 'cat',
+ usage: 'cat [FILE...]',
+ description: 'Concatenate the FILE(s) and print the result.\n\n' +
+ 'If no FILE is given, or a FILE is `-`, read the standard input.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ input: {
+ syncLines: true,
+ },
+ output: 'text',
+ execute: async ctx => {
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ const paths = [...positionals];
+ if ( paths.length < 1 ) paths.push('-');
+
+ for ( const relPath of paths ) {
+ if ( relPath === '-' ) {
+ let line, done;
+ const next_line = async () => {
+ ({ value: line, done } = await ctx.externs.in_.read());
+ console.log('CAT LOOP', { line, done });
+ }
+ for ( await next_line() ; ! done ; await next_line() ) {
+ await ctx.externs.out.write(line);
+ }
+ continue;
+ }
+ const absPath = resolveRelativePath(ctx.vars, relPath);
+
+ const result = await filesystem.read(absPath);
+
+ await ctx.externs.out.write(result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/cd.js b/packages/phoenix/src/puter-shell/coreutils/cd.js
new file mode 100644
index 00000000..7352d39c
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/cd.js
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+import { resolveRelativePath } from '../../util/path.js';
+
+export default {
+ name: 'cd',
+ usage: 'cd PATH',
+ description: 'Change the current directory to PATH.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ // ctx.params to access processed args
+ // ctx.args to access raw args
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ let [ target ] = positionals;
+ target = resolveRelativePath(ctx.vars, target);
+
+ const result = await filesystem.readdir(target);
+
+ if ( result.$ === 'error' ) {
+ await ctx.externs.err.write('cd: error: ' + result.message + '\n');
+ throw new Exit(1);
+ }
+
+ ctx.vars.pwd = target;
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/changelog.js b/packages/phoenix/src/puter-shell/coreutils/changelog.js
new file mode 100644
index 00000000..027ec4c9
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/changelog.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { SHELL_VERSIONS } from "../../meta/versions.js";
+
+async function printVersion(ctx, version) {
+ await ctx.externs.out.write(`\x1B[35;1m[v${version.v}]\x1B[0m\n`);
+ for ( const change of version.changes ) {
+ await ctx.externs.out.write(`\x1B[32;1m+\x1B[0m ${change}\n`);
+ }
+}
+
+export default {
+ name: 'changelog',
+ description: 'Print the changelog for the Phoenix Shell, ordered oldest to newest.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: false,
+ options: {
+ latest: {
+ description: 'Print only the changes for the most recent version',
+ type: 'boolean'
+ }
+ }
+ },
+ execute: async ctx => {
+ if (ctx.locals.values.latest) {
+ await printVersion(ctx, SHELL_VERSIONS[0]);
+ return;
+ }
+
+ for ( const version of SHELL_VERSIONS.toReversed() ) {
+ await printVersion(ctx, version);
+ }
+ }
+};
+
diff --git a/packages/phoenix/src/puter-shell/coreutils/clear.js b/packages/phoenix/src/puter-shell/coreutils/clear.js
new file mode 100644
index 00000000..d38bf876
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/clear.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'clear',
+ usage: 'clear',
+ description: 'Clear the terminal output.',
+ args: {
+ // TODO: add 'none-parser'
+ $: 'simple-parser',
+ allowPositionals: false
+ },
+ execute: async ctx => {
+ await ctx.externs.out.write('\x1B[H\x1B[2J');
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/concept-parser.js b/packages/phoenix/src/puter-shell/coreutils/concept-parser.js
new file mode 100644
index 00000000..31888220
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/concept-parser.js
@@ -0,0 +1,320 @@
+import { GrammarContext, standard_parsers } from '../../../packages/newparser/exports.js';
+import { Parser, UNRECOGNIZED, VALUE } from '../../../packages/newparser/lib.js';
+
+class NumberParser extends Parser {
+ static data = {
+ startDigit: /[1-9]/,
+ digit: /[0-9]/,
+ }
+ _parse (stream) {
+ const subStream = stream.fork();
+
+ const { startDigit, digit } = this.constructor.data;
+
+ let { done, value } = subStream.look();
+ if ( done ) return UNRECOGNIZED;
+ let text = '';
+
+ // Returns true if there is a next character
+ const consume = () => {
+ text += value;
+ subStream.next();
+ ({ done, value } = subStream.look());
+
+ return !done;
+ };
+
+ // Returns the number of consumed characters
+ const consumeDigitSequence = () => {
+ let consumed = 0;
+ while (!done && digit.test(value)) {
+ consumed++;
+ consume();
+ }
+ return consumed;
+ };
+
+ // Sign
+ if ( value === '-' ) {
+ if ( !consume() ) return UNRECOGNIZED;
+ }
+
+ // Digits
+ if (value === '0') {
+ if ( !consume() ) return UNRECOGNIZED;
+ } else if (startDigit.test(value)) {
+ if (consumeDigitSequence() === 0) return UNRECOGNIZED;
+ } else {
+ return UNRECOGNIZED;
+ }
+
+ // Decimal + digits
+ if (value === '.') {
+ if ( !consume() ) return UNRECOGNIZED;
+ if (consumeDigitSequence() === 0) return UNRECOGNIZED;
+ }
+
+ // Exponent
+ if (value === 'e' || value === 'E') {
+ if ( !consume() ) return UNRECOGNIZED;
+
+ if (value === '+' || value === '-') {
+ if ( !consume() ) return UNRECOGNIZED;
+ }
+ if (consumeDigitSequence() === 0) return UNRECOGNIZED;
+ }
+
+ if ( text.length === 0 ) return UNRECOGNIZED;
+ stream.join(subStream);
+ return { status: VALUE, $: 'number', value: Number.parseFloat(text) };
+ }
+}
+
+class StringParser extends Parser {
+ static data = {
+ escapes: {
+ '"': '"',
+ '\\': '\\',
+ '/': '/',
+ 'b': String.fromCharCode(8),
+ 'f': String.fromCharCode(0x0C),
+ '\n': '\n',
+ '\r': '\r',
+ '\t': '\t',
+ },
+ hexDigit: /[0-9A-Fa-f]/,
+ }
+ _parse (stream) {
+ const { escapes, hexDigit } = this.constructor.data;
+
+ const subStream = stream.fork();
+ let { done, value } = subStream.look();
+ if ( done ) return UNRECOGNIZED;
+
+ let text = '';
+
+ // Returns true if there is a next character
+ const next = () => {
+ subStream.next();
+ ({ done, value } = subStream.look());
+ return !done;
+ };
+
+ // Opening "
+ if (value === '"') {
+ if (!next()) return UNRECOGNIZED;
+ } else {
+ return UNRECOGNIZED;
+ }
+
+ let insideString = true;
+ while (insideString) {
+ if (value === '"')
+ break;
+
+ // Escape sequences
+ if (value === '\\') {
+ if (!next()) return UNRECOGNIZED;
+ const escape = escapes[value];
+ if (escape) {
+ text += escape;
+ if (!next()) return UNRECOGNIZED;
+ continue;
+ }
+
+ if (value === 'u') {
+ if (!next()) return UNRECOGNIZED;
+
+ // Consume 4 hex digits, and decode as a unicode codepoint
+ let hexString = '';
+ while (!done && hexString.length < 4) {
+ if (hexDigit.test(value)) {
+ hexString += value;
+ if (!next()) return UNRECOGNIZED;
+ continue;
+ }
+ // Less than 4 hex digits read
+ return UNRECOGNIZED;
+ }
+ let codepoint = Number.parseInt(hexString, 16);
+ text += String.fromCodePoint(codepoint);
+ continue;
+ }
+
+ // Otherwise, it's an invalid escape sequence
+ return UNRECOGNIZED;
+ }
+
+ // Anything else is valid string content
+ text += value;
+ if (!next()) return UNRECOGNIZED;
+ }
+
+ // Closing "
+ if (value === '"') {
+ next();
+ } else {
+ return UNRECOGNIZED;
+ }
+
+ if ( text.length === 0 ) return UNRECOGNIZED;
+ stream.join(subStream);
+ return { status: VALUE, $: 'string', value: text };
+ }
+}
+
+class StringStream {
+ constructor (str, startIndex = 0) {
+ this.str = str;
+ this.i = startIndex;
+ }
+
+ value_at (index) {
+ if ( index >= this.str.length ) {
+ return { done: true, value: undefined };
+ }
+
+ return { done: false, value: this.str[index] };
+ }
+
+ look () {
+ return this.value_at(this.i);
+ }
+
+ next () {
+ const result = this.value_at(this.i);
+ this.i++;
+ return result;
+ }
+
+ fork () {
+ return new StringStream(this.str, this.i);
+ }
+
+ join (forked) {
+ this.i = forked.i;
+ }
+}
+
+export default {
+ name: 'concept-parser',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ const { in_, out, err } = ctx.externs;
+ await out.write("STARTING CONCEPT PARSER\n");
+ const grammar_context = new GrammarContext(standard_parsers());
+ await out.write("Constructed a grammar context\n");
+
+ const parser = grammar_context.define_parser({
+ element: a => a.sequence(
+ a.symbol('whitespace'),
+ a.symbol('value'),
+ a.symbol('whitespace'),
+ ),
+ value: a => a.firstMatch(
+ a.symbol('object'),
+ a.symbol('array'),
+ a.symbol('string'),
+ a.symbol('number'),
+ a.symbol('true'),
+ a.symbol('false'),
+ a.symbol('null'),
+ ),
+ array: a => a.sequence(
+ a.literal('['),
+ a.symbol('whitespace'),
+ a.optional(
+ a.repeat(
+ a.symbol('element'),
+ a.literal(','),
+ { trailing: true },
+ ),
+ ),
+ a.symbol('whitespace'),
+ a.literal(']'),
+ ),
+ member: a => a.sequence(
+ a.symbol('whitespace'),
+ a.symbol('string'),
+ a.symbol('whitespace'),
+ a.literal(':'),
+ a.symbol('whitespace'),
+ a.symbol('value'),
+ a.symbol('whitespace'),
+ ),
+ object: a => a.sequence(
+ a.literal('{'),
+ a.symbol('whitespace'),
+ a.optional(
+ a.repeat(
+ a.symbol('member'),
+ a.literal(','),
+ { trailing: true },
+ ),
+ ),
+ a.symbol('whitespace'),
+ a.literal('}'),
+ ),
+ true: a => a.literal('true'),
+ false: a => a.literal('false'),
+ null: a => a.literal('null'),
+ number: a => new NumberParser(),
+ string: a => new StringParser(),
+ whitespace: a => a.optional(
+ a.stringOf(' \r\n\t'.split('')),
+ ),
+ }, {
+ element: it => it[0].value,
+ value: it => it,
+ array: it => {
+ // A parsed array contains 3 values: `[`, the entries array, and `]`, so we only care about index 1.
+ // If it's less than 3, there were no entries.
+ if (it.length < 3) return [];
+ return (it[1].value || [])
+ .filter(it => it.$ !== 'literal')
+ .map(it => it.value);
+ },
+ member: it => {
+ // A parsed member contains 3 values: a name, `:`, and a value.
+ const [ name_part, colon, value_part ] = it;
+ return { name: name_part.value, value: value_part.value };
+ },
+ object: it => {
+ console.log('OBJECT!!!!');
+ console.log(it[1]);
+ // A parsed object contains 3 values: `{`, the members array, and `}`, so we only care about index 1.
+ // If it's less than 3, there were no members.
+ if (it.length < 3) return {};
+ const result = {};
+ // FIXME: This is all wrong!!!
+ (it[1].value || [])
+ .filter(it => it.$ === 'member')
+ .forEach(it => {
+ result[it.name] = it.value;
+ });
+ return result;
+ },
+ true: _ => true,
+ false: _ => false,
+ null: _ => null,
+ number: it => it,
+ string: it => it,
+ whitespace: _ => {},
+ });
+
+ // TODO: What do we want our streams to be like?
+ const input = ctx.locals.positionals.shift();
+ const stream = new StringStream(input);
+ try {
+ const result = parser(stream, 'element');
+ console.log('Parsed something!', result);
+ await out.write('Parsed: `' + JSON.stringify(result, undefined, 2) + '`\n');
+ } catch (e) {
+ await err.write(`Error while parsing: ${e.toString()}\n`);
+ await err.write(e.stack + '\n');
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js
new file mode 100644
index 00000000..c084d723
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/echo_escapes.js
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+/*
+ Echo Escapes Implementations
+ ----------------------------
+
+ This documentation describes how functions in this file
+ should be implemented.
+
+ SITUATION
+ The function is passed an object called `fns` containing
+ functions to interact with the caller.
+
+ It can be assumped that the called has already advanced
+ a "text cursor" just past the first character identifying
+ the escape sequence. For example, for escape sequence `\a`
+ the text cursor will be positioned immediately after `a`.
+
+ INPUTS
+ function: peek()
+ returns the character at the position of the text cursor
+
+ function: advance(n=1)
+ advances the text cursor `n` bytes forward
+
+ function: markIgnored
+ informs the caller that the escape sequence should be
+ treated as literal text
+
+ function: output
+ commands the caller to write a string
+
+ function: outputETX
+ informs the caller that this is the end of text;
+ \c is Ctrl+C is ETX
+*/
+
+// TODO: get these values from a common place
+const NUL = String.fromCharCode(1);
+const BEL = String.fromCharCode(7);
+const BS = String.fromCharCode(8);
+const VT = String.fromCharCode(0x0B);
+const FF = String.fromCharCode(0x0C);
+const ESC = String.fromCharCode(0x1B);
+
+const HEX_REGEX = /^[A-Fa-f0-9]/;
+const OCT_REGEX = /^[0-7]/;
+const maybeGetHex = chr => {
+ let hexchars = '';
+ if ( chr.match(HEX_REGEX) ) {
+ //
+ }
+};
+
+const echo_escapes = {
+ 'a': caller => caller.output(BEL),
+ 'b': caller => caller.output(BS),
+ 'c': caller => caller.outputETX(),
+ 'e': caller => caller.output(ESC),
+ 'f': caller => caller.output(FF),
+ 'n': caller => caller.output('\n'),
+ 'r': caller => caller.output('\r'),
+ 't': caller => caller.output('\t'),
+ 'v': caller => caller.output(VT),
+ 'x': caller => {
+ let hexchars = '';
+ while ( caller.peek().match(HEX_REGEX) ) {
+ hexchars += caller.peek();
+ caller.advance();
+
+ if ( hexchars.length === 2 ) break;
+ }
+ if ( hexchars.length === 0 ) {
+ caller.markIgnored();
+ return;
+ }
+ caller.output(String.fromCharCode(Number.parseInt(hexchars, 16)));
+ },
+ '0': caller => {
+ let octchars = '';
+ while ( caller.peek().match(OCT_REGEX) ) {
+ octchars += caller.peek();
+ caller.advance();
+
+ if ( octchars.length === 3 ) break;
+ }
+ if ( octchars.length === 0 ) {
+ caller.output(NUL);
+ return;
+ }
+ caller.output(String.fromCharCode(Number.parseInt(hexchars, 8)));
+ },
+ '\\': caller => caller.output('\\'),
+};
+
+export const processEscapes = str => {
+ let output = '';
+
+ let state = null;
+ const states = {};
+ states.STATE_ESCAPE = i => {
+ state = states.STATE_NORMAL;
+
+ let ignored = false;
+
+ const chr = str[i];
+ i++;
+ const apiToCaller = {
+ advance: n => {
+ n = n ?? 1;
+ i += n;
+ },
+ peek: () => str[i],
+ output: text => output += text,
+ markIgnored: () => ignored = true,
+ outputETX: () => {
+ state = states.STATE_ETX;
+ }
+ };
+ echo_escapes[chr](apiToCaller);
+
+ if ( ignored ) {
+ output += '\\' + str[i];
+ return;
+ }
+
+ return i;
+ };
+ states.STATE_NORMAL = i => {
+ console.log('str@i', str[i]);
+ if ( str[i] === '\\' ) {
+ console.log('escape state?');
+ state = states.STATE_ESCAPE;
+ return;
+ }
+ output += str[i];
+ };
+ states.STATE_ETX = () => str.length;
+ state = states.STATE_NORMAL;
+
+ for ( let i=0 ; i < str.length ; ) {
+ i = state(i) ?? i+1;
+ }
+
+ return output;
+};
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js
new file mode 100644
index 00000000..1d7b419b
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/exit.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class Exit extends Error {
+ constructor (code) {
+ super(`exit ${code}`);
+ this.code = code;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js
new file mode 100644
index 00000000..fbf16f4e
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/help.js
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { wrapText } from '../../../util/wrap-text.js';
+
+const TAB_SIZE = 8;
+
+export const DEFAULT_OPTIONS = {
+ help: {
+ description: 'Display this help text, and exit',
+ type: 'boolean',
+ },
+};
+
+export const printUsage = async (command, out, vars) => {
+ const { name, usage, description, args, helpSections } = command;
+ const options = Object.create(DEFAULT_OPTIONS);
+ Object.assign(options, args.options);
+
+ const heading = async text => {
+ await out.write(`\x1B[34;1m${text}:\x1B[0m\n`);
+ };
+ const colorOption = text => {
+ return `\x1B[92m${text}\x1B[0m`;
+ };
+ const colorOptionArgument = text => {
+ return `\x1B[91m${text}\x1B[0m`;
+ };
+ const wrap = text => {
+ return wrapText(text, vars.size.cols).join('\n') + '\n';
+ }
+
+ await heading('Usage');
+ if (!usage) {
+ let output = name;
+ if (options) {
+ output += ' [OPTIONS]';
+ }
+ if (args.allowPositionals) {
+ output += ' INPUTS...';
+ }
+ await out.write(` ${output}\n\n`);
+ } else if (typeof usage === 'string') {
+ await out.write(` ${usage}\n\n`);
+ } else {
+ for (const line of usage) {
+ await out.write(` ${line}\n`);
+ }
+ await out.write('\n');
+ }
+
+ if (description) {
+ await out.write(wrap(description));
+ await out.write(`\n`);
+ }
+
+ if (options) {
+ await heading('Options');
+
+ for (const optionName in options) {
+ let optionText = ' ';
+ let indentSize = optionText.length;
+ const option = options[optionName];
+ if (option.short) {
+ optionText += colorOption('-' + option.short) + ', ';
+ indentSize += `-${option.short}, `.length;
+ } else {
+ optionText += ` `;
+ indentSize += ` `.length;
+ }
+ optionText += colorOption(`--${optionName}`);
+ indentSize += `--${optionName}`.length;
+ if (option.type !== 'boolean') {
+ const valueName = option.valueName || 'VALUE';
+ optionText += `=${colorOptionArgument(valueName)}`;
+ indentSize += `=${valueName}`.length;
+ }
+ if (option.description) {
+ const indentSizeIncludingTab = (size) => {
+ return (Math.floor(size / TAB_SIZE) + 1) * TAB_SIZE + 1;
+ };
+
+ // Wrap the description based on the terminal width, with each line indented.
+ let remainingWidth = vars.size.cols - indentSizeIncludingTab(indentSize);
+ let skipIndentOnFirstLine = true;
+
+ // If there's not enough room after a very long option name, start on the next line.
+ if (remainingWidth < 30) {
+ optionText += '\n';
+ indentSize = 8;
+ remainingWidth = vars.size.cols - indentSizeIncludingTab(indentSize);
+ skipIndentOnFirstLine = false;
+ }
+
+ const wrappedDescriptionLines = wrapText(option.description, remainingWidth);
+ for (const line of wrappedDescriptionLines) {
+ if (skipIndentOnFirstLine) {
+ skipIndentOnFirstLine = false;
+ } else {
+ optionText += ' '.repeat(indentSize);
+ }
+ optionText += `\t ${line}\n`;
+ }
+ } else {
+ optionText += '\n';
+ }
+ await out.write(optionText);
+ }
+ await out.write('\n');
+ }
+
+ if (helpSections) {
+ for (const [title, contents] of Object.entries(helpSections)) {
+ await heading(title);
+ await out.write(wrap(contents));
+ await out.write('\n\n');
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js
new file mode 100644
index 00000000..807f748b
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/coreutil_lib/validate.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const validate_string = (str, meta) => {
+ if ( str === undefined ) {
+ if ( ! meta.allow_empty ) {
+ throw new Error(`${meta?.name} is required`);
+ }
+ return '';
+ }
+
+ if ( typeof str !== 'string' ) {
+ throw new Error(`${meta?.name} must be a string`);
+ }
+
+ if ( ! meta.allow_empty && str.length === 0 ) {
+ throw new Error(`${meta?.name} must not be empty`);
+ }
+
+ return str;
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/cp.js b/packages/phoenix/src/puter-shell/coreutils/cp.js
new file mode 100644
index 00000000..032f7ecc
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/cp.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from "./coreutil_lib/exit.js";
+import { resolveRelativePath } from '../../util/path.js';
+
+export default {
+ name: 'cp',
+ usage: ['cp [OPTIONS] SOURCE DESTINATION', 'cp [OPTIONS] SOURCE... DIRECTORY'],
+ description: 'Copy the SOURCE to DESTINATION, or multiple SOURCE(s) to DIRECTORY.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ recursive: {
+ description: 'Copy directories recursively',
+ type: 'boolean',
+ short: 'R'
+ }
+ }
+ },
+ execute: async ctx => {
+ const { positionals, values } = ctx.locals;
+ const { out, err } = ctx.externs;
+ const { filesystem } = ctx.platform;
+
+ if ( positionals.length < 1 ) {
+ await err.write('cp: missing file operand\n');
+ throw new Exit(1);
+ }
+
+ const srcRelPath = positionals.shift();
+
+ if ( positionals.length < 1 ) {
+ const aft = positionals[0];
+ await err.write(`cp: missing destination file operand after '${aft}'\n`);
+ throw new Exit(1);
+ }
+
+ const dstRelPath = positionals.shift();
+
+ const srcAbsPath = resolveRelativePath(ctx.vars, srcRelPath);
+ let dstAbsPath = resolveRelativePath(ctx.vars, dstRelPath);
+
+ const srcStat = await filesystem.stat(srcAbsPath);
+ if ( srcStat && srcStat.is_dir && ! values.recursive ) {
+ await err.write(`cp: -R not specified; skipping directory '${srcRelPath}'\n`);
+ throw new Exit(1);
+ }
+
+ await filesystem.copy(srcAbsPath, dstAbsPath);
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/date.js b/packages/phoenix/src/puter-shell/coreutils/date.js
new file mode 100644
index 00000000..f694544f
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/date.js
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+// "When no formatting operand is specified, the output in the POSIX locale shall be equivalent to specifying:"
+const DEFAULT_FORMAT = '+%a %b %e %H:%M:%S %Z %Y';
+
+function padStart(number, length, padChar) {
+ let string = number.toString();
+ if ( string.length >= length ) {
+ return string;
+ }
+
+ return padChar.repeat(length - string.length) + string;
+}
+
+function highlight(text) {
+ return `\x1B[92m${text}\x1B[0m`;
+}
+
+export default {
+ name: 'date',
+ usage: 'date [OPTIONS] [+FORMAT]',
+ description: 'Print the system date and time\n\n' +
+ 'If FORMAT is provided, it controls the date format used.',
+ helpSections: {
+ 'Format Sequences': 'The following format sequences are understood:\n\n' +
+ ` ${highlight('%a')} Weekday name, abbreviated.\n` +
+ ` ${highlight('%A')} Weekday name\n` +
+ ` ${highlight('%b')} Month name, abbreviated\n` +
+ ` ${highlight('%B')} Month name\n` +
+ ` ${highlight('%c')} Default date and time representation\n` +
+ ` ${highlight('%C')} Century, 2 digits padded with '0'\n` +
+ ` ${highlight('%d')} Day of the month, 2 digits padded with '0'\n` +
+ ` ${highlight('%D')} Date in the format mm/dd/yy\n` +
+ ` ${highlight('%e')} Day of the month, 2 characters padded with leading spaces\n` +
+ ` ${highlight('%h')} Same as ${highlight('%b')}\n` +
+ ` ${highlight('%H')} Hour (24-hour clock), 2 digits padded with '0'\n` +
+ ` ${highlight('%I')} Hour (12-hour clock), 2 digits padded with '0'\n` +
+ // ` ${highlight('%j')} TODO: Day of the year, 3 digits padded with '0'\n` +
+ ` ${highlight('%m')} Month, 2 digits padded with '0', with January = 01\n` +
+ ` ${highlight('%M')} Minutes, 2 digits padded with '0'\n` +
+ ` ${highlight('%n')} A newline character\n` +
+ ` ${highlight('%p')} AM or PM\n` +
+ ` ${highlight('%r')} Time (12-hour clock) with AM/PM, as 'HH:MM:SS AM/PM'\n` +
+ ` ${highlight('%S')} Seconds, 2 digits padded with '0'\n` +
+ ` ${highlight('%t')} A tab character\n` +
+ ` ${highlight('%T')} Time (24-hour clock), as 'HH:MM:SS'\n` +
+ ` ${highlight('%u')} Weekday as a number, with Monday = 1 and Sunday = 7\n` +
+ // ` ${highlight('%U')} TODO: Week of the year (Sunday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Sunday shall be considered to be in week 0.\n` +
+ // ` ${highlight('%V')} TODO: Week of the year (Monday as the first day of the week) as a decimal number [01,53]. If the week containing January 1 has four or more days in the new year, then it shall be considered week 1; otherwise, it shall be the last week of the previous year, and the next week shall be week 1.\n` +
+ ` ${highlight('%w')} Weekday as a number, with Sunday = 0\n` +
+ // ` ${highlight('%W')} TODO: Week of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday shall be considered to be in week 0.\n` +
+ ` ${highlight('%x')} Default date representation\n` +
+ ` ${highlight('%X')} Default time representation\n` +
+ ` ${highlight('%y')} Year within century, 2 digits padded with '0'\n` +
+ ` ${highlight('%Y')} Year\n` +
+ ` ${highlight('%Z')} Timezone name, if it can be determined\n` +
+ ` ${highlight('%%')} A percent sign\n`
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ utc: {
+ description: 'Operate in UTC instead of the local timezone',
+ type: 'boolean',
+ short: 'u',
+ default: false,
+ }
+ }
+ },
+ execute: async ctx => {
+ const { out, err } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+
+ if ( positionals.length > 1 ) {
+ await err.write('date: Too many arguments\n');
+ throw new Exit(1);
+ }
+
+ let format = positionals.shift() ?? DEFAULT_FORMAT;
+
+ if ( ! format.startsWith('+') ) {
+ await err.write('date: Format does not begin with `+`\n');
+ throw new Exit(1);
+ }
+ format = format.substring(1);
+
+ // TODO: Should we use the server time instead? Maybe put that behind an option.
+ const date = new Date();
+ const locale = 'en-US'; // TODO: POSIX: Pull this from the user's settings.
+ const timeZone = values.utc ? 'UTC' : undefined;
+
+ let output = '';
+ for (let i = 0; i < format.length; i++) {
+ let char = format[i];
+ if ( char === '%' ) {
+ char = format[++i];
+ switch (char) {
+ // "Locale's abbreviated weekday name."
+ case 'a': {
+ output += date.toLocaleDateString(locale, { timeZone: timeZone, weekday: 'short' });
+ break;
+ }
+
+ // "Locale's full weekday name."
+ case 'A': {
+ output += date.toLocaleDateString(locale, { timeZone: timeZone, weekday: 'long' });
+ break;
+ }
+
+ // "Locale's abbreviated month name."
+ case 'b':
+ // "A synonym for %b."
+ case 'h': {
+ output += date.toLocaleDateString(locale, { timeZone: timeZone, month: 'short' });
+ break;
+ }
+
+ // "Locale's full month name."
+ case 'B': {
+ output += date.toLocaleDateString(locale, { timeZone: timeZone, month: 'long' });
+ break;
+ }
+
+ // "Locale's appropriate date and time representation."
+ case 'c': {
+ output += date.toLocaleString(locale, { timeZone: timeZone });
+ break;
+ }
+
+ // "Century (a year divided by 100 and truncated to an integer) as a decimal number [00,99]."
+ case 'C': {
+ output += Math.trunc(date.getFullYear() / 100);
+ break;
+ }
+
+ // "Day of the month as a decimal number [01,31]."
+ case 'd': {
+ output += padStart(date.getDate(), 2, '0');
+ break;
+ }
+
+ // "Date in the format mm/dd/yy."
+ case 'D': {
+ const month = padStart(date.getMonth() + 1, 2, '0');
+ const day = padStart(date.getDate(), 2, '0');
+ const year = padStart(date.getFullYear() % 100, 2, '0');
+ output += `${month}/${day}/${year}`;
+ break;
+ }
+
+ // "Day of the month as a decimal number [1,31] in a two-digit field with leading
+ // character fill."
+ case 'e': {
+ output += padStart(date.getDate(), 2, ' ');
+ break;
+ }
+
+ // "Hour (24-hour clock) as a decimal number [00,23]."
+ case 'H': {
+ output += padStart(date.getHours(), 2, '0');
+ break;
+ }
+
+ // "Hour (12-hour clock) as a decimal number [01,12]."
+ case 'I': {
+ output += padStart((date.getHours() % 12) || 12, 2, '0');
+ break;
+ }
+
+ // TODO: "Day of the year as a decimal number [001,366]."
+ case 'j': break;
+
+ // "Month as a decimal number [01,12]."
+ case 'm': {
+ // getMonth() starts at 0 for January
+ output += padStart(date.getMonth() + 1, 2, '0');
+ break;
+ }
+
+ // "Minute as a decimal number [00,59]."
+ case 'M': {
+ output += padStart(date.getMinutes(), 2, '0');
+ break;
+ }
+
+ // "A ."
+ case 'n': output += '\n'; break;
+
+ // "Locale's equivalent of either AM or PM."
+ case 'p': {
+ // TODO: We should access this from the locale.
+ output += date.getHours() < 12 ? 'AM' : 'PM';
+ break;
+ }
+
+ // "12-hour clock time [01,12] using the AM/PM notation; in the POSIX locale, this shall be
+ // equivalent to %I : %M : %S %p."
+ case 'r': {
+ const rawHours = date.getHours();
+ const hours = padStart((rawHours % 12) || 12, 2, '0');
+ // TODO: We should access this from the locale.
+ const am_pm = rawHours < 12 ? 'AM' : 'PM';
+ const minutes = padStart(date.getMinutes(), 2, '0');
+ const seconds = padStart(date.getSeconds(), 2, '0');
+ output += `${hours}:${minutes}:${seconds} ${am_pm}`;
+ break;
+ }
+
+ // "Seconds as a decimal number [00,60]."
+ case 'S': {
+ output += padStart(date.getSeconds(), 2, '0');
+ break;
+ }
+
+ // "A ."
+ case 't': output += '\t'; break;
+
+ // "24-hour clock time [00,23] in the format HH:MM:SS."
+ case 'T': {
+ const hours = padStart(date.getHours(), 2, '0');
+ const minutes = padStart(date.getMinutes(), 2, '0');
+ const seconds = padStart(date.getSeconds(), 2, '0');
+ output += `${hours}:${minutes}:${seconds}`;
+ break;
+ }
+
+ // "Weekday as a decimal number [1,7] (1=Monday)."
+ case 'u': {
+ // getDay() returns 0 for Sunday
+ output += date.getDay() || 7;
+ break;
+ }
+
+ // TODO: "Week of the year (Sunday as the first day of the week) as a decimal number [00,53].
+ // All days in a new year preceding the first Sunday shall be considered to be in week 0."
+ case 'U': break;
+
+ // TODO: "Week of the year (Monday as the first day of the week) as a decimal number [01,53].
+ // If the week containing January 1 has four or more days in the new year, then it shall be
+ // considered week 1; otherwise, it shall be the last week of the previous year, and the next
+ // week shall be week 1."
+ case 'V': break;
+
+ // "Weekday as a decimal number [0,6] (0=Sunday)."
+ case 'w': {
+ output += date.getDay();
+ break;
+ }
+
+ // TODO: "Week of the year (Monday as the first day of the week) as a decimal number [00,53].
+ // All days in a new year preceding the first Monday shall be considered to be in week 0."
+ case 'W': break;
+
+ // "Locale's appropriate date representation."
+ case 'x': {
+ output += date.toLocaleDateString(locale, { timeZone: timeZone });
+ break;
+ }
+
+ // "Locale's appropriate time representation."
+ case 'X': {
+ output += date.toLocaleTimeString(locale, { timeZone: timeZone });
+ break;
+ }
+
+ // "Year within century [00,99]."
+ case 'y': {
+ output += date.getFullYear() % 100;
+ break;
+ }
+
+ // "Year with century as a decimal number."
+ case 'Y': {
+ output += date.getFullYear();
+ break;
+ }
+
+ // "Timezone name, or no characters if no timezone is determinable."
+ case 'Z': {
+ const parts = new Intl.DateTimeFormat(locale, { timeZone: timeZone, timeZoneName: 'short' }).formatToParts(date);
+ output += parts.find(it => it.type === 'timeZoneName').value;
+ break;
+ }
+
+ // "A character."
+ case '%': output += '%'; break;
+
+ // We reached the end of the string, just output the %.
+ case undefined: output += '%'; break;
+
+ // If nothing matched, just output the input verbatim
+ default: output += '%' + char; break;
+ }
+ continue;
+ }
+ output += char;
+ }
+ output += '\n';
+
+ await out.write(output);
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/dcall.js b/packages/phoenix/src/puter-shell/coreutils/dcall.js
new file mode 100644
index 00000000..dad4bdf4
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/dcall.js
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'driver-call',
+ usage: 'driver-call METHOD [JSON]',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ const [ method, json ] = positionals;
+
+ const { drivers } = ctx.platform;
+
+ let a_interface, a_method, a_args;
+ if ( method === 'test' ) {
+ // a_interface = 'puter-kvstore';
+ // a_method = 'get';
+ // a_args = { key: 'something' };
+ a_interface = 'puter-image-generation',
+ a_method = 'generate';
+ a_args = {
+ prompt: 'a blue cat',
+ };
+ } else {
+ [a_interface, a_method] = method.split(':');
+ try {
+ a_args = JSON.parse(json);
+ } catch (e) {
+ a_args = {};
+ }
+ }
+
+ const result = await drivers.call({
+ interface: a_interface,
+ method: a_method,
+ args: a_args,
+ });
+
+ await ctx.externs.out.write(result);
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/dirname.js b/packages/phoenix/src/puter-shell/coreutils/dirname.js
new file mode 100644
index 00000000..c0f52d51
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/dirname.js
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'dirname',
+ usage: 'dirname PATH',
+ description: 'Print PATH without its final segment.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ let string = ctx.locals.positionals[0];
+ const removeTrailingSlashes = (input) => {
+ return input.replace(/\/+$/, '');
+ }
+
+ if (string === undefined) {
+ await ctx.externs.err.write('dirname: Missing path argument\n');
+ throw new Exit(1);
+ }
+ if (ctx.locals.positionals.length > 1) {
+ await ctx.externs.err.write('dirname: Too many arguments, expected 1\n');
+ throw new Exit(1);
+ }
+
+ // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/dirname.html
+ let skipToAfterStep8 = false;
+
+ // 1. If string is //, skip steps 2 to 5.
+ if (string !== '//') {
+ // 2. If string consists entirely of characters, string shall be set to a single character.
+ // In this case, skip steps 3 to 8.
+ if (string === '/'.repeat(string.length)) {
+ string = '/';
+ skipToAfterStep8 = true;
+ } else {
+ // 3. If there are any trailing characters in string, they shall be removed.
+ string = removeTrailingSlashes(string);
+
+ // 4. If there are no characters remaining in string, string shall be set to a single character.
+ // In this case, skip steps 5 to 8.
+ if (string.indexOf('/') === -1) {
+ string = '.';
+ skipToAfterStep8 = true;
+ }
+
+ // 5. If there are any trailing non- characters in string, they shall be removed.
+ else {
+ const lastSlashIndex = string.lastIndexOf('/');
+ if (lastSlashIndex === -1) {
+ string = '';
+ } else {
+ string = string.substring(0, lastSlashIndex);
+ }
+ }
+ }
+ }
+
+ if (!skipToAfterStep8) {
+ // 6. If the remaining string is //, it is implementation-defined whether steps 7 and 8 are skipped or processed.
+ // NOTE: We process it normally.
+
+ // 7. If there are any trailing characters in string, they shall be removed.
+ string = removeTrailingSlashes(string);
+
+ // 8. If the remaining string is empty, string shall be set to a single character.
+ if (string.length === 0) {
+ string = '/';
+ }
+ }
+
+ // The resulting string shall be written to standard output.
+ await ctx.externs.out.write(string + '\n');
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/echo.js b/packages/phoenix/src/puter-shell/coreutils/echo.js
new file mode 100644
index 00000000..d29509a5
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/echo.js
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { processEscapes } from "./coreutil_lib/echo_escapes.js";
+
+export default {
+ name: 'echo',
+ usage: 'echo [OPTIONS] INPUTS...',
+ description: 'Print the inputs to standard output.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ 'no-newline': {
+ description: 'Do not print a trailing newline',
+ type: 'boolean',
+ short: 'n'
+ },
+ 'enable-escapes': {
+ description: 'Interpret backslash escape sequences',
+ type: 'boolean',
+ short: 'e'
+ },
+ 'disable-escapes': {
+ description: 'Disable interpreting backslash escape sequences',
+ type: 'boolean',
+ short: 'E'
+ }
+ }
+ },
+ execute: async ctx => {
+ const { positionals, values } = ctx.locals;
+
+ let output = '';
+ let notFirst = false;
+ for ( const positional of positionals ) {
+ if ( notFirst ) {
+ output += ' ';
+ } else notFirst = true;
+ output += positional;
+ }
+
+ if ( ! values.n ) {
+ output += '\n';
+ }
+
+ if ( values.e && ! values.E ) {
+ console.log('processing');
+ output = processEscapes(output);
+ }
+
+ const lines = output.split('\n');
+ for ( let i=0 ; i < lines.length ; i++ ) {
+ const line = lines[i];
+ const isLast = i === lines.length - 1;
+ await ctx.externs.out.write(line + (isLast ? '' : '\n'));
+ }
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/env.js b/packages/phoenix/src/puter-shell/coreutils/env.js
new file mode 100644
index 00000000..6f9eb4ab
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/env.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'env',
+ usage: 'env',
+ description: 'Print environment variables, one per line, as NAME=VALUE.',
+ args: {
+ // TODO: add 'none-parser'
+ $: 'simple-parser',
+ allowPositionals: false
+ },
+ execute: async ctx => {
+ const env = ctx.env;
+ const out = ctx.externs.out;
+
+ for ( const k in env ) {
+ await out.write(`${k}=${env[k]}\n`);
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/errno.js b/packages/phoenix/src/puter-shell/coreutils/errno.js
new file mode 100644
index 00000000..dd36d8dd
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/errno.js
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { ErrorCodes, ErrorMetadata, errorFromIntegerCode } from '../../platform/PosixError.js';
+import { Exit } from './coreutil_lib/exit.js';
+
+const maxErrorNameLength = Object.keys(ErrorCodes)
+ .reduce((longest, name) => Math.max(longest, name.length), 0);
+const maxNumberLength = 3;
+
+async function printSingleErrno(errorCode, out) {
+ const metadata = ErrorMetadata.get(errorCode);
+ const paddedName = errorCode.description + ' '.repeat(maxErrorNameLength - errorCode.description.length);
+ const code = metadata.code.toString();
+ const paddedCode = ' '.repeat(maxNumberLength - code.length) + code;
+ await out.write(`${paddedName} ${paddedCode} ${metadata.description}\n`);
+}
+
+export default {
+ name: 'errno',
+ usage: 'errno [OPTIONS] [NAME-OR-CODE...]',
+ description: 'Look up and describe errno codes.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ list: {
+ description: 'List all errno values',
+ type: 'boolean',
+ short: 'l'
+ },
+ search: {
+ description: 'Search for errors whose descriptions contain NAME-OR-CODEs, case-insensitively',
+ type: 'boolean',
+ short: 's'
+ }
+ }
+ },
+ execute: async ctx => {
+ const { err, out } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+
+ if (values.search) {
+ for (const [errorCode, metadata] of ErrorMetadata) {
+ const description = metadata.description.toLowerCase();
+ let matches = true;
+ for (const nameOrCode of positionals) {
+ if (! description.includes(nameOrCode.toLowerCase())) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ await printSingleErrno(errorCode, out);
+ }
+ }
+ return;
+ }
+
+ if (values.list) {
+ for (const errorCode of ErrorMetadata.keys()) {
+ await printSingleErrno(errorCode, out);
+ }
+ return;
+ }
+
+ let failedToMatchSomething = false;
+ const fail = async (nameOrCode) => {
+ await err.write(`ERROR: Not understood: ${nameOrCode}\n`);
+ failedToMatchSomething = true;
+ };
+
+ for (const nameOrCode of positionals) {
+ let errorCode = ErrorCodes[nameOrCode.toUpperCase()];
+ if (errorCode) {
+ await printSingleErrno(errorCode, out);
+ continue;
+ }
+
+ const code = Number.parseInt(nameOrCode);
+ if (!isFinite(code)) {
+ await fail(nameOrCode);
+ continue;
+ }
+ errorCode = errorFromIntegerCode(code);
+ if (errorCode) {
+ await printSingleErrno(errorCode, out);
+ continue;
+ }
+
+ await fail(nameOrCode);
+ }
+
+ if (failedToMatchSomething) {
+ throw new Exit(1);
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/false.js b/packages/phoenix/src/puter-shell/coreutils/false.js
new file mode 100644
index 00000000..d888e53a
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/false.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'false',
+ usage: 'false',
+ description: 'Do nothing, and return a failure code.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ throw new Exit(1);
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/grep.js b/packages/phoenix/src/puter-shell/coreutils/grep.js
new file mode 100644
index 00000000..29351e1c
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/grep.js
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { resolveRelativePath } from '../../util/path.js';
+
+const lxor = (a, b) => a ? !b : b;
+
+import path_ from "path-browserify";
+
+export default {
+ name: 'grep',
+ usage: 'grep [OPTIONS] PATTERN FILE...',
+ description: 'Search FILE(s) for PATTERN, and print any matches.',
+ input: {
+ syncLines: true,
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ 'ignore-case': {
+ description: 'Match the pattern case-insensitively',
+ type: 'boolean',
+ short: 'i'
+ },
+ 'invert-match': {
+ description: 'Print lines that do not match the pattern',
+ type: 'boolean',
+ short: 'v'
+ },
+ 'line-number': {
+ description: 'Print the line number before each result',
+ type: 'boolean',
+ short: 'n'
+ },
+ recursive: {
+ description: 'Recursively search in directories',
+ type: 'boolean',
+ short: 'r'
+ }
+ },
+ },
+ output: 'text',
+ execute: async ctx => {
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ const [ pattern, ...files ] = positionals;
+
+ const do_grep_dir = async ( path ) => {
+ const entries = await filesystem.readdir(path);
+
+ for ( const entry of entries ) {
+ const entryPath = path_.join(path, entry.name);
+
+ if ( entry.type === 'directory' ) {
+ if ( values.recursive ) {
+ await do_grep_dir(entryPath);
+ }
+ } else {
+ await do_grep_file(entryPath);
+ }
+ }
+ }
+
+ const do_grep_line = async ( line ) => {
+ if ( line.endsWith('\n') ) line = line.slice(0, -1);
+ const re = new RegExp(
+ pattern,
+ values['ignore-case'] ? 'i' : ''
+ );
+
+ console.log(
+ 'Attempting to match line',
+ line,
+ 'with pattern',
+ pattern,
+ 'and re',
+ re,
+ 'and parameters',
+ values
+ );
+
+ if ( lxor(values['invert-match'], re.test(line)) ) {
+ const lineNumber = values['line-number'] ? i + 1 : '';
+ const lineToPrint =
+ lineNumber ? lineNumber + ':' : '' +
+ line;
+
+ console.log(`LINE{${lineToPrint}}`);
+ await ctx.externs.out.write(lineToPrint + '\n');
+ }
+ }
+
+ const do_grep_lines = async ( lines ) => {
+ for ( let i=0 ; i < lines.length ; i++ ) {
+ const line = lines[i];
+
+ await do_grep_line(line);
+ }
+ }
+
+ const do_grep_file = async ( path ) => {
+ console.log('about to read path', path);
+ const data_blob = await filesystem.read(path);
+ const data_string = await data_blob.text();
+
+ const lines = data_string.split('\n');
+
+ await do_grep_lines(lines);
+ }
+
+
+
+ if ( files.length === 0 ) {
+ if ( values.recursive ) {
+ files.push('.');
+ } else {
+ files.push('-');
+ }
+ }
+
+ console.log('FILES', files);
+
+ for ( let file of files ) {
+ if ( file === '-' ) {
+ for ( ;; ) {
+ const { value, done } = await ctx.externs.in_.read();
+ if ( done ) break;
+ await do_grep_line(value);
+ }
+ } else {
+ file = resolveRelativePath(ctx.vars, file);
+ const stat = await filesystem.stat(file);
+ if ( stat.is_dir ) {
+ if ( values.recursive ) {
+ await do_grep_dir(file);
+ } else {
+ await ctx.externs.err.write('grep: ' + file + ': Is a directory\n');
+ }
+ } else {
+ await do_grep_file(file);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/head.js b/packages/phoenix/src/puter-shell/coreutils/head.js
new file mode 100644
index 00000000..e96d8570
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/head.js
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+import { fileLines } from '../../util/file.js';
+
+export default {
+ name: 'head',
+ usage: 'head [OPTIONS] [FILE]',
+ description: 'Read a file and print the first lines to standard output.\n\n' +
+ 'Defaults to 10 lines unless --lines is given. ' +
+ 'If no FILE is provided, or FILE is `-`, read standard input.',
+ input: {
+ syncLines: true
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ lines: {
+ description: 'Print the last COUNT lines',
+ type: 'string',
+ short: 'n',
+ valueName: 'COUNT',
+ }
+ }
+ },
+ execute: async ctx => {
+ const { out, err } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+
+ if (positionals.length > 1) {
+ // TODO: Support multiple files (this is POSIX)
+ await err.write('head: Only one FILE parameter is allowed\n');
+ throw new Exit(1);
+ }
+ const relPath = positionals[0] || '-';
+
+ let lineCount = 10;
+
+ if (values.lines) {
+ const parsedLineCount = Number.parseFloat(values.lines);
+ if (isNaN(parsedLineCount) || ! Number.isInteger(parsedLineCount) || parsedLineCount < 1) {
+ await err.write(`head: Invalid number of lines '${values.lines}'\n`);
+ throw new Exit(1);
+ }
+ lineCount = parsedLineCount;
+ }
+
+ let processedLineCount = 0;
+ for await (const line of fileLines(ctx, relPath)) {
+ await out.write(line);
+ processedLineCount++;
+ if (processedLineCount >= lineCount)
+ break;
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/help.js b/packages/phoenix/src/puter-shell/coreutils/help.js
new file mode 100644
index 00000000..817074c0
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/help.js
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+// TODO: fetch help information from command registry
+
+import { printUsage } from "./coreutil_lib/help.js";
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'help',
+ usage: ['help', 'help COMMAND'],
+ description: 'Print help information for a specific command, or list available commands.\n\n' +
+ 'If COMMAND is provided, print the documentation for that command. ' +
+ 'Otherwise, list all the commands that are available.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ const { builtins } = ctx.registries;
+
+ const { out, err } = ctx.externs;
+
+ if (positionals.length > 1) {
+ await err.write('help: Too many arguments, expected 0 or 1\n');
+ throw new Exit(1);
+ }
+
+ if (positionals.length === 1) {
+ const commandName = positionals[0];
+ const command = builtins[commandName];
+ if (!command) {
+ await err.write(`help: No builtin found named '${commandName}'\n`);
+ throw new Exit(1);
+ }
+ await printUsage(command, out, ctx.vars);
+ return;
+ }
+
+ const heading = txt => {
+ out.write(`\x1B[34;1m~ ${txt} ~\x1B[0m\n`);
+ };
+
+ heading('available commands');
+ out.write('Use \x1B[34;1mhelp COMMAND-NAME\x1B[0m for more information\n');
+ for ( const k in builtins ) {
+ out.write(' - ' + k + '\n');
+ }
+ out.write('\n');
+ heading('available features');
+ out.write(' - pipes; ex: ls | tail -n 2\n')
+ out.write(' - redirects; ex: ls > some_file.txt\n')
+ out.write(' - simple tab completion\n')
+ out.write(' - in-memory command history\n')
+ out.write('\n');
+ heading('what\'s coming up?');
+ out.write(' - keep watching for \x1B[34;1mmore\x1B[0m (est: v0.1.11)\n')
+ // out.write(' - \x1B[34;1mcurl\x1B[0m up with your favorite terminal (est: TBA)\n')
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/jq.js b/packages/phoenix/src/puter-shell/coreutils/jq.js
new file mode 100644
index 00000000..ab3a641d
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/jq.js
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import jsonQuery from 'json-query';
+import { signals } from '../../ansi-shell/signals.js';
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'jq',
+ usage: 'jq FILTER [FILE...]',
+ description: 'Process JSON input FILE(s) according to FILTER.\n\n' +
+ 'Reads from standard input if no FILE is provided.',
+ input: {
+ syncLines: true,
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { externs } = ctx;
+ const { sdkv2 } = externs;
+
+ const { positionals } = ctx.locals;
+ const [query] = positionals;
+
+ // Read one line at a time
+ const { in_, out, err } = ctx.externs;
+
+ let rslv_sigint;
+ const p_int = new Promise(rslv => rslv_sigint = rslv);
+ ctx.externs.sig.on((signal) => {
+ if ( signal === signals.SIGINT ) {
+ rslv_sigint({ is_sigint: true });
+ }
+ });
+
+
+ let line, done;
+ const next_line = async () => {
+ let is_sigint = false;
+ ({ value: line, done, is_sigint } = await Promise.race([
+ p_int, in_.read(),
+ ]));
+ if ( is_sigint ) {
+ throw new Exit(130);
+ }
+ // ({ value: line, done } = await in_.read());
+ }
+ for ( await next_line() ; ! done ; await next_line() ) {
+ let data; try {
+ data = JSON.parse(line);
+ } catch (e) {
+ await err.write('Error: ' + e.message + '\n');
+ continue;
+ }
+ const result = jsonQuery(query, { data });
+ await out.write(JSON.stringify(result.value) + '\n');
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/login.js b/packages/phoenix/src/puter-shell/coreutils/login.js
new file mode 100644
index 00000000..e5117cfa
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/login.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'login',
+ usage: 'login',
+ description: 'Log in to a Puter.com account.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: false,
+ },
+ execute: async ctx => {
+ // ctx.params to access processed args
+ // ctx.args to access raw args
+ const { positionals, values } = ctx.locals;
+ const { puterSDK } = ctx.externs;
+
+ console.log('this is athe puter sdk', puterSDK);
+
+ if ( puterSDK.APIOrigin === undefined ) {
+ await ctx.externs.err.write('login: API origin not set\n');
+ throw new Exit(1);
+ }
+
+ const res = await puterSDK.auth.signIn();
+
+ ctx.vars.user = res?.username;
+ ctx.vars.home = '/' + res?.username;
+ ctx.vars.pwd = '/' + res?.username + `/AppData/` + puterSDK.appID;
+
+ return res?.username;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/ls.js b/packages/phoenix/src/puter-shell/coreutils/ls.js
new file mode 100644
index 00000000..c30d8fbe
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/ls.js
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import columnify from "columnify";
+import cli_columns from "cli-columns";
+import { resolveRelativePath } from '../../util/path.js';
+
+// formatLsTimestamp(): written by AI
+function formatLsTimestamp(unixTimestamp) {
+ const date = new Date(unixTimestamp * 1000); // Convert Unix timestamp to JavaScript Date
+ const now = new Date();
+
+ const optionsCurrentYear = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false };
+ const optionsPreviousYear = { month: 'short', day: 'numeric', year: 'numeric' };
+
+ // Check if the year of the date is the same as the current year
+ if (date.getFullYear() === now.getFullYear()) {
+ // Format for current year
+ return date.toLocaleString('en-US', optionsCurrentYear)
+ .replace(',', ''); // Remove comma from time);
+ } else {
+ // Format for previous year
+ return date.toLocaleString('en-US', optionsPreviousYear)
+ .replace(',', ''); // Remove comma from time);
+ }
+}
+
+const B_to_human_readable = B => {
+ const KiB = B / 1024;
+ const MiB = KiB / 1024;
+ const GiB = MiB / 1024;
+ const TiB = GiB / 1024;
+ if ( TiB > 1 ) {
+ return `${TiB.toFixed(3)} TiB`;
+ } else if ( GiB > 1 ) {
+ return `${GiB.toFixed(3)} GiB`;
+ } else if ( MiB > 1 ) {
+ return `${MiB.toFixed(3)} MiB`;
+ } else {
+ return `${KiB.toFixed(3)} KiB`;
+ }
+}
+
+export default {
+ name: 'ls',
+ usage: 'ls [OPTIONS] [PATH...]',
+ description: 'List directory contents.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ all: {
+ description: 'List all entries, including those starting with `.`',
+ type: 'boolean',
+ short: 'a'
+ },
+ long: {
+ description: 'List entries in long format, as a table',
+ type: 'boolean',
+ short: 'l'
+ },
+ 'human-readable': {
+ description: 'Print sizes in a human readable format (eg 12MiB, 3GiB), instead of byte counts',
+ type: 'boolean',
+ short: 'h'
+ },
+ time: {
+ description: 'Specify which time to display, one of atime (access time), ctime (creation time), or mtime (modification time)',
+ type: 'string'
+ },
+ S: {
+ description: 'Sort the results',
+ type: 'boolean',
+ },
+ t: {
+ description: 'Sort by time, newest first. See --time',
+ type: 'boolean',
+ },
+ reverse: {
+ description: 'Reverse the sort direction',
+ type: 'boolean',
+ short: 'r',
+ },
+ }
+ },
+ execute: async ctx => {
+ console.log('ls context', ctx);
+ console.log('env.COLS', ctx.env.COLS);
+ // ctx.params to access processed args
+ // ctx.args to access raw args
+ const { positionals, values, pwd } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ const paths = positionals.length < 1
+ ? [pwd] : positionals ;
+
+ const showHeadings = paths.length > 1 ? async ({ i, path }) => {
+ if ( i !== 0 ) await ctx.externs.out.write('\n');
+ await ctx.externs.out.write(path + ':\n');
+ } : () => {};
+
+ for ( let i=0 ; i < paths.length ; i++ ) {
+ let path = paths[i];
+ await showHeadings({ i, path });
+ path = resolveRelativePath(ctx.vars, path);
+ let result = await filesystem.readdir(path);
+ console.log('ls items', result);
+
+ if ( ! values.all ) {
+ result = result.filter(item => !item.name.startsWith('.'));
+ }
+
+ const reverse_sort = values.reverse;
+ const decsort = (delegate) => {
+ if ( ! reverse_sort ) return delegate;
+ return (a, b) => -delegate(a, b);
+ };
+
+ const time_properties = {
+ mtime: 'modified',
+ ctime: 'created',
+ atime: 'accessed',
+ };
+
+ if ( values.t ) {
+ const timeprop = time_properties[values.time || 'mtime'];
+ result = result.sort(decsort((a, b) => {
+ return b[timeprop] - a[timeprop];
+ }));
+ }
+
+ if ( values.S ) {
+ result = result.sort(decsort((a, b) => {
+ if ( a.is_dir && !b.is_dir ) return 1;
+ if ( !a.is_dir && b.is_dir ) return -1;
+ return b.size - a.size;
+ }));
+ }
+
+ // const write_item = values.long
+ // ? item => {
+ // let line = '';
+ // line += item.is_dir ? 'd' : item.is_symlink ? 'l' : '-';
+ // line += ' ';
+ // line += item.is_dir ? 'N/A' : item.size;
+ // line += ' ';
+ // line += item.name;
+ // return line;
+ // }
+ // : item => item.name
+ //
+ const icons = {
+ // d: '📁',
+ // l: '🔗',
+ };
+
+ const colors = {
+ 'd-': 'blue',
+ 'ds': 'magenta',
+ 'l-': 'cyan',
+ };
+
+ const col_to_ansi = {
+ blue: '34',
+ cyan: '36',
+ green: '32',
+ magenta: '35',
+ };
+
+ const col = (type, text) => {
+ if ( ! colors[type] ) return text;
+ return `\x1b[${col_to_ansi[colors[type]]};1m${text}\x1b[0m`;
+ }
+
+
+ const POSIX = filesystem.capabilities['readdir.posix-mode'];
+
+ const simpleTypeForItem = (item) => {
+ return (item.is_dir ? 'd' : item.is_symlink ? 'l' : '-')
+ + ( (item.subdomains && item.subdomains.length) ? 's' : '-' );
+ };
+
+ if ( values.long ) {
+ const time = values.time || 'mtime';
+ const items = result.map(item => {
+ const ts = item[time_properties[time]];
+ const www =
+ (!item.subdomains) ? 'N/A' :
+ (!item.subdomains.length) ? '---' :
+ item.subdomains[0].address + (
+ item.subdomains.length > 1
+ ? ` +${item.subdomains.length - 1}`
+ : ''
+ )
+ const type = simpleTypeForItem(item);
+ const mode = POSIX ? item.mode_human_readable : null;
+
+ let size = item.size;
+ if ( values['human-readable'] ) {
+ size = B_to_human_readable(size);
+ }
+ if ( item.is_dir && ! POSIX ) size = 'N/A';
+ return {
+ ...item,
+ user: item.uid,
+ group: item.gid,
+ mode,
+ type: icons[type] || type,
+ name: col(type, item.name),
+ www: www,
+ size: size,
+ [time_properties[time]]: formatLsTimestamp(ts),
+ };
+ });
+ const text = columnify(items, {
+ columns: [
+ POSIX ? 'mode' : 'type',
+ 'name',
+ ...(POSIX ? ['user', 'group'] : []),
+ ...(filesystem.capabilities['readdir.www'] ? ['www'] : []),
+ 'size', time_properties[time],
+ ],
+ maxLineWidth: ctx.env.COLS,
+ config: {
+ // json: {
+ // maxWidth: 20,
+ // }
+ }
+ });
+ const lines = text.split('\n');
+ for ( const line of lines ) {
+ await ctx.externs.out.write(line + '\n');
+ }
+ continue;
+ }
+
+ console.log('what is', cli_columns);
+
+ const names = result.map(item => {
+ return col(simpleTypeForItem(item), item.name);
+ });
+ const text = cli_columns(names, {
+ width: ctx.env.COLS,
+ })
+
+ const lines = text.split('\n');
+
+ for ( const line of lines ) {
+ await ctx.externs.out.write(line + '\n');
+ }
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/man.js b/packages/phoenix/src/puter-shell/coreutils/man.js
new file mode 100644
index 00000000..2c16c1de
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/man.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'man',
+ usage: 'man',
+ description: 'Stub command. Please use `help` instead.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ await ctx.externs.out.write('`\x1B[34;1mman\x1B[0m` is not supported. ' +
+ 'Please use `\x1B[34;1mhelp COMMAND\x1B[0m` for documentation.\n');
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/mkdir.js b/packages/phoenix/src/puter-shell/coreutils/mkdir.js
new file mode 100644
index 00000000..f14d32e6
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/mkdir.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { validate_string } from "./coreutil_lib/validate.js";
+import { EMPTY } from "../../util/singleton.js";
+import { Exit } from './coreutil_lib/exit.js';
+import { resolveRelativePath } from '../../util/path.js';
+
+// DRY: very similar to `cd`
+export default {
+ name: 'mkdir',
+ usage: 'mkdir [OPTIONS] PATH',
+ description: 'Create a directory at PATH.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ parents: {
+ description: 'Create parent directories if they do not exist. Do not treat existing directories as an error',
+ type: 'boolean',
+ short: 'p'
+ }
+ }
+ },
+ decorators: { errors: EMPTY },
+ execute: async ctx => {
+ // ctx.params to access processed args
+ // ctx.args to access raw args
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ let [ target ] = positionals;
+
+ try {
+ validate_string(target, { name: 'path' });
+ } catch (e) {
+ await ctx.externs.err.write(`mkdir: ${e.message}\n`);
+ throw new Exit(1);
+ }
+
+ target = resolveRelativePath(ctx.vars, target);
+
+ const result = await filesystem.mkdir(target, { createMissingParents: values.parents });
+
+ if ( result && result.$ === 'error' ) {
+ await ctx.externs.err.write(`mkdir: ${result.message}\n`);
+ throw new Exit(1);
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/mv.js b/packages/phoenix/src/puter-shell/coreutils/mv.js
new file mode 100644
index 00000000..bf05b50e
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/mv.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+import { resolveRelativePath } from '../../util/path.js';
+
+export default {
+ name: 'mv',
+ usage: 'mv SOURCE DESTINATION',
+ description: 'Move SOURCE file or directory to DESTINATION.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ const { out, err } = ctx.externs;
+ const { filesystem } = ctx.platform;
+
+ if ( positionals.length < 1 ) {
+ await err.write('mv: missing file operand\n');
+ throw new Exit(1);
+ }
+
+ const srcRelPath = positionals.shift();
+
+ if ( positionals.length < 1 ) {
+ const aft = positionals[0];
+ await err.write(`mv: missing destination file operand after '${aft}'\n`);
+ throw new Exit(1);
+ }
+
+ const dstRelPath = positionals.shift();
+
+ const srcAbsPath = resolveRelativePath(ctx.vars, srcRelPath);
+ let dstAbsPath = resolveRelativePath(ctx.vars, dstRelPath);
+
+ await filesystem.move(srcAbsPath, dstAbsPath);
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/neofetch.js b/packages/phoenix/src/puter-shell/coreutils/neofetch.js
new file mode 100644
index 00000000..2906b842
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/neofetch.js
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { SHELL_VERSIONS } from "../../meta/versions.js";
+
+const logo = `
+ ▄████▄ ▄▄█████▄_
+ _▄███▀▀██▀ ▐██▀¬ '▀█▄
+ ╓██└ ▐█▀ ▀█
+ (█▀ █▌ ██
+ █▌ ▄██▀▀▀██▄ ▀██▄
+ ██ ▐█ ██
+ █▌ ▐█
+ ▀█▄_ ▄█▀
+ '▀▀████ █████ ████▌ ██▀▀
+ ▐█ ██ █▌
+ ▐█ ██ █▌
+ _▄_▄██\` ██ '▀█▄_▄_
+ ╒█▀▀▀█▌ ██ ▐█▀\`▀█▄
+ ▐█▄_▄█▌ ╓████▄ ▐█▄_▄█▌
+ ▀▀▀\` █▌ ▐█ ▀▀▀\`
+ '▀███▀
+`.slice(1);
+
+export default {
+ name: 'neofetch',
+ usage: 'neofetch',
+ description: 'Print information about the system.',
+ execute: async ctx => {
+ const cols = [17,18,19,26,27].reverse();
+ const C25 = n => `\x1B[38;5;${n}m`;
+ const B25 = n => `\x1B[48;5;${n}m`;
+ const COL = C25(27);
+ const END = "\x1B[0m";
+ const lines = logo.split('\n').map(line => {
+ while ( line.length < 40 ) line += ' ';
+ return line;
+ });
+
+ for ( let i=0 ; i < lines.length ; i++ ) {
+ let ind = Math.floor(i / 5);
+ const col = cols[ind];
+ lines[i] = `\x1B[38;5;${col}m` + lines[i] + END;
+ }
+
+ {
+ const org = lines[9];
+ lines[9] = org.slice(0, 34) + C25(cols[2]) + org.slice(34);
+ }
+ {
+ let org = lines[10];
+ org = org.slice(10);
+ lines[10] = C25(cols[1]) + org.slice(0, 12) +
+ C25(cols[2]) + org.slice(12);
+ }
+
+ lines[0] += COL + ctx.env.USER + END + '@' +
+ COL + 'puter.com' + END;
+ lines[1] += '-----------------';
+ lines[2] += COL + 'OS' + END + ': Puter'
+ lines[3] += COL + 'Shell' + END + ': Puter Shell v' + SHELL_VERSIONS[0].v
+ lines[4] += COL + 'Window' + END + `: ${ctx.env.COLS}x${ctx.env.ROWS}`
+ lines[5] += COL + 'Commands' + END + `: ${Object.keys(ctx.registries.builtins).length}`
+
+ const colors = [[],[]];
+ for ( let i=0 ; i < 16 ; i++ ) {
+ let ri = i < 8 ? 14 : 15;
+ let esc = i < 9
+ ? `\x1B[3${i}m\x1B[4${i}m`
+ : C25(i)+B25(i) ;
+ lines[ri] += esc + ' ';
+ }
+ lines[14] += '\x1B[0m';
+ lines[15] += '\x1B[0m';
+
+ for ( const line of lines ) {
+ await ctx.externs.out.write(line + '\n');
+ }
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/printf.js b/packages/phoenix/src/puter-shell/coreutils/printf.js
new file mode 100644
index 00000000..0a99d87d
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/printf.js
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+// TODO: get these values from a common place
+// DRY: Copied from echo_escapes.js
+const BEL = String.fromCharCode(7);
+const BS = String.fromCharCode(8);
+const VT = String.fromCharCode(0x0B);
+const FF = String.fromCharCode(0x0C);
+
+function parseFormat(input, startOffset) {
+ let i = startOffset;
+
+ if (input[i] !== '%') {
+ throw new Error('Called parseFormat() without a format specifier!');
+ }
+ i++;
+
+ const result = {
+ flags: {
+ leftJustify: false,
+ prefixWithSign: false,
+ prefixWithSpaceIfWithoutSign: false,
+ alternativeForm: false,
+ padWithLeadingZeroes: false,
+ },
+ fieldWidth: null,
+ precision: null,
+ conversionSpecifier: null,
+
+ newOffset: startOffset,
+ };
+
+ // Output a single % for '%%' or '%' followed by the end of the input.
+ if (input[i] === '%') {
+ i++;
+ result.conversionSpecifier = '%';
+ result.newOffset = i;
+ return result;
+ }
+
+ const consumeInteger = () => {
+ const startIndex = i;
+ while (input[i] >= '0' && input[i] <= '9') {
+ i++;
+ }
+ if (startIndex === i) {
+ return null;
+ }
+
+ const integerString = input.substring(startIndex, i);
+ return Number.parseInt(integerString, 10);
+ };
+
+ // Flags
+ const possibleFlags = '-+ #0';
+ while (possibleFlags.includes(input[i])) {
+ switch (input[i]) {
+ case '-': result.flags.leftJustify = true; break;
+ case '+': result.flags.prefixWithSign = true; break;
+ case ' ': result.flags.prefixWithSpaceIfWithoutSign = true; break;
+ case '#': result.flags.alternativeForm = true; break;
+ case '0': result.flags.padWithLeadingZeroes = true; break;
+ }
+ i++;
+ }
+
+ // Field width
+ result.fieldWidth = consumeInteger();
+
+ // Precision
+ if (input[i] === '.') {
+ i++;
+ result.precision = consumeInteger() || 0;
+ }
+
+ // Conversion specifier
+ const possibleConversionSpecifiers = 'cdeEfFgGiousxX';
+ if (possibleConversionSpecifiers.includes(input[i])) {
+ result.conversionSpecifier = input[i];
+ i++;
+ } else {
+ throw new Error(`Invalid conversion specifier '${input.substring(startOffset, i + 1)}'`);
+ }
+
+ result.newOffset = i;
+ return result;
+}
+
+function formatOutput(parsedFormat, remainingArguments) {
+ const { flags, fieldWidth, precision, conversionSpecifier } = parsedFormat;
+
+ const padAndAlignString = (input) => {
+ if (!fieldWidth || input.length >= fieldWidth) {
+ return input;
+ }
+
+ const padding = ' '.repeat(fieldWidth - input.length);
+ return flags.leftJustify ? (input + padding) : (padding + input);
+ };
+
+ const formatInteger = (integer, specifier) => {
+ const unsigned = 'ouxX'.includes(specifier);
+ const radix = (() => {
+ switch (specifier) {
+ case 'o': return 8;
+ case 'x':
+ case 'X': return 16;
+ default: return 10;
+ }
+ })();
+
+ // POSIX doesn't specify what we should do to format a negative number as %u.
+ // Common behavior seems to be bit-casting it to unsigned.
+ if (unsigned && integer < 0) {
+ integer = integer >>> 0;
+ }
+
+ let digits = Math.abs(integer).toString(radix);
+ if (specifier === 'o' && flags.alternativeForm && digits[0] !== '0') {
+ // "For the o conversion specifier, it shall increase the precision to force the first digit of the result to be a zero."
+ // (Where 'it' is the alternative form flag.)
+ digits = '0' + digits;
+ }
+ const signOrPrefix = (() => {
+ if (flags.alternativeForm) {
+ if (specifier === 'x') return '0x';
+ if (specifier === 'X') return '0X';
+ }
+ if (unsigned) return '';
+ if (integer < 0) return '-';
+ if (flags.prefixWithSign) return '+';
+ if (flags.prefixWithSpaceIfWithoutSign) return ' ';
+ return '';
+ })();
+
+ // Expand digits with 0s, up to `precision` characters.
+ // "The default precision shall be 1."
+ const usedPrecision = precision ?? 1;
+ // Special case: "The result of converting a zero value with a precision of 0 shall be no characters."
+ if (usedPrecision === 0 && integer === 0) {
+ digits = '';
+ } else if (digits.length < precision) {
+ digits = '0'.repeat(precision - digits.length) + digits;
+ }
+
+ // Pad up to `fieldWidth` with spaces or 0s.
+ const width = signOrPrefix.length + digits.length;
+ let output = signOrPrefix + digits;
+ if (width < fieldWidth) {
+ if (flags.leftJustify) {
+ output = signOrPrefix + digits + ' '.repeat(fieldWidth - width);
+ } else if (precision === null && flags.padWithLeadingZeroes) {
+ // "For d, i , o, u, x, and X conversion specifiers, if a precision is specified, the '0' flag shall be ignored."
+ output = signOrPrefix + '0'.repeat(fieldWidth - width) + digits;
+ } else {
+ output = ' '.repeat(fieldWidth - width) + signOrPrefix + digits;
+ }
+ }
+
+ if (specifier === specifier.toUpperCase()) {
+ output = output.toUpperCase();
+ }
+
+ return output;
+ };
+
+ const formatFloat = (float, specifier) => {
+ if (float === undefined) float = 0;
+
+ const sign = (() => {
+ if (float < 0) return '-';
+ if (flags.prefixWithSign) return '+';
+ if (flags.prefixWithSpaceIfWithoutSign) return ' ';
+ return '';
+ })();
+ const floatString = (() => {
+ // NaN and Infinity are the same regardless of representation
+ if (!isFinite(float)) {
+ return float.toString();
+ }
+
+ const formatExponential = (mantissaString, exponent) => {
+ // #: "For [...] e, E, [...] conversion specifiers, the result shall always contain a radix character,
+ // even if no digits follow the radix character."
+ if (flags.alternativeForm && !mantissaString.includes('.')) {
+ mantissaString += '.';
+ }
+
+ // "The exponent shall always contain at least two digits."
+ const exponentOutput = (() => {
+ if (exponent <= -10 || exponent >= 10) return exponent.toString();
+ if (exponent < 0) return '-0' + Math.abs(exponent).toString();
+ return '+0' + Math.abs(exponent).toString();
+ })();
+ return mantissaString + 'e' + exponentOutput;
+ };
+
+ switch (specifier) {
+ // TODO: %a and %A, floats in hexadecimal
+ case 'e':
+ case 'E': {
+ // "When the precision is missing, six digits shall be written after the radix character"
+ const usedPrecision = precision ?? 6;
+ // We unfortunately can't fully rely on toExponential() because printf has different formatting rules.
+ const [mantissaString, exponentString] = Math.abs(float).toExponential(usedPrecision).split('e');
+ const exponent = Number.parseInt(exponentString);
+ return formatExponential(mantissaString, exponent);
+ }
+ case 'f':
+ case 'F': {
+ // "If the precision is omitted from the argument, six digits shall be written after the radix character"
+ const usedPrecision = precision ?? 6;
+ const result = Math.abs(float).toFixed(usedPrecision);
+ if (flags.alternativeForm && usedPrecision === 0) {
+ // #: "For [...] f, F, [...] conversion specifiers, the result shall always contain a radix character,
+ // even if no digits follow the radix character."
+ return result + '.';
+ }
+ return result;
+ }
+ case 'g':
+ case 'G': {
+ // Default isn't specified in the spec, but 6 matches behavior of other implementations.
+ const usedPrecision = precision ?? 6;
+
+ // "The style used depends on the value converted: style e (or E) shall be used only if the exponent
+ // resulting from the conversion is less than -4 or greater than or equal to the precision."
+ // We add a digit of precision to make sure we don't break things when rounding later.
+ const [mantissaString, exponentString] = Math.abs(float).toExponential(usedPrecision + 1).split('e');
+ const mantissa = Number.parseFloat(mantissaString);
+ const exponent = Number.parseInt(exponentString);
+
+ // Unfortunately, `float.toPrecision()` doesn't use the same rules as printf to decide whether to
+ // use decimal or exponential representation, so we have to construct the output ourselves.
+ const usingExponential = exponent > usedPrecision || exponent < -4;
+ if (usingExponential) {
+ const decimalDigits = Math.max(0, usedPrecision - (mantissa < 1 ? 0 : 1));
+ // "Trailing zeros are removed from the result."
+ let mantissaOutput = mantissa.toFixed(decimalDigits)
+ .replace(/\.0+/, '');
+ return formatExponential(mantissaOutput, exponent);
+ }
+
+ // Decimal representation
+ const result = Math.abs(float).toPrecision(usedPrecision);
+ if (flags.alternativeForm && usedPrecision === 0) {
+ // #: "For [...] g, and G conversion specifiers, the result shall always contain a radix character,
+ // even if no digits follow the radix character."
+ return result + '.';
+ }
+ // Trailing zeros are removed from the result.
+ return result.replace(/\.0+/, '');
+ }
+ default: throw new Error(`Invalid float specifier '${specifier}'`);
+ }
+ })();
+
+ // Pad up to `fieldWidth` with spaces or 0s.
+ const width = sign.length + floatString.length;
+ let output = sign + floatString;
+ if (width < fieldWidth) {
+ if (flags.leftJustify) {
+ output = sign + floatString + ' '.repeat(fieldWidth - width);
+ } else if (flags.padWithLeadingZeroes && isFinite(float)) {
+ output = sign + '0'.repeat(fieldWidth - width) + floatString;
+ } else {
+ output = ' '.repeat(fieldWidth - width) + sign + floatString;
+ }
+ }
+
+ if (specifier === specifier.toUpperCase()) {
+ output = output.toUpperCase();
+ } else {
+ output = output.toLowerCase();
+ }
+
+ return output;
+ };
+
+ switch (conversionSpecifier) {
+ // TODO: a,A: Float in hexadecimal format
+ // TODO: b: binary data with escapes
+ // TODO: Any other common options that are not in the posix spec
+
+ // Integers
+ case 'd':
+ case 'i':
+ case 'o':
+ case 'u':
+ case 'x':
+ case 'X': {
+ return formatInteger(Number.parseInt(remainingArguments.shift()) || 0, conversionSpecifier);
+ }
+
+ // Floating point numbers
+ case 'e':
+ case 'E':
+ case 'f':
+ case 'F':
+ case 'g':
+ case 'G': {
+ return formatFloat(Number.parseFloat(remainingArguments.shift()), conversionSpecifier);
+ }
+
+ // Single character
+ case 'c': {
+ const argument = remainingArguments.shift() || '';
+ // It's unspecified whether an empty string produces a null byte or nothing.
+ // We'll go with nothing for now.
+ return padAndAlignString(argument[0] || '');
+ }
+
+ // String
+ case 's': {
+ let argument = remainingArguments.shift() || '';
+ if (precision && precision < argument.length) {
+ argument = argument.substring(0, precision);
+ }
+ return padAndAlignString(argument);
+ }
+
+ // Percent sign
+ case '%': return '%';
+ }
+}
+
+function highlight(text) {
+ return `\x1B[92m${text}\x1B[0m`;
+}
+
+// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/printf.html
+export default {
+ name: 'printf',
+ usage: 'printf FORMAT [ARGUMENT...]',
+ description: 'Write a formatted string to standard output.\n\n' +
+ 'The output is determined by FORMAT, with any escape sequences replaced, and any format strings applied to the following ARGUMENTs.\n\n' +
+ 'FORMAT is written repeatedly until all ARGUMENTs are consumed. If FORMAT does not consume any ARGUMENTs, it is only written once.',
+ helpSections: {
+ 'Escape Sequences': 'The following escape sequences are understood:\n\n' +
+ ` ${highlight('\\\\')} A literal \\\n` +
+ ` ${highlight('\\a')} Terminal BELL\n` +
+ ` ${highlight('\\b')} Backspace\n` +
+ ` ${highlight('\\f')} Form-feed\n` +
+ ` ${highlight('\\n')} Newline\n` +
+ ` ${highlight('\\r')} Carriage return\n` +
+ ` ${highlight('\\t')} Horizontal tab\n` +
+ ` ${highlight('\\v')} Vertical tab\n` +
+ ` ${highlight('\\###')} A byte with the octal value of ### (between 1 and 3 digits)`,
+ 'Format Strings': 'Format strings behave like C printf. ' +
+ 'A format string is, in order: a `%`, zero or more flags, a width, a precision, and a conversion specifier. ' +
+ 'All except the initial `%` and the conversion specifier are optional.\n\n' +
+ 'Flags:\n\n' +
+ ` ${highlight('-')} Left-justify the result\n` +
+ ` ${highlight('+')} For numeric types, always include a sign character\n` +
+ ` ${highlight('\' \'')} ${highlight('(space)')} For numeric types, include a space where the sign would go for positive numbers. Overridden by ${highlight('+')}.\n`+
+ ` ${highlight('#')} Use alternative form, depending on the conversion:\n` +
+ ` ${highlight('o')} Ensure result is always prefixed with a '0'\n` +
+ ` ${highlight('x,X')} Prefix result with '0x' or '0X' respectively\n` +
+ ` ${highlight('e,E,f,F,g,G')} Always include a decimal point. For ${highlight('g,G')}, also keep trailing 0s\n\n` +
+ 'Width:\n\n' +
+ 'A number, for how many characters the result should occupy.\n\n' +
+ 'Precision:\n\n' +
+ 'A \'.\' followed optionally by a number. If no number is specified, it is taken as 0. Effect depends on the conversion:\n\n' +
+ ` ${highlight('d,i,o,u,x,X')} Determines the minimum number of digits\n` +
+ ` ${highlight('e,E,f,F')} Determines the number of digits after the decimal point\n\n` +
+ ` ${highlight('g,G')} Determines the number of significant figures\n\n` +
+ ` ${highlight('s')} Determines the maximum number of characters to be printed\n\n` +
+ 'Conversion specifiers:\n\n' +
+ ` ${highlight('%')} A literal '%'\n` +
+ ` ${highlight('s')} ARGUMENT as a string\n` +
+ ` ${highlight('c')} The first character of ARGUMENT as a string\n` +
+ ` ${highlight('d,i')} ARGUMENT as a number, formatted as a signed decimal integer\n` +
+ ` ${highlight('u')} ARGUMENT as a number, formatted as an unsigned decimal integer\n` +
+ ` ${highlight('o')} ARGUMENT as a number, formatted as an unsigned octal integer\n` +
+ ` ${highlight('x,X')} ARGUMENT as a number, formatted as an unsigned hexadecimal integer, in lower or uppercase respectively\n` +
+ ` ${highlight('e,E')} ARGUMENT as a number, formatted as a float in exponential notation, in lower or uppercase respectively\n` +
+ ` ${highlight('f,F')} ARGUMENT as a number, formatted as a float in decimal notation, in lower or uppercase respectively\n` +
+ ` ${highlight('g,G')} ARGUMENT as a number, formatted as a float in either decimal or exponential notation, in lower or uppercase respectively`,
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ const { out, err } = ctx.externs;
+ const { positionals } = ctx.locals;
+ const [ format, ...remainingArguments ] = ctx.locals.positionals;
+
+ if (positionals.length === 0) {
+ await err.write('printf: Missing format argument\n');
+ throw new Exit(1);
+ }
+
+ // We process the format as many times as needed to consume all of remainingArguments, but always at least once.
+ do {
+ const previousRemainingArgumentCount = remainingArguments.length;
+ let output = '';
+
+ for (let i = 0; i < format.length; ++i) {
+ let char = format[i];
+ // Escape sequences
+ if (char === '\\') {
+ char = format[++i];
+ switch (char) {
+ case undefined: {
+ // We reached the end of the string, just output the slash.
+ output += '\\';
+ break;
+ }
+ case '\\': output += '\\'; break;
+ case 'a': output += BEL; break;
+ case 'b': output += BS; break;
+ case 'f': output += FF; break;
+ case 'n': output += '\n'; break;
+ case 'r': output += '\r'; break;
+ case 't': output += '\t'; break;
+ case 'v': output += VT; break;
+ default: {
+ // 1 to 3-digit octal number
+ if (char >= '0' && char <= '9') {
+ const digitsStartI = i;
+ if (format[i+1] >= '0' && format[i+1] <= '9') {
+ i++;
+ if (format[i+1] >= '0' && format[i+1] <= '9') {
+ i++;
+ }
+ }
+
+ const octalString = format.substring(digitsStartI, i + 1);
+ const octalValue = Number.parseInt(octalString, 8);
+ output += String.fromCodePoint(octalValue);
+ break;
+ }
+
+ // Unrecognized, so just output the sequence verbatim.
+ output += '\\' + char;
+ break;
+ }
+ }
+ continue;
+ }
+
+ // Conversion specifiers
+ if (char === '%') {
+ // Parse the conversion specifier
+ let parsedFormat;
+ try {
+ parsedFormat = parseFormat(format, i);
+ } catch (e) {
+ await err.write(`printf: ${e.message}\n`);
+ throw new Exit(1);
+ }
+ i = parsedFormat.newOffset - 1; // -1 because we're about to increment i in the loop header
+
+ // Output the result
+ output += formatOutput(parsedFormat, remainingArguments);
+ continue;
+ }
+
+ // Everything else is copied directly.
+ // TODO: Append these to the output in batches, for performance?
+ output += char;
+ }
+
+ await out.write(output);
+
+ // "If the format operand contains no conversion specifications and argument operands are present, the results are unspecified."
+ // We handle this by printing it once and stopping.
+ if (remainingArguments.length === previousRemainingArgumentCount) {
+ break;
+ }
+ } while (remainingArguments.length > 0);
+
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/printhist.js b/packages/phoenix/src/puter-shell/coreutils/printhist.js
new file mode 100644
index 00000000..d740fcc6
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/printhist.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'printhist',
+ usage: 'printhist',
+ description: 'Print shell history.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { historyManager } = ctx.externs;
+ console.log('test????', ctx);
+ for ( const item of historyManager.items ) {
+ await ctx.externs.out.write(item + '\n');
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/puter-shell/coreutils/pwd.js b/packages/phoenix/src/puter-shell/coreutils/pwd.js
new file mode 100644
index 00000000..1fd5907f
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/pwd.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export default {
+ name: 'pwd',
+ usage: 'pwd',
+ description: 'Print the current working directory.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: false,
+ },
+ execute: async ctx => {
+ await ctx.externs.out.write(ctx.vars.pwd + '\n');
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/rm.js b/packages/phoenix/src/puter-shell/coreutils/rm.js
new file mode 100644
index 00000000..358e41b5
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/rm.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { resolveRelativePath } from '../../util/path.js';
+
+// TODO: add logic to check if directory is empty
+// TODO: add check for `--dir`
+// TODO: allow multiple paths
+
+// DRY: very similar to `cd`
+export default {
+ name: 'rm',
+ usage: 'rm [OPTIONS] PATH',
+ description: 'Remove the file or directory at PATH.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ dir: {
+ description: 'Remove empty directories',
+ type: 'boolean',
+ short: 'd'
+ },
+ recursive: {
+ description: 'Recursively remove directories and their contents',
+ type: 'boolean',
+ short: 'r'
+ },
+ force: {
+ description: 'Ignore non-existent paths, and never prompt',
+ type: 'boolean',
+ short: 'f'
+ }
+ }
+ },
+ execute: async ctx => {
+ // ctx.params to access processed args
+ // ctx.args to access raw args
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ let [ target ] = positionals;
+ target = resolveRelativePath(ctx.vars, target);
+
+ await filesystem.rm(target, { recursive: values.recursive })
+ }
+};
+
+
diff --git a/packages/phoenix/src/puter-shell/coreutils/rmdir.js b/packages/phoenix/src/puter-shell/coreutils/rmdir.js
new file mode 100644
index 00000000..d03048f0
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/rmdir.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { resolveRelativePath } from '../../util/path.js';
+
+// TODO: add logic to check if directory is empty
+// TODO: add check for `--dir`
+// TODO: allow multiple paths
+
+// DRY: very similar to `cd`
+export default {
+ name: 'rmdir',
+ usage: 'rmdir [OPTIONS] DIRECTORY',
+ description: 'Remove the DIRECTORY if it is empty.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ parents: {
+ description: 'Also remove empty parent directories',
+ type: 'boolean',
+ short: 'p'
+ }
+ }
+ },
+ execute: async ctx => {
+ // ctx.params to access processed args
+ // ctx.args to access raw args
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ let [ target ] = positionals;
+ target = resolveRelativePath(ctx.vars, target);
+
+ await filesystem.rmdir(target);
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/sample-data.js b/packages/phoenix/src/puter-shell/coreutils/sample-data.js
new file mode 100644
index 00000000..039b6900
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/sample-data.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'sample-data',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ const [ what ] = positionals;
+
+ if ( what === 'blob' ) {
+ // Hello world blob
+ const blob = new Blob([ 'Hello, world!' ]);
+ console.log('before writing');
+ await ctx.externs.out.write(blob);
+ console.log('after writing');
+ return;
+ }
+
+ console.log('before writing');
+ await ctx.externs.out.write('Hello, World!\n');
+ console.log('after writing');
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/sed.js b/packages/phoenix/src/puter-shell/coreutils/sed.js
new file mode 100644
index 00000000..99d46e9a
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/sed.js
@@ -0,0 +1,725 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+import { fileLines } from '../../util/file.js';
+
+function makeIndent(size) {
+ return ' '.repeat(size);
+}
+
+// Either a line number or a regex
+class Address {
+ constructor(value) {
+ this.value = value;
+ }
+
+ matches(lineNumber, line) {
+ if (this.value instanceof RegExp) {
+ return this.value.test(line);
+ }
+ return this.value === lineNumber;
+ }
+
+ isLineNumberBefore(lineNumber) {
+ return (typeof this.value === 'number') && this.value < lineNumber;
+ }
+
+ dump(indent) {
+ if (this.value instanceof RegExp) {
+ return `${makeIndent(indent)}REGEX: ${this.value}\n`;
+ }
+ return `${makeIndent(indent)}LINE: ${this.value}\n`;
+ }
+}
+
+class AddressRange {
+ // Three kinds of AddressRange:
+ // - Empty (includes everything)
+ // - Single (matches individual line)
+ // - Range (matches lines between start and end, inclusive)
+ constructor({ start, end, inverted = false } = {}) {
+ this.start = start;
+ this.end = end;
+ this.inverted = inverted;
+ this.insideRange = false;
+ this.leaveRangeNextLine = false;
+ }
+
+ updateMatchState(lineNumber, line) {
+ // Only ranges have a state to update
+ if (!(this.start && this.end)) {
+ return;
+ }
+
+ // Reset our state each time we start a new file.
+ if (lineNumber === 1) {
+ this.insideRange = false;
+ this.leaveRangeNextLine = false;
+ }
+
+ // Leave the range if the previous line matched the end.
+ if (this.leaveRangeNextLine) {
+ this.insideRange = false;
+ this.leaveRangeNextLine = false;
+ }
+
+ if (this.insideRange) {
+ // We're inside the range, does this line end it?
+ // If the end address is a line number in the past, yes, immediately.
+ if (this.end.isLineNumberBefore(lineNumber)) {
+ this.insideRange = false;
+ return;
+ }
+ // If the line matches the end address, include it but leave the range on the next line.
+ this.leaveRangeNextLine = this.end.matches(lineNumber, line);
+ } else {
+ // Does this line start the range?
+ this.insideRange = this.start.matches(lineNumber, line);
+ }
+ }
+
+ matches(lineNumber, line) {
+ const invertIfNeeded = (value) => {
+ return this.inverted ? !value : value;
+ };
+
+ // Empty - matches all lines
+ if (!this.start) {
+ return invertIfNeeded(true);
+ }
+
+ // Range
+ if (this.end) {
+ return invertIfNeeded(this.insideRange);
+ }
+
+ // Single
+ return invertIfNeeded(this.start.matches(lineNumber, line));
+ }
+
+ dump(indent) {
+ const inverted = this.inverted ? `${makeIndent(indent+1)}(INVERTED)\n` : '';
+
+ if (!this.start) {
+ return `${makeIndent(indent)}ADDRESS RANGE (EMPTY)\n`
+ + inverted;
+ }
+
+ if (this.end) {
+ return `${makeIndent(indent)}ADDRESS RANGE (RANGE):\n`
+ + inverted
+ + this.start.dump(indent+1)
+ + this.end.dump(indent+1);
+ }
+
+ return `${makeIndent(indent)}ADDRESS RANGE (SINGLE):\n`
+ + this.start.dump(indent+1)
+ + inverted;
+ }
+}
+
+const JumpLocation = {
+ None: Symbol('None'),
+ EndOfCycle: Symbol('EndOfCycle'),
+ StartOfCycle: Symbol('StartOfCycle'),
+ Label: Symbol('Label'),
+ Quit: Symbol('Quit'),
+ QuitSilent: Symbol('QuitSilent'),
+};
+
+class Command {
+ constructor(addressRange) {
+ this.addressRange = addressRange ?? new AddressRange();
+ }
+
+ updateMatchState(context) {
+ this.addressRange.updateMatchState(context.lineNumber, context.patternSpace);
+ }
+
+ async runCommand(context) {
+ if (this.addressRange.matches(context.lineNumber, context.patternSpace)) {
+ return await this.run(context);
+ }
+ return JumpLocation.None;
+ }
+
+ async run(context) {
+ throw new Error('run() not implemented for ' + this.constructor.name);
+ }
+
+ dump(indent) {
+ throw new Error('dump() not implemented for ' + this.constructor.name);
+ }
+}
+
+// '{}' - Group other commands
+class GroupCommand extends Command {
+ constructor(addressRange, subCommands) {
+ super(addressRange);
+ this.subCommands = subCommands;
+ }
+
+ updateMatchState(context) {
+ super.updateMatchState(context);
+ for (const command of this.subCommands) {
+ command.updateMatchState(context);
+ }
+ }
+
+ async run(context) {
+ for (const command of this.subCommands) {
+ const result = await command.runCommand(context);
+ if (result !== JumpLocation.None) {
+ return result;
+ }
+ }
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}GROUP:\n`
+ + this.addressRange.dump(indent+1)
+ + `${makeIndent(indent+1)}CHILDREN:\n`
+ + this.subCommands.map(command => command.dump(indent+2)).join('');
+ }
+}
+
+// '=' - Output line number
+class LineNumberCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ await context.out.write(`${context.lineNumber}\n`);
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}LINE-NUMBER:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'a' - Append text
+class AppendTextCommand extends Command {
+ constructor(addressRange, text) {
+ super(addressRange);
+ this.text = text;
+ }
+
+ async run(context) {
+ context.queuedOutput += this.text + '\n';
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}APPEND-TEXT:\n`
+ + this.addressRange.dump(indent+1)
+ + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`;
+ }
+}
+
+// 'c' - Replace line with text
+class ReplaceCommand extends Command {
+ constructor(addressRange, text) {
+ super(addressRange);
+ this.text = text;
+ }
+
+ async run(context) {
+ context.patternSpace = '';
+ // Output if we're either a 0-address range, 1-address range, or 2-address on the last line.
+ if (this.addressRange.leaveRangeNextLine || !this.addressRange.end) {
+ await context.out.write(this.text + '\n');
+ }
+ return JumpLocation.EndOfCycle;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}REPLACE-TEXT:\n`
+ + this.addressRange.dump(indent+1)
+ + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`;
+ }
+}
+
+// 'd' - Delete pattern
+class DeleteCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ context.patternSpace = '';
+ return JumpLocation.EndOfCycle;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}DELETE:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'D' - Delete first line of pattern
+class DeleteLineCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ const [ firstLine, rest ] = context.patternSpace.split('\n', 2);
+ context.patternSpace = rest ?? '';
+ if (rest === undefined) {
+ return JumpLocation.EndOfCycle;
+ }
+ return JumpLocation.StartOfCycle;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}DELETE-LINE:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'g' - Get the held line into the pattern
+class GetCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ context.patternSpace = context.holdSpace;
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}GET-HELD:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'G' - Get the held line and append it to the pattern
+class GetAppendCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ context.patternSpace += '\n' + context.holdSpace;
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}GET-HELD-APPEND:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'h' - Hold the pattern
+class HoldCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ context.holdSpace = context.patternSpace;
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}HOLD:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'H' - Hold append the pattern
+class HoldAppendCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ context.holdSpace += '\n' + context.patternSpace;
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}HOLD-APPEND:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'i' - Insert text
+class InsertTextCommand extends Command {
+ constructor(addressRange, text) {
+ super(addressRange);
+ this.text = text;
+ }
+
+ async run(context) {
+ await context.out.write(this.text + '\n');
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}INSERT-TEXT:\n`
+ + this.addressRange.dump(indent+1)
+ + `${makeIndent(indent+1)}CONTENTS: '${this.text}'\n`;
+ }
+}
+
+// 'l' - Print pattern in debug format
+class DebugPrintCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ let output = '';
+ for (const c of context.patternSpace) {
+ if (c < ' ') {
+ const charCode = c.charCodeAt(0);
+ switch (charCode) {
+ case 0x07: output += '\\a'; break;
+ case 0x08: output += '\\b'; break;
+ case 0x0C: output += '\\f'; break;
+ case 0x0A: output += '$\n'; break;
+ case 0x0D: output += '\\r'; break;
+ case 0x09: output += '\\t'; break;
+ case 0x0B: output += '\\v'; break;
+ default: {
+ const octal = charCode.toString(8);
+ output += '\\' + '0'.repeat(3 - octal.length) + octal;
+ }
+ }
+ } else if (c === '\\') {
+ output += '\\\\';
+ } else {
+ output += c;
+ }
+ }
+ await context.out.write(output);
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}DEBUG-PRINT:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'p' - Print pattern
+class PrintCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ await context.out.write(context.patternSpace);
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}PRINT:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'P' - Print first line of pattern
+class PrintLineCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ const firstLine = context.patternSpace.split('\n', 2)[0];
+ await context.out.write(firstLine);
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}PRINT-LINE:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'q' - Quit
+class QuitCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ return JumpLocation.Quit;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}QUIT:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'Q' - Quit, suppressing the default output
+class QuitSilentCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ return JumpLocation.QuitSilent;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}QUIT-SILENT:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'x' - Exchange hold and pattern
+class ExchangeCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ const oldPattern = context.patternSpace;
+ context.patternSpace = context.holdSpace;
+ context.holdSpace = oldPattern;
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}EXCHANGE:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+// 'y' - Transliterate characters
+class TransliterateCommand extends Command {
+ constructor(addressRange, inputCharacters, replacementCharacters) {
+ super(addressRange);
+ this.inputCharacters = inputCharacters;
+ this.replacementCharacters = replacementCharacters;
+
+ if (inputCharacters.length !== replacementCharacters.length) {
+ throw new Error('inputCharacters and replacementCharacters must be the same length!');
+ }
+ }
+
+ async run(context) {
+ let newPatternSpace = '';
+ for (let i = 0; i < context.patternSpace.length; ++i) {
+ const char = context.patternSpace[i];
+ const replacementIndex = this.inputCharacters.indexOf(char);
+ if (replacementIndex !== -1) {
+ newPatternSpace += this.replacementCharacters[replacementIndex];
+ continue;
+ }
+ newPatternSpace += char;
+ }
+ context.patternSpace = newPatternSpace;
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}TRANSLITERATE:\n`
+ + this.addressRange.dump(indent+1)
+ + `${makeIndent(indent+1)}FROM '${this.inputCharacters}'\n`
+ + `${makeIndent(indent+1)}TO '${this.replacementCharacters}'\n`;
+ }
+}
+
+// 'z' - Zap, delete the pattern without ending cycle
+class ZapCommand extends Command {
+ constructor(addressRange) {
+ super(addressRange);
+ }
+
+ async run(context) {
+ context.patternSpace = '';
+ return JumpLocation.None;
+ }
+
+ dump(indent) {
+ return `${makeIndent(indent)}ZAP:\n`
+ + this.addressRange.dump(indent+1);
+ }
+}
+
+const CycleResult = {
+ Continue: Symbol('Continue'),
+ Quit: Symbol('Quit'),
+ QuitSilent: Symbol('QuitSilent'),
+};
+
+class Script {
+ constructor(commands) {
+ this.commands = commands;
+ }
+
+ async runCycle(context) {
+ for (let i = 0; i < this.commands.length; i++) {
+ const command = this.commands[i];
+ command.updateMatchState(context);
+ const result = await command.runCommand(context);
+ switch (result) {
+ case JumpLocation.Label:
+ // TODO: Implement labels
+ break;
+ case JumpLocation.Quit:
+ return CycleResult.Quit;
+ case JumpLocation.QuitSilent:
+ return CycleResult.QuitSilent;
+ case JumpLocation.StartOfCycle:
+ i = -1; // To start at 0 after the loop increment.
+ continue;
+ case JumpLocation.EndOfCycle:
+ return CycleResult.Continue;
+ case JumpLocation.None:
+ continue;
+ }
+ }
+ }
+
+ dump() {
+ return `SCRIPT:\n`
+ + this.commands.map(command => command.dump(1)).join('');
+ }
+}
+
+function parseScript(scriptString) {
+ const commands = [];
+
+ // Generate a hard-coded script for now.
+ // TODO: Actually parse input!
+
+ commands.push(new TransliterateCommand(new AddressRange(), 'abcdefABCDEF', 'ABCDEFabcdef'));
+ // commands.push(new ZapCommand(new AddressRange({start: new Address(1), end: new Address(10)})));
+ // commands.push(new HoldAppendCommand(new AddressRange({start: new Address(1), end: new Address(10)})));
+ // commands.push(new GetCommand(new AddressRange({start: new Address(11)})));
+ // commands.push(new DebugPrintCommand(new AddressRange()));
+
+ // commands.push(new ReplaceCommand(new AddressRange({start: new Address(3), end: new Address(30)}), "LOL"));
+
+ // commands.push(new GroupCommand(new AddressRange({ start: new Address(5), end: new Address(10) }), [
+ // // new LineNumberCommand(),
+ // // new TextCommand(new AddressRange({ start: new Address(8) }), "Well hello friends! :^)"),
+ // new QuitCommand(new AddressRange({ start: new Address(8) })),
+ // new NoopCommand(new AddressRange()),
+ // new PrintCommand(new AddressRange({ start: new Address(2), end: new Address(14) })),
+ // ]));
+
+ // commands.push(new LineNumberCommand(new AddressRange({ start: new Address(5), end: new Address(10) })));
+ // commands.push(new PrintCommand());
+ // commands.push(new NoopCommand());
+ // commands.push(new PrintCommand());
+
+ return new Script(commands);
+}
+
+export default {
+ name: 'sed',
+ usage: 'sed [OPTIONS] [SCRIPT] FILE...',
+ description: 'Filter and transform text, line by line.\n\n' +
+ 'Treats the first positional argument as the SCRIPT if no -e options are provided. ' +
+ 'If a FILE is `-`, read standard input.',
+ input: {
+ syncLines: true
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ expression: {
+ description: 'Specify an additional script to execute. May be specified multiple times.',
+ type: 'string',
+ short: 'e',
+ multiple: true,
+ default: [],
+ },
+ quiet: {
+ description: 'Suppress default printing of selected lines.',
+ type: 'boolean',
+ short: 'n',
+ default: false,
+ },
+ }
+ },
+ execute: async ctx => {
+ const { out, err } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+
+ if (positionals.length < 1) {
+ await err.write('sed: No inputs given\n');
+ throw new Exit(1);
+ }
+
+ // "If any -e or -f options are specified, the script of editing commands shall initially be empty. The commands
+ // specified by each -e or -f option shall be added to the script in the order specified. When each addition is
+ // made, if the previous addition (if any) was from a -e option, a shall be inserted before the new
+ // addition. The resulting script shall have the same properties as the script operand, described in the
+ // OPERANDS section."
+ // TODO: -f loads scripts from a file
+ let scriptString = '';
+ if (values.expression.length > 0) {
+ scriptString = values.expression.join('\n');
+ } else {
+ scriptString = positionals.shift();
+ }
+
+ const script = parseScript(scriptString);
+ await out.write(script.dump());
+
+ const context = {
+ out: out,
+ patternSpace: '',
+ holdSpace: '\n',
+ lineNumber: 1,
+ queuedOutput: '',
+ }
+
+ // All remaining positionals are file paths to process.
+ for (const relPath of positionals) {
+ context.lineNumber = 1;
+ for await (const line of fileLines(ctx, relPath)) {
+ context.patternSpace = line.replace(/\n$/, '');
+ const result = await script.runCycle(context);
+ switch (result) {
+ case CycleResult.Quit: {
+ if (!values.quiet) {
+ await out.write(context.patternSpace + '\n');
+ }
+ return;
+ }
+ case CycleResult.QuitSilent: {
+ return;
+ }
+ }
+ if (!values.quiet) {
+ await out.write(context.patternSpace + '\n');
+ }
+ if (context.queuedOutput) {
+ await out.write(context.queuedOutput + '\n');
+ context.queuedOutput = '';
+ }
+ context.lineNumber++;
+ }
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/sleep.js b/packages/phoenix/src/puter-shell/coreutils/sleep.js
new file mode 100644
index 00000000..9023053f
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/sleep.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'sleep',
+ usage: 'sleep TIME',
+ description: 'Pause for at least TIME seconds, where TIME is a positive number.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ if (positionals.length !== 1) {
+ await ctx.externs.err.write('sleep: Exactly one TIME parameter is required');
+ throw new Exit(1);
+ }
+
+ let time = Number.parseFloat(positionals[0]);
+ if (isNaN(time) || time < 0) {
+ await ctx.externs.err.write('sleep: Invalid TIME parameter; must be a positive number');
+ throw new Exit(1);
+ }
+
+ await new Promise(r => setTimeout(r, time * 1000));
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/sort.js b/packages/phoenix/src/puter-shell/coreutils/sort.js
new file mode 100644
index 00000000..3f725a82
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/sort.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { resolveRelativePath } from '../../util/path.js';
+
+export default {
+ name: 'sort',
+ usage: 'sort [FILE...]',
+ description: 'Sort the combined lines from the files provided, and output them.\n\n' +
+ 'If no FILE is specified, or FILE is `-`, read standard input.',
+ input: {
+ syncLines: true
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ 'dictionary-order': {
+ description: 'Only consider alphanumeric characters and whitespace',
+ type: 'boolean',
+ short: 'd'
+ },
+ 'ignore-case': {
+ description: 'Sort case-insensitively',
+ type: 'boolean',
+ short: 'f'
+ },
+ 'ignore-nonprinting': {
+ description: 'Only consider printable characters',
+ type: 'boolean',
+ short: 'i'
+ },
+ output: {
+ description: 'Output to this file, instead of standard output',
+ type: 'string',
+ short: 'o'
+ },
+ unique: {
+ description: 'Remove duplicates of previous lines',
+ type: 'boolean',
+ short: 'u'
+ },
+ reverse: {
+ description: 'Sort in reverse order',
+ type: 'boolean',
+ short: 'r'
+ },
+ }
+ },
+ execute: async ctx => {
+ const { in_, out, err } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ let relPaths = [...positionals];
+ if (relPaths.length === 0) {
+ relPaths.push('-');
+ }
+
+ const lines = [];
+
+ for (const relPath of relPaths) {
+ if (relPath === '-') {
+ lines.push(...await in_.collect());
+ } else {
+ const absPath = resolveRelativePath(ctx.vars, relPath);
+ const fileData = await filesystem.read(absPath);
+ // DRY: Similar logic in wc and tail
+ if (fileData instanceof Blob) {
+ const arrayBuffer = await fileData.arrayBuffer();
+ const fileText = new TextDecoder().decode(arrayBuffer);
+ lines.push(...fileText.split(/\n|\r|\r\n/).map(it => it + '\n'));
+ } else if (typeof fileData === 'string') {
+ lines.push(...fileData.split(/\n|\r|\r\n/).map(it => it + '\n'));
+ } else {
+ // ArrayBuffer or TypedArray
+ const fileText = new TextDecoder().decode(fileData);
+ lines.push(...fileText.split(/\n|\r|\r\n/).map(it => it + '\n'));
+ }
+ }
+ }
+
+ const compareStrings = (a,b) => {
+ let aIndex = 0;
+ let bIndex = 0;
+
+ const skipIgnored = (string, index) => {
+ if (values['dictionary-order'] && values['ignore-nonprinting']) {
+ // Combining --dictionary-order and --ignore-nonprinting is unspecified.
+ // We'll treat that as "must be alphanumeric only".
+ while (index < string.length && ! /[a-zA-Z0-9]/.test(string[index])) {
+ index++;
+ }
+ return index;
+ }
+ if (values['dictionary-order']) {
+ // Only consider whitespace and alphanumeric characters
+ while (index < string.length && ! /[a-zA-Z0-9\s]/.test(string[index])) {
+ index++;
+ }
+ return index;
+ }
+ if (values['ignore-nonprinting']) {
+ // Only consider printing characters
+ // So, ignore anything below an ascii space, inclusive. TODO: detect unicode control characters too?
+ while (index < string.length && string[index] <= ' ') {
+ index++;
+ }
+ return index;
+ }
+
+ return index;
+ };
+
+ aIndex = skipIgnored(a, aIndex);
+ bIndex = skipIgnored(b, bIndex);
+ while (aIndex < a.length && bIndex < b.length) {
+ // POSIX: Sorting should be locale-dependent
+ let comparedCharA = a[aIndex];
+ let comparedCharB = b[bIndex];
+ if (values['ignore-case']) {
+ comparedCharA = comparedCharA.toUpperCase();
+ comparedCharB = comparedCharB.toUpperCase();
+ }
+
+ if (comparedCharA !== comparedCharB) {
+ if (values.reverse) {
+ return comparedCharA < comparedCharB ? 1 : -1;
+ }
+ return comparedCharA < comparedCharB ? -1 : 1;
+ }
+
+ aIndex++;
+ bIndex++;
+ aIndex = skipIgnored(a, aIndex);
+ bIndex = skipIgnored(b, bIndex);
+ }
+
+ // If we got here, we reached the end of one of the strings.
+ // If we reached the end of both, they're equal. Otherwise, return whichever ended.
+ if (aIndex >= a.length) {
+ if (bIndex >= b.length) {
+ return 0;
+ }
+ return -1;
+ }
+ return 1;
+ };
+
+ lines.sort(compareStrings);
+
+ let resultLines = lines;
+ if (values.unique) {
+ resultLines = lines.filter((value, index, array) => {
+ return !index || compareStrings(value, array[index - 1]) !== 0;
+ });
+ }
+
+ if (values.output) {
+ const outputPath = resolveRelativePath(ctx.vars, values.output);
+ await filesystem.write(outputPath, resultLines.join(''));
+ } else {
+ for (const line of resultLines) {
+ await out.write(line);
+ }
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/tail.js b/packages/phoenix/src/puter-shell/coreutils/tail.js
new file mode 100644
index 00000000..f93c5482
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/tail.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+import { fileLines } from '../../util/file.js';
+
+export default {
+ name: 'tail',
+ usage: 'tail [OPTIONS] [FILE]',
+ description: 'Read a file and print the last lines to standard output.\n\n' +
+ 'Defaults to 10 lines unless --lines is given. ' +
+ 'If no FILE is provided, or FILE is `-`, read standard input.',
+ input: {
+ syncLines: true
+ },
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ lines: {
+ description: 'Print the last COUNT lines',
+ type: 'string',
+ short: 'n',
+ valueName: 'COUNT',
+ }
+ }
+ },
+ execute: async ctx => {
+ const { out, err } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+
+ if (positionals.length > 1) {
+ // TODO: Support multiple files (this is an extension to POSIX, but available in the GNU tail)
+ await err.write('tail: Only one FILE parameter is allowed\n');
+ throw new Exit(1);
+ }
+ const relPath = positionals[0] || '-';
+
+ let lineCount = 10;
+
+ if (values.lines) {
+ const parsedLineCount = Number.parseFloat(values.lines);
+ if (isNaN(parsedLineCount) || ! Number.isInteger(parsedLineCount) || parsedLineCount < 1) {
+ await err.write(`tail: Invalid number of lines '${values.lines}'\n`);
+ throw new Exit(1);
+ }
+ lineCount = parsedLineCount;
+ }
+
+ let lines = [];
+ for await (const line of fileLines(ctx, relPath)) {
+ lines.push(line);
+ // We keep lineCount+1 lines, to account for a possible trailing blank line.
+ if (lines.length > lineCount + 1) {
+ lines.shift();
+ }
+ }
+
+ // Ignore trailing blank line
+ if ( lines.length > 0 && lines[lines.length - 1] === '\n') {
+ lines.pop();
+ }
+ // Now we remove the extra line if it's there.
+ if ( lines.length > lineCount ) {
+ lines.shift();
+ }
+
+ for ( const line of lines ) {
+ await out.write(line);
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/test.js b/packages/phoenix/src/puter-shell/coreutils/test.js
new file mode 100644
index 00000000..baf0e19b
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/test.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'test',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { historyManager } = ctx.externs;
+ const { chatHistory } = ctx.plugins;
+
+
+ console.log('test????', chatHistory.get_messages());
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/touch.js b/packages/phoenix/src/puter-shell/coreutils/touch.js
new file mode 100644
index 00000000..8f15f13c
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/touch.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+import { resolveRelativePath } from '../../util/path.js';
+import { ErrorCodes } from '../../platform/PosixError.js';
+
+export default {
+ name: 'touch',
+ usage: 'touch FILE...',
+ description: 'Mark the FILE(s) as accessed and modified at the current time, creating them if they do not exist.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ if ( positionals.length === 0 ) {
+ await ctx.externs.err.write('touch: missing file operand\n');
+ throw new Exit(1);
+ }
+
+ for ( let i=0 ; i < positionals.length ; i++ ) {
+ const path = resolveRelativePath(ctx.vars, positionals[i]);
+
+ let stat = null;
+ try {
+ stat = await filesystem.stat(path);
+ } catch (e) {
+ if (e.posixCode !== ErrorCodes.ENOENT) {
+ await ctx.externs.err.write(`touch: ${e.message}\n`);
+ throw new Exit(1);
+ }
+ }
+
+ if ( stat ) continue;
+
+ await filesystem.write(path, '');
+ }
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/true.js b/packages/phoenix/src/puter-shell/coreutils/true.js
new file mode 100644
index 00000000..fa2ddce9
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/true.js
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'true',
+ usage: 'true',
+ description: 'Do nothing, and return a success code.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true
+ },
+ execute: async ctx => {
+ return;
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/txt2img.js b/packages/phoenix/src/puter-shell/coreutils/txt2img.js
new file mode 100644
index 00000000..bc432d24
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/txt2img.js
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'txt2img',
+ usage: 'txt2img PROMPT',
+ description: 'Send PROMPT to an image-drawing AI, and print the result to standard output.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+ const [ prompt ] = positionals;
+
+ if ( ! prompt ) {
+ await ctx.externs.err.write('txt2img: missing prompt\n');
+ throw new Exit(1);
+ }
+ if ( positionals.length > 1 ) {
+ await ctx.externs.err.write('txt2img: prompt must be wrapped in quotes\n');
+ throw new Exit(1);
+ }
+
+ const { drivers } = ctx.platform;
+
+ let a_interface, a_method, a_args;
+
+ a_interface = 'puter-image-generation';
+ a_method = 'generate';
+ a_args = { prompt };
+
+ const result = await drivers.call({
+ interface: a_interface,
+ method: a_method,
+ args: a_args,
+ });
+
+ await ctx.externs.out.write(result);
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/usages.js b/packages/phoenix/src/puter-shell/coreutils/usages.js
new file mode 100644
index 00000000..a04c39e9
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/usages.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export default {
+ name: 'usages',
+ usage: 'usages',
+ description: 'Print usage statistics, formatted as JSON.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ },
+ execute: async ctx => {
+ const { positionals } = ctx.locals;
+
+ const { drivers } = ctx.platform;
+
+ const result = await drivers.usage();
+
+ await ctx.externs.out.write(JSON.stringify(result, undefined, 2));
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/coreutils/wc.js b/packages/phoenix/src/puter-shell/coreutils/wc.js
new file mode 100644
index 00000000..1924a4d3
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/wc.js
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { resolveRelativePath } from '../../util/path.js';
+import { fileLines } from '../../util/file.js';
+
+const TAB_SIZE = 8;
+
+export default {
+ name: 'wc',
+ usage: 'wc [OPTIONS] [FILE...]',
+ description: 'Count newlines, words, and bytes in each specified FILE, and print them in a table.\n\n' +
+ 'If no FILE is specified, or FILE is `-`, read standard input. ' +
+ 'If more than one FILE is specified, also print a line for the totals.\n\n' +
+ 'The outputs are always printed in the order: newlines, words, characters, bytes, maximum line length, followed by the file name. ' +
+ 'If no options are given to output specific counts, the default is `-lwc`.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ bytes: {
+ description: 'Output the number of bytes in each file',
+ type: 'boolean',
+ short: 'c'
+ },
+ chars: {
+ description: 'Output the number of characters in each file',
+ type: 'boolean',
+ short: 'm'
+ },
+ lines: {
+ description: 'Output the number of newlines in each file',
+ type: 'boolean',
+ short: 'l'
+ },
+ 'max-line-length': {
+ description: 'Output the maximum line length in each file. Tabs are expanded to the nearest multiple of 8',
+ type: 'boolean',
+ short: 'L'
+ },
+ words: {
+ description: 'Output the number of words in each file. A word is a sequence of non-whitespace characters',
+ type: 'boolean',
+ short: 'w'
+ },
+ }
+ },
+ execute: async ctx => {
+ const { positionals, values } = ctx.locals;
+ const { filesystem } = ctx.platform;
+
+ const paths = [...positionals];
+ // "If no input file operands are specified, no name shall be written and no characters preceding the
+ // pathname shall be written."
+ // For convenience, we add '-' to paths, but make a note not to output the filename.
+ let emptyStdinPath = false;
+ if (paths.length < 1) {
+ emptyStdinPath = true;
+ paths.push('-');
+ }
+
+ let { bytes: printBytes, chars: printChars, lines: printNewlines, 'max-line-length': printMaxLineLengths, words: printWords } = values;
+ const anyOutputOptionsSpecified = printBytes || printChars || printNewlines || printMaxLineLengths || printWords;
+ if (!anyOutputOptionsSpecified) {
+ printBytes = true;
+ printNewlines = true;
+ printWords = true;
+ }
+
+ let perFile = [];
+ let newlinesWidth = 1;
+ let wordsWidth = 1;
+ let charsWidth = 1;
+ let bytesWidth = 1;
+ let maxLineLengthWidth = 1;
+
+ for (const relPath of paths) {
+ let counts = {
+ filename: relPath,
+ newlines: 0,
+ words: 0,
+ chars: 0,
+ bytes: 0,
+ maxLineLength: 0,
+ };
+
+ let inWord = false;
+ let currentLineLength = 0;
+
+ for await (const line of fileLines(ctx, relPath)) {
+ counts.chars += line.length;
+ if (printBytes) {
+ const byteInput = new TextEncoder().encode(line);
+ counts.bytes += byteInput.length;
+ }
+
+ for (const char of line) {
+ // "The wc utility shall consider a word to be a non-zero-length string of characters delimited by white space."
+ if (/\s/.test(char)) {
+ if (char === '\r' || char === '\n') {
+ counts.newlines++;
+ counts.maxLineLength = Math.max(counts.maxLineLength, currentLineLength);
+ currentLineLength = 0;
+ } else if (char === '\t') {
+ currentLineLength = (Math.floor(currentLineLength / TAB_SIZE) + 1) * TAB_SIZE;
+ } else {
+ currentLineLength++;
+ }
+ inWord = false;
+ continue;
+ }
+ currentLineLength++;
+ if (!inWord) {
+ counts.words++;
+ inWord = true;
+ }
+ }
+ }
+
+ counts.maxLineLength = Math.max(counts.maxLineLength, currentLineLength);
+
+ newlinesWidth = Math.max(newlinesWidth, counts.newlines.toString().length);
+ wordsWidth = Math.max(wordsWidth, counts.words.toString().length);
+ charsWidth = Math.max(charsWidth, counts.chars.toString().length);
+ bytesWidth = Math.max(bytesWidth, counts.bytes.toString().length);
+ maxLineLengthWidth = Math.max(maxLineLengthWidth, counts.maxLineLength.toString().length);
+ perFile.push(counts);
+ }
+
+ let printCounts = async (count) => {
+ let output = '';
+ const append = (string) => {
+ if (output.length !== 0) output += ' ';
+ output += string;
+ };
+
+ if (printNewlines) append(count.newlines.toString().padStart(newlinesWidth, ' '));
+ if (printWords) append(count.words.toString().padStart(wordsWidth, ' '));
+ if (printChars) append(count.chars.toString().padStart(charsWidth, ' '));
+ if (printBytes) append(count.bytes.toString().padStart(bytesWidth, ' '));
+ if (printMaxLineLengths) append(count.maxLineLength.toString().padStart(maxLineLengthWidth, ' '));
+ // The only time emptyStdinPath is true, is if we had no file paths given as arguments. That means only one
+ // input (stdin), so this won't be called to print a "totals" line.
+ if (!emptyStdinPath) append(count.filename);
+ output += '\n';
+ await ctx.externs.out.write(output);
+ }
+
+ let totalCounts = {
+ filename: 'total', // POSIX: This is locale-dependent
+ newlines: 0,
+ words: 0,
+ chars: 0,
+ bytes: 0,
+ maxLineLength: 0,
+ };
+ for (const count of perFile) {
+ totalCounts.newlines += count.newlines;
+ totalCounts.words += count.words;
+ totalCounts.chars += count.chars;
+ totalCounts.bytes += count.bytes;
+ totalCounts.maxLineLength = Math.max(totalCounts.maxLineLength, count.maxLineLength);
+ await printCounts(count);
+ }
+ if (perFile.length > 1) {
+ await printCounts(totalCounts);
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/coreutils/which.js b/packages/phoenix/src/puter-shell/coreutils/which.js
new file mode 100644
index 00000000..6871acdc
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/coreutils/which.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Exit } from './coreutil_lib/exit.js';
+
+export default {
+ name: 'which',
+ usage: 'which COMMAND...',
+ description: 'Look up each COMMAND, and return the path name of its executable.\n\n' +
+ 'Returns 1 if any COMMAND is not found, otherwise returns 0.',
+ args: {
+ $: 'simple-parser',
+ allowPositionals: true,
+ options: {
+ 'all': {
+ description: 'Return all matching path names of each COMMAND, not just the first',
+ type: 'boolean',
+ short: 'a',
+ },
+ },
+ },
+ execute: async ctx => {
+ const { out, err, commandProvider } = ctx.externs;
+ const { positionals, values } = ctx.locals;
+
+ let anyCommandsNotFound = false;
+
+ const printPath = async ( commandName, command ) => {
+ if (command.path) {
+ await out.write(`${command.path}\n`);
+ } else {
+ await out.write(`${commandName}: shell built-in command\n`);
+ }
+ };
+
+ for ( const commandName of positionals ) {
+ const result = values.all
+ ? await commandProvider.lookupAll(commandName, { ctx })
+ : await commandProvider.lookup(commandName, { ctx });
+
+ if ( ! result ) {
+ anyCommandsNotFound = true;
+ await err.write(`${commandName} not found\n`);
+ continue;
+ }
+
+ if ( values.all ) {
+ for ( const command of result ) {
+ await printPath(commandName, command);
+ }
+ } else {
+ await printPath(commandName, result);
+ }
+ }
+
+ if ( anyCommandsNotFound ) {
+ throw new Exit(1);
+ }
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/main.js b/packages/phoenix/src/puter-shell/main.js
new file mode 100644
index 00000000..f0e85f6d
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/main.js
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import builtins from './coreutils/__exports__.js';
+import ReadlineLib from "../ansi-shell/readline/readline.js";
+
+// TODO: auto-gen argument parser registry from files
+import SimpleArgParser from "../ansi-shell/arg-parsers/simple-parser.js";
+import ErrorsDecorator from "../ansi-shell/decorators/errors.js";
+import { ANSIShell } from "../ansi-shell/ANSIShell.js";
+import { Context } from "contextlink";
+import { SHELL_VERSIONS } from "../meta/versions.js";
+import { PuterShellParser } from "../ansi-shell/parsing/PuterShellParser.js";
+import { BuiltinCommandProvider } from "./providers/BuiltinCommandProvider.js";
+import { CreateChatHistoryPlugin } from './plugins/ChatHistoryPlugin.js';
+import { Pipe } from '../ansi-shell/pipeline/Pipe.js';
+import { Coupler } from '../ansi-shell/pipeline/Coupler.js';
+import { BetterReader } from 'dev-pty';
+import { MultiWriter } from '../ansi-shell/ioutil/MultiWriter.js';
+import { CompositeCommandProvider } from './providers/CompositeCommandProvider.js';
+import { ScriptCommandProvider } from './providers/ScriptCommandProvider.js';
+import { PuterAppCommandProvider } from './providers/PuterAppCommandProvider.js';
+
+const argparser_registry = {
+ [SimpleArgParser.name]: SimpleArgParser
+};
+
+const decorator_registry = {
+ [ErrorsDecorator.name]: ErrorsDecorator
+};
+
+const GH_LINK = {
+ 'terminal': 'https://github.com/HeyPuter/terminal',
+ 'phoenix': 'https://github.com/HeyPuter/phoenix',
+};
+
+export const launchPuterShell = async (ctx) => {
+ const config = ctx.config;
+ const ptt = ctx.ptt;
+ const puterShell = ctx.puterShell;
+
+ // Need to replace `in` with something we can write to
+ const real_pipe = new Pipe();
+ const echo_pipe = new Pipe();
+ const out_writer = new MultiWriter({
+ delegates: [
+ echo_pipe.in,
+ real_pipe.in,
+ ]
+ })
+ new Coupler(ptt.in, out_writer);
+ const echo = new Coupler(echo_pipe.out, ptt.out);
+ const stdin = new BetterReader({ delegate: real_pipe.out });
+ echo.off();
+
+ const readline = ReadlineLib.create({
+ in: stdin,
+ out: ptt.out
+ });
+
+ const sdkv2 = globalThis.puter;
+ if ( ctx.platform.name !== 'node' ) {
+ await sdkv2.setAuthToken(config['puter.auth.token']);
+ await sdkv2.setAPIOrigin(config['puter.api_origin']);
+ }
+
+ // PathCommandProvider is only compatible with node.js for now
+ // HACK: The import path is split to fool rollup into not including it.
+ const { PathCommandProvider } = (ctx.platform.name === 'node')
+ ? await import('./providers/' + 'PathCommandProvider.js')
+ : { PathCommandProvider: null };
+
+ const commandProvider = new CompositeCommandProvider([
+ new BuiltinCommandProvider(),
+ // PathCommandProvider is only compatible with node.js for now
+ ...(ctx.platform.name === 'node' ? [new PathCommandProvider()] : []),
+ // PuterAppCommandProvider is only usable on Puter
+ ...(ctx.platform.name === 'puter' ? [new PuterAppCommandProvider()] : []),
+ new ScriptCommandProvider(),
+ ]);
+
+ ctx = ctx.sub({
+ externs: new Context({
+ config, puterShell,
+ readline: readline.readline.bind(readline),
+ in: stdin,
+ out: ptt.out,
+ echo,
+ parser: new PuterShellParser(),
+ commandProvider,
+ sdkv2,
+ historyManager: readline.history,
+ }),
+ registries: new Context({
+ argparsers: argparser_registry,
+ decorators: decorator_registry,
+ // While we use the BuiltinCommandProvider to provide the
+ // functionality of command lookup, we still need a registry
+ // of builtins to support the `help` command.
+ builtins,
+ }),
+ plugins: new Context(),
+ locals: new Context(),
+ });
+
+ {
+ const name = "chatHistory";
+ const p = CreateChatHistoryPlugin(ctx);
+ ctx.plugins[name] = new Context(p.expose);
+ p.init();
+ }
+
+ const ansiShell = new ANSIShell(ctx);
+
+ // TODO: move ioctl to PTY
+ ptt.on('ioctl.set', evt => {
+ ansiShell.dispatchEvent(new CustomEvent('signal.window-resize', {
+ detail: {
+ ...evt.windowSize
+ }
+ }));
+ });
+
+ const fire = (text) => {
+ // Define fire-like colors (ANSI 256-color codes)
+ const fireColors = [202, 208, 166];
+
+ // Split the text into an array of characters
+ const chars = text.split('');
+
+ // Apply a fire-like color to each character
+ const fireText = chars.map(char => {
+ // Select a random fire color for each character
+ const colorCode = fireColors[Math.floor(Math.random() * fireColors.length)];
+ // Return the character wrapped in the ANSI escape code for the selected color
+ return `\x1b[38;5;${colorCode}m${char}\x1b[0m`;
+ }).join('');
+
+ return fireText;
+ }
+
+ const blue = (text) => {
+ return `\x1b[38:5:27;1m${text}\x1b[0m`;
+ }
+
+ const mklink = (url, text) => {
+ return `\x1b]8;;${url}\x07${text || url}\x1b]8;;\x07`
+ };
+
+ ctx.externs.out.write(
+ `${fire('Phoenix Shell')} [v${SHELL_VERSIONS[0].v}]\n` +
+ `⛷ try typing \x1B[34;1mhelp\x1B[0m or ` +
+ `\x1B[34;1mchangelog\x1B[0m to get started.\n` +
+ '\n' +
+ `${
+ mklink(GH_LINK['phoenix'], fire('This shell'))
+ } and ${
+ mklink(GH_LINK['terminal'], blue('Puter\'s Terminal Emulator'))
+ } are free software:\n` +
+ // `- ${fire('phoenix')}: ` + mklink(GH_LINK['phoenix'], fire(GH_LINK['phoenix'])) + '\n' +
+ // `- ${blue('terminal')}: ` + mklink(GH_LINK['terminal'], blue(GH_LINK['terminal'])) + '\n' +
+ `- ${'phoenix'}: ` + mklink(GH_LINK['phoenix']) + '\n' +
+ `- ${'terminal'}: ` + mklink(GH_LINK['terminal']) + '\n' +
+ // `🔗 ${mklink('https://puter.com', 'puter.com')} ` +
+ ''
+ // `🔗 ${mklink('https://puter.com', 'puter.com')} ` +
+ );
+
+ if ( ! config.hasOwnProperty('puter.auth.token') ) {
+ ctx.externs.out.write('\n');
+ ctx.externs.out.write(
+ `\x1B[33;1m⚠\x1B[0m` +
+ `\x1B[31;1m` +
+ ' You are not running this terminal or shell within puter.com\n' +
+ `\x1B[0m` +
+ 'Use of the shell outside of puter.com is still experimental.\n' +
+ 'You must enter the command \x1B[34;1m`login`\x1B[0m to access most functionality.\n' +
+ ''
+ );
+ }
+
+ ctx.externs.out.write('\n');
+
+ for ( ;; ) {
+ await ansiShell.doPromptIteration();
+ }
+};
diff --git a/packages/phoenix/src/puter-shell/plugins/ChatHistoryPlugin.js b/packages/phoenix/src/puter-shell/plugins/ChatHistoryPlugin.js
new file mode 100644
index 00000000..8d9df329
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/plugins/ChatHistoryPlugin.js
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const CreateChatHistoryPlugin = ctx => {
+ const messages = [
+ {
+ role: 'system',
+ content:
+ 'You are running inside the Puter terminal via the `ai` command. Refer to yourself as Puter Terminal AI.',
+ },
+ {
+ role: 'system',
+ content:
+ // note: this really doesn't work at all; GPT is effectively incapable of following this instruction.
+ 'You can provide commands to the user by prefixing a line in your response with %%%. The user will then be able to run the command by accepting confirmation.',
+ },
+ {
+ role: 'system',
+ content:
+ 'If the user asks you about commands they have run, read them from system messages; if you don\'t see any, just let them know.',
+ },
+ {
+ role: 'system',
+ content:
+ 'If the user asks what commands are available, tell them you don\'t yet have the ability to list commands but the `help` command is available for this purpose.'
+ },
+ {
+ role: 'system',
+ content:
+ [
+ 'FAQ, in case the user asks (rephrase these answers in character as Puter Terminal AI):',
+ 'Q: What is the command language?',
+ 'A: A subset of the POSIX Command Language, commonly known as the shell language.',
+ 'Q: Is this POSIX compliant?',
+ 'A: Our goal is to eventually be POSIX compliant, but support for most syntax is currently incomplete.',
+ 'Q: Is this a real shell?',
+ 'A: Yes, this is a real shell. You can interact with Puter\'s filesystem and drivers.',
+ 'Q: What is Puter?',
+ 'A: Puter is an operating system on the cloud, accessible from your browser. It is designed to be a platform for running applications and services with tools and interfaces you\'re already familiar with.',
+ 'Q: Is Puter a real operating system?',
+ 'A: Puter has a filesystem, manages cloud resources, and provides online services we call "drivers". It is the higher-level equivalent of a traditional operating system.',
+ ].join(' ')
+ },
+ ];
+ return {
+ expose: {
+ add_message (a_message) {
+ messages.push(a_message);
+ },
+ get_messages () {
+ return [...messages];
+ },
+ },
+ init () {
+ const history = ctx.externs.historyManager;
+ history.on('add', (input) => {
+ // To the best of our ability, we want to ignore invocations
+ // of the "ai" command itself. This won't always work because
+ // the history manager can't resolve command substitutions.
+ if ( input.startsWith('ai ') ) return;
+
+ messages.push({
+ role: 'system',
+ content:
+ `The user entered a command in the terminal: ` +
+ input
+ });
+ });
+ }
+ };
+};
diff --git a/packages/phoenix/src/puter-shell/providers/BuiltinCommandProvider.js b/packages/phoenix/src/puter-shell/providers/BuiltinCommandProvider.js
new file mode 100644
index 00000000..efd86382
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/providers/BuiltinCommandProvider.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import builtins from '../coreutils/__exports__.js';
+
+export class BuiltinCommandProvider {
+ async lookup (id) {
+ return builtins[id];
+ }
+
+ // Only a single builtin can match a given name
+ async lookupAll (...a) {
+ const result = await this.lookup(...a);
+ if ( result ) {
+ return [ result ];
+ }
+ return undefined;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/providers/CompositeCommandProvider.js b/packages/phoenix/src/puter-shell/providers/CompositeCommandProvider.js
new file mode 100644
index 00000000..58cf5f27
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/providers/CompositeCommandProvider.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class CompositeCommandProvider {
+ constructor (providers) {
+ this.providers = providers;
+ }
+
+ async lookup (...a) {
+ for (const provider of this.providers) {
+ const command = await provider.lookup(...a);
+ if (command) {
+ return command;
+ }
+ }
+ }
+
+ async lookupAll (...a) {
+ const results = [];
+ for (const provider of this.providers) {
+ const commands = await provider.lookupAll(...a);
+ if ( commands ) {
+ results.push(...commands);
+ }
+ }
+
+ if ( results.length === 0 ) return undefined;
+ return results;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/providers/PathCommandProvider.js b/packages/phoenix/src/puter-shell/providers/PathCommandProvider.js
new file mode 100644
index 00000000..bf84a052
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/providers/PathCommandProvider.js
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import path_ from "path-browserify";
+import child_process from "node:child_process";
+import stream from "node:stream";
+import { signals } from '../../ansi-shell/signals.js';
+import { Exit } from '../coreutils/coreutil_lib/exit.js';
+import pty from 'node-pty';
+
+function spawn_process(ctx, executablePath) {
+ console.log(`Spawning ${executablePath} as a child process`);
+ const child = child_process.spawn(executablePath, ctx.locals.args, {
+ stdio: ['pipe', 'pipe', 'pipe'],
+ cwd: ctx.vars.pwd,
+ });
+
+ const in_ = new stream.PassThrough();
+ const out = new stream.PassThrough();
+ const err = new stream.PassThrough();
+
+ in_.on('data', (chunk) => {
+ child.stdin.write(chunk);
+ });
+ out.on('data', (chunk) => {
+ ctx.externs.out.write(chunk);
+ });
+ err.on('data', (chunk) => {
+ ctx.externs.err.write(chunk);
+ });
+
+ const fn_err = label => err => {
+ console.log(`ERR(${label})`, err);
+ };
+ in_.on('error', fn_err('in_'));
+ out.on('error', fn_err('out'));
+ err.on('error', fn_err('err'));
+ child.stdin.on('error', fn_err('stdin'));
+ child.stdout.on('error', fn_err('stdout'));
+ child.stderr.on('error', fn_err('stderr'));
+
+ child.stdout.pipe(out);
+ child.stderr.pipe(err);
+
+ child.on('error', (err) => {
+ console.error(`Error running path executable '${executablePath}':`, err);
+ });
+
+ const sigint_promise = new Promise((resolve, reject) => {
+ ctx.externs.sig.on((signal) => {
+ if ( signal === signals.SIGINT ) {
+ reject(new Exit(130));
+ }
+ });
+ });
+
+ const exit_promise = new Promise((resolve, reject) => {
+ child.on('exit', (code) => {
+ ctx.externs.out.write(`Exited with code ${code}\n`);
+ if (code === 0) {
+ resolve({ done: true });
+ } else {
+ reject(new Exit(code));
+ }
+ });
+ });
+
+ // Repeatedly copy data from stdin to the child, while it's running.
+ let data, done;
+ const next_data = async () => {
+ // FIXME: This waits for one more read() after we finish.
+ ({ value: data, done } = await Promise.race([
+ exit_promise, sigint_promise, ctx.externs.in_.read(),
+ ]));
+ if ( data ) {
+ in_.write(data);
+ if ( ! done ) setTimeout(next_data, 0);
+ }
+ }
+ setTimeout(next_data, 0);
+
+ return Promise.race([ exit_promise, sigint_promise ]);
+}
+
+function spawn_pty(ctx, executablePath) {
+ console.log(`Spawning ${executablePath} as a pty`);
+ const child = pty.spawn(executablePath, ctx.locals.args, {
+ name: 'xterm-color',
+ rows: ctx.env.ROWS,
+ cols: ctx.env.COLS,
+ cwd: ctx.vars.pwd,
+ env: ctx.env
+ });
+ child.onData(chunk => {
+ ctx.externs.out.write(chunk);
+ });
+
+ const sigint_promise = new Promise((resolve, reject) => {
+ ctx.externs.sig.on((signal) => {
+ if ( signal === signals.SIGINT ) {
+ child.kill('SIGINT'); // FIXME: Docs say this will throw when used on Windows
+ reject(new Exit(130));
+ }
+ });
+ });
+
+ const exit_promise = new Promise((resolve, reject) => {
+ child.onExit(({code, signal}) => {
+ ctx.externs.out.write(`Exited with code ${code || 0} and signal ${signal || 0}\n`);
+ if ( signal ) {
+ reject(new Exit(1));
+ } else if ( code ) {
+ reject(new Exit(code));
+ } else {
+ resolve({ done: true });
+ }
+ });
+ });
+
+ // Repeatedly copy data from stdin to the child, while it's running.
+ let data, done;
+ const next_data = async () => {
+ // FIXME: This waits for one more read() after we finish.
+ ({ value: data, done } = await Promise.race([
+ exit_promise, sigint_promise, ctx.externs.in_.read(),
+ ]));
+ if ( data ) {
+ child.write(data);
+ if ( ! done ) setTimeout(next_data, 0);
+ }
+ }
+ setTimeout(next_data, 0);
+
+ return Promise.race([ exit_promise, sigint_promise ]);
+}
+
+function makeCommand(id, executablePath) {
+ return {
+ name: id,
+ path: executablePath,
+ async execute(ctx) {
+ // TODO: spawn_pty() does a lot of things better than spawn_process(), but can't handle output redirection.
+ // At some point, we'll need to implement more ioctls within spawn_process() and then remove spawn_pty(),
+ // but for now, the best experience is to use spawn_pty() unless we need the redirection.
+ if (ctx.locals.outputIsRedirected) {
+ return spawn_process(ctx, executablePath);
+ }
+ return spawn_pty(ctx, executablePath);
+ }
+ };
+}
+
+async function findCommandsInPath(id, ctx, firstOnly) {
+ const PATH = ctx.env['PATH'];
+ if (!PATH)
+ return;
+ const pathDirectories = PATH.split(':');
+
+ const results = [];
+
+ for (const dir of pathDirectories) {
+ const executablePath = path_.resolve(dir, id);
+ let stat;
+ try {
+ stat = await ctx.platform.filesystem.stat(executablePath);
+ } catch (e) {
+ // Stat failed -> file does not exist
+ continue;
+ }
+ // TODO: Detect if the file is executable, and ignore it if not.
+ const command = makeCommand(id, executablePath);
+
+ if ( firstOnly ) return command;
+ results.push(command);
+ }
+
+ return results.length > 0 ? results : undefined;
+}
+
+export class PathCommandProvider {
+ async lookup (id, { ctx }) {
+ return findCommandsInPath(id, ctx, true);
+ }
+
+ async lookupAll(id, { ctx }) {
+ return findCommandsInPath(id, ctx, false);
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js
new file mode 100644
index 00000000..44bf6cbb
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+const BUILT_IN_APPS = [
+ 'explorer',
+];
+
+export class PuterAppCommandProvider {
+
+ async lookup (id) {
+ // Built-in apps will not be returned by the fetch query below, so we handle them separately.
+ if (BUILT_IN_APPS.includes(id)) {
+ return {
+ name: id,
+ path: 'Built-in Puter app',
+ // TODO: Parameters and options?
+ async execute(ctx) {
+ const args = {}; // TODO: Passed-in parameters and options would go here
+ // NOTE: No await here, because launchApp() currently only resolves for Puter SDK apps.
+ puter.ui.launchApp(id, args);
+ }
+ };
+ }
+
+ const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
+ "headers": {
+ "Content-Type": "application/json",
+ "Authorization": `Bearer ${puter.authToken}`,
+ },
+ "body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }),
+ "method": "POST",
+ });
+
+ const { success, result } = await request.json();
+
+ if (!success) return;
+
+ const { name, index_url } = result;
+ return {
+ name,
+ path: index_url,
+ // TODO: Parameters and options?
+ async execute(ctx) {
+ const args = {}; // TODO: Passed-in parameters and options would go here
+ // NOTE: No await here, yet, because launchApp() currently only resolves for Puter SDK apps.
+ puter.ui.launchApp(name, args);
+ }
+ };
+ }
+
+ // Only a single Puter app can match a given name
+ async lookupAll (...a) {
+ const result = await this.lookup(...a);
+ if ( result ) {
+ return [ result ];
+ }
+ return undefined;
+ }
+}
diff --git a/packages/phoenix/src/puter-shell/providers/ScriptCommandProvider.js b/packages/phoenix/src/puter-shell/providers/ScriptCommandProvider.js
new file mode 100644
index 00000000..66be2041
--- /dev/null
+++ b/packages/phoenix/src/puter-shell/providers/ScriptCommandProvider.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import path_ from "path-browserify";
+import { Pipeline } from "../../ansi-shell/pipeline/Pipeline.js";
+import { resolveRelativePath } from '../../util/path.js';
+
+export class ScriptCommandProvider {
+ async lookup (id, { ctx }) {
+ const { filesystem } = ctx.platform;
+
+ const is_path = id.match(/^[.\/]/);
+ if ( ! is_path ) return undefined;
+
+ const absPath = resolveRelativePath(ctx.vars, id);
+ try {
+ await filesystem.stat(absPath);
+ // TODO: More rigorous check that it's an executable text file
+ } catch (e) {
+ return undefined;
+ }
+
+ return {
+ path: id,
+ async execute (ctx) {
+ const script_blob = await filesystem.read(absPath);
+ const script_text = await script_blob.text();
+
+ console.log('result though?', script_text);
+
+ // note: it's still called `parseLineForProcessing` but
+ // it has since been extended to parse the entire file
+ const ast = ctx.externs.parser.parseScript(script_text);
+ const statements = ast[0].statements;
+
+ for (const stmt of statements) {
+ const pipeline = await Pipeline.createFromAST(ctx, stmt);
+ await pipeline.execute(ctx);
+ }
+ }
+ };
+ }
+
+ // Only a single script can match a given path
+ async lookupAll (...a) {
+ const result = await this.lookup(...a);
+ if ( result ) {
+ return [ result ];
+ }
+ return undefined;
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/util/bytes.js b/packages/phoenix/src/util/bytes.js
new file mode 100644
index 00000000..cee745c9
--- /dev/null
+++ b/packages/phoenix/src/util/bytes.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class Uint8List {
+ constructor (initialSize) {
+ initialSize = initialSize || 2;
+
+ this.array = new Uint8Array(initialSize);
+ this.size = 0;
+ }
+
+ get capacity () {
+ return this.array.length;
+ }
+
+ append (chunk) {
+ if ( typeof chunk === 'number' ) {
+ chunk = new Uint8Array([chunk]);
+ }
+
+ const sizeNeeded = this.size + chunk.length;
+ let newCapacity = this.capacity;
+ while ( sizeNeeded > newCapacity ) {
+ newCapacity *= 2;
+ }
+
+ if ( newCapacity !== this.capacity ) {
+ const newArray = new Uint8Array(newCapacity);
+ newArray.set(this.array, 0);
+ this.array = newArray;
+ }
+
+ this.array.set(chunk, this.size);
+ this.size += chunk.length;
+ }
+
+ toArray () {
+ return this.array.subarray(0, this.size);
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/util/file.js b/packages/phoenix/src/util/file.js
new file mode 100644
index 00000000..a29045f2
--- /dev/null
+++ b/packages/phoenix/src/util/file.js
@@ -0,0 +1,28 @@
+import { resolveRelativePath } from './path.js';
+
+// Iterate the given file, one line at a time.
+// TODO: Make this read one line at a time, instead of all at once.
+export async function* fileLines(ctx, relPath, options = { dashIsStdin: true }) {
+ let lines = [];
+ if (options.dashIsStdin && relPath === '-') {
+ lines = await ctx.externs.in_.collect();
+ } else {
+ const absPath = resolveRelativePath(ctx.vars, relPath);
+ const fileData = await ctx.platform.filesystem.read(absPath);
+ if (fileData instanceof Blob) {
+ const arrayBuffer = await fileData.arrayBuffer();
+ const fileText = new TextDecoder().decode(arrayBuffer);
+ lines = fileText.split(/\n|\r|\r\n/).map(it => it + '\n');
+ } else if (typeof fileData === 'string') {
+ lines = fileData.split(/\n|\r|\r\n/).map(it => it + '\n');
+ } else {
+ // ArrayBuffer or TypedArray
+ const fileText = new TextDecoder().decode(fileData);
+ lines = fileText.split(/\n|\r|\r\n/).map(it => it + '\n');
+ }
+ }
+
+ for (const line of lines) {
+ yield line;
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/util/lang.js b/packages/phoenix/src/util/lang.js
new file mode 100644
index 00000000..dea8aa06
--- /dev/null
+++ b/packages/phoenix/src/util/lang.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const disallowAccessToUndefined = (obj) => {
+ return new Proxy(obj, {
+ get (target, prop, receiver) {
+ if ( ! target.hasOwnProperty(prop) ) {
+ throw new Error(
+ `disallowed access to undefined property` +
+ `: ${JSON.stringify(prop)}.`
+ );
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ })
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/util/log.js b/packages/phoenix/src/util/log.js
new file mode 100644
index 00000000..f68f772a
--- /dev/null
+++ b/packages/phoenix/src/util/log.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export class Log {
+ static log (...items) {
+ items = items.map(this.toString);
+ console.log(...items);
+ }
+
+ static toString (item) {
+ if ( item instanceof Uint8Array ) {
+ return [...item]
+ .map(x => x.toString(16).padStart(2, '0'))
+ .join(' ');
+ }
+
+ return item;
+ }
+}
diff --git a/packages/phoenix/src/util/path.js b/packages/phoenix/src/util/path.js
new file mode 100644
index 00000000..bd418be0
--- /dev/null
+++ b/packages/phoenix/src/util/path.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import path_ from "path-browserify";
+
+export const resolveRelativePath = (vars, relativePath) => {
+ if (!relativePath) {
+ // If relativePath is undefined, return home directory
+ return vars.home;
+ }
+ if ( relativePath.startsWith('/') ) {
+ return relativePath;
+ }
+ if ( relativePath.startsWith('~') ) {
+ return path_.resolve(vars.home, '.' + relativePath.slice(1));
+ }
+ return path_.resolve(vars.pwd, relativePath);
+};
diff --git a/packages/phoenix/src/util/singleton.js b/packages/phoenix/src/util/singleton.js
new file mode 100644
index 00000000..4bcbf85d
--- /dev/null
+++ b/packages/phoenix/src/util/singleton.js
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export const EMPTY = Object.freeze({});
diff --git a/packages/phoenix/src/util/statemachine.js b/packages/phoenix/src/util/statemachine.js
new file mode 100644
index 00000000..ca4a77ee
--- /dev/null
+++ b/packages/phoenix/src/util/statemachine.js
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { disallowAccessToUndefined } from "./lang.js";
+import { Context } from "contextlink";
+
+export class StatefulProcessor {
+ constructor (params) {
+ for ( const k in params ) this[k] = params[k];
+
+ let lastState = null;
+ }
+ async run (imports) {
+ this.state = 'start';
+ imports = imports ?? {};
+ const externals = {};
+ for ( const k in this.externals ) {
+ if ( this.externals[k].required && ! imports[k] ) {
+ throw new Error(`missing required external: ${k}`);
+ }
+ if ( ! imports[k] ) continue;
+ externals[k] = imports[k];
+ }
+
+ const ctx = new Context({
+ consts: disallowAccessToUndefined(this.constants),
+ externs: externals,
+ vars: this.createVariables_(),
+ setState: this.setState_.bind(this)
+ });
+
+ for ( ;; ) {
+ if ( this.state === 'end' ) break;
+
+ await this.iter_(ctx);
+ }
+
+ return ctx.vars;
+ }
+ setState_ (newState) {
+ this.state = newState;
+ }
+ async iter_ (runContext) {
+ const ctx = runContext.sub({
+ locals: {}
+ });
+
+ ctx.trigger = name => {
+ return this.actions[name](ctx);
+ }
+ if ( this.state !== this.lastState ) {
+ this.lastState = this.state;
+ if ( this.transitions.hasOwnProperty(this.state) ) {
+ for ( const handler of this.transitions[this.state] ) {
+ await handler(ctx);
+ }
+ }
+ }
+
+ for ( const beforeAll of this.beforeAlls ) {
+ await beforeAll.handler(ctx);
+ }
+
+ await this.states[this.state](ctx);
+ }
+ createVariables_ () {
+ const o = {};
+ for ( const k in this.variables ) {
+ if ( this.variables[k].getDefaultValue ) {
+ o[k] = this.variables[k].getDefaultValue();
+ }
+ }
+ return o;
+ }
+}
+
+export class StatefulProcessorBuilder {
+ static COMMON_1 = [
+ 'variable', 'external', 'state', 'action'
+ ]
+
+ constructor () {
+ this.constants = {};
+ this.beforeAlls = [];
+ this.transitions = {};
+
+ for ( const facet of this.constructor.COMMON_1 ) {
+ this[facet + 's'] = {};
+ this[facet] = function (name, value) {
+ this[facet + 's'][name] = value;
+ return this;
+ }
+ }
+ }
+
+ installContext (context) {
+ for ( const k in context.constants ) {
+ this.constant(k, context.constants[k]);
+ }
+ return this;
+ }
+
+ constant (name, value) {
+ Object.defineProperty(this.constants, name, {
+ value
+ });
+ return this;
+ }
+
+ beforeAll (name, handler) {
+ this.beforeAlls.push({
+ name, handler
+ });
+ return this;
+ }
+
+ onTransitionTo (name, handler) {
+ if ( ! this.transitions.hasOwnProperty(name) ) {
+ this.transitions[name] = [];
+ }
+ this.transitions[name].push(handler);
+ return this;
+ }
+
+ build () {
+ const params = {};
+ for ( const facet of this.constructor.COMMON_1 ) {
+ params[facet + 's'] = this[facet + 's'];
+ }
+ return new StatefulProcessor({
+ ...params,
+ constants: this.constants,
+ beforeAlls: this.beforeAlls,
+ transitions: this.transitions,
+ });
+ }
+}
\ No newline at end of file
diff --git a/packages/phoenix/src/util/wrap-text.js b/packages/phoenix/src/util/wrap-text.js
new file mode 100644
index 00000000..478f1e76
--- /dev/null
+++ b/packages/phoenix/src/util/wrap-text.js
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+export function lengthIgnoringEscapes(text) {
+ const escape = '\x1b';
+ // There are a lot of different ones, but we only use graphics-mode ones, so only parse those for now.
+ // TODO: Parse other escape sequences as needed.
+ // Format is: ESC, '[', DIGIT, 0 or more characters, and then 'm'
+ const escapeSequenceRegex = /^\x1B\[\d.*?m/;
+
+ let length = 0;
+ for (let i = 0; i < text.length; i++) {
+ const char = text[i];
+ if (char === escape) {
+ // Consume an ANSI escape sequence
+ const match = text.substring(i).match(escapeSequenceRegex);
+ if (match) {
+ i += match[0].length - 1;
+ }
+ continue;
+ }
+ length++;
+ }
+ return length;
+}
+
+// TODO: Ensure this works with multi-byte characters (UTF-8)
+export const wrapText = (text, width) => {
+ const whitespaceChars = ' \t'.split('');
+ const isWhitespace = c => {
+ return whitespaceChars.includes(c);
+ };
+
+ // If width was invalid, just return the original text as a failsafe.
+ if (typeof width !== 'number' || width < 1)
+ return [text];
+
+ const lines = [];
+ let currentLine = '';
+ const splitWordIfTooLong = (word) => {
+ while (lengthIgnoringEscapes(word) > width) {
+ lines.push(word.substring(0, width - 1) + '-');
+ word = word.substring(width - 1);
+ }
+
+ currentLine = word;
+ };
+
+ for (let i = 0; i < text.length; i++) {
+ const char = text.charAt(i);
+ // Handle special characters
+ if (char === '\n') {
+ lines.push(currentLine.trimEnd());
+ currentLine = '';
+ // Don't skip whitespace after a newline, to allow for indentation.
+ continue;
+ }
+ // TODO: Handle \t?
+ if (/\S/.test(char)) {
+ // Grab next word
+ let word = char;
+ while ((i+1) < text.length && /\S/.test(text[i + 1])) {
+ word += text[i+1];
+ i++;
+ }
+ if (lengthIgnoringEscapes(currentLine) === 0) {
+ splitWordIfTooLong(word);
+ continue;
+ }
+ if ((lengthIgnoringEscapes(currentLine) + lengthIgnoringEscapes(word)) > width) {
+ // Next line
+ lines.push(currentLine.trimEnd());
+ splitWordIfTooLong(word);
+ continue;
+ }
+ currentLine += word;
+ continue;
+ }
+
+ currentLine += char;
+ if (lengthIgnoringEscapes(currentLine) >= width) {
+ lines.push(currentLine.trimEnd());
+ currentLine = '';
+ // Skip whitespace at end of line.
+ while (isWhitespace(text[i + 1])) {
+ i++;
+ }
+ continue;
+ }
+ }
+ if (currentLine.length >= 0) { // Not lengthIgnoringEscapes!
+ lines.push(currentLine);
+ }
+
+ return lines;
+};
\ No newline at end of file
diff --git a/packages/phoenix/test.js b/packages/phoenix/test.js
new file mode 100644
index 00000000..2414e287
--- /dev/null
+++ b/packages/phoenix/test.js
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import {
+ StringPStratumImpl,
+ StrataParser,
+ ParserFactory,
+} from 'strataparse';
+import { buildParserFirstHalf } from './src/ansi-shell/parsing/buildParserFirstHalf.js';
+import { buildParserSecondHalf } from './src/ansi-shell/parsing/buildParserSecondHalf.js';
+
+
+const sp = new StrataParser();
+
+const cstParserFac = new ParserFactory()
+cstParserFac.concrete = true;
+cstParserFac.rememberSource = true;
+
+sp.add(
+ new StringPStratumImpl(`
+ ls | tail -n 2 "ab" > "te\\"st"
+ `)
+);
+
+// buildParserFirstHalf(sp, 'syntaxHighlighting');
+buildParserFirstHalf(sp, 'interpreting');
+buildParserSecondHalf(sp);
+
+const result = sp.parse();
+console.log(result && JSON.stringify(result, undefined, ' '));
+if ( sp.error ) {
+ console.log('has error:', sp.error);
+}
diff --git a/packages/phoenix/test/coreutils.test.js b/packages/phoenix/test/coreutils.test.js
new file mode 100644
index 00000000..004b7677
--- /dev/null
+++ b/packages/phoenix/test/coreutils.test.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { runBasenameTests } from "./coreutils/basename.js";
+import { runDateTests } from "./coreutils/date.js";
+import { runDirnameTests } from "./coreutils/dirname.js";
+import { runEchoTests } from "./coreutils/echo.js";
+import { runEnvTests } from "./coreutils/env.js";
+import { runErrnoTests } from './coreutils/errno.js';
+import { runFalseTests } from "./coreutils/false.js";
+import { runHeadTests } from "./coreutils/head.js";
+import { runPrintfTests } from './coreutils/printf.js';
+import { runSleepTests } from "./coreutils/sleep.js";
+import { runSortTests } from "./coreutils/sort.js";
+import { runTailTests } from "./coreutils/tail.js";
+import { runTrueTests } from "./coreutils/true.js";
+import { runWcTests } from "./coreutils/wc.js";
+
+describe('coreutils', function () {
+ runBasenameTests();
+ runDateTests();
+ runDirnameTests();
+ runEchoTests();
+ runEnvTests();
+ runErrnoTests();
+ runFalseTests();
+ runHeadTests();
+ runPrintfTests();
+ runSleepTests();
+ runSortTests();
+ runTailTests();
+ runTrueTests();
+ runWcTests();
+});
diff --git a/packages/phoenix/test/coreutils/basename.js b/packages/phoenix/test/coreutils/basename.js
new file mode 100644
index 00000000..888e1299
--- /dev/null
+++ b/packages/phoenix/test/coreutils/basename.js
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runBasenameTests = () => {
+ describe('basename', function () {
+ it('expects at least 1 argument', async () => {
+ let ctx = MakeTestContext(builtins.basename, {});
+ let hadError = false;
+ try {
+ await builtins.basename.execute(ctx);
+ } catch (e) {
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('should fail when given 0 arguments');
+ }
+ assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout');
+ // Output to stderr is allowed but not required.
+ });
+ it('expects at most 2 arguments', async () => {
+ let ctx = MakeTestContext(builtins.basename, {positionals: ['a', 'b', 'c']});
+ let hadError = false;
+ try {
+ await builtins.basename.execute(ctx);
+ } catch (e) {
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('should fail when given 3 arguments');
+ }
+ assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout');
+ // Output to stderr is allowed but not required.
+ });
+
+ const testCases = [
+ {
+ description: '"foo.txt" produces "foo.txt"',
+ input: ['foo.txt'],
+ expectedStdout: 'foo.txt\n'
+ },
+ {
+ description: '"./foo.txt" produces "foo.txt"',
+ input: ['./foo.txt'],
+ expectedStdout: 'foo.txt\n'
+ },
+ {
+ description: '"/a/b/c/foo.txt" produces "foo.txt"',
+ input: ['/a/b/c/foo.txt'],
+ expectedStdout: 'foo.txt\n'
+ },
+ {
+ description: 'two slashes produces "/"',
+ input: ['//'],
+ expectedStdout: '/\n'
+ },
+ {
+ description: 'a series of slashes produces "/"',
+ input: ['/////'],
+ expectedStdout: '/\n'
+ },
+ {
+ description: 'empty string produces "/"',
+ input: [''],
+ expectedStdout: '.\n'
+ },
+ {
+ description: 'trailing slashes are trimmed',
+ input: ['foo.txt/'],
+ expectedStdout: 'foo.txt\n'
+ },
+ {
+ description: 'suffix is removed from simple filename',
+ input: ['foo.txt', '.txt'],
+ expectedStdout: 'foo\n'
+ },
+ {
+ description: 'suffix is removed from path',
+ input: ['/a/b/c/foo.txt', '.txt'],
+ expectedStdout: 'foo\n'
+ },
+ {
+ description: 'suffix is removed only once',
+ input: ['/a/b/c/foo.txt.txt.txt', '.txt'],
+ expectedStdout: 'foo.txt.txt\n'
+ },
+ {
+ description: 'suffix is ignored if not found in the input',
+ input: ['/a/b/c/foo.txt', '.png'],
+ expectedStdout: 'foo.txt\n'
+ },
+ {
+ description: 'suffix is removed even if input has a trailing slash',
+ input: ['/a/b/c/foo.txt/', '.txt'],
+ expectedStdout: 'foo\n'
+ },
+ ];
+ for (const {description, input, expectedStdout} of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.basename, {positionals: input});
+ try {
+ const result = await builtins.basename.execute(ctx);
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/date.js b/packages/phoenix/test/coreutils/date.js
new file mode 100644
index 00000000..efc800ea
--- /dev/null
+++ b/packages/phoenix/test/coreutils/date.js
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import * as ck from 'chronokinesis';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runDateTests = () => {
+ describe('date', function () {
+ beforeEach(() => {
+ ck.freeze();
+ ck.timezone('UTC', '2024-03-07 13:05:07');
+ });
+ afterEach(() => {
+ ck.reset();
+ });
+
+ const testCases = [
+ {
+ description: 'outputs the date and time in a standard format when no format parameter is given',
+ input: [ ],
+ options: { utc: true },
+ expectedStdout: 'Thu Mar 7 13:05:07 UTC 2024\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'outputs the format verbatim if no format sequences are included',
+ input: [ '+hello' ],
+ options: { utc: true },
+ expectedStdout: 'hello\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%a outputs abbreviated weekday name',
+ input: [ '+%a' ],
+ options: { utc: true },
+ expectedStdout: 'Thu\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%A outputs full weekday name',
+ input: [ '+%A' ],
+ options: { utc: true },
+ expectedStdout: 'Thursday\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%b outputs abbreviated month name',
+ input: [ '+%b' ],
+ options: { utc: true },
+ expectedStdout: 'Mar\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%B outputs full month name',
+ input: [ '+%B' ],
+ options: { utc: true },
+ expectedStdout: 'March\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%c outputs full date and time',
+ input: [ '+%c' ],
+ options: { utc: true },
+ expectedStdout: '3/7/2024, 1:05:07 PM\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%C outputs century as 2 digits',
+ input: [ '+%C' ],
+ options: { utc: true },
+ expectedStdout: '20\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%d outputs day of the month as 2 digits',
+ input: [ '+%d' ],
+ options: { utc: true },
+ expectedStdout: '07\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%D outputs date as mm/dd/yy',
+ input: [ '+%D' ],
+ options: { utc: true },
+ expectedStdout: '03/07/24\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%e outputs day of the month as 2 characters padded with a leading space',
+ input: [ '+%e' ],
+ options: { utc: true },
+ expectedStdout: ' 7\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%H outputs the 24-hour clock hour, as 2 digits',
+ input: [ '+%H' ],
+ options: { utc: true },
+ expectedStdout: '13\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%h outputs the same as %b',
+ input: [ '+%h' ],
+ options: { utc: true },
+ expectedStdout: 'Mar\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%I outputs the 12-hour clock hour, as 2 digits',
+ input: [ '+%I' ],
+ options: { utc: true },
+ expectedStdout: '01\n',
+ expectedStderr: '',
+ },
+ // TODO: %j outputs the day of the year as a 3-digit number, starting at 001.
+ {
+ description: '%m outputs the month, as 2 digits, with January as 01',
+ input: [ '+%m' ],
+ options: { utc: true },
+ expectedStdout: '03\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%M outputs the minute, as 2 digits',
+ input: [ '+%M' ],
+ options: { utc: true },
+ expectedStdout: '05\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%n outputs a newline character',
+ input: [ '+%n' ],
+ options: { utc: true },
+ expectedStdout: '\n\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%p outputs AM or PM',
+ input: [ '+%p' ],
+ options: { utc: true },
+ expectedStdout: 'PM\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%r outputs the 12-hour clock time',
+ input: [ '+%r' ],
+ options: { utc: true },
+ expectedStdout: '01:05:07 PM\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%S outputs seconds, as 2 digits',
+ input: [ '+%S' ],
+ options: { utc: true },
+ expectedStdout: '07\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%t outputs a tab character',
+ input: [ '+%t' ],
+ options: { utc: true },
+ expectedStdout: '\t\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%T outputs the 24-hour clock time',
+ input: [ '+%T' ],
+ options: { utc: true },
+ expectedStdout: '13:05:07\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%u outputs the week day as a number, with Monday = 1 and Sunday = 7',
+ input: [ '+%u' ],
+ options: { utc: true },
+ expectedStdout: '4\n',
+ expectedStderr: '',
+ },
+ // TODO: %U outputs the week of the year, as 2 digits, with weeks starting on Sunday, and the first being week 00
+ // TODO: %V outputs the week of the year, as 2 digits, with weeks starting on Monday, and the first being week 01
+ {
+ description: '%w outputs the week day as a number, with Sunday = 0 and Saturday = 6',
+ input: [ '+%w' ],
+ options: { utc: true },
+ expectedStdout: '4\n',
+ expectedStderr: '',
+ },
+ // TODO: %W outputs the week of the year, as 2 digits,, with weeks starting on Monday, and the first being week 00
+ {
+ description: '%x outputs a local date representation',
+ input: [ '+%x' ],
+ options: { utc: true },
+ expectedStdout: '3/7/2024\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%X outputs a local time representation',
+ input: [ '+%X' ],
+ options: { utc: true },
+ expectedStdout: '1:05:07 PM\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%y outputs the year within a century, as 2 digits',
+ input: [ '+%y' ],
+ options: { utc: true },
+ expectedStdout: '24\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%Y outputs the year',
+ input: [ '+%Y' ],
+ options: { utc: true },
+ expectedStdout: '2024\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%Z outputs the timezone name',
+ input: [ '+%Z' ],
+ options: { utc: true },
+ expectedStdout: 'UTC\n',
+ expectedStderr: '',
+ },
+ {
+ description: '%% outputs a percent sign',
+ input: [ '+%%' ],
+ options: { utc: true },
+ expectedStdout: '%\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'multiple format sequences can be included at once',
+ input: [ '+%B is month %m' ],
+ options: { utc: true },
+ expectedStdout: 'March is month 03\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'unrecognized formats are output verbatim',
+ input: [ '+%4%L hello' ],
+ options: { utc: true },
+ expectedStdout: '%4%L hello\n',
+ expectedStderr: '',
+ },
+ ];
+
+ for (const { description, input, options, expectedStdout, expectedStderr, expectedFail } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.date, { positionals: input, values: options });
+ let hadError = false;
+ try {
+ const result = await builtins.date.execute(ctx);
+ if (!expectedFail) {
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ }
+ } catch (e) {
+ hadError = true;
+ if (!expectedFail) {
+ assert.fail(e);
+ }
+ }
+ if (expectedFail && !hadError) {
+ assert.fail('should have returned an error code');
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr');
+ });
+ }
+ });
+};
diff --git a/packages/phoenix/test/coreutils/dirname.js b/packages/phoenix/test/coreutils/dirname.js
new file mode 100644
index 00000000..a263b9d7
--- /dev/null
+++ b/packages/phoenix/test/coreutils/dirname.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runDirnameTests = () => {
+ describe('dirname', function () {
+ it('expects at least 1 argument', async () => {
+ let ctx = MakeTestContext(builtins.dirname, {});
+ let hadError = false;
+ try {
+ await builtins.dirname.execute(ctx);
+ } catch (e) {
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('should fail when given 0 arguments');
+ }
+ assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout');
+ // Output to stderr is allowed but not required.
+ });
+ it('expects at most 1 argument', async () => {
+ let ctx = MakeTestContext(builtins.dirname, {positionals: ['a', 'b']});
+ let hadError = false;
+ try {
+ await builtins.dirname.execute(ctx);
+ } catch (e) {
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('should fail when given 2 or more arguments');
+ }
+ assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout');
+ // Output to stderr is allowed but not required.
+ });
+
+ const testCases = [
+ {
+ description: '"foo.txt" produces "."',
+ input: 'foo.txt',
+ expectedStdout: '.\n'
+ },
+ {
+ description: '"./foo.txt" produces "."',
+ input: './foo.txt',
+ expectedStdout: '.\n'
+ },
+ {
+ description: '"/a/b/c/foo.txt" produces "/a/b/c"',
+ input: '/a/b/c/foo.txt',
+ expectedStdout: '/a/b/c\n'
+ },
+ {
+ description: '"a/b/c/foo.txt" produces "a/b/c"',
+ input: 'a/b/c/foo.txt',
+ expectedStdout: 'a/b/c\n'
+ },
+ {
+ description: 'two slashes produces "/"',
+ input: '//',
+ expectedStdout: '/\n'
+ },
+ {
+ description: 'a series of slashes produces "/"',
+ input: '/////',
+ expectedStdout: '/\n'
+ },
+ {
+ description: 'empty string produces "/"',
+ input: '',
+ expectedStdout: '/\n'
+ },
+ {
+ description: 'trailing slashes are trimmed',
+ input: 'a/b/c////foo//',
+ expectedStdout: 'a/b/c\n'
+ },
+ ];
+ for (const {description, input, expectedStdout} of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.dirname, {positionals: [input]});
+ try {
+ const result = await builtins.dirname.execute(ctx);
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/echo.js b/packages/phoenix/test/coreutils/echo.js
new file mode 100644
index 00000000..099f762c
--- /dev/null
+++ b/packages/phoenix/test/coreutils/echo.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runEchoTests = () => {
+ describe('echo', function () {
+ const testCases = [
+ {
+ description: 'empty input prints a newline',
+ input: [],
+ options: {},
+ expectedStdout: '\n'
+ },
+ {
+ description: 'single input is printed',
+ input: ['hello'],
+ options: {},
+ expectedStdout: 'hello\n'
+ },
+ {
+ description: 'multiple inputs are printed, separated by spaces',
+ input: ['hello', 'world'],
+ options: {},
+ expectedStdout: 'hello world\n'
+ },
+ {
+ description: '-n suppresses newlines',
+ input: ['hello', 'world'],
+ options: {
+ n: true
+ },
+ expectedStdout: 'hello world'
+ },
+ // TODO: Test the `-e` option for interpreting backslash escapes.
+ ];
+ for (const {description, input, options, expectedStdout} of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.echo, {positionals: input, values: options});
+ try {
+ const result = await builtins.echo.execute(ctx);
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/env.js b/packages/phoenix/test/coreutils/env.js
new file mode 100644
index 00000000..5cb30a54
--- /dev/null
+++ b/packages/phoenix/test/coreutils/env.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runEnvTests = () => {
+ describe('env', function () {
+ it('should return a non-zero exit code, and output the env variables', async function () {
+ let ctx = MakeTestContext(builtins.env, { env: {'a': '1', 'b': '2' } });
+ try {
+ await builtins.env.execute(ctx);
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, 'a=1\nb=2\n', 'env should output the env variables, one per line');
+ assert.equal(ctx.externs.err.output, '', 'env should not write to stderr');
+ });
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/errno.js b/packages/phoenix/test/coreutils/errno.js
new file mode 100644
index 00000000..fbb63b53
--- /dev/null
+++ b/packages/phoenix/test/coreutils/errno.js
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+import { ErrorCodes, ErrorMetadata } from '../../src/platform/PosixError.js';
+
+export const runErrnoTests = () => {
+ describe('errno', function () {
+
+ const testCases = [
+ {
+ description: 'exits normally if nothing is passed in',
+ input: [ ],
+ values: {},
+ expectedStdout: '',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'can search by number',
+ input: [ ErrorMetadata.get(ErrorCodes.EFBIG).code.toString() ],
+ values: {},
+ expectedStdout: 'EFBIG 27 File too big\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'can search by number',
+ input: [ ErrorCodes.EIO.description ],
+ values: {},
+ expectedStdout: 'EIO 5 IO error\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'prints an error message and returns a code > 0 if an error is not found',
+ input: [ 'NOT-A-REAL-ERROR' ],
+ values: {},
+ expectedStdout: '',
+ expectedStderr: 'ERROR: Not understood: NOT-A-REAL-ERROR\n',
+ expectedFail: true,
+ },
+ {
+ description: 'accepts multiple arguments and prints each',
+ input: [ ErrorMetadata.get(ErrorCodes.ENOENT).code.toString(), 'NOT-A-REAL-ERROR', ErrorCodes.EPIPE.description ],
+ values: {},
+ expectedStdout:
+ 'ENOENT 2 File or directory not found\n' +
+ 'EPIPE 32 Pipe broken\n',
+ expectedStderr: 'ERROR: Not understood: NOT-A-REAL-ERROR\n',
+ expectedFail: true,
+ },
+ {
+ description: 'searches descriptions if --search is provided',
+ input: [ 'directory' ],
+ values: { search: true },
+ expectedStdout:
+ 'ENOENT 2 File or directory not found\n' +
+ 'ENOTDIR 20 Is not a directory\n' +
+ 'EISDIR 21 Is a directory\n' +
+ 'ENOTEMPTY 39 Directory is not empty\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: 'lists all errors if --list is provided, ignoring parameters',
+ input: [ 'directory' ],
+ values: { list: true },
+ expectedStdout:
+ 'EPERM 1 Operation not permitted\n' +
+ 'ENOENT 2 File or directory not found\n' +
+ 'EIO 5 IO error\n' +
+ 'EACCES 13 Permission denied\n' +
+ 'EEXIST 17 File already exists\n' +
+ 'ENOTDIR 20 Is not a directory\n' +
+ 'EISDIR 21 Is a directory\n' +
+ 'EINVAL 22 Argument invalid\n' +
+ 'EMFILE 24 Too many open files\n' +
+ 'EFBIG 27 File too big\n' +
+ 'ENOSPC 28 Device out of space\n' +
+ 'EPIPE 32 Pipe broken\n' +
+ 'ENOTEMPTY 39 Directory is not empty\n' +
+ 'EADDRINUSE 98 Address already in use\n' +
+ 'ECONNRESET 104 Connection reset\n' +
+ 'ETIMEDOUT 110 Connection timed out\n' +
+ 'ECONNREFUSED 111 Connection refused\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ {
+ description: '--search overrides --list',
+ input: [ 'directory' ],
+ values: { list: true, search: true },
+ expectedStdout:
+ 'ENOENT 2 File or directory not found\n' +
+ 'ENOTDIR 20 Is not a directory\n' +
+ 'EISDIR 21 Is a directory\n' +
+ 'ENOTEMPTY 39 Directory is not empty\n',
+ expectedStderr: '',
+ expectedFail: false,
+ },
+ ];
+
+ for (const { description, input, values, expectedStdout, expectedStderr, expectedFail } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.errno, { positionals: input, values });
+ let hadError = false;
+ try {
+ const result = await builtins.errno.execute(ctx);
+ if (!expectedFail) {
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ }
+ } catch (e) {
+ hadError = true;
+ if (!expectedFail) {
+ assert.fail(e);
+ }
+ }
+ if (expectedFail && !hadError) {
+ assert.fail('should have returned an error code');
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/false.js b/packages/phoenix/test/coreutils/false.js
new file mode 100644
index 00000000..bb88d31e
--- /dev/null
+++ b/packages/phoenix/test/coreutils/false.js
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+import { Exit } from "../../src/puter-shell/coreutils/coreutil_lib/exit.js";
+
+async function testFalse(options) {
+ let ctx = MakeTestContext(builtins.false, options);
+ let hadError = false;
+ try {
+ await builtins.false.execute(ctx);
+ } catch (e) {
+ assert(e instanceof Exit);
+ assert.notEqual(e.code, 0, 'returned exit code 0, meaning success');
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('didn\'t return an exit code');
+ }
+ assert.equal(ctx.externs.out.output, '', 'false should not write to stdout');
+ assert.equal(ctx.externs.err.output, '', 'false should not write to stderr');
+}
+
+export const runFalseTests = () => {
+ describe('false', function () {
+ it('should return a non-zero exit code, with no output', async function () {
+ await testFalse({});
+ });
+ it('should allow, but ignore, positional arguments', async function () {
+ await testFalse({positionals: ['foo', 'bar', 'baz']});
+ });
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/harness.js b/packages/phoenix/test/coreutils/harness.js
new file mode 100644
index 00000000..339b9b9d
--- /dev/null
+++ b/packages/phoenix/test/coreutils/harness.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import { Context } from "contextlink";
+import { SyncLinesReader } from '../../src/ansi-shell/ioutil/SyncLinesReader.js';
+import { CommandStdinDecorator } from '../../src/ansi-shell/pipeline/iowrappers.js';
+
+export class WritableStringStream extends WritableStream {
+ constructor() {
+ super({
+ write: (chunk) => {
+ if (this.output_ === undefined)
+ this.output_ = "";
+ this.output_ += chunk;
+ }
+ });
+ }
+
+ write(chunk) {
+ if (!this.writer_)
+ this.writer_ = this.getWriter();
+ return this.writer_.write(chunk);
+ }
+
+ get output() { return this.output_ || ""; }
+}
+
+// TODO: Flesh this out as needed.
+export const MakeTestContext = (command, { positionals = [], values = {}, stdinInputs = [], env = {} }) => {
+
+ let in_ = ReadableStream.from(stdinInputs).getReader();
+ if (command.input?.syncLines) {
+ in_ = new SyncLinesReader({ delegate: in_ });
+ }
+ in_ = new CommandStdinDecorator(in_);
+
+ return new Context({
+ cmdExecState: { valid: true },
+ externs: new Context({
+ in_,
+ out: new WritableStringStream(),
+ err: new WritableStringStream(),
+ sig: null,
+ }),
+ locals: new Context({
+ args: [],
+ command,
+ positionals,
+ values,
+ }),
+ platform: new Context({}),
+ plugins: new Context({}),
+ registries: new Context({}),
+ env: env,
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/head.js b/packages/phoenix/test/coreutils/head.js
new file mode 100644
index 00000000..fb34034f
--- /dev/null
+++ b/packages/phoenix/test/coreutils/head.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runHeadTests = () => {
+ describe('head', function () {
+ // Too many parameters
+ // Bad -n
+ const failureCases = [
+ {
+ description: 'expects at most 1 argument',
+ options: {},
+ positionals: ['1', '2'],
+ },
+ {
+ description: 'expects --lines, if set, to be a number',
+ options: { lines: 'frog' },
+ positionals: ['-'],
+ },
+ {
+ description: 'expects --lines, if set, to be an integer',
+ options: { lines: '1.75' },
+ positionals: ['-'],
+ },
+ {
+ description: 'expects --lines, if set, to be positive',
+ options: { lines: '-3' },
+ positionals: ['-'],
+ },
+ {
+ description: 'expects --lines, if set, to not be 0',
+ options: { lines: '0' },
+ positionals: ['-'],
+ },
+ ];
+ for (const { description, options, positionals } of failureCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.head, { positionals, values: options });
+ let hadError = false;
+ try {
+ await builtins.head.execute(ctx);
+ } catch (e) {
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('didn\'t return an error code');
+ }
+ assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout');
+ // Output to stderr is allowed but not required.
+ });
+ }
+
+ const alphabet = 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\n';
+ const testCases = [
+ {
+ description: 'reads from stdin if no parameter is given',
+ options: {},
+ positionals: [],
+ stdin: alphabet,
+ expectedStdout: 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n',
+ },
+ {
+ description: 'reads from stdin if parameter is `-`',
+ options: {},
+ positionals: ['-'],
+ stdin: alphabet,
+ expectedStdout: 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n',
+ },
+ {
+ description: '--lines/-n specifies how many lines to write',
+ options: { lines: 5 },
+ positionals: ['-'],
+ stdin: alphabet,
+ expectedStdout: 'a\nb\nc\nd\ne\n',
+ },
+ {
+ description: 'when --lines/-n is greater than the number of lines, write everything',
+ options: { lines: 500 },
+ positionals: ['-'],
+ stdin: alphabet,
+ expectedStdout: alphabet,
+ },
+ // TODO: Test with files once the harness supports that.
+ ];
+ for (const { description, options, positionals, stdin, expectedStdout } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.head, { positionals, values: options, stdinInputs: [stdin] });
+ try {
+ const result = await builtins.head.execute(ctx);
+ assert.equal(result, undefined);
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, '', 'sleep should not write to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/printf.js b/packages/phoenix/test/coreutils/printf.js
new file mode 100644
index 00000000..04c6c6a9
--- /dev/null
+++ b/packages/phoenix/test/coreutils/printf.js
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runPrintfTests = () => {
+ describe('printf', function () {
+ const testCases = [
+ {
+ description: 'outputs format verbatim if no operands were given',
+ input: [ 'hello' ],
+ expectedStdout: 'hello',
+ expectedStderr: '',
+ },
+ {
+ description: 'outputs octal escape sequences',
+ input: [ '\\0\\41\\041' ],
+ expectedStdout: '\0!!',
+ expectedStderr: '',
+ },
+ {
+ description: 'outputs a trailing backslash as itself',
+ input: [ '\\' ],
+ expectedStdout: '\\',
+ expectedStderr: '',
+ },
+ {
+ description: 'outputs unrecognized escape sequences as themselves',
+ input: [ '\\z\\@\\#' ],
+ expectedStdout: '\\z\\@\\#',
+ expectedStderr: '',
+ },
+ {
+ description: 'outputs escape sequences',
+ input: [ '\\a\\b\\f\\n\\r\\t\\v' ],
+ expectedStdout: '\x07\x08\x0C\n\r\t\x0B',
+ expectedStderr: '',
+ },
+ {
+ description: 'rejects empty format specifier',
+ input: [ '%' ],
+ expectedStdout: '',
+ expectedStderr: 'printf: Invalid conversion specifier \'%\'\n',
+ expectedFail: true,
+ },
+ {
+ description: 'outputs `%%` as `%`',
+ input: [ '%%' ],
+ expectedStdout: '%',
+ expectedStderr: '',
+ },
+
+ //
+ // %c: Character
+ //
+ {
+ description: 'outputs single characters for `%c`',
+ input: [ '%c', 'hello', '123' ],
+ expectedStdout: 'h1',
+ expectedStderr: '',
+ },
+ {
+ description: 'outputs single characters for `%c`',
+ input: [ '%c', 'hello', '123' ],
+ expectedStdout: 'h1',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding and alignment for `%c`',
+ input: [ '"%-12c" "%12c"', 'hello', '123' ],
+ expectedStdout: '"h " " 1"',
+ expectedStderr: '',
+ },
+
+ //
+ // %s: String
+ //
+ {
+ description: 'outputs whole value as string for `%s`',
+ input: [ '%s', 'hello', '123' ],
+ expectedStdout: 'hello123',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding and alignment for `%s`',
+ input: [ '"%-12s" "%12s"', 'hello', '123' ],
+ expectedStdout: '"hello " " 123"',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%s`',
+ input: [ '%.4s\n', 'hello', '123' ],
+ expectedStdout: 'hell\n123\n',
+ expectedStderr: '',
+ },
+
+ //
+ // %d and %i: Signed decimal integer
+ //
+ {
+ description: 'outputs a signed decimal integer for `%d` or `%i`',
+ input: [ '%d %i\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '13 13\n-127 -127\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding for `%d` and `%i`',
+ input: [ '"%5d" "%05i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '" 13" "00013"\n" -127" "-0127"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports alignment for `%d` and `%i`',
+ input: [ '"%-5d" "%0-5i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '"13 " "13 "\n"-127 " "-127 "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag for `%d` and `%i`',
+ input: [ '"%+5d" "%+05i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '" +13" "+0013"\n" -127" "-0127"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag with alignment for `%d` and `%i`',
+ input: [ '"%+-5d" "%+-05i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '"+13 " "+13 "\n"-127 " "-127 "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag for `%d` and `%i`',
+ input: [ '"% 5d" "% 05i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '" 13" " 0013"\n" -127" "-0127"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag with alignment for `%d` and `%i`',
+ input: [ '"% -5d" "% -05i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '" 13 " " 13 "\n"-127 " "-127 "\n',
+ expectedStderr: '',
+ },
+ {
+ description: '`+` flag overrides ` ` for `%d` and `%i`',
+ input: [ '"%+ -5d" "%+ 05i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '"+13 " "+0013"\n"-127 " "-0127"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%d` and `%i`',
+ input: [ '"%.5d" "%0.5i"\n', '13', '13', '-127', '-127' ],
+ expectedStdout: '"00013" "00013"\n"-00127" "-00127"\n',
+ expectedStderr: '',
+ },
+ {
+ description: '0 precision for `%d` and `%i`',
+ input: [ '"%.d" "%.0i"\n', '13', '13', '-127', '-127', '0', '0' ],
+ expectedStdout: '"13" "13"\n"-127" "-127"\n"" ""\n',
+ expectedStderr: '',
+ },
+
+ //
+ // %u: Unsigned decimal integer
+ //
+ {
+ description: 'outputs an unsigned decimal integer for `%u`',
+ input: [ '%u\n', '13', '0', '-127' ],
+ expectedStdout: '13\n0\n4294967169\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding for `%u`',
+ input: [ '"%5u" "%05u"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '" 13" "00013"\n" 0" "00000"\n"4294967169" "4294967169"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports alignment for `%u`',
+ input: [ '"%-5u" "%0-5u"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"13 " "13 "\n"0 " "0 "\n"4294967169" "4294967169"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%u`',
+ input: [ '"%.5u" "%0.5u"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"00013" "00013"\n"00000" "00000"\n"4294967169" "4294967169"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'ignores `+` and ` ` flags for `%u`',
+ input: [ '"%+u" "% u"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"13" "13"\n"0" "0"\n"4294967169" "4294967169"\n',
+ expectedStderr: '',
+ },
+ {
+ description: '0 precision for `%u`',
+ input: [ '"%.u" "%.0u"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"13" "13"\n"" ""\n"4294967169" "4294967169"\n',
+ expectedStderr: '',
+ },
+
+ //
+ // %o: Unsigned octal integer
+ //
+ {
+ description: 'outputs an unsigned octal integer for `%o`',
+ input: [ '%o\n', '13', '0', '-127' ],
+ expectedStdout: '15\n0\n37777777601\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding for `%o`',
+ input: [ '"%5o" "%05o"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '" 15" "00015"\n" 0" "00000"\n"37777777601" "37777777601"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports alignment for `%o`',
+ input: [ '"%-5o" "%0-5o"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"15 " "15 "\n"0 " "0 "\n"37777777601" "37777777601"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%o`',
+ input: [ '"%.5o" "%0.5o"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"00015" "00015"\n"00000" "00000"\n"37777777601" "37777777601"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'ignores `+` and ` ` flags for `%o`',
+ input: [ '"%+o" "% o"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"15" "15"\n"0" "0"\n"37777777601" "37777777601"\n',
+ expectedStderr: '',
+ },
+ {
+ description: '0 precision for `%o`',
+ input: [ '"%.o" "%.0o"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"15" "15"\n"" ""\n"37777777601" "37777777601"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'ensures a starting `0` when using the `#` flag for `%o`',
+ input: [ '"%#o" "%#0o"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"015" "015"\n"0" "0"\n"037777777601" "037777777601"\n',
+ expectedStderr: '',
+ },
+
+ //
+ // %x and %X: Unsigned hexadecimal integer
+ //
+ {
+ description: 'outputs an unsigned hexadecimal integer for `%x` and `%X`',
+ input: [ '"%x" "%X"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"d" "D"\n"0" "0"\n"ffffff81" "FFFFFF81"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding for `%x` and `%X`',
+ input: [ '"%5x" "%05X"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '" d" "0000D"\n" 0" "00000"\n"ffffff81" "FFFFFF81"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports alignment for `%x` and `%X`',
+ input: [ '"%-5x" "%0-5X"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"d " "D "\n"0 " "0 "\n"ffffff81" "FFFFFF81"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%x` and `%X`',
+ input: [ '"%.5x" "%0.5X"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"0000d" "0000D"\n"00000" "00000"\n"ffffff81" "FFFFFF81"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'ignores `+` and ` ` flags for `%x` and `%X`',
+ input: [ '"%+x" "% X"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"d" "D"\n"0" "0"\n"ffffff81" "FFFFFF81"\n',
+ expectedStderr: '',
+ },
+ {
+ description: '0 precision for `%x` and `%X`',
+ input: [ '"%.x" "%.0X"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"d" "D"\n"" ""\n"ffffff81" "FFFFFF81"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'ensures a starting `0x` or `0X` when using the `#` flag for `%x` and `%X`',
+ input: [ '"%#x" "%#0X"\n', '13', '13', '0', '0', '-127', '-127' ],
+ expectedStdout: '"0xd" "0XD"\n"0x0" "0X0"\n"0xffffff81" "0XFFFFFF81"\n',
+ expectedStderr: '',
+ },
+
+ //
+ // %f and %F: Floating point, decimal notation
+ //
+ {
+ description: 'outputs a floating point number in decimal notation for `%f` and `%F`',
+ input: [ '"%f" "%F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13.000000" "13.000000"\n"-12345.678900" "-12345.678900"\n"0.000010" "0.000010"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding for `%f` and `%F`',
+ input: [ '"%12f" "%012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13.000000" "00013.000000"\n"-12345.678900" "-12345.678900"\n" 0.000010" "00000.000010"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding and alignment for `%f` and `%F`',
+ input: [ '"%-12f" "%-012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13.000000 " "13.000000 "\n"-12345.678900" "-12345.678900"\n"0.000010 " "0.000010 "\n' +
+ '"infinity " "INFINITY "\n"nan " "NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag for `%f` and `%F`',
+ input: [ '"%+12f" "%+012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" +13.000000" "+0013.000000"\n"-12345.678900" "-12345.678900"\n" +0.000010" "+0000.000010"\n' +
+ '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag with alignment for `%f` and `%F`',
+ input: [ '"%+-12f" "%+-012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"+13.000000 " "+13.000000 "\n"-12345.678900" "-12345.678900"\n"+0.000010 " "+0.000010 "\n' +
+ '"+infinity " "+INFINITY "\n"+nan " "+NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag for `%f` and `%F`',
+ input: [ '"% 12f" "% 012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13.000000" " 0013.000000"\n"-12345.678900" "-12345.678900"\n" 0.000010" " 0000.000010"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag with alignment for `%f` and `%F`',
+ input: [ '"% -12f" "% -012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13.000000 " " 13.000000 "\n"-12345.678900" "-12345.678900"\n" 0.000010 " " 0.000010 "\n' +
+ '" infinity " " INFINITY "\n" nan " " NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: '`+` flag overrides ` ` for `%f` and `%F`',
+ input: [ '"% +12f" "% +012F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" +13.000000" "+0013.000000"\n"-12345.678900" "-12345.678900"\n" +0.000010" "+0000.000010"\n' +
+ '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%f` and `%F`',
+ input: [ '"%.3f" "%0.3F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13.000" "13.000"\n"-12345.679" "-12345.679"\n"0.000" "0.000"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'zero precision removes decimal point for `%f` and `%F`',
+ input: [ '"%.0f" "%0.0F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13" "13"\n"-12346" "-12346"\n"0" "0"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'zero precision with `#` flag forces a decimal point for `%f` and `%F`',
+ input: [ '"%#.0f" "%0#.0F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13." "13."\n"-12346." "-12346."\n"0." "0."\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports width and precision for `%f` and `%F`',
+ input: [ '"%12.3f" "%012.3F"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13.000" "00000013.000"\n" -12345.679" "-0012345.679"\n" 0.000" "00000000.000"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+
+ //
+ // %e and %E: Floating point, exponential notation
+ //
+ {
+ description: 'outputs a floating point number in exponential notation for `%e` and `%E`',
+ input: [ '"%e" "%E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"1.300000e+01" "1.300000E+01"\n"-1.234568e+04" "-1.234568E+04"\n"1.000000e-05" "1.000000E-05"\n' +
+ '"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding for `%e` and `%E`',
+ input: [ '"%15e" "%015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 1.300000e+01" "0001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" 1.000000e-05" "0001.000000E-05"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding and alignment for `%e` and `%E`',
+ input: [ '"%-15e" "%-015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"1.300000e+01 " "1.300000E+01 "\n"-1.234568e+04 " "-1.234568E+04 "\n"1.000000e-05 " "1.000000E-05 "\n' +
+ '"infinity " "INFINITY "\n"nan " "NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag for `%e` and `%E`',
+ input: [ '"%+15e" "%+015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" +1.300000e+01" "+001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" +1.000000e-05" "+001.000000E-05"\n' +
+ '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag with alignment for `%e` and `%E`',
+ input: [ '"%+-15e" "%+-015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"+1.300000e+01 " "+1.300000E+01 "\n"-1.234568e+04 " "-1.234568E+04 "\n"+1.000000e-05 " "+1.000000E-05 "\n' +
+ '"+infinity " "+INFINITY "\n"+nan " "+NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag for `%e` and `%E`',
+ input: [ '"% 15e" "% 015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 1.300000e+01" " 001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" 1.000000e-05" " 001.000000E-05"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag with alignment for `%e` and `%E`',
+ input: [ '"% -15e" "% -015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 1.300000e+01 " " 1.300000E+01 "\n"-1.234568e+04 " "-1.234568E+04 "\n" 1.000000e-05 " " 1.000000E-05 "\n' +
+ '" infinity " " INFINITY "\n" nan " " NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: '`+` flag overrides ` ` for `%e` and `%E`',
+ input: [ '"% +15e" "% +015E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" +1.300000e+01" "+001.300000E+01"\n" -1.234568e+04" "-001.234568E+04"\n" +1.000000e-05" "+001.000000E-05"\n' +
+ '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%e` and `%E`',
+ input: [ '"%.3e" "%0.3E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"1.300e+01" "1.300E+01"\n"-1.235e+04" "-1.235E+04"\n"1.000e-05" "1.000E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'zero precision removes decimal point for `%e` and `%E`',
+ input: [ '"%.0e" "%0.0E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"1e+01" "1E+01"\n"-1e+04" "-1E+04"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'zero precision with `#` flag forces a decimal point for `%e` and `%E`',
+ input: [ '"%#.0e" "%0#.0E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"1.e+01" "1.E+01"\n"-1.e+04" "-1.E+04"\n"1.e-05" "1.E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports width and precision for `%e` and `%E`',
+ input: [ '"%15.3e" "%015.3E"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 1.300e+01" "0000001.300E+01"\n" -1.235e+04" "-000001.235E+04"\n" 1.000e-05" "0000001.000E-05"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+
+ //
+ // %g and %G: Floating point, set number of significant digits, may be decimal or exponential notation
+ //
+ {
+ description: 'outputs a floating point number for `%g` and `%G`',
+ input: [ '"%g" "%G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13" "13"\n"-12345.7" "-12345.7"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding for `%g` and `%G`',
+ input: [ '"%12g" "%012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13" "000000000013"\n" -12345.7" "-000012345.7"\n" 1e-05" "00000001E-05"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports padding and alignment for `%g` and `%G`',
+ input: [ '"%-12g" "%-012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13 " "13 "\n"-12345.7 " "-12345.7 "\n"1e-05 " "1E-05 "\n' +
+ '"infinity " "INFINITY "\n"nan " "NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag for `%g` and `%G`',
+ input: [ '"%+12g" "%+012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" +13" "+00000000013"\n" -12345.7" "-000012345.7"\n" +1e-05" "+0000001E-05"\n' +
+ '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports `+` flag with alignment for `%g` and `%G`',
+ input: [ '"%+-12g" "%+-012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"+13 " "+13 "\n"-12345.7 " "-12345.7 "\n"+1e-05 " "+1E-05 "\n' +
+ '"+infinity " "+INFINITY "\n"+nan " "+NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag for `%g` and `%G`',
+ input: [ '"% 12g" "% 012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13" " 00000000013"\n" -12345.7" "-000012345.7"\n" 1e-05" " 0000001E-05"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports ` ` flag with alignment for `%g` and `%G`',
+ input: [ '"% -12g" "% -012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13 " " 13 "\n"-12345.7 " "-12345.7 "\n" 1e-05 " " 1E-05 "\n' +
+ '" infinity " " INFINITY "\n" nan " " NAN "\n',
+ expectedStderr: '',
+ },
+ {
+ description: '`+` flag overrides ` ` for `%g` and `%G`',
+ input: [ '"% +12g" "% +012G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" +13" "+00000000013"\n" -12345.7" "-000012345.7"\n" +1e-05" "+0000001E-05"\n' +
+ '" +infinity" " +INFINITY"\n" +nan" " +NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports precision for `%g` and `%G`',
+ input: [ '"%.3g" "%0.3G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"13" "13"\n"-1.23e+04" "-1.23E+04"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'zero precision removes decimal point for `%g` and `%G`',
+ input: [ '"%.0g" "%0.0G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"1e+01" "1E+01"\n"-1e+04" "-1E+04"\n"1e-05" "1E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'zero precision with `#` flag forces a decimal point for `%g` and `%G`',
+ input: [ '"%#.0g" "%0#.0G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '"1.e+01" "1.E+01"\n"-1.e+04" "-1.E+04"\n"1.e-05" "1.E-05"\n"infinity" "INFINITY"\n"nan" "NAN"\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports width and precision for `%g` and `%G`',
+ input: [ '"%12.3g" "%012.3G"\n', '13', '13', '-12345.67890', '-12345.67890', '0.00001', '0.00001', 'Infinity', 'Infinity', 'NaN', 'NaN' ],
+ expectedStdout: '" 13" "000000000013"\n" -1.23e+04" "-0001.23E+04"\n" 1e-05" "00000001E-05"\n' +
+ '" infinity" " INFINITY"\n" nan" " NAN"\n',
+ expectedStderr: '',
+ },
+ ];
+
+ for (const { description, input, expectedStdout, expectedStderr, expectedFail } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.printf, { positionals: input });
+ let hadError = false;
+ try {
+ const result = await builtins.printf.execute(ctx);
+ if (!expectedFail) {
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ }
+ } catch (e) {
+ hadError = true;
+ if (!expectedFail) {
+ assert.fail(e);
+ }
+ }
+ if (expectedFail && !hadError) {
+ assert.fail('should have returned an error code');
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr');
+ });
+ }
+ });
+};
diff --git a/packages/phoenix/test/coreutils/sleep.js b/packages/phoenix/test/coreutils/sleep.js
new file mode 100644
index 00000000..039dbd8c
--- /dev/null
+++ b/packages/phoenix/test/coreutils/sleep.js
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import sinon from 'sinon';
+import { MakeTestContext } from './harness.js';
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runSleepTests = () => {
+ describe('sleep', function () {
+ let clock;
+ beforeEach(() => {
+ clock = sinon.useFakeTimers();
+ });
+ afterEach(() => {
+ clock.restore();
+ });
+
+ const failureCases = [
+ {
+ description: 'expects at least 1 argument',
+ positionals: [],
+ },
+ {
+ description: 'expects at most 1 argument',
+ positionals: ['1', '2'],
+ },
+ {
+ description: 'expects its argument to be a number',
+ positionals: ['frog'],
+ },
+ {
+ description: 'expects its argument to be positive',
+ positionals: ['-1'],
+ },
+ ];
+ for (const { description, positionals } of failureCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.sleep, { positionals });
+ let hadError = false;
+ try {
+ await builtins.sleep.execute(ctx);
+ } catch (e) {
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('didn\'t return an error code');
+ }
+ assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout');
+ // Output to stderr is allowed but not required.
+ });
+ }
+
+ const testCases = [
+ {
+ description: 'sleep 0.5',
+ positionals: ['0.5'],
+ durationS: 0.5,
+ },
+ {
+ description: 'sleep 1',
+ positionals: ['1'],
+ durationS: 1,
+ },
+ {
+ description: 'sleep 1.5',
+ positionals: ['1.5'],
+ durationS: 1.5,
+ },
+ {
+ description: 'sleep 27',
+ positionals: ['27'],
+ durationS: 27,
+ },
+ ];
+ for (const { description, positionals, durationS } of testCases) {
+ it(description, async () => {
+ const durationMs = durationS * 1000;
+ let ctx = MakeTestContext(builtins.sleep, { positionals });
+ const startTimeMs = performance.now();
+ let endTimeMs;
+ builtins.sleep.execute(ctx)
+ .then(() => { endTimeMs = performance.now(); })
+ .catch((e) => { assert.fail(e); });
+ await clock.tickAsync(durationMs - 5);
+ assert.ok(endTimeMs === undefined, `sleep took less than ${durationS}s, took ${(endTimeMs - startTimeMs) / 1000}s`);
+ await clock.tickAsync(10);
+ assert.ok(endTimeMs !== undefined, `sleep took more than ${durationS}s, not done after ${(durationS + 0.005)}s`);
+
+ assert.equal(ctx.externs.out.output, '', 'sleep should not write to stdout');
+ assert.equal(ctx.externs.err.output, '', 'sleep should not write to stderr');
+ });
+ }
+ });
+};
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/sort.js b/packages/phoenix/test/coreutils/sort.js
new file mode 100644
index 00000000..98302935
--- /dev/null
+++ b/packages/phoenix/test/coreutils/sort.js
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runSortTests = () => {
+ describe('sort', function () {
+ const testCases = [
+ {
+ description: 'reads from stdin if no parameter is given',
+ options: {},
+ positionals: [],
+ stdin: 'a\nb\nc\n',
+ expectedStdout: 'a\nb\nc\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'reads from stdin if parameter is `-`',
+ options: {},
+ positionals: ['-'],
+ stdin: 'a\nb\nc\n',
+ expectedStdout: 'a\nb\nc\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'sorts the output by byte value by default',
+ options: {},
+ positionals: ['-'],
+ stdin: 'awesome\nCOOL\nAmazing\n!\ncold\n123\n',
+ expectedStdout: '!\n123\nAmazing\nCOOL\nawesome\ncold\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'keeps duplicates by default',
+ options: {},
+ positionals: ['-'],
+ stdin: 'a\na\na\n',
+ expectedStdout: 'a\na\na\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'removes duplicates when -u/--unique is specified',
+ options: { unique: true },
+ positionals: ['-'],
+ stdin: 'a\nd\na\nb\nc\nc\nb\na\n',
+ expectedStdout: 'a\nb\nc\nd\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'reverses the order when -r/--reverse is specified',
+ options: { reverse: true },
+ positionals: ['-'],
+ stdin: 'a\nd\na\nb\nc\nc\nb\na\n',
+ expectedStdout: 'd\nc\nc\nb\nb\na\na\na\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports --reverse and --unique together',
+ options: { reverse: true, unique: true },
+ positionals: ['-'],
+ stdin: 'a\nd\na\nb\nc\nc\nb\na\n',
+ expectedStdout: 'd\nc\nb\na\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'sorts case-insensitively when -f/--ignore-case is specified',
+ options: { 'ignore-case': true },
+ positionals: ['-'],
+ stdin: 'b\nB\nA\na\n',
+ expectedStdout: 'A\na\nb\nB\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports --ignore-case and --unique together',
+ options: { 'ignore-case': true, unique: true },
+ positionals: ['-'],
+ stdin: 'b\nB\nA\na\n',
+ expectedStdout: 'A\nb\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'considers only printing characters when -i/--ignore-nonprinting is specified',
+ options: { 'ignore-nonprinting': true },
+ positionals: ['-'],
+ stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n',
+ expectedStdout: '*-*-*z\n=======a=======\n????b\n?a\na\n\0\0\0\0b\n hello\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports --ignore-nonprinting and --unique together',
+ options: { 'ignore-nonprinting': true, unique: true },
+ positionals: ['-'],
+ stdin: '\0\0c\n\0b\nA\na\n\0a\n',
+ expectedStdout: 'A\na\n\0b\n\0\0c\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'considers only alphanumeric and whitespace characters when -d/--dictionary-order is specified',
+ options: { 'dictionary-order': true },
+ positionals: ['-'],
+ stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n',
+ expectedStdout: ' hello\na\n?a\n=======a=======\n????b\n\0\0\0\0b\n*-*-*z\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports --dictionary-order and --unique together',
+ options: { 'dictionary-order': true, unique: true },
+ positionals: ['-'],
+ stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n',
+ expectedStdout: ' hello\na\n????b\n*-*-*z\n',
+ expectedStderr: '',
+ },
+ {
+ description: 'supports --dictionary-order and --ignore-nonprinting together',
+ options: { 'dictionary-order': true, 'ignore-nonprinting': true },
+ positionals: ['-'],
+ stdin: '*-*-*z\n????b\na\n hello\n?a\n=======a=======\n\0\0\0\0b\n',
+ expectedStdout: 'a\n?a\n=======a=======\n????b\n\0\0\0\0b\n hello\n*-*-*z\n',
+ expectedStderr: '',
+ },
+ // TODO: Test with files once the harness supports that.
+ ];
+ for (const { description, options, positionals, stdin, expectedStdout, expectedStderr } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.sort, { positionals, values: options, stdinInputs: [stdin] });
+ try {
+ const result = await builtins.sort.execute(ctx);
+ assert.equal(result, undefined);
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, expectedStderr, 'wrong output written to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/tail.js b/packages/phoenix/test/coreutils/tail.js
new file mode 100644
index 00000000..93cd28f0
--- /dev/null
+++ b/packages/phoenix/test/coreutils/tail.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runTailTests = () => {
+ describe('tail', function () {
+ // Too many parameters
+ // Bad -n
+ const failureCases = [
+ {
+ description: 'expects at most 1 argument',
+ options: {},
+ positionals: ['1', '2'],
+ },
+ {
+ description: 'expects --lines, if set, to be a number',
+ options: { lines: 'frog' },
+ positionals: ['-'],
+ },
+ {
+ description: 'expects --lines, if set, to be an integer',
+ options: { lines: '1.75' },
+ positionals: ['-'],
+ },
+ {
+ description: 'expects --lines, if set, to be positive',
+ options: { lines: '-3' },
+ positionals: ['-'],
+ },
+ {
+ description: 'expects --lines, if set, to not be 0',
+ options: { lines: '0' },
+ positionals: ['-'],
+ },
+ ];
+ for (const { description, options, positionals } of failureCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.tail, { positionals, values: options });
+ let hadError = false;
+ try {
+ await builtins.tail.execute(ctx);
+ } catch (e) {
+ hadError = true;
+ }
+ if (!hadError) {
+ assert.fail('didn\'t return an error code');
+ }
+ assert.equal(ctx.externs.out.output, '', 'nothing should be written to stdout');
+ // Output to stderr is allowed but not required.
+ });
+ }
+
+ const alphabet = 'a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\n';
+ const testCases = [
+ {
+ description: 'reads from stdin if no parameter is given',
+ options: {},
+ positionals: [],
+ stdin: alphabet,
+ expectedStdout: 'q\nr\ns\nt\nu\nv\nw\nx\ny\nz\n',
+ },
+ {
+ description: 'reads from stdin if parameter is `-`',
+ options: {},
+ positionals: ['-'],
+ stdin: alphabet,
+ expectedStdout: 'q\nr\ns\nt\nu\nv\nw\nx\ny\nz\n',
+ },
+ {
+ description: '--lines/-n specifies how many lines to write',
+ options: { lines: 5 },
+ positionals: ['-'],
+ stdin: alphabet,
+ expectedStdout: 'v\nw\nx\ny\nz\n',
+ },
+ {
+ description: 'when --lines/-n is greater than the number of lines, write everything',
+ options: { lines: 500 },
+ positionals: ['-'],
+ stdin: alphabet,
+ expectedStdout: alphabet,
+ },
+ // TODO: Test with files once the harness supports that.
+ ];
+ for (const { description, options, positionals, stdin, expectedStdout } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.tail, { positionals, values: options, stdinInputs: [stdin] });
+ try {
+ const result = await builtins.tail.execute(ctx);
+ assert.equal(result, undefined);
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, '', 'sleep should not write to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/true.js b/packages/phoenix/test/coreutils/true.js
new file mode 100644
index 00000000..7ef5dfe8
--- /dev/null
+++ b/packages/phoenix/test/coreutils/true.js
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+async function testTrue(options) {
+ let ctx = MakeTestContext(builtins.true, options);
+ try {
+ const result = await builtins.true.execute(ctx);
+ assert.equal(result, undefined);
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, '', 'true should not write to stdout');
+ assert.equal(ctx.externs.err.output, '', 'true should not write to stderr');
+}
+
+export const runTrueTests = () => {
+ describe('true', function () {
+ it('should execute successfully with no output', async function () {
+ await testTrue({});
+ });
+ it('should allow, but ignore, positional arguments', async function () {
+ await testTrue({positionals: ['foo', 'bar', 'baz']});
+ });
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/coreutils/wc.js b/packages/phoenix/test/coreutils/wc.js
new file mode 100644
index 00000000..16fa45bb
--- /dev/null
+++ b/packages/phoenix/test/coreutils/wc.js
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { MakeTestContext } from './harness.js'
+import builtins from '../../src/puter-shell/coreutils/__exports__.js';
+
+export const runWcTests = () => {
+ describe('wc', function () {
+ const testCases = [
+ {
+ description: 'can read from stdin when given `-`',
+ positionals: ['-'],
+ stdin: 'Well hello friends!',
+ expectedStdout: '0 3 19 -\n',
+ },
+ {
+ description: 'reads from stdin when given no arguments',
+ positionals: [],
+ stdin: 'Well hello friends!',
+ expectedStdout: '0 3 19\n',
+ },
+ {
+ description: 'handles empty stdin',
+ positionals: ['-'],
+ stdin: '',
+ expectedStdout: '0 0 0 -\n',
+ },
+ {
+ description: 'counts newlines',
+ positionals: ['-'],
+ stdin: 'Well\nhello\nfriends!\n',
+ expectedStdout: '3 3 20 -\n',
+ },
+ {
+ description: '-lwc produces the default output',
+ options: { bytes: true, lines: true, words: true },
+ positionals: ['-'],
+ stdin: 'Well\nhello\nfriends!\n',
+ expectedStdout: '3 3 20 -\n',
+ },
+ {
+ description: '-l outputs only lines',
+ options: { lines: true },
+ positionals: ['-'],
+ stdin: 'Well\nhello\nmy friends!\n',
+ expectedStdout: '3 -\n',
+ },
+ {
+ description: '-w outputs only words',
+ options: { words: true },
+ positionals: ['-'],
+ stdin: 'Well\nhello\nmy friends!\n',
+ expectedStdout: '4 -\n',
+ },
+ {
+ description: '-c outputs only bytes',
+ options: { bytes: true },
+ positionals: ['-'],
+ stdin: '🖥️ Well\nhello\nmy friends!\n',
+ expectedStdout: '31 -\n',
+ },
+ {
+ description: '-m outputs only characters',
+ options: { chars: true },
+ positionals: ['-'],
+ stdin: '🖥️ Well\nhello\nmy friends!\n',
+ expectedStdout: '27 -\n',
+ },
+ {
+ description: '-L outputs the maximum line length',
+ options: { 'max-line-length': true },
+ positionals: ['-'],
+ stdin: '🖥️ Well\nhello\nmy friends!\n',
+ expectedStdout: '11 -\n',
+ },
+ {
+ description: '-L treats tabs as jumping to the next multiple of 8 columns',
+ options: { 'max-line-length': true },
+ positionals: ['-'],
+ stdin: 'hi\tmum\t!\n',
+ expectedStdout: '17 -\n',
+ },
+ {
+ description: '-lwmcL outputs everything',
+ options: { bytes: true, chars: true, lines: true, 'max-line-length': true, words: true },
+ positionals: ['-'],
+ stdin: '🖥️ Well\nhello\nmy friends!\n',
+ expectedStdout: '3 5 27 31 11 -\n',
+ },
+ // TODO: Test with files once the harness supports that.
+ ];
+ for (const { description, options, positionals, stdin, expectedStdout } of testCases) {
+ it(description, async () => {
+ let ctx = MakeTestContext(builtins.wc, { positionals, values: options, stdinInputs: [stdin] });
+ try {
+ const result = await builtins.wc.execute(ctx);
+ assert.equal(result, undefined, 'should exit successfully, returning nothing');
+ } catch (e) {
+ assert.fail(e);
+ }
+ assert.equal(ctx.externs.out.output, expectedStdout, 'wrong output written to stdout');
+ assert.equal(ctx.externs.err.output, '', 'nothing should be written to stderr');
+ });
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/phoenix/test/readtoken.js b/packages/phoenix/test/readtoken.js
new file mode 100644
index 00000000..549e3589
--- /dev/null
+++ b/packages/phoenix/test/readtoken.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { readtoken, TOKENS } from '../src/ansi-shell/readline/readtoken.js';
+
+describe('readtoken', () => {
+ const tcases = [
+ {
+ desc: 'should accept unquoted string',
+ input: 'asdf',
+ expected: ['asdf']
+ },
+ {
+ desc: 'should accept leading spaces',
+ input: ' asdf',
+ expected: ['asdf']
+ },
+ {
+ desc: 'should accept trailing spaces',
+ input: 'asdf ',
+ expected: ['asdf']
+ },
+ {
+ desc: 'should expected quoted string',
+ input: '"asdf"',
+ expected: ['asdf']
+ },
+ {
+ desc: 'should recognize pipe with no whitespace',
+ input: 'asdf|zxcv',
+ expected: ['asdf', TOKENS['|'], 'zxcv']
+ },
+ {
+ desc: 'mixed quoted and unquoted should work',
+ input: '"asdf" zxcv',
+ expected: ['asdf', 'zxcv']
+ },
+ ];
+ for ( const { desc, input, expected } of tcases ) {
+ it(desc, () => {
+ assert.deepEqual(readtoken(input), expected)
+ });
+ }
+})
\ No newline at end of file
diff --git a/packages/phoenix/test/test-bytes.js b/packages/phoenix/test/test-bytes.js
new file mode 100644
index 00000000..ba583762
--- /dev/null
+++ b/packages/phoenix/test/test-bytes.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { Uint8List } from '../src/util/bytes.js';
+
+describe('bytes', () => {
+ describe('Uint8List', () => {
+ it ('should satisfy: 5 bytes of input', () => {
+ const list = new Uint8List();
+ for ( let i = 0 ; i < 5 ; i++ ) {
+ list.append(i);
+ }
+ const array = list.toArray();
+ assert.equal(array.length, 5);
+ for ( let i = 0 ; i < 5 ; i++ ) {
+ assert.equal(array[i], i);
+ }
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/phoenix/test/test-stateful-processor.js b/packages/phoenix/test/test-stateful-processor.js
new file mode 100644
index 00000000..8e6ecd5a
--- /dev/null
+++ b/packages/phoenix/test/test-stateful-processor.js
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+
+import { StatefulProcessorBuilder } from '../src/util/statemachine.js';
+
+describe('StatefulProcessor', async () => {
+ it ('should satisfy: simple example', async () => {
+ const messages = [];
+ const processor = new StatefulProcessorBuilder()
+ .state('start', async ctx => {
+ messages.push('start');
+ ctx.setState('intermediate');
+ })
+ .state('intermediate', async ctx => {
+ messages.push('intermediate');
+ ctx.setState('end');
+ })
+ .build();
+ await processor.run();
+ assert.deepEqual(messages, ['start', 'intermediate']);
+ });
+ it ('should handle transition', async () => {
+ const messages = [];
+ const processor = new StatefulProcessorBuilder()
+ .state('start', async ctx => {
+ messages.push('start');
+ ctx.setState('intermediate');
+ })
+ .onTransitionTo('intermediate', ctx => {
+ messages.push('transition');
+ ctx.locals.test1 = true;
+ })
+ .state('intermediate', async ctx => {
+ messages.push('intermediate');
+ assert.equal(ctx.locals.test1, true);
+ ctx.setState('end');
+ })
+ .build();
+ await processor.run();
+ assert.deepEqual(messages, [
+ 'start', 'transition', 'intermediate'
+ ]);
+ });
+ it ('should handle beforeAll', async () => {
+ const messages = [];
+ const processor = new StatefulProcessorBuilder()
+ .state('start', async ctx => {
+ messages.push('start');
+ assert.equal(ctx.locals.test2, 'undefined_a');
+ ctx.setState('intermediate');
+ })
+ .beforeAll('example-hook', async ctx => {
+ messages.push('before');
+ ctx.locals.test2 += '_a';
+ })
+ .state('intermediate', async ctx => {
+ messages.push('intermediate');
+ assert.equal(ctx.locals.test2, 'undefined_a');
+ ctx.setState('end');
+ })
+ .build();
+ await processor.run();
+ assert.deepEqual(messages, [
+ 'before', 'start', 'before', 'intermediate'
+ ]);
+ });
+ it ('should fail when export is missing', async () => {
+ const messages = [];
+ const processor = new StatefulProcessorBuilder()
+ .external('test3', { required: true })
+ .state('start', async ctx => {
+ ctx.setState('end');
+ })
+ .build();
+ await assert.rejects(processor.run());
+ });
+ it ('should succeed when export is provided', async () => {
+ const messages = [];
+ const processor = new StatefulProcessorBuilder()
+ .external('test3', { required: true })
+ .state('start', async ctx => {
+ messages.push(ctx.externs.test3)
+ ctx.setState('end');
+ })
+ .build();
+ await processor.run({ test3: 'test4' });
+ assert.deepEqual(messages, ['test4']);
+ });
+})
+
diff --git a/packages/phoenix/test/wrap-text.js b/packages/phoenix/test/wrap-text.js
new file mode 100644
index 00000000..3ee7a7bd
--- /dev/null
+++ b/packages/phoenix/test/wrap-text.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import assert from 'assert';
+import { lengthIgnoringEscapes, wrapText } from '../src/util/wrap-text.js';
+
+describe('wrapText', () => {
+ const testCases = [
+ {
+ description: 'should wrap text',
+ input: 'Well, hello friends! How are you today?',
+ width: 12,
+ output: ['Well, hello', 'friends! How', 'are you', 'today?'],
+ },
+ {
+ description: 'should break too-long words onto multiple lines',
+ input: 'Antidisestablishmentarianism.',
+ width: 20,
+ output: ['Antidisestablishmen-', 'tarianism.'],
+ },
+ {
+ description: 'should break too-long words onto multiple lines',
+ input: 'Antidisestablishmentarianism.',
+ width: 10,
+ output: ['Antidises-', 'tablishme-', 'ntarianis-', 'm.'],
+ },
+ {
+ description: 'should break too-long words when there is already text on the line',
+ input: 'The longest word I can think of is antidisestablishmentarianism.',
+ width: 20,
+ output: ['The longest word I', 'can think of is', 'antidisestablishmen-', 'tarianism.'],
+ },
+ {
+ description: 'should return the original text if the width is invalid',
+ input: 'Well, hello friends!',
+ width: 0,
+ output: ['Well, hello friends!'],
+ },
+ {
+ description: 'should maintain existing newlines',
+ input: 'Well\nhello\n\nfriends!',
+ width: 20,
+ output: ['Well', 'hello', '', 'friends!'],
+ },
+ {
+ description: 'should maintain indentation after newlines',
+ input: 'Well\n hello\n\nfriends!',
+ width: 20,
+ output: ['Well', ' hello', '', 'friends!'],
+ },
+ {
+ description: 'should ignore ansi escape sequences',
+ input: '\x1B[34;1mWell this is some text with ansi escape sequences\x1B[0m',
+ width: 20,
+ output: ['\x1B[34;1mWell this is some', 'text with ansi', 'escape sequences\x1B[0m'],
+ },
+ ];
+ for (const { description, input, width, output } of testCases) {
+ it (description, () => {
+ const result = wrapText(input, width);
+ for (const line of result) {
+ if (typeof width === 'number' && width > 0) {
+ assert.ok(lengthIgnoringEscapes(line) <= width, `Line is too long: '${line}'`);
+ }
+ }
+ assert.equal('|' + result.join('|\n|') + '|', '|' + output.join('|\n|') + '|');
+ });
+ }
+})
\ No newline at end of file
diff --git a/packages/phoenix/tools/build_tar.sh b/packages/phoenix/tools/build_tar.sh
new file mode 100755
index 00000000..c064787b
--- /dev/null
+++ b/packages/phoenix/tools/build_tar.sh
@@ -0,0 +1,21 @@
+if [ $(basename "$(pwd)") != "phoenix" ]; then
+ echo "This should be run in the dev-ansi-termial repo"
+ exit 1
+fi
+
+export CONFIG_FILE='config/release.js'
+npx rollup -c rollup.config.js
+
+if [ -d ./release ]; then
+ rm -rf ./release/*
+fi
+
+mkdir -p release
+mkdir -p release/puter-shell
+
+cp -r ./dist/* ./release
+
+cd ../dev-puter-shell
+npx rollup -c rollup.config.js
+cp -r ./dist/* ../phoenix/release/puter-shell
+cd -
diff --git a/packages/phoenix/tools/gen.js b/packages/phoenix/tools/gen.js
new file mode 100644
index 00000000..031d6932
--- /dev/null
+++ b/packages/phoenix/tools/gen.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Phoenix Shell.
+ *
+ * Phoenix Shell is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+// Script that generates some of the javascript files
+import fs from 'fs';
+import path from 'path';
+
+const [directory] = process.argv.slice(2);
+const target = path.resolve(process.cwd(), directory);
+const outputFile = path.resolve(target, '__exports__.js');
+
+const files = fs.readdirSync(target);
+
+let output = '';
+const line = str => {
+ output += str + '\n';
+}
+
+const toVar = name => {
+ name = name.replace(/-/g, '_');
+ return 'module_' + name;
+}
+
+const licenseLines = fs.readFileSync('../doc/license_header.txt', {encoding: 'utf8'}).split('\n');
+licenseLines.pop(); // Remove trailing empty line
+line('/*');
+for (const licenseLine of licenseLines) {
+ if (licenseLine.length === 0) {
+ line(' *');
+ } else {
+ line(` * ${licenseLine}`);
+ }
+}
+line(' */');
+line('// Generated by /tools/gen.js');
+
+for ( const file of files ) {
+ if ( ! file.endsWith('.js') ) continue;
+ const name = path.parse(file).name;
+ if ( name === '__exports__' ) continue;
+ line(`import ${toVar(name)} from './${file}'`);
+}
+
+line('');
+line('export default {');
+
+for ( const file of files ) {
+ if ( ! file.endsWith('.js') ) continue;
+ const name = path.parse(file).name;
+ if ( name === '__exports__' ) continue;
+ line(` ${JSON.stringify(name)}: ${toVar(name)},`);
+}
+
+line('};');
+
+fs.writeFileSync(outputFile, output);