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);