@dolusoft/command-protocol 0.0.0-stage → 1.0.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.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dolusoft
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/README.md CHANGED
@@ -1,3 +1,75 @@
1
- # Temporary Holding Version
1
+ # @dolusoft/command-protocol
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The JSON types of [Command Console](https://github.com/dolusoft/command-console) and its line tokenizer. No
4
+ dependencies, no framework, no DOM.
5
+
6
+ ```sh
7
+ pnpm add @dolusoft/command-protocol
8
+ ```
9
+
10
+ Most consumers use it through `@dolusoft/command-console` (which re-exports its types) or
11
+ `@dolusoft/command-console-core`. Use it directly when a server or a test builds console output, or to split a line the way the console does.
12
+
13
+ ## Types
14
+
15
+ Every type here is plain JSON: finite numbers only, no `undefined` (an optional field without a value is absent), and
16
+ a value survives `JSON.parse(JSON.stringify(x))` unchanged. `JsonValue`, `JsonPrimitive` and `isJsonValue` describe and
17
+ check it. Functions (a command's `run` and `complete`, callbacks) live in the core, never here.
18
+
19
+ - **Entries** of the scrollback: `ConsoleEntry` is an `InputEntry` (`id`, `prompt` when the line ended, `text` or
20
+ `null` when redacted, `redacted`, `status: 'submitted' | 'cancelled'`) or an `OutputEntry` (`id`,
21
+ `tone: 'default' | 'error' | 'notice'`, `blocks`, optional `inputId` of the line it answers, optional `quiet: true`
22
+ for output a live region should not read out).
23
+ - **Blocks** of an output entry (`OutputBlock`), never HTML:
24
+
25
+ | Block | Fields |
26
+ | --------- | -------------------------------------------------------------------- |
27
+ | `text` | `text` |
28
+ | `json` | `value: JsonValue` |
29
+ | `table` | `columns: { key, label }[]`, `rows: Record<string, JsonPrimitive>[]` |
30
+ | `list` | `items: string[]` |
31
+ | `message` | `code: MessageCode`, `params?` (made by the library) |
32
+ | `custom` | `kind`, `data?: JsonValue` (drawn by the consumer) |
33
+
34
+ - **Messages** (`MessageCode`, `MessageParams`): the library writes no text, only codes. `command-not-found`
35
+ (`{ name }`), `command-failed` (no params: the error's text is never shown), `unterminated-quote` (`{ index }`),
36
+ `interrupted`. For a sensitive line `name` and `index` are `null`.
37
+ - **Commands**: `CommandSpec` is the JSON part of a command (`name`, `aliases`, `summary`, `usage`, `hidden`,
38
+ `sensitive: boolean | { fromArg }`). `CommandResponse` is what a command may return (`output`, `tone`, `quiet`,
39
+ `effects`).
40
+ - **Effects** (`ConsoleEffect`): `clear`, `close`, `push-context` (`frame`), `pop-context` (`count?`, default 1),
41
+ `reset-context`, `clear-recall`. They apply in order, before the response's output.
42
+ - **Context frames** (`ContextFrame`): `{ id, prompt, data? }`. The top frame's `prompt` is the prompt of the next line.
43
+
44
+ ## Line syntax
45
+
46
+ Whitespace (tabs included) separates tokens. `'…'` groups literally. `"…"` groups, and inside it `\"` and `\\` are the
47
+ only escapes. Outside quotes `\` is an ordinary character, so `C:\data` stays `C:\data`. Adjacent parts join one token
48
+ (`a"b c"d` is `ab cd`); `''` and `""` give an empty token. There is no line continuation.
49
+
50
+ ```ts
51
+ import { quoteArgument, tokenize } from '@dolusoft/command-protocol'
52
+
53
+ tokenize(`set title "Hello \\"world\\""`).tokens.map(t => t.value)
54
+ // ['set', 'title', 'Hello "world"']
55
+
56
+ const result = tokenize(`set title 'Hello`)
57
+ // result.ok === false, result.error: { code: 'unterminated-quote', index: 10 }
58
+ // result.tokens still holds the tolerant tokens; the last one has open: true
59
+
60
+ const args = ['set', 'path', 'C:\\Program Files']
61
+ const line = args.map(quoteArgument).join(' ')
62
+ // "set path 'C:\\Program Files'", which tokenizes back to the same three values
63
+ ```
64
+
65
+ Each `Token` has its `value` (quotes removed), `start` and `end` (UTF-16 offsets into the line, `end` exclusive,
66
+ covering the raw text including its quotes), `quoted` and `open`. An unclosed quote gives `ok: false` with the index of
67
+ the opening quote, and the tokens are still returned: the last one is `open` and runs to the end of the line. The
68
+ console refuses such a line; completion works on the tolerant tokens. Nothing is ever dropped.
69
+
70
+ `quoteArgument(value)` returns the shortest of: the bare value, `'value'`, or `"value"` with escapes; it always
71
+ tokenizes back to `value`.
72
+
73
+ ## License
74
+
75
+ MIT, see [LICENSE.md](./LICENSE.md).
@@ -0,0 +1,166 @@
1
+ // src/json.ts
2
+ /**
3
+ * Whether `value` is a JSON value that survives
4
+ * `JSON.parse(JSON.stringify(value))` unchanged: strings, booleans, `null`,
5
+ * finite numbers, dense arrays and plain objects (prototype `Object.prototype`
6
+ * or `null`) of such values, without cycles. `undefined`, functions, symbols,
7
+ * bigints, class instances (`Date`, `Map`, …), sparse arrays and non-finite
8
+ * numbers are not. `-0` is accepted; it reads back as `0`.
9
+ */
10
+ function isJsonValue(value) {
11
+ return check(value, []);
12
+ }
13
+ function check(value, path) {
14
+ if (value === null) return true;
15
+ switch (typeof value) {
16
+ case "string":
17
+ case "boolean": return true;
18
+ case "number": return Number.isFinite(value);
19
+ case "object": break;
20
+ default: return false;
21
+ }
22
+ const object = value;
23
+ if (path.includes(object)) return false;
24
+ path.push(object);
25
+ const result = Array.isArray(object) ? checkArray(object, path) : checkObject(object, path);
26
+ path.pop();
27
+ return result;
28
+ }
29
+ function checkArray(array, path) {
30
+ if (Object.getPrototypeOf(array) !== Array.prototype) return false;
31
+ for (let i = 0; i < array.length; i++) if (!(i in array) || !check(array[i], path)) return false;
32
+ return true;
33
+ }
34
+ function checkObject(object, path) {
35
+ const prototype = Object.getPrototypeOf(object);
36
+ if (prototype !== Object.prototype && prototype !== null) return false;
37
+ if (Object.getOwnPropertySymbols(object).length > 0) return false;
38
+ const record = object;
39
+ for (const key of Object.keys(record)) if (!check(record[key], path)) return false;
40
+ return true;
41
+ }
42
+ // src/syntax/tokenize.ts
43
+ var whitespace = /\s/;
44
+ /** Whether `char` separates tokens (`\s`, so tabs and line breaks too). */
45
+ function isSeparator(char) {
46
+ return whitespace.test(char);
47
+ }
48
+ /**
49
+ * Splits a line into tokens. Runs of whitespace separate. `'…'` groups
50
+ * literally. `"…"` groups, and inside it `\"` and `\\` are the only escapes;
51
+ * any other `\` is kept. Outside quotes `\` is an ordinary character. Adjacent
52
+ * parts join one token (`a"b c"d` is `ab cd`); `''` and `""` give an empty
53
+ * token. `start`/`end` cover the raw text of each token, quotes included.
54
+ */
55
+ function tokenize(line) {
56
+ const tokens = [];
57
+ let error = null;
58
+ let i = 0;
59
+ const length = line.length;
60
+ while (i < length) {
61
+ if (isSeparator(line[i])) {
62
+ i++;
63
+ continue;
64
+ }
65
+ const start = i;
66
+ let value = "";
67
+ let quoted = false;
68
+ let open = false;
69
+ while (i < length && !isSeparator(line[i])) {
70
+ const char = line[i];
71
+ if (char === "'") {
72
+ quoted = true;
73
+ const close = line.indexOf("'", i + 1);
74
+ if (close === -1) {
75
+ value += line.slice(i + 1);
76
+ open = true;
77
+ error = {
78
+ code: "unterminated-quote",
79
+ index: i
80
+ };
81
+ i = length;
82
+ break;
83
+ }
84
+ value += line.slice(i + 1, close);
85
+ i = close + 1;
86
+ } else if (char === "\"") {
87
+ quoted = true;
88
+ const opening = i;
89
+ i++;
90
+ let closed = false;
91
+ while (i < length) {
92
+ const inner = line[i];
93
+ if (inner === "\"") {
94
+ closed = true;
95
+ i++;
96
+ break;
97
+ }
98
+ const next = line.charAt(i + 1);
99
+ if (inner === "\\" && (next === "\"" || next === "\\")) {
100
+ value += next;
101
+ i += 2;
102
+ } else {
103
+ value += inner;
104
+ i++;
105
+ }
106
+ }
107
+ if (!closed) {
108
+ open = true;
109
+ error = {
110
+ code: "unterminated-quote",
111
+ index: opening
112
+ };
113
+ break;
114
+ }
115
+ } else {
116
+ value += char;
117
+ i++;
118
+ }
119
+ }
120
+ tokens.push({
121
+ value,
122
+ start,
123
+ end: i,
124
+ quoted,
125
+ open
126
+ });
127
+ }
128
+ return error === null ? {
129
+ ok: true,
130
+ tokens
131
+ } : {
132
+ ok: false,
133
+ tokens,
134
+ error
135
+ };
136
+ }
137
+ // src/syntax/quote.ts
138
+ /**
139
+ * The shortest form of `value` that `tokenize` reads back as one token with
140
+ * that value: bare when it is non-empty and has no whitespace and no quote,
141
+ * else `'…'` when it has no `'`, else `"…"` with `"` written as `\"` and a
142
+ * `\` written as `\\` where it comes before a `"`, a `\` or the closing quote.
143
+ */
144
+ function quoteArgument(value) {
145
+ if (value === "" || needsQuotes(value)) return value.includes("'") ? doubleQuote(value) : `'${value}'`;
146
+ return value;
147
+ }
148
+ function needsQuotes(value) {
149
+ for (const char of value) if (char === "'" || char === "\"" || isSeparator(char)) return true;
150
+ return false;
151
+ }
152
+ function doubleQuote(value) {
153
+ let result = "\"";
154
+ for (let i = 0; i < value.length; i++) {
155
+ const char = value[i];
156
+ if (char === "\"") result += "\\\"";
157
+ else if (char === "\\") {
158
+ const next = value.charAt(i + 1);
159
+ result += next === "" || next === "\"" || next === "\\" ? "\\\\" : "\\";
160
+ } else result += char;
161
+ }
162
+ return `${result}"`;
163
+ }
164
+ export { isJsonValue, quoteArgument, tokenize };
165
+
166
+ //# sourceMappingURL=command-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-protocol.js","names":[],"sources":["../src/json.ts","../src/syntax/tokenize.ts","../src/syntax/quote.ts"],"sourcesContent":["/** A JSON leaf. Numbers are finite: `NaN` and the infinities are not JSON. */\nexport type JsonPrimitive = string | number | boolean | null\n\n/**\n * A JSON value: a primitive, an array of values or a plain object of values.\n * Every protocol type is made of these (P4).\n */\nexport type JsonValue =\n JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }\n\n/**\n * Whether `value` is a JSON value that survives\n * `JSON.parse(JSON.stringify(value))` unchanged: strings, booleans, `null`,\n * finite numbers, dense arrays and plain objects (prototype `Object.prototype`\n * or `null`) of such values, without cycles. `undefined`, functions, symbols,\n * bigints, class instances (`Date`, `Map`, …), sparse arrays and non-finite\n * numbers are not. `-0` is accepted; it reads back as `0`.\n */\nexport function isJsonValue(value: unknown): value is JsonValue {\n return check(value, [])\n}\n\nfunction check(value: unknown, path: object[]): boolean {\n if (value === null) {\n return true\n }\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return true\n case 'number':\n return Number.isFinite(value)\n case 'object':\n break\n default:\n return false\n }\n const object = value\n if (path.includes(object)) {\n return false\n }\n path.push(object)\n const result = Array.isArray(object)\n ? checkArray(object as unknown[], path)\n : checkObject(object, path)\n path.pop()\n return result\n}\n\nfunction checkArray(array: unknown[], path: object[]): boolean {\n if (Object.getPrototypeOf(array) !== Array.prototype) {\n return false\n }\n for (let i = 0; i < array.length; i++) {\n if (!(i in array) || !check(array[i], path)) {\n return false\n }\n }\n return true\n}\n\nfunction checkObject(object: object, path: object[]): boolean {\n const prototype: unknown = Object.getPrototypeOf(object)\n if (prototype !== Object.prototype && prototype !== null) {\n return false\n }\n if (Object.getOwnPropertySymbols(object).length > 0) {\n return false\n }\n const record = object as Record<string, unknown>\n for (const key of Object.keys(record)) {\n if (!check(record[key], path)) {\n return false\n }\n }\n return true\n}\n","// The line syntax (ADR 0007): shell-like quoting, `\\` literal outside double\n// quotes, no continuation. The tokenizer never drops a character: every\n// non-whitespace character of the line is inside exactly one token's span.\n\n/** One token of a line. */\nexport interface Token {\n /** The token's text with grouping quotes and escapes removed. */\n value: string\n /** UTF-16 offset of the token's first raw character in the line. */\n start: number\n /** UTF-16 offset after the token's last raw character (exclusive). */\n end: number\n /** True when any part of the token is quoted (`'…'` or `\"…\"`). */\n quoted: boolean\n /**\n * True when the token ends in a quote that is not closed; it then runs to\n * the end of the line. Only the last token can be open.\n */\n open: boolean\n}\n\n/** The error of a line whose quote is not closed. */\nexport interface TokenizeError {\n code: 'unterminated-quote'\n /** UTF-16 offset of the opening quote that is not closed. */\n index: number\n}\n\n/**\n * The tokens of a line. With `ok: false` the tokens are still returned\n * (tolerant): the last one has `open: true`. Dispatch refuses such a line;\n * completion works on its tokens.\n */\nexport type TokenizeResult =\n | { ok: true; tokens: Token[] }\n | { ok: false; tokens: Token[]; error: TokenizeError }\n\nconst whitespace = /\\s/\n\n/** Whether `char` separates tokens (`\\s`, so tabs and line breaks too). */\nexport function isSeparator(char: string): boolean {\n return whitespace.test(char)\n}\n\n/**\n * Splits a line into tokens. Runs of whitespace separate. `'…'` groups\n * literally. `\"…\"` groups, and inside it `\\\"` and `\\\\` are the only escapes;\n * any other `\\` is kept. Outside quotes `\\` is an ordinary character. Adjacent\n * parts join one token (`a\"b c\"d` is `ab cd`); `''` and `\"\"` give an empty\n * token. `start`/`end` cover the raw text of each token, quotes included.\n */\nexport function tokenize(line: string): TokenizeResult {\n const tokens: Token[] = []\n let error: TokenizeError | null = null\n let i = 0\n const length = line.length\n\n while (i < length) {\n if (isSeparator(line[i])) {\n i++\n continue\n }\n const start = i\n let value = ''\n let quoted = false\n let open = false\n\n while (i < length && !isSeparator(line[i])) {\n const char = line[i]\n if (char === \"'\") {\n quoted = true\n const close = line.indexOf(\"'\", i + 1)\n if (close === -1) {\n value += line.slice(i + 1)\n open = true\n error = { code: 'unterminated-quote', index: i }\n i = length\n break\n }\n value += line.slice(i + 1, close)\n i = close + 1\n } else if (char === '\"') {\n quoted = true\n const opening = i\n i++\n let closed = false\n while (i < length) {\n const inner = line[i]\n if (inner === '\"') {\n closed = true\n i++\n break\n }\n const next = line.charAt(i + 1)\n if (inner === '\\\\' && (next === '\"' || next === '\\\\')) {\n value += next\n i += 2\n } else {\n value += inner\n i++\n }\n }\n if (!closed) {\n open = true\n error = { code: 'unterminated-quote', index: opening }\n break\n }\n } else {\n value += char\n i++\n }\n }\n\n tokens.push({ value, start, end: i, quoted, open })\n }\n\n return error === null ? { ok: true, tokens } : { ok: false, tokens, error }\n}\n","import { isSeparator } from './tokenize'\n\n/**\n * The shortest form of `value` that `tokenize` reads back as one token with\n * that value: bare when it is non-empty and has no whitespace and no quote,\n * else `'…'` when it has no `'`, else `\"…\"` with `\"` written as `\\\"` and a\n * `\\` written as `\\\\` where it comes before a `\"`, a `\\` or the closing quote.\n */\nexport function quoteArgument(value: string): string {\n if (value === '' || needsQuotes(value)) {\n return value.includes(\"'\") ? doubleQuote(value) : `'${value}'`\n }\n return value\n}\n\nfunction needsQuotes(value: string): boolean {\n for (const char of value) {\n if (char === \"'\" || char === '\"' || isSeparator(char)) {\n return true\n }\n }\n return false\n}\n\nfunction doubleQuote(value: string): string {\n let result = '\"'\n for (let i = 0; i < value.length; i++) {\n const char = value[i]\n if (char === '\"') {\n result += '\\\\\"'\n } else if (char === '\\\\') {\n const next = value.charAt(i + 1)\n result += next === '' || next === '\"' || next === '\\\\' ? '\\\\\\\\' : '\\\\'\n } else {\n result += char\n }\n }\n return `${result}\"`\n}\n"],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import type { JsonPrimitive, JsonValue } from './json.js';
2
+ /** Plain text, rendered preformatted. */
3
+ export interface TextBlock {
4
+ type: 'text';
5
+ text: string;
6
+ }
7
+ /** A JSON value, rendered as `JSON.stringify(value, null, 2)`. */
8
+ export interface JsonBlock {
9
+ type: 'json';
10
+ value: JsonValue;
11
+ }
12
+ /** One column of a table block: the row key it reads and its header text. */
13
+ export interface TableColumn {
14
+ key: string;
15
+ label: string;
16
+ }
17
+ /** A table: a header row from `columns`, then one row per `rows` item. */
18
+ export interface TableBlock {
19
+ type: 'table';
20
+ columns: TableColumn[];
21
+ rows: Record<string, JsonPrimitive>[];
22
+ }
23
+ /** A list of text items. */
24
+ export interface ListBlock {
25
+ type: 'list';
26
+ items: string[];
27
+ }
28
+ /**
29
+ * A message the library makes. It carries a code, never text: the Vue package
30
+ * renders it from its labels (P7).
31
+ */
32
+ export interface MessageBlock {
33
+ type: 'message';
34
+ code: MessageCode;
35
+ params?: Record<string, JsonPrimitive>;
36
+ }
37
+ /** A block the consumer renders itself, told apart by `kind`. */
38
+ export interface CustomBlock {
39
+ type: 'custom';
40
+ kind: string;
41
+ data?: JsonValue;
42
+ }
43
+ /** One piece of an output entry. Output is never HTML (ADR 0005). */
44
+ export type OutputBlock = TextBlock | JsonBlock | TableBlock | ListBlock | MessageBlock | CustomBlock;
45
+ /**
46
+ * The messages the library makes:
47
+ *
48
+ * - `command-not-found`: the first token names no command.
49
+ * - `command-failed`: the command threw or rejected; the error's text is never
50
+ * shown (S-06).
51
+ * - `unterminated-quote`: the line has a quote that is not closed.
52
+ * - `interrupted`: a running command was cancelled.
53
+ */
54
+ export type MessageCode = 'command-not-found' | 'command-failed' | 'unterminated-quote' | 'interrupted';
55
+ /**
56
+ * The parameters of each message code. For a sensitive line `name` and
57
+ * `index` are `null`. A code without parameters has an empty record; its
58
+ * block carries no `params`.
59
+ */
60
+ export interface MessageParams {
61
+ 'command-not-found': {
62
+ name: string | null;
63
+ };
64
+ 'command-failed': Record<string, never>;
65
+ 'unterminated-quote': {
66
+ index: number | null;
67
+ };
68
+ interrupted: Record<string, never>;
69
+ }
@@ -0,0 +1,62 @@
1
+ import type { OutputBlock } from './block.js';
2
+ import type { ContextFrame } from './context.js';
3
+ import type { OutputEntry } from './entry.js';
4
+ /**
5
+ * The JSON part of a command: its names and how it is listed and redacted.
6
+ * The core adds the functions (`run`, `complete`) to it.
7
+ */
8
+ export interface CommandSpec {
9
+ /** Non-empty, no whitespace, no quote characters; unique with the aliases. */
10
+ name: string;
11
+ /** Other names that run the same command; the same rules as `name`. */
12
+ aliases?: string[];
13
+ /** One line on what the command does, for the consumer's help. */
14
+ summary?: string;
15
+ /** How the command is called, for the consumer's help. */
16
+ usage?: string;
17
+ /** Not listed by `getCommands` and not completed by name. */
18
+ hidden?: boolean;
19
+ /**
20
+ * Arguments at and after `fromArg` are secret, so the line is redacted and
21
+ * those positions are never completed. `true` is `{ fromArg: 0 }`.
22
+ */
23
+ sensitive?: boolean | {
24
+ fromArg: number;
25
+ };
26
+ }
27
+ /** What a command returns, as JSON. */
28
+ export interface CommandResponse {
29
+ /** A string is one text block. */
30
+ output?: string | OutputBlock[];
31
+ /** Default `'default'`. */
32
+ tone?: OutputEntry['tone'];
33
+ /** Copied to the output entry. */
34
+ quiet?: true;
35
+ /** Applied in order, before the output is appended. */
36
+ effects?: ConsoleEffect[];
37
+ }
38
+ /**
39
+ * A change a command asks the console to make:
40
+ *
41
+ * - `clear`: empty the entries.
42
+ * - `close`: ask the consumer to close the console.
43
+ * - `push-context`: push a frame on the context stack.
44
+ * - `pop-context`: pop `count` frames (default 1); stops at the root.
45
+ * - `reset-context`: return to the root.
46
+ * - `clear-recall`: empty the recall list.
47
+ */
48
+ export type ConsoleEffect = {
49
+ type: 'clear';
50
+ } | {
51
+ type: 'close';
52
+ } | {
53
+ type: 'push-context';
54
+ frame: ContextFrame;
55
+ } | {
56
+ type: 'pop-context';
57
+ count?: number;
58
+ } | {
59
+ type: 'reset-context';
60
+ } | {
61
+ type: 'clear-recall';
62
+ };
@@ -0,0 +1,14 @@
1
+ import type { JsonValue } from './json.js';
2
+ /**
3
+ * One level of the context stack. The stack runs bottom → top; the root is
4
+ * implicit and has no frame. The prompt of a new input entry is the top
5
+ * frame's `prompt`. What a frame means is the consumer's (`data`).
6
+ */
7
+ export interface ContextFrame {
8
+ /** Identifies the frame; a change of the id sequence is a context change. */
9
+ id: string;
10
+ /** The prompt shown while this frame is on top. */
11
+ prompt: string;
12
+ /** The consumer's data for this frame. */
13
+ data?: JsonValue;
14
+ }
@@ -0,0 +1,27 @@
1
+ import type { OutputBlock } from './block.js';
2
+ /** One entry of the scrollback: a line the user ended, or output. */
3
+ export type ConsoleEntry = InputEntry | OutputEntry;
4
+ /** A line the user ended, with the prompt it was ended with. */
5
+ export interface InputEntry {
6
+ id: string;
7
+ kind: 'input';
8
+ /** The prompt when the line ended. */
9
+ prompt: string;
10
+ /** The trimmed line; `null` when the line was sensitive. */
11
+ text: string | null;
12
+ /** True when the line was sensitive and its text is not kept. */
13
+ redacted: boolean;
14
+ /** `cancelled` when the line was ended with Ctrl+C. */
15
+ status: 'submitted' | 'cancelled';
16
+ }
17
+ /** What a command or the library wrote. */
18
+ export interface OutputEntry {
19
+ id: string;
20
+ kind: 'output';
21
+ tone: 'default' | 'error' | 'notice';
22
+ blocks: OutputBlock[];
23
+ /** The input entry this output answers. */
24
+ inputId?: string;
25
+ /** Not announced by the live region (long output such as a whole config). */
26
+ quiet?: true;
27
+ }
@@ -0,0 +1,9 @@
1
+ export type { JsonPrimitive, JsonValue } from './json.js';
2
+ export { isJsonValue } from './json.js';
3
+ export type { ConsoleEntry, InputEntry, OutputEntry } from './entry.js';
4
+ export type { CustomBlock, JsonBlock, ListBlock, MessageBlock, MessageCode, MessageParams, OutputBlock, TableBlock, TableColumn, TextBlock } from './block.js';
5
+ export type { CommandResponse, CommandSpec, ConsoleEffect } from './command.js';
6
+ export type { ContextFrame } from './context.js';
7
+ export type { Token, TokenizeError, TokenizeResult } from './syntax/tokenize.js';
8
+ export { tokenize } from './syntax/tokenize.js';
9
+ export { quoteArgument } from './syntax/quote.js';
@@ -0,0 +1,18 @@
1
+ /** A JSON leaf. Numbers are finite: `NaN` and the infinities are not JSON. */
2
+ export type JsonPrimitive = string | number | boolean | null;
3
+ /**
4
+ * A JSON value: a primitive, an array of values or a plain object of values.
5
+ * Every protocol type is made of these (P4).
6
+ */
7
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
8
+ [key: string]: JsonValue;
9
+ };
10
+ /**
11
+ * Whether `value` is a JSON value that survives
12
+ * `JSON.parse(JSON.stringify(value))` unchanged: strings, booleans, `null`,
13
+ * finite numbers, dense arrays and plain objects (prototype `Object.prototype`
14
+ * or `null`) of such values, without cycles. `undefined`, functions, symbols,
15
+ * bigints, class instances (`Date`, `Map`, …), sparse arrays and non-finite
16
+ * numbers are not. `-0` is accepted; it reads back as `0`.
17
+ */
18
+ export declare function isJsonValue(value: unknown): value is JsonValue;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The shortest form of `value` that `tokenize` reads back as one token with
3
+ * that value: bare when it is non-empty and has no whitespace and no quote,
4
+ * else `'…'` when it has no `'`, else `"…"` with `"` written as `\"` and a
5
+ * `\` written as `\\` where it comes before a `"`, a `\` or the closing quote.
6
+ */
7
+ export declare function quoteArgument(value: string): string;
@@ -0,0 +1,45 @@
1
+ /** One token of a line. */
2
+ export interface Token {
3
+ /** The token's text with grouping quotes and escapes removed. */
4
+ value: string;
5
+ /** UTF-16 offset of the token's first raw character in the line. */
6
+ start: number;
7
+ /** UTF-16 offset after the token's last raw character (exclusive). */
8
+ end: number;
9
+ /** True when any part of the token is quoted (`'…'` or `"…"`). */
10
+ quoted: boolean;
11
+ /**
12
+ * True when the token ends in a quote that is not closed; it then runs to
13
+ * the end of the line. Only the last token can be open.
14
+ */
15
+ open: boolean;
16
+ }
17
+ /** The error of a line whose quote is not closed. */
18
+ export interface TokenizeError {
19
+ code: 'unterminated-quote';
20
+ /** UTF-16 offset of the opening quote that is not closed. */
21
+ index: number;
22
+ }
23
+ /**
24
+ * The tokens of a line. With `ok: false` the tokens are still returned
25
+ * (tolerant): the last one has `open: true`. Dispatch refuses such a line;
26
+ * completion works on its tokens.
27
+ */
28
+ export type TokenizeResult = {
29
+ ok: true;
30
+ tokens: Token[];
31
+ } | {
32
+ ok: false;
33
+ tokens: Token[];
34
+ error: TokenizeError;
35
+ };
36
+ /** Whether `char` separates tokens (`\s`, so tabs and line breaks too). */
37
+ export declare function isSeparator(char: string): boolean;
38
+ /**
39
+ * Splits a line into tokens. Runs of whitespace separate. `'…'` groups
40
+ * literally. `"…"` groups, and inside it `\"` and `\\` are the only escapes;
41
+ * any other `\` is kept. Outside quotes `\` is an ordinary character. Adjacent
42
+ * parts join one token (`a"b c"d` is `ab cd`); `''` and `""` give an empty
43
+ * token. `start`/`end` cover the raw text of each token, quotes included.
44
+ */
45
+ export declare function tokenize(line: string): TokenizeResult;
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@dolusoft/command-protocol",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "The JSON of a command console: entries, output blocks, command specs, responses, effects, context frames, and the line tokenizer. No dependencies.",
5
+ "private": false,
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE.md"
11
+ ],
12
+ "sideEffects": false,
13
+ "main": "./dist/command-protocol.js",
14
+ "types": "./dist/types/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/types/index.d.ts",
18
+ "default": "./dist/command-protocol.js"
19
+ }
20
+ },
21
+ "engines": {
22
+ "node": ">=22.18"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/dolusoft/command-console.git",
27
+ "directory": "packages/command-protocol"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/dolusoft/command-console/issues"
31
+ },
32
+ "homepage": "https://github.com/dolusoft/command-console#readme",
33
+ "license": "MIT",
34
+ "scripts": {
35
+ "build": "vite build && tsc -p tsconfig.build.json && node ../../scripts/emit-type-extensions.mjs dist/types"
36
+ }
6
37
  }