team 0.0.1 → 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 +310 -0
- package/dist/approve/approval.d.ts +38 -0
- package/dist/approve/approval.js +78 -0
- package/dist/approve/diff.d.ts +13 -0
- package/dist/approve/diff.js +47 -0
- package/dist/approve/fingerprint.d.ts +44 -0
- package/dist/approve/fingerprint.js +109 -0
- package/dist/args.d.ts +7 -0
- package/dist/args.js +22 -0
- package/dist/budgets/checks.d.ts +35 -0
- package/dist/budgets/checks.js +77 -0
- package/dist/budgets/gate.d.ts +20 -0
- package/dist/budgets/gate.js +96 -0
- package/dist/budgets/readings.d.ts +47 -0
- package/dist/budgets/readings.js +119 -0
- package/dist/budgets/run.d.ts +66 -0
- package/dist/budgets/run.js +119 -0
- package/dist/budgets/table.d.ts +24 -0
- package/dist/budgets/table.js +90 -0
- package/dist/caller.d.ts +48 -0
- package/dist/caller.js +126 -0
- package/dist/check/config.d.ts +51 -0
- package/dist/check/config.js +50 -0
- package/dist/check/git.d.ts +27 -0
- package/dist/check/git.js +97 -0
- package/dist/check/message.d.ts +27 -0
- package/dist/check/message.js +101 -0
- package/dist/check/run.d.ts +31 -0
- package/dist/check/run.js +77 -0
- package/dist/check/signature.d.ts +10 -0
- package/dist/check/signature.js +22 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +76 -0
- package/dist/clis.d.ts +7 -0
- package/dist/clis.js +9 -0
- package/dist/commands/add.d.ts +23 -0
- package/dist/commands/add.js +445 -0
- package/dist/commands/approve.d.ts +11 -0
- package/dist/commands/approve.js +146 -0
- package/dist/commands/check.d.ts +17 -0
- package/dist/commands/check.js +103 -0
- package/dist/commands/doctor.d.ts +38 -0
- package/dist/commands/doctor.js +232 -0
- package/dist/commands/down.d.ts +37 -0
- package/dist/commands/down.js +258 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +138 -0
- package/dist/commands/remove.d.ts +23 -0
- package/dist/commands/remove.js +235 -0
- package/dist/commands/status.d.ts +41 -0
- package/dist/commands/status.js +147 -0
- package/dist/commands/up.d.ts +49 -0
- package/dist/commands/up.js +361 -0
- package/dist/commands/watch.d.ts +35 -0
- package/dist/commands/watch.js +302 -0
- package/dist/commands/worktree.d.ts +12 -0
- package/dist/commands/worktree.js +388 -0
- package/dist/end/condition.d.ts +28 -0
- package/dist/end/condition.js +77 -0
- package/dist/file/current.d.ts +13 -0
- package/dist/file/current.js +53 -0
- package/dist/file/lines.d.ts +25 -0
- package/dist/file/lines.js +277 -0
- package/dist/file/load.d.ts +7 -0
- package/dist/file/load.js +90 -0
- package/dist/file/paths.d.ts +6 -0
- package/dist/file/paths.js +77 -0
- package/dist/file/secrets.d.ts +6 -0
- package/dist/file/secrets.js +54 -0
- package/dist/file/signature.d.ts +13 -0
- package/dist/file/signature.js +36 -0
- package/dist/file/types.d.ts +130 -0
- package/dist/file/types.js +1 -0
- package/dist/file/validate.d.ts +13 -0
- package/dist/file/validate.js +702 -0
- package/dist/file/write.d.ts +11 -0
- package/dist/file/write.js +13 -0
- package/dist/herdr.d.ts +38 -0
- package/dist/herdr.js +227 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/io.d.ts +10 -0
- package/dist/io.js +1 -0
- package/dist/launch/deliver.d.ts +10 -0
- package/dist/launch/deliver.js +39 -0
- package/dist/launch/execute.d.ts +46 -0
- package/dist/launch/execute.js +284 -0
- package/dist/launch/plan.d.ts +185 -0
- package/dist/launch/plan.js +280 -0
- package/dist/launch/rules.d.ts +25 -0
- package/dist/launch/rules.js +36 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.js +24 -0
- package/dist/profiles/antigravity.yaml +66 -0
- package/dist/profiles/claude-code.yaml +69 -0
- package/dist/profiles/codex.yaml +77 -0
- package/dist/profiles/cursor.yaml +68 -0
- package/dist/profiles/index.d.ts +1 -0
- package/dist/profiles/index.js +1 -0
- package/dist/profiles/profile.d.ts +60 -0
- package/dist/profiles/profile.js +313 -0
- package/dist/profiles/quota.d.ts +22 -0
- package/dist/profiles/quota.js +56 -0
- package/dist/state.d.ts +59 -0
- package/dist/state.js +127 -0
- package/dist/status/compare.d.ts +26 -0
- package/dist/status/compare.js +110 -0
- package/dist/status/statusline.d.ts +9 -0
- package/dist/status/statusline.js +26 -0
- package/dist/store/store.d.ts +62 -0
- package/dist/store/store.js +116 -0
- package/dist/watch/check.d.ts +82 -0
- package/dist/watch/check.js +29 -0
- package/dist/watch/checks/approval.d.ts +2 -0
- package/dist/watch/checks/approval.js +16 -0
- package/dist/watch/checks/attention.d.ts +2 -0
- package/dist/watch/checks/attention.js +23 -0
- package/dist/watch/checks/budget.d.ts +2 -0
- package/dist/watch/checks/budget.js +141 -0
- package/dist/watch/checks/disk.d.ts +2 -0
- package/dist/watch/checks/disk.js +13 -0
- package/dist/watch/checks/extra.d.ts +2 -0
- package/dist/watch/checks/extra.js +18 -0
- package/dist/watch/checks/idle.d.ts +2 -0
- package/dist/watch/checks/idle.js +32 -0
- package/dist/watch/checks/load.d.ts +2 -0
- package/dist/watch/checks/load.js +11 -0
- package/dist/watch/checks/memory.d.ts +2 -0
- package/dist/watch/checks/memory.js +11 -0
- package/dist/watch/checks/missing.d.ts +2 -0
- package/dist/watch/checks/missing.js +12 -0
- package/dist/watch/checks/model-drift.d.ts +2 -0
- package/dist/watch/checks/model-drift.js +14 -0
- package/dist/watch/checks/swap-free.d.ts +2 -0
- package/dist/watch/checks/swap-free.js +12 -0
- package/dist/watch/checks/swap-growth.d.ts +2 -0
- package/dist/watch/checks/swap-growth.js +18 -0
- package/dist/watch/checks/team-idle.d.ts +2 -0
- package/dist/watch/checks/team-idle.js +18 -0
- package/dist/watch/checks/unsent.d.ts +2 -0
- package/dist/watch/checks/unsent.js +18 -0
- package/dist/watch/close.d.ts +19 -0
- package/dist/watch/close.js +40 -0
- package/dist/watch/dialect.d.ts +5 -0
- package/dist/watch/dialect.js +430 -0
- package/dist/watch/end.d.ts +11 -0
- package/dist/watch/end.js +17 -0
- package/dist/watch/machine.d.ts +38 -0
- package/dist/watch/machine.js +137 -0
- package/dist/watch/notify.d.ts +1 -0
- package/dist/watch/notify.js +15 -0
- package/dist/watch/pass.d.ts +30 -0
- package/dist/watch/pass.js +237 -0
- package/dist/watch/screen-core.d.ts +14 -0
- package/dist/watch/screen-core.js +320 -0
- package/dist/watch/screen-data.d.ts +65 -0
- package/dist/watch/screen-data.js +1 -0
- package/dist/watch/screen-file.d.ts +2 -0
- package/dist/watch/screen-file.js +269 -0
- package/dist/watch/screen.d.ts +20 -0
- package/dist/watch/screen.js +38 -0
- package/dist/worktree/place.d.ts +18 -0
- package/dist/worktree/place.js +126 -0
- package/dist/yaml.d.ts +30 -0
- package/dist/yaml.js +380 -0
- package/examples/checks/codex-quota +217 -0
- package/examples/team.yaml +76 -0
- package/package.json +29 -10
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// A profile file, in team's YAML subset, checked as it is loaded. The JSON Schema
|
|
2
|
+
// next to the profiles describes the same shape; this is what actually refuses a file,
|
|
3
|
+
// because the package does not carry a schema validator.
|
|
4
|
+
import { DialectError, compilePattern } from "./dialect.js";
|
|
5
|
+
import { YamlError, parseYaml } from "../yaml.js";
|
|
6
|
+
const STAGES = ['unknown', 'trust', 'permission', 'question', 'working'];
|
|
7
|
+
const KINDS = ['idle', 'working', 'unsent', 'permission', 'trust', 'question', 'unknown'];
|
|
8
|
+
const CHOICE_SAMPLES = ['❯ 1. Yes', '› 1. Yes', '> 1. Yes', '❯ 1.', '› 1.', '> 1.'];
|
|
9
|
+
export function loadScreen(text) {
|
|
10
|
+
const root = parseYaml(text);
|
|
11
|
+
const entries = mapping(root, 'a profile');
|
|
12
|
+
// 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']);
|
|
14
|
+
const format = required(entries, 'format', root.line);
|
|
15
|
+
if (format.value.kind !== 'scalar' || format.value.value !== 1)
|
|
16
|
+
fail(format.line, '"format" must be 1');
|
|
17
|
+
const cli = required(entries, 'cli', root.line);
|
|
18
|
+
if (!stringOf(cli.value))
|
|
19
|
+
fail(cli.line, '"cli" must be a string');
|
|
20
|
+
const screen = required(entries, 'screen', root.line);
|
|
21
|
+
return screenOf(screen.value);
|
|
22
|
+
}
|
|
23
|
+
function screenOf(node) {
|
|
24
|
+
const entries = mapping(node, 'screen');
|
|
25
|
+
only(entries, ['chrome', 'composer', ...STAGES]);
|
|
26
|
+
const chrome = optional(entries, 'chrome');
|
|
27
|
+
const composer = required(entries, 'composer', node.line);
|
|
28
|
+
const data = {
|
|
29
|
+
chrome: chrome ? chromeOf(chrome.value) : [],
|
|
30
|
+
composer: composerOf(composer.value),
|
|
31
|
+
};
|
|
32
|
+
for (const name of STAGES) {
|
|
33
|
+
const entry = optional(entries, name);
|
|
34
|
+
if (entry)
|
|
35
|
+
data[name] = stageOf(entry.value);
|
|
36
|
+
}
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
function stageOf(node) {
|
|
40
|
+
if (node.kind === 'seq')
|
|
41
|
+
return { rules: node.items.map((item) => ruleOf(item, false)) };
|
|
42
|
+
const entries = mapping(node, 'a stage');
|
|
43
|
+
only(entries, ['ignore_case', 'rules']);
|
|
44
|
+
const flag = optional(entries, 'ignore_case');
|
|
45
|
+
const ignoreCase = flag ? boolOf(flag.value, 'ignore_case') : false;
|
|
46
|
+
const rules = required(entries, 'rules', node.line);
|
|
47
|
+
if (rules.value.kind !== 'seq')
|
|
48
|
+
fail(rules.line, '"rules" must be a list');
|
|
49
|
+
return { rules: rules.value.items.map((item) => ruleOf(item, ignoreCase)) };
|
|
50
|
+
}
|
|
51
|
+
function ruleOf(node, ignoreCase) {
|
|
52
|
+
const entries = mapping(node, 'a rule');
|
|
53
|
+
only(entries, ['any', 'all', 'footer', 'below_last_rule', 'none_after']);
|
|
54
|
+
if (entries.length === 0)
|
|
55
|
+
fail(node.line, 'a rule has no primitive');
|
|
56
|
+
const rule = {};
|
|
57
|
+
const any = optional(entries, 'any');
|
|
58
|
+
const all = optional(entries, 'all');
|
|
59
|
+
const footer = optional(entries, 'footer');
|
|
60
|
+
const below = optional(entries, 'below_last_rule');
|
|
61
|
+
const noneAfter = optional(entries, 'none_after');
|
|
62
|
+
if (any)
|
|
63
|
+
rule.any = patternsOf(any.value, 'any', ignoreCase);
|
|
64
|
+
if (all)
|
|
65
|
+
rule.all = patternsOf(all.value, 'all', ignoreCase);
|
|
66
|
+
if (footer)
|
|
67
|
+
rule.footer = stringOf(footer.value) ?? fail(footer.line, '"footer" must be a string');
|
|
68
|
+
if (below)
|
|
69
|
+
rule.belowLastRule = patternOf(stringOf(below.value) ?? fail(below.line, '"below_last_rule" must be a string'), ignoreCase, below.line);
|
|
70
|
+
if (noneAfter)
|
|
71
|
+
rule.noneAfter = noneAfterOf(noneAfter.value, ignoreCase);
|
|
72
|
+
return rule;
|
|
73
|
+
}
|
|
74
|
+
function noneAfterOf(node, ignoreCase) {
|
|
75
|
+
const entries = mapping(node, 'none_after');
|
|
76
|
+
only(entries, ['anchor', 'patterns']);
|
|
77
|
+
const anchor = required(entries, 'anchor', node.line);
|
|
78
|
+
const patterns = required(entries, 'patterns', node.line);
|
|
79
|
+
return {
|
|
80
|
+
anchor: linePattern(anchor.value, ignoreCase),
|
|
81
|
+
patterns: patternsOf(patterns.value, 'patterns', ignoreCase),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function patternsOf(node, key, ignoreCase) {
|
|
85
|
+
if (node.kind !== 'seq' || node.items.length === 0)
|
|
86
|
+
fail(node.line, `"${key}" must be a non-empty list`);
|
|
87
|
+
return node.items.map((item) => linePattern(item, ignoreCase));
|
|
88
|
+
}
|
|
89
|
+
function linePattern(node, ignoreCase) {
|
|
90
|
+
if (node.kind === 'scalar') {
|
|
91
|
+
const text = stringOf(node);
|
|
92
|
+
if (text === null)
|
|
93
|
+
fail(node.line, 'a pattern must be a string');
|
|
94
|
+
return { match: patternOf(text, ignoreCase, node.line), except: [] };
|
|
95
|
+
}
|
|
96
|
+
const entries = mapping(node, 'a pattern');
|
|
97
|
+
only(entries, ['match', 'except']);
|
|
98
|
+
const match = required(entries, 'match', node.line);
|
|
99
|
+
const text = stringOf(match.value);
|
|
100
|
+
if (text === null)
|
|
101
|
+
fail(match.line, '"match" must be a string');
|
|
102
|
+
const except = optional(entries, 'except');
|
|
103
|
+
return { match: patternOf(text, ignoreCase, match.line), except: except ? exceptOf(except.value, ignoreCase) : [] };
|
|
104
|
+
}
|
|
105
|
+
function exceptOf(node, ignoreCase) {
|
|
106
|
+
if (node.kind !== 'seq' || node.items.length === 0)
|
|
107
|
+
fail(node.line, '"except" must be a non-empty list');
|
|
108
|
+
return node.items.map((item) => {
|
|
109
|
+
const text = stringOf(item);
|
|
110
|
+
if (text === null)
|
|
111
|
+
fail(item.line, '"except" entries must be strings');
|
|
112
|
+
return patternOf(text, ignoreCase, item.line);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function composerOf(node) {
|
|
116
|
+
const entries = mapping(node, 'composer');
|
|
117
|
+
const mode = required(entries, 'mode', node.line);
|
|
118
|
+
const name = stringOf(mode.value);
|
|
119
|
+
if (name === 'box-to-rule') {
|
|
120
|
+
only(entries, ['mode', 'prompt', 'rule', 'placeholders']);
|
|
121
|
+
return { mode: name, prompt: regexField(entries, 'prompt', node.line), rule: regexField(entries, 'rule', node.line), placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value) };
|
|
122
|
+
}
|
|
123
|
+
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) };
|
|
126
|
+
}
|
|
127
|
+
if (name === 'status-then-one') {
|
|
128
|
+
only(entries, ['mode', 'status_line', 'prompt', 'placeholders', 'strip_suffix', 'fallback']);
|
|
129
|
+
const suffix = optional(entries, 'strip_suffix');
|
|
130
|
+
const fallback = required(entries, 'fallback', node.line);
|
|
131
|
+
return {
|
|
132
|
+
mode: name,
|
|
133
|
+
statusLine: regexField(entries, 'status_line', node.line),
|
|
134
|
+
prompt: regexField(entries, 'prompt', node.line),
|
|
135
|
+
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
136
|
+
stripSuffix: suffix ? patternOf(stringOf(suffix.value) ?? fail(suffix.line, '"strip_suffix" must be a string'), false, suffix.line) : null,
|
|
137
|
+
fallback: fallbackOf(fallback.value),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (name === 'two-rules-footer-below') {
|
|
141
|
+
only(entries, ['mode', 'ignore_case', 'prompt', 'rule', 'footers', 'placeholders']);
|
|
142
|
+
const flag = optional(entries, 'ignore_case');
|
|
143
|
+
const ignoreCase = flag ? boolOf(flag.value, 'ignore_case') : false;
|
|
144
|
+
const footers = required(entries, 'footers', node.line);
|
|
145
|
+
if (footers.value.kind !== 'seq' || footers.value.items.length === 0)
|
|
146
|
+
fail(footers.line, '"footers" must be a non-empty list');
|
|
147
|
+
return {
|
|
148
|
+
mode: name,
|
|
149
|
+
prompt: regexField(entries, 'prompt', node.line, ignoreCase),
|
|
150
|
+
rule: regexField(entries, 'rule', node.line, ignoreCase),
|
|
151
|
+
footers: footers.value.items.map((item) => {
|
|
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
|
+
}),
|
|
157
|
+
placeholders: placeholdersOf(required(entries, 'placeholders', node.line).value),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
fail(mode.line, `"mode" must be box-to-rule, status-last, status-then-one or two-rules-footer-below`);
|
|
161
|
+
}
|
|
162
|
+
function fallbackOf(node) {
|
|
163
|
+
if (node.kind !== 'seq')
|
|
164
|
+
fail(node.line, '"fallback" must be a list');
|
|
165
|
+
return node.items.map((item) => {
|
|
166
|
+
const entries = mapping(item, 'a fallback rule');
|
|
167
|
+
only(entries, ['all', 'kind']);
|
|
168
|
+
const kind = required(entries, 'kind', item.line);
|
|
169
|
+
const name = stringOf(kind.value);
|
|
170
|
+
if (!name || !KINDS.includes(name))
|
|
171
|
+
fail(kind.line, '"kind" is not a screen kind');
|
|
172
|
+
return { all: patternsOf(required(entries, 'all', item.line).value, 'all', false), kind: name };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function placeholdersOf(node) {
|
|
176
|
+
if (node.kind !== 'seq')
|
|
177
|
+
fail(node.line, '"placeholders" must be a list');
|
|
178
|
+
return node.items.map((item) => {
|
|
179
|
+
const entries = mapping(item, 'a placeholder');
|
|
180
|
+
only(entries, ['equals', 'prefix']);
|
|
181
|
+
const equals = optional(entries, 'equals');
|
|
182
|
+
const prefix = optional(entries, 'prefix');
|
|
183
|
+
if (equals && prefix)
|
|
184
|
+
fail(item.line, 'a placeholder is "equals" or "prefix", not both');
|
|
185
|
+
if (equals) {
|
|
186
|
+
const text = stringOf(equals.value);
|
|
187
|
+
if (text === null)
|
|
188
|
+
fail(equals.line, '"equals" must be a string');
|
|
189
|
+
return { equals: text };
|
|
190
|
+
}
|
|
191
|
+
if (prefix) {
|
|
192
|
+
const text = stringOf(prefix.value);
|
|
193
|
+
if (!text)
|
|
194
|
+
fail(prefix.line, '"prefix" must be a non-empty string');
|
|
195
|
+
return { prefix: text };
|
|
196
|
+
}
|
|
197
|
+
fail(item.line, 'a placeholder needs "equals" or "prefix"');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function chromeOf(node) {
|
|
201
|
+
if (node.kind !== 'seq')
|
|
202
|
+
fail(node.line, '"chrome" must be a list');
|
|
203
|
+
return node.items.map((item) => {
|
|
204
|
+
const text = stringOf(item);
|
|
205
|
+
if (text === null)
|
|
206
|
+
fail(item.line, '"chrome" entries must be strings');
|
|
207
|
+
if (!anchored(text))
|
|
208
|
+
fail(item.line, 'a chrome pattern must match the whole line: "^…$"');
|
|
209
|
+
const compiled = patternOf(text, false, item.line);
|
|
210
|
+
if (CHOICE_SAMPLES.some((sample) => compiled.test(sample))) {
|
|
211
|
+
fail(item.line, 'a chrome pattern would match a numbered choice line');
|
|
212
|
+
}
|
|
213
|
+
return compiled;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function regexField(entries, key, line, ignoreCase = false) {
|
|
217
|
+
const entry = required(entries, key, line);
|
|
218
|
+
const text = stringOf(entry.value);
|
|
219
|
+
if (text === null)
|
|
220
|
+
fail(entry.line, `"${key}" must be a string`);
|
|
221
|
+
return patternOf(text, ignoreCase, entry.line);
|
|
222
|
+
}
|
|
223
|
+
function patternOf(text, ignoreCase, line) {
|
|
224
|
+
try {
|
|
225
|
+
return compilePattern(text, ignoreCase);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (error instanceof DialectError)
|
|
229
|
+
fail(line, error.message);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function anchored(text) {
|
|
234
|
+
if (!text.startsWith('^') || !text.endsWith('$'))
|
|
235
|
+
return false;
|
|
236
|
+
let slashes = 0;
|
|
237
|
+
for (let i = text.length - 2; i >= 0 && text[i] === '\\'; i--)
|
|
238
|
+
slashes++;
|
|
239
|
+
return slashes % 2 === 0;
|
|
240
|
+
}
|
|
241
|
+
function mapping(node, what) {
|
|
242
|
+
if (node.kind !== 'map')
|
|
243
|
+
fail(node.line, `${what} must be a map`);
|
|
244
|
+
return node.entries;
|
|
245
|
+
}
|
|
246
|
+
function only(entries, allowed) {
|
|
247
|
+
for (const entry of entries)
|
|
248
|
+
if (!allowed.includes(entry.key))
|
|
249
|
+
fail(entry.line, `unknown key "${entry.key}"`);
|
|
250
|
+
}
|
|
251
|
+
function required(entries, key, line) {
|
|
252
|
+
return optional(entries, key) ?? fail(line, `missing "${key}"`);
|
|
253
|
+
}
|
|
254
|
+
function optional(entries, key) {
|
|
255
|
+
return entries.find((entry) => entry.key === key);
|
|
256
|
+
}
|
|
257
|
+
function stringOf(node) {
|
|
258
|
+
if (node.kind !== 'scalar' || typeof node.value !== 'string')
|
|
259
|
+
return null;
|
|
260
|
+
return node.value;
|
|
261
|
+
}
|
|
262
|
+
function boolOf(node, key) {
|
|
263
|
+
if (node.kind !== 'scalar' || typeof node.value !== 'boolean')
|
|
264
|
+
fail(node.line, `"${key}" must be true or false`);
|
|
265
|
+
return node.value;
|
|
266
|
+
}
|
|
267
|
+
function fail(line, message) {
|
|
268
|
+
throw new YamlError(line, message);
|
|
269
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Screen = {
|
|
2
|
+
kind: 'idle';
|
|
3
|
+
} | {
|
|
4
|
+
kind: 'working';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'unsent';
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'permission';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'trust';
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'question';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'unknown';
|
|
15
|
+
};
|
|
16
|
+
/** Every stage, in the core's order. The first one that matches wins. */
|
|
17
|
+
export declare function classify(cli: string, lines: string[]): Screen;
|
|
18
|
+
/** 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;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// What a pane's visible text shows, read against the shapes of its CLI. A screen that matches no
|
|
2
|
+
// shape is "unknown": never ready, never idle, and never grounds for typing anything.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { classifyLines, composeLines } from "./screen-core.js";
|
|
5
|
+
import { loadScreen } from "./screen-file.js";
|
|
6
|
+
function load(name) {
|
|
7
|
+
return loadScreen(readFileSync(new URL(`../profiles/${name}.yaml`, import.meta.url), 'utf8'));
|
|
8
|
+
}
|
|
9
|
+
// Every shipped CLI is data. The core owns the order.
|
|
10
|
+
const DATA = {
|
|
11
|
+
'claude-code': load('claude-code'),
|
|
12
|
+
codex: load('codex'),
|
|
13
|
+
cursor: load('cursor'),
|
|
14
|
+
antigravity: load('antigravity'),
|
|
15
|
+
};
|
|
16
|
+
// The window every pattern sees: the pane's last 20 lines, each trimmed at the end.
|
|
17
|
+
function windowOf(lines) {
|
|
18
|
+
return lines.map((line) => line.trimEnd()).slice(-20);
|
|
19
|
+
}
|
|
20
|
+
/** Every stage, in the core's order. The first one that matches wins. */
|
|
21
|
+
export function classify(cli, lines) {
|
|
22
|
+
const data = DATA[cli];
|
|
23
|
+
if (!data)
|
|
24
|
+
return { kind: 'unknown' };
|
|
25
|
+
return classifyLines(data, windowOf(lines));
|
|
26
|
+
}
|
|
27
|
+
/** The composer alone. A running turn would otherwise hide an empty input box. */
|
|
28
|
+
export function classifyComposer(cli, lines) {
|
|
29
|
+
const data = DATA[cli];
|
|
30
|
+
if (!data)
|
|
31
|
+
return { kind: 'unknown' };
|
|
32
|
+
return composeLines(data, windowOf(lines));
|
|
33
|
+
}
|
|
34
|
+
export function readScreen(cli, screen) {
|
|
35
|
+
if (screen === undefined)
|
|
36
|
+
return { kind: 'unknown' };
|
|
37
|
+
return classify(cli, screen.split('\n'));
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Seat, TeamFile } from '../file/types.ts';
|
|
2
|
+
export declare function taskProblem(task: string): string | null;
|
|
3
|
+
export declare function fillPattern(pattern: string, values: Record<string, string | undefined>): string | null;
|
|
4
|
+
export declare function lobbyPath(team: Pick<TeamFile, 'project' | 'workspace'>): string | null;
|
|
5
|
+
export declare function realLanding(root: string, folder: string): {
|
|
6
|
+
logical: string;
|
|
7
|
+
real: string;
|
|
8
|
+
};
|
|
9
|
+
/** Where a seat starts: the folder it waits in, or why it can't start. */
|
|
10
|
+
export type SeatStart = {
|
|
11
|
+
cwd: string;
|
|
12
|
+
lobby?: true;
|
|
13
|
+
} | {
|
|
14
|
+
problem: string;
|
|
15
|
+
once?: true;
|
|
16
|
+
};
|
|
17
|
+
export declare function seatStart(team: Pick<TeamFile, 'project' | 'workspace' | 'trust'>, seat: Seat, root: string): SeatStart;
|
|
18
|
+
export declare function publicNameHit(team: Pick<TeamFile, 'visibility' | 'identity'>, names: string[]): string | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { insideTrust, protectedBy } from "../file/paths.js";
|
|
4
|
+
// A task name is one path segment. It is also the last segment of the folder and part of the branch.
|
|
5
|
+
const TASK_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
6
|
+
export function taskProblem(task) {
|
|
7
|
+
if (!TASK_NAME.test(task)) {
|
|
8
|
+
return 'a task name is one segment of letters, digits, ".", "_" and "-", and it starts with a letter or a digit';
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
// Fills `{kind}` and `{task}`. Null when a placeholder has no value, or one is left that this command doesn't know.
|
|
13
|
+
export function fillPattern(pattern, values) {
|
|
14
|
+
let unknown = false;
|
|
15
|
+
const filled = pattern.replace(/\{([^}]*)\}/g, (token, name) => {
|
|
16
|
+
const value = values[name];
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
unknown = true;
|
|
19
|
+
return token;
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
});
|
|
23
|
+
return unknown ? null : filled;
|
|
24
|
+
}
|
|
25
|
+
// The lobby: the neutral folder a seat that works in worktrees waits in until a brief names its
|
|
26
|
+
// worktree — the folder the worktrees go in, with ".lobby" beside them, so a trust pattern that
|
|
27
|
+
// covers the worktrees covers it too. Null when the file names no folder to take a parent from.
|
|
28
|
+
export function lobbyPath(team) {
|
|
29
|
+
const path = team.workspace.path;
|
|
30
|
+
if (!path)
|
|
31
|
+
return null;
|
|
32
|
+
const parent = path.split('/').slice(0, -1).join('/').replaceAll('{repo}', team.project) || '.';
|
|
33
|
+
return join(parent, '.lobby');
|
|
34
|
+
}
|
|
35
|
+
// Where a folder will really land, `root` being the project root: `logical` is the path the file
|
|
36
|
+
// names; `real` follows a symlink in an ancestor that already exists. The worktree command tests
|
|
37
|
+
// both, and so does a seat's start: a worktrees folder that is a symlink into the project puts the
|
|
38
|
+
// lobby — and any folder under it — physically inside the protected checkout.
|
|
39
|
+
export function realLanding(root, folder) {
|
|
40
|
+
const logical = resolve(root, folder);
|
|
41
|
+
const tail = [];
|
|
42
|
+
let current = logical;
|
|
43
|
+
while (!existsSync(current)) {
|
|
44
|
+
const parent = dirname(current);
|
|
45
|
+
if (parent === current)
|
|
46
|
+
return { logical, real: logical };
|
|
47
|
+
tail.push(basename(current));
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
return { logical, real: join(realpathSync(current), ...tail.reverse()) };
|
|
51
|
+
}
|
|
52
|
+
// The protected checkout `folder` is in, in the text and then on disk: `protectedBy` reads the path
|
|
53
|
+
// as written, then the folder and the checkouts are resolved — symlinks in the ancestors that exist
|
|
54
|
+
// today followed — and tested again, so a folder that is physically inside one is inside it here.
|
|
55
|
+
function protectedLanding(root, folder, checkouts) {
|
|
56
|
+
const written = protectedBy(folder, checkouts);
|
|
57
|
+
if (written)
|
|
58
|
+
return written;
|
|
59
|
+
const real = realLanding(root, folder).real;
|
|
60
|
+
for (const checkout of checkouts) {
|
|
61
|
+
const land = realLanding(root, checkout).real;
|
|
62
|
+
if (real === land || real.startsWith(`${land}${sep}`))
|
|
63
|
+
return checkout;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
// Where a seat starts. A shared seat starts in the folder the file names — usually the project
|
|
68
|
+
// root — and reads there. Every other seat starts outside every protected checkout: in the lobby
|
|
69
|
+
// when the file gives it no folder of its own, or in the folder it names when that one is safe.
|
|
70
|
+
// A folder is outside a checkout when it is outside it on disk too, symlinks resolved. `once` marks
|
|
71
|
+
// a problem that is the same for every seat, so a caller says it once.
|
|
72
|
+
export function seatStart(team, seat, root) {
|
|
73
|
+
if (seat.mode === 'shared')
|
|
74
|
+
return { cwd: seat.cwd };
|
|
75
|
+
if (seat.cwd !== '.') {
|
|
76
|
+
const hit = protectedLanding(root, seat.cwd, team.workspace.protected);
|
|
77
|
+
if (hit) {
|
|
78
|
+
return {
|
|
79
|
+
problem: `seat ${seat.name} would start in ${seat.cwd}, inside the protected checkout ${hit}; ` +
|
|
80
|
+
"a seat that isn't `mode: shared` never starts in one",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { cwd: seat.cwd };
|
|
84
|
+
}
|
|
85
|
+
const lobby = lobbyPath(team);
|
|
86
|
+
if (lobby === null) {
|
|
87
|
+
return {
|
|
88
|
+
problem: 'a seat that works in worktrees has no lobby to wait in: workspace.path must name the folder {task} goes under',
|
|
89
|
+
once: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const hit = protectedLanding(root, lobby, team.workspace.protected);
|
|
93
|
+
if (hit) {
|
|
94
|
+
return {
|
|
95
|
+
problem: `seat ${seat.name} would start in the lobby ${lobby}, inside the protected checkout ${hit}; ` +
|
|
96
|
+
"a seat that isn't `mode: shared` never starts in one",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!insideTrust(lobby, team.trust)) {
|
|
100
|
+
return {
|
|
101
|
+
problem: `the lobby ${lobby} matches no trust pattern (${team.trust.join(', ') || 'none'}): ` +
|
|
102
|
+
'add one that covers it and run `team approve`',
|
|
103
|
+
once: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { cwd: lobby, lobby: true };
|
|
107
|
+
}
|
|
108
|
+
// The first name a public project's forbidden_public pattern matches, as "name matches pattern".
|
|
109
|
+
export function publicNameHit(team, names) {
|
|
110
|
+
if (team.visibility !== 'public')
|
|
111
|
+
return null;
|
|
112
|
+
for (const source of team.identity.forbiddenPublic) {
|
|
113
|
+
let regex;
|
|
114
|
+
try {
|
|
115
|
+
regex = new RegExp(source);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return `forbidden_public pattern ${JSON.stringify(source)} is not a regular expression: ${error.message}`;
|
|
119
|
+
}
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
if (regex.test(name))
|
|
122
|
+
return `${JSON.stringify(name)} matches forbidden_public ${JSON.stringify(source)}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
package/dist/yaml.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type YamlScalar = string | number | boolean | null;
|
|
2
|
+
export type YamlNode = {
|
|
3
|
+
kind: 'scalar';
|
|
4
|
+
line: number;
|
|
5
|
+
value: YamlScalar;
|
|
6
|
+
quoted: boolean;
|
|
7
|
+
raw: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'seq';
|
|
10
|
+
line: number;
|
|
11
|
+
items: YamlNode[];
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'map';
|
|
14
|
+
line: number;
|
|
15
|
+
entries: YamlEntry[];
|
|
16
|
+
};
|
|
17
|
+
export type YamlEntry = {
|
|
18
|
+
key: string;
|
|
19
|
+
line: number;
|
|
20
|
+
value: YamlNode;
|
|
21
|
+
};
|
|
22
|
+
export type YamlValue = YamlScalar | YamlValue[] | {
|
|
23
|
+
[key: string]: YamlValue;
|
|
24
|
+
};
|
|
25
|
+
export declare class YamlError extends Error {
|
|
26
|
+
line: number;
|
|
27
|
+
constructor(line: number, message: string);
|
|
28
|
+
}
|
|
29
|
+
export declare function parseYaml(text: string): YamlNode;
|
|
30
|
+
export declare function toValue(node: YamlNode): YamlValue;
|