team 0.1.2 → 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 +22 -11
- package/dist/approve/approval.js +14 -9
- package/dist/approve/fingerprint.d.ts +14 -2
- package/dist/approve/fingerprint.js +21 -21
- package/dist/budgets/gate.js +7 -2
- package/dist/budgets/readings.d.ts +6 -2
- package/dist/budgets/readings.js +28 -2
- package/dist/budgets/run.js +8 -2
- package/dist/budgets/table.d.ts +2 -0
- package/dist/budgets/table.js +10 -5
- 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 +7 -5
- package/dist/commands/approve.js +17 -1
- package/dist/commands/doctor.d.ts +15 -1
- package/dist/commands/doctor.js +126 -8
- package/dist/commands/down.d.ts +4 -0
- package/dist/commands/down.js +32 -13
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/remove.js +5 -2
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +19 -1
- package/dist/commands/up.js +3 -1
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +28 -4
- 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 +1 -0
- package/dist/file/validate.d.ts +4 -12
- package/dist/file/validate.js +45 -688
- package/dist/herdr.d.ts +1 -0
- package/dist/herdr.js +11 -1
- package/dist/launch/agent.js +4 -3
- package/dist/launch/deliver.d.ts +4 -0
- package/dist/launch/deliver.js +151 -8
- package/dist/launch/execute.d.ts +2 -0
- package/dist/launch/execute.js +21 -11
- package/dist/launch/plan.js +16 -2
- package/dist/profiles/antigravity.yaml +12 -2
- package/dist/profiles/claude-code.yaml +21 -6
- package/dist/profiles/codex.yaml +14 -1
- package/dist/profiles/cursor.yaml +45 -5
- 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 +2 -0
- package/dist/status/compare.js +7 -4
- package/dist/store/store.d.ts +5 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/dist/watch/checks/budget.js +49 -53
- package/dist/watch/dialect.d.ts +6 -1
- package/dist/watch/dialect.js +68 -12
- package/dist/watch/pass.d.ts +8 -1
- package/dist/watch/pass.js +19 -9
- package/dist/watch/screen-core.d.ts +41 -4
- package/dist/watch/screen-core.js +458 -93
- package/dist/watch/screen-data.d.ts +25 -0
- package/dist/watch/screen-file.d.ts +8 -2
- package/dist/watch/screen-file.js +325 -38
- package/dist/watch/screen-profile.d.ts +19 -0
- package/dist/watch/screen-profile.js +1 -0
- package/dist/watch/screen.d.ts +22 -4
- package/dist/watch/screen.js +39 -5
- 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;
|
|
@@ -25,6 +26,13 @@ export type Placeholder = {
|
|
|
25
26
|
} | {
|
|
26
27
|
prefix: string;
|
|
27
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
|
+
};
|
|
28
36
|
export type FallbackRule = {
|
|
29
37
|
all: LinePattern[];
|
|
30
38
|
kind: Screen['kind'];
|
|
@@ -34,8 +42,15 @@ type Box = {
|
|
|
34
42
|
mode: 'box-to-rule';
|
|
35
43
|
prompt: RegExp;
|
|
36
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[];
|
|
37
50
|
placeholders: Placeholder[];
|
|
38
51
|
placeholderStyle?: PlaceholderStyle;
|
|
52
|
+
wrap?: Wrap;
|
|
53
|
+
frameRows: number;
|
|
39
54
|
};
|
|
40
55
|
type StatusLast = {
|
|
41
56
|
mode: 'status-last';
|
|
@@ -43,6 +58,8 @@ type StatusLast = {
|
|
|
43
58
|
prompt: RegExp;
|
|
44
59
|
placeholders: Placeholder[];
|
|
45
60
|
placeholderStyle?: PlaceholderStyle;
|
|
61
|
+
wrap?: Wrap;
|
|
62
|
+
frameRows: number;
|
|
46
63
|
};
|
|
47
64
|
type StatusThenOne = {
|
|
48
65
|
mode: 'status-then-one';
|
|
@@ -52,14 +69,21 @@ type StatusThenOne = {
|
|
|
52
69
|
placeholderStyle?: PlaceholderStyle;
|
|
53
70
|
stripSuffix: RegExp | null;
|
|
54
71
|
fallback: FallbackRule[];
|
|
72
|
+
wrap?: Wrap;
|
|
73
|
+
frameRows: number;
|
|
55
74
|
};
|
|
56
75
|
type TwoRules = {
|
|
57
76
|
mode: 'two-rules-footer-below';
|
|
58
77
|
prompt: RegExp;
|
|
59
78
|
rule: RegExp;
|
|
79
|
+
/** Any line below the closing rule matches any pattern in the list. */
|
|
60
80
|
footers: RegExp[];
|
|
61
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;
|
|
62
84
|
placeholderStyle?: PlaceholderStyle;
|
|
85
|
+
wrap?: Wrap;
|
|
86
|
+
frameRows: number;
|
|
63
87
|
};
|
|
64
88
|
export type Composer = Box | StatusLast | StatusThenOne | TwoRules;
|
|
65
89
|
export type ScreenData = {
|
|
@@ -70,5 +94,6 @@ export type ScreenData = {
|
|
|
70
94
|
question?: Stage;
|
|
71
95
|
working?: Stage;
|
|
72
96
|
composer: Composer;
|
|
97
|
+
profile?: ScreenProfile;
|
|
73
98
|
};
|
|
74
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,7 +265,7 @@ 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
270
|
only(entries, ['any', 'all', 'footer', 'on_footer', 'below_last_rule', 'without_rule', 'none_after']);
|
|
54
271
|
if (entries.length === 0)
|
|
@@ -62,9 +279,9 @@ function ruleOf(node, ignoreCase) {
|
|
|
62
279
|
const withoutRule = optional(entries, 'without_rule');
|
|
63
280
|
const noneAfter = optional(entries, 'none_after');
|
|
64
281
|
if (any)
|
|
65
|
-
rule.any = patternsOf(any.value, 'any', ignoreCase);
|
|
282
|
+
rule.any = patternsOf(any.value, 'any', ignoreCase, allowCase);
|
|
66
283
|
if (all)
|
|
67
|
-
rule.all = patternsOf(all.value, 'all', ignoreCase);
|
|
284
|
+
rule.all = patternsOf(all.value, 'all', ignoreCase, allowCase);
|
|
68
285
|
if (footer)
|
|
69
286
|
rule.footer = stringOf(footer.value) ?? fail(footer.line, '"footer" must be a string');
|
|
70
287
|
if (onFooter)
|
|
@@ -74,25 +291,29 @@ function ruleOf(node, ignoreCase) {
|
|
|
74
291
|
if (withoutRule)
|
|
75
292
|
rule.withoutRule = boolOf(withoutRule.value, 'without_rule');
|
|
76
293
|
if (noneAfter)
|
|
77
|
-
rule.noneAfter = noneAfterOf(noneAfter.value, ignoreCase);
|
|
294
|
+
rule.noneAfter = noneAfterOf(noneAfter.value, ignoreCase, allowCase);
|
|
78
295
|
return rule;
|
|
79
296
|
}
|
|
80
|
-
function noneAfterOf(node, ignoreCase) {
|
|
297
|
+
function noneAfterOf(node, ignoreCase, allowCase = true) {
|
|
81
298
|
const entries = mapping(node, 'none_after');
|
|
82
299
|
only(entries, ['anchor', 'patterns']);
|
|
83
300
|
const anchor = required(entries, 'anchor', node.line);
|
|
84
301
|
const patterns = required(entries, 'patterns', node.line);
|
|
85
302
|
return {
|
|
86
|
-
anchor: linePattern(anchor.value, ignoreCase),
|
|
87
|
-
patterns: patternsOf(patterns.value, 'patterns', ignoreCase),
|
|
303
|
+
anchor: linePattern(anchor.value, ignoreCase, allowCase),
|
|
304
|
+
patterns: patternsOf(patterns.value, 'patterns', ignoreCase, allowCase),
|
|
88
305
|
};
|
|
89
306
|
}
|
|
90
|
-
function patternsOf(node, key, ignoreCase) {
|
|
307
|
+
function patternsOf(node, key, ignoreCase, allowCase = true) {
|
|
91
308
|
if (node.kind !== 'seq' || node.items.length === 0)
|
|
92
309
|
fail(node.line, `"${key}" must be a non-empty list`);
|
|
93
|
-
return node.items.map((item) => linePattern(item, ignoreCase));
|
|
310
|
+
return node.items.map((item) => linePattern(item, ignoreCase, allowCase));
|
|
94
311
|
}
|
|
95
|
-
|
|
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) {
|
|
96
317
|
if (node.kind === 'scalar') {
|
|
97
318
|
const text = stringOf(node);
|
|
98
319
|
if (text === null)
|
|
@@ -100,13 +321,17 @@ function linePattern(node, ignoreCase) {
|
|
|
100
321
|
return { match: patternOf(text, ignoreCase, node.line), except: [] };
|
|
101
322
|
}
|
|
102
323
|
const entries = mapping(node, 'a pattern');
|
|
103
|
-
only(entries, ['match', 'except']);
|
|
324
|
+
only(entries, ['match', 'except', 'ignore_case']);
|
|
104
325
|
const match = required(entries, 'match', node.line);
|
|
105
326
|
const text = stringOf(match.value);
|
|
106
327
|
if (text === null)
|
|
107
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;
|
|
108
333
|
const except = optional(entries, 'except');
|
|
109
|
-
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) : [] };
|
|
110
335
|
}
|
|
111
336
|
function exceptOf(node, ignoreCase) {
|
|
112
337
|
if (node.kind !== 'seq' || node.items.length === 0)
|
|
@@ -126,16 +351,38 @@ function composerOf(node) {
|
|
|
126
351
|
// a composer whose suggestions plain text already names needs none of it.
|
|
127
352
|
const style = optional(entries, 'placeholder_style');
|
|
128
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;
|
|
129
364
|
if (name === 'box-to-rule') {
|
|
130
|
-
only(entries, ['mode', 'prompt', 'rule', 'placeholders', 'placeholder_style']);
|
|
131
|
-
|
|
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
|
+
};
|
|
132
379
|
}
|
|
133
380
|
if (name === 'status-last') {
|
|
134
|
-
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'placeholder_style']);
|
|
135
|
-
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 };
|
|
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 };
|
|
136
383
|
}
|
|
137
384
|
if (name === 'status-then-one') {
|
|
138
|
-
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'placeholder_style', 'strip_suffix', 'fallback']);
|
|
385
|
+
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'placeholder_style', 'strip_suffix', 'fallback', 'wrap', 'frame_rows']);
|
|
139
386
|
const suffix = optional(entries, 'strip_suffix');
|
|
140
387
|
const fallback = required(entries, 'fallback', node.line);
|
|
141
388
|
return {
|
|
@@ -144,33 +391,64 @@ function composerOf(node) {
|
|
|
144
391
|
prompt: regexField(entries, 'prompt', node.line),
|
|
145
392
|
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
146
393
|
placeholderStyle,
|
|
147
|
-
stripSuffix: suffix ?
|
|
394
|
+
stripSuffix: suffix ? composerString(suffix.value, 'strip_suffix', suffix.line) : null,
|
|
148
395
|
fallback: fallbackOf(fallback.value),
|
|
396
|
+
wrap,
|
|
397
|
+
frameRows,
|
|
149
398
|
};
|
|
150
399
|
}
|
|
151
400
|
if (name === 'two-rules-footer-below') {
|
|
152
|
-
only(entries, ['mode', 'ignore_case', 'prompt', 'rule', 'footers', 'placeholders', 'placeholder_style']);
|
|
401
|
+
only(entries, ['mode', 'ignore_case', 'prompt', 'rule', 'footers', 'placeholders', 'fold', 'placeholder_style', 'wrap', 'frame_rows']);
|
|
153
402
|
const flag = optional(entries, 'ignore_case');
|
|
154
403
|
const ignoreCase = flag ? boolOf(flag.value, 'ignore_case') : false;
|
|
404
|
+
// Any line below the closing rule matches any pattern in the list.
|
|
155
405
|
const footers = required(entries, 'footers', node.line);
|
|
156
|
-
|
|
157
|
-
fail(footers.line, '"footers" must be a non-empty list');
|
|
406
|
+
const fold = optional(entries, 'fold');
|
|
158
407
|
return {
|
|
159
408
|
mode: name,
|
|
160
409
|
prompt: regexField(entries, 'prompt', node.line, ignoreCase),
|
|
161
410
|
rule: regexField(entries, 'rule', node.line, ignoreCase),
|
|
162
|
-
footers: footers
|
|
163
|
-
const text = stringOf(item);
|
|
164
|
-
if (text === null)
|
|
165
|
-
fail(item.line, '"footers" entries must be strings');
|
|
166
|
-
return patternOf(text, ignoreCase, item.line);
|
|
167
|
-
}),
|
|
411
|
+
footers: footersOf(footers, ignoreCase),
|
|
168
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,
|
|
169
414
|
placeholderStyle,
|
|
415
|
+
wrap,
|
|
416
|
+
frameRows,
|
|
170
417
|
};
|
|
171
418
|
}
|
|
172
419
|
fail(mode.line, `"mode" must be box-to-rule, status-last, status-then-one or two-rules-footer-below`);
|
|
173
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
|
+
}
|
|
174
452
|
function placeholderStyleOf(entry) {
|
|
175
453
|
const text = stringOf(entry.value);
|
|
176
454
|
if (text !== 'dim')
|
|
@@ -233,10 +511,19 @@ function chromeOf(node) {
|
|
|
233
511
|
}
|
|
234
512
|
function regexField(entries, key, line, ignoreCase = false) {
|
|
235
513
|
const entry = required(entries, key, line);
|
|
236
|
-
|
|
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);
|
|
237
524
|
if (text === null)
|
|
238
|
-
fail(
|
|
239
|
-
return patternOf(text, ignoreCase,
|
|
525
|
+
fail(line, `"${key}" must be a string`);
|
|
526
|
+
return patternOf(text, ignoreCase, line);
|
|
240
527
|
}
|
|
241
528
|
function patternOf(text, ignoreCase, line) {
|
|
242
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 {};
|
package/dist/watch/screen.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type Box, type Fold } from './screen-core.ts';
|
|
2
|
+
import type { ScreenData } from './screen-data.ts';
|
|
3
|
+
export type { Box, Fold };
|
|
1
4
|
export type Screen = {
|
|
2
5
|
kind: 'idle';
|
|
3
6
|
} | {
|
|
@@ -13,13 +16,28 @@ export type Screen = {
|
|
|
13
16
|
} | {
|
|
14
17
|
kind: 'unknown';
|
|
15
18
|
};
|
|
19
|
+
/** The shipped screen for a CLI, or null when this version has none. */
|
|
20
|
+
export declare function screenData(cli: string): ScreenData | null;
|
|
21
|
+
/** The same classification `classify` runs, against a screen that may carry added patterns. */
|
|
22
|
+
export declare function classifyData(data: ScreenData, lines: string[]): Screen;
|
|
16
23
|
/** Every stage, in the core's order. The first one that matches wins. */
|
|
17
|
-
export declare function classify(cli: string, lines: string[]): Screen;
|
|
24
|
+
export declare function classify(cli: string | ScreenData, lines: string[]): Screen;
|
|
18
25
|
/** The composer alone. A running turn would otherwise hide an empty input box. */
|
|
19
|
-
export declare function classifyComposer(cli: string, lines: string[]): Screen;
|
|
20
|
-
export declare function readScreen(cli: string, screen: string | undefined): Screen;
|
|
26
|
+
export declare function classifyComposer(cli: string | ScreenData, lines: string[]): Screen;
|
|
27
|
+
export declare function readScreen(cli: string | ScreenData, screen: string | undefined): Screen;
|
|
28
|
+
/** The folded form of a paste the pane's composer shows, or null. The shape only; the caller
|
|
29
|
+
* verifies it holds the text it typed before trusting it. */
|
|
30
|
+
export declare function readFold(cli: string | ScreenData, screen: string | undefined): Fold | null;
|
|
31
|
+
/** Whether the pane shows a fold marker at all, whatever count it names. The case `readFold`
|
|
32
|
+
* cannot report — a marker claiming zero hidden rows — must still keep an Enter from taking the
|
|
33
|
+
* box for an ordinary unsent one. */
|
|
34
|
+
export declare function readFoldMark(cli: string, screen: string | undefined): boolean;
|
|
35
|
+
/** The input box the composer draws for its current text, or null when it does not read `unsent`.
|
|
36
|
+
* The box's rows are the rows the pane drew for the text; the caller compares them to the text
|
|
37
|
+
* it typed before any Enter. */
|
|
38
|
+
export declare function readBox(cli: string, screen: string | undefined): Box | null;
|
|
21
39
|
/** The one line a quota figure may come from: the composer's own status row, in the pane's last
|
|
22
40
|
* 20 lines, read plain — a figure is never styled. Null for a CLI with no status line, and for
|
|
23
41
|
* a window that shows no status row — a dialog, a question, a trust screen, a shell or an
|
|
24
42
|
* unknown one reads no figures. */
|
|
25
|
-
export declare function statusRow(cli: string, screen: string | undefined): string | null;
|
|
43
|
+
export declare function statusRow(cli: string | ScreenData, screen: string | undefined): string | null;
|