team 0.1.1 → 0.2.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/README.md +38 -12
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +75 -0
- package/dist/approve/approval.d.ts +11 -0
- package/dist/approve/approval.js +80 -8
- package/dist/approve/fingerprint.d.ts +29 -6
- package/dist/approve/fingerprint.js +57 -32
- package/dist/budgets/gate.d.ts +5 -4
- package/dist/budgets/gate.js +23 -14
- package/dist/budgets/readings.d.ts +57 -9
- package/dist/budgets/readings.js +134 -18
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +4 -2
- package/dist/budgets/table.js +29 -11
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +8 -5
- package/dist/commands/add.d.ts +0 -1
- package/dist/commands/add.js +18 -9
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +17 -1
- package/dist/commands/doctor.js +128 -8
- package/dist/commands/down.d.ts +6 -2
- package/dist/commands/down.js +40 -14
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +26 -4
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +22 -3
- package/dist/commands/up.d.ts +3 -1
- package/dist/commands/up.js +12 -6
- package/dist/commands/watch.d.ts +4 -0
- package/dist/commands/watch.js +99 -19
- package/dist/conformance/adapter.d.ts +5 -0
- package/dist/conformance/adapter.js +44 -0
- package/dist/file/check.d.ts +20 -0
- package/dist/file/check.js +112 -0
- package/dist/file/lines.js +13 -2
- package/dist/file/sections/budgets.d.ts +15 -0
- package/dist/file/sections/budgets.js +180 -0
- package/dist/file/sections/coordinator.d.ts +2 -0
- package/dist/file/sections/coordinator.js +11 -0
- package/dist/file/sections/format.d.ts +2 -0
- package/dist/file/sections/format.js +13 -0
- package/dist/file/sections/identity.d.ts +2 -0
- package/dist/file/sections/identity.js +103 -0
- package/dist/file/sections/index.d.ts +9 -0
- package/dist/file/sections/index.js +43 -0
- package/dist/file/sections/lead.d.ts +4 -0
- package/dist/file/sections/lead.js +18 -0
- package/dist/file/sections/limits.d.ts +2 -0
- package/dist/file/sections/limits.js +43 -0
- package/dist/file/sections/machine.d.ts +2 -0
- package/dist/file/sections/machine.js +38 -0
- package/dist/file/sections/operator.d.ts +2 -0
- package/dist/file/sections/operator.js +11 -0
- package/dist/file/sections/project.d.ts +2 -0
- package/dist/file/sections/project.js +13 -0
- package/dist/file/sections/rules.d.ts +2 -0
- package/dist/file/sections/rules.js +9 -0
- package/dist/file/sections/seats.d.ts +7 -0
- package/dist/file/sections/seats.js +161 -0
- package/dist/file/sections/section.d.ts +32 -0
- package/dist/file/sections/section.js +7 -0
- package/dist/file/sections/session.d.ts +2 -0
- package/dist/file/sections/session.js +16 -0
- package/dist/file/sections/tools.d.ts +2 -0
- package/dist/file/sections/tools.js +43 -0
- package/dist/file/sections/trust.d.ts +2 -0
- package/dist/file/sections/trust.js +20 -0
- package/dist/file/sections/units.d.ts +16 -0
- package/dist/file/sections/units.js +8 -0
- package/dist/file/sections/visibility.d.ts +2 -0
- package/dist/file/sections/visibility.js +12 -0
- package/dist/file/sections/watch-checks.d.ts +9 -0
- package/dist/file/sections/watch-checks.js +14 -0
- package/dist/file/sections/watch.d.ts +14 -0
- package/dist/file/sections/watch.js +111 -0
- package/dist/file/sections/workspace.d.ts +4 -0
- package/dist/file/sections/workspace.js +77 -0
- package/dist/file/types.d.ts +6 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -668
- package/dist/herdr.d.ts +5 -0
- package/dist/herdr.js +51 -12
- package/dist/launch/agent.d.ts +1 -0
- package/dist/launch/agent.js +13 -0
- package/dist/launch/deliver.d.ts +7 -1
- package/dist/launch/deliver.js +159 -7
- package/dist/launch/execute.d.ts +4 -2
- package/dist/launch/execute.js +36 -13
- package/dist/launch/plan.js +16 -2
- package/dist/launch/rules.d.ts +4 -2
- package/dist/launch/rules.js +17 -3
- package/dist/profiles/antigravity.yaml +25 -2
- package/dist/profiles/claude-code.yaml +51 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +46 -3
- package/dist/profiles/overrides.d.ts +67 -0
- package/dist/profiles/overrides.js +190 -0
- package/dist/profiles/profile.d.ts +3 -0
- package/dist/profiles/profile.js +5 -1
- package/dist/state.d.ts +9 -4
- package/dist/state.js +37 -0
- package/dist/status/compare.js +7 -4
- package/dist/status/statusline.js +3 -1
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/check.d.ts +1 -0
- package/dist/watch/checks/budget.js +50 -51
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +10 -2
- package/dist/watch/pass.js +70 -29
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +510 -89
- package/dist/watch/screen-data.d.ts +34 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +344 -39
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +25 -6
- package/dist/watch/screen.js +48 -9
- package/examples/team.yaml +8 -0
- package/package.json +9 -3
- package/schema/team.schema.json +498 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ScreenProfile } from './screen-profile.ts';
|
|
1
2
|
import type { Screen } from './screen.ts';
|
|
2
3
|
export type LinePattern = {
|
|
3
4
|
match: RegExp;
|
|
@@ -7,7 +8,11 @@ export type Rule = {
|
|
|
7
8
|
any?: LinePattern[];
|
|
8
9
|
all?: LinePattern[];
|
|
9
10
|
footer?: string;
|
|
11
|
+
/** `any` and `all` are read on the dialog's last line, not on every line. */
|
|
12
|
+
onFooter?: boolean;
|
|
10
13
|
belowLastRule?: RegExp;
|
|
14
|
+
/** The window has no composer rule: the dialog is the pane. */
|
|
15
|
+
withoutRule?: boolean;
|
|
11
16
|
noneAfter?: {
|
|
12
17
|
anchor: LinePattern;
|
|
13
18
|
patterns: LinePattern[];
|
|
@@ -21,36 +26,64 @@ export type Placeholder = {
|
|
|
21
26
|
} | {
|
|
22
27
|
prefix: string;
|
|
23
28
|
};
|
|
29
|
+
/** How a composer continues a line onto its next row, from a capture that shows it: the
|
|
30
|
+
* continuation starts at the text column (the box's own indent), and the break folds the space
|
|
31
|
+
* away at a word boundary or falls mid-word (hard). */
|
|
32
|
+
export type Wrap = {
|
|
33
|
+
continuation: 'text-column';
|
|
34
|
+
kind: 'word' | 'hard';
|
|
35
|
+
};
|
|
24
36
|
export type FallbackRule = {
|
|
25
37
|
all: LinePattern[];
|
|
26
38
|
kind: Screen['kind'];
|
|
27
39
|
};
|
|
40
|
+
export type PlaceholderStyle = 'dim';
|
|
28
41
|
type Box = {
|
|
29
42
|
mode: 'box-to-rule';
|
|
30
43
|
prompt: RegExp;
|
|
31
44
|
rule: RegExp;
|
|
45
|
+
/**
|
|
46
|
+
* For a scrolled-out box, the non-blank lines under the closing rule must match
|
|
47
|
+
* every pattern, in order, and the counts must be equal.
|
|
48
|
+
*/
|
|
49
|
+
footers: RegExp[];
|
|
32
50
|
placeholders: Placeholder[];
|
|
51
|
+
placeholderStyle?: PlaceholderStyle;
|
|
52
|
+
wrap?: Wrap;
|
|
53
|
+
frameRows: number;
|
|
33
54
|
};
|
|
34
55
|
type StatusLast = {
|
|
35
56
|
mode: 'status-last';
|
|
36
57
|
statusLine: RegExp;
|
|
37
58
|
prompt: RegExp;
|
|
38
59
|
placeholders: Placeholder[];
|
|
60
|
+
placeholderStyle?: PlaceholderStyle;
|
|
61
|
+
wrap?: Wrap;
|
|
62
|
+
frameRows: number;
|
|
39
63
|
};
|
|
40
64
|
type StatusThenOne = {
|
|
41
65
|
mode: 'status-then-one';
|
|
42
66
|
statusLine: RegExp;
|
|
43
67
|
prompt: RegExp;
|
|
44
68
|
placeholders: Placeholder[];
|
|
69
|
+
placeholderStyle?: PlaceholderStyle;
|
|
45
70
|
stripSuffix: RegExp | null;
|
|
46
71
|
fallback: FallbackRule[];
|
|
72
|
+
wrap?: Wrap;
|
|
73
|
+
frameRows: number;
|
|
47
74
|
};
|
|
48
75
|
type TwoRules = {
|
|
49
76
|
mode: 'two-rules-footer-below';
|
|
50
77
|
prompt: RegExp;
|
|
51
78
|
rule: RegExp;
|
|
79
|
+
/** Any line below the closing rule matches any pattern in the list. */
|
|
52
80
|
footers: RegExp[];
|
|
53
81
|
placeholders: Placeholder[];
|
|
82
|
+
/** A row that stands for hidden rows of a folded paste; capture 1 is the hidden count. */
|
|
83
|
+
fold: RegExp | null;
|
|
84
|
+
placeholderStyle?: PlaceholderStyle;
|
|
85
|
+
wrap?: Wrap;
|
|
86
|
+
frameRows: number;
|
|
54
87
|
};
|
|
55
88
|
export type Composer = Box | StatusLast | StatusThenOne | TwoRules;
|
|
56
89
|
export type ScreenData = {
|
|
@@ -61,5 +94,6 @@ export type ScreenData = {
|
|
|
61
94
|
question?: Stage;
|
|
62
95
|
working?: Stage;
|
|
63
96
|
composer: Composer;
|
|
97
|
+
profile?: ScreenProfile;
|
|
64
98
|
};
|
|
65
99
|
export {};
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
import type { ScreenData } from './screen-data.ts';
|
|
2
|
-
|
|
1
|
+
import type { Rule, ScreenData } from './screen-data.ts';
|
|
2
|
+
import { type YamlNode } from '../yaml.ts';
|
|
3
|
+
export declare function loadScreen(text: string, baseDir?: string, profileFile?: string): ScreenData;
|
|
4
|
+
/**
|
|
5
|
+
* Rules an override adds to one dialog stage. The stage is a list of patterns:
|
|
6
|
+
* a map would be a case flag or a renamed stage, and neither is an added pattern.
|
|
7
|
+
*/
|
|
8
|
+
export declare function addedRules(node: YamlNode): Rule[];
|
|
@@ -1,34 +1,240 @@
|
|
|
1
1
|
// A profile file, in team's YAML subset, checked as it is loaded. The JSON Schema
|
|
2
2
|
// next to the profiles describes the same shape; this is what actually refuses a file,
|
|
3
3
|
// because the package does not carry a schema validator.
|
|
4
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { isAbsolute, resolve, sep } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
4
8
|
import { DialectError, compilePattern } from "./dialect.js";
|
|
5
9
|
import { YamlError, parseYaml } from "../yaml.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
6
11
|
const STAGES = ['unknown', 'trust', 'permission', 'question', 'working'];
|
|
7
12
|
const KINDS = ['idle', 'working', 'unsent', 'permission', 'trust', 'question', 'unknown'];
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
// The lines a dialog draws for its choices, built from their parts: the mark on the choice
|
|
14
|
+
// the cursor is on (claude-code ❯, codex ›, antigravity >) or the indent of the others, the
|
|
15
|
+
// number, and the labels the profiles' rules and fixtures show, run-on forms included — the
|
|
16
|
+
// trust dialog's "Yes, I trust this folder" and "No, exit", the permission dialog's "No, and
|
|
17
|
+
// tell Claude what to do differently" (escape hint and all), codex's "Yes, proceed (y)" and
|
|
18
|
+
// its own long No. The safety floor reads the first two numbered lines (screen-core's
|
|
19
|
+
// choiceLine and twoLine), so chrome must match none of them.
|
|
20
|
+
const CHOICE_MARKS = ['❯ ', '› ', '> ', ' '];
|
|
21
|
+
const CHOICE_TAILS = [
|
|
22
|
+
'',
|
|
23
|
+
' Yes',
|
|
24
|
+
' No',
|
|
25
|
+
' Yes, I trust this folder',
|
|
26
|
+
' No, exit',
|
|
27
|
+
' No, and tell Claude what to do differently',
|
|
28
|
+
' No, and tell Claude what to do differently (esc)',
|
|
29
|
+
' Yes, proceed (y)',
|
|
30
|
+
' No, and tell Codex what to do differently (esc)',
|
|
31
|
+
];
|
|
32
|
+
const CHOICE_SAMPLES = CHOICE_MARKS.flatMap((mark) => ['1', '2'].flatMap((number) => CHOICE_TAILS.map((tail) => `${mark}${number}.${tail}`)));
|
|
33
|
+
const DEFAULT_COMPOSER = {
|
|
34
|
+
mode: 'box-to-rule',
|
|
35
|
+
prompt: /(?!)/,
|
|
36
|
+
rule: /(?!)/,
|
|
37
|
+
footers: [],
|
|
38
|
+
placeholders: [],
|
|
39
|
+
frameRows: 0,
|
|
40
|
+
};
|
|
41
|
+
export function loadScreen(text, baseDir, profileFile) {
|
|
10
42
|
const root = parseYaml(text);
|
|
11
43
|
const entries = mapping(root, 'a profile');
|
|
12
44
|
// Launch keys are read by profile.ts. A screen-only snippet, as in the tests, omits them.
|
|
13
|
-
only(entries, ['format', 'cli', 'screen', 'quota', 'binary', 'process_names', 'tested', 'unattended', 'rules', 'login', 'exit', 'timeouts', 'models', 'status_model']);
|
|
45
|
+
only(entries, ['format', 'cli', 'screen', 'screen_module', 'quota', 'binary', 'process_names', 'tested', 'unattended', 'rules', 'login', 'exit', 'timeouts', 'models', 'status_model']);
|
|
14
46
|
const format = required(entries, 'format', root.line);
|
|
15
47
|
if (format.value.kind !== 'scalar' || format.value.value !== 1)
|
|
16
48
|
fail(format.line, '"format" must be 1');
|
|
17
49
|
const cli = required(entries, 'cli', root.line);
|
|
18
50
|
if (!stringOf(cli.value))
|
|
19
51
|
fail(cli.line, '"cli" must be a string');
|
|
52
|
+
const topScreenModule = optional(entries, 'screen_module');
|
|
20
53
|
const screen = required(entries, 'screen', root.line);
|
|
21
|
-
return screenOf(screen.value);
|
|
54
|
+
return deepFreeze(screenOf(screen.value, topScreenModule, baseDir, profileFile));
|
|
22
55
|
}
|
|
23
|
-
function
|
|
56
|
+
function deepFreeze(obj) {
|
|
57
|
+
if (obj === null || typeof obj !== 'object' || obj instanceof RegExp)
|
|
58
|
+
return obj;
|
|
59
|
+
if (Object.isFrozen(obj))
|
|
60
|
+
return obj;
|
|
61
|
+
Object.freeze(obj);
|
|
62
|
+
for (const key of Object.getOwnPropertyNames(obj)) {
|
|
63
|
+
const val = obj[key];
|
|
64
|
+
if (val !== null && typeof val === 'object' && !(val instanceof RegExp)) {
|
|
65
|
+
deepFreeze(val);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
function loadScreenModule(specifier, baseDir, line, profileFile) {
|
|
71
|
+
const label = profileFile ? `profile "${profileFile}"` : 'profile';
|
|
72
|
+
// 1. Refuse empty string
|
|
73
|
+
if (!specifier || specifier.trim() === '') {
|
|
74
|
+
fail(line, `${label}: "screen_module" cannot be an empty string`);
|
|
75
|
+
}
|
|
76
|
+
// 2. Refuse current directory "."
|
|
77
|
+
if (specifier === '.' || specifier === './' || specifier === '.\\') {
|
|
78
|
+
fail(line, `${label}: "screen_module" cannot be current directory "."`);
|
|
79
|
+
}
|
|
80
|
+
// 3. Refuse absolute path
|
|
81
|
+
if (isAbsolute(specifier) || specifier.startsWith('/') || specifier.startsWith('\\')) {
|
|
82
|
+
fail(line, `${label}: "screen_module" cannot be an absolute path: "${specifier}"`);
|
|
83
|
+
}
|
|
84
|
+
// 4. Refuse home directory path
|
|
85
|
+
if (specifier.startsWith('~')) {
|
|
86
|
+
fail(line, `${label}: "screen_module" cannot be a home directory path: "${specifier}"`);
|
|
87
|
+
}
|
|
88
|
+
// 5. Refuse URL or other scheme
|
|
89
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(specifier)) {
|
|
90
|
+
fail(line, `${label}: "screen_module" cannot be a URL or scheme: "${specifier}"`);
|
|
91
|
+
}
|
|
92
|
+
// 6. Refuse ".." segment
|
|
93
|
+
if (specifier.split(/[/\\]/).includes('..') || /%2[eE]%2[eE]/.test(specifier)) {
|
|
94
|
+
fail(line, `${label}: "screen_module" cannot contain ".." segments: "${specifier}"`);
|
|
95
|
+
}
|
|
96
|
+
const defaultDir = fileURLToPath(new URL('../profiles', import.meta.url));
|
|
97
|
+
const dir = baseDir ? resolve(baseDir) : defaultDir;
|
|
98
|
+
const resolvedDir = resolve(dir);
|
|
99
|
+
// 7. Refuse paths in the project outside profiles
|
|
100
|
+
const projectSegments = ['test', 'src', 'dist', 'scripts', 'examples', 'node_modules', 'worktrees', '.github'];
|
|
101
|
+
const stripped = specifier.replace(/^(?:\.[/\\])+/, '');
|
|
102
|
+
const firstSegment = stripped.split(/[/\\]/)[0] ?? '';
|
|
103
|
+
if (projectSegments.includes(firstSegment) ||
|
|
104
|
+
(existsSync(resolve(process.cwd(), specifier)) && !resolve(process.cwd(), specifier).startsWith(resolvedDir + sep))) {
|
|
105
|
+
fail(line, `${label}: "screen_module" cannot be a path in the project: "${specifier}"`);
|
|
106
|
+
}
|
|
107
|
+
// 8. Must resolve inside profiles directory
|
|
108
|
+
let target = resolve(resolvedDir, specifier);
|
|
109
|
+
if (!target.startsWith(resolvedDir + sep) && target !== resolvedDir) {
|
|
110
|
+
fail(line, `${label}: "screen_module" must resolve inside profiles directory: "${specifier}"`);
|
|
111
|
+
}
|
|
112
|
+
// Check if target exists and if it is a directory or symlink loop
|
|
113
|
+
let st;
|
|
114
|
+
try {
|
|
115
|
+
st = statSync(target);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err && err.code === 'ELOOP') {
|
|
119
|
+
fail(line, `${label}: "screen_module" contains a symlink loop: "${specifier}"`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (st && st.isDirectory()) {
|
|
123
|
+
fail(line, `${label}: "screen_module" cannot be a directory: "${specifier}"`);
|
|
124
|
+
}
|
|
125
|
+
// 9. Refuse bare names or paths without a script extension (.ts, .js, .cjs, .mjs)
|
|
126
|
+
if (!/\.(ts|js|cjs|mjs)$/.test(specifier)) {
|
|
127
|
+
if (!specifier.includes('/') && !specifier.includes('\\') && !/\.[^/\\]+$/.test(specifier)) {
|
|
128
|
+
fail(line, `${label}: "screen_module" cannot be a bare name: "${specifier}"`);
|
|
129
|
+
}
|
|
130
|
+
fail(line, `${label}: "screen_module" must have a script extension (.ts, .js, .cjs, .mjs): "${specifier}"`);
|
|
131
|
+
}
|
|
132
|
+
let fileTarget = target;
|
|
133
|
+
if (!existsSync(fileTarget)) {
|
|
134
|
+
if (fileTarget.endsWith('.ts') && existsSync(fileTarget.slice(0, -3) + '.js')) {
|
|
135
|
+
fileTarget = fileTarget.slice(0, -3) + '.js';
|
|
136
|
+
}
|
|
137
|
+
else if (fileTarget.endsWith('.js') && existsSync(fileTarget.slice(0, -3) + '.ts')) {
|
|
138
|
+
fileTarget = fileTarget.slice(0, -3) + '.ts';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let fileStat;
|
|
142
|
+
try {
|
|
143
|
+
fileStat = statSync(fileTarget);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err && err.code === 'ELOOP') {
|
|
147
|
+
fail(line, `${label}: "screen_module" contains a symlink loop: "${specifier}"`);
|
|
148
|
+
}
|
|
149
|
+
fail(line, `${label}: cannot load "screen_module": Cannot find module "${specifier}"`);
|
|
150
|
+
}
|
|
151
|
+
if (!fileStat.isFile()) {
|
|
152
|
+
if (fileStat.isDirectory()) {
|
|
153
|
+
fail(line, `${label}: "screen_module" cannot be a directory: "${specifier}"`);
|
|
154
|
+
}
|
|
155
|
+
fail(line, `${label}: cannot load "screen_module": Cannot find module "${specifier}"`);
|
|
156
|
+
}
|
|
157
|
+
// Symlink check: realpath must be inside profiles directory
|
|
158
|
+
let realTarget;
|
|
159
|
+
let realDir;
|
|
160
|
+
try {
|
|
161
|
+
realTarget = realpathSync(fileTarget);
|
|
162
|
+
realDir = realpathSync(resolvedDir);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
if (err && err.code === 'ELOOP') {
|
|
166
|
+
fail(line, `${label}: "screen_module" contains a symlink loop: "${specifier}"`);
|
|
167
|
+
}
|
|
168
|
+
fail(line, `${label}: cannot load "screen_module": ${err instanceof Error ? err.message : String(err)}`);
|
|
169
|
+
}
|
|
170
|
+
if (!realTarget.startsWith(realDir + sep) && realTarget !== realDir) {
|
|
171
|
+
fail(line, `${label}: "screen_module" symlink leads outside profiles directory: "${specifier}"`);
|
|
172
|
+
}
|
|
173
|
+
let mod;
|
|
174
|
+
try {
|
|
175
|
+
mod = require(realTarget);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
fail(line, `${label}: cannot load "screen_module": ${error instanceof Error ? error.message : String(error)}`);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const defaultExport = (typeof mod === 'object' && mod !== null) || typeof mod === 'function'
|
|
182
|
+
? mod.default
|
|
183
|
+
: undefined;
|
|
184
|
+
const candidate = defaultExport && (typeof defaultExport === 'object' || typeof defaultExport === 'function')
|
|
185
|
+
? defaultExport
|
|
186
|
+
: mod;
|
|
187
|
+
const readExport = (key) => {
|
|
188
|
+
let val = candidate !== null && (typeof candidate === 'object' || typeof candidate === 'function')
|
|
189
|
+
? candidate[key]
|
|
190
|
+
: undefined;
|
|
191
|
+
if (val === undefined && mod !== candidate && mod !== null && (typeof mod === 'object' || typeof mod === 'function')) {
|
|
192
|
+
val = mod[key];
|
|
193
|
+
}
|
|
194
|
+
return typeof val === 'function' ? val : undefined;
|
|
195
|
+
};
|
|
196
|
+
const snapshot = Object.freeze({
|
|
197
|
+
unknown: readExport('unknown'),
|
|
198
|
+
trust: readExport('trust'),
|
|
199
|
+
permission: readExport('permission'),
|
|
200
|
+
question: readExport('question'),
|
|
201
|
+
working: readExport('working'),
|
|
202
|
+
composer: readExport('composer'),
|
|
203
|
+
});
|
|
204
|
+
return snapshot;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
fail(line, `${label}: cannot load "screen_module": ${error instanceof Error ? error.message : String(error)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function screenOf(node, topScreenModule, baseDir, profileFile) {
|
|
24
211
|
const entries = mapping(node, 'screen');
|
|
25
|
-
only(entries, ['chrome', 'composer', ...STAGES]);
|
|
212
|
+
only(entries, ['chrome', 'composer', 'screen_module', ...STAGES]);
|
|
26
213
|
const chrome = optional(entries, 'chrome');
|
|
27
|
-
const
|
|
214
|
+
const screenScreenModule = optional(entries, 'screen_module');
|
|
215
|
+
const moduleEntry = screenScreenModule ?? topScreenModule;
|
|
216
|
+
const composer = optional(entries, 'composer');
|
|
217
|
+
const label = profileFile ? `profile "${profileFile}"` : 'profile';
|
|
218
|
+
let profile;
|
|
219
|
+
if (moduleEntry) {
|
|
220
|
+
const specifier = stringOf(moduleEntry.value);
|
|
221
|
+
if (specifier === null)
|
|
222
|
+
fail(moduleEntry.line, '"screen_module" must be a string');
|
|
223
|
+
profile = loadScreenModule(specifier, baseDir, moduleEntry.line, profileFile);
|
|
224
|
+
}
|
|
225
|
+
// Rule (b): A composer comes from data or from the hatch, never both; the load refuses both.
|
|
226
|
+
if (composer && typeof profile?.composer === 'function') {
|
|
227
|
+
fail(moduleEntry.line, `${label}: profile has a data composer and screen_module exports a composer: a composer comes from data or from the hatch, never both`);
|
|
228
|
+
}
|
|
229
|
+
if (!composer && typeof profile?.composer !== 'function') {
|
|
230
|
+
fail(node.line, `${label}: missing "composer"`);
|
|
231
|
+
}
|
|
28
232
|
const data = {
|
|
29
233
|
chrome: chrome ? chromeOf(chrome.value) : [],
|
|
30
|
-
composer: composerOf(composer.value),
|
|
234
|
+
composer: composer ? composerOf(composer.value) : DEFAULT_COMPOSER,
|
|
31
235
|
};
|
|
236
|
+
if (profile)
|
|
237
|
+
data.profile = profile;
|
|
32
238
|
for (const name of STAGES) {
|
|
33
239
|
const entry = optional(entries, name);
|
|
34
240
|
if (entry)
|
|
@@ -36,6 +242,17 @@ function screenOf(node) {
|
|
|
36
242
|
}
|
|
37
243
|
return data;
|
|
38
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Rules an override adds to one dialog stage. The stage is a list of patterns:
|
|
247
|
+
* a map would be a case flag or a renamed stage, and neither is an added pattern.
|
|
248
|
+
*/
|
|
249
|
+
export function addedRules(node) {
|
|
250
|
+
if (node.kind !== 'seq' || node.items.length === 0)
|
|
251
|
+
fail(node.line, 'an override adds a non-empty list of patterns');
|
|
252
|
+
// A shipped profile may set a pattern's own case. An override may not: the flag would
|
|
253
|
+
// loosen a match the shipped patterns made case-sensitively.
|
|
254
|
+
return node.items.map((item) => ruleOf(item, false, false));
|
|
255
|
+
}
|
|
39
256
|
function stageOf(node) {
|
|
40
257
|
if (node.kind === 'seq')
|
|
41
258
|
return { rules: node.items.map((item) => ruleOf(item, false)) };
|
|
@@ -48,45 +265,55 @@ function stageOf(node) {
|
|
|
48
265
|
fail(rules.line, '"rules" must be a list');
|
|
49
266
|
return { rules: rules.value.items.map((item) => ruleOf(item, ignoreCase)) };
|
|
50
267
|
}
|
|
51
|
-
function ruleOf(node, ignoreCase) {
|
|
268
|
+
function ruleOf(node, ignoreCase, allowCase = true) {
|
|
52
269
|
const entries = mapping(node, 'a rule');
|
|
53
|
-
only(entries, ['any', 'all', 'footer', 'below_last_rule', 'none_after']);
|
|
270
|
+
only(entries, ['any', 'all', 'footer', 'on_footer', 'below_last_rule', 'without_rule', 'none_after']);
|
|
54
271
|
if (entries.length === 0)
|
|
55
272
|
fail(node.line, 'a rule has no primitive');
|
|
56
273
|
const rule = {};
|
|
57
274
|
const any = optional(entries, 'any');
|
|
58
275
|
const all = optional(entries, 'all');
|
|
59
276
|
const footer = optional(entries, 'footer');
|
|
277
|
+
const onFooter = optional(entries, 'on_footer');
|
|
60
278
|
const below = optional(entries, 'below_last_rule');
|
|
279
|
+
const withoutRule = optional(entries, 'without_rule');
|
|
61
280
|
const noneAfter = optional(entries, 'none_after');
|
|
62
281
|
if (any)
|
|
63
|
-
rule.any = patternsOf(any.value, 'any', ignoreCase);
|
|
282
|
+
rule.any = patternsOf(any.value, 'any', ignoreCase, allowCase);
|
|
64
283
|
if (all)
|
|
65
|
-
rule.all = patternsOf(all.value, 'all', ignoreCase);
|
|
284
|
+
rule.all = patternsOf(all.value, 'all', ignoreCase, allowCase);
|
|
66
285
|
if (footer)
|
|
67
286
|
rule.footer = stringOf(footer.value) ?? fail(footer.line, '"footer" must be a string');
|
|
287
|
+
if (onFooter)
|
|
288
|
+
rule.onFooter = boolOf(onFooter.value, 'on_footer');
|
|
68
289
|
if (below)
|
|
69
290
|
rule.belowLastRule = patternOf(stringOf(below.value) ?? fail(below.line, '"below_last_rule" must be a string'), ignoreCase, below.line);
|
|
291
|
+
if (withoutRule)
|
|
292
|
+
rule.withoutRule = boolOf(withoutRule.value, 'without_rule');
|
|
70
293
|
if (noneAfter)
|
|
71
|
-
rule.noneAfter = noneAfterOf(noneAfter.value, ignoreCase);
|
|
294
|
+
rule.noneAfter = noneAfterOf(noneAfter.value, ignoreCase, allowCase);
|
|
72
295
|
return rule;
|
|
73
296
|
}
|
|
74
|
-
function noneAfterOf(node, ignoreCase) {
|
|
297
|
+
function noneAfterOf(node, ignoreCase, allowCase = true) {
|
|
75
298
|
const entries = mapping(node, 'none_after');
|
|
76
299
|
only(entries, ['anchor', 'patterns']);
|
|
77
300
|
const anchor = required(entries, 'anchor', node.line);
|
|
78
301
|
const patterns = required(entries, 'patterns', node.line);
|
|
79
302
|
return {
|
|
80
|
-
anchor: linePattern(anchor.value, ignoreCase),
|
|
81
|
-
patterns: patternsOf(patterns.value, 'patterns', ignoreCase),
|
|
303
|
+
anchor: linePattern(anchor.value, ignoreCase, allowCase),
|
|
304
|
+
patterns: patternsOf(patterns.value, 'patterns', ignoreCase, allowCase),
|
|
82
305
|
};
|
|
83
306
|
}
|
|
84
|
-
function patternsOf(node, key, ignoreCase) {
|
|
307
|
+
function patternsOf(node, key, ignoreCase, allowCase = true) {
|
|
85
308
|
if (node.kind !== 'seq' || node.items.length === 0)
|
|
86
309
|
fail(node.line, `"${key}" must be a non-empty list`);
|
|
87
|
-
return node.items.map((item) => linePattern(item, ignoreCase));
|
|
310
|
+
return node.items.map((item) => linePattern(item, ignoreCase, allowCase));
|
|
88
311
|
}
|
|
89
|
-
|
|
312
|
+
// A pattern is a string, or a mapping with `match`, its optional `except` list and its
|
|
313
|
+
// own `ignore_case`. The pattern's flag is read with the stage's: a screen whose own
|
|
314
|
+
// case varies carries the flag beside the pattern, where a stage-wide flag would reach
|
|
315
|
+
// every rule. The flag is the entry's, so `except` is read on the same lines `match` is.
|
|
316
|
+
function linePattern(node, ignoreCase, allowCase = true) {
|
|
90
317
|
if (node.kind === 'scalar') {
|
|
91
318
|
const text = stringOf(node);
|
|
92
319
|
if (text === null)
|
|
@@ -94,13 +321,17 @@ function linePattern(node, ignoreCase) {
|
|
|
94
321
|
return { match: patternOf(text, ignoreCase, node.line), except: [] };
|
|
95
322
|
}
|
|
96
323
|
const entries = mapping(node, 'a pattern');
|
|
97
|
-
only(entries, ['match', 'except']);
|
|
324
|
+
only(entries, ['match', 'except', 'ignore_case']);
|
|
98
325
|
const match = required(entries, 'match', node.line);
|
|
99
326
|
const text = stringOf(match.value);
|
|
100
327
|
if (text === null)
|
|
101
328
|
fail(match.line, '"match" must be a string');
|
|
329
|
+
const flag = optional(entries, 'ignore_case');
|
|
330
|
+
if (flag && !allowCase)
|
|
331
|
+
fail(flag.line, 'unknown key "ignore_case"');
|
|
332
|
+
const own = flag ? boolOf(flag.value, 'ignore_case') : false;
|
|
102
333
|
const except = optional(entries, 'except');
|
|
103
|
-
return { match: patternOf(text, ignoreCase, match.line), except: except ? exceptOf(except.value, ignoreCase) : [] };
|
|
334
|
+
return { match: patternOf(text, ignoreCase || own, match.line), except: except ? exceptOf(except.value, ignoreCase || own) : [] };
|
|
104
335
|
}
|
|
105
336
|
function exceptOf(node, ignoreCase) {
|
|
106
337
|
if (node.kind !== 'seq' || node.items.length === 0)
|
|
@@ -116,16 +347,42 @@ function composerOf(node) {
|
|
|
116
347
|
const entries = mapping(node, 'composer');
|
|
117
348
|
const mode = required(entries, 'mode', node.line);
|
|
118
349
|
const name = stringOf(mode.value);
|
|
350
|
+
// How the CLI renders its greyed suggestions, read off the input line's styling. Optional:
|
|
351
|
+
// a composer whose suggestions plain text already names needs none of it.
|
|
352
|
+
const style = optional(entries, 'placeholder_style');
|
|
353
|
+
const placeholderStyle = style ? placeholderStyleOf(style) : undefined;
|
|
354
|
+
// How the box continues a line onto its next row, declared only where a capture showed the
|
|
355
|
+
// wrap. A composer without one is read by tiling the typed text's own runs.
|
|
356
|
+
const wrapEntry = optional(entries, 'wrap');
|
|
357
|
+
const wrap = wrapEntry ? wrapOf(wrapEntry) : undefined;
|
|
358
|
+
// The empty rows a capture shows the pane drawing under the text, inside the box's frame —
|
|
359
|
+
// the drop before the status line, or the closing rule. Counted from the captures; a
|
|
360
|
+
// composer whose captures show none declares none, and the box read then keeps every
|
|
361
|
+
// trailing empty row and refuses one the typed text does not have. Don't guess.
|
|
362
|
+
const frameEntry = optional(entries, 'frame_rows');
|
|
363
|
+
const frameRows = frameEntry ? frameRowsOf(frameEntry) : 0;
|
|
119
364
|
if (name === 'box-to-rule') {
|
|
120
|
-
only(entries, ['mode', 'prompt', 'rule', 'placeholders']);
|
|
121
|
-
|
|
365
|
+
only(entries, ['mode', 'prompt', 'rule', 'footers', 'placeholders', 'placeholder_style', 'wrap', 'frame_rows']);
|
|
366
|
+
// For a scrolled-out box, the non-blank lines under the closing rule must match
|
|
367
|
+
// every pattern, in order, and the counts must be equal.
|
|
368
|
+
const footers = optional(entries, 'footers');
|
|
369
|
+
return {
|
|
370
|
+
mode: name,
|
|
371
|
+
prompt: regexField(entries, 'prompt', node.line),
|
|
372
|
+
rule: regexField(entries, 'rule', node.line),
|
|
373
|
+
footers: footers ? footersOf(footers, false) : [],
|
|
374
|
+
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
375
|
+
placeholderStyle,
|
|
376
|
+
wrap,
|
|
377
|
+
frameRows,
|
|
378
|
+
};
|
|
122
379
|
}
|
|
123
380
|
if (name === 'status-last') {
|
|
124
|
-
only(entries, ['mode', 'status_line', 'prompt', 'placeholders']);
|
|
125
|
-
return { mode: name, statusLine: regexField(entries, 'status_line', node.line), prompt: regexField(entries, 'prompt', node.line), placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value) };
|
|
381
|
+
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'placeholder_style', 'wrap', 'frame_rows']);
|
|
382
|
+
return { mode: name, statusLine: regexField(entries, 'status_line', node.line), prompt: regexField(entries, 'prompt', node.line), placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value), placeholderStyle, wrap, frameRows };
|
|
126
383
|
}
|
|
127
384
|
if (name === 'status-then-one') {
|
|
128
|
-
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'strip_suffix', 'fallback']);
|
|
385
|
+
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'placeholder_style', 'strip_suffix', 'fallback', 'wrap', 'frame_rows']);
|
|
129
386
|
const suffix = optional(entries, 'strip_suffix');
|
|
130
387
|
const fallback = required(entries, 'fallback', node.line);
|
|
131
388
|
return {
|
|
@@ -133,32 +390,71 @@ function composerOf(node) {
|
|
|
133
390
|
statusLine: regexField(entries, 'status_line', node.line),
|
|
134
391
|
prompt: regexField(entries, 'prompt', node.line),
|
|
135
392
|
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
136
|
-
|
|
393
|
+
placeholderStyle,
|
|
394
|
+
stripSuffix: suffix ? composerString(suffix.value, 'strip_suffix', suffix.line) : null,
|
|
137
395
|
fallback: fallbackOf(fallback.value),
|
|
396
|
+
wrap,
|
|
397
|
+
frameRows,
|
|
138
398
|
};
|
|
139
399
|
}
|
|
140
400
|
if (name === 'two-rules-footer-below') {
|
|
141
|
-
only(entries, ['mode', 'ignore_case', 'prompt', 'rule', 'footers', 'placeholders']);
|
|
401
|
+
only(entries, ['mode', 'ignore_case', 'prompt', 'rule', 'footers', 'placeholders', 'fold', 'placeholder_style', 'wrap', 'frame_rows']);
|
|
142
402
|
const flag = optional(entries, 'ignore_case');
|
|
143
403
|
const ignoreCase = flag ? boolOf(flag.value, 'ignore_case') : false;
|
|
404
|
+
// Any line below the closing rule matches any pattern in the list.
|
|
144
405
|
const footers = required(entries, 'footers', node.line);
|
|
145
|
-
|
|
146
|
-
fail(footers.line, '"footers" must be a non-empty list');
|
|
406
|
+
const fold = optional(entries, 'fold');
|
|
147
407
|
return {
|
|
148
408
|
mode: name,
|
|
149
409
|
prompt: regexField(entries, 'prompt', node.line, ignoreCase),
|
|
150
410
|
rule: regexField(entries, 'rule', node.line, ignoreCase),
|
|
151
|
-
footers: footers
|
|
152
|
-
const text = stringOf(item);
|
|
153
|
-
if (text === null)
|
|
154
|
-
fail(item.line, '"footers" entries must be strings');
|
|
155
|
-
return patternOf(text, ignoreCase, item.line);
|
|
156
|
-
}),
|
|
411
|
+
footers: footersOf(footers, ignoreCase),
|
|
157
412
|
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
413
|
+
fold: fold ? patternOf(stringOf(fold.value) ?? fail(fold.line, '"fold" must be a string'), ignoreCase, fold.line) : null,
|
|
414
|
+
placeholderStyle,
|
|
415
|
+
wrap,
|
|
416
|
+
frameRows,
|
|
158
417
|
};
|
|
159
418
|
}
|
|
160
419
|
fail(mode.line, `"mode" must be box-to-rule, status-last, status-then-one or two-rules-footer-below`);
|
|
161
420
|
}
|
|
421
|
+
// The composer's wrap rule: the continuation starts at the text column and the break is at a
|
|
422
|
+
// word boundary or hard. Either alone does not join a wrapped box — where it starts and where
|
|
423
|
+
// it breaks are both needed — so both keys are required and nothing else is read.
|
|
424
|
+
function wrapOf(entry) {
|
|
425
|
+
const entries = mapping(entry.value, 'wrap');
|
|
426
|
+
only(entries, ['continuation', 'kind']);
|
|
427
|
+
const continuation = required(entries, 'continuation', entry.line);
|
|
428
|
+
const where = stringOf(continuation.value);
|
|
429
|
+
if (where !== 'text-column')
|
|
430
|
+
fail(continuation.line, '"continuation" must be text-column');
|
|
431
|
+
const kind = required(entries, 'kind', entry.line);
|
|
432
|
+
const shape = stringOf(kind.value);
|
|
433
|
+
if (shape !== 'word' && shape !== 'hard')
|
|
434
|
+
fail(kind.line, '"kind" must be word or hard');
|
|
435
|
+
return { continuation: where, kind: shape };
|
|
436
|
+
}
|
|
437
|
+
// The pane's own empty rows inside a box's frame, as a capture shows them: a whole number,
|
|
438
|
+
// zero or more. Omitting the key is how a composer whose captures show no such row declares
|
|
439
|
+
// zero — the box read then keeps every trailing empty row.
|
|
440
|
+
function frameRowsOf(entry) {
|
|
441
|
+
const value = entry.value;
|
|
442
|
+
if (value.kind !== 'scalar' || typeof value.value !== 'number' || !Number.isInteger(value.value) || value.value < 0) {
|
|
443
|
+
fail(entry.line, '"frame_rows" must be a whole number of rows, zero or more');
|
|
444
|
+
}
|
|
445
|
+
return value.value;
|
|
446
|
+
}
|
|
447
|
+
function footersOf(entry, ignoreCase) {
|
|
448
|
+
if (entry.value.kind !== 'seq' || entry.value.items.length === 0)
|
|
449
|
+
fail(entry.line, '"footers" must be a non-empty list');
|
|
450
|
+
return entry.value.items.map((item) => composerString(item, 'footers', item.line, ignoreCase));
|
|
451
|
+
}
|
|
452
|
+
function placeholderStyleOf(entry) {
|
|
453
|
+
const text = stringOf(entry.value);
|
|
454
|
+
if (text !== 'dim')
|
|
455
|
+
fail(entry.line, '"placeholder_style" must be dim');
|
|
456
|
+
return text;
|
|
457
|
+
}
|
|
162
458
|
function fallbackOf(node) {
|
|
163
459
|
if (node.kind !== 'seq')
|
|
164
460
|
fail(node.line, '"fallback" must be a list');
|
|
@@ -215,10 +511,19 @@ function chromeOf(node) {
|
|
|
215
511
|
}
|
|
216
512
|
function regexField(entries, key, line, ignoreCase = false) {
|
|
217
513
|
const entry = required(entries, key, line);
|
|
218
|
-
|
|
514
|
+
return composerString(entry.value, key, entry.line, ignoreCase);
|
|
515
|
+
}
|
|
516
|
+
// A composer key that reads a pattern string. The dialog shape aims its flag at the one
|
|
517
|
+
// line it sits on; a composer's own patterns — its prompt, rule, footers, status line,
|
|
518
|
+
// suffix — are strings, and the flag among them is refused in words that name the key.
|
|
519
|
+
function composerString(node, key, line, ignoreCase = false) {
|
|
520
|
+
if (node.kind === 'map' && node.entries.some((item) => item.key === 'ignore_case')) {
|
|
521
|
+
fail(line, `"${key}" cannot ignore case: only a dialog pattern may`);
|
|
522
|
+
}
|
|
523
|
+
const text = stringOf(node);
|
|
219
524
|
if (text === null)
|
|
220
|
-
fail(
|
|
221
|
-
return patternOf(text, ignoreCase,
|
|
525
|
+
fail(line, `"${key}" must be a string`);
|
|
526
|
+
return patternOf(text, ignoreCase, line);
|
|
222
527
|
}
|
|
223
528
|
function patternOf(text, ignoreCase, line) {
|
|
224
529
|
try {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Screen } from './screen.ts';
|
|
2
|
+
export type ComposerReading = {
|
|
3
|
+
kind: Screen['kind'];
|
|
4
|
+
from?: number;
|
|
5
|
+
input?: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Every hatch predicate is monotone toward caution, running alongside the data stage for unknown, trust, permission, question, and working so a hatch can only add caution, never remove it.
|
|
9
|
+
* A composer comes from data or from the hatch, never both, and a profile that has a data composer while its screen_module exports a composer is refused at load.
|
|
10
|
+
* The guarantees cover what a hatch returns and what load accepts; a hatch is trusted package code, not a sandbox.
|
|
11
|
+
*/
|
|
12
|
+
export interface ScreenProfile {
|
|
13
|
+
unknown?: (lines: string[]) => boolean;
|
|
14
|
+
trust?: (lines: string[]) => boolean;
|
|
15
|
+
permission?: (lines: string[]) => boolean;
|
|
16
|
+
question?: (lines: string[]) => boolean;
|
|
17
|
+
working?: (lines: string[]) => boolean;
|
|
18
|
+
composer?: (lines: string[]) => ComposerReading;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|