@softov/ahpc 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/README.md +296 -0
- package/dist/src/ahp/channels.d.ts +146 -0
- package/dist/src/ahp/channels.js +423 -0
- package/dist/src/ahp/connection.d.ts +656 -0
- package/dist/src/ahp/connection.js +1 -0
- package/dist/src/ahp/fake.d.ts +69 -0
- package/dist/src/ahp/fake.js +1996 -0
- package/dist/src/ahp/live.d.ts +160 -0
- package/dist/src/ahp/live.js +2641 -0
- package/dist/src/ahp/operate.d.ts +36 -0
- package/dist/src/ahp/operate.js +56 -0
- package/dist/src/ahp/publish.d.ts +39 -0
- package/dist/src/ahp/publish.js +370 -0
- package/dist/src/ahp/status.d.ts +23 -0
- package/dist/src/ahp/status.js +23 -0
- package/dist/src/ahp/types.d.ts +745 -0
- package/dist/src/ahp/types.js +27 -0
- package/dist/src/app.d.ts +54 -0
- package/dist/src/app.js +369 -0
- package/dist/src/blocks.d.ts +88 -0
- package/dist/src/blocks.js +102 -0
- package/dist/src/cli/main.d.ts +13 -0
- package/dist/src/cli/main.js +1537 -0
- package/dist/src/cli/render.d.ts +43 -0
- package/dist/src/cli/render.js +91 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +45 -0
- package/dist/src/connect.d.ts +53 -0
- package/dist/src/connect.js +69 -0
- package/dist/src/control.d.ts +189 -0
- package/dist/src/control.js +2241 -0
- package/dist/src/diff.d.ts +44 -0
- package/dist/src/diff.js +111 -0
- package/dist/src/flags.d.ts +28 -0
- package/dist/src/flags.js +76 -0
- package/dist/src/main.d.ts +10 -0
- package/dist/src/main.js +54 -0
- package/dist/src/schedule.d.ts +46 -0
- package/dist/src/schedule.js +140 -0
- package/dist/src/screens.d.ts +79 -0
- package/dist/src/screens.js +1322 -0
- package/dist/src/state.d.ts +355 -0
- package/dist/src/state.js +531 -0
- package/dist/src/terminal.d.ts +44 -0
- package/dist/src/terminal.js +101 -0
- package/dist/src/tui.d.ts +92 -0
- package/dist/src/tui.js +372 -0
- package/dist/src/view/automations.d.ts +31 -0
- package/dist/src/view/automations.js +76 -0
- package/dist/src/view/bood/art.d.ts +90 -0
- package/dist/src/view/bood/art.js +152 -0
- package/dist/src/view/bood/beetle.d.ts +7 -0
- package/dist/src/view/bood/beetle.js +123 -0
- package/dist/src/view/bood/bunny.d.ts +8 -0
- package/dist/src/view/bood/bunny.js +228 -0
- package/dist/src/view/bood/cat.d.ts +9 -0
- package/dist/src/view/bood/cat.js +358 -0
- package/dist/src/view/bood/crab.d.ts +7 -0
- package/dist/src/view/bood/crab.js +121 -0
- package/dist/src/view/bood/index.d.ts +29 -0
- package/dist/src/view/bood/index.js +25 -0
- package/dist/src/view/bood/motion.d.ts +178 -0
- package/dist/src/view/bood/motion.js +298 -0
- package/dist/src/view/bood/owl.d.ts +9 -0
- package/dist/src/view/bood/owl.js +285 -0
- package/dist/src/view/bood/registry.d.ts +36 -0
- package/dist/src/view/bood/registry.js +218 -0
- package/dist/src/view/bood/render.d.ts +31 -0
- package/dist/src/view/bood/render.js +196 -0
- package/dist/src/view/bood/sprite.d.ts +18 -0
- package/dist/src/view/bood/sprite.js +130 -0
- package/dist/src/view/bood/sprout.d.ts +8 -0
- package/dist/src/view/bood/sprout.js +121 -0
- package/dist/src/view/bood/types.d.ts +158 -0
- package/dist/src/view/bood/types.js +57 -0
- package/dist/src/view/bubble.d.ts +75 -0
- package/dist/src/view/bubble.js +86 -0
- package/dist/src/view/changes.d.ts +35 -0
- package/dist/src/view/changes.js +22 -0
- package/dist/src/view/composer.d.ts +64 -0
- package/dist/src/view/composer.js +192 -0
- package/dist/src/view/controls.d.ts +44 -0
- package/dist/src/view/controls.js +49 -0
- package/dist/src/view/creature.d.ts +69 -0
- package/dist/src/view/creature.js +83 -0
- package/dist/src/view/customizations.d.ts +28 -0
- package/dist/src/view/customizations.js +53 -0
- package/dist/src/view/details.d.ts +65 -0
- package/dist/src/view/details.js +65 -0
- package/dist/src/view/filediff.d.ts +29 -0
- package/dist/src/view/filediff.js +24 -0
- package/dist/src/view/files.d.ts +31 -0
- package/dist/src/view/files.js +39 -0
- package/dist/src/view/hitl.d.ts +43 -0
- package/dist/src/view/hitl.js +171 -0
- package/dist/src/view/icons.d.ts +13 -0
- package/dist/src/view/icons.js +71 -0
- package/dist/src/view/picker.d.ts +42 -0
- package/dist/src/view/picker.js +71 -0
- package/dist/src/view/sessionhead.d.ts +41 -0
- package/dist/src/view/sessionhead.js +60 -0
- package/dist/src/view/sessions.d.ts +34 -0
- package/dist/src/view/sessions.js +61 -0
- package/dist/src/view/terminal.d.ts +30 -0
- package/dist/src/view/terminal.js +32 -0
- package/dist/src/view/toolcall.d.ts +27 -0
- package/dist/src/view/toolcall.js +48 -0
- package/dist/src/view/transcript.d.ts +50 -0
- package/dist/src/view/transcript.js +60 -0
- package/package.json +72 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line diff, small enough to read.
|
|
3
|
+
*
|
|
4
|
+
* The host sends two whole files and a count of what changed between them; it
|
|
5
|
+
* does not send the diff itself, so somebody has to work out which lines those
|
|
6
|
+
* were. This is that, and it is deliberately the textbook algorithm rather
|
|
7
|
+
* than anything clever: the longest common subsequence of the two line arrays,
|
|
8
|
+
* with everything not in it marked as removed on the left or added on the
|
|
9
|
+
* right.
|
|
10
|
+
*
|
|
11
|
+
* The cost is quadratic in the number of lines, which is why `diffLines` takes
|
|
12
|
+
* a ceiling. Two files of ten thousand lines each is a hundred million cells
|
|
13
|
+
* and a terminal that stops answering, and the honest answer at that size is
|
|
14
|
+
* to say the files are too big rather than to spend a minute proving it.
|
|
15
|
+
*/
|
|
16
|
+
export type DiffKind = 'same' | 'added' | 'removed';
|
|
17
|
+
export interface DiffRow {
|
|
18
|
+
kind: DiffKind;
|
|
19
|
+
/** 1-based, on the side this row exists on. Absent on the side it does not. */
|
|
20
|
+
before?: number;
|
|
21
|
+
after?: number;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
export interface DiffResult {
|
|
25
|
+
rows: DiffRow[];
|
|
26
|
+
added: number;
|
|
27
|
+
removed: number;
|
|
28
|
+
/** Set instead of a diff when the pair was over `limit`. */
|
|
29
|
+
tooLarge?: {
|
|
30
|
+
lines: number;
|
|
31
|
+
limit: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Lines of a file, with the trailing newline not counted as an empty last line. */
|
|
35
|
+
export declare function toLines(text: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* The two sides, lined up.
|
|
38
|
+
*
|
|
39
|
+
* A creation has no `before` and a deletion no `after`; both are passed as an
|
|
40
|
+
* empty string rather than as a special case, because "every line is an
|
|
41
|
+
* addition" is exactly the right diff for a new file and needs no branch of
|
|
42
|
+
* its own.
|
|
43
|
+
*/
|
|
44
|
+
export declare function diffLines(before: string, after: string, limit?: number): DiffResult;
|
package/dist/src/diff.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line diff, small enough to read.
|
|
3
|
+
*
|
|
4
|
+
* The host sends two whole files and a count of what changed between them; it
|
|
5
|
+
* does not send the diff itself, so somebody has to work out which lines those
|
|
6
|
+
* were. This is that, and it is deliberately the textbook algorithm rather
|
|
7
|
+
* than anything clever: the longest common subsequence of the two line arrays,
|
|
8
|
+
* with everything not in it marked as removed on the left or added on the
|
|
9
|
+
* right.
|
|
10
|
+
*
|
|
11
|
+
* The cost is quadratic in the number of lines, which is why `diffLines` takes
|
|
12
|
+
* a ceiling. Two files of ten thousand lines each is a hundred million cells
|
|
13
|
+
* and a terminal that stops answering, and the honest answer at that size is
|
|
14
|
+
* to say the files are too big rather than to spend a minute proving it.
|
|
15
|
+
*/
|
|
16
|
+
/** Lines of a file, with the trailing newline not counted as an empty last line. */
|
|
17
|
+
export function toLines(text) {
|
|
18
|
+
if (text === '')
|
|
19
|
+
return [];
|
|
20
|
+
const lines = text.split('\n');
|
|
21
|
+
if (lines[lines.length - 1] === '')
|
|
22
|
+
lines.pop();
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The two sides, lined up.
|
|
27
|
+
*
|
|
28
|
+
* A creation has no `before` and a deletion no `after`; both are passed as an
|
|
29
|
+
* empty string rather than as a special case, because "every line is an
|
|
30
|
+
* addition" is exactly the right diff for a new file and needs no branch of
|
|
31
|
+
* its own.
|
|
32
|
+
*/
|
|
33
|
+
export function diffLines(before, after, limit = 4000) {
|
|
34
|
+
const a = toLines(before);
|
|
35
|
+
const b = toLines(after);
|
|
36
|
+
if (a.length + b.length > limit) {
|
|
37
|
+
return { rows: [], added: 0, removed: 0, tooLarge: { lines: a.length + b.length, limit } };
|
|
38
|
+
}
|
|
39
|
+
// The common head and tail first. Two files that differ in one line share
|
|
40
|
+
// everything either side of it, and taking those off shrinks the table the
|
|
41
|
+
// quadratic part has to fill to the part that actually differs.
|
|
42
|
+
let head = 0;
|
|
43
|
+
while (head < a.length && head < b.length && a[head] === b[head])
|
|
44
|
+
head++;
|
|
45
|
+
let tail = 0;
|
|
46
|
+
while (tail < a.length - head
|
|
47
|
+
&& tail < b.length - head
|
|
48
|
+
&& a[a.length - 1 - tail] === b[b.length - 1 - tail])
|
|
49
|
+
tail++;
|
|
50
|
+
const midA = a.slice(head, a.length - tail);
|
|
51
|
+
const midB = b.slice(head, b.length - tail);
|
|
52
|
+
const table = lcs(midA, midB);
|
|
53
|
+
const rows = [];
|
|
54
|
+
let added = 0;
|
|
55
|
+
let removed = 0;
|
|
56
|
+
const push = (kind, text, ai, bi) => {
|
|
57
|
+
rows.push({
|
|
58
|
+
kind,
|
|
59
|
+
...(kind !== 'added' ? { before: ai + 1 } : {}),
|
|
60
|
+
...(kind !== 'removed' ? { after: bi + 1 } : {}),
|
|
61
|
+
text,
|
|
62
|
+
});
|
|
63
|
+
if (kind === 'added')
|
|
64
|
+
added++;
|
|
65
|
+
if (kind === 'removed')
|
|
66
|
+
removed++;
|
|
67
|
+
};
|
|
68
|
+
for (let i = 0; i < head; i++)
|
|
69
|
+
push('same', a[i], i, i);
|
|
70
|
+
// Walking the table forwards, so the rows come out in file order.
|
|
71
|
+
let i = 0;
|
|
72
|
+
let j = 0;
|
|
73
|
+
while (i < midA.length || j < midB.length) {
|
|
74
|
+
if (i < midA.length && j < midB.length && midA[i] === midB[j]) {
|
|
75
|
+
push('same', midA[i], head + i, head + j);
|
|
76
|
+
i++;
|
|
77
|
+
j++;
|
|
78
|
+
// A tie goes to the removal, so a replaced line reads `-old` then `+new`
|
|
79
|
+
// the way every other diff on the machine prints it. With `>=` here the
|
|
80
|
+
// pair comes out the other way round, which is not wrong so much as
|
|
81
|
+
// unreadable next to `git diff`.
|
|
82
|
+
}
|
|
83
|
+
else if (j < midB.length && (i === midA.length || (table[i]?.[j + 1] ?? 0) > (table[i + 1]?.[j] ?? 0))) {
|
|
84
|
+
push('added', midB[j], head + i, head + j);
|
|
85
|
+
j++;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
push('removed', midA[i], head + i, head + j);
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (let k = 0; k < tail; k++) {
|
|
93
|
+
push('same', a[a.length - tail + k], a.length - tail + k, b.length - tail + k);
|
|
94
|
+
}
|
|
95
|
+
return { rows, added, removed };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `table[i][j]` is the length of the longest common subsequence of `a[i..]`
|
|
99
|
+
* and `b[j..]`, filled from the end so the walk above can go forwards.
|
|
100
|
+
*/
|
|
101
|
+
function lcs(a, b) {
|
|
102
|
+
const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
103
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
104
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
105
|
+
table[i][j] = a[i] === b[j]
|
|
106
|
+
? (table[i + 1]?.[j + 1] ?? 0) + 1
|
|
107
|
+
: Math.max(table[i + 1]?.[j] ?? 0, table[i]?.[j + 1] ?? 0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return table;
|
|
111
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words that mean "no screen".
|
|
3
|
+
*
|
|
4
|
+
* A closed set rather than "anything that is not a flag": every other argument
|
|
5
|
+
* shape has always started the screen, and a typo becoming a silent CLI run
|
|
6
|
+
* would be a worse answer than a refusal.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COMMANDS: ReadonlySet<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Every flag that takes no value, in either front end.
|
|
11
|
+
*
|
|
12
|
+
* One set rather than one per component, because a flag takes a value or it
|
|
13
|
+
* does not - that is a fact about the flag, not about who is reading it. What
|
|
14
|
+
* each component does with the answer differs: the entry point stops itself
|
|
15
|
+
* swallowing the next word, the CLI keeps a positional a positional, and the
|
|
16
|
+
* screen matches its own arms.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SWITCHES: ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* The command, wherever it is.
|
|
21
|
+
*
|
|
22
|
+
* `ahpc --claude status` and `ahpc status --claude` mean the same thing, so
|
|
23
|
+
* this is a scan rather than a look at the first word - reading only the first
|
|
24
|
+
* one made every flag before a command silently open the screen instead.
|
|
25
|
+
* A flag that takes a value swallows the next word, or `--path status` would
|
|
26
|
+
* be a command.
|
|
27
|
+
*/
|
|
28
|
+
export declare const commandIn: (argv: string[]) => string | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The words the command line is made of, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Three components need to know which flags take a value: the entry point,
|
|
5
|
+
* which scans for a command word and must not read a flag's value as one; the
|
|
6
|
+
* CLI, whose positionals are whatever is left after the flags; and the screen,
|
|
7
|
+
* whose parser consumes the next word for some flags and not others. Held as
|
|
8
|
+
* three tables it was three vocabularies - the entry point knew 18 flags, the
|
|
9
|
+
* CLI knew 18, and only 10 were shared, so `ahpc --force resource write ...`
|
|
10
|
+
* swallowed the command word and opened the screen, which then refused a flag
|
|
11
|
+
* that was never meant for it. Seven flags behaved that way.
|
|
12
|
+
*
|
|
13
|
+
* This file imports nothing on purpose. The entry point decides which front
|
|
14
|
+
* end to load and must not load either one to decide, which is why the tables
|
|
15
|
+
* were copied there rather than imported in the first place; a leaf with no
|
|
16
|
+
* dependencies costs it nothing.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* The words that mean "no screen".
|
|
20
|
+
*
|
|
21
|
+
* A closed set rather than "anything that is not a flag": every other argument
|
|
22
|
+
* shape has always started the screen, and a typo becoming a silent CLI run
|
|
23
|
+
* would be a worse answer than a refusal.
|
|
24
|
+
*/
|
|
25
|
+
export const COMMANDS = new Set([
|
|
26
|
+
'help', 'status', 'config', 'session', 'chat', 'terminal', 'resource',
|
|
27
|
+
'agents', 'models', 'commands', 'customizations', 'completions', 'changes', 'content',
|
|
28
|
+
'prompt', 'exec', 'cancel', 'queue', 'unqueue',
|
|
29
|
+
'watch', 'confirm', 'answer', 'dispatch',
|
|
30
|
+
]);
|
|
31
|
+
/**
|
|
32
|
+
* Every flag that takes no value, in either front end.
|
|
33
|
+
*
|
|
34
|
+
* One set rather than one per component, because a flag takes a value or it
|
|
35
|
+
* does not - that is a fact about the flag, not about who is reading it. What
|
|
36
|
+
* each component does with the answer differs: the entry point stops itself
|
|
37
|
+
* swallowing the next word, the CLI keeps a positional a positional, and the
|
|
38
|
+
* screen matches its own arms.
|
|
39
|
+
*/
|
|
40
|
+
export const SWITCHES = new Set([
|
|
41
|
+
// The screen's own.
|
|
42
|
+
'--static', '-s', '--settled', '--approve', '--answer', '--bood', '--help', '-h',
|
|
43
|
+
// Shape and scope, on both sides.
|
|
44
|
+
'--json', '--full', '--all', '--archived', '--unread', '--undo',
|
|
45
|
+
'--off', '--deny', '--reject', '--claude', '--chat',
|
|
46
|
+
// The write half's, which take no value: without them a positional after one
|
|
47
|
+
// is read as that flag's argument and disappears.
|
|
48
|
+
'--create-only', '--recursive', '--fail-if-exists', '--follow', '--force',
|
|
49
|
+
// What this client serves back to the host.
|
|
50
|
+
'--publish-writable',
|
|
51
|
+
// Output shape, on the two commands that offer a second one.
|
|
52
|
+
'--operations', '--markdown',
|
|
53
|
+
// Asking rather than doing, and agreeing in advance.
|
|
54
|
+
'--list', '--yes',
|
|
55
|
+
]);
|
|
56
|
+
/**
|
|
57
|
+
* The command, wherever it is.
|
|
58
|
+
*
|
|
59
|
+
* `ahpc --claude status` and `ahpc status --claude` mean the same thing, so
|
|
60
|
+
* this is a scan rather than a look at the first word - reading only the first
|
|
61
|
+
* one made every flag before a command silently open the screen instead.
|
|
62
|
+
* A flag that takes a value swallows the next word, or `--path status` would
|
|
63
|
+
* be a command.
|
|
64
|
+
*/
|
|
65
|
+
export const commandIn = (argv) => {
|
|
66
|
+
for (let i = 0; i < argv.length; i++) {
|
|
67
|
+
const word = argv[i];
|
|
68
|
+
if (word.startsWith('-')) {
|
|
69
|
+
if (!SWITCHES.has(word))
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
return COMMANDS.has(word) ? word : undefined;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The entry point, and the one decision it makes.
|
|
4
|
+
*
|
|
5
|
+
* `ahpc` is two front ends over one client: a screen, and a shell. Which one
|
|
6
|
+
* runs is decided by the first word of argv and nothing else - so this file
|
|
7
|
+
* imports neither. Both are loaded on demand, because the screen pulls in a
|
|
8
|
+
* whole renderer and `ahpc session list --json` should not pay for one.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/src/main.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The entry point, and the one decision it makes.
|
|
4
|
+
*
|
|
5
|
+
* `ahpc` is two front ends over one client: a screen, and a shell. Which one
|
|
6
|
+
* runs is decided by the first word of argv and nothing else - so this file
|
|
7
|
+
* imports neither. Both are loaded on demand, because the screen pulls in a
|
|
8
|
+
* whole renderer and `ahpc session list --json` should not pay for one.
|
|
9
|
+
*/
|
|
10
|
+
// Both tables live in a leaf of their own: three components read this
|
|
11
|
+
// vocabulary and holding a copy each is what let them disagree. Importing it
|
|
12
|
+
// does not load a front end, which is the reason they were copied here.
|
|
13
|
+
import { commandIn } from './flags.js';
|
|
14
|
+
const argv = process.argv.slice(2);
|
|
15
|
+
const first = commandIn(argv);
|
|
16
|
+
if (first !== undefined) {
|
|
17
|
+
/*
|
|
18
|
+
* A closed pipe is not an error.
|
|
19
|
+
*
|
|
20
|
+
* `ahpc session list | head` closes stdout part-way through the writing,
|
|
21
|
+
* and Node turns that into an unhandled EPIPE that prints a stack trace
|
|
22
|
+
* over the output the reader actually wanted. Every command here is a
|
|
23
|
+
* writer, so every one of them can be cut short this way.
|
|
24
|
+
*/
|
|
25
|
+
process.stdout.on('error', (error) => {
|
|
26
|
+
if (error.code === 'EPIPE')
|
|
27
|
+
process.exit(0);
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
const { cli, Fault } = await import('./cli/main.js');
|
|
31
|
+
try {
|
|
32
|
+
// The command word taken out, so what is left is only flags and the
|
|
33
|
+
// command's own arguments - wherever in the line it happened to sit.
|
|
34
|
+
const at = argv.indexOf(first);
|
|
35
|
+
process.exitCode = await cli(first, [...argv.slice(0, at), ...argv.slice(at + 1)]);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// A `Fault` is a sentence written for the person who typed the command;
|
|
39
|
+
// anything else is this client going wrong, and hiding its stack would
|
|
40
|
+
// make that indistinguishable from the first kind.
|
|
41
|
+
if (error instanceof Fault) {
|
|
42
|
+
process.stderr.write(`${error.message}\n`);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const { tui } = await import('./tui.js');
|
|
53
|
+
await tui(argv);
|
|
54
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** The protocol's five-field cron, checked for grammar and nothing else. */
|
|
2
|
+
/**
|
|
3
|
+
* What is wrong with an expression, in a sentence, or nothing.
|
|
4
|
+
*
|
|
5
|
+
* A sentence rather than a boolean because it is shown under the field being
|
|
6
|
+
* typed into, and "invalid" tells somebody only that they are not finished.
|
|
7
|
+
*/
|
|
8
|
+
export declare function scheduleProblem(expression: string): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a zone is one this machine knows.
|
|
11
|
+
*
|
|
12
|
+
* Asked of `Intl`, which is the only thing here that holds a tz database, and
|
|
13
|
+
* asked by trying it: there is no list to check against. A zone this client
|
|
14
|
+
* does not know may still be one the host knows, so this is a warning about a
|
|
15
|
+
* likely typo rather than a refusal - which is why it is separate from
|
|
16
|
+
* `scheduleProblem` and reads as a question.
|
|
17
|
+
*/
|
|
18
|
+
export declare function zoneIsKnownHere(timeZone: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Schedules people actually ask for, as expressions.
|
|
21
|
+
*
|
|
22
|
+
* A label paired with a *known* expression, which is the whole reason this can
|
|
23
|
+
* say "every day at 02:00" in English while `scheduleProblem` refuses to
|
|
24
|
+
* paraphrase anything. Reading a label off a table is not the same as claiming
|
|
25
|
+
* to understand an arbitrary expression: choose one of these and the sentence
|
|
26
|
+
* is true by construction, write your own and this says nothing about it.
|
|
27
|
+
*
|
|
28
|
+
* Empty is first and is a real choice: no trigger at all is what manual-only
|
|
29
|
+
* means, and it is the thing somebody wants more often than any single cron.
|
|
30
|
+
*/
|
|
31
|
+
export interface Preset {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
/** Empty for manual-only. */
|
|
35
|
+
expression: string;
|
|
36
|
+
}
|
|
37
|
+
export declare const PRESETS: Preset[];
|
|
38
|
+
/**
|
|
39
|
+
* Which preset an expression is, if it is one.
|
|
40
|
+
*
|
|
41
|
+
* Matched on the expression rather than remembered as a choice, so a preset
|
|
42
|
+
* typed out by hand is recognised and an edited preset stops claiming to be
|
|
43
|
+
* one. Undefined means the expression is the person's own, and nothing here
|
|
44
|
+
* will put words in their mouth about it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function presetFor(expression: string): Preset | undefined;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** The protocol's five-field cron, checked for grammar and nothing else. */
|
|
2
|
+
/**
|
|
3
|
+
* Why this is here at all, and why it stops where it does.
|
|
4
|
+
*
|
|
5
|
+
* A schedule is protocol-defined - five whitespace-separated fields, minute
|
|
6
|
+
* through day-of-week, with names for months and weekdays - so checking one is
|
|
7
|
+
* reading the protocol rather than knowing anything about a particular host.
|
|
8
|
+
* That is the line this client does not cross, and this stays on the right
|
|
9
|
+
* side of it.
|
|
10
|
+
*
|
|
11
|
+
* It checks **grammar, not meaning**. It will not tell you when an expression
|
|
12
|
+
* next comes round, because that is the host's answer: the host evaluates the
|
|
13
|
+
* expression, in the zone, against its own clock, and says so by sending back
|
|
14
|
+
* `nextRunAt`. A client that computed its own would be a second answer to a
|
|
15
|
+
* question somebody is going to be woken up by.
|
|
16
|
+
*
|
|
17
|
+
* What it is for is the moment of typing. The daemon keeps a definition whose
|
|
18
|
+
* expression it could not read - losing the whole thing over a typo helps
|
|
19
|
+
* nobody - and reports the problem to its own log, where the person who made
|
|
20
|
+
* the typo will never see it. Without this, a mistyped schedule is silence
|
|
21
|
+
* until the morning it does not run.
|
|
22
|
+
*/
|
|
23
|
+
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
24
|
+
const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
25
|
+
const FIELDS = [
|
|
26
|
+
{ name: 'minute', min: 0, max: 59 },
|
|
27
|
+
{ name: 'hour', min: 0, max: 23 },
|
|
28
|
+
{ name: 'day of month', min: 1, max: 31 },
|
|
29
|
+
{ name: 'month', min: 1, max: 12, names: MONTHS },
|
|
30
|
+
// 0 and 7 both mean Sunday, which is what the protocol says and what every
|
|
31
|
+
// crontab accepts.
|
|
32
|
+
{ name: 'day of week', min: 0, max: 7, names: DAYS },
|
|
33
|
+
];
|
|
34
|
+
/** One value, by number or by name. Undefined when it is neither. */
|
|
35
|
+
function valueOf(text, field) {
|
|
36
|
+
const named = field.names?.indexOf(text.toLowerCase()) ?? -1;
|
|
37
|
+
if (named >= 0)
|
|
38
|
+
return named + (field.names === MONTHS ? 1 : 0);
|
|
39
|
+
if (!/^\d+$/.test(text))
|
|
40
|
+
return undefined;
|
|
41
|
+
const value = Number(text);
|
|
42
|
+
return value < field.min || value > field.max ? undefined : value;
|
|
43
|
+
}
|
|
44
|
+
/** What is wrong with one field, or nothing. */
|
|
45
|
+
function checkField(text, field) {
|
|
46
|
+
for (const term of text.split(',')) {
|
|
47
|
+
if (term === '')
|
|
48
|
+
return `${field.name} has an empty entry`;
|
|
49
|
+
const parts = term.split('/');
|
|
50
|
+
if (parts.length > 2)
|
|
51
|
+
return `${term} has more than one step`;
|
|
52
|
+
const [range, step] = parts;
|
|
53
|
+
if (step !== undefined && (!/^\d+$/.test(step) || Number(step) === 0)) {
|
|
54
|
+
return `${step} is not a step - a step must be a positive whole number`;
|
|
55
|
+
}
|
|
56
|
+
if (range === '*')
|
|
57
|
+
continue;
|
|
58
|
+
if (range.includes('-')) {
|
|
59
|
+
const ends = range.split('-');
|
|
60
|
+
if (ends.length > 2)
|
|
61
|
+
return `${range} is not a range`;
|
|
62
|
+
const [a, b] = ends;
|
|
63
|
+
const from = valueOf(a, field);
|
|
64
|
+
const to = valueOf(b, field);
|
|
65
|
+
if (from === undefined)
|
|
66
|
+
return `${a} is not a ${field.name}`;
|
|
67
|
+
if (to === undefined)
|
|
68
|
+
return `${b} is not a ${field.name}`;
|
|
69
|
+
if (to < from)
|
|
70
|
+
return `${range} runs backwards`;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (valueOf(range, field) === undefined)
|
|
74
|
+
return `${range} is not a ${field.name}`;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What is wrong with an expression, in a sentence, or nothing.
|
|
80
|
+
*
|
|
81
|
+
* A sentence rather than a boolean because it is shown under the field being
|
|
82
|
+
* typed into, and "invalid" tells somebody only that they are not finished.
|
|
83
|
+
*/
|
|
84
|
+
export function scheduleProblem(expression) {
|
|
85
|
+
const trimmed = expression.trim();
|
|
86
|
+
if (trimmed === '')
|
|
87
|
+
return 'A schedule needs five fields, like 0 9 * * 1-5';
|
|
88
|
+
if (trimmed.startsWith('@'))
|
|
89
|
+
return 'AHP has no @daily or @hourly - write the five fields out';
|
|
90
|
+
const fields = trimmed.split(/\s+/);
|
|
91
|
+
if (fields.length !== 5) {
|
|
92
|
+
return `A schedule has five fields - minute, hour, day, month, weekday - and this has ${fields.length}`;
|
|
93
|
+
}
|
|
94
|
+
for (let at = 0; at < FIELDS.length; at++) {
|
|
95
|
+
const problem = checkField(fields[at], FIELDS[at]);
|
|
96
|
+
if (problem !== undefined)
|
|
97
|
+
return problem;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether a zone is one this machine knows.
|
|
103
|
+
*
|
|
104
|
+
* Asked of `Intl`, which is the only thing here that holds a tz database, and
|
|
105
|
+
* asked by trying it: there is no list to check against. A zone this client
|
|
106
|
+
* does not know may still be one the host knows, so this is a warning about a
|
|
107
|
+
* likely typo rather than a refusal - which is why it is separate from
|
|
108
|
+
* `scheduleProblem` and reads as a question.
|
|
109
|
+
*/
|
|
110
|
+
export function zoneIsKnownHere(timeZone) {
|
|
111
|
+
if (timeZone.trim() === '')
|
|
112
|
+
return false;
|
|
113
|
+
try {
|
|
114
|
+
new Intl.DateTimeFormat('en-US', { timeZone });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export const PRESETS = [
|
|
122
|
+
{ id: 'manual', label: 'By hand only', expression: '' },
|
|
123
|
+
{ id: 'half-hourly', label: 'Every 30 minutes', expression: '*/30 * * * *' },
|
|
124
|
+
{ id: 'hourly', label: 'Every hour, on the hour', expression: '0 * * * *' },
|
|
125
|
+
{ id: 'daily', label: 'Every day at 02:00', expression: '0 2 * * *' },
|
|
126
|
+
{ id: 'weekdays', label: 'Weekdays at 09:00', expression: '0 9 * * 1-5' },
|
|
127
|
+
{ id: 'saturday', label: 'Saturdays at 23:00', expression: '0 23 * * 6' },
|
|
128
|
+
];
|
|
129
|
+
/**
|
|
130
|
+
* Which preset an expression is, if it is one.
|
|
131
|
+
*
|
|
132
|
+
* Matched on the expression rather than remembered as a choice, so a preset
|
|
133
|
+
* typed out by hand is recognised and an edited preset stops claiming to be
|
|
134
|
+
* one. Undefined means the expression is the person's own, and nothing here
|
|
135
|
+
* will put words in their mouth about it.
|
|
136
|
+
*/
|
|
137
|
+
export function presetFor(expression) {
|
|
138
|
+
const trimmed = expression.trim();
|
|
139
|
+
return PRESETS.find((one) => one.expression === trimmed);
|
|
140
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { RenderOutput } from '@textui/core';
|
|
2
|
+
export declare const SessionsScreen: (props: Record<string, never>) => RenderOutput;
|
|
3
|
+
/**
|
|
4
|
+
* The terminal screen.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is decided here: which terminals exist, which is open and what
|
|
7
|
+
* happens when a line is sent all live in `terminal.ts`, and this puts them on
|
|
8
|
+
* screen. Unmounting it therefore does not lose the shell, which is the
|
|
9
|
+
* reason for the split.
|
|
10
|
+
*/
|
|
11
|
+
export declare const TerminalScreen: (props: Record<string, never>) => RenderOutput;
|
|
12
|
+
export declare const ChatScreen: (props: Record<string, never>) => RenderOutput;
|
|
13
|
+
export declare const NewSessionScreen: (props: Record<string, never>) => RenderOutput;
|
|
14
|
+
/**
|
|
15
|
+
* The changeset, and one file out of it.
|
|
16
|
+
*
|
|
17
|
+
* Two states of one screen rather than two screens, because opening a file is
|
|
18
|
+
* not somewhere you navigate to: escape goes back to the list and then out,
|
|
19
|
+
* the way it does everywhere else here. Which file is in the store so that
|
|
20
|
+
* leaving for the conversation and coming back arrives where it was.
|
|
21
|
+
*
|
|
22
|
+
* The content is fetched when a row opens and never before. A changeset is a
|
|
23
|
+
* list of names the host sends up front and a pile of bytes it does not, and
|
|
24
|
+
* a screen that read both together would download a session's whole diff to
|
|
25
|
+
* draw a list of filenames.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ChangesScreen: (props: Record<string, never>) => RenderOutput;
|
|
28
|
+
/**
|
|
29
|
+
* Writing one down.
|
|
30
|
+
*
|
|
31
|
+
* The half that was missing: this client could list, run and switch off an
|
|
32
|
+
* automation and could not make one, so the only way to have any was to write
|
|
33
|
+
* the daemon's own file by hand and restart it. That is not a workflow, it is
|
|
34
|
+
* what a person does when a client has a hole in it.
|
|
35
|
+
*
|
|
36
|
+
* The expression is checked here and *understood* on the host. `scheduleProblem`
|
|
37
|
+
* reads the protocol's grammar and stops there; when it next comes round is the
|
|
38
|
+
* host's answer, and it arrives as `nextRunAt` on the row this screen leaves
|
|
39
|
+
* behind. So the confirmation that a schedule is real is the list saying when it
|
|
40
|
+
* will fire, not this form saying it looks right.
|
|
41
|
+
*
|
|
42
|
+
* Manual-only is a real choice and not an empty box. The protocol says an
|
|
43
|
+
* automation with no triggers is one nothing fires, so leaving the schedule
|
|
44
|
+
* blank writes a definition with an empty trigger list rather than a broken one.
|
|
45
|
+
*/
|
|
46
|
+
export declare const NewAutomationScreen: (props: Record<string, never>) => RenderOutput;
|
|
47
|
+
/**
|
|
48
|
+
* What the host does without being asked.
|
|
49
|
+
*
|
|
50
|
+
* The catalogue answers "what has been said"; this answers "what will happen".
|
|
51
|
+
* They are both the host's and only one of them was drawn, so a session that
|
|
52
|
+
* appeared at nine this morning was a session with no account of itself - which
|
|
53
|
+
* is what protocol 0.9.0's `origin` is for, and it is read on the catalogue
|
|
54
|
+
* rather than here.
|
|
55
|
+
*
|
|
56
|
+
* Read whole on every change rather than patched. The channel says an
|
|
57
|
+
* automation moved and the list is a dozen rows; a client that reduced its own
|
|
58
|
+
* copy would be a second answer to what the host holds, for no gain at this
|
|
59
|
+
* size.
|
|
60
|
+
*/
|
|
61
|
+
export declare const AutomationsScreen: (props: Record<string, never>) => RenderOutput;
|
|
62
|
+
/**
|
|
63
|
+
* The host's filesystem, browsed.
|
|
64
|
+
*
|
|
65
|
+
* `resourceList` and `resourceRead` were on the connection and behind
|
|
66
|
+
* `ahpc resource`, and the only place a host's files were *drawn* was the `@`
|
|
67
|
+
* completion in the composer - so a host that serves a project made people
|
|
68
|
+
* type paths they could not see.
|
|
69
|
+
*
|
|
70
|
+
* One directory at a time rather than a tree: `resourceList` answers about one
|
|
71
|
+
* directory, and an expanding tree would be a request per node with nothing on
|
|
72
|
+
* screen meanwhile. Opening a file reads it and shows it; nothing here writes,
|
|
73
|
+
* though the host would now allow it - see the roadmap.
|
|
74
|
+
*/
|
|
75
|
+
export declare const FilesScreen: (props: Record<string, never>) => RenderOutput;
|
|
76
|
+
export declare const SkillsScreen: (props: Record<string, never>) => RenderOutput;
|
|
77
|
+
export declare const McpScreen: (props: Record<string, never>) => RenderOutput;
|
|
78
|
+
export declare const SettingsScreen: (props: Record<string, never>) => RenderOutput;
|
|
79
|
+
export declare const HostsScreen: (props: Record<string, never>) => RenderOutput;
|