mirror of
https://github.com/HeyPuter/puter.git
synced 2025-01-23 14:20:22 +08:00
Merge pull request #274 from HeyPuter/eric/builtin-apps
Move terminal and phoenix into main repo
This commit is contained in:
commit
fbbc73308e
923
package-lock.json
generated
923
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,18 @@ class SelfhostedModule extends AdvancedBase {
|
||||
command: 'npm',
|
||||
args: ['run', 'start-webpack'],
|
||||
},
|
||||
{
|
||||
name: 'terminal:rollup-watch',
|
||||
directory: 'packages/terminal',
|
||||
command: 'npx',
|
||||
args: ['rollup', '-c', 'rollup.config.js', '--watch'],
|
||||
},
|
||||
{
|
||||
name: 'phoenix:rollup-watch',
|
||||
directory: 'packages/terminal',
|
||||
command: 'npx',
|
||||
args: ['rollup', '-c', 'rollup.config.js', '--watch'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@ -31,6 +43,14 @@ class SelfhostedModule extends AdvancedBase {
|
||||
prefix: '/sdk',
|
||||
path: path_.resolve(__dirname, '../../../packages/puter-js/dist'),
|
||||
},
|
||||
{
|
||||
prefix: '/builtin/terminal',
|
||||
path: path_.resolve(__dirname, '../../../packages/terminal/dist'),
|
||||
},
|
||||
{
|
||||
prefix: '/builtin/phoenix',
|
||||
path: path_.resolve(__dirname, '../../../packages/phoenix/dist'),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ module.exports = {
|
||||
maxlen: 7000,
|
||||
},
|
||||
maximize_on_start: 'flag',
|
||||
background: 'flag',
|
||||
subdomain: {
|
||||
type: 'string',
|
||||
transient: true,
|
||||
|
@ -72,6 +72,7 @@ router.get('/apps', auth, express.json({limit: '50mb'}), async (req, res, next)=
|
||||
icon: apps_res[i].icon,
|
||||
index_url: apps_res[i].index_url,
|
||||
godmode: apps_res[i].godmode,
|
||||
background: apps_res[i].background,
|
||||
maximize_on_start: apps_res[i].maximize_on_start,
|
||||
filetype_associations: filetype_associations,
|
||||
...stats,
|
||||
@ -111,6 +112,7 @@ router.get('/apps/:name', auth, express.json({limit: '50mb'}), async (req, res,
|
||||
title: app.title,
|
||||
icon: app.icon,
|
||||
godmode: app.godmode,
|
||||
background: app.background,
|
||||
maximize_on_start: app.maximize_on_start,
|
||||
index_url: app.index_url,
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
this.db = new Database(this.config.path);
|
||||
|
||||
// Database upgrade logic
|
||||
const TARGET_VERSION = 2;
|
||||
const TARGET_VERSION = 3;
|
||||
|
||||
if ( do_setup ) {
|
||||
this.log.noticeme(`SETUP: creating database at ${this.config.path}`);
|
||||
@ -51,6 +51,7 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
'0002_add-default-apps.sql',
|
||||
'0003_user-permissions.sql',
|
||||
'0004_sessions.sql',
|
||||
'0005_background-apps.sql',
|
||||
].map(p => path_.join(__dirname, 'sqlite_setup', p));
|
||||
const fs = require('fs');
|
||||
for ( const filename of sql_files ) {
|
||||
@ -75,6 +76,10 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
upgrade_files.push('0004_sessions.sql');
|
||||
}
|
||||
|
||||
if ( user_version <= 2 ) {
|
||||
upgrade_files.push('0005_background-apps.sql');
|
||||
}
|
||||
|
||||
if ( upgrade_files.length > 0 ) {
|
||||
this.log.noticeme(`Database out of date: ${this.config.path}`);
|
||||
this.log.noticeme(`UPGRADING DATABASE: ${user_version} -> ${TARGET_VERSION}`);
|
||||
|
@ -0,0 +1 @@
|
||||
ALTER TABLE apps ADD COLUMN "background" BOOLEAN DEFAULT 0;
|
4
packages/phoenix/.gitignore
vendored
Normal file
4
packages/phoenix/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
release/
|
||||
*.tar
|
661
packages/phoenix/LICENSE
Normal file
661
packages/phoenix/LICENSE
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
62
packages/phoenix/README.md
Normal file
62
packages/phoenix/README.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Important notice
|
||||
|
||||
This repository is being moved to [the monorepo](https://github.com/HeyPuter/puter).
|
||||
|
||||
<hr />
|
||||
|
||||
<h2 align="center">Phoenix</h2>
|
||||
<h3 align="center">Puter's pure-javascript shell</h3>
|
||||
<h3 align="center"><img alt="" src="./doc/readme-gif.gif"></h3>
|
||||
<hr>
|
||||
|
||||
`phoenix` is a pure-javascript shell built for [puter.com](https://puter.com).
|
||||
Following the spirit of open-source initiatives we've seen like
|
||||
[SerenityOS](https://serenityos.org/),
|
||||
we've built much of the shell's functionality from scratch.
|
||||
Some interesting portions of this shell include:
|
||||
- A shell parser which produces a Concrete-Syntax-Tree
|
||||
- Pipeline constructs built on top of the [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
|
||||
- Platform support for Puter
|
||||
|
||||
The shell is a work in progress. The following improvements are considered in-scope:
|
||||
- Anything specified in [POSIX.1-2017 Chapter 2](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html)
|
||||
- UX improvements over traditional shells
|
||||
> examples include: readline syntax highlighting, hex view for binary streams
|
||||
- Platform support, so `phoenix` can run in more environments
|
||||
|
||||
## Running Phoenix
|
||||
|
||||
### In a Browser
|
||||
|
||||
You can use the [terminal on Puter](https://puter.com/app/terminal),
|
||||
or run from source by following the instructions provided for
|
||||
[Puter's terminal emulator](https://github.com/HeyPuter/terminal).
|
||||
|
||||
### Running in Node
|
||||
|
||||
Under node.js Phoenix acts as a shell for your operating system.
|
||||
This is a work-in-progress and lots of things are not working
|
||||
yet. If you'd like to try it out you can run `src/main_cli.js`.
|
||||
Check [this issue](https://github.com/HeyPuter/phoenix/issues/14)
|
||||
for updated information on our progress.
|
||||
|
||||
## Testing
|
||||
|
||||
You can find our tests in the [test/](./test) directory.
|
||||
Testing is done with [mocha](https://www.npmjs.com/package/mocha).
|
||||
Make sure it's installed, then run:
|
||||
|
||||
```sh
|
||||
npm test
|
||||
```
|
||||
|
||||
## What's on the Roadmap?
|
||||
|
||||
We're looking to continue improving the shell and broaden its usefulness.
|
||||
Here are a few ideas we have for the future:
|
||||
|
||||
- local machine platform support
|
||||
> See [this issue](https://github.com/HeyPuter/phoenix/issues/14)
|
||||
- further support for the POSIX Command Language
|
||||
> Check our list of [missing features](doc/missing-posix.md)
|
||||
|
20
packages/phoenix/assets/index.html
Normal file
20
packages/phoenix/assets/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="xterm.css">
|
||||
<link rel="stylesheet" href="normalize.css">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="__SDK_URL__"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="bundle.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
main_shell();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
23
packages/phoenix/config/dev.js
Normal file
23
packages/phoenix/config/dev.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
globalThis.__CONFIG__ = {
|
||||
"origin": "https://puter.local:8080",
|
||||
"shell.href": "https://puter.local:8081",
|
||||
"sdk_url": "http://puter.localhost:4100/sdk/puter.js",
|
||||
};
|
23
packages/phoenix/config/release.js
Normal file
23
packages/phoenix/config/release.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
globalThis.__CONFIG__ = {
|
||||
origin: location.origin,
|
||||
'shell.href': location.origin + '/puter-shell/',
|
||||
sdk_url: 'https://puter.com/puter.js/v2',
|
||||
};
|
1152
packages/phoenix/doc/devlog.md
Normal file
1152
packages/phoenix/doc/devlog.md
Normal file
File diff suppressed because it is too large
Load Diff
77
packages/phoenix/doc/graveyard/keyboard_modifiers.md
Normal file
77
packages/phoenix/doc/graveyard/keyboard_modifiers.md
Normal file
@ -0,0 +1,77 @@
|
||||
## keyboard modifier translation
|
||||
|
||||
Encoding of modifier keys in `xterm` is done following this
|
||||
table:
|
||||
encoded | keys pressed
|
||||
--------|---------------------------
|
||||
2 | Shift
|
||||
3 | Alt
|
||||
4 | Shift + Alt
|
||||
5 | Control
|
||||
6 | Shift + Control
|
||||
7 | Alt + Control
|
||||
8 | Shift + Alt + Control
|
||||
9 | Meta
|
||||
10 | Meta + Shift
|
||||
11 | Meta + Alt
|
||||
12 | Meta + Alt + Shift
|
||||
13 | Meta + Ctrl
|
||||
14 | Meta + Ctrl + Shift
|
||||
15 | Meta + Ctrl + Alt
|
||||
16 | Meta + Ctrl + Alt + Shift
|
||||
|
||||
This script was used to convert between more useful bit flags
|
||||
and the xterm encodings of the modifiers:
|
||||
|
||||
```javascript
|
||||
const modifier_keys = ['shift', 'ctrl', 'alt', 'meta'];
|
||||
const MODIFIER = {};
|
||||
for ( let i=0 ; i < modifier_keys.length ; i++ ) {
|
||||
MODIFIER[modifier_keys[i].toUpperCase()] = 1 << i;
|
||||
}
|
||||
|
||||
const pc_modifier_list = [
|
||||
MODIFIER.SHIFT,
|
||||
MODIFIER.ALT,
|
||||
MODIFIER.CTRL,
|
||||
MODIFIER.META
|
||||
];
|
||||
|
||||
const PC_STYLE_MODIFIER_MAP = {};
|
||||
|
||||
(() => {
|
||||
let i = 2;
|
||||
for ( const mod of pc_modifier_list ) {
|
||||
const new_entries = { [i++]: mod };
|
||||
for ( const key in PC_STYLE_MODIFIER_MAP ) {
|
||||
new_entries[i++] = mod | PC_STYLE_MODIFIER_MAP[key];
|
||||
}
|
||||
for ( const key in new_entries ) {
|
||||
PC_STYLE_MODIFIER_MAP[key] = new_entries[key];
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
for ( const k in PC_STYLE_MODIFIER_MAP ) {
|
||||
console.log(`${k} :: ${print(PC_STYLE_MODIFIER_MAP[k])}`);
|
||||
}
|
||||
```
|
||||
|
||||
However, it was eventually determined that the PC-style function
|
||||
keys, although this is not documented, really do represent bit
|
||||
flags if you simply subtract 1.
|
||||
|
||||
For example, this situation doesn't look like it can be explained
|
||||
using bit flags:
|
||||
- **shift** is `2`
|
||||
- **ctrl** is `5`, and has two `1` bits
|
||||
- **shift** + **ctrl** is `6`
|
||||
- flags don't explain this: `2 | 5 = 7`
|
||||
|
||||
But after subtracting `1` from each value:
|
||||
- **shift** is `1`
|
||||
- **ctrl** is `4`
|
||||
- **shift** + **ctrl** is `5`
|
||||
- flags work correctly: `1 | 4 = 5`
|
||||
|
||||
This is true for all examples.
|
59
packages/phoenix/doc/graveyard/readline.md
Normal file
59
packages/phoenix/doc/graveyard/readline.md
Normal file
@ -0,0 +1,59 @@
|
||||
## method `readline` from `BetterReader`
|
||||
|
||||
This method was meant to be a low-level line reader that simply
|
||||
terminates at the first line feed character and returns the
|
||||
input.
|
||||
|
||||
This might be useful for non-visible inputs like passwords, but
|
||||
for visible inputs it is not practical unless the output stream
|
||||
provided is decorated with something that can filter undesired
|
||||
input characters that would move the terminal cursor.
|
||||
|
||||
It's especially not useful for a prompt with history, since the
|
||||
up arrow should clear the input buffer and replace it with something
|
||||
else.
|
||||
|
||||
Where this may shine is in a benchmark. The approach here doesn't
|
||||
explicitly iterate over every byte, so assuming methods like
|
||||
`.indexOf` and `.subarray` on TypedArray values are efficient this
|
||||
would be faster than the implementation which is now used.
|
||||
|
||||
```javascript
|
||||
async readLine (options) {
|
||||
options = options ?? {};
|
||||
|
||||
let stringSoFar = '';
|
||||
|
||||
let lineFeedFound = false;
|
||||
while ( ! lineFeedFound ) {
|
||||
let chunk = await this.getChunk_();
|
||||
|
||||
const iLF = chunk.indexOf(CHAR_LF);
|
||||
|
||||
// do we have a line feed character?
|
||||
if ( iLF >= 0 ) {
|
||||
lineFeedFound = true;
|
||||
|
||||
// defer the rest of the chunk until next read
|
||||
if ( iLF !== chunk.length - 1 ) {
|
||||
this.chunks_.push(chunk.subarray(iLF + 1))
|
||||
}
|
||||
|
||||
// (note): LF is not included in return value or next read
|
||||
chunk = chunk.subarray(0, iLF);
|
||||
}
|
||||
|
||||
if ( options.stream ) {
|
||||
options.stream.write(chunk);
|
||||
if ( lineFeedFound ) {
|
||||
options.stream.write(new Uint8Array([CHAR_LF]));
|
||||
}
|
||||
}
|
||||
|
||||
const text = new TextDecoder().decode(chunk);
|
||||
stringSoFar += text;
|
||||
}
|
||||
|
||||
return stringSoFar;
|
||||
}
|
||||
```
|
16
packages/phoenix/doc/license_header.txt
Normal file
16
packages/phoenix/doc/license_header.txt
Normal file
@ -0,0 +1,16 @@
|
||||
Copyright (C) 2024 Puter Technologies Inc.
|
||||
|
||||
This file is part of Phoenix Shell.
|
||||
|
||||
Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
23
packages/phoenix/doc/missing-posix.md
Normal file
23
packages/phoenix/doc/missing-posix.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Missing POSIX Functionality
|
||||
|
||||
### References
|
||||
|
||||
- [POSIX.1-2017 Chapter 2: Shell Command Language](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html)
|
||||
|
||||
### Shell Command Language features known to be missing from `phoenix`
|
||||
|
||||
- Parameter expansion
|
||||
> This is support for `$variables`, and this is **highest priority**.
|
||||
- Compound commands
|
||||
> This is `if`, `case`, `while`, `for`, etc
|
||||
- Arithmetic expansion
|
||||
- Alias substitution
|
||||
|
||||
### How to Contribute
|
||||
|
||||
- Check the [README.md file](../README.md) for contributor guidelines.
|
||||
- Additional features will require updates to
|
||||
[the parser](phoenix/src/ansi-shell/parsing).
|
||||
Right now there are repeated concerns between
|
||||
`buildParserFirstHalf` and `buildParserSecondHalf` which need to
|
||||
be factored out.
|
55
packages/phoenix/doc/parser.md
Normal file
55
packages/phoenix/doc/parser.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Puter Terminal Parser
|
||||
|
||||
## The `strataparse` package
|
||||
|
||||
The `strataparse` package makes it possible to build parser in distinct
|
||||
layers that we call "strata" (each one called a "stratum"). Rather then
|
||||
distinguish between a "lexer" and "parser", we can instead have an
|
||||
arbitrary number of layers that use different approaches to processing
|
||||
or parsing.
|
||||
|
||||
Each stratum implements the method `next (api)`. The `api` object is
|
||||
provided by strataparse as the bridge between which the strata
|
||||
interact. Typically, it's used to call `api.delegate` to get a reference
|
||||
to the lower-level parser. Terminal strata like `StringPStratumImpl`, don't
|
||||
do this. The `next` method returns the next value in an object of the
|
||||
form `{ done: true/false, value: ... }`, matching the typical interface
|
||||
for iterators within this source code. When `done` is true, `value`
|
||||
can be a message (such as an error) indicating why parsing halted.
|
||||
|
||||
## PuterShellParser
|
||||
|
||||
At the time of writing this, the PuterShellParser class builds a parser
|
||||
with 4 strata, listed here from bottom up:
|
||||
|
||||
### buildParserFirstHalf (the "lexer half")
|
||||
|
||||
[source code](../src/ansi-shell/parsing/buildParserFirstHalf.js)
|
||||
|
||||
- A "FirstRecognized" strata which behaves like a lexer. It converts
|
||||
characters like `|` to AST nodes like `{ $: 'op.pipe' }`.
|
||||
AST nodes use the key `$` to identify the type and can have other
|
||||
arbitrary values.
|
||||
- A "MergeWhitespace" strata which is provided by `strataparse`.
|
||||
It converts whitespace to a `{ $: 'whitespace' }` AST node, and
|
||||
adds a property called `$cst` to all nodes from the delegate
|
||||
(the "lexer") as well as these whitespace nodes. This effectively
|
||||
transforms the AST nodes from before into CST nodes, providing
|
||||
information about whitespace, line numbers, and column numbers
|
||||
in a way subsequent layers can digest.
|
||||
(note that these will still be referred to as "AST nodes throughout
|
||||
this documentation).
|
||||
|
||||
[source code](../src/ansi-shell/parsing/buildParserSecondHalf.js)
|
||||
|
||||
### buildParserSecondHalf (the "parser half")
|
||||
- "ReducePrimitives" creates higher-level AST nodes from some of the
|
||||
AST nodes provided by the "previous"(lower/"lexer half") step.
|
||||
At the time of writing it's specifically just to deal with strings,
|
||||
reducing multiple `{ $: 'string.segment' }` and `{ $: 'string.escape }`
|
||||
nodes into a `{ $: 'string' }` node.
|
||||
- "ShellConstructs" creates higher-level nodes to model the behaviour
|
||||
of the shell. For example, a sequence of tokens including
|
||||
`{ $: 'op.pipe' }` nodes will be composed into a new `{ $: 'pipeline' }`
|
||||
node. The pipeline node contains an array called `components` which
|
||||
contains the tokens in between pipe operators.
|
BIN
packages/phoenix/doc/readme-gif.gif
Normal file
BIN
packages/phoenix/doc/readme-gif.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 254 KiB |
54
packages/phoenix/doc/stash/SymbolParserImpl.js
Normal file
54
packages/phoenix/doc/stash/SymbolParserImpl.js
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Here for safe-keeping - wasn't correct for shell tokens but
|
||||
// it will be needed later for variable assignments
|
||||
|
||||
export class SymbolParserImpl {
|
||||
static meta = {
|
||||
inputs: 'bytes',
|
||||
outputs: 'node'
|
||||
}
|
||||
static data = {
|
||||
rexp0: /[A-Za-z_]/,
|
||||
rexpN: /[A-Za-z0-9_]/,
|
||||
}
|
||||
parse (lexer) {
|
||||
let { done, value } = lexer.look();
|
||||
if ( done ) return;
|
||||
|
||||
const { rexp0, rexpN } = this.constructor.data;
|
||||
|
||||
value = String.fromCharCode(value);
|
||||
if ( ! rexp0.test(value) ) return;
|
||||
|
||||
let text = '' + value;
|
||||
lexer.next();
|
||||
|
||||
for ( ;; ) {
|
||||
({ done, value } = lexer.look());
|
||||
if ( done ) break;
|
||||
value = String.fromCharCode(value);
|
||||
if ( ! rexpN.test(value) ) break;
|
||||
text += value;
|
||||
lexer.next();
|
||||
}
|
||||
|
||||
return { $: 'symbol', text };
|
||||
}
|
||||
}
|
26
packages/phoenix/notalicense-license-checker-config.json
Normal file
26
packages/phoenix/notalicense-license-checker-config.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"ignore": ["**/!(*.js|*.css)"],
|
||||
"license": "doc/license_header.txt",
|
||||
"licenseFormats": {
|
||||
"js": {
|
||||
"prepend": "/*",
|
||||
"append": " */",
|
||||
"eachLine": {
|
||||
"prepend": " * "
|
||||
}
|
||||
},
|
||||
"dotfile|^Dockerfile": {
|
||||
"eachLine": {
|
||||
"prepend": "# "
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"prepend": "/*",
|
||||
"append": " */",
|
||||
"eachLine": {
|
||||
"prepend": " * "
|
||||
}
|
||||
}
|
||||
},
|
||||
"trailingWhitespace": "TRIM"
|
||||
}
|
1888
packages/phoenix/package-lock.json
generated
Normal file
1888
packages/phoenix/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
packages/phoenix/package.json
Normal file
39
packages/phoenix/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "dev-ansi-terminal",
|
||||
"version": "0.0.0",
|
||||
"description": "ANSI Terminal for Puter",
|
||||
"main": "exports.js",
|
||||
"scripts": {
|
||||
"test": "mocha ./test"
|
||||
},
|
||||
"author": "Puter Technologies Inc.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"mocha": "^10.2.0",
|
||||
"rollup": "^3.21.4",
|
||||
"rollup-plugin-copy": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0",
|
||||
"capture-console": "^1.0.2",
|
||||
"chronokinesis": "^6.0.0",
|
||||
"cli-columns": "^4.0.0",
|
||||
"columnify": "^1.6.0",
|
||||
"fs-mode-to-string": "^0.0.2",
|
||||
"json-query": "^2.2.2",
|
||||
"node-pty": "^1.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"sinon": "^17.0.1",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-fit": "^0.7.0"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/pty",
|
||||
"packages/strataparse",
|
||||
"packages/contextlink"
|
||||
]
|
||||
}
|
1
packages/phoenix/packages/contextlink/.gitignore
vendored
Normal file
1
packages/phoenix/packages/contextlink/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
45
packages/phoenix/packages/contextlink/context.js
Normal file
45
packages/phoenix/packages/contextlink/context.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class Context {
|
||||
constructor (values) {
|
||||
for ( const k in values ) this[k] = values[k];
|
||||
}
|
||||
sub (newValues) {
|
||||
if ( newValues === undefined ) newValues = {};
|
||||
const sub = Object.create(this);
|
||||
|
||||
const alreadyApplied = {};
|
||||
for ( const k in sub ) {
|
||||
if ( sub[k] instanceof Context ) {
|
||||
const newValuesForK =
|
||||
newValues.hasOwnProperty(k)
|
||||
? newValues[k] : undefined;
|
||||
sub[k] = sub[k].sub(newValuesForK);
|
||||
alreadyApplied[k] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for ( const k in newValues ) {
|
||||
if ( alreadyApplied[k] ) continue;
|
||||
sub[k] = newValues[k];
|
||||
}
|
||||
|
||||
return sub;
|
||||
}
|
||||
}
|
19
packages/phoenix/packages/contextlink/entry.js
Normal file
19
packages/phoenix/packages/contextlink/entry.js
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export { Context } from "./context.js";
|
916
packages/phoenix/packages/contextlink/package-lock.json
generated
Normal file
916
packages/phoenix/packages/contextlink/package-lock.json
generated
Normal file
@ -0,0 +1,916 @@
|
||||
{
|
||||
"name": "dev-contextlink",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dev-contextlink",
|
||||
"version": "0.0.0",
|
||||
"license": "UNLICENSED",
|
||||
"devDependencies": {
|
||||
"mocha": "^10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||
"integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-stdout": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
|
||||
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/debug/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
|
||||
"integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
|
||||
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"flat": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"is-unicode-supported": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
|
||||
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
|
||||
"integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-colors": "4.1.1",
|
||||
"browser-stdout": "1.3.1",
|
||||
"chokidar": "3.5.3",
|
||||
"debug": "4.3.4",
|
||||
"diff": "5.0.0",
|
||||
"escape-string-regexp": "4.0.0",
|
||||
"find-up": "5.0.0",
|
||||
"glob": "7.2.0",
|
||||
"he": "1.2.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"minimatch": "5.0.1",
|
||||
"ms": "2.1.3",
|
||||
"nanoid": "3.3.3",
|
||||
"serialize-javascript": "6.0.0",
|
||||
"strip-json-comments": "3.1.1",
|
||||
"supports-color": "8.1.1",
|
||||
"workerpool": "6.2.1",
|
||||
"yargs": "16.2.0",
|
||||
"yargs-parser": "20.2.4",
|
||||
"yargs-unparser": "2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"_mocha": "bin/_mocha",
|
||||
"mocha": "bin/mocha.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mochajs"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
|
||||
"integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
"integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/workerpool": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
|
||||
"integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "20.2.4",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
|
||||
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-unparser": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
|
||||
"integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"camelcase": "^6.0.0",
|
||||
"decamelize": "^4.0.0",
|
||||
"flat": "^5.0.2",
|
||||
"is-plain-obj": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
packages/phoenix/packages/contextlink/package.json
Normal file
18
packages/phoenix/packages/contextlink/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "contextlink",
|
||||
"version": "0.0.0",
|
||||
"main": "entry.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "npx mocha"
|
||||
},
|
||||
"author": "Puter Technologies Inc.",
|
||||
"license": "AGPL-3.0-only",
|
||||
"devDependencies": {
|
||||
"mocha": "^10.2.0"
|
||||
},
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"description": ""
|
||||
}
|
48
packages/phoenix/packages/contextlink/test/testcontext.js
Normal file
48
packages/phoenix/packages/contextlink/test/testcontext.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import assert from 'assert';
|
||||
import { Context } from "../context.js";
|
||||
|
||||
describe('context', () => {
|
||||
it ('works', () => {
|
||||
const ctx = new Context({ a: 1 });
|
||||
const subCtx = ctx.sub({ b: 2 });
|
||||
|
||||
assert.equal(ctx.a, 1);
|
||||
assert.equal(ctx.b, undefined);
|
||||
assert.equal(subCtx.a, 1);
|
||||
assert.equal(subCtx.b, 2);
|
||||
}),
|
||||
it ('doesn\'t mangle inner-contexts', () => {
|
||||
const ctx = new Context({
|
||||
plainObject: { a: 1, b: 2, c: 3 },
|
||||
contextObject: new Context({ i: 4, j: 5, k: 6 }),
|
||||
});
|
||||
const subCtx = ctx.sub({
|
||||
plainObject: { a: 101 },
|
||||
contextObject: { i: 104 },
|
||||
});
|
||||
assert.equal(subCtx.plainObject.a, 101);
|
||||
assert.equal(subCtx.plainObject.b, undefined);
|
||||
|
||||
assert.equal(subCtx.contextObject.i, 104);
|
||||
assert.equal(subCtx.contextObject.j, 5);
|
||||
|
||||
})
|
||||
});
|
101
packages/phoenix/packages/newparser/exports.js
Normal file
101
packages/phoenix/packages/newparser/exports.js
Normal file
@ -0,0 +1,101 @@
|
||||
import { adapt_parser, INVALID, Parser, UNRECOGNIZED, VALUE } from './lib.js';
|
||||
import { Discard, FirstMatch, None, Optional, Repeat, Sequence } from './parsers/combinators.js';
|
||||
import { Literal, StringOf } from './parsers/terminals.js';
|
||||
|
||||
class Symbol extends Parser {
|
||||
_create(symbolName) {
|
||||
this.symbolName = symbolName;
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
const parser = this.symbol_registry[this.symbolName];
|
||||
if ( ! parser ) {
|
||||
throw new Error(`No symbol defined named '${this.symbolName}'`);
|
||||
}
|
||||
const subStream = stream.fork();
|
||||
const result = parser.parse(subStream);
|
||||
console.log(`Result of parsing symbol('${this.symbolName}'):`, result);
|
||||
if ( result.status === UNRECOGNIZED ) {
|
||||
return UNRECOGNIZED;
|
||||
}
|
||||
if ( result.status === INVALID ) {
|
||||
return { status: INVALID, value: result };
|
||||
}
|
||||
stream.join(subStream);
|
||||
result.$ = this.symbolName;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class ParserWithAction {
|
||||
#parser;
|
||||
#action;
|
||||
|
||||
constructor(parser, action) {
|
||||
this.#parser = adapt_parser(parser);
|
||||
this.#action = action;
|
||||
}
|
||||
|
||||
parse (stream) {
|
||||
const parsed = this.#parser.parse(stream);
|
||||
if (parsed.status === VALUE) {
|
||||
parsed.value = this.#action(parsed.value);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
export class GrammarContext {
|
||||
constructor (parsers) {
|
||||
// Object of { parser_name: Parser, ... }
|
||||
this.parsers = parsers;
|
||||
}
|
||||
|
||||
sub (more_parsers) {
|
||||
return new GrammarContext({...this.parsers, ...more_parsers});
|
||||
}
|
||||
|
||||
define_parser (grammar, actions) {
|
||||
const symbol_registry = {};
|
||||
const api = {};
|
||||
|
||||
for (const [name, parserCls] of Object.entries(this.parsers)) {
|
||||
api[name] = (...params) => {
|
||||
const result = new parserCls();
|
||||
result._create(...params);
|
||||
result.set_symbol_registry(symbol_registry);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
for (const [name, builder] of Object.entries(grammar)) {
|
||||
if (actions[name]) {
|
||||
symbol_registry[name] = new ParserWithAction(builder(api), actions[name]);
|
||||
} else {
|
||||
symbol_registry[name] = builder(api);
|
||||
}
|
||||
}
|
||||
|
||||
return (stream, entry_symbol) => {
|
||||
const entry_parser = symbol_registry[entry_symbol];
|
||||
if (!entry_parser) {
|
||||
throw new Error(`Entry symbol '${entry_symbol}' not found in grammar.`);
|
||||
}
|
||||
return entry_parser.parse(stream);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const standard_parsers = () => {
|
||||
return {
|
||||
discard: Discard,
|
||||
firstMatch: FirstMatch,
|
||||
literal: Literal,
|
||||
none: None,
|
||||
optional: Optional,
|
||||
repeat: Repeat,
|
||||
sequence: Sequence,
|
||||
stringOf: StringOf,
|
||||
symbol: Symbol,
|
||||
}
|
||||
}
|
29
packages/phoenix/packages/newparser/lib.js
Normal file
29
packages/phoenix/packages/newparser/lib.js
Normal file
@ -0,0 +1,29 @@
|
||||
export const adapt_parser = v => v;
|
||||
|
||||
export const UNRECOGNIZED = Symbol('unrecognized');
|
||||
export const INVALID = Symbol('invalid');
|
||||
export const VALUE = Symbol('value');
|
||||
|
||||
export class Parser {
|
||||
result (o) {
|
||||
if (o.value && o.value.$discard) {
|
||||
delete o.value;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
parse (stream) {
|
||||
let result = this._parse(stream);
|
||||
if ( typeof result !== 'object' ) {
|
||||
result = { status: result };
|
||||
}
|
||||
return this.result(result);
|
||||
}
|
||||
|
||||
set_symbol_registry (symbol_registry) {
|
||||
this.symbol_registry = symbol_registry;
|
||||
}
|
||||
|
||||
_create () { throw new Error(`${this.constructor.name}._create() not implemented`); }
|
||||
_parse (stream) { throw new Error(`${this.constructor.name}._parse() not implemented`); }
|
||||
}
|
139
packages/phoenix/packages/newparser/parsers/combinators.js
Normal file
139
packages/phoenix/packages/newparser/parsers/combinators.js
Normal file
@ -0,0 +1,139 @@
|
||||
import { INVALID, UNRECOGNIZED, VALUE, adapt_parser, Parser } from '../lib.js';
|
||||
|
||||
export class Discard extends Parser {
|
||||
_create (parser) {
|
||||
this.parser = adapt_parser(parser);
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
const subStream = stream.fork();
|
||||
const result = this.parser.parse(subStream);
|
||||
if ( result.status === UNRECOGNIZED ) {
|
||||
return UNRECOGNIZED;
|
||||
}
|
||||
if ( result.status === INVALID ) {
|
||||
return result;
|
||||
}
|
||||
stream.join(subStream);
|
||||
return { status: VALUE, $: 'none', $discard: true, value: result };
|
||||
}
|
||||
}
|
||||
|
||||
export class FirstMatch extends Parser {
|
||||
_create (...parsers) {
|
||||
this.parsers = parsers.map(adapt_parser);
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
for ( const parser of this.parsers ) {
|
||||
const subStream = stream.fork();
|
||||
const result = parser.parse(subStream);
|
||||
if ( result.status === UNRECOGNIZED ) {
|
||||
continue;
|
||||
}
|
||||
if ( result.status === INVALID ) {
|
||||
return result;
|
||||
}
|
||||
stream.join(subStream);
|
||||
return result;
|
||||
}
|
||||
|
||||
return UNRECOGNIZED;
|
||||
}
|
||||
}
|
||||
|
||||
export class None extends Parser {
|
||||
_create () {}
|
||||
|
||||
_parse (stream) {
|
||||
return { status: VALUE, $: 'none', $discard: true };
|
||||
}
|
||||
}
|
||||
|
||||
export class Optional extends Parser {
|
||||
_create (parser) {
|
||||
this.parser = adapt_parser(parser);
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
const subStream = stream.fork();
|
||||
const result = this.parser.parse(subStream);
|
||||
if ( result.status === VALUE ) {
|
||||
stream.join(subStream);
|
||||
return result;
|
||||
}
|
||||
return { status: VALUE, $: 'none', $discard: true };
|
||||
}
|
||||
}
|
||||
|
||||
export class Repeat extends Parser {
|
||||
_create (value_parser, separator_parser, { trailing = false } = {}) {
|
||||
this.value_parser = adapt_parser(value_parser);
|
||||
this.separator_parser = adapt_parser(separator_parser);
|
||||
this.trailing = trailing;
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
const results = [];
|
||||
for ( ;; ) {
|
||||
const subStream = stream.fork();
|
||||
|
||||
// Value
|
||||
const result = this.value_parser.parse(subStream);
|
||||
if ( result.status === UNRECOGNIZED ) {
|
||||
break;
|
||||
}
|
||||
if ( result.status === INVALID ) {
|
||||
return { status: INVALID, value: result };
|
||||
}
|
||||
stream.join(subStream);
|
||||
if ( ! result.$discard ) results.push(result);
|
||||
|
||||
// Separator
|
||||
if ( ! this.separator_parser ) {
|
||||
continue;
|
||||
}
|
||||
const separatorResult = this.separator_parser.parse(subStream);
|
||||
if ( separatorResult.status === UNRECOGNIZED ) {
|
||||
break;
|
||||
}
|
||||
if ( separatorResult.status === INVALID ) {
|
||||
return { status: INVALID, value: separatorResult };
|
||||
}
|
||||
stream.join(subStream);
|
||||
if ( ! result.$discard ) results.push(separatorResult);
|
||||
|
||||
// TODO: Detect trailing separator and reject it if trailing==false
|
||||
}
|
||||
|
||||
if ( results.length === 0 ) {
|
||||
return UNRECOGNIZED;
|
||||
}
|
||||
|
||||
return { status: VALUE, value: results };
|
||||
}
|
||||
}
|
||||
|
||||
export class Sequence extends Parser {
|
||||
_create (...parsers) {
|
||||
this.parsers = parsers.map(adapt_parser);
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
const results = [];
|
||||
for ( const parser of this.parsers ) {
|
||||
const subStream = stream.fork();
|
||||
const result = parser.parse(subStream);
|
||||
if ( result.status === UNRECOGNIZED ) {
|
||||
return UNRECOGNIZED;
|
||||
}
|
||||
if ( result.status === INVALID ) {
|
||||
return { status: INVALID, value: result };
|
||||
}
|
||||
stream.join(subStream);
|
||||
if ( ! result.$discard ) results.push(result);
|
||||
}
|
||||
|
||||
return { status: VALUE, value: results };
|
||||
}
|
||||
}
|
46
packages/phoenix/packages/newparser/parsers/terminals.js
Normal file
46
packages/phoenix/packages/newparser/parsers/terminals.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { Parser, UNRECOGNIZED, VALUE } from '../lib.js';
|
||||
|
||||
export class Literal extends Parser {
|
||||
_create (value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
const subStream = stream.fork();
|
||||
for ( let i=0 ; i < this.value.length ; i++ ) {
|
||||
let { done, value } = subStream.next();
|
||||
if ( done ) return UNRECOGNIZED;
|
||||
if ( this.value[i] !== value ) return UNRECOGNIZED;
|
||||
}
|
||||
|
||||
stream.join(subStream);
|
||||
return { status: VALUE, $: 'literal', value: this.value };
|
||||
}
|
||||
}
|
||||
|
||||
export class StringOf extends Parser {
|
||||
_create (values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
_parse (stream) {
|
||||
const subStream = stream.fork();
|
||||
let text = '';
|
||||
|
||||
while (true) {
|
||||
let { done, value } = subStream.look();
|
||||
if ( done ) break;
|
||||
if ( ! this.values.includes(value) ) break;
|
||||
|
||||
subStream.next();
|
||||
text += value;
|
||||
}
|
||||
|
||||
if (text.length === 0) {
|
||||
return UNRECOGNIZED;
|
||||
}
|
||||
|
||||
stream.join(subStream);
|
||||
return { status: VALUE, $: 'stringOf', value: text };
|
||||
}
|
||||
}
|
181
packages/phoenix/packages/pty/exports.js
Normal file
181
packages/phoenix/packages/pty/exports.js
Normal file
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const CHAR_LF = '\n'.charCodeAt(0);
|
||||
const CHAR_CR = '\r'.charCodeAt(0);
|
||||
|
||||
export class BetterReader {
|
||||
constructor ({ delegate }) {
|
||||
this.delegate = delegate;
|
||||
this.chunks_ = [];
|
||||
}
|
||||
|
||||
async read (opt_buffer) {
|
||||
if ( ! opt_buffer && this.chunks_.length === 0 ) {
|
||||
return await this.delegate.read();
|
||||
}
|
||||
|
||||
const chunk = await this.getChunk_();
|
||||
|
||||
if ( ! opt_buffer ) {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
this.chunks_.push(chunk);
|
||||
|
||||
while ( this.getTotalBytesReady_() < opt_buffer.length ) {
|
||||
this.chunks_.push(await this.getChunk_())
|
||||
}
|
||||
|
||||
// TODO: need to handle EOT condition in this loop
|
||||
let offset = 0;
|
||||
for (;;) {
|
||||
let item = this.chunks_.shift();
|
||||
if ( item === undefined ) {
|
||||
throw new Error('calculation is wrong')
|
||||
}
|
||||
if ( offset + item.length > opt_buffer.length ) {
|
||||
const diff = opt_buffer.length - offset;
|
||||
this.chunks_.unshift(item.subarray(diff));
|
||||
item = item.subarray(0, diff);
|
||||
}
|
||||
opt_buffer.set(item, offset);
|
||||
offset += item.length;
|
||||
|
||||
if ( offset == opt_buffer.length ) break;
|
||||
}
|
||||
|
||||
// return opt_buffer.length;
|
||||
}
|
||||
|
||||
async getChunk_() {
|
||||
if ( this.chunks_.length === 0 ) {
|
||||
const { value } = await this.delegate.read();
|
||||
return value;
|
||||
}
|
||||
|
||||
const len = this.getTotalBytesReady_();
|
||||
const merged = new Uint8Array(len);
|
||||
let offset = 0;
|
||||
for ( const item of this.chunks_ ) {
|
||||
merged.set(item, offset);
|
||||
offset += item.length;
|
||||
}
|
||||
|
||||
this.chunks_ = [];
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
getTotalBytesReady_ () {
|
||||
return this.chunks_.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PTT: pseudo-terminal target; called "slave" in POSIX
|
||||
*/
|
||||
export class PTT {
|
||||
constructor(pty) {
|
||||
this.readableStream = new ReadableStream({
|
||||
start: controller => {
|
||||
this.readController = controller;
|
||||
}
|
||||
});
|
||||
this.writableStream = new WritableStream({
|
||||
start: controller => {
|
||||
this.writeController = controller;
|
||||
},
|
||||
write: chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = encoder.encode(chunk);
|
||||
}
|
||||
if ( pty.outputModeflags?.outputNLCR ) {
|
||||
chunk = pty.LF_to_CRLF(chunk);
|
||||
}
|
||||
pty.readController.enqueue(chunk);
|
||||
}
|
||||
});
|
||||
this.out = this.writableStream.getWriter();
|
||||
this.in = this.readableStream.getReader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PTY: pseudo-terminal
|
||||
*
|
||||
* This implements the PTY device driver.
|
||||
*/
|
||||
export class PTY {
|
||||
constructor () {
|
||||
this.outputModeflags = {
|
||||
outputNLCR: true
|
||||
};
|
||||
this.readableStream = new ReadableStream({
|
||||
start: controller => {
|
||||
this.readController = controller;
|
||||
}
|
||||
});
|
||||
this.writableStream = new WritableStream({
|
||||
start: controller => {
|
||||
this.writeController = controller;
|
||||
},
|
||||
write: chunk => {
|
||||
if ( typeof chunk === 'string' ) {
|
||||
chunk = encoder.encode(chunk);
|
||||
}
|
||||
for ( const target of this.targets ) {
|
||||
target.readController.enqueue(chunk);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.out = this.writableStream.getWriter();
|
||||
this.in = this.readableStream.getReader();
|
||||
this.targets = [];
|
||||
}
|
||||
|
||||
getPTT () {
|
||||
const target = new PTT(this);
|
||||
this.targets.push(target);
|
||||
return target;
|
||||
}
|
||||
|
||||
LF_to_CRLF (input) {
|
||||
let lfCount = 0;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
if (input[i] === 0x0A) {
|
||||
lfCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const output = new Uint8Array(input.length + lfCount);
|
||||
|
||||
let outputIndex = 0;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
// If LF is encountered, insert CR (0x0D) before LF (0x0A)
|
||||
if (input[i] === 0x0A) {
|
||||
output[outputIndex++] = 0x0D;
|
||||
}
|
||||
output[outputIndex++] = input[i];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
12
packages/phoenix/packages/pty/package.json
Normal file
12
packages/phoenix/packages/pty/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "dev-pty",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "exports.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Puter Technologies Inc.",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
92
packages/phoenix/packages/strataparse/dsl/ParserBuilder.js
Normal file
92
packages/phoenix/packages/strataparse/dsl/ParserBuilder.js
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { SingleParserFactory } from "../parse.js";
|
||||
|
||||
export class ParserConfigDSL extends SingleParserFactory {
|
||||
constructor (parserFactory, cls) {
|
||||
super();
|
||||
this.parserFactory = parserFactory;
|
||||
this.cls_ = cls;
|
||||
this.parseParams_ = {};
|
||||
this.grammarParams_ = {
|
||||
assign: {},
|
||||
};
|
||||
}
|
||||
|
||||
parseParams (obj) {
|
||||
Object.assign(this.parseParams_, obj);
|
||||
return this;
|
||||
}
|
||||
|
||||
assign (obj) {
|
||||
Object.assign(this.grammarParams_.assign, obj);
|
||||
return this;
|
||||
}
|
||||
|
||||
create () {
|
||||
return this.parserFactory.create(
|
||||
this.cls_, this.parseParams_, this.grammarParams_,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ParserBuilder {
|
||||
constructor ({
|
||||
parserFactory,
|
||||
parserRegistry,
|
||||
}) {
|
||||
this.parserFactory = parserFactory;
|
||||
this.parserRegistry = parserRegistry;
|
||||
this.parserAPI_ = null;
|
||||
}
|
||||
|
||||
get parserAPI () {
|
||||
if ( this.parserAPI_ ) return this.parserAPI_;
|
||||
|
||||
const parserAPI = {};
|
||||
|
||||
const parsers = this.parserRegistry.parsers;
|
||||
for ( const parserId in parsers ) {
|
||||
const parserCls = parsers[parserId];
|
||||
parserAPI[parserId] =
|
||||
this.createParserFunction(parserCls);
|
||||
}
|
||||
|
||||
return this.parserAPI_ = parserAPI;
|
||||
}
|
||||
|
||||
createParserFunction (parserCls) {
|
||||
if ( parserCls.hasOwnProperty('createFunction') ) {
|
||||
return parserCls.createFunction({
|
||||
parserFactory: this.parserFactory
|
||||
});
|
||||
}
|
||||
|
||||
return params => {
|
||||
const configDSL = new ParserConfigDSL(parserCls)
|
||||
configDSL.parseParams(params);
|
||||
return configDSL;
|
||||
};
|
||||
}
|
||||
|
||||
def (def) {
|
||||
const a = this.parserAPI;
|
||||
return def(a);
|
||||
}
|
||||
}
|
29
packages/phoenix/packages/strataparse/dsl/ParserRegistry.js
Normal file
29
packages/phoenix/packages/strataparse/dsl/ParserRegistry.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class ParserRegistry {
|
||||
constructor () {
|
||||
this.parsers_ = {};
|
||||
}
|
||||
register (id, parser) {
|
||||
this.parsers_[id] = parser;
|
||||
}
|
||||
get parsers () {
|
||||
return this.parsers_;
|
||||
}
|
||||
}
|
118
packages/phoenix/packages/strataparse/exports.js
Normal file
118
packages/phoenix/packages/strataparse/exports.js
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ParserRegistry } from './dsl/ParserRegistry.js';
|
||||
import { PStratum } from './strata.js';
|
||||
|
||||
export {
|
||||
Parser,
|
||||
ParseResult,
|
||||
ParserFactory,
|
||||
} from './parse.js';
|
||||
|
||||
import WhitespaceParserImpl from './parse_impls/whitespace.js';
|
||||
import LiteralParserImpl from './parse_impls/literal.js';
|
||||
import StrUntilParserImpl from './parse_impls/StrUntilParserImpl.js';
|
||||
|
||||
import {
|
||||
SequenceParserImpl,
|
||||
ChoiceParserImpl,
|
||||
RepeatParserImpl,
|
||||
NoneParserImpl,
|
||||
} from './parse_impls/combinators.js';
|
||||
|
||||
export {
|
||||
WhitespaceParserImpl,
|
||||
LiteralParserImpl,
|
||||
SequenceParserImpl,
|
||||
ChoiceParserImpl,
|
||||
RepeatParserImpl,
|
||||
StrUntilParserImpl,
|
||||
}
|
||||
|
||||
export {
|
||||
PStratum,
|
||||
TerminalPStratumImplType,
|
||||
DelegatingPStratumImplType,
|
||||
} from './strata.js';
|
||||
|
||||
export {
|
||||
BytesPStratumImpl,
|
||||
StringPStratumImpl
|
||||
} from './strata_impls/terminals.js';
|
||||
|
||||
export {
|
||||
default as FirstRecognizedPStratumImpl,
|
||||
} from './strata_impls/FirstRecognizedPStratumImpl.js';
|
||||
|
||||
export {
|
||||
default as ContextSwitchingPStratumImpl,
|
||||
} from './strata_impls/ContextSwitchingPStratumImpl.js';
|
||||
|
||||
export { ParserBuilder } from './dsl/ParserBuilder.js';
|
||||
|
||||
export class StrataParseFacade {
|
||||
static getDefaultParserRegistry() {
|
||||
const r = new ParserRegistry();
|
||||
r.register('sequence', SequenceParserImpl);
|
||||
r.register('choice', ChoiceParserImpl);
|
||||
r.register('repeat', RepeatParserImpl);
|
||||
r.register('literal', LiteralParserImpl);
|
||||
r.register('none', NoneParserImpl);
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
export class StrataParser {
|
||||
constructor () {
|
||||
this.strata = [];
|
||||
this.error = null;
|
||||
}
|
||||
add (stratum) {
|
||||
if ( ! ( stratum instanceof PStratum ) ) {
|
||||
stratum = new PStratum(stratum);
|
||||
}
|
||||
|
||||
// TODO: verify that terminals don't delegate
|
||||
// TODO: verify the delegating strata delegate
|
||||
if ( this.strata.length > 0 ) {
|
||||
const delegate = this.strata[this.strata.length - 1];
|
||||
stratum.setDelegate(delegate);
|
||||
}
|
||||
|
||||
this.strata.push(stratum);
|
||||
}
|
||||
next () {
|
||||
return this.strata[this.strata.length - 1].next();
|
||||
}
|
||||
parse () {
|
||||
let done, value;
|
||||
const result = [];
|
||||
for ( ;; ) {
|
||||
({ done, value } =
|
||||
this.strata[this.strata.length - 1].next());
|
||||
if ( done ) break
|
||||
result.push(value);
|
||||
}
|
||||
if ( value ) {
|
||||
this.error = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
13
packages/phoenix/packages/strataparse/package.json
Normal file
13
packages/phoenix/packages/strataparse/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "strataparse",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "exports.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Puter Technologies Inc.",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
|
141
packages/phoenix/packages/strataparse/parse.js
Normal file
141
packages/phoenix/packages/strataparse/parse.js
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class Parser {
|
||||
constructor ({
|
||||
impl,
|
||||
assign,
|
||||
}) {
|
||||
this.impl = impl;
|
||||
this.assign = assign ?? {};
|
||||
}
|
||||
parse (lexer) {
|
||||
const unadaptedResult = this.impl.parse(lexer);
|
||||
const pr = unadaptedResult instanceof ParseResult
|
||||
? unadaptedResult : new ParseResult(unadaptedResult);
|
||||
if ( pr.status === ParseResult.VALUE ) {
|
||||
pr.value = {
|
||||
...pr.value,
|
||||
...this.assign,
|
||||
};
|
||||
}
|
||||
return pr;
|
||||
}
|
||||
}
|
||||
|
||||
export class ParseResult {
|
||||
static UNRECOGNIZED = { name: 'unrecognized' };
|
||||
static VALUE = { name: 'value' };
|
||||
static INVALID = { name: 'invalid' };
|
||||
constructor (value, opt_status) {
|
||||
if (
|
||||
value === ParseResult.UNRECOGNIZED ||
|
||||
value === ParseResult.INVALID
|
||||
) {
|
||||
this.status = value;
|
||||
return;
|
||||
}
|
||||
this.status = opt_status ?? (
|
||||
value === undefined
|
||||
? ParseResult.UNRECOGNIZED
|
||||
: ParseResult.VALUE
|
||||
);
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class ConcreteSyntaxParserDecorator {
|
||||
constructor (delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
parse (lexer, ...a) {
|
||||
const start = lexer.seqNo;
|
||||
const result = this.delegate.parse(lexer, ...a);
|
||||
if ( result.status === ParseResult.VALUE ) {
|
||||
const end = lexer.seqNo;
|
||||
result.value.$cst = { start, end };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class RememberSourceParserDecorator {
|
||||
constructor (delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
parse (lexer, ...a) {
|
||||
const start = lexer.seqNo;
|
||||
const result = this.delegate.parse(lexer, ...a);
|
||||
if ( result.status === ParseResult.VALUE ) {
|
||||
const end = lexer.seqNo;
|
||||
result.value.$source = lexer.reach(start, end);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class ParserFactory {
|
||||
constructor () {
|
||||
this.concrete = false;
|
||||
this.rememberSource = false;
|
||||
}
|
||||
decorate (obj) {
|
||||
if ( this.concrete ) {
|
||||
obj = new ConcreteSyntaxParserDecorator(obj);
|
||||
}
|
||||
if ( this.rememberSource ) {
|
||||
obj = new RememberSourceParserDecorator(obj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
create (cls, parserParams, resultParams) {
|
||||
parserParams = parserParams ?? {};
|
||||
|
||||
resultParams = resultParams ?? {};
|
||||
resultParams.assign = resultParams.assign ?? {};
|
||||
|
||||
const impl = new cls(parserParams);
|
||||
const parser = new Parser({
|
||||
impl,
|
||||
assign: resultParams.assign
|
||||
});
|
||||
|
||||
// return parser;
|
||||
return this.decorate(parser);
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleParserFactory {
|
||||
create () {
|
||||
throw new Error('abstract create() must be implemented');
|
||||
}
|
||||
}
|
||||
|
||||
export class AcceptParserUtil {
|
||||
static adapt (parser) {
|
||||
if ( parser === undefined ) return undefined;
|
||||
if ( parser instanceof SingleParserFactory ) {
|
||||
parser = parser.create();
|
||||
}
|
||||
if ( ! (parser instanceof Parser) ) {
|
||||
parser = new Parser({ impl: parser });
|
||||
}
|
||||
return parser;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export default class StrUntilParserImpl {
|
||||
constructor ({ stopChars }) {
|
||||
this.stopChars = stopChars;
|
||||
}
|
||||
parse (lexer) {
|
||||
let text = '';
|
||||
for ( ;; ) {
|
||||
console.log('B')
|
||||
let { done, value } = lexer.look();
|
||||
|
||||
if ( done ) break;
|
||||
|
||||
// TODO: doing this strictly one byte at a time
|
||||
// doesn't allow multi-byte stop characters
|
||||
if ( typeof value === 'number' ) value =
|
||||
String.fromCharCode(value);
|
||||
|
||||
if ( this.stopChars.includes(value) ) break;
|
||||
|
||||
text += value;
|
||||
lexer.next();
|
||||
}
|
||||
|
||||
if ( text.length === 0 ) return;
|
||||
|
||||
console.log('test?', text)
|
||||
|
||||
return { $: 'until', text };
|
||||
}
|
||||
}
|
125
packages/phoenix/packages/strataparse/parse_impls/combinators.js
Normal file
125
packages/phoenix/packages/strataparse/parse_impls/combinators.js
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ParserConfigDSL } from "../dsl/ParserBuilder.js";
|
||||
import { AcceptParserUtil, Parser, ParseResult } from "../parse.js";
|
||||
|
||||
export class SequenceParserImpl {
|
||||
static createFunction ({ parserFactory }) {
|
||||
return (...parsers) => {
|
||||
const conf = new ParserConfigDSL(parserFactory, this);
|
||||
conf.parseParams({ parsers });
|
||||
return conf;
|
||||
};
|
||||
}
|
||||
constructor ({ parsers }) {
|
||||
this.parsers = parsers.map(AcceptParserUtil.adapt);
|
||||
}
|
||||
parse (lexer) {
|
||||
const results = [];
|
||||
for ( const parser of this.parsers ) {
|
||||
const subLexer = lexer.fork();
|
||||
const result = parser.parse(subLexer);
|
||||
if ( result.status === ParseResult.UNRECOGNIZED ) {
|
||||
return;
|
||||
}
|
||||
if ( result.status === ParseResult.INVALID ) {
|
||||
// TODO: this is wrong
|
||||
return { done: true, value: result };
|
||||
}
|
||||
lexer.join(subLexer);
|
||||
results.push(result.value);
|
||||
}
|
||||
|
||||
return { $: 'sequence', results };
|
||||
}
|
||||
}
|
||||
|
||||
export class ChoiceParserImpl {
|
||||
static createFunction ({ parserFactory }) {
|
||||
return (...parsers) => {
|
||||
const conf = new ParserConfigDSL(parserFactory, this);
|
||||
conf.parseParams({ parsers });
|
||||
return conf;
|
||||
};
|
||||
}
|
||||
constructor ({ parsers }) {
|
||||
this.parsers = parsers.map(AcceptParserUtil.adapt);
|
||||
}
|
||||
parse (lexer) {
|
||||
for ( const parser of this.parsers ) {
|
||||
const subLexer = lexer.fork();
|
||||
const result = parser.parse(subLexer);
|
||||
if ( result.status === ParseResult.UNRECOGNIZED ) {
|
||||
continue;
|
||||
}
|
||||
if ( result.status === ParseResult.INVALID ) {
|
||||
// TODO: this is wrong
|
||||
return { done: true, value: result };
|
||||
}
|
||||
lexer.join(subLexer);
|
||||
return result.value;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export class RepeatParserImpl {
|
||||
static createFunction ({ parserFactory }) {
|
||||
return (delegate) => {
|
||||
const conf = new ParserConfigDSL(parserFactory, this);
|
||||
conf.parseParams({ delegate });
|
||||
return conf;
|
||||
};
|
||||
}
|
||||
constructor ({ delegate }) {
|
||||
delegate = AcceptParserUtil.adapt(delegate);
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
parse (lexer) {
|
||||
const results = [];
|
||||
for ( ;; ) {
|
||||
const subLexer = lexer.fork();
|
||||
const result = this.delegate.parse(subLexer);
|
||||
if ( result.status === ParseResult.UNRECOGNIZED ) {
|
||||
break;
|
||||
}
|
||||
if ( result.status === ParseResult.INVALID ) {
|
||||
return { done: true, value: result };
|
||||
}
|
||||
lexer.join(subLexer);
|
||||
results.push(result.value);
|
||||
}
|
||||
|
||||
return { $: 'repeat', results };
|
||||
}
|
||||
}
|
||||
|
||||
export class NoneParserImpl {
|
||||
static createFunction ({ parserFactory }) {
|
||||
return () => {
|
||||
const conf = new ParserConfigDSL(parserFactory, this);
|
||||
return conf;
|
||||
};
|
||||
}
|
||||
parse () {
|
||||
return { $: 'none', $discard: true };
|
||||
}
|
||||
}
|
62
packages/phoenix/packages/strataparse/parse_impls/literal.js
Normal file
62
packages/phoenix/packages/strataparse/parse_impls/literal.js
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ParserConfigDSL } from "../dsl/ParserBuilder.js";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export default class LiteralParserImpl {
|
||||
static meta = {
|
||||
inputs: 'bytes',
|
||||
outputs: 'node'
|
||||
}
|
||||
static createFunction ({ parserFactory }) {
|
||||
return (value) => {
|
||||
const conf = new ParserConfigDSL(parserFactory, this);
|
||||
conf.parseParams({ value });
|
||||
return conf;
|
||||
};
|
||||
}
|
||||
constructor ({ value }) {
|
||||
// adapt value
|
||||
if ( typeof value === 'string' ) {
|
||||
value = encoder.encode(value);
|
||||
}
|
||||
|
||||
if ( value.length === 0 ) {
|
||||
throw new Error(
|
||||
'tried to construct a LiteralParser with an ' +
|
||||
'empty value, which could cause infinite ' +
|
||||
'iteration'
|
||||
);
|
||||
}
|
||||
|
||||
this.value = value;
|
||||
}
|
||||
parse (lexer) {
|
||||
for ( let i=0 ; i < this.value.length ; i++ ) {
|
||||
let { done, value } = lexer.next();
|
||||
if ( done ) return;
|
||||
if ( this.value[i] !== value ) return;
|
||||
}
|
||||
|
||||
const text = decoder.decode(this.value);
|
||||
return { $: 'literal', text };
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export default class WhitespaceParserImpl {
|
||||
static meta = {
|
||||
inputs: 'bytes',
|
||||
outputs: 'node'
|
||||
}
|
||||
static data = {
|
||||
whitespaceCharCodes: ' \r\t'.split('')
|
||||
.map(chr => chr.charCodeAt(0))
|
||||
}
|
||||
parse (lexer) {
|
||||
const { whitespaceCharCodes } = this.constructor.data;
|
||||
|
||||
let text = '';
|
||||
|
||||
for ( ;; ) {
|
||||
const { done, value } = lexer.look();
|
||||
if ( done ) break;
|
||||
if ( ! whitespaceCharCodes.includes(value) ) break;
|
||||
text += String.fromCharCode(value);
|
||||
lexer.next();
|
||||
}
|
||||
|
||||
if ( text.length === 0 ) return;
|
||||
|
||||
return { $: 'whitespace', text };
|
||||
}
|
||||
}
|
115
packages/phoenix/packages/strataparse/strata.js
Normal file
115
packages/phoenix/packages/strataparse/strata.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class DelegatingPStratumImplAPI {
|
||||
constructor (facade) {
|
||||
this.facade = facade;
|
||||
}
|
||||
get delegate () {
|
||||
return this.facade.delegate;
|
||||
}
|
||||
}
|
||||
|
||||
export class DelegatingPStratumImplType {
|
||||
constructor (facade) {
|
||||
this.facade = facade;
|
||||
}
|
||||
getImplAPI () {
|
||||
return new DelegatingPStratumImplAPI(this.facade);
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalPStratumImplType {
|
||||
getImplAPI () {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export class PStratum {
|
||||
constructor (impl) {
|
||||
this.impl = impl;
|
||||
|
||||
const implTypeClass = this.impl.constructor.TYPE
|
||||
?? DelegatingPStratumImplType;
|
||||
|
||||
this.implType = new implTypeClass(this);
|
||||
this.api = this.implType.getImplAPI();
|
||||
|
||||
this.lookValue = null;
|
||||
this.seqNo = 0;
|
||||
|
||||
this.history = [];
|
||||
// TODO: make this configurable
|
||||
this.historyOn = ! this.impl.reach;
|
||||
}
|
||||
|
||||
setDelegate (delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
look () {
|
||||
if ( this.looking ) {
|
||||
return this.lookValue;
|
||||
}
|
||||
this.looking = true;
|
||||
this.lookValue = this.impl.next(this.api);
|
||||
return this.lookValue;
|
||||
}
|
||||
|
||||
next () {
|
||||
this.seqNo++;
|
||||
let toReturn;
|
||||
if ( this.looking ) {
|
||||
this.looking = false;
|
||||
toReturn = this.lookValue;
|
||||
} else {
|
||||
toReturn = this.impl.next(this.api);
|
||||
}
|
||||
this.history.push(toReturn.value);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
fork () {
|
||||
const forkImpl = this.impl.fork(this.api);
|
||||
const fork = new PStratum(forkImpl);
|
||||
// DRY: sync state
|
||||
fork.looking = this.looking;
|
||||
fork.lookValue = this.lookValue;
|
||||
fork.seqNo = this.seqNo;
|
||||
fork.history = [...this.history];
|
||||
return fork;
|
||||
}
|
||||
|
||||
join (friend) {
|
||||
// DRY: sync state
|
||||
this.looking = friend.looking;
|
||||
this.lookValue = friend.lookValue;
|
||||
this.seqNo = friend.seqNo;
|
||||
this.history = friend.history;
|
||||
this.impl.join(this.api, friend.impl);
|
||||
}
|
||||
|
||||
reach (start, end) {
|
||||
if ( this.impl.reach ) {
|
||||
return this.impl.reach(this.api, start, end)
|
||||
}
|
||||
if ( this.historyOn ) {
|
||||
return this.history.slice(start, end);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { AcceptParserUtil, ParseResult, Parser } from "../parse.js";
|
||||
|
||||
export default class ContextSwitchingPStratumImpl {
|
||||
constructor ({ contexts, entry }) {
|
||||
this.contexts = { ...contexts };
|
||||
for ( const key in this.contexts ) {
|
||||
console.log('parsers?', this.contexts[key]);
|
||||
const new_array = [];
|
||||
for ( const parser of this.contexts[key] ) {
|
||||
if ( parser.hasOwnProperty('transition') ) {
|
||||
new_array.push({
|
||||
...parser,
|
||||
parser: AcceptParserUtil.adapt(parser.parser),
|
||||
})
|
||||
} else {
|
||||
new_array.push(AcceptParserUtil.adapt(parser));
|
||||
}
|
||||
}
|
||||
this.contexts[key] = new_array;
|
||||
}
|
||||
this.stack = [{
|
||||
context_name: entry,
|
||||
}];
|
||||
this.valid = true;
|
||||
|
||||
this.lastvalue = null;
|
||||
}
|
||||
get stack_top () {
|
||||
console.log('stack top?', this.stack[this.stack.length - 1])
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
get current_context () {
|
||||
return this.contexts[this.stack_top.context_name];
|
||||
}
|
||||
next (api) {
|
||||
if ( ! this.valid ) return { done: true };
|
||||
const lexer = api.delegate;
|
||||
|
||||
const context = this.current_context;
|
||||
console.log('context?', context);
|
||||
for ( const spec of context ) {
|
||||
{
|
||||
const { done, value } = lexer.look();
|
||||
this.anti_cycle_i = value === this.lastvalue ? (this.anti_cycle_i || 0) + 1 : 0;
|
||||
if ( this.anti_cycle_i > 30 ) {
|
||||
throw new Error('infinite loop');
|
||||
}
|
||||
this.lastvalue = value;
|
||||
console.log('last value?', value, done);
|
||||
if ( done ) return { done };
|
||||
}
|
||||
|
||||
let parser, transition, peek;
|
||||
if ( spec.hasOwnProperty('parser') ) {
|
||||
({ parser, transition, peek } = spec);
|
||||
} else {
|
||||
parser = spec;
|
||||
}
|
||||
|
||||
const subLexer = lexer.fork();
|
||||
// console.log('spec?', spec);
|
||||
const result = parser.parse(subLexer);
|
||||
if ( result.status === ParseResult.UNRECOGNIZED ) {
|
||||
continue;
|
||||
}
|
||||
if ( result.status === ParseResult.INVALID ) {
|
||||
return { done: true, value: result };
|
||||
}
|
||||
console.log('RESULT', result, spec)
|
||||
if ( ! peek ) lexer.join(subLexer);
|
||||
|
||||
if ( transition ) {
|
||||
console.log('GOT A TRANSITION')
|
||||
if ( transition.pop ) this.stack.pop();
|
||||
if ( transition.to ) this.stack.push({
|
||||
context_name: transition.to,
|
||||
});
|
||||
}
|
||||
|
||||
if ( result.value.$discard || peek ) return this.next(api);
|
||||
|
||||
console.log('PROVIDING VALUE', result.value);
|
||||
return { done: false, value: result.value };
|
||||
}
|
||||
|
||||
return { done: true, value: 'ran out of parsers' };
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { AcceptParserUtil, ParseResult, Parser } from "../parse.js";
|
||||
|
||||
export default class FirstRecognizedPStratumImpl {
|
||||
static meta = {
|
||||
description: `
|
||||
Implements a layer of top-down parsing by
|
||||
iterating over parsers for higher-level constructs
|
||||
and returning the first recognized value that was
|
||||
produced from lower-level constructs.
|
||||
`
|
||||
}
|
||||
constructor ({ parsers }) {
|
||||
this.parsers = parsers.map(AcceptParserUtil.adapt);
|
||||
this.valid = true;
|
||||
}
|
||||
next (api) {
|
||||
if ( ! this.valid ) return { done: true };
|
||||
const lexer = api.delegate;
|
||||
|
||||
for ( const parser of this.parsers ) {
|
||||
{
|
||||
const { done } = lexer.look();
|
||||
if ( done ) return { done };
|
||||
}
|
||||
|
||||
const subLexer = lexer.fork();
|
||||
const result = parser.parse(subLexer);
|
||||
if ( result.status === ParseResult.UNRECOGNIZED ) {
|
||||
continue;
|
||||
}
|
||||
if ( result.status === ParseResult.INVALID ) {
|
||||
return { done: true, value: result };
|
||||
}
|
||||
lexer.join(subLexer);
|
||||
return { done: false, value: result.value };
|
||||
}
|
||||
|
||||
return { done: true, value: 'ran out of parsers' };
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export class MergeWhitespacePStratumImpl {
|
||||
static meta = {
|
||||
inputs: 'node',
|
||||
outputs: 'node',
|
||||
}
|
||||
constructor (tabWidth) {
|
||||
this.tabWidth = tabWidth ?? 1;
|
||||
this.line = 0;
|
||||
this.col = 0;
|
||||
}
|
||||
countChar (c) {
|
||||
if ( c === '\n' ) {
|
||||
this.line++;
|
||||
this.col = 0;
|
||||
return;
|
||||
}
|
||||
if ( c === '\t' ) {
|
||||
this.col += this.tabWidth;
|
||||
return;
|
||||
}
|
||||
if ( c === '\r' ) return;
|
||||
this.col++;
|
||||
}
|
||||
next (api) {
|
||||
const lexer = api.delegate;
|
||||
|
||||
for ( ;; ) {
|
||||
const { value, done } = lexer.next();
|
||||
if ( done ) return { value, done };
|
||||
|
||||
if ( value.$ === 'whitespace' ) {
|
||||
for ( const c of value.text ) {
|
||||
this.countChar(c);
|
||||
}
|
||||
return { value, done: false };
|
||||
// continue;
|
||||
}
|
||||
|
||||
value.$cst = {
|
||||
...(value.$cst ?? {}),
|
||||
line: this.line,
|
||||
col: this.col,
|
||||
};
|
||||
|
||||
if ( value.hasOwnProperty('$source') ) {
|
||||
let source = value.$source;
|
||||
if ( source instanceof Uint8Array ) {
|
||||
source = decoder.decode(source);
|
||||
}
|
||||
for ( let c of source ) {
|
||||
this.countChar(c);
|
||||
}
|
||||
} else {
|
||||
console.warn('source missing; can\'t count position');
|
||||
}
|
||||
|
||||
return { value, done: false };
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { TerminalPStratumImplType } from "../strata.js";
|
||||
|
||||
export class BytesPStratumImpl {
|
||||
static TYPE = TerminalPStratumImplType
|
||||
|
||||
constructor (bytes, opt_i) {
|
||||
this.bytes = bytes;
|
||||
this.i = opt_i ?? 0;
|
||||
}
|
||||
next () {
|
||||
if ( this.i === this.bytes.length ) {
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
|
||||
const i = this.i++;
|
||||
return { done: false, value: this.bytes[i] };
|
||||
}
|
||||
fork () {
|
||||
return new BytesPStratumImpl(this.bytes, this.i);
|
||||
}
|
||||
join (api, forked) {
|
||||
this.i = forked.i;
|
||||
}
|
||||
reach (api, start, end) {
|
||||
return this.bytes.slice(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
export class StringPStratumImpl {
|
||||
static TYPE = TerminalPStratumImplType
|
||||
|
||||
constructor (str) {
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(str);
|
||||
this.delegate = new BytesPStratumImpl(bytes);
|
||||
}
|
||||
// DRY: proxy methods
|
||||
next (...a) {
|
||||
return this.delegate.next(...a);
|
||||
}
|
||||
fork (...a) {
|
||||
return this.delegate.fork(...a);
|
||||
}
|
||||
join (...a) {
|
||||
return this.delegate.join(...a);
|
||||
}
|
||||
reach (...a) {
|
||||
return this.delegate.reach(...a);
|
||||
}
|
||||
}
|
49
packages/phoenix/rollup.config.js
Normal file
49
packages/phoenix/rollup.config.js
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
|
||||
const configFile = process.env.CONFIG_FILE ?? 'config/dev.js';
|
||||
await import(`./${configFile}`);
|
||||
|
||||
export default {
|
||||
input: "src/main_puter.js",
|
||||
output: {
|
||||
file: "dist/bundle.js",
|
||||
format: "iife"
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve(),
|
||||
commonjs(),
|
||||
copy({
|
||||
targets: [
|
||||
{
|
||||
src: 'assets/index.html',
|
||||
dest: 'dist',
|
||||
transform: (contents, name) => {
|
||||
return contents.toString().replace('__SDK_URL__', globalThis.__CONFIG__.sdk_url);
|
||||
}
|
||||
},
|
||||
{ src: 'assets/shell.html', dest: 'dist' },
|
||||
{ src: configFile, dest: 'dist', rename: 'config.js' }
|
||||
]
|
||||
}),
|
||||
]
|
||||
}
|
15
packages/phoenix/run.json5
Normal file
15
packages/phoenix/run.json5
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
services: [
|
||||
{
|
||||
name: 'shell.http',
|
||||
pwd: './dist',
|
||||
// command: 'npx http-server -p 8080 -S -C "{cert}" -K "{key}"',
|
||||
command: 'npx http-server -p 8080',
|
||||
},
|
||||
{
|
||||
name: 'shell.rollup',
|
||||
command: 'npx rollup -c rollup.config.js --watch',
|
||||
pwd: '.'
|
||||
},
|
||||
],
|
||||
}
|
61
packages/phoenix/src/ansi-shell/ANSIContext.js
Normal file
61
packages/phoenix/src/ansi-shell/ANSIContext.js
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Context } from "../context/context.js";
|
||||
|
||||
const modifiers = ['shift', 'alt', 'ctrl', 'meta'];
|
||||
|
||||
const keyboardModifierBits = {};
|
||||
for ( let i=0 ; i < modifiers.length ; i++ ) {
|
||||
const key = `KEYBOARD_BIT_${modifiers[i].toUpperCase()}`;
|
||||
keyboardModifierBits[key] = 1 << i;
|
||||
}
|
||||
|
||||
export const ANSIContext = new Context({
|
||||
constants: {
|
||||
CHAR_LF: '\n'.charCodeAt(0),
|
||||
CHAR_CR: '\r'.charCodeAt(0),
|
||||
CHAR_TAB: '\t'.charCodeAt(0),
|
||||
CHAR_CSI: '['.charCodeAt(0),
|
||||
CHAR_OSC: ']'.charCodeAt(0),
|
||||
CHAR_ETX: 0x03,
|
||||
CHAR_EOT: 0x04,
|
||||
CHAR_ESC: 0x1B,
|
||||
CHAR_DEL: 0x7F,
|
||||
CHAR_BEL: 0x07,
|
||||
CHAR_FF: 0x0C,
|
||||
CSI_F_0: 0x40,
|
||||
CSI_F_E: 0x7F,
|
||||
...keyboardModifierBits
|
||||
}
|
||||
});
|
||||
|
||||
export const getActiveModifiersFromXTerm = (n) => {
|
||||
// decrement explained in doc/graveyard/keyboard_modifiers.md
|
||||
n--;
|
||||
|
||||
const active = {};
|
||||
|
||||
for ( let i=0 ; i < modifiers.length ; i++ ) {
|
||||
if ( n & 1 << i ) {
|
||||
active[modifiers[i]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return active;
|
||||
};
|
243
packages/phoenix/src/ansi-shell/ANSIShell.js
Normal file
243
packages/phoenix/src/ansi-shell/ANSIShell.js
Normal file
@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ConcreteSyntaxError } from "./ConcreteSyntaxError.js";
|
||||
import { MultiWriter } from "./ioutil/MultiWriter.js";
|
||||
import { Coupler } from "./pipeline/Coupler.js";
|
||||
import { Pipe } from "./pipeline/Pipe.js";
|
||||
import { Pipeline } from "./pipeline/Pipeline.js";
|
||||
|
||||
export class ANSIShell extends EventTarget {
|
||||
constructor (ctx) {
|
||||
super();
|
||||
|
||||
this.ctx = ctx;
|
||||
this.variables_ = {};
|
||||
this.config = ctx.externs.config;
|
||||
|
||||
this.debugFeatures = {};
|
||||
|
||||
const self = this;
|
||||
this.variables = new Proxy(this.variables_, {
|
||||
get (target, k) {
|
||||
return Reflect.get(target, k);
|
||||
},
|
||||
set (target, k, v) {
|
||||
const oldval = target[k];
|
||||
const retval = Reflect.set(target, k, v);
|
||||
self.dispatchEvent(new CustomEvent('shell-var-change', {
|
||||
key: k,
|
||||
oldValue: oldval,
|
||||
newValue: target[k],
|
||||
}))
|
||||
return retval;
|
||||
}
|
||||
})
|
||||
|
||||
this.addEventListener('signal.window-resize', evt => {
|
||||
this.variables.size = evt.detail;
|
||||
})
|
||||
|
||||
this.env = {};
|
||||
|
||||
this.initializeReasonableDefaults();
|
||||
}
|
||||
|
||||
export_ (k, v) {
|
||||
if ( typeof v === 'function' ) {
|
||||
Object.defineProperty(this.env, k, {
|
||||
enumerable: true,
|
||||
get: v
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.env[k] = v;
|
||||
}
|
||||
|
||||
initializeReasonableDefaults() {
|
||||
const { env } = this.ctx.platform;
|
||||
const home = env.get('HOME');
|
||||
const user = env.get('USER');
|
||||
this.variables.pwd = home;
|
||||
this.variables.home = home;
|
||||
this.variables.user = user;
|
||||
|
||||
this.variables.host = env.get('HOSTNAME');
|
||||
|
||||
// Computed values
|
||||
Object.defineProperty(this.env, 'PWD', {
|
||||
enumerable: true,
|
||||
get: () => this.variables.pwd,
|
||||
set: v => this.variables.pwd = v
|
||||
})
|
||||
Object.defineProperty(this.env, 'ROWS', {
|
||||
enumerable: true,
|
||||
get: () => this.variables.size?.rows ?? 0
|
||||
})
|
||||
Object.defineProperty(this.env, 'COLS', {
|
||||
enumerable: true,
|
||||
get: () => this.variables.size?.cols ?? 0
|
||||
})
|
||||
|
||||
this.export_('LANG', 'en_US.UTF-8');
|
||||
this.export_('PS1', '[\\u@puter.com \\w]\\$ ');
|
||||
|
||||
for ( const k in env.getEnv() ) {
|
||||
console.log('setting', k, env.get(k));
|
||||
this.export_(k, env.get(k));
|
||||
}
|
||||
|
||||
// Default values
|
||||
this.export_('HOME', () => this.variables.home);
|
||||
this.export_('USER', () => this.variables.user);
|
||||
this.export_('TERM', 'xterm-256color');
|
||||
this.export_('TERM_PROGRAM', 'puter-ansi');
|
||||
// TODO: determine how localization will affect this
|
||||
// TODO: add TERM_PROGRAM_VERSION
|
||||
// TODO: add OLDPWD
|
||||
}
|
||||
|
||||
async doPromptIteration() {
|
||||
if ( globalThis.force_eot && this.ctx.platform.name === 'node' ) {
|
||||
process.exit(0);
|
||||
}
|
||||
const { readline } = this.ctx.externs;
|
||||
// DRY: created the same way in runPipeline
|
||||
const executionCtx = this.ctx.sub({
|
||||
vars: this.variables,
|
||||
env: this.env,
|
||||
locals: {
|
||||
pwd: this.variables.pwd,
|
||||
}
|
||||
});
|
||||
this.ctx.externs.echo.off();
|
||||
const input = await readline(
|
||||
this.expandPromptString(this.env.PS1),
|
||||
executionCtx,
|
||||
);
|
||||
this.ctx.externs.echo.on();
|
||||
|
||||
if ( input.trim() === '' ) {
|
||||
this.ctx.externs.out.write('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Specially-processed inputs for debug features
|
||||
if ( input.startsWith('%%%') ) {
|
||||
this.ctx.externs.out.write('%%%: interpreting as debug instruction\n');
|
||||
const [prefix, flag, onOff] = input.split(' ');
|
||||
const isOn = onOff === 'on' ? true : false;
|
||||
this.ctx.externs.out.write(
|
||||
`%%%: Setting ${JSON.stringify(flag)} to ` +
|
||||
(isOn ? 'ON' : 'OFF') + '\n'
|
||||
)
|
||||
this.debugFeatures[flag] = isOn;
|
||||
return; // don't run as a pipeline
|
||||
}
|
||||
|
||||
// TODO: catch here, but errors need to be more structured first
|
||||
try {
|
||||
await this.runPipeline(input);
|
||||
} catch (e) {
|
||||
if ( e instanceof ConcreteSyntaxError ) {
|
||||
const here = e.print_here(input);
|
||||
this.ctx.externs.out.write(here + '\n');
|
||||
}
|
||||
this.ctx.externs.out.write('error: ' + e.message + '\n');
|
||||
console.log(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
readtoken (str) {
|
||||
return this.ctx.externs.parser.parseLineForProcessing(str);
|
||||
}
|
||||
|
||||
async runPipeline (cmdOrTokens) {
|
||||
const tokens = typeof cmdOrTokens === 'string'
|
||||
? (() => {
|
||||
// TODO: move to doPromptIter with better error objects
|
||||
try {
|
||||
return this.readtoken(cmdOrTokens)
|
||||
} catch (e) {
|
||||
this.ctx.externs.out.write('error: ' +
|
||||
e.message + '\n');
|
||||
return;
|
||||
}
|
||||
})()
|
||||
: cmdOrTokens ;
|
||||
|
||||
if ( tokens.length === 0 ) return;
|
||||
|
||||
if ( tokens.length > 1 ) {
|
||||
// TODO: as exception instead, and more descriptive
|
||||
this.ctx.externs.out.write(
|
||||
"something went wrong...\n"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let ast = tokens[0];
|
||||
|
||||
// Left the code below here (commented) because I think it's
|
||||
// interesting; the AST now always has a pipeline at the top
|
||||
// level after recent changes to the parser.
|
||||
|
||||
// // wrap an individual command in a pipeline
|
||||
// // TODO: should this be done here, or elsewhere?
|
||||
// if ( ast.$ === 'command' ) {
|
||||
// ast = {
|
||||
// $: 'pipeline',
|
||||
// components: [ast]
|
||||
// };
|
||||
// }
|
||||
|
||||
if ( this.debugFeatures['show-ast'] ) {
|
||||
this.ctx.externs.out.write(
|
||||
JSON.stringify(tokens, undefined, ' ') + '\n'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const executionCtx = this.ctx.sub({
|
||||
vars: this.variables,
|
||||
env: this.env,
|
||||
locals: {
|
||||
pwd: this.variables.pwd,
|
||||
}
|
||||
});
|
||||
|
||||
const pipeline = await Pipeline.createFromAST(executionCtx, ast);
|
||||
|
||||
await pipeline.execute(executionCtx);
|
||||
}
|
||||
|
||||
expandPromptString (str) {
|
||||
str = str.replace('\\u', this.variables.user);
|
||||
str = str.replace('\\w', this.variables.pwd);
|
||||
str = str.replace('\\h', this.variables.host);
|
||||
str = str.replace('\\$', '$');
|
||||
return str;
|
||||
}
|
||||
|
||||
async outputANSI (ctx) {
|
||||
await ctx.iterate(async item => {
|
||||
ctx.externs.out.write(item.name + '\n');
|
||||
});
|
||||
}
|
||||
}
|
57
packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js
Normal file
57
packages/phoenix/src/ansi-shell/ConcreteSyntaxError.js
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/**
|
||||
* An error for which the location it occurred within the input is known.
|
||||
*/
|
||||
export class ConcreteSyntaxError extends Error {
|
||||
constructor(message, cst_location) {
|
||||
super(message);
|
||||
this.cst_location = cst_location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the location of the error in the input.
|
||||
*
|
||||
* Example output:
|
||||
*
|
||||
* ```
|
||||
* 1: echo $($(echo zxcv))
|
||||
* ^^^^^^^^^^^
|
||||
* ```
|
||||
*
|
||||
* @param {*} input
|
||||
*/
|
||||
print_here (input) {
|
||||
const lines = input.split('\n');
|
||||
const line = lines[this.cst_location.line];
|
||||
const str_line_number = String(this.cst_location.line + 1) + ': ';
|
||||
const n_spaces =
|
||||
str_line_number.length +
|
||||
this.cst_location.start;
|
||||
const n_arrows = Math.max(
|
||||
this.cst_location.end - this.cst_location.start,
|
||||
1
|
||||
);
|
||||
|
||||
return (
|
||||
str_line_number + line + '\n' +
|
||||
' '.repeat(n_spaces) + '^'.repeat(n_arrows)
|
||||
);
|
||||
}
|
||||
}
|
53
packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js
Normal file
53
packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { parseArgs } from '@pkgjs/parseargs';
|
||||
import { DEFAULT_OPTIONS } from '../../puter-shell/coreutils/coreutil_lib/help.js';
|
||||
|
||||
export default {
|
||||
name: 'simple-parser',
|
||||
async process (ctx, spec) {
|
||||
console.log({
|
||||
...spec,
|
||||
args: ctx.locals.args
|
||||
});
|
||||
|
||||
// Insert standard options
|
||||
spec.options = Object.assign(spec.options || {}, DEFAULT_OPTIONS);
|
||||
|
||||
let result;
|
||||
try {
|
||||
if ( ! ctx.locals.args ) debugger;
|
||||
result = parseArgs({ ...spec, args: ctx.locals.args });
|
||||
} catch (e) {
|
||||
await ctx.externs.out.write(
|
||||
'\x1B[31;1m' +
|
||||
'error parsing arguments: ' +
|
||||
e.message + '\x1B[0m\n');
|
||||
ctx.cmdExecState.valid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.values.help) {
|
||||
ctx.cmdExecState.printHelpAndExit = true;
|
||||
}
|
||||
|
||||
ctx.locals.values = result.values;
|
||||
ctx.locals.positionals = result.positionals;
|
||||
}
|
||||
}
|
38
packages/phoenix/src/ansi-shell/decorators/errors.js
Normal file
38
packages/phoenix/src/ansi-shell/decorators/errors.js
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export default {
|
||||
name: 'errors',
|
||||
decorate (fn, { command, ctx }) {
|
||||
return async (...a) => {
|
||||
try {
|
||||
await fn(...a);
|
||||
} catch (e) {
|
||||
console.log('GOT IT HERE');
|
||||
// message without "Error:"
|
||||
let message = e.message;
|
||||
if (message.startsWith('Error: ')) {
|
||||
message = message.slice(7);
|
||||
}
|
||||
ctx.externs.err.write(
|
||||
'\x1B[31;1m' + command.name + ': ' + message + '\x1B[0m\n'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js
Normal file
33
packages/phoenix/src/ansi-shell/ioutil/ByteWriter.js
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ProxyWriter } from "./ProxyWriter.js";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export class ByteWriter extends ProxyWriter {
|
||||
async write (item) {
|
||||
if ( typeof item === 'string' ) {
|
||||
item = encoder.encode(item);
|
||||
}
|
||||
if ( item instanceof Blob ) {
|
||||
item = new Uint8Array(await item.arrayBuffer());
|
||||
}
|
||||
await this.delegate.write(item);
|
||||
}
|
||||
}
|
44
packages/phoenix/src/ansi-shell/ioutil/MemReader.js
Normal file
44
packages/phoenix/src/ansi-shell/ioutil/MemReader.js
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class MemReader {
|
||||
constructor (data) {
|
||||
this.data = data;
|
||||
this.pos = 0;
|
||||
}
|
||||
async read (opt_buffer) {
|
||||
if ( this.pos >= this.data.length ) {
|
||||
return { done: true };
|
||||
}
|
||||
|
||||
if ( ! opt_buffer ) {
|
||||
this.pos = this.data.length;
|
||||
return { value: this.data, done: false };
|
||||
}
|
||||
|
||||
const toReturn = this.data.slice(
|
||||
this.pos,
|
||||
Math.min(this.pos + opt_buffer.length, this.data.length),
|
||||
);
|
||||
|
||||
return {
|
||||
value: opt_buffer,
|
||||
size: toReturn.length
|
||||
};
|
||||
}
|
||||
}
|
70
packages/phoenix/src/ansi-shell/ioutil/MemWriter.js
Normal file
70
packages/phoenix/src/ansi-shell/ioutil/MemWriter.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export class MemWriter {
|
||||
constructor () {
|
||||
this.items = [];
|
||||
}
|
||||
async write (item) {
|
||||
this.items.push(item);
|
||||
}
|
||||
async close () {}
|
||||
|
||||
getAsUint8Array() {
|
||||
const uint8arrays = [];
|
||||
for ( let item of this.items ) {
|
||||
if ( typeof item === 'string' ) {
|
||||
item = encoder.encode(item);
|
||||
}
|
||||
|
||||
if ( ! ( item instanceof Uint8Array ) ) {
|
||||
throw new Error('could not convert to Uint8Array');
|
||||
}
|
||||
|
||||
uint8arrays.push(item);
|
||||
}
|
||||
|
||||
const outputUint8Array = new Uint8Array(
|
||||
uint8arrays.reduce((sum, item) => sum + item.length, 0)
|
||||
);
|
||||
|
||||
let pos = 0;
|
||||
for ( const item of uint8arrays ) {
|
||||
outputUint8Array.set(item, pos);
|
||||
pos += item.length;
|
||||
}
|
||||
|
||||
return outputUint8Array;
|
||||
}
|
||||
|
||||
getAsBlob () {
|
||||
// If there is just one item and it's a blob, return it
|
||||
if ( this.items.length === 1 && this.items[0] instanceof Blob ) {
|
||||
return this.items[0];
|
||||
}
|
||||
|
||||
const uint8array = this.getAsUint8Array();
|
||||
return new Blob([uint8array]);
|
||||
}
|
||||
|
||||
getAsString () {
|
||||
return new TextDecoder().decode(this.getAsUint8Array());
|
||||
}
|
||||
}
|
35
packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js
Normal file
35
packages/phoenix/src/ansi-shell/ioutil/MultiWriter.js
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class MultiWriter {
|
||||
constructor ({ delegates }) {
|
||||
this.delegates = delegates;
|
||||
}
|
||||
|
||||
async write (item) {
|
||||
for ( const delegate of this.delegates ) {
|
||||
await delegate.write(item);
|
||||
}
|
||||
}
|
||||
|
||||
async close () {
|
||||
for ( const delegate of this.delegates ) {
|
||||
await delegate.close();
|
||||
}
|
||||
}
|
||||
}
|
29
packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js
Normal file
29
packages/phoenix/src/ansi-shell/ioutil/NullifyWriter.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ProxyWriter } from "./ProxyWriter.js";
|
||||
|
||||
export class NullifyWriter extends ProxyWriter {
|
||||
async write (item) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
async close () {
|
||||
await this.delegate.close();
|
||||
}
|
||||
}
|
25
packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js
Normal file
25
packages/phoenix/src/ansi-shell/ioutil/ProxyReader.js
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class ProxyReader {
|
||||
constructor ({ delegate }) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
read (...a) { return this.delegate.read(...a); }
|
||||
}
|
26
packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js
Normal file
26
packages/phoenix/src/ansi-shell/ioutil/ProxyWriter.js
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class ProxyWriter {
|
||||
constructor ({ delegate }) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
write (...a) { return this.delegate.write(...a); }
|
||||
close (...a) { return this.delegate.close(...a); }
|
||||
}
|
64
packages/phoenix/src/ansi-shell/ioutil/SignalReader.js
Normal file
64
packages/phoenix/src/ansi-shell/ioutil/SignalReader.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ANSIContext } from "../ANSIContext.js";
|
||||
import { signals } from "../signals.js";
|
||||
import { ProxyReader } from "./ProxyReader.js";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export class SignalReader extends ProxyReader {
|
||||
constructor ({ sig, ...kv }, ...a) {
|
||||
super({ ...kv }, ...a);
|
||||
this.sig = sig;
|
||||
}
|
||||
|
||||
async read (opt_buffer) {
|
||||
const mapping = [
|
||||
[ANSIContext.constants.CHAR_ETX, signals.SIGINT],
|
||||
[ANSIContext.constants.CHAR_EOT, signals.SIGQUIT],
|
||||
];
|
||||
|
||||
let { value, done } = await this.delegate.read(opt_buffer);
|
||||
|
||||
if ( value === undefined ) {
|
||||
return { value, done };
|
||||
}
|
||||
|
||||
const tmp_value = value;
|
||||
|
||||
if ( ! tmp_value instanceof Uint8Array ) {
|
||||
tmp_value = encoder.encode(value);
|
||||
}
|
||||
|
||||
// show hex for debugging
|
||||
// console.log(value.split('').map(c => c.charCodeAt(0).toString(16)).join(' '));
|
||||
console.log('value??', value)
|
||||
|
||||
for ( const [key, signal] of mapping ) {
|
||||
if ( tmp_value.includes(key) ) {
|
||||
// this.sig.emit(signal);
|
||||
// if ( signal === signals.SIGQUIT ) {
|
||||
return { done: true };
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
return { value, done };
|
||||
}
|
||||
}
|
80
packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js
Normal file
80
packages/phoenix/src/ansi-shell/ioutil/SyncLinesReader.js
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ProxyReader } from "./ProxyReader.js";
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export class SyncLinesReader extends ProxyReader {
|
||||
constructor (...a) {
|
||||
super(...a);
|
||||
this.lines = [];
|
||||
this.fragment = '';
|
||||
}
|
||||
async read (opt_buffer) {
|
||||
if ( opt_buffer ) {
|
||||
// Line sync contradicts buffered reads
|
||||
return await this.delegate.read(opt_buffer);
|
||||
}
|
||||
|
||||
return await this.readNextLine_();
|
||||
}
|
||||
async readNextLine_ () {
|
||||
if ( this.lines.length > 0 ) {
|
||||
return { value: this.lines.shift() };
|
||||
}
|
||||
|
||||
for ( ;; ) {
|
||||
// CHECK: this might read once more after done; is that ok?
|
||||
let { value, done } = await this.delegate.read();
|
||||
|
||||
if ( value instanceof Uint8Array ) {
|
||||
value = decoder.decode(value);
|
||||
}
|
||||
|
||||
if ( done ) {
|
||||
if ( this.fragment.length === 0 ) {
|
||||
return { value, done };
|
||||
}
|
||||
|
||||
value = this.fragment;
|
||||
this.fragment = '';
|
||||
return { value };
|
||||
}
|
||||
|
||||
if ( ! value.match(/\n|\r|\r\n/) ) {
|
||||
this.fragment += value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Guaranteed to be 2 items, because value includes a newline
|
||||
const lines = value.split(/\n|\r|\r\n/);
|
||||
|
||||
// The first line continues from the existing fragment
|
||||
const firstLine = this.fragment + lines.shift();
|
||||
// The last line is incomplete, and goes on the fragment
|
||||
this.fragment = lines.pop();
|
||||
|
||||
// Any lines between are enqueued for subsequent reads,
|
||||
// and they include a line-feed character.
|
||||
this.lines.push(...lines.map(txt => txt + '\n'));
|
||||
|
||||
return { value: firstLine + '\n' };
|
||||
}
|
||||
}
|
||||
}
|
40
packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js
Normal file
40
packages/phoenix/src/ansi-shell/parsing/PARSE_CONSTANTS.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const PARSE_CONSTANTS = {
|
||||
list_ws: [' ', '\n', '\t'],
|
||||
list_quot: [`"`, `'`],
|
||||
};
|
||||
|
||||
PARSE_CONSTANTS.list_stoptoken = [
|
||||
'|','>','<','&','\\','#',';','(',')',
|
||||
...PARSE_CONSTANTS.list_ws,
|
||||
...PARSE_CONSTANTS.list_quot,
|
||||
]
|
||||
|
||||
PARSE_CONSTANTS.escapeSubstitutions = {
|
||||
'\\': '\\',
|
||||
'/': '/',
|
||||
b: '\b',
|
||||
f: '\f',
|
||||
n: '\n',
|
||||
r: '\r',
|
||||
t: '\t',
|
||||
'"': '"',
|
||||
"'": "'",
|
||||
};
|
54
packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js
Normal file
54
packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { StrataParser, StringPStratumImpl } from "strataparse";
|
||||
import { buildParserFirstHalf } from "./buildParserFirstHalf.js";
|
||||
import { buildParserSecondHalf } from "./buildParserSecondHalf.js";
|
||||
|
||||
export class PuterShellParser {
|
||||
constructor () {
|
||||
{
|
||||
}
|
||||
}
|
||||
parseLineForSyntax () {}
|
||||
parseLineForProcessing (input) {
|
||||
const sp = new StrataParser();
|
||||
sp.add(new StringPStratumImpl(input));
|
||||
// TODO: optimize by re-using this parser
|
||||
// buildParserFirstHalf(sp, "interpreting");
|
||||
buildParserFirstHalf(sp, "syntaxHighlighting");
|
||||
buildParserSecondHalf(sp);
|
||||
const result = sp.parse();
|
||||
if ( sp.error ) {
|
||||
throw new Error(sp.error);
|
||||
}
|
||||
console.log('PARSER RESULT', result);
|
||||
return result;
|
||||
}
|
||||
parseScript (input) {
|
||||
const sp = new StrataParser();
|
||||
sp.add(new StringPStratumImpl(input));
|
||||
buildParserFirstHalf(sp, "syntaxHighlighting");
|
||||
buildParserSecondHalf(sp, { multiline: true });
|
||||
const result = sp.parse();
|
||||
if ( sp.error ) {
|
||||
throw new Error(sp.error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const list_ws = [' ', '\n', '\t'];
|
||||
const list_quot = [`"`, `'`];
|
||||
const list_stoptoken = [
|
||||
'|','>','<','&','\\','#',';','(',')',
|
||||
...list_ws,
|
||||
...list_quot
|
||||
];
|
||||
|
||||
export class UnquotedTokenParserImpl {
|
||||
static meta = {
|
||||
inputs: 'bytes',
|
||||
outputs: 'node'
|
||||
}
|
||||
static data = {
|
||||
excludes: list_stoptoken
|
||||
}
|
||||
parse (lexer) {
|
||||
const { excludes } = this.constructor.data;
|
||||
let text = '';
|
||||
|
||||
for ( ;; ) {
|
||||
const { done, value } = lexer.look();
|
||||
if ( done ) break;
|
||||
const str = String.fromCharCode(value);
|
||||
if ( excludes.includes(str) ) break;
|
||||
text += str;
|
||||
lexer.next();
|
||||
}
|
||||
|
||||
if ( text.length === 0 ) return;
|
||||
|
||||
return { $: 'symbol', text };
|
||||
}
|
||||
}
|
25
packages/phoenix/src/ansi-shell/parsing/brainstorming.js
Normal file
25
packages/phoenix/src/ansi-shell/parsing/brainstorming.js
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
const seq = [
|
||||
{ $: 'symbol', text: 'command' },
|
||||
{ $: 'string.dquote' },
|
||||
{ $: 'string.segment', text: '-' },
|
||||
{ $: 'op.cmd-subst' },
|
||||
{ $: 'op.close' },
|
||||
];
|
222
packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js
Normal file
222
packages/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { FirstRecognizedPStratumImpl, ParserBuilder, ParserFactory, StrUntilParserImpl, StrataParseFacade, WhitespaceParserImpl } from "strataparse";
|
||||
import { UnquotedTokenParserImpl } from "./UnquotedTokenParserImpl.js";
|
||||
import { PARSE_CONSTANTS } from "./PARSE_CONSTANTS.js";
|
||||
import { MergeWhitespacePStratumImpl } from "strataparse/strata_impls/MergeWhitespacePStratumImpl.js";
|
||||
import ContextSwitchingPStratumImpl from "strataparse/strata_impls/ContextSwitchingPStratumImpl.js";
|
||||
|
||||
const parserConfigProfiles = {
|
||||
syntaxHighlighting: { cst: true },
|
||||
interpreting: { cst: false }
|
||||
};
|
||||
|
||||
const list_ws = [' ', '\n', '\t'];
|
||||
const list_quot = [`"`, `'`];
|
||||
const list_stoptoken = [
|
||||
'|','>','<','&','\\','#',';','(',')',
|
||||
...list_ws,
|
||||
...list_quot
|
||||
];
|
||||
|
||||
export const buildParserFirstHalf = (sp, profile) => {
|
||||
const options = profile ? parserConfigProfiles[profile]
|
||||
: { cst: false };
|
||||
|
||||
const parserFactory = new ParserFactory();
|
||||
if ( options.cst ) {
|
||||
parserFactory.concrete = true;
|
||||
parserFactory.rememberSource = true;
|
||||
}
|
||||
|
||||
const parserRegistry = StrataParseFacade.getDefaultParserRegistry();
|
||||
|
||||
const parserBuilder = new ParserBuilder({
|
||||
parserFactory,
|
||||
parserRegistry,
|
||||
});
|
||||
|
||||
|
||||
// TODO: unquoted tokens will actually need to be parsed in
|
||||
// segments to because `$(echo "la")h` works in sh
|
||||
const buildStringParserDef = quote => {
|
||||
return a => a.sequence(
|
||||
a.literal(quote),
|
||||
a.repeat(a.choice(
|
||||
// TODO: replace this with proper parser
|
||||
parserFactory.create(StrUntilParserImpl, {
|
||||
stopChars: ['\\', quote],
|
||||
}, { assign: { $: 'string.segment' } }),
|
||||
a.sequence(
|
||||
a.literal('\\'),
|
||||
a.choice(
|
||||
a.literal(quote),
|
||||
...Object.keys(
|
||||
PARSE_CONSTANTS.escapeSubstitutions
|
||||
).map(chr => a.literal(chr))
|
||||
// TODO: \u[4],\x[2],\0[3]
|
||||
)
|
||||
).assign({ $: 'string.escape' })
|
||||
)),
|
||||
a.literal(quote),
|
||||
).assign({ $: 'string' })
|
||||
};
|
||||
|
||||
|
||||
const buildStringContext = quote => [
|
||||
parserFactory.create(StrUntilParserImpl, {
|
||||
stopChars: ['\\', "$", quote],
|
||||
}, { assign: { $: 'string.segment' } }),
|
||||
parserBuilder.def(a => a.sequence(
|
||||
a.literal('\\'),
|
||||
a.choice(
|
||||
a.literal(quote),
|
||||
...Object.keys(
|
||||
PARSE_CONSTANTS.escapeSubstitutions
|
||||
).map(chr => a.literal(chr))
|
||||
// TODO: \u[4],\x[2],\0[3]
|
||||
)
|
||||
).assign({ $: 'string.escape' })),
|
||||
{
|
||||
parser: parserBuilder.def(a => a.literal(quote).assign({ $: 'string.close' })),
|
||||
transition: { pop: true }
|
||||
},
|
||||
{
|
||||
parser: parserBuilder.def(a => {
|
||||
return a.literal('$(').assign({ $: 'op.cmd-subst' })
|
||||
}),
|
||||
transition: {
|
||||
to: 'command',
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// sp.add(
|
||||
// new FirstRecognizedPStratumImpl({
|
||||
// parsers: [
|
||||
// parserFactory.create(WhitespaceParserImpl),
|
||||
// parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })),
|
||||
// parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })),
|
||||
// parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })),
|
||||
// parserBuilder.def(a => a.literal('$((').assign({ $: 'op.arithmetic' })),
|
||||
// parserBuilder.def(a => a.literal('$(').assign({ $: 'op.cmd-subst' })),
|
||||
// parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
|
||||
// parserFactory.create(StrUntilParserImpl, {
|
||||
// stopChars: list_stoptoken,
|
||||
// }, { assign: { $: 'symbol' } }),
|
||||
// // parserFactory.create(UnquotedTokenParserImpl),
|
||||
// parserBuilder.def(buildStringParserDef('"')),
|
||||
// parserBuilder.def(buildStringParserDef(`'`)),
|
||||
// ]
|
||||
// })
|
||||
// )
|
||||
|
||||
sp.add(
|
||||
new ContextSwitchingPStratumImpl({
|
||||
entry: 'command',
|
||||
contexts: {
|
||||
command: [
|
||||
parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })),
|
||||
parserFactory.create(WhitespaceParserImpl),
|
||||
parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })),
|
||||
parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })),
|
||||
parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })),
|
||||
{
|
||||
parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
|
||||
transition: {
|
||||
pop: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
parser: parserBuilder.def(a => a.literal('"').assign({ $: 'string.dquote' })),
|
||||
transition: {
|
||||
to: 'string.dquote',
|
||||
}
|
||||
},
|
||||
{
|
||||
parser: parserBuilder.def(a => a.literal(`'`).assign({ $: 'string.squote' })),
|
||||
transition: {
|
||||
to: 'string.squote',
|
||||
}
|
||||
},
|
||||
{
|
||||
parser: parserBuilder.def(a => a.none()),
|
||||
transition: {
|
||||
to: 'symbol',
|
||||
}
|
||||
},
|
||||
],
|
||||
'string.dquote': buildStringContext('"'),
|
||||
'string.squote': buildStringContext(`'`),
|
||||
symbol: [
|
||||
parserFactory.create(StrUntilParserImpl, {
|
||||
stopChars: [...list_stoptoken, '$'],
|
||||
}, { assign: { $: 'symbol' } }),
|
||||
{
|
||||
// TODO: redundant definition to the one in 'command'
|
||||
parser:
|
||||
parserBuilder.def(a => a.literal('\n').assign({ $: 'op.line-terminator' })),
|
||||
transition: { pop: true }
|
||||
},
|
||||
{
|
||||
parser: parserFactory.create(WhitespaceParserImpl),
|
||||
transition: { pop: true }
|
||||
},
|
||||
{
|
||||
peek: true,
|
||||
parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })),
|
||||
transition: { pop: true }
|
||||
},
|
||||
{
|
||||
parser: parserBuilder.def(a => {
|
||||
return a.literal('$(').assign({ $: 'op.cmd-subst' })
|
||||
}),
|
||||
transition: {
|
||||
to: 'command',
|
||||
}
|
||||
},
|
||||
{
|
||||
parser: parserBuilder.def(a => a.none()),
|
||||
transition: { pop: true }
|
||||
},
|
||||
{
|
||||
parser: parserBuilder.def(a => a.choice(
|
||||
...list_stoptoken.map(chr => a.literal(chr))
|
||||
)),
|
||||
transition: { pop: true }
|
||||
}
|
||||
],
|
||||
},
|
||||
wrappers: {
|
||||
'string.dquote': {
|
||||
$: 'string',
|
||||
quote: '"',
|
||||
},
|
||||
'string.squote': {
|
||||
$: 'string',
|
||||
quote: `'`,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
sp.add(
|
||||
new MergeWhitespacePStratumImpl()
|
||||
)
|
||||
};
|
441
packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js
Normal file
441
packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js
Normal file
@ -0,0 +1,441 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ParserBuilder, ParserFactory, StrataParseFacade } from "strataparse"
|
||||
|
||||
import { PARSE_CONSTANTS } from "./PARSE_CONSTANTS.js";
|
||||
const escapeSubstitutions = PARSE_CONSTANTS.escapeSubstitutions;
|
||||
|
||||
const splitTokens = (items, delimPredicate) => {
|
||||
const result = [];
|
||||
{
|
||||
let buffer = [];
|
||||
// single pass to split by pipe token
|
||||
for ( let i=0 ; i < items.length ; i++ ) {
|
||||
if ( delimPredicate(items[i]) ) {
|
||||
result.push(buffer);
|
||||
buffer = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.push(items[i]);
|
||||
}
|
||||
|
||||
if ( buffer.length !== 0 ) {
|
||||
result.push(buffer);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
class ReducePrimitivesPStratumImpl {
|
||||
next (api) {
|
||||
const lexer = api.delegate;
|
||||
|
||||
let { value, done } = lexer.next();
|
||||
|
||||
if ( value.$ === 'string' ) {
|
||||
const [lQuote, contents, rQuote] = value.results;
|
||||
let text = '';
|
||||
for ( const item of contents.results ) {
|
||||
if ( item.$ === 'string.segment' ) {
|
||||
// console.log('segment?', item.text)
|
||||
text += item.text;
|
||||
continue;
|
||||
}
|
||||
if ( item.$ === 'string.escape' ) {
|
||||
const [escChar, escValue] = item.results;
|
||||
if ( escValue.$ === 'literal' ) {
|
||||
text += escapeSubstitutions[escValue.text];
|
||||
} // else
|
||||
if ( escValue.$ === 'sequence' ) {
|
||||
// TODO: \u[4],\x[2],\0[3]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value.text = text;
|
||||
delete value.results;
|
||||
}
|
||||
|
||||
return { value, done };
|
||||
}
|
||||
}
|
||||
|
||||
class ShellConstructsPStratumImpl {
|
||||
static states = [
|
||||
{
|
||||
name: 'pipeline',
|
||||
enter ({ node }) {
|
||||
node.$ = 'pipeline';
|
||||
node.commands = [];
|
||||
},
|
||||
exit ({ node }) {
|
||||
console.log('!!!!!',this.stack_top.node)
|
||||
if ( this.stack_top?.node?.$ === 'script' ) {
|
||||
this.stack_top.node.statements.push(node);
|
||||
}
|
||||
if ( this.stack_top?.node?.$ === 'string' ) {
|
||||
this.stack_top.node.components.push(node);
|
||||
}
|
||||
},
|
||||
next ({ value, lexer }) {
|
||||
if ( value.$ === 'op.line-terminator' ) {
|
||||
console.log('the stack??', this.stack)
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'op.close' ) {
|
||||
if ( this.stack.length === 1 ) {
|
||||
throw new Error('unexpected close');
|
||||
}
|
||||
lexer.next();
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'op.pipe' ) {
|
||||
lexer.next();
|
||||
}
|
||||
this.push('command');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'command',
|
||||
enter ({ node }) {
|
||||
node.$ = 'command';
|
||||
node.tokens = [];
|
||||
node.inputRedirects = [];
|
||||
node.outputRedirects = [];
|
||||
},
|
||||
next ({ value, lexer }) {
|
||||
if ( value.$ === 'op.line-terminator' ) {
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'whitespace' ) {
|
||||
lexer.next();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'op.close' ) {
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'op.pipe' ) {
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'op.redirect' ) {
|
||||
this.push('redirect', { direction: value.direction });
|
||||
lexer.next();
|
||||
return;
|
||||
}
|
||||
this.push('token');
|
||||
},
|
||||
exit ({ node }) {
|
||||
this.stack_top.node.commands.push(node);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'redirect',
|
||||
enter ({ node }) {
|
||||
node.$ = 'redirect';
|
||||
node.tokens = [];
|
||||
},
|
||||
exit ({ node }) {
|
||||
const { direction } = node;
|
||||
const arry = direction === 'in' ?
|
||||
this.stack_top.node.inputRedirects :
|
||||
this.stack_top.node.outputRedirects;
|
||||
arry.push(node.tokens[0]);
|
||||
},
|
||||
next ({ node, value, lexer }) {
|
||||
if ( node.tokens.length === 1 ) {
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'whitespace' ) {
|
||||
lexer.next();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'op.close' ) {
|
||||
throw new Error('unexpected close');
|
||||
}
|
||||
this.push('token');
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'token',
|
||||
enter ({ node }) {
|
||||
node.$ = 'token';
|
||||
node.components = [];
|
||||
},
|
||||
exit ({ node }) {
|
||||
this.stack_top.node.tokens.push(node);
|
||||
},
|
||||
next ({ value, lexer }) {
|
||||
if ( value.$ === 'op.line-terminator' ) {
|
||||
console.log('well, got here')
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'string.dquote' ) {
|
||||
this.push('string', { quote: '"' });
|
||||
lexer.next();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'string.squote' ) {
|
||||
this.push('string', { quote: "'" });
|
||||
lexer.next();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
value.$ === 'whitespace' ||
|
||||
value.$ === 'op.close'
|
||||
) {
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
this.push('string', { quote: null });
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
enter ({ node }) {
|
||||
node.$ = 'string';
|
||||
node.components = [];
|
||||
},
|
||||
exit ({ node }) {
|
||||
this.stack_top.node.components.push(...node.components);
|
||||
},
|
||||
next ({ node, value, lexer }) {
|
||||
console.log('WHAT THO', node)
|
||||
if ( value.$ === 'op.line-terminator' && node.quote === null ) {
|
||||
console.log('well, got here')
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'string.close' && node.quote !== null ) {
|
||||
lexer.next();
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
node.quote === null && (
|
||||
value.$ === 'whitespace' ||
|
||||
value.$ === 'op.close'
|
||||
)
|
||||
) {
|
||||
this.pop();
|
||||
return;
|
||||
}
|
||||
if ( value.$ === 'op.cmd-subst' ) {
|
||||
this.push('pipeline');
|
||||
lexer.next();
|
||||
return;
|
||||
}
|
||||
node.components.push(value);
|
||||
lexer.next();
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
constructor () {
|
||||
this.states = this.constructor.states;
|
||||
this.buffer = [];
|
||||
this.stack = [];
|
||||
this.done_ = false;
|
||||
|
||||
this._init();
|
||||
}
|
||||
|
||||
_init () {
|
||||
this.push('pipeline');
|
||||
}
|
||||
|
||||
get stack_top () {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
push (state_name, node) {
|
||||
const state = this.states.find(s => s.name === state_name);
|
||||
if ( ! node ) node = {};
|
||||
this.stack.push({ state, node });
|
||||
state.enter && state.enter.call(this, { node });
|
||||
}
|
||||
|
||||
pop () {
|
||||
const { state, node } = this.stack.pop();
|
||||
state.exit && state.exit.call(this, { node });
|
||||
}
|
||||
|
||||
chstate (state) {
|
||||
this.stack_top.state = state;
|
||||
}
|
||||
|
||||
next (api) {
|
||||
if ( this.done_ ) return { done: true };
|
||||
|
||||
const lexer = api.delegate;
|
||||
|
||||
console.log('THE NODE', this.stack[0].node);
|
||||
// return { done: true, value: { $: 'test' } };
|
||||
|
||||
for ( let i=0 ; i < 500 ; i++ ) {
|
||||
const { done, value } = lexer.look();
|
||||
|
||||
if ( done ) {
|
||||
while ( this.stack.length > 1 ) {
|
||||
this.pop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const { state, node } = this.stack_top;
|
||||
console.log('value?', value, done)
|
||||
console.log('state?', state.name);
|
||||
|
||||
state.next.call(this, { lexer, value, node, state });
|
||||
|
||||
// if ( done ) break;
|
||||
}
|
||||
|
||||
console.log('THE NODE', this.stack[0]);
|
||||
|
||||
this.done_ = true;
|
||||
return { done: false, value: this.stack[0].node };
|
||||
}
|
||||
|
||||
// old method; not used anymore
|
||||
consolidateTokens (tokens) {
|
||||
const types = tokens.map(token => token.$);
|
||||
|
||||
if ( tokens.length === 0 ) {
|
||||
throw new Error('expected some tokens');
|
||||
}
|
||||
|
||||
if ( types.includes('op.pipe') ) {
|
||||
const components =
|
||||
splitTokens(tokens, t => t.$ === 'op.pipe')
|
||||
.map(tokens => this.consolidateTokens(tokens));
|
||||
|
||||
return { $: 'pipeline', components };
|
||||
}
|
||||
|
||||
// const command = tokens.shift();
|
||||
const args = [];
|
||||
const outputRedirects = [];
|
||||
const inputRedirects = [];
|
||||
|
||||
const states = {
|
||||
STATE_NORMAL: {},
|
||||
STATE_REDIRECT: {
|
||||
direction: null
|
||||
},
|
||||
};
|
||||
const stack = [];
|
||||
let dest = args;
|
||||
let state = states.STATE_NORMAL;
|
||||
for ( const token of tokens ) {
|
||||
if ( state === states.STATE_REDIRECT ) {
|
||||
const arry = state.direction === 'out' ?
|
||||
outputRedirects : inputRedirects;
|
||||
arry.push({
|
||||
// TODO: get string value only
|
||||
path: token,
|
||||
})
|
||||
state = states.STATE_NORMAL;
|
||||
continue;
|
||||
}
|
||||
if ( token.$ === 'op.redirect' ) {
|
||||
state = states.STATE_REDIRECT;
|
||||
state.direction = token.direction;
|
||||
continue;
|
||||
}
|
||||
if ( token.$ === 'op.cmd-subst' ) {
|
||||
const new_dest = [];
|
||||
dest = new_dest;
|
||||
stack.push({
|
||||
$: 'command-substitution',
|
||||
tokens: new_dest,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if ( token.$ === 'op.close' ) {
|
||||
const sub = stack.pop();
|
||||
dest = stack.length === 0 ? args : stack[stack.length-1].tokens;
|
||||
const cmd_node = this.consolidateTokens(sub.tokens);
|
||||
dest.push(cmd_node);
|
||||
continue;
|
||||
}
|
||||
dest.push(token);
|
||||
}
|
||||
|
||||
const command = args.shift();
|
||||
|
||||
return {
|
||||
$: 'command',
|
||||
command,
|
||||
args,
|
||||
inputRedirects,
|
||||
outputRedirects,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MultilinePStratumImpl extends ShellConstructsPStratumImpl {
|
||||
static states = [
|
||||
{
|
||||
name: 'script',
|
||||
enter ({ node }) {
|
||||
node.$ = 'script';
|
||||
node.statements = [];
|
||||
},
|
||||
next ({ value, lexer }) {
|
||||
if ( value.$ === 'op.line-terminator' ) {
|
||||
lexer.next();
|
||||
return;
|
||||
}
|
||||
|
||||
this.push('pipeline');
|
||||
}
|
||||
},
|
||||
...ShellConstructsPStratumImpl.states,
|
||||
];
|
||||
|
||||
_init () {
|
||||
this.push('script');
|
||||
}
|
||||
}
|
||||
|
||||
export const buildParserSecondHalf = (sp, { multiline } = {}) => {
|
||||
const parserFactory = new ParserFactory();
|
||||
const parserRegistry = StrataParseFacade.getDefaultParserRegistry();
|
||||
|
||||
const parserBuilder = new ParserBuilder(
|
||||
parserFactory,
|
||||
parserRegistry,
|
||||
);
|
||||
|
||||
// sp.add(new ReducePrimitivesPStratumImpl());
|
||||
if ( multiline ) {
|
||||
console.log('USING MULTILINE');
|
||||
sp.add(new MultilinePStratumImpl());
|
||||
} else {
|
||||
sp.add(new ShellConstructsPStratumImpl());
|
||||
}
|
||||
}
|
54
packages/phoenix/src/ansi-shell/pipeline/Coupler.js
Normal file
54
packages/phoenix/src/ansi-shell/pipeline/Coupler.js
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class Coupler {
|
||||
static description = `
|
||||
Connects a read stream to a write stream.
|
||||
Does not close the write stream when the read stream is closed.
|
||||
`
|
||||
|
||||
constructor (source, target) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.on_ = true;
|
||||
this.isDone = new Promise(rslv => {
|
||||
this.resolveIsDone = rslv;
|
||||
})
|
||||
this.listenLoop_();
|
||||
}
|
||||
|
||||
off () { this.on_ = false; }
|
||||
on () { this.on_ = true; }
|
||||
|
||||
async listenLoop_ () {
|
||||
this.active = true;
|
||||
for (;;) {
|
||||
const { value, done } = await this.source.read();
|
||||
if ( done ) {
|
||||
this.source = null;
|
||||
this.target = null;
|
||||
this.active = false;
|
||||
this.resolveIsDone();
|
||||
break;
|
||||
}
|
||||
if ( this.on_ ) {
|
||||
await this.target.write(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
packages/phoenix/src/ansi-shell/pipeline/Pipe.js
Normal file
43
packages/phoenix/src/ansi-shell/pipeline/Pipe.js
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class Pipe {
|
||||
constructor () {
|
||||
this.readableStream = new ReadableStream({
|
||||
start: controller => {
|
||||
this.readController = controller;
|
||||
},
|
||||
close: () => {
|
||||
this.writableController.close();
|
||||
}
|
||||
});
|
||||
this.writableStream = new WritableStream({
|
||||
start: controller => {
|
||||
this.writableController = controller;
|
||||
},
|
||||
write: item => {
|
||||
this.readController.enqueue(item);
|
||||
},
|
||||
close: () => {
|
||||
this.readController.close();
|
||||
}
|
||||
});
|
||||
this.in = this.writableStream.getWriter();
|
||||
this.out = this.readableStream.getReader();
|
||||
}
|
||||
}
|
407
packages/phoenix/src/ansi-shell/pipeline/Pipeline.js
Normal file
407
packages/phoenix/src/ansi-shell/pipeline/Pipeline.js
Normal file
@ -0,0 +1,407 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { SyncLinesReader } from "../ioutil/SyncLinesReader.js";
|
||||
import { TOKENS } from "../readline/readtoken.js";
|
||||
import { ByteWriter } from "../ioutil/ByteWriter.js";
|
||||
import { Coupler } from "./Coupler.js";
|
||||
import { CommandStdinDecorator } from "./iowrappers.js";
|
||||
import { Pipe } from "./Pipe.js";
|
||||
import { MemReader } from "../ioutil/MemReader.js";
|
||||
import { MemWriter } from "../ioutil/MemWriter.js";
|
||||
import { MultiWriter } from "../ioutil/MultiWriter.js";
|
||||
import { NullifyWriter } from "../ioutil/NullifyWriter.js";
|
||||
import { ConcreteSyntaxError } from "../ConcreteSyntaxError.js";
|
||||
import { SignalReader } from "../ioutil/SignalReader.js";
|
||||
import { Exit } from "../../puter-shell/coreutils/coreutil_lib/exit.js";
|
||||
import { resolveRelativePath } from '../../util/path.js';
|
||||
import { printUsage } from '../../puter-shell/coreutils/coreutil_lib/help.js';
|
||||
|
||||
class Token {
|
||||
static createFromAST (ctx, ast) {
|
||||
if ( ast.$ !== 'token' ) {
|
||||
throw new Error('expected token node');
|
||||
}
|
||||
|
||||
console.log('ast has cst?',
|
||||
ast,
|
||||
ast.components?.[0]?.$cst
|
||||
)
|
||||
|
||||
return new Token(ast);
|
||||
}
|
||||
constructor (ast) {
|
||||
this.ast = ast;
|
||||
this.$cst = ast.components?.[0]?.$cst;
|
||||
}
|
||||
maybeStaticallyResolve (ctx) {
|
||||
// If the only components are of type 'symbol' and 'string.segment'
|
||||
// then we can statically resolve the value of the token.
|
||||
|
||||
console.log('checking viability of static resolve', this.ast)
|
||||
|
||||
const isStatic = this.ast.components.every(c => {
|
||||
return c.$ === 'symbol' || c.$ === 'string.segment';
|
||||
});
|
||||
|
||||
if ( ! isStatic ) return;
|
||||
|
||||
console.log('doing static thing', this.ast)
|
||||
|
||||
// TODO: Variables can also be statically resolved, I think...
|
||||
let value = '';
|
||||
for ( const component of this.ast.components ) {
|
||||
console.log('component', component);
|
||||
value += component.text;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async resolve (ctx) {
|
||||
let value = '';
|
||||
for ( const component of this.ast.components ) {
|
||||
if ( component.$ === 'string.segment' || component.$ === 'symbol' ) {
|
||||
value += component.text;
|
||||
continue;
|
||||
}
|
||||
if ( component.$ === 'pipeline' ) {
|
||||
const pipeline = await Pipeline.createFromAST(ctx, component);
|
||||
const memWriter = new MemWriter();
|
||||
const cmdCtx = { externs: { out: memWriter } }
|
||||
const subCtx = ctx.sub(cmdCtx);
|
||||
await pipeline.execute(subCtx);
|
||||
value += memWriter.getAsString().trimEnd();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// const name_subst = await PreparedCommand.createFromAST(this.ctx, command);
|
||||
// const memWriter = new MemWriter();
|
||||
// const cmdCtx = { externs: { out: memWriter } }
|
||||
// const ctx = this.ctx.sub(cmdCtx);
|
||||
// name_subst.setContext(ctx);
|
||||
// await name_subst.execute();
|
||||
// const cmd = memWriter.getAsString().trimEnd();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export class PreparedCommand {
|
||||
static async createFromAST (ctx, ast) {
|
||||
if ( ast.$ !== 'command' ) {
|
||||
throw new Error('expected command node');
|
||||
}
|
||||
|
||||
ast = { ...ast };
|
||||
const command_token = Token.createFromAST(ctx, ast.tokens.shift());
|
||||
|
||||
|
||||
// TODO: check that node for command name is of a
|
||||
// supported type - maybe use adapt pattern
|
||||
console.log('ast?', ast);
|
||||
const cmd = command_token.maybeStaticallyResolve(ctx);
|
||||
|
||||
const { commands } = ctx.registries;
|
||||
const { commandProvider } = ctx.externs;
|
||||
|
||||
const command = cmd
|
||||
? await commandProvider.lookup(cmd, { ctx })
|
||||
: command_token;
|
||||
|
||||
if ( command === undefined ) {
|
||||
console.log('command token?', command_token);
|
||||
throw new ConcreteSyntaxError(
|
||||
`no command: ${JSON.stringify(cmd)}`,
|
||||
command_token.$cst,
|
||||
);
|
||||
throw new Error('no command: ' + JSON.stringify(cmd));
|
||||
}
|
||||
|
||||
// TODO: test this
|
||||
console.log('ast?', ast);
|
||||
const inputRedirect = ast.inputRedirects.length > 0 ? (() => {
|
||||
const token = Token.createFromAST(ctx, ast.inputRedirects[0]);
|
||||
return token.maybeStaticallyResolve(ctx) ?? token;
|
||||
})() : null;
|
||||
// TODO: test this
|
||||
const outputRedirects = ast.outputRedirects.map(rdirNode => {
|
||||
const token = Token.createFromAST(ctx, rdirNode);
|
||||
return token.maybeStaticallyResolve(ctx) ?? token;
|
||||
});
|
||||
|
||||
return new PreparedCommand({
|
||||
command,
|
||||
args: ast.tokens.map(node => Token.createFromAST(ctx, node)),
|
||||
// args: ast.args.map(node => node.text),
|
||||
inputRedirect,
|
||||
outputRedirects,
|
||||
});
|
||||
}
|
||||
|
||||
constructor ({ command, args, inputRedirect, outputRedirects }) {
|
||||
this.command = command;
|
||||
this.args = args;
|
||||
this.inputRedirect = inputRedirect;
|
||||
this.outputRedirects = outputRedirects;
|
||||
}
|
||||
|
||||
setContext (ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
async execute () {
|
||||
let { command, args } = this;
|
||||
|
||||
// If we have an AST node of type `command` it means we
|
||||
// need to run that command to get the name of the
|
||||
// command to run.
|
||||
if ( command instanceof Token ) {
|
||||
const cmd = await command.resolve(this.ctx);
|
||||
console.log('RUNNING CMD?', cmd)
|
||||
const { commandProvider } = this.ctx.externs;
|
||||
command = await commandProvider.lookup(cmd, { ctx: this.ctx });
|
||||
if ( command === undefined ) {
|
||||
throw new Error('no command: ' + JSON.stringify(cmd));
|
||||
}
|
||||
}
|
||||
|
||||
args = await Promise.all(args.map(async node => {
|
||||
if ( node instanceof Token ) {
|
||||
return await node.resolve(this.ctx);
|
||||
}
|
||||
|
||||
return node.text;
|
||||
}));
|
||||
|
||||
const { argparsers } = this.ctx.registries;
|
||||
const { decorators } = this.ctx.registries;
|
||||
|
||||
let in_ = this.ctx.externs.in_;
|
||||
if ( this.inputRedirect ) {
|
||||
const { filesystem } = this.ctx.platform;
|
||||
const dest_path = this.inputRedirect instanceof Token
|
||||
? await this.inputRedirect.resolve(this.ctx)
|
||||
: this.inputRedirect;
|
||||
const response = await filesystem.read(
|
||||
resolveRelativePath(this.ctx.vars, dest_path));
|
||||
in_ = new MemReader(response);
|
||||
}
|
||||
|
||||
// simple naive implementation for now
|
||||
const sig = {
|
||||
listeners_: [],
|
||||
emit (signal) {
|
||||
for ( const listener of this.listeners_ ) {
|
||||
listener(signal);
|
||||
}
|
||||
},
|
||||
on (listener) {
|
||||
this.listeners_.push(listener);
|
||||
}
|
||||
};
|
||||
|
||||
in_ = new SignalReader({ delegate: in_, sig });
|
||||
|
||||
if ( command.input?.syncLines ) {
|
||||
in_ = new SyncLinesReader({ delegate: in_ });
|
||||
}
|
||||
in_ = new CommandStdinDecorator(in_);
|
||||
|
||||
let out = this.ctx.externs.out;
|
||||
const outputMemWriters = [];
|
||||
if ( this.outputRedirects.length > 0 ) {
|
||||
for ( let i=0 ; i < this.outputRedirects.length ; i++ ) {
|
||||
outputMemWriters.push(new MemWriter());
|
||||
}
|
||||
out = new NullifyWriter({ delegate: out });
|
||||
out = new MultiWriter({
|
||||
delegates: [...outputMemWriters, out],
|
||||
});
|
||||
}
|
||||
|
||||
const ctx = this.ctx.sub({
|
||||
externs: {
|
||||
in_,
|
||||
out,
|
||||
sig,
|
||||
},
|
||||
cmdExecState: {
|
||||
valid: true,
|
||||
printHelpAndExit: false,
|
||||
},
|
||||
locals: {
|
||||
command,
|
||||
args,
|
||||
outputIsRedirected: this.outputRedirects.length > 0,
|
||||
}
|
||||
});
|
||||
|
||||
if ( command.args ) {
|
||||
const argProcessorId = command.args.$;
|
||||
const argProcessor = argparsers[argProcessorId];
|
||||
const spec = { ...command.args };
|
||||
delete spec.$;
|
||||
await argProcessor.process(ctx, spec);
|
||||
}
|
||||
|
||||
if ( ! ctx.cmdExecState.valid ) {
|
||||
ctx.locals.exit = -1;
|
||||
await ctx.externs.out.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ctx.cmdExecState.printHelpAndExit ) {
|
||||
ctx.locals.exit = 0;
|
||||
await printUsage(command, ctx.externs.out, ctx.vars);
|
||||
await ctx.externs.out.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let execute = command.execute.bind(command);
|
||||
if ( command.decorators ) {
|
||||
for ( const decoratorId in command.decorators ) {
|
||||
const params = command.decorators[decoratorId];
|
||||
const decorator = decorators[decoratorId];
|
||||
execute = decorator.decorate(execute, {
|
||||
command, params, ctx
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is really sketchy...
|
||||
// `await execute(ctx);` should automatically throw any promise rejections,
|
||||
// but for some reason Node crashes first, unless we set this handler,
|
||||
// EVEN IF IT DOES NOTHING. I also can't find a place to safely remove it,
|
||||
// so apologies if it makes debugging promises harder.
|
||||
if (ctx.platform.name === 'node') {
|
||||
const rejectionCatcher = (reason, promise) => {
|
||||
};
|
||||
process.on('unhandledRejection', rejectionCatcher);
|
||||
}
|
||||
|
||||
let exit_code = 0;
|
||||
try {
|
||||
await execute(ctx);
|
||||
} catch (e) {
|
||||
if ( e instanceof Exit ) {
|
||||
exit_code = e.code;
|
||||
} else if ( e.code ) {
|
||||
await ctx.externs.err.write(
|
||||
'\x1B[31;1m' +
|
||||
command.name + ': ' +
|
||||
e.message + '\x1B[0m\n'
|
||||
);
|
||||
} else {
|
||||
await ctx.externs.err.write(
|
||||
'\x1B[31;1m' +
|
||||
command.name + ': ' +
|
||||
e.toString() + '\x1B[0m\n'
|
||||
);
|
||||
ctx.locals.exit = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.externs.in?.close?.();
|
||||
// ctx.externs.out?.close?.();
|
||||
await ctx.externs.out.close();
|
||||
|
||||
// TODO: need write command from puter-shell before this can be done
|
||||
for ( let i=0 ; i < this.outputRedirects.length ; i++ ) {
|
||||
console.log('output redirect??', this.outputRedirects[i]);
|
||||
const { filesystem } = this.ctx.platform;
|
||||
const outputRedirect = this.outputRedirects[i];
|
||||
const dest_path = outputRedirect instanceof Token
|
||||
? await outputRedirect.resolve(this.ctx)
|
||||
: outputRedirect;
|
||||
const path = resolveRelativePath(ctx.vars, dest_path);
|
||||
console.log('it should work?', {
|
||||
path,
|
||||
outputMemWriters,
|
||||
})
|
||||
// TODO: error handling here
|
||||
|
||||
await filesystem.write(path, outputMemWriters[i].getAsBlob());
|
||||
}
|
||||
|
||||
console.log('OUTPUT WRITERS', outputMemWriters);
|
||||
}
|
||||
}
|
||||
|
||||
export class Pipeline {
|
||||
static async createFromAST (ctx, ast) {
|
||||
if ( ast.$ !== 'pipeline' ) {
|
||||
throw new Error('expected pipeline node');
|
||||
}
|
||||
|
||||
const preparedCommands = [];
|
||||
|
||||
for ( const cmdNode of ast.commands ) {
|
||||
const command = await PreparedCommand.createFromAST(ctx, cmdNode);
|
||||
preparedCommands.push(command);
|
||||
}
|
||||
|
||||
return new Pipeline({ preparedCommands });
|
||||
}
|
||||
constructor ({ preparedCommands }) {
|
||||
this.preparedCommands = preparedCommands;
|
||||
}
|
||||
async execute (ctx) {
|
||||
const preparedCommands = this.preparedCommands;
|
||||
|
||||
let nextIn = ctx.externs.in;
|
||||
let lastPipe = null;
|
||||
|
||||
// TOOD: this will eventually defer piping of certain
|
||||
// sub-pipelines to the Puter Shell.
|
||||
|
||||
for ( let i=0 ; i < preparedCommands.length ; i++ ) {
|
||||
const command = preparedCommands[i];
|
||||
|
||||
// if ( command.command.input?.syncLines ) {
|
||||
// nextIn = new SyncLinesReader({ delegate: nextIn });
|
||||
// }
|
||||
|
||||
const cmdCtx = { externs: { in_: nextIn } };
|
||||
|
||||
const pipe = new Pipe();
|
||||
lastPipe = pipe;
|
||||
let cmdOut = pipe.in;
|
||||
cmdOut = new ByteWriter({ delegate: cmdOut });
|
||||
cmdCtx.externs.out = cmdOut;
|
||||
cmdCtx.externs.commandProvider = ctx.externs.commandProvider;
|
||||
nextIn = pipe.out;
|
||||
|
||||
// TODO: need to consider redirect from out to err
|
||||
cmdCtx.externs.err = ctx.externs.out;
|
||||
command.setContext(ctx.sub(cmdCtx));
|
||||
}
|
||||
|
||||
|
||||
const coupler = new Coupler(lastPipe.out, ctx.externs.out);
|
||||
|
||||
const commandPromises = [];
|
||||
for ( let i = preparedCommands.length - 1 ; i >= 0 ; i-- ) {
|
||||
const command = preparedCommands[i];
|
||||
commandPromises.push(command.execute());
|
||||
}
|
||||
await Promise.all(commandPromises);
|
||||
console.log('PIPELINE DONE');
|
||||
|
||||
await coupler.isDone;
|
||||
}
|
||||
}
|
45
packages/phoenix/src/ansi-shell/pipeline/iowrappers.js
Normal file
45
packages/phoenix/src/ansi-shell/pipeline/iowrappers.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class CommandStdinDecorator {
|
||||
constructor (rs) {
|
||||
this.rs = rs;
|
||||
}
|
||||
async read (...a) {
|
||||
return await this.rs.read(...a);
|
||||
}
|
||||
|
||||
// utility methods
|
||||
async collect () {
|
||||
const items = [];
|
||||
for (;;) {
|
||||
const { value, done } = await this.rs.read();
|
||||
if ( done ) return items;
|
||||
items.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CommandStdoutDecorator {
|
||||
constructor (delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
async write (...a) {
|
||||
return await this.delegate.write(...a);
|
||||
}
|
||||
}
|
80
packages/phoenix/src/ansi-shell/readline/history.js
Normal file
80
packages/phoenix/src/ansi-shell/readline/history.js
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class HistoryManager {
|
||||
constructor({ enableLogging = false } = {}) {
|
||||
this.items = [];
|
||||
this.index_ = 0;
|
||||
this.listeners_ = {};
|
||||
this.enableLogging_ = enableLogging;
|
||||
}
|
||||
|
||||
log(...a) {
|
||||
// TODO: Command line option for configuring logging
|
||||
if ( this.enableLogging_ ) {
|
||||
console.log('[HistoryManager]', ...a);
|
||||
}
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this.index_;
|
||||
}
|
||||
|
||||
set index(v) {
|
||||
this.log('setting index', v);
|
||||
this.index_ = v;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.items[this.index];
|
||||
}
|
||||
|
||||
// Save, overwriting the current history item
|
||||
save(data, { opt_debug } = {}) {
|
||||
this.log('saving', data, 'at', this.index,
|
||||
...(opt_debug ? [ 'from', opt_debug ] : []));
|
||||
this.items[this.index] = data;
|
||||
|
||||
if (this.listeners_.hasOwnProperty('add')) {
|
||||
for (const listener of this.listeners_.add) {
|
||||
listener(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
append(data) {
|
||||
if (
|
||||
this.items.length !== 0 &&
|
||||
this.index !== this.items.length
|
||||
) {
|
||||
this.log('POP');
|
||||
// remove last item
|
||||
this.items.pop();
|
||||
}
|
||||
this.index = this.items.length;
|
||||
this.save(data, { opt_debug: 'append' });
|
||||
this.index++;
|
||||
}
|
||||
|
||||
on(topic, listener) {
|
||||
if (!this.listeners_.hasOwnProperty(topic)) {
|
||||
this.listeners_[topic] = [];
|
||||
}
|
||||
this.listeners_[topic].push(listener);
|
||||
}
|
||||
}
|
362
packages/phoenix/src/ansi-shell/readline/readline.js
Normal file
362
packages/phoenix/src/ansi-shell/readline/readline.js
Normal file
@ -0,0 +1,362 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Context } from '../../context/context.js';
|
||||
import { CommandCompleter } from '../../puter-shell/completers/command_completer.js';
|
||||
import { FileCompleter } from '../../puter-shell/completers/file_completer.js';
|
||||
import { OptionCompleter } from '../../puter-shell/completers/option_completer.js';
|
||||
import { Uint8List } from '../../util/bytes.js';
|
||||
import { StatefulProcessorBuilder } from '../../util/statemachine.js';
|
||||
import { ANSIContext } from '../ANSIContext.js';
|
||||
import { readline_comprehend } from './rl_comprehend.js';
|
||||
import { CSI_HANDLERS } from './rl_csi_handlers.js';
|
||||
import { HistoryManager } from './history.js';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const cc = chr => chr.charCodeAt(0);
|
||||
|
||||
const ReadlineProcessorBuilder = builder => builder
|
||||
// TODO: import these constants from a package
|
||||
.installContext(ANSIContext)
|
||||
.installContext(new Context({
|
||||
variables: {
|
||||
result: { value: '' },
|
||||
cursor: { value: 0 },
|
||||
},
|
||||
// TODO: dormant configuration; waiting on ContextSignature
|
||||
imports: {
|
||||
out: {},
|
||||
in_: {},
|
||||
history: {}
|
||||
}
|
||||
}))
|
||||
.variable('result', { getDefaultValue: () => '' })
|
||||
.variable('cursor', { getDefaultValue: () => 0 })
|
||||
.external('out', { required: true })
|
||||
.external('in_', { required: true })
|
||||
.external('history', { required: true })
|
||||
.external('prompt', { required: true })
|
||||
.external('commandCtx', { required: true })
|
||||
.beforeAll('get-byte', async ctx => {
|
||||
const { locals, externs } = ctx;
|
||||
|
||||
const byteBuffer = new Uint8Array(1);
|
||||
await externs.in_.read(byteBuffer);
|
||||
locals.byteBuffer = byteBuffer;
|
||||
locals.byte = byteBuffer[0];
|
||||
})
|
||||
.state('start', async ctx => {
|
||||
const { consts, vars, externs, locals } = ctx;
|
||||
|
||||
if ( locals.byte === consts.CHAR_LF || locals.byte === consts.CHAR_CR ) {
|
||||
externs.out.write('\n');
|
||||
ctx.setState('end');
|
||||
return;
|
||||
}
|
||||
|
||||
if ( locals.byte === consts.CHAR_ETX ) {
|
||||
externs.out.write('^C\n');
|
||||
// Exit if input line is empty
|
||||
// FIXME: Check for 'process' is so we only do this on Node. How should we handle exiting in Puter terminal?
|
||||
if ( typeof process !== 'undefined' && ctx.vars.result.length === 0 ) {
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
// Otherwise clear it
|
||||
ctx.vars.result = '';
|
||||
ctx.setState('end');
|
||||
return;
|
||||
}
|
||||
|
||||
if ( locals.byte === consts.CHAR_EOT ) {
|
||||
externs.out.write('^D\n');
|
||||
ctx.vars.result = '';
|
||||
ctx.setState('end');
|
||||
return;
|
||||
}
|
||||
|
||||
if ( locals.byte === consts.CHAR_FF ) {
|
||||
externs.out.write('\x1B[H\x1B[2J');
|
||||
externs.out.write(externs.prompt);
|
||||
externs.out.write(vars.result);
|
||||
const invCurPos = vars.result.length - vars.cursor;
|
||||
console.log(invCurPos)
|
||||
if ( invCurPos !== 0 ) {
|
||||
externs.out.write(`\x1B[${invCurPos}D`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ( locals.byte === consts.CHAR_TAB ) {
|
||||
const inputState = readline_comprehend(ctx.sub({
|
||||
params: {
|
||||
input: vars.result,
|
||||
cursor: vars.cursor
|
||||
}
|
||||
}));
|
||||
// NEXT: get tab completer for input state
|
||||
console.log('input state', inputState);
|
||||
|
||||
let completer = null;
|
||||
if ( inputState.$ === 'redirect' ) {
|
||||
completer = new FileCompleter();
|
||||
}
|
||||
|
||||
if ( inputState.$ === 'command' ) {
|
||||
if ( inputState.tokens.length === 1 ) {
|
||||
// Match first token against command names
|
||||
completer = new CommandCompleter();
|
||||
} else if ( inputState.input.startsWith('--') ) {
|
||||
// Match `--*` against option names, if they exist
|
||||
completer = new OptionCompleter();
|
||||
} else {
|
||||
// Match everything else against file names
|
||||
completer = new FileCompleter();
|
||||
}
|
||||
}
|
||||
|
||||
if ( completer === null ) return;
|
||||
|
||||
const completions = await completer.getCompletions(
|
||||
externs.commandCtx,
|
||||
inputState,
|
||||
);
|
||||
|
||||
const applyCompletion = txt => {
|
||||
const p1 = vars.result.slice(0, vars.cursor);
|
||||
const p2 = vars.result.slice(vars.cursor);
|
||||
console.log({ p1, p2 });
|
||||
vars.result = p1 + txt + p2;
|
||||
vars.cursor += txt.length;
|
||||
externs.out.write(txt);
|
||||
};
|
||||
|
||||
if ( completions.length === 0 ) return;
|
||||
|
||||
if ( completions.length === 1 ) {
|
||||
applyCompletion(completions[0]);
|
||||
}
|
||||
|
||||
if ( completions.length > 1 ) {
|
||||
let inCommon = '';
|
||||
for ( let i=0 ; true ; i++ ) {
|
||||
if ( ! completions.every(completion => {
|
||||
return completion.length > i;
|
||||
}) ) break;
|
||||
|
||||
let matches = true;
|
||||
|
||||
const chrFirst = completions[0][i];
|
||||
for ( let ci=1 ; ci < completions.length ; ci++ ) {
|
||||
const chrOther = completions[ci][i];
|
||||
if ( chrFirst !== chrOther ) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! matches ) break;
|
||||
inCommon += chrFirst;
|
||||
}
|
||||
|
||||
if ( inCommon.length > 0 ) {
|
||||
applyCompletion(inCommon);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ( locals.byte === consts.CHAR_ESC ) {
|
||||
ctx.setState('ESC');
|
||||
return;
|
||||
}
|
||||
|
||||
// (note): DEL is actually the backspace key
|
||||
// [explained here](https://en.wikipedia.org/wiki/Backspace#Common_use)
|
||||
// TOOD: very similar to delete in CSI_HANDLERS; how can this be unified?
|
||||
if ( locals.byte === consts.CHAR_DEL ) {
|
||||
// can't backspace at beginning of line
|
||||
if ( vars.cursor === 0 ) return;
|
||||
|
||||
vars.result = vars.result.slice(0, vars.cursor - 1) +
|
||||
vars.result.slice(vars.cursor)
|
||||
|
||||
vars.cursor--;
|
||||
|
||||
// TODO: maybe wrap these CSI codes in a library
|
||||
const backspaceSequence = new Uint8Array([
|
||||
// consts.CHAR_ESC, consts.CHAR_CSI, cc('s'), // save cur
|
||||
consts.CHAR_ESC, consts.CHAR_CSI, cc('D'), // left
|
||||
consts.CHAR_ESC, consts.CHAR_CSI, cc('P'),
|
||||
// consts.CHAR_ESC, consts.CHAR_CSI, cc('u'), // restore cur
|
||||
// consts.CHAR_ESC, consts.CHAR_CSI, cc('D'), // left
|
||||
]);
|
||||
|
||||
externs.out.write(backspaceSequence);
|
||||
return;
|
||||
}
|
||||
|
||||
const part = decoder.decode(locals.byteBuffer);
|
||||
|
||||
if ( vars.cursor === vars.result.length ) {
|
||||
// output
|
||||
externs.out.write(locals.byteBuffer);
|
||||
// update buffer
|
||||
vars.result = vars.result + part;
|
||||
// update cursor
|
||||
vars.cursor += part.length;
|
||||
} else {
|
||||
// output
|
||||
const insertSequence = new Uint8Array([
|
||||
consts.CHAR_ESC,
|
||||
consts.CHAR_CSI,
|
||||
'@'.charCodeAt(0),
|
||||
...locals.byteBuffer
|
||||
]);
|
||||
externs.out.write(insertSequence);
|
||||
// update buffer
|
||||
vars.result =
|
||||
vars.result.slice(0, vars.cursor) +
|
||||
part +
|
||||
vars.result.slice(vars.cursor)
|
||||
// update cursor
|
||||
vars.cursor += part.length;
|
||||
}
|
||||
})
|
||||
.onTransitionTo('ESC-CSI', async ctx => {
|
||||
ctx.vars.controlSequence = new Uint8List();
|
||||
})
|
||||
.state('ESC', async ctx => {
|
||||
const { consts, vars, externs, locals } = ctx;
|
||||
|
||||
if ( locals.byte === consts.CHAR_ESC ) {
|
||||
externs.out.write(consts.CHAR_ESC);
|
||||
ctx.setState('start');
|
||||
return;
|
||||
}
|
||||
|
||||
if ( locals.byte === ctx.consts.CHAR_CSI ) {
|
||||
ctx.setState('ESC-CSI');
|
||||
return;
|
||||
}
|
||||
if ( locals.byte === ctx.consts.CHAR_OSC ) {
|
||||
ctx.setState('ESC-OSC');
|
||||
return;
|
||||
}
|
||||
})
|
||||
.state('ESC-CSI', async ctx => {
|
||||
const { consts, locals, vars } = ctx;
|
||||
|
||||
if (
|
||||
locals.byte >= consts.CSI_F_0 &&
|
||||
locals.byte < consts.CSI_F_E
|
||||
) {
|
||||
ctx.trigger('ESC-CSI.post');
|
||||
ctx.setState('start');
|
||||
return;
|
||||
}
|
||||
|
||||
vars.controlSequence.append(locals.byte);
|
||||
})
|
||||
.state('ESC-OSC', async ctx => {
|
||||
const { consts, locals, vars } = ctx;
|
||||
|
||||
// TODO: ESC\ can also end an OSC sequence according
|
||||
// to sources, but this has not been implemented
|
||||
// because it would add another state.
|
||||
// This should be implemented when there's a
|
||||
// simpler solution ("peek" & "scan" functionality)
|
||||
if (
|
||||
locals.byte === 0x07
|
||||
) {
|
||||
// ctx.trigger('ESC-OSC.post');
|
||||
ctx.setState('start');
|
||||
return;
|
||||
}
|
||||
|
||||
vars.controlSequence.append(locals.byte);
|
||||
})
|
||||
.action('ESC-CSI.post', async ctx => {
|
||||
const { vars, externs, locals } = ctx;
|
||||
|
||||
const finalByte = locals.byte;
|
||||
const controlSequence = vars.controlSequence.toArray();
|
||||
|
||||
// Log.log('controlSequence', finalByte, controlSequence);
|
||||
|
||||
if ( ! CSI_HANDLERS.hasOwnProperty(finalByte) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.locals.controlSequence = controlSequence;
|
||||
ctx.locals.doWrite = false;
|
||||
CSI_HANDLERS[finalByte](ctx);
|
||||
|
||||
if ( ctx.locals.doWrite ) {
|
||||
externs.out.write(new Uint8Array([
|
||||
ctx.consts.CHAR_ESC,
|
||||
ctx.consts.CHAR_CSI,
|
||||
...controlSequence,
|
||||
finalByte
|
||||
]))
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
const ReadlineProcessor = ReadlineProcessorBuilder(
|
||||
new StatefulProcessorBuilder()
|
||||
);
|
||||
|
||||
class Readline {
|
||||
constructor (params) {
|
||||
this.internal_ = {};
|
||||
for ( const k in params ) this.internal_[k] = params[k];
|
||||
|
||||
this.history = new HistoryManager();
|
||||
}
|
||||
|
||||
async readline (prompt, commandCtx) {
|
||||
const out = this.internal_.out;
|
||||
const in_ = this.internal_.in;
|
||||
|
||||
await out.write(prompt);
|
||||
|
||||
const {
|
||||
result
|
||||
} = await ReadlineProcessor.run({
|
||||
prompt,
|
||||
out, in_,
|
||||
history: this.history,
|
||||
commandCtx,
|
||||
});
|
||||
|
||||
if ( result.trim() !== '' ) {
|
||||
this.history.append(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ReadlineLib {
|
||||
static create(params) {
|
||||
const rl = new Readline(params);
|
||||
return rl;
|
||||
}
|
||||
}
|
107
packages/phoenix/src/ansi-shell/readline/readtoken.js
Normal file
107
packages/phoenix/src/ansi-shell/readline/readtoken.js
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// [reference impl](https://github.com/brgl/busybox/blob/master/shell/ash.c)
|
||||
|
||||
const list_ws = [' ', '\n', '\t'];
|
||||
const list_recorded_tokens = [
|
||||
'|','>','<','&',';','(',')',
|
||||
];
|
||||
const list_stoptoken = [
|
||||
'|','>','<','&','\\','#',';','(',')',
|
||||
...list_ws
|
||||
];
|
||||
|
||||
export const TOKENS = {};
|
||||
for ( const k of list_recorded_tokens ) {
|
||||
TOKENS[k] = {};
|
||||
}
|
||||
|
||||
export const readtoken = str => {
|
||||
let state = null;
|
||||
let buffer = '';
|
||||
let quoteType = '';
|
||||
const tokens = [];
|
||||
|
||||
const actions = {
|
||||
endToken: () => {
|
||||
tokens.push(buffer);
|
||||
buffer = '';
|
||||
}
|
||||
};
|
||||
|
||||
const states = {
|
||||
start: i => {
|
||||
if ( list_ws.includes(str[i]) ) {
|
||||
return;
|
||||
}
|
||||
if ( str[i] === '#' ) return str.length;
|
||||
if ( list_recorded_tokens.includes(str[i]) ) {
|
||||
tokens.push(TOKENS[str[i]]);
|
||||
return;
|
||||
}
|
||||
if ( str[i] === '"' || str[i] === "'" ) {
|
||||
state = states.quote;
|
||||
quoteType = str[i];
|
||||
return;
|
||||
}
|
||||
state = states.text;
|
||||
return i; // prevent increment
|
||||
},
|
||||
text: i => {
|
||||
if ( str[i] === '"' || str[i] === "'" ) {
|
||||
state = states.quote;
|
||||
quoteType = str[i];
|
||||
return;
|
||||
}
|
||||
if ( list_stoptoken.includes(str[i]) ) {
|
||||
state = states.start;
|
||||
actions.endToken();
|
||||
return i; // prevent increment
|
||||
}
|
||||
buffer += str[i];
|
||||
},
|
||||
quote: i => {
|
||||
if ( str[i] === '\\' ) {
|
||||
state = states.quote_esc;
|
||||
return;
|
||||
}
|
||||
if ( str[i] === quoteType ) {
|
||||
state = states.text;
|
||||
return;
|
||||
}
|
||||
buffer += str[i];
|
||||
},
|
||||
quote_esc: i => {
|
||||
if ( str[i] !== quoteType ) {
|
||||
buffer += '\\';
|
||||
}
|
||||
buffer += str[i];
|
||||
state = states.quote;
|
||||
}
|
||||
};
|
||||
state = states.start;
|
||||
for ( let i=0 ; i < str.length ; ) {
|
||||
let newI = state(i);
|
||||
i = newI !== undefined ? newI : i+1;
|
||||
}
|
||||
|
||||
if ( buffer !== '' ) actions.endToken();
|
||||
|
||||
return tokens;
|
||||
};
|
135
packages/phoenix/src/ansi-shell/readline/rl_comprehend.js
Normal file
135
packages/phoenix/src/ansi-shell/readline/rl_comprehend.js
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// This function comprehends the readline input and returns something
|
||||
// called a "readline input state" - this includes any information needed
|
||||
|
||||
import { readtoken, TOKENS } from "./readtoken.js";
|
||||
|
||||
// TODO: update to use syntax parser
|
||||
|
||||
// REMINDER: input state will be sent to readline first,
|
||||
// then readline will use the input state to determine
|
||||
// what component to ask for tab completion
|
||||
|
||||
// to perform autocomplete functions
|
||||
export const readline_comprehend = (ctx) => {
|
||||
const { input, cursor } = ctx.params;
|
||||
|
||||
// TODO: CST for input tokens might be a good idea
|
||||
// for now, tokens up to the current cursor position
|
||||
// will be considered.
|
||||
|
||||
const relevantInput = input.slice(0, cursor);
|
||||
|
||||
const endsWithWhitespace = (() => {
|
||||
const lastChar = relevantInput[relevantInput.length - 1];
|
||||
return lastChar === ' ' ||
|
||||
lastChar === '\t' ||
|
||||
lastChar === '\r' ||
|
||||
lastChar === '\n'
|
||||
})();
|
||||
|
||||
let tokens = readtoken(relevantInput);
|
||||
let tokensStart = 0;
|
||||
|
||||
// We now go backwards through the tokens, looking for:
|
||||
// - a redirect token immediately to the left
|
||||
// - a pipe token to the left
|
||||
|
||||
if ( tokens.length === 0 ) return { $: 'empty' };
|
||||
|
||||
// Remove tokens for previous commands
|
||||
for ( let i=tokens.length ; i >= 0 ; i-- ) {
|
||||
const token = tokens[i];
|
||||
const isCommandSeparator =
|
||||
token === TOKENS['|'] ||
|
||||
token === TOKENS[';'] ;
|
||||
if ( isCommandSeparator ) {
|
||||
tokens = tokens.slice(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if current input is for a redirect operator
|
||||
const resultIfRedirectOperator = (() => {
|
||||
if ( tokens.length < 1 ) return;
|
||||
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if (
|
||||
lastToken === TOKENS['<'] ||
|
||||
lastToken === TOKENS['>']
|
||||
) {
|
||||
return {
|
||||
$: 'redirect'
|
||||
};
|
||||
}
|
||||
|
||||
if ( tokens.length < 2 ) return;
|
||||
if ( endsWithWhitespace ) return;
|
||||
|
||||
const secondFromLastToken = tokens[tokens.length - 2];
|
||||
if (
|
||||
secondFromLastToken === TOKENS['<'] ||
|
||||
secondFromLastToken === TOKENS['>']
|
||||
) {
|
||||
return {
|
||||
$: 'redirect',
|
||||
input: lastToken
|
||||
};
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
if ( resultIfRedirectOperator ) return resultIfRedirectOperator;
|
||||
|
||||
if ( tokens.length === 0 ) {
|
||||
return { $: 'empty' };
|
||||
}
|
||||
|
||||
// If the first token is not a command name, then
|
||||
// this input is not considered comprehensible
|
||||
if ( typeof tokens[0] !== 'string' ) {
|
||||
return {
|
||||
$: 'unrecognized'
|
||||
};
|
||||
}
|
||||
|
||||
// DRY: command arguments are parsed by readline
|
||||
const argTokens = [];
|
||||
for ( let i=0 ; i < tokens.length ; i++ ) {
|
||||
if (
|
||||
tokens[i] === TOKENS['<'] ||
|
||||
tokens[i] === TOKENS['>']
|
||||
) {
|
||||
// skip this token and the next one
|
||||
i++; continue;
|
||||
}
|
||||
|
||||
argTokens.push(tokens[i]);
|
||||
}
|
||||
|
||||
return {
|
||||
$: 'command',
|
||||
id: tokens[0],
|
||||
tokens: argTokens,
|
||||
input: endsWithWhitespace ?
|
||||
'' : argTokens[argTokens.length - 1],
|
||||
endsWithWhitespace,
|
||||
};
|
||||
};
|
212
packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js
Normal file
212
packages/phoenix/src/ansi-shell/readline/rl_csi_handlers.js
Normal file
@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
## this source file
|
||||
- maps: CSI (Control Sequence Introducer) sequences
|
||||
- to: expected functionality in the context of readline
|
||||
|
||||
## relevant articles
|
||||
- [ECMA-48](https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf)
|
||||
- [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
*/
|
||||
|
||||
import { ANSIContext, getActiveModifiersFromXTerm } from "../ANSIContext.js";
|
||||
import { findNextWord } from "./rl_words.js";
|
||||
|
||||
// TODO: potentially include metadata in handlers
|
||||
|
||||
// --- util ---
|
||||
const cc = chr => chr.charCodeAt(0);
|
||||
|
||||
const CHAR_DEL = 127;
|
||||
const CHAR_ESC = 0x1B;
|
||||
|
||||
const { consts } = ANSIContext;
|
||||
|
||||
// --- convenience function decorators ---
|
||||
const CSI_INT_ARG = delegate => ctx => {
|
||||
const controlSequence = ctx.locals.controlSequence;
|
||||
|
||||
let str = new TextDecoder().decode(controlSequence);
|
||||
|
||||
// Detection of modifier keys like ctrl and shift
|
||||
if ( str.includes(';') ) {
|
||||
const parts = str.split(';');
|
||||
str = parts[0];
|
||||
const modsStr = parts[parts.length - 1];
|
||||
let modN = Number.parseInt(modsStr);
|
||||
const mods = getActiveModifiersFromXTerm(modN);
|
||||
for ( const k in mods ) ctx.locals[k] = mods[k];
|
||||
}
|
||||
|
||||
let num = str === '' ? 1 : Number.parseInt(str);
|
||||
if ( Number.isNaN(num) ) num = 0;
|
||||
|
||||
ctx.locals.num = num;
|
||||
|
||||
return delegate(ctx);
|
||||
};
|
||||
|
||||
// --- PC-Style Function Key handles (see `~` final byte in CSI_HANDLERS) ---
|
||||
export const PC_FN_HANDLERS = {
|
||||
// delete key
|
||||
3: ctx => {
|
||||
const { vars } = ctx;
|
||||
const deleteSequence = new Uint8Array([
|
||||
consts.CHAR_ESC, consts.CHAR_CSI, cc('P')
|
||||
]);
|
||||
vars.result = vars.result.slice(0, vars.cursor) +
|
||||
vars.result.slice(vars.cursor + 1);
|
||||
ctx.externs.out.write(deleteSequence);
|
||||
}
|
||||
};
|
||||
|
||||
const save_history = ctx => {
|
||||
const { history } = ctx.externs;
|
||||
history.save(ctx.vars.result);
|
||||
};
|
||||
|
||||
const correct_cursor = (ctx, oldCursor) => {
|
||||
// TODO: make this work differently if oldCursor is not defined
|
||||
|
||||
const amount = ctx.vars.cursor - oldCursor;
|
||||
ctx.vars.cursor = ctx.vars.result.length;
|
||||
const L = amount < 0 ? 'D' : 'C';
|
||||
if ( amount === 0 ) return;
|
||||
const moveSequence = new Uint8Array([
|
||||
consts.CHAR_ESC, consts.CHAR_CSI,
|
||||
...(new TextEncoder().encode('' + Math.abs(amount))),
|
||||
cc(L)
|
||||
]);
|
||||
ctx.externs.out.write(moveSequence);
|
||||
};
|
||||
|
||||
const home = ctx => {
|
||||
const amount = ctx.vars.cursor;
|
||||
ctx.vars.cursor = 0;
|
||||
const moveSequence = new Uint8Array([
|
||||
consts.CHAR_ESC, consts.CHAR_CSI,
|
||||
...(new TextEncoder().encode('' + amount)),
|
||||
cc('D')
|
||||
]);
|
||||
if ( amount !== 0 ) ctx.externs.out.write(moveSequence);
|
||||
};
|
||||
|
||||
const select_current_history = ctx => {
|
||||
const { history } = ctx.externs;
|
||||
home(ctx);
|
||||
ctx.vars.result = history.get();
|
||||
ctx.vars.cursor = ctx.vars.result.length;
|
||||
const clearToEndSequence = new Uint8Array([
|
||||
consts.CHAR_ESC, consts.CHAR_CSI,
|
||||
...(new TextEncoder().encode('0')),
|
||||
cc('K')
|
||||
]);
|
||||
ctx.externs.out.write(clearToEndSequence);
|
||||
ctx.externs.out.write(history.get());
|
||||
};
|
||||
|
||||
// --- CSI handlers: this is the last definition in this file ---
|
||||
export const CSI_HANDLERS = {
|
||||
[cc('A')]: CSI_INT_ARG(ctx => {
|
||||
save_history(ctx);
|
||||
const { history } = ctx.externs;
|
||||
|
||||
if ( history.index === 0 ) return;
|
||||
|
||||
history.index--;
|
||||
select_current_history(ctx);
|
||||
}),
|
||||
[cc('B')]: CSI_INT_ARG(ctx => {
|
||||
save_history(ctx);
|
||||
const { history } = ctx.externs;
|
||||
|
||||
if ( history.index === history.items.length - 1 ) return;
|
||||
|
||||
history.index++;
|
||||
select_current_history(ctx);
|
||||
}),
|
||||
// cursor back
|
||||
[cc('D')]: CSI_INT_ARG(ctx => {
|
||||
if ( ctx.vars.cursor === 0 ) {
|
||||
return;
|
||||
}
|
||||
if ( ctx.locals.ctrl ) {
|
||||
// TODO: temporary inaccurate implementation
|
||||
const txt = ctx.vars.result;
|
||||
const ind = findNextWord(txt, ctx.vars.cursor, true);
|
||||
const diff = ctx.vars.cursor - ind;
|
||||
ctx.vars.cursor = ind;
|
||||
const moveSequence = new Uint8Array([
|
||||
consts.CHAR_ESC, consts.CHAR_CSI,
|
||||
...(new TextEncoder().encode('' + diff)),
|
||||
cc('D')
|
||||
]);
|
||||
ctx.externs.out.write(moveSequence);
|
||||
return;
|
||||
}
|
||||
ctx.vars.cursor -= ctx.locals.num;
|
||||
ctx.locals.doWrite = true;
|
||||
}),
|
||||
// cursor forward
|
||||
[cc('C')]: CSI_INT_ARG(ctx => {
|
||||
if ( ctx.vars.cursor >= ctx.vars.result.length ) {
|
||||
return;
|
||||
}
|
||||
if ( ctx.locals.ctrl ) {
|
||||
// TODO: temporary inaccurate implementation
|
||||
const txt = ctx.vars.result;
|
||||
const ind = findNextWord(txt, ctx.vars.cursor);
|
||||
const diff = ind - ctx.vars.cursor;
|
||||
ctx.vars.cursor = ind;
|
||||
const moveSequence = new Uint8Array([
|
||||
consts.CHAR_ESC, consts.CHAR_CSI,
|
||||
...(new TextEncoder().encode('' + diff)),
|
||||
cc('C')
|
||||
]);
|
||||
ctx.externs.out.write(moveSequence);
|
||||
return;
|
||||
}
|
||||
ctx.vars.cursor += ctx.locals.num;
|
||||
ctx.locals.doWrite = true;
|
||||
}),
|
||||
// PC-Style Function Keys
|
||||
[cc('~')]: CSI_INT_ARG(ctx => {
|
||||
if ( ! PC_FN_HANDLERS.hasOwnProperty(ctx.locals.num) ) {
|
||||
console.error(`unrecognized PC Function: ${ctx.locals.num}`);
|
||||
return;
|
||||
}
|
||||
PC_FN_HANDLERS[ctx.locals.num](ctx);
|
||||
}),
|
||||
// Home
|
||||
[cc('H')]: ctx => {
|
||||
home(ctx);
|
||||
},
|
||||
// End
|
||||
[cc('F')]: ctx => {
|
||||
const amount = ctx.vars.result.length - ctx.vars.cursor;
|
||||
ctx.vars.cursor = ctx.vars.result.length;
|
||||
const moveSequence = new Uint8Array([
|
||||
consts.CHAR_ESC, consts.CHAR_CSI,
|
||||
...(new TextEncoder().encode('' + amount)),
|
||||
cc('C')
|
||||
]);
|
||||
if ( amount !== 0 ) ctx.externs.out.write(moveSequence);
|
||||
},
|
||||
};
|
34
packages/phoenix/src/ansi-shell/readline/rl_words.js
Normal file
34
packages/phoenix/src/ansi-shell/readline/rl_words.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const findNextWord = (str, from, reverse) => {
|
||||
let stage = 0;
|
||||
let incr = reverse ? -1 : 1;
|
||||
const cond = reverse ? i => i > 0 : i => i < str.length;
|
||||
if ( reverse && from !== 0 ) from--;
|
||||
for ( let i=from ; cond(i) ; i += incr ) {
|
||||
if ( stage === 0 ) {
|
||||
if ( str[i] !== ' ' ) stage++;
|
||||
continue;
|
||||
}
|
||||
if ( stage === 1 ) {
|
||||
if ( str[i] === ' ' ) return reverse ? i + 1 : i;
|
||||
}
|
||||
}
|
||||
return reverse ? 0 : str.length;
|
||||
}
|
22
packages/phoenix/src/ansi-shell/signals.js
Normal file
22
packages/phoenix/src/ansi-shell/signals.js
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const signals = Object.freeze({
|
||||
SIGINT: 2,
|
||||
SIGQUIT: 3,
|
||||
});
|
68
packages/phoenix/src/context/context.js
Normal file
68
packages/phoenix/src/context/context.js
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class AbstractContext {
|
||||
get constants () {
|
||||
return this.instance_.constants;
|
||||
}
|
||||
get consts () {
|
||||
return this.constants;
|
||||
}
|
||||
get variables () {
|
||||
return this.instance_.valuesAccessor;
|
||||
}
|
||||
get vars () {
|
||||
return this.variables;
|
||||
}
|
||||
}
|
||||
|
||||
// export class SubContext extends AbstractContext {
|
||||
// constructor ({ parent, changes }) {
|
||||
// for ( const k in parent.spec )
|
||||
// }
|
||||
// }
|
||||
|
||||
export class Context extends AbstractContext {
|
||||
constructor (spec) {
|
||||
super();
|
||||
this.spec = { ...spec };
|
||||
|
||||
this.instance_ = {};
|
||||
|
||||
if ( ! spec.constants ) spec.constants = {};
|
||||
|
||||
const constants = {};
|
||||
for ( const k in this.spec.constants ) {
|
||||
Object.defineProperty(constants, k, {
|
||||
value: this.spec.constants[k],
|
||||
enumerable: true
|
||||
})
|
||||
}
|
||||
this.instance_.constants = constants;
|
||||
|
||||
// const values = {};
|
||||
// for ( const k in this.spec.variables ) {
|
||||
// Object.defineProperty(values, k, {
|
||||
// value: this.spec.variables[k],
|
||||
// enumerable: true,
|
||||
// writable: true
|
||||
// });
|
||||
// }
|
||||
// this.instance_.values = values;
|
||||
}
|
||||
}
|
70
packages/phoenix/src/main_cli.js
Normal file
70
packages/phoenix/src/main_cli.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Context } from 'contextlink';
|
||||
import { launchPuterShell } from './puter-shell/main.js';
|
||||
import { NodeStdioPTT } from './pty/NodeStdioPTT.js';
|
||||
import { CreateFilesystemProvider } from './platform/node/filesystem.js';
|
||||
import { CreateEnvProvider } from './platform/node/env.js';
|
||||
import { parseArgs } from '@pkgjs/parseargs';
|
||||
import capcon from 'capture-console';
|
||||
import fs from 'fs';
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
'log': {
|
||||
type: 'string',
|
||||
}
|
||||
},
|
||||
args: process.argv.slice(2),
|
||||
});
|
||||
const logFile = await (async () => {
|
||||
if (!values.log)
|
||||
return;
|
||||
return await fs.promises.open(values.log, 'w');
|
||||
})();
|
||||
|
||||
|
||||
// Capture console.foo() output and either send it to the log file, or to nowhere.
|
||||
for (const [name, oldMethod] of Object.entries(console)) {
|
||||
console[name] = async (...args) => {
|
||||
let result;
|
||||
const stdio = capcon.interceptStdio(() => {
|
||||
result = oldMethod(...args);
|
||||
});
|
||||
|
||||
if (logFile) {
|
||||
await logFile.write(stdio.stdout);
|
||||
await logFile.write(stdio.stderr);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const ctx = new Context({
|
||||
ptt: new NodeStdioPTT(),
|
||||
config: {},
|
||||
platform: new Context({
|
||||
name: 'node',
|
||||
filesystem: CreateFilesystemProvider(),
|
||||
env: CreateEnvProvider(),
|
||||
}),
|
||||
});
|
||||
|
||||
await launchPuterShell(ctx);
|
79
packages/phoenix/src/main_puter.js
Normal file
79
packages/phoenix/src/main_puter.js
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Context } from 'contextlink';
|
||||
import { launchPuterShell } from './puter-shell/main.js';
|
||||
import { CreateFilesystemProvider } from './platform/puter/filesystem.js';
|
||||
import { CreateDriversProvider } from './platform/puter/drivers.js';
|
||||
import { XDocumentPTT } from './pty/XDocumentPTT.js';
|
||||
import { CreateEnvProvider } from './platform/puter/env.js';
|
||||
|
||||
window.main_shell = async () => {
|
||||
const config = {};
|
||||
|
||||
let resolveConfigured = null;
|
||||
const configured_ = new Promise(rslv => {
|
||||
resolveConfigured = rslv;
|
||||
});
|
||||
|
||||
const terminal = puter.ui.parentApp();
|
||||
if (!terminal) {
|
||||
console.error('Phoenix cannot run without a parent Terminal. Exiting...');
|
||||
puter.exit();
|
||||
return;
|
||||
}
|
||||
terminal.on('message', message => {
|
||||
if (message.$ === 'config') {
|
||||
const configValues = { ...message };
|
||||
delete configValues.$;
|
||||
for ( const k in configValues ) {
|
||||
config[k] = configValues[k];
|
||||
}
|
||||
resolveConfigured();
|
||||
}
|
||||
});
|
||||
terminal.on('close', () => {
|
||||
console.log('Terminal closed; exiting Phoenix...');
|
||||
puter.exit();
|
||||
});
|
||||
|
||||
// FIXME: on terminal close, close ourselves
|
||||
|
||||
terminal.postMessage({ $: 'ready' });
|
||||
|
||||
await configured_;
|
||||
|
||||
const puterSDK = globalThis.puter;
|
||||
if ( config['puter.auth.token'] ) {
|
||||
await puterSDK.setAuthToken(config['puter.auth.token']);
|
||||
}
|
||||
await puterSDK.setAPIOrigin(config['puter.api_origin']);
|
||||
|
||||
const ptt = new XDocumentPTT(terminal);
|
||||
await launchPuterShell(new Context({
|
||||
ptt,
|
||||
config, puterSDK,
|
||||
externs: new Context({ puterSDK }),
|
||||
platform: new Context({
|
||||
name: 'puter',
|
||||
filesystem: CreateFilesystemProvider({ puterSDK }),
|
||||
drivers: CreateDriversProvider({ puterSDK }),
|
||||
env: CreateEnvProvider({ config }),
|
||||
}),
|
||||
}));
|
||||
};
|
144
packages/phoenix/src/meta/versions.js
Normal file
144
packages/phoenix/src/meta/versions.js
Normal file
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const SHELL_VERSIONS = [
|
||||
{
|
||||
v: '0.2.4',
|
||||
changes: [
|
||||
'more completers for tab-completion',
|
||||
'help updates',
|
||||
'"which" command added',
|
||||
'"date" command added',
|
||||
'improvements when running under node.js',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.2.3',
|
||||
changes: [
|
||||
'"printf" command added',
|
||||
'"help" command updated',
|
||||
'"errno" command added',
|
||||
'POSIX error code associations added',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.2.2',
|
||||
changes: [
|
||||
'wc works with BLOB inputs',
|
||||
'"~" path resolution fixed',
|
||||
'"head" command added',
|
||||
'"tail" command updated',
|
||||
'"ls" symlink support improved',
|
||||
'"sort" command added',
|
||||
'Testing improved',
|
||||
'"cd" with no arguments works',
|
||||
'Filesystem errors are more consistent',
|
||||
'"help" output improved',
|
||||
'"pwd" argument processing updated'
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.2.1',
|
||||
changes: [
|
||||
'commands: true, false',
|
||||
'commands: basename, dirname',
|
||||
'more node.js support',
|
||||
'wc command',
|
||||
'sleep command',
|
||||
'improved coreutils documentation',
|
||||
'updates to existing coreutils',
|
||||
'readline fixes',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.2.0',
|
||||
changes: [
|
||||
'brand change: Phoenix Shell',
|
||||
'open-sourced under AGPL-3.0',
|
||||
'new commands: ai, txt2img, jq, and more',
|
||||
'added login command',
|
||||
'coreutils updates',
|
||||
'added command substitution',
|
||||
'parser improvements',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.1.10',
|
||||
changes: [
|
||||
'new input parser',
|
||||
'add pwd command',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.1.9',
|
||||
changes: [
|
||||
'add help command',
|
||||
'add changelog command',
|
||||
'add ioctl messages for window size',
|
||||
'add env.ROWS and env.COLS',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.1.8',
|
||||
changes: [
|
||||
'add neofetch command',
|
||||
'add simple tab completion',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.1.7',
|
||||
changes: [
|
||||
'add clear and printenv',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.1.6',
|
||||
changes: [
|
||||
'add redirect syntax',
|
||||
],
|
||||
},
|
||||
{
|
||||
v: '0.1.5',
|
||||
changes: [
|
||||
'add cp command',
|
||||
],
|
||||
},
|
||||
{
|
||||
v: '0.1.4',
|
||||
changes: [
|
||||
'improve error handling',
|
||||
],
|
||||
},
|
||||
{
|
||||
v: '0.1.3',
|
||||
changes: [
|
||||
'fixes for existing commands',
|
||||
'mv added',
|
||||
'cat added',
|
||||
'readline history (transient) added',
|
||||
]
|
||||
},
|
||||
{
|
||||
v: '0.1.2',
|
||||
changes: [
|
||||
'add echo',
|
||||
'fix synchronization of pipe coupler',
|
||||
]
|
||||
}
|
||||
];
|
143
packages/phoenix/src/platform/PosixError.js
Normal file
143
packages/phoenix/src/platform/PosixError.js
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const ErrorCodes = {
|
||||
EACCES: Symbol.for('EACCES'),
|
||||
EADDRINUSE: Symbol.for('EADDRINUSE'),
|
||||
ECONNREFUSED: Symbol.for('ECONNREFUSED'),
|
||||
ECONNRESET: Symbol.for('ECONNRESET'),
|
||||
EEXIST: Symbol.for('EEXIST'),
|
||||
EFBIG: Symbol.for('EFBIG'),
|
||||
EINVAL: Symbol.for('EINVAL'),
|
||||
EIO: Symbol.for('EIO'),
|
||||
EISDIR: Symbol.for('EISDIR'),
|
||||
EMFILE: Symbol.for('EMFILE'),
|
||||
ENOENT: Symbol.for('ENOENT'),
|
||||
ENOSPC: Symbol.for('ENOSPC'),
|
||||
ENOTDIR: Symbol.for('ENOTDIR'),
|
||||
ENOTEMPTY: Symbol.for('ENOTEMPTY'),
|
||||
EPERM: Symbol.for('EPERM'),
|
||||
EPIPE: Symbol.for('EPIPE'),
|
||||
ETIMEDOUT: Symbol.for('ETIMEDOUT'),
|
||||
};
|
||||
|
||||
// Codes taken from `errno` on Linux.
|
||||
export const ErrorMetadata = new Map([
|
||||
[ErrorCodes.EPERM, { code: 1, description: 'Operation not permitted' }],
|
||||
[ErrorCodes.ENOENT, { code: 2, description: 'File or directory not found' }],
|
||||
[ErrorCodes.EIO, { code: 5, description: 'IO error' }],
|
||||
[ErrorCodes.EACCES, { code: 13, description: 'Permission denied' }],
|
||||
[ErrorCodes.EEXIST, { code: 17, description: 'File already exists' }],
|
||||
[ErrorCodes.ENOTDIR, { code: 20, description: 'Is not a directory' }],
|
||||
[ErrorCodes.EISDIR, { code: 21, description: 'Is a directory' }],
|
||||
[ErrorCodes.EINVAL, { code: 22, description: 'Argument invalid' }],
|
||||
[ErrorCodes.EMFILE, { code: 24, description: 'Too many open files' }],
|
||||
[ErrorCodes.EFBIG, { code: 27, description: 'File too big' }],
|
||||
[ErrorCodes.ENOSPC, { code: 28, description: 'Device out of space' }],
|
||||
[ErrorCodes.EPIPE, { code: 32, description: 'Pipe broken' }],
|
||||
[ErrorCodes.ENOTEMPTY, { code: 39, description: 'Directory is not empty' }],
|
||||
[ErrorCodes.EADDRINUSE, { code: 98, description: 'Address already in use' }],
|
||||
[ErrorCodes.ECONNRESET, { code: 104, description: 'Connection reset'}],
|
||||
[ErrorCodes.ETIMEDOUT, { code: 110, description: 'Connection timed out' }],
|
||||
[ErrorCodes.ECONNREFUSED, { code: 111, description: 'Connection refused' }],
|
||||
]);
|
||||
|
||||
export const errorFromIntegerCode = (code) => {
|
||||
for (const [errorCode, metadata] of ErrorMetadata) {
|
||||
if (metadata.code === code) {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export class PosixError extends Error {
|
||||
// posixErrorCode can be either a string, or one of the ErrorCodes above.
|
||||
// If message is undefined, a default message will be used.
|
||||
constructor(posixErrorCode, message) {
|
||||
let posixCode;
|
||||
if (typeof posixErrorCode === 'symbol') {
|
||||
if (ErrorCodes[Symbol.keyFor(posixErrorCode)] !== posixErrorCode) {
|
||||
throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`);
|
||||
}
|
||||
posixCode = posixErrorCode;
|
||||
} else {
|
||||
const code = ErrorCodes[posixErrorCode];
|
||||
if (!code) throw new Error(`Unrecognized POSIX error code: '${posixErrorCode}'`);
|
||||
posixCode = code;
|
||||
}
|
||||
|
||||
super(message ?? ErrorMetadata.get(posixCode).description);
|
||||
this.posixCode = posixCode;
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers for constructing a PosixError when you don't already have an error message.
|
||||
//
|
||||
static AccessNotPermitted({ message, path } = {}) {
|
||||
return new PosixError(ErrorCodes.EACCES, message ?? (path ? `Access not permitted to: '${path}'` : undefined));
|
||||
}
|
||||
static AddressInUse({ message, address } = {}) {
|
||||
return new PosixError(ErrorCodes.EADDRINUSE, message ?? (address ? `Address '${address}' in use` : undefined));
|
||||
}
|
||||
static ConnectionRefused({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.ECONNREFUSED, message);
|
||||
}
|
||||
static ConnectionReset({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.ECONNRESET, message);
|
||||
}
|
||||
static PathAlreadyExists({ message, path } = {}) {
|
||||
return new PosixError(ErrorCodes.EEXIST, message ?? (path ? `Path already exists: '${path}'` : undefined));
|
||||
}
|
||||
static FileTooLarge({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.EFBIG, message);
|
||||
}
|
||||
static InvalidArgument({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.EINVAL, message);
|
||||
}
|
||||
static IO({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.EIO, message);
|
||||
}
|
||||
static IsDirectory({ message, path } = {}) {
|
||||
return new PosixError(ErrorCodes.EISDIR, message ?? (path ? `Path is directory: '${path}'` : undefined));
|
||||
}
|
||||
static TooManyOpenFiles({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.EMFILE, message);
|
||||
}
|
||||
static DoesNotExist({ message, path } = {}) {
|
||||
return new PosixError(ErrorCodes.ENOENT, message ?? (path ? `Path not found: '${path}'` : undefined));
|
||||
}
|
||||
static NotEnoughSpace({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.ENOSPC, message);
|
||||
}
|
||||
static IsNotDirectory({ message, path } = {}) {
|
||||
return new PosixError(ErrorCodes.ENOTDIR, message ?? (path ? `Path is not a directory: '${path}'` : undefined));
|
||||
}
|
||||
static DirectoryIsNotEmpty({ message, path } = {}) {
|
||||
return new PosixError(ErrorCodes.ENOTEMPTY, message ?? (path ?`Directory is not empty: '${path}'` : undefined));
|
||||
}
|
||||
static OperationNotPermitted({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.EPERM, message);
|
||||
}
|
||||
static BrokenPipe({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.EPIPE, message);
|
||||
}
|
||||
static TimedOut({ message } = {}) {
|
||||
return new PosixError(ErrorCodes.ETIMEDOUT, message);
|
||||
}
|
||||
}
|
20
packages/phoenix/src/platform/node/env.js
Normal file
20
packages/phoenix/src/platform/node/env.js
Normal file
@ -0,0 +1,20 @@
|
||||
import os from 'os';
|
||||
|
||||
export const CreateEnvProvider = () => {
|
||||
return {
|
||||
getEnv: () => {
|
||||
let env = process.env;
|
||||
if ( ! env.PS1 ) {
|
||||
env.PS1 = `[\\u@\\h \\w]\\$ `;
|
||||
}
|
||||
if ( ! env.HOSTNAME ) {
|
||||
env.HOSTNAME = os.hostname();
|
||||
}
|
||||
return env;
|
||||
},
|
||||
|
||||
get (k) {
|
||||
return this.getEnv()[k];
|
||||
}
|
||||
}
|
||||
}
|
219
packages/phoenix/src/platform/node/filesystem.js
Normal file
219
packages/phoenix/src/platform/node/filesystem.js
Normal file
@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path_ from 'path';
|
||||
|
||||
import modeString from 'fs-mode-to-string';
|
||||
import { ErrorCodes, PosixError } from '../PosixError.js';
|
||||
|
||||
function convertNodeError(e) {
|
||||
switch (e.code) {
|
||||
case 'EACCES': return new PosixError(ErrorCodes.EACCES, e.message);
|
||||
case 'EADDRINUSE': return new PosixError(ErrorCodes.EADDRINUSE, e.message);
|
||||
case 'ECONNREFUSED': return new PosixError(ErrorCodes.ECONNREFUSED, e.message);
|
||||
case 'ECONNRESET': return new PosixError(ErrorCodes.ECONNRESET, e.message);
|
||||
case 'EEXIST': return new PosixError(ErrorCodes.EEXIST, e.message);
|
||||
case 'EIO': return new PosixError(ErrorCodes.EIO, e.message);
|
||||
case 'EISDIR': return new PosixError(ErrorCodes.EISDIR, e.message);
|
||||
case 'EMFILE': return new PosixError(ErrorCodes.EMFILE, e.message);
|
||||
case 'ENOENT': return new PosixError(ErrorCodes.ENOENT, e.message);
|
||||
case 'ENOTDIR': return new PosixError(ErrorCodes.ENOTDIR, e.message);
|
||||
case 'ENOTEMPTY': return new PosixError(ErrorCodes.ENOTEMPTY, e.message);
|
||||
// ENOTFOUND is Node-specific. ECONNREFUSED is similar enough.
|
||||
case 'ENOTFOUND': return new PosixError(ErrorCodes.ECONNREFUSED, e.message);
|
||||
case 'EPERM': return new PosixError(ErrorCodes.EPERM, e.message);
|
||||
case 'EPIPE': return new PosixError(ErrorCodes.EPIPE, e.message);
|
||||
case 'ETIMEDOUT': return new PosixError(ErrorCodes.ETIMEDOUT, e.message);
|
||||
}
|
||||
// Some other kind of error
|
||||
return e;
|
||||
}
|
||||
|
||||
// DRY: Almost the same as puter/filesystem.js
|
||||
function wrapAPIs(apis) {
|
||||
for (const method in apis) {
|
||||
if (typeof apis[method] !== 'function') {
|
||||
continue;
|
||||
}
|
||||
const original = apis[method];
|
||||
apis[method] = async (...args) => {
|
||||
try {
|
||||
return await original(...args);
|
||||
} catch (e) {
|
||||
throw convertNodeError(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
return apis;
|
||||
}
|
||||
|
||||
export const CreateFilesystemProvider = () => {
|
||||
return wrapAPIs({
|
||||
capabilities: {
|
||||
'readdir.posix-mode': true,
|
||||
},
|
||||
readdir: async (path) => {
|
||||
const names = await fs.promises.readdir(path);
|
||||
|
||||
const items = [];
|
||||
|
||||
const users = {};
|
||||
const groups = {};
|
||||
|
||||
for ( const name of names ) {
|
||||
const filePath = path_.join(path, name);
|
||||
const stat = await fs.promises.lstat(filePath);
|
||||
|
||||
items.push({
|
||||
name,
|
||||
is_dir: stat.isDirectory(),
|
||||
is_symlink: stat.isSymbolicLink(),
|
||||
symlink_path: stat.isSymbolicLink() ? await fs.promises.readlink(filePath) : null,
|
||||
size: stat.size,
|
||||
modified: stat.mtimeMs / 1000,
|
||||
created: stat.ctimeMs / 1000,
|
||||
accessed: stat.atimeMs / 1000,
|
||||
mode: stat.mode,
|
||||
mode_human_readable: modeString(stat.mode),
|
||||
uid: stat.uid,
|
||||
gid: stat.gid,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
stat: async (path) => {
|
||||
const stat = await fs.promises.lstat(path);
|
||||
const fullPath = await fs.promises.realpath(path);
|
||||
const parsedPath = path_.parse(fullPath);
|
||||
// TODO: Fill in more of these?
|
||||
return {
|
||||
id: stat.ino,
|
||||
associated_app_id: null,
|
||||
public_token: null,
|
||||
file_request_token: null,
|
||||
uid: stat.uid,
|
||||
parent_id: null,
|
||||
parent_uid: null,
|
||||
is_dir: stat.isDirectory(),
|
||||
is_public: null,
|
||||
is_shortcut: null,
|
||||
is_symlink: stat.isSymbolicLink(),
|
||||
symlink_path: stat.isSymbolicLink() ? await fs.promises.readlink(path) : null,
|
||||
sort_by: null,
|
||||
sort_order: null,
|
||||
immutable: null,
|
||||
name: parsedPath.base,
|
||||
path: fullPath,
|
||||
dirname: parsedPath.dir,
|
||||
dirpath: parsedPath.dir,
|
||||
metadata: null,
|
||||
modified: stat.mtime,
|
||||
created: stat.birthtime,
|
||||
accessed: stat.atime,
|
||||
size: stat.size,
|
||||
layout: null,
|
||||
owner: null,
|
||||
type: null,
|
||||
is_empty: await (async (stat) => {
|
||||
if (!stat.isDirectory())
|
||||
return null;
|
||||
const children = await fs.promises.readdir(path);
|
||||
return children.length === 0;
|
||||
})(stat),
|
||||
};
|
||||
},
|
||||
mkdir: async (path, options = { createMissingParents: false }) => {
|
||||
const createMissingParents = options['createMissingParents'] || false;
|
||||
return await fs.promises.mkdir(path, { recursive: createMissingParents });
|
||||
},
|
||||
read: async (path) => {
|
||||
return await fs.promises.readFile(path);
|
||||
},
|
||||
write: async (path, data) => {
|
||||
if (data instanceof Blob) {
|
||||
return await fs.promises.writeFile(path, data.stream());
|
||||
}
|
||||
return await fs.promises.writeFile(path, data);
|
||||
},
|
||||
rm: async (path, options = { recursive: false }) => {
|
||||
const recursive = options['recursive'] || false;
|
||||
const stat = await fs.promises.stat(path);
|
||||
|
||||
if ( stat.isDirectory() && ! recursive ) {
|
||||
throw PosixError.IsDirectory({ path });
|
||||
}
|
||||
|
||||
return await fs.promises.rm(path, { recursive });
|
||||
},
|
||||
rmdir: async (path) => {
|
||||
const stat = await fs.promises.stat(path);
|
||||
|
||||
if ( !stat.isDirectory() ) {
|
||||
throw PosixError.IsNotDirectory({ path });
|
||||
}
|
||||
|
||||
return await fs.promises.rmdir(path);
|
||||
},
|
||||
move: async (oldPath, newPath) => {
|
||||
let destStat = null;
|
||||
try {
|
||||
destStat = await fs.promises.stat(newPath);
|
||||
} catch (e) {
|
||||
if ( e.code !== 'ENOENT' ) throw e;
|
||||
}
|
||||
|
||||
// fs.promises.rename() expects the new path to include the filename.
|
||||
// So, if newPath is a directory, append the old filename to it to produce the target path and name.
|
||||
if ( destStat && destStat.isDirectory() ) {
|
||||
if ( ! newPath.endsWith('/') ) newPath += '/';
|
||||
newPath += path_.basename(oldPath);
|
||||
}
|
||||
|
||||
return await fs.promises.rename(oldPath, newPath);
|
||||
},
|
||||
copy: async (oldPath, newPath) => {
|
||||
const srcStat = await fs.promises.stat(oldPath);
|
||||
const srcIsDir = srcStat.isDirectory();
|
||||
|
||||
let destStat = null;
|
||||
try {
|
||||
destStat = await fs.promises.stat(newPath);
|
||||
} catch (e) {
|
||||
if ( e.code !== 'ENOENT' ) throw e;
|
||||
}
|
||||
const destIsDir = destStat && destStat.isDirectory();
|
||||
|
||||
// fs.promises.cp() is experimental, but does everything we want. Maybe implement this manually if needed.
|
||||
|
||||
// `dir -> file`: invalid
|
||||
if ( srcIsDir && destStat && ! destStat.isDirectory() ) {
|
||||
throw new PosixError(ErrorCodes.ENOTDIR, 'Cannot copy a directory into a file');
|
||||
}
|
||||
|
||||
// `file -> dir`: fs.promises.cp() expects the new path to include the filename.
|
||||
if ( ! srcIsDir && destIsDir ) {
|
||||
if ( ! newPath.endsWith('/') ) newPath += '/';
|
||||
newPath += path_.basename(oldPath);
|
||||
}
|
||||
|
||||
return await fs.promises.cp(oldPath, newPath, { recursive: srcIsDir });
|
||||
}
|
||||
});
|
||||
};
|
74
packages/phoenix/src/pty/NodeStdioPTT.js
Normal file
74
packages/phoenix/src/pty/NodeStdioPTT.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { ReadableStream, WritableStream } from 'stream/web';
|
||||
import { signals } from "../ansi-shell/signals.js";
|
||||
|
||||
const writestream_node_to_web = node_stream => {
|
||||
return node_stream;
|
||||
// return new WritableStream({
|
||||
// write: chunk => {
|
||||
// node_stream.write(chunk);
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
export class NodeStdioPTT {
|
||||
constructor() {
|
||||
// this.in = process.stdin;
|
||||
// this.out = process.stdout;
|
||||
// this.err = process.stderr;
|
||||
|
||||
// this.in = ReadableStream.from(process.stdin).getReader();
|
||||
|
||||
let readController;
|
||||
const readableStream = new ReadableStream({
|
||||
start: controller => {
|
||||
readController = controller;
|
||||
}
|
||||
});
|
||||
this.in = readableStream.getReader();
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.on('data', chunk => {
|
||||
const input = new Uint8Array(chunk);
|
||||
readController.enqueue(input);
|
||||
});
|
||||
|
||||
this.out = writestream_node_to_web(process.stdout);
|
||||
this.err = writestream_node_to_web(process.stderr);
|
||||
|
||||
this.ioctl_listeners = {};
|
||||
|
||||
process.stdout.on('resize', () => {
|
||||
this.emit('ioctl.set', {
|
||||
data: {
|
||||
windowSize: {
|
||||
rows: process.stdout.rows,
|
||||
cols: process.stdout.columns,
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
globalThis.force_eot = true;
|
||||
readController.enqueue(new Uint8Array([4]));
|
||||
});
|
||||
}
|
||||
|
||||
on (name, listener) {
|
||||
if ( ! this.ioctl_listeners.hasOwnProperty(name) ) {
|
||||
this.ioctl_listeners[name] = [];
|
||||
}
|
||||
this.ioctl_listeners[name].push(listener);
|
||||
|
||||
// Hack: Pretend the window got resized, so that listeners get notified of the current size.
|
||||
if (name === 'ioctl.set') {
|
||||
process.stdout.emit('resize');
|
||||
}
|
||||
}
|
||||
|
||||
emit (name, evt) {
|
||||
if ( ! this.ioctl_listeners.hasOwnProperty(name) ) return;
|
||||
for ( const listener of this.ioctl_listeners[name] ) {
|
||||
listener(evt);
|
||||
}
|
||||
}
|
||||
}
|
75
packages/phoenix/src/pty/XDocumentPTT.js
Normal file
75
packages/phoenix/src/pty/XDocumentPTT.js
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { BetterReader } from "dev-pty";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export class XDocumentPTT {
|
||||
constructor(terminalConnection) {
|
||||
this.ioctl_listeners = {};
|
||||
|
||||
this.readableStream = new ReadableStream({
|
||||
start: controller => {
|
||||
this.readController = controller;
|
||||
}
|
||||
});
|
||||
this.writableStream = new WritableStream({
|
||||
start: controller => {
|
||||
this.writeController = controller;
|
||||
},
|
||||
write: chunk => {
|
||||
if (typeof chunk === 'string') {
|
||||
chunk = encoder.encode(chunk);
|
||||
}
|
||||
terminalConnection.postMessage({
|
||||
$: 'output',
|
||||
data: chunk,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.out = this.writableStream.getWriter();
|
||||
this.in = this.readableStream.getReader();
|
||||
this.in = new BetterReader({ delegate: this.in });
|
||||
|
||||
terminalConnection.on('message', message => {
|
||||
if (message.$ === 'ioctl.set') {
|
||||
this.emit('ioctl.set', message);
|
||||
return;
|
||||
}
|
||||
if (message.$ === 'input') {
|
||||
this.readController.enqueue(message.data);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
on (name, listener) {
|
||||
if ( ! this.ioctl_listeners.hasOwnProperty(name) ) {
|
||||
this.ioctl_listeners[name] = [];
|
||||
}
|
||||
this.ioctl_listeners[name].push(listener);
|
||||
}
|
||||
|
||||
emit (name, evt) {
|
||||
if ( ! this.ioctl_listeners.hasOwnProperty(name) ) return;
|
||||
for ( const listener of this.ioctl_listeners[name] ) {
|
||||
listener(evt);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export class CommandCompleter {
|
||||
async getCompletions (ctx, inputState) {
|
||||
const { builtins } = ctx.registries;
|
||||
const query = inputState.input;
|
||||
|
||||
if ( query === '' ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const completions = [];
|
||||
|
||||
// TODO: Match executable names as well as builtins
|
||||
for ( const commandName of Object.keys(builtins) ) {
|
||||
if ( commandName.startsWith(query) ) {
|
||||
completions.push(commandName.slice(query.length));
|
||||
}
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import path_ from "path-browserify";
|
||||
import { resolveRelativePath } from '../../util/path.js';
|
||||
|
||||
export class FileCompleter {
|
||||
async getCompletions (ctx, inputState) {
|
||||
const { filesystem } = ctx.platform;
|
||||
|
||||
if ( inputState.input === '' ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let path = resolveRelativePath(ctx.vars, inputState.input);
|
||||
let dir = path_.dirname(path);
|
||||
let base = path_.basename(path);
|
||||
|
||||
const completions = [];
|
||||
|
||||
const result = await filesystem.readdir(dir);
|
||||
if ( result === undefined ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for ( const item of result ) {
|
||||
if ( item.name.startsWith(base) ) {
|
||||
completions.push(item.name.slice(base.length));
|
||||
}
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { DEFAULT_OPTIONS } from '../coreutils/coreutil_lib/help.js';
|
||||
|
||||
export class OptionCompleter {
|
||||
async getCompletions (ctx, inputState) {
|
||||
const { builtins } = ctx.registries;
|
||||
const query = inputState.input;
|
||||
|
||||
if ( query === '' ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: Query the command through the providers system.
|
||||
// Or, we could include the command in the context that's given to completers?
|
||||
const command = builtins[inputState.tokens[0]];
|
||||
if ( ! command ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const completions = [];
|
||||
|
||||
const processOptions = (options) => {
|
||||
for ( const optionName of Object.keys(options) ) {
|
||||
const prefixedOptionName = `--${optionName}`;
|
||||
if ( prefixedOptionName.startsWith(query) ) {
|
||||
completions.push(prefixedOptionName.slice(query.length));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Only check these for builtins!
|
||||
processOptions(DEFAULT_OPTIONS);
|
||||
|
||||
if ( command.args?.options ) {
|
||||
processOptions(command.args.options);
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
}
|
106
packages/phoenix/src/puter-shell/coreutils/__exports__.js
Normal file
106
packages/phoenix/src/puter-shell/coreutils/__exports__.js
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Generated by /tools/gen.js
|
||||
import module_ai from './ai.js'
|
||||
import module_basename from './basename.js'
|
||||
import module_cat from './cat.js'
|
||||
import module_cd from './cd.js'
|
||||
import module_changelog from './changelog.js'
|
||||
import module_clear from './clear.js'
|
||||
import module_concept_parser from './concept-parser.js'
|
||||
import module_cp from './cp.js'
|
||||
import module_date from './date.js'
|
||||
import module_dcall from './dcall.js'
|
||||
import module_dirname from './dirname.js'
|
||||
import module_echo from './echo.js'
|
||||
import module_env from './env.js'
|
||||
import module_errno from './errno.js'
|
||||
import module_false from './false.js'
|
||||
import module_grep from './grep.js'
|
||||
import module_head from './head.js'
|
||||
import module_help from './help.js'
|
||||
import module_jq from './jq.js'
|
||||
import module_login from './login.js'
|
||||
import module_ls from './ls.js'
|
||||
import module_man from './man.js'
|
||||
import module_mkdir from './mkdir.js'
|
||||
import module_mv from './mv.js'
|
||||
import module_neofetch from './neofetch.js'
|
||||
import module_printf from './printf.js'
|
||||
import module_printhist from './printhist.js'
|
||||
import module_pwd from './pwd.js'
|
||||
import module_rm from './rm.js'
|
||||
import module_rmdir from './rmdir.js'
|
||||
import module_sample_data from './sample-data.js'
|
||||
import module_sed from './sed.js'
|
||||
import module_sleep from './sleep.js'
|
||||
import module_sort from './sort.js'
|
||||
import module_tail from './tail.js'
|
||||
import module_test from './test.js'
|
||||
import module_touch from './touch.js'
|
||||
import module_true from './true.js'
|
||||
import module_txt2img from './txt2img.js'
|
||||
import module_usages from './usages.js'
|
||||
import module_wc from './wc.js'
|
||||
import module_which from './which.js'
|
||||
|
||||
export default {
|
||||
"ai": module_ai,
|
||||
"basename": module_basename,
|
||||
"cat": module_cat,
|
||||
"cd": module_cd,
|
||||
"changelog": module_changelog,
|
||||
"clear": module_clear,
|
||||
"concept-parser": module_concept_parser,
|
||||
"cp": module_cp,
|
||||
"date": module_date,
|
||||
"dcall": module_dcall,
|
||||
"dirname": module_dirname,
|
||||
"echo": module_echo,
|
||||
"env": module_env,
|
||||
"errno": module_errno,
|
||||
"false": module_false,
|
||||
"grep": module_grep,
|
||||
"head": module_head,
|
||||
"help": module_help,
|
||||
"jq": module_jq,
|
||||
"login": module_login,
|
||||
"ls": module_ls,
|
||||
"man": module_man,
|
||||
"mkdir": module_mkdir,
|
||||
"mv": module_mv,
|
||||
"neofetch": module_neofetch,
|
||||
"printf": module_printf,
|
||||
"printhist": module_printhist,
|
||||
"pwd": module_pwd,
|
||||
"rm": module_rm,
|
||||
"rmdir": module_rmdir,
|
||||
"sample-data": module_sample_data,
|
||||
"sed": module_sed,
|
||||
"sleep": module_sleep,
|
||||
"sort": module_sort,
|
||||
"tail": module_tail,
|
||||
"test": module_test,
|
||||
"touch": module_touch,
|
||||
"true": module_true,
|
||||
"txt2img": module_txt2img,
|
||||
"usages": module_usages,
|
||||
"wc": module_wc,
|
||||
"which": module_which,
|
||||
};
|
87
packages/phoenix/src/puter-shell/coreutils/ai.js
Normal file
87
packages/phoenix/src/puter-shell/coreutils/ai.js
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Exit } from './coreutil_lib/exit.js';
|
||||
|
||||
export default {
|
||||
name: 'ai',
|
||||
usage: 'ai PROMPT',
|
||||
description: 'Send PROMPT to Puter\'s AI chatbot, and print its response.',
|
||||
args: {
|
||||
$: 'simple-parser',
|
||||
allowPositionals: true,
|
||||
},
|
||||
execute: async ctx => {
|
||||
const { positionals } = ctx.locals;
|
||||
const [ prompt ] = positionals;
|
||||
|
||||
if ( ! prompt ) {
|
||||
await ctx.externs.err.write('ai: missing prompt\n');
|
||||
throw new Exit(1);
|
||||
}
|
||||
if ( positionals.length > 1 ) {
|
||||
await ctx.externs.err.write('ai: prompt must be wrapped in quotes\n');
|
||||
throw new Exit(1);
|
||||
}
|
||||
|
||||
const { drivers } = ctx.platform;
|
||||
const { chatHistory } = ctx.plugins;
|
||||
|
||||
let a_interface, a_method, a_args;
|
||||
|
||||
a_interface = 'puter-chat-completion';
|
||||
a_method = 'complete';
|
||||
a_args = {
|
||||
messages: [
|
||||
...chatHistory.get_messages(),
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
console.log('THESE ARE THE MESSAGES', a_args.messages);
|
||||
|
||||
const result = await drivers.call({
|
||||
interface: a_interface,
|
||||
method: a_method,
|
||||
args: a_args,
|
||||
});
|
||||
|
||||
const resobj = JSON.parse(await result.text(), null, 2);
|
||||
|
||||
if ( resobj.success !== true ) {
|
||||
await ctx.externs.err.write('request failed\n');
|
||||
await ctx.externs.err.write(resobj);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = resobj?.result?.message?.content;
|
||||
|
||||
if ( ! message ) {
|
||||
await ctx.externs.err.write('message not found in response\n');
|
||||
await ctx.externs.err.write(result);
|
||||
return;
|
||||
}
|
||||
|
||||
chatHistory.add_message(resobj?.result?.message);
|
||||
|
||||
await ctx.externs.out.write(message + '\n');
|
||||
}
|
||||
}
|
81
packages/phoenix/src/puter-shell/coreutils/basename.js
Normal file
81
packages/phoenix/src/puter-shell/coreutils/basename.js
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Exit } from './coreutil_lib/exit.js';
|
||||
|
||||
export default {
|
||||
name: 'basename',
|
||||
usage: 'basename PATH [SUFFIX]',
|
||||
description: 'Print PATH without leading directory segments.\n\n' +
|
||||
'If SUFFIX is provided, it is removed from the end of the result.',
|
||||
args: {
|
||||
$: 'simple-parser',
|
||||
allowPositionals: true
|
||||
},
|
||||
execute: async ctx => {
|
||||
let string = ctx.locals.positionals[0];
|
||||
const suffix = ctx.locals.positionals[1];
|
||||
|
||||
if (string === undefined) {
|
||||
await ctx.externs.err.write('basename: Missing path argument\n');
|
||||
throw new Exit(1);
|
||||
}
|
||||
if (ctx.locals.positionals.length > 2) {
|
||||
await ctx.externs.err.write('basename: Too many arguments, expected 1 or 2\n');
|
||||
throw new Exit(1);
|
||||
}
|
||||
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/basename.html
|
||||
|
||||
// 1. If string is a null string, it is unspecified whether the resulting string is '.' or a null string.
|
||||
// In either case, skip steps 2 through 6.
|
||||
if (string === '') {
|
||||
string = '.';
|
||||
} else {
|
||||
// 2. If string is "//", it is implementation-defined whether steps 3 to 6 are skipped or processed.
|
||||
// NOTE: We process it normally.
|
||||
|
||||
// 3. If string consists entirely of <slash> characters, string shall be set to a single <slash> character.
|
||||
// In this case, skip steps 4 to 6.
|
||||
if (/^\/+$/.test(string)) {
|
||||
string = '/';
|
||||
} else {
|
||||
// 4. If there are any trailing <slash> characters in string, they shall be removed.
|
||||
string = string.replace(/\/+$/, '');
|
||||
|
||||
// 5. If there are any <slash> characters remaining in string, the prefix of string up to and including
|
||||
// the last <slash> character in string shall be removed.
|
||||
const lastSlashIndex = string.lastIndexOf('/');
|
||||
if (lastSlashIndex !== -1) {
|
||||
string = string.substring(lastSlashIndex + 1);
|
||||
}
|
||||
|
||||
// 6. If the suffix operand is present, is not identical to the characters remaining in string, and is
|
||||
// identical to a suffix of the characters remaining in string, the suffix suffix shall be removed
|
||||
// from string. Otherwise, string is not modified by this step. It shall not be considered an error
|
||||
// if suffix is not found in string.
|
||||
if (suffix !== undefined && suffix !== string && string.endsWith(suffix)) {
|
||||
string = string.substring(0, string.length - suffix.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The resulting string shall be written to standard output.
|
||||
await ctx.externs.out.write(string + '\n');
|
||||
}
|
||||
};
|
60
packages/phoenix/src/puter-shell/coreutils/cat.js
Normal file
60
packages/phoenix/src/puter-shell/coreutils/cat.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { resolveRelativePath } from '../../util/path.js';
|
||||
|
||||
export default {
|
||||
name: 'cat',
|
||||
usage: 'cat [FILE...]',
|
||||
description: 'Concatenate the FILE(s) and print the result.\n\n' +
|
||||
'If no FILE is given, or a FILE is `-`, read the standard input.',
|
||||
args: {
|
||||
$: 'simple-parser',
|
||||
allowPositionals: true
|
||||
},
|
||||
input: {
|
||||
syncLines: true,
|
||||
},
|
||||
output: 'text',
|
||||
execute: async ctx => {
|
||||
const { positionals, values } = ctx.locals;
|
||||
const { filesystem } = ctx.platform;
|
||||
|
||||
const paths = [...positionals];
|
||||
if ( paths.length < 1 ) paths.push('-');
|
||||
|
||||
for ( const relPath of paths ) {
|
||||
if ( relPath === '-' ) {
|
||||
let line, done;
|
||||
const next_line = async () => {
|
||||
({ value: line, done } = await ctx.externs.in_.read());
|
||||
console.log('CAT LOOP', { line, done });
|
||||
}
|
||||
for ( await next_line() ; ! done ; await next_line() ) {
|
||||
await ctx.externs.out.write(line);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const absPath = resolveRelativePath(ctx.vars, relPath);
|
||||
|
||||
const result = await filesystem.read(absPath);
|
||||
|
||||
await ctx.externs.out.write(result);
|
||||
}
|
||||
}
|
||||
}
|
48
packages/phoenix/src/puter-shell/coreutils/cd.js
Normal file
48
packages/phoenix/src/puter-shell/coreutils/cd.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Exit } from './coreutil_lib/exit.js';
|
||||
import { resolveRelativePath } from '../../util/path.js';
|
||||
|
||||
export default {
|
||||
name: 'cd',
|
||||
usage: 'cd PATH',
|
||||
description: 'Change the current directory to PATH.',
|
||||
args: {
|
||||
$: 'simple-parser',
|
||||
allowPositionals: true
|
||||
},
|
||||
execute: async ctx => {
|
||||
// ctx.params to access processed args
|
||||
// ctx.args to access raw args
|
||||
const { positionals, values } = ctx.locals;
|
||||
const { filesystem } = ctx.platform;
|
||||
|
||||
let [ target ] = positionals;
|
||||
target = resolveRelativePath(ctx.vars, target);
|
||||
|
||||
const result = await filesystem.readdir(target);
|
||||
|
||||
if ( result.$ === 'error' ) {
|
||||
await ctx.externs.err.write('cd: error: ' + result.message + '\n');
|
||||
throw new Exit(1);
|
||||
}
|
||||
|
||||
ctx.vars.pwd = target;
|
||||
}
|
||||
};
|
52
packages/phoenix/src/puter-shell/coreutils/changelog.js
Normal file
52
packages/phoenix/src/puter-shell/coreutils/changelog.js
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Phoenix Shell.
|
||||
*
|
||||
* Phoenix Shell is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { SHELL_VERSIONS } from "../../meta/versions.js";
|
||||
|
||||
async function printVersion(ctx, version) {
|
||||
await ctx.externs.out.write(`\x1B[35;1m[v${version.v}]\x1B[0m\n`);
|
||||
for ( const change of version.changes ) {
|
||||
await ctx.externs.out.write(`\x1B[32;1m+\x1B[0m ${change}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'changelog',
|
||||
description: 'Print the changelog for the Phoenix Shell, ordered oldest to newest.',
|
||||
args: {
|
||||
$: 'simple-parser',
|
||||
allowPositionals: false,
|
||||
options: {
|
||||
latest: {
|
||||
description: 'Print only the changes for the most recent version',
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
},
|
||||
execute: async ctx => {
|
||||
if (ctx.locals.values.latest) {
|
||||
await printVersion(ctx, SHELL_VERSIONS[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
for ( const version of SHELL_VERSIONS.toReversed() ) {
|
||||
await printVersion(ctx, version);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user