@nexus-aethra/dshell-std 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/lib/contracts.js +129 -0
- package/lib/contracts.js.map +1 -0
- package/lib/host-copy.js +25 -0
- package/lib/host-copy.js.map +1 -0
- package/lib/index.js +21 -0
- package/lib/index.js.map +1 -0
- package/lib/shell-line.js +275 -0
- package/lib/shell-line.js.map +1 -0
- package/lib/storage.js +41 -0
- package/lib/storage.js.map +1 -0
- package/lib/types/contracts.d.ts +753 -0
- package/lib/types/contracts.d.ts.map +1 -0
- package/lib/types/host-copy.d.ts +60 -0
- package/lib/types/host-copy.d.ts.map +1 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/shell-line.d.ts +130 -0
- package/lib/types/shell-line.d.ts.map +1 -0
- package/lib/types/storage.d.ts +147 -0
- package/lib/types/storage.d.ts.map +1 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nexus Aethra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/lib/contracts.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every dshell wire contract: the `/api/dshell/*` paths and the shapes that
|
|
3
|
+
* cross them.
|
|
4
|
+
*
|
|
5
|
+
* Layering rule for this file, and for this package as a whole: it declares
|
|
6
|
+
* FACTS, not behaviour. No dsh packages, no runtime state, and no imports except
|
|
7
|
+
* types from this package's own sibling modules — so both halves of every plugin
|
|
8
|
+
* (host route and browser face) can import the same declaration, and a contract
|
|
9
|
+
* change is a compile error on both sides instead of a silent drift. The browser
|
|
10
|
+
* faces used to restate these shapes by hand precisely because there was nowhere
|
|
11
|
+
* shared to put them.
|
|
12
|
+
*
|
|
13
|
+
* Sections below were moved verbatim from the packages' own protocol modules,
|
|
14
|
+
* which now re-export from here; the package-local headers are kept because they
|
|
15
|
+
* carry the reasoning for each shape.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The file navigator's wire vocabulary, shared by the host route and the
|
|
19
|
+
* browser face.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately free of value imports: the browser half imports this module, and
|
|
22
|
+
* anything it pulls in would be bundled into `client.js`.
|
|
23
|
+
*
|
|
24
|
+
* A path on the wire is always the CANONICAL absolute path **in the session's
|
|
25
|
+
* own execution world** — the host path for a local session, the device path
|
|
26
|
+
* for a device-bound one. That is the string the client shows, splits into
|
|
27
|
+
* segments, trims for `..`, and hands back to list again, so both ends agree on
|
|
28
|
+
* one spelling without either knowing which world produced it.
|
|
29
|
+
*/
|
|
30
|
+
/** The exact `/api` path the file navigator talks to. */
|
|
31
|
+
export const DSHELL_FILES_PATH = '/api/dshell/files';
|
|
32
|
+
// ─── files — the two-world transfer view ───
|
|
33
|
+
// moved from packages/dshell/files/src/transfer-protocol.ts
|
|
34
|
+
/**
|
|
35
|
+
* The file-transfer wire vocabulary: one device session's two file trees, and
|
|
36
|
+
* the copies between them.
|
|
37
|
+
*
|
|
38
|
+
* Separate from `./protocol.ts` because it is a different route with a
|
|
39
|
+
* different subject. The navigator's route answers about ONE world (the
|
|
40
|
+
* session's); this one is about the pair — this machine and the device the
|
|
41
|
+
* session runs on — and about work that outlives one request.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately free of value imports: the browser face imports this module, and
|
|
44
|
+
* anything it pulled in would be bundled into `client.js`. `Buffer`-style host
|
|
45
|
+
* types are not needed on the browser side at all.
|
|
46
|
+
*/
|
|
47
|
+
/** The exact `/api` path the transfer view talks to. */
|
|
48
|
+
export const DSHELL_TRANSFER_PATH = '/api/dshell/transfer';
|
|
49
|
+
// ─── buffer — the cross-session pipe: links, tickets, grants, transfers ───
|
|
50
|
+
// moved from packages/dshell/buffer/src/protocol.ts
|
|
51
|
+
/**
|
|
52
|
+
* The buffer's wire vocabulary, shared by the host route and the browser face.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately free of value imports: the browser half imports this module, and
|
|
55
|
+
* anything it pulls in would be bundled into `client.js`. Identifiers that the
|
|
56
|
+
* host validates against (a request `action`, a ticket `state`) are plain
|
|
57
|
+
* strings here and narrowed on the host side.
|
|
58
|
+
*
|
|
59
|
+
* Session ids travel as plain strings rather than the branded `SessionId`
|
|
60
|
+
* because this is a JSON boundary; the host converts at the edges.
|
|
61
|
+
*/
|
|
62
|
+
/** The exact `/api` path the pipe UI talks to. */
|
|
63
|
+
export const DSHELL_BUFFER_PATH = '/api/dshell/buffer';
|
|
64
|
+
/** The plugin name every buffer-authored message carries as its provenance. */
|
|
65
|
+
export const BUFFER_PLUGIN = 'dshell-buffer';
|
|
66
|
+
/** States after which a ticket is settled and never changes again. */
|
|
67
|
+
export const SETTLED_STATES = ['done', 'failed', 'timeout', 'cancelled'];
|
|
68
|
+
// ─── ssh — devices, bindings, their requests/responses ───
|
|
69
|
+
// moved from packages/dshell/ssh/src/protocol.ts
|
|
70
|
+
/**
|
|
71
|
+
* dshell-ssh wire — one exact `/api/dshell/ssh` route, plus the device shapes
|
|
72
|
+
* both halves agree on.
|
|
73
|
+
*
|
|
74
|
+
* The route carries what must not travel through the settings document: the
|
|
75
|
+
* private keys themselves. A device's durable record is small JSON; its key is
|
|
76
|
+
* a separate file written 0600 under `$DSH_HOME/dshell/ssh/keys/`.
|
|
77
|
+
*
|
|
78
|
+
* This module is imported by the browser half through the package's
|
|
79
|
+
* `./protocol` subpath, so it must stay free of value imports: anything it
|
|
80
|
+
* pulls in would be bundled into the client.
|
|
81
|
+
*/
|
|
82
|
+
/** Exact `/api` route path owned by the SSH device registry. */
|
|
83
|
+
export const DSHELL_SSH_PATH = '/api/dshell/ssh';
|
|
84
|
+
/** Settings namespace owned by this plugin; also the device card's slot key. */
|
|
85
|
+
export const SSH_SETTINGS_NAMESPACE = 'dshell-ssh';
|
|
86
|
+
// ─── workspace — the session archive routes ───
|
|
87
|
+
// moved from packages/dshell/workspace/src/protocol.ts
|
|
88
|
+
/**
|
|
89
|
+
* dshell session-panel wire — design 4.7 follow-up.
|
|
90
|
+
*
|
|
91
|
+
* The sidebar's session rows need two things dsh does not offer: an archive
|
|
92
|
+
* tag that hides a session without touching its log, and a history purge.
|
|
93
|
+
* Both are dshell concepts, so they travel over dshell's own exact route on
|
|
94
|
+
* the shared `/api` channel rather than through the Typert Remote table
|
|
95
|
+
* (whose client artifacts are generated from dsh's own packages).
|
|
96
|
+
*
|
|
97
|
+
* The path sits below the shared channel exactly like file-upload's: the
|
|
98
|
+
* physical carrier applies dsh's trust and authentication policy before the
|
|
99
|
+
* handler ever runs, so the route itself does no authorization.
|
|
100
|
+
*
|
|
101
|
+
* Shared by both halves of the package, so the constant and the shapes live
|
|
102
|
+
* in one file that neither the host nor the client bundle owns.
|
|
103
|
+
*/
|
|
104
|
+
/** Exact `/api` route path owned by the dshell session panel. */
|
|
105
|
+
export const DSHELL_SESSIONS_PATH = '/api/dshell/sessions';
|
|
106
|
+
// ─── terminal-bridge — the shell's history read ───
|
|
107
|
+
// moved from packages/dshell/terminal-bridge/src/route.ts
|
|
108
|
+
export const DSHELL_PTY_PATH = '/api/dshell/pty';
|
|
109
|
+
// ─── terminal-bridge — the frame stream over the shared API channel ───
|
|
110
|
+
/**
|
|
111
|
+
* Downstream half of the frame stream: a long-lived GET whose response body is
|
|
112
|
+
* newline-delimited JSON, one bridge frame per line.
|
|
113
|
+
*
|
|
114
|
+
* The wire itself is unchanged — these are the very frames the ws upgrade
|
|
115
|
+
* carries — but a Response body is reachable from any composition the
|
|
116
|
+
* `connection` service is in, while `registerUpgrade` needs `webServer`, which
|
|
117
|
+
* the desktop shell deliberately does not compose. Identity is explicit because
|
|
118
|
+
* an HTTP stream has no socket to key on: both halves carry the client's own
|
|
119
|
+
* `clientId`, and the GET carries the bind (`sessionId` + `stream`) that a ws
|
|
120
|
+
* would have sent as a frame.
|
|
121
|
+
*/
|
|
122
|
+
export const DSHELL_STREAM_PATH = '/api/dshell/stream';
|
|
123
|
+
/**
|
|
124
|
+
* Upstream half: one POST per control frame (`input`, `resize`, `signal`,
|
|
125
|
+
* `agent-open`, `reconnect`), identified by the same `clientId` the GET opened
|
|
126
|
+
* with. Answers with no body; the stream is the only place results appear.
|
|
127
|
+
*/
|
|
128
|
+
export const DSHELL_STREAM_SEND_PATH = '/api/dshell/stream/send';
|
|
129
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH;;;;;;;;;;;;GAYG;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAA;AA2KpD,8CAA8C;AAC9C,4DAA4D;AAE5D;;;;;;;;;;;;GAYG;AAEH,wDAAwD;AACxD,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAA;AAsH1D,6EAA6E;AAC7E,oDAAoD;AAEpD;;;;;;;;;;GAUG;AAEH,kDAAkD;AAClD,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAEtD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAA;AAW5C,sEAAsE;AACtE,MAAM,CAAC,MAAM,cAAc,GAAiC,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,CAAC,CAAA;AAyLtG,4DAA4D;AAC5D,iDAAiD;AAEjD;;;;;;;;;;;GAWG;AAEH,gEAAgE;AAChE,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;AAEhD,gFAAgF;AAChF,MAAM,CAAC,MAAM,sBAAsB,GAAG,YAAY,CAAA;AA6GlD,iDAAiD;AACjD,uDAAuD;AAEvD;;;;;;;;;;;;;;;GAeG;AAEH,iEAAiE;AACjE,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAA;AA0B1D,qDAAqD;AACrD,0DAA0D;AAE1D,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;AAuChD,yEAAyE;AAEzE;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAEtD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,yBAAyB,CAAA"}
|
package/lib/host-copy.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host copy direction: the contract for text a HOST writes that the user
|
|
3
|
+
* reads.
|
|
4
|
+
*
|
|
5
|
+
* Browser faces learn their language from dsh's `ctx.locale`, which the host
|
|
6
|
+
* cannot see. Host-composed text — route refusals, device errors, the delegated
|
|
7
|
+
* request a peer's model reads, a command's result line — therefore needs its
|
|
8
|
+
* own statement of "which language", and it needs it from the browser that is
|
|
9
|
+
* actually on screen. Two signals stand in for it, in order: what the browser
|
|
10
|
+
* last reported to {@link DSHELL_LOCALE_PATH} (exact, and it covers a language
|
|
11
|
+
* the browser picked for itself when the user never chose one), then the durable
|
|
12
|
+
* `locale.preference` dsh's own Language row writes. With neither, the host
|
|
13
|
+
* writes the project's source-of-truth language, `zh`.
|
|
14
|
+
*
|
|
15
|
+
* The rules here follow this package's layering: declaration only — the shapes
|
|
16
|
+
* and the path — with no imports, no dsh packages and no runtime state. The
|
|
17
|
+
* service that RESOLVES the language is provided by `dshell-mode`
|
|
18
|
+
* (`ctx.dshellHostCopy`), so every package binds its own host dictionaries to it
|
|
19
|
+
* instead of re-implementing the lookup.
|
|
20
|
+
*/
|
|
21
|
+
/** Where the browser reports the locale it actually resolved. */
|
|
22
|
+
export const DSHELL_LOCALE_PATH = '/api/dshell/locale';
|
|
23
|
+
/** The languages a host message can be written in, in report order. */
|
|
24
|
+
export const HOST_LOCALE_IDS = ['zh', 'en'];
|
|
25
|
+
//# sourceMappingURL=host-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-copy.js","sourceRoot":"","sources":["../src/host-copy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAA;AAEtD,uEAAuE;AACvE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,IAAI,CAAU,CAAA"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dshell's standard layer.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is shared machinery that must not belong to any one feature:
|
|
5
|
+
* the wire contracts ({@link ./contracts.ts}), the host copy direction
|
|
6
|
+
* ({@link ./host-copy.ts}) that a host half binds its own dictionaries to, the
|
|
7
|
+
* storage contract ({@link ./storage.ts}) that a feature calls without knowing
|
|
8
|
+
* its medium, and as the refactor continues, the dsh seam adapters (route
|
|
9
|
+
* definition, session/world addressing, capability probing) that today live
|
|
10
|
+
* duplicated inside feature packages.
|
|
11
|
+
*
|
|
12
|
+
* The rule that makes this layer worth having: it is the ONLY dshell package
|
|
13
|
+
* allowed to care about how dsh spells things. A feature package imports from
|
|
14
|
+
* here and keeps to its own product logic, so a dsh interface change is
|
|
15
|
+
* absorbed in one place instead of re-implemented per plugin.
|
|
16
|
+
*/
|
|
17
|
+
export * from './contracts.js';
|
|
18
|
+
export * from './host-copy.js';
|
|
19
|
+
export * from './shell-line.js';
|
|
20
|
+
export * from './storage.js';
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA"}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell line, tokenized and classified — the rule table behind Tab.
|
|
3
|
+
*
|
|
4
|
+
* The composer's completion has to know WHICH position the caret is in before it
|
|
5
|
+
* can know what to offer, because a position is a property of the LINE, not of
|
|
6
|
+
* the word: `dock` is a command in `dock`, an argument in `cd dock`, and a file
|
|
7
|
+
* in `tee > dock`. bash's own grammar is what defines it (the start of a *simple
|
|
8
|
+
* command*), and that start is introduced by an operator, a keyword, or a prefix
|
|
9
|
+
* word — never by "it happens to be first".
|
|
10
|
+
*
|
|
11
|
+
* This module is that rule table, kept pure: no services, no filesystem, no
|
|
12
|
+
* process. Both halves of the feature depend on it — the host decides which
|
|
13
|
+
* source answers, the browser decides when an empty answer is worth showing —
|
|
14
|
+
* and `packages/dshell/std/tests/shell-line.spec.ts` drives it with real lines.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT a shell parser. bash's grammar is unbounded (`eval`, nested
|
|
17
|
+
* substitutions, heredocs, functions), so this walk answers the shapes it can
|
|
18
|
+
* PROVE and admits what it cannot: a blank line or an operator under the caret
|
|
19
|
+
* reports "nothing to complete" rather than guessing, and the caller decides
|
|
20
|
+
* what no-answer means. There is no "maybe" position — a caller cannot act on
|
|
21
|
+
* one, and a wrong KIND of candidate (`-launcher.sh` offered for `-la<Tab>`) is
|
|
22
|
+
* worse than silence.
|
|
23
|
+
*/
|
|
24
|
+
/** Characters that end a word. */
|
|
25
|
+
const BLANKS = new Set([' ', '\t', '\n', '\r']);
|
|
26
|
+
/**
|
|
27
|
+
* Words after which a simple command begins, so the NEXT word is that command's
|
|
28
|
+
* name: wrapper commands (`sudo`, `env`, `time`, `xargs`, … — bash's own
|
|
29
|
+
* bash-completion registers several of these, e.g. `complete -F _comp_command
|
|
30
|
+
* time xargs nohup`) and the keywords that open a command list.
|
|
31
|
+
*/
|
|
32
|
+
const BEFORE_COMMAND = new Set([
|
|
33
|
+
'if', 'then', 'else', 'elif', 'while', 'until', 'do', '!', 'time', '{',
|
|
34
|
+
'sudo', 'doas', 'env', 'nice', 'ionice', 'nohup', 'setsid', 'strace',
|
|
35
|
+
'command', 'builtin', 'exec', 'xargs', 'watch', 'timeout', 'noglob', 'nocorrect',
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Keywords that END a command list rather than open one. `in` is here on
|
|
39
|
+
* purpose: what follows it are words (`for f in a b`), not a command.
|
|
40
|
+
*
|
|
41
|
+
* The walk does not need this split — a keyword in the command position is never
|
|
42
|
+
* the command itself, whatever list it belongs to — so this set only decides
|
|
43
|
+
* which words are tokens of kind `reserved` rather than plain words.
|
|
44
|
+
*/
|
|
45
|
+
const AFTER_COMMAND = new Set(['fi', 'done', 'esac', '}', 'in', 'case', 'select', 'function', 'for']);
|
|
46
|
+
/** One operator form. Order matters: the longest/dup forms must be tried first. */
|
|
47
|
+
const OPERATORS = [
|
|
48
|
+
{ re: /^&&/u, kind: 'separator', target: false },
|
|
49
|
+
{ re: /^\|\|/u, kind: 'separator', target: false },
|
|
50
|
+
{ re: /^\|&/u, kind: 'separator', target: false },
|
|
51
|
+
{ re: /^;/u, kind: 'separator', target: false },
|
|
52
|
+
{ re: /^\|/u, kind: 'separator', target: false },
|
|
53
|
+
// A file-descriptor duplication (`2>&1`, `>&2`) names a descriptor, not a
|
|
54
|
+
// file — it must be matched before the plain redirections below.
|
|
55
|
+
{ re: /^[0-9]*>&[0-9-]+/u, kind: 'redirection', target: false },
|
|
56
|
+
{ re: /^&>>?/u, kind: 'redirection', target: true },
|
|
57
|
+
{ re: /^&/u, kind: 'separator', target: false },
|
|
58
|
+
{ re: /^[0-9]*(?:>>|>\||>\|?|<[<>]?)/u, kind: 'redirection', target: true },
|
|
59
|
+
{ re: /^\$\(/u, kind: 'open', target: false },
|
|
60
|
+
{ re: /^\(/u, kind: 'open', target: false },
|
|
61
|
+
{ re: /^\)/u, kind: 'close', target: false },
|
|
62
|
+
{ re: /^`/u, kind: 'open', target: false },
|
|
63
|
+
];
|
|
64
|
+
/** The operator at `index`, if any. */
|
|
65
|
+
function operatorAt(line, index) {
|
|
66
|
+
for (const form of OPERATORS) {
|
|
67
|
+
const matched = form.re.exec(line.slice(index));
|
|
68
|
+
if (matched !== null)
|
|
69
|
+
return { kind: form.kind, text: matched[0], target: form.target };
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/** Whether a bare word is an assignment rather than a name. */
|
|
74
|
+
function isAssignment(text) {
|
|
75
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/u.test(text);
|
|
76
|
+
}
|
|
77
|
+
/** The kind of a word, from its own shape alone (its position decides the rest). */
|
|
78
|
+
function kindOfWord(text, quoted) {
|
|
79
|
+
if (!quoted && (text === '{' || text === '}'))
|
|
80
|
+
return text === '{' ? 'open' : 'close';
|
|
81
|
+
// An assignment tests the RAW text, not the quoting: what makes `FOO='a b'`
|
|
82
|
+
// an assignment is its unquoted `name=` head, and the quotes only hide the
|
|
83
|
+
// value. Quoting the NAME (`'FOO'=a`) makes it an ordinary word, which is
|
|
84
|
+
// exactly what anchoring the test at offset 0 already gives.
|
|
85
|
+
if (isAssignment(text))
|
|
86
|
+
return 'assign';
|
|
87
|
+
if (quoted)
|
|
88
|
+
return 'word';
|
|
89
|
+
if (BEFORE_COMMAND.has(text) || AFTER_COMMAND.has(text))
|
|
90
|
+
return 'reserved';
|
|
91
|
+
return 'word';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Split one line into tokens, stopping at nothing: a completion also runs on a
|
|
95
|
+
* half-written line, so quotes may be open and the last word may be cut off.
|
|
96
|
+
*
|
|
97
|
+
* Quote handling is what a completion needs, not what a shell needs: whitespace
|
|
98
|
+
* inside quotes does not split, an unterminated quote keeps its token whole (the
|
|
99
|
+
* reader is still typing it), and the operators that are not separated by spaces
|
|
100
|
+
* (`a>b`, `x;y`) still split — which the word scanner does by asking
|
|
101
|
+
* {@link operatorAt} before consuming a character.
|
|
102
|
+
*
|
|
103
|
+
* @param line - the line as written.
|
|
104
|
+
* @returns the tokens in order, with their offsets.
|
|
105
|
+
*/
|
|
106
|
+
export function tokenizeShellLine(line) {
|
|
107
|
+
const tokens = [];
|
|
108
|
+
let index = 0;
|
|
109
|
+
while (index < line.length) {
|
|
110
|
+
if (BLANKS.has(line[index])) {
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const operator = operatorAt(line, index);
|
|
115
|
+
if (operator !== undefined) {
|
|
116
|
+
tokens.push({
|
|
117
|
+
kind: operator.kind, text: operator.text, start: index, end: index + operator.text.length,
|
|
118
|
+
quoted: false, target: operator.target,
|
|
119
|
+
});
|
|
120
|
+
index += operator.text.length;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const start = index;
|
|
124
|
+
let text = '';
|
|
125
|
+
let quoted = false;
|
|
126
|
+
while (index < line.length) {
|
|
127
|
+
const char = line[index];
|
|
128
|
+
if (BLANKS.has(char))
|
|
129
|
+
break;
|
|
130
|
+
if (char === '\\' && index + 1 < line.length) {
|
|
131
|
+
text += line.slice(index, index + 2);
|
|
132
|
+
index += 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === '\'' || char === '"' || char === '`') {
|
|
136
|
+
quoted = true;
|
|
137
|
+
text += char;
|
|
138
|
+
index += 1;
|
|
139
|
+
while (index < line.length && line[index] !== char) {
|
|
140
|
+
if (char === '"' && line[index] === '\\' && index + 1 < line.length) {
|
|
141
|
+
text += line.slice(index, index + 2);
|
|
142
|
+
index += 2;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
text += line[index];
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
// An unterminated quote is a word still being written: keep it whole.
|
|
149
|
+
if (index < line.length) {
|
|
150
|
+
text += char;
|
|
151
|
+
index += 1;
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (operatorAt(line, index) !== undefined)
|
|
156
|
+
break;
|
|
157
|
+
text += char;
|
|
158
|
+
index += 1;
|
|
159
|
+
}
|
|
160
|
+
if (text.length === 0) {
|
|
161
|
+
index += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
tokens.push({ kind: kindOfWord(text, quoted), text, start, end: index, quoted, target: false });
|
|
165
|
+
}
|
|
166
|
+
return tokens;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Walk the tokens before the caret and report what the line expects there.
|
|
170
|
+
*
|
|
171
|
+
* `command` is the command the caret's word belongs to, so it is undefined
|
|
172
|
+
* wherever the line has not named one YET — which is why every operator, and
|
|
173
|
+
* every keyword sitting where a command would go, clears it again.
|
|
174
|
+
*
|
|
175
|
+
* @param tokens - the line's tokens, in order.
|
|
176
|
+
* @param index - the token under the caret, or the token count for a word that
|
|
177
|
+
* has not been typed yet (an empty token after whitespace).
|
|
178
|
+
* @returns the position, and the command it belongs to when there is one.
|
|
179
|
+
*/
|
|
180
|
+
export function classifyShellPosition(tokens, index) {
|
|
181
|
+
let expecting = 'command';
|
|
182
|
+
let command;
|
|
183
|
+
for (let at = 0; at < index && at < tokens.length; at += 1) {
|
|
184
|
+
const token = tokens[at];
|
|
185
|
+
if (token.kind === 'redirection') {
|
|
186
|
+
// A duplication (`2>&1`) names no file, so it leaves the expectation
|
|
187
|
+
// alone; only a real redirection opens the file slot that follows it.
|
|
188
|
+
expecting = token.target ? 'file' : expecting;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (token.kind === 'separator' || token.kind === 'open') {
|
|
192
|
+
expecting = 'command';
|
|
193
|
+
command = undefined;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (token.kind === 'close') {
|
|
197
|
+
expecting = 'argument';
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (token.kind === 'assign')
|
|
201
|
+
continue;
|
|
202
|
+
if (token.kind === 'reserved' && expecting === 'command') {
|
|
203
|
+
// A keyword or a wrapper word in the command position does not become the
|
|
204
|
+
// command: `if`/`then`/`do` open a list and `fi`/`done` close one (either
|
|
205
|
+
// way a command follows), while `sudo`/`env`/`time` hand the position on
|
|
206
|
+
// to the next word. So all of them clear `command` and keep waiting.
|
|
207
|
+
command = undefined;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (expecting === 'command') {
|
|
211
|
+
command = token.text;
|
|
212
|
+
expecting = 'argument';
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (expecting === 'file')
|
|
216
|
+
expecting = 'argument';
|
|
217
|
+
}
|
|
218
|
+
const caret = tokens[index];
|
|
219
|
+
// No word under the caret yet: the expectation IS the answer, with the file
|
|
220
|
+
// slot reported as the `redir` position it always was.
|
|
221
|
+
if (caret === undefined)
|
|
222
|
+
return { position: expecting === 'file' ? 'redir' : expecting, command };
|
|
223
|
+
if (expecting === 'file')
|
|
224
|
+
return { position: 'redir', command };
|
|
225
|
+
if (expecting === 'command')
|
|
226
|
+
return { position: 'command', command };
|
|
227
|
+
if (caret.kind === 'word' && caret.text.length > 1 && caret.text.startsWith('-')) {
|
|
228
|
+
return { position: 'flag', command };
|
|
229
|
+
}
|
|
230
|
+
return { position: 'argument', command };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Everything the completion needs to know about one caret: where the word is,
|
|
234
|
+
* which part of it is the name, and what the line expects there.
|
|
235
|
+
*
|
|
236
|
+
* @param line - the line as written (the draft).
|
|
237
|
+
* @param cursor - the caret offset within it.
|
|
238
|
+
* @returns the caret reading, or undefined when there is nothing to complete —
|
|
239
|
+
* a blank line, or an operator under the caret, which names no word (a
|
|
240
|
+
* redirection WITH a target is the exception: `ls >` is a file slot with an
|
|
241
|
+
* empty word in it, and the reader reports it as such).
|
|
242
|
+
*/
|
|
243
|
+
export function readShellCaret(line, cursor) {
|
|
244
|
+
const before = line.slice(0, Math.max(0, Math.min(cursor, line.length)));
|
|
245
|
+
const tokens = tokenizeShellLine(before);
|
|
246
|
+
const last = tokens[tokens.length - 1];
|
|
247
|
+
let index = tokens.length;
|
|
248
|
+
let start = before.length;
|
|
249
|
+
let dirPart = '';
|
|
250
|
+
let prefix = '';
|
|
251
|
+
let empty = true;
|
|
252
|
+
if (last !== undefined && last.end === before.length && last.kind === 'redirection' && last.target) {
|
|
253
|
+
// `ls >` — the operator is complete and the word it names is empty, so the
|
|
254
|
+
// walk (which now sees that redirection) reports the file slot. bash reads
|
|
255
|
+
// it the same way: Tab right after `>` lists the directory.
|
|
256
|
+
}
|
|
257
|
+
else if (last !== undefined && last.end === before.length) {
|
|
258
|
+
// An operator under the caret names nothing: there is no word being typed.
|
|
259
|
+
if (last.kind !== 'word' && last.kind !== 'assign' && last.kind !== 'reserved')
|
|
260
|
+
return undefined;
|
|
261
|
+
const slash = last.text.lastIndexOf('/');
|
|
262
|
+
dirPart = slash < 0 ? '' : last.text.slice(0, slash + 1);
|
|
263
|
+
prefix = last.text.slice(slash + 1);
|
|
264
|
+
start = last.start + dirPart.length;
|
|
265
|
+
index = tokens.length - 1;
|
|
266
|
+
empty = false;
|
|
267
|
+
}
|
|
268
|
+
else if (before.trim().length === 0) {
|
|
269
|
+
// A blank line completes nothing, which is what a terminal does too.
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
const { position, command } = classifyShellPosition(tokens, index);
|
|
273
|
+
return { position, command, start, end: before.length, dirPart, prefix, empty };
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=shell-line.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-line.js","sourceRoot":"","sources":["../src/shell-line.ts"],"names":[],"mappings":"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"}
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storage contract: what a dshell store *is*, with no medium attached.
|
|
3
|
+
*
|
|
4
|
+
* dshell has one durable store for shell history today and a reason to expect
|
|
5
|
+
* more (agent-facing records that want indexes). The split that keeps that from
|
|
6
|
+
* becoming each feature's private file format:
|
|
7
|
+
*
|
|
8
|
+
* - **the contract** lives here — the record shape, the store surface, the
|
|
9
|
+
* on-disk naming and layout version, and the failure vocabulary;
|
|
10
|
+
* - **the medium** lives in `@nexus-aethra/dshell-storage` (`node:sqlite`);
|
|
11
|
+
* - **a feature** only calls the store, and never names a file, a pragma or a
|
|
12
|
+
* schema.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here may import a Node builtin: `std` is inlined into client bundles,
|
|
15
|
+
* so this file has to stay an isomorphic declaration. The engine is host-only
|
|
16
|
+
* for exactly that reason — `node:sqlite` cannot cross into a browser bundle.
|
|
17
|
+
*/
|
|
18
|
+
/** File name of the shared database inside a log directory. */
|
|
19
|
+
export const HISTORY_STORE_FILENAME = 'history.sqlite';
|
|
20
|
+
/**
|
|
21
|
+
* Physical layout version of the history database, stamped in
|
|
22
|
+
* `PRAGMA user_version`.
|
|
23
|
+
*
|
|
24
|
+
* The engine migrates the layouts it knows: 1 dropped `commands_at(at)`, an
|
|
25
|
+
* index for a cross-session time query that was never built and that nothing
|
|
26
|
+
* read; 2 added `command_output`, the per-command output the agent-facing read
|
|
27
|
+
* tool addresses by offset. A database stamped with anything else is rejected:
|
|
28
|
+
* this code is the only producer, and history is a convenience the user may
|
|
29
|
+
* lose without losing a session.
|
|
30
|
+
*/
|
|
31
|
+
export const HISTORY_STORE_SCHEMA_VERSION = 3;
|
|
32
|
+
/** A store that cannot be opened, or was written by another layout. */
|
|
33
|
+
export class HistoryStoreError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
constructor(code, message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = 'HistoryStoreError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,sBAAsB,GAAG,gBAAgB,CAAA;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAA;AAK7C,uEAAuE;AACvE,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACrB;IAArB,YAAqB,IAA2B,EAAE,OAAe;QAC/D,KAAK,CAAC,OAAO,CAAC,CAAA;QADK,SAAI,GAAJ,IAAI,CAAuB;QAE9C,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;IACjC,CAAC;CACF"}
|