team 0.0.1 → 0.1.1

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.
Files changed (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +310 -0
  3. package/dist/approve/approval.d.ts +38 -0
  4. package/dist/approve/approval.js +78 -0
  5. package/dist/approve/diff.d.ts +13 -0
  6. package/dist/approve/diff.js +47 -0
  7. package/dist/approve/fingerprint.d.ts +44 -0
  8. package/dist/approve/fingerprint.js +109 -0
  9. package/dist/args.d.ts +7 -0
  10. package/dist/args.js +22 -0
  11. package/dist/budgets/checks.d.ts +35 -0
  12. package/dist/budgets/checks.js +77 -0
  13. package/dist/budgets/gate.d.ts +24 -0
  14. package/dist/budgets/gate.js +127 -0
  15. package/dist/budgets/readings.d.ts +69 -0
  16. package/dist/budgets/readings.js +156 -0
  17. package/dist/budgets/run.d.ts +66 -0
  18. package/dist/budgets/run.js +119 -0
  19. package/dist/budgets/table.d.ts +24 -0
  20. package/dist/budgets/table.js +90 -0
  21. package/dist/caller.d.ts +48 -0
  22. package/dist/caller.js +126 -0
  23. package/dist/check/config.d.ts +51 -0
  24. package/dist/check/config.js +50 -0
  25. package/dist/check/git.d.ts +27 -0
  26. package/dist/check/git.js +97 -0
  27. package/dist/check/message.d.ts +27 -0
  28. package/dist/check/message.js +101 -0
  29. package/dist/check/run.d.ts +31 -0
  30. package/dist/check/run.js +77 -0
  31. package/dist/check/signature.d.ts +10 -0
  32. package/dist/check/signature.js +22 -0
  33. package/dist/cli.d.ts +9 -0
  34. package/dist/cli.js +76 -0
  35. package/dist/clis.d.ts +7 -0
  36. package/dist/clis.js +9 -0
  37. package/dist/commands/add.d.ts +23 -0
  38. package/dist/commands/add.js +445 -0
  39. package/dist/commands/approve.d.ts +11 -0
  40. package/dist/commands/approve.js +146 -0
  41. package/dist/commands/check.d.ts +17 -0
  42. package/dist/commands/check.js +103 -0
  43. package/dist/commands/doctor.d.ts +38 -0
  44. package/dist/commands/doctor.js +232 -0
  45. package/dist/commands/down.d.ts +37 -0
  46. package/dist/commands/down.js +258 -0
  47. package/dist/commands/init.d.ts +9 -0
  48. package/dist/commands/init.js +138 -0
  49. package/dist/commands/remove.d.ts +23 -0
  50. package/dist/commands/remove.js +235 -0
  51. package/dist/commands/status.d.ts +41 -0
  52. package/dist/commands/status.js +147 -0
  53. package/dist/commands/up.d.ts +49 -0
  54. package/dist/commands/up.js +362 -0
  55. package/dist/commands/watch.d.ts +35 -0
  56. package/dist/commands/watch.js +324 -0
  57. package/dist/commands/worktree.d.ts +12 -0
  58. package/dist/commands/worktree.js +388 -0
  59. package/dist/end/condition.d.ts +28 -0
  60. package/dist/end/condition.js +77 -0
  61. package/dist/file/current.d.ts +13 -0
  62. package/dist/file/current.js +53 -0
  63. package/dist/file/lines.d.ts +25 -0
  64. package/dist/file/lines.js +277 -0
  65. package/dist/file/load.d.ts +7 -0
  66. package/dist/file/load.js +90 -0
  67. package/dist/file/paths.d.ts +6 -0
  68. package/dist/file/paths.js +77 -0
  69. package/dist/file/secrets.d.ts +6 -0
  70. package/dist/file/secrets.js +54 -0
  71. package/dist/file/signature.d.ts +13 -0
  72. package/dist/file/signature.js +36 -0
  73. package/dist/file/types.d.ts +130 -0
  74. package/dist/file/types.js +1 -0
  75. package/dist/file/validate.d.ts +13 -0
  76. package/dist/file/validate.js +702 -0
  77. package/dist/file/write.d.ts +11 -0
  78. package/dist/file/write.js +13 -0
  79. package/dist/herdr.d.ts +38 -0
  80. package/dist/herdr.js +227 -0
  81. package/dist/index.d.ts +10 -0
  82. package/dist/index.js +6 -0
  83. package/dist/io.d.ts +10 -0
  84. package/dist/io.js +1 -0
  85. package/dist/launch/deliver.d.ts +10 -0
  86. package/dist/launch/deliver.js +39 -0
  87. package/dist/launch/execute.d.ts +46 -0
  88. package/dist/launch/execute.js +284 -0
  89. package/dist/launch/plan.d.ts +185 -0
  90. package/dist/launch/plan.js +280 -0
  91. package/dist/launch/rules.d.ts +25 -0
  92. package/dist/launch/rules.js +36 -0
  93. package/dist/log.d.ts +1 -0
  94. package/dist/log.js +24 -0
  95. package/dist/profiles/antigravity.yaml +66 -0
  96. package/dist/profiles/claude-code.yaml +69 -0
  97. package/dist/profiles/codex.yaml +77 -0
  98. package/dist/profiles/cursor.yaml +68 -0
  99. package/dist/profiles/index.d.ts +1 -0
  100. package/dist/profiles/index.js +1 -0
  101. package/dist/profiles/profile.d.ts +60 -0
  102. package/dist/profiles/profile.js +313 -0
  103. package/dist/profiles/quota.d.ts +27 -0
  104. package/dist/profiles/quota.js +57 -0
  105. package/dist/state.d.ts +61 -0
  106. package/dist/state.js +127 -0
  107. package/dist/status/compare.d.ts +26 -0
  108. package/dist/status/compare.js +110 -0
  109. package/dist/status/statusline.d.ts +9 -0
  110. package/dist/status/statusline.js +26 -0
  111. package/dist/store/store.d.ts +62 -0
  112. package/dist/store/store.js +116 -0
  113. package/dist/watch/check.d.ts +82 -0
  114. package/dist/watch/check.js +29 -0
  115. package/dist/watch/checks/approval.d.ts +2 -0
  116. package/dist/watch/checks/approval.js +16 -0
  117. package/dist/watch/checks/attention.d.ts +2 -0
  118. package/dist/watch/checks/attention.js +23 -0
  119. package/dist/watch/checks/budget.d.ts +2 -0
  120. package/dist/watch/checks/budget.js +141 -0
  121. package/dist/watch/checks/disk.d.ts +2 -0
  122. package/dist/watch/checks/disk.js +13 -0
  123. package/dist/watch/checks/extra.d.ts +2 -0
  124. package/dist/watch/checks/extra.js +18 -0
  125. package/dist/watch/checks/idle.d.ts +2 -0
  126. package/dist/watch/checks/idle.js +32 -0
  127. package/dist/watch/checks/load.d.ts +2 -0
  128. package/dist/watch/checks/load.js +11 -0
  129. package/dist/watch/checks/memory.d.ts +2 -0
  130. package/dist/watch/checks/memory.js +11 -0
  131. package/dist/watch/checks/missing.d.ts +2 -0
  132. package/dist/watch/checks/missing.js +12 -0
  133. package/dist/watch/checks/model-drift.d.ts +2 -0
  134. package/dist/watch/checks/model-drift.js +14 -0
  135. package/dist/watch/checks/swap-free.d.ts +2 -0
  136. package/dist/watch/checks/swap-free.js +12 -0
  137. package/dist/watch/checks/swap-growth.d.ts +2 -0
  138. package/dist/watch/checks/swap-growth.js +18 -0
  139. package/dist/watch/checks/team-idle.d.ts +2 -0
  140. package/dist/watch/checks/team-idle.js +18 -0
  141. package/dist/watch/checks/unsent.d.ts +2 -0
  142. package/dist/watch/checks/unsent.js +18 -0
  143. package/dist/watch/close.d.ts +19 -0
  144. package/dist/watch/close.js +40 -0
  145. package/dist/watch/dialect.d.ts +5 -0
  146. package/dist/watch/dialect.js +430 -0
  147. package/dist/watch/end.d.ts +11 -0
  148. package/dist/watch/end.js +17 -0
  149. package/dist/watch/machine.d.ts +38 -0
  150. package/dist/watch/machine.js +137 -0
  151. package/dist/watch/notify.d.ts +1 -0
  152. package/dist/watch/notify.js +15 -0
  153. package/dist/watch/pass.d.ts +42 -0
  154. package/dist/watch/pass.js +242 -0
  155. package/dist/watch/screen-core.d.ts +20 -0
  156. package/dist/watch/screen-core.js +356 -0
  157. package/dist/watch/screen-data.d.ts +65 -0
  158. package/dist/watch/screen-data.js +1 -0
  159. package/dist/watch/screen-file.d.ts +2 -0
  160. package/dist/watch/screen-file.js +269 -0
  161. package/dist/watch/screen.d.ts +24 -0
  162. package/dist/watch/screen.js +47 -0
  163. package/dist/worktree/place.d.ts +18 -0
  164. package/dist/worktree/place.js +126 -0
  165. package/dist/yaml.d.ts +30 -0
  166. package/dist/yaml.js +380 -0
  167. package/examples/checks/codex-quota +217 -0
  168. package/examples/team.yaml +76 -0
  169. package/package.json +29 -10
@@ -0,0 +1,430 @@
1
+ // The regex dialect both engines share. The loader rewrites the shorthand classes
2
+ // before compiling, so a pattern means the same thing under JavaScript's `u` flag
3
+ // and under Rust's Unicode mode. The shape rules are what bound a single pattern:
4
+ // JavaScript cannot interrupt a RegExp that is already running.
5
+ const MAX_PATTERN = 200;
6
+ const MAX_QUANTIFIER = 1000;
7
+ // JavaScript's `\s`, ASCII and the Unicode spaces it has matched since ES2019.
8
+ // U+180E is not among them. The set is fixed so the rewrite does not depend on
9
+ // which flag the host gives `\s`.
10
+ const WS_CODEPOINTS = [
11
+ 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x0020, 0x00a0, 0x1680,
12
+ 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a,
13
+ 0x2028, 0x2029, 0x202f, 0x205f, 0x3000, 0xfeff,
14
+ ];
15
+ const DIGITS = range(0x30, 0x39);
16
+ const WORD = [...range(0x30, 0x39), ...range(0x41, 0x5a), ...range(0x61, 0x7a), 0x5f];
17
+ export class DialectError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = 'DialectError';
21
+ }
22
+ }
23
+ /** Compile one pattern. `ignoreCase` is the stage's flag, never an inline `(?i)`. */
24
+ export function compilePattern(source, ignoreCase = false) {
25
+ if (source.length === 0)
26
+ throw new DialectError('a pattern is empty');
27
+ if (source.length > MAX_PATTERN)
28
+ throw new DialectError(`a pattern is longer than ${MAX_PATTERN} characters`);
29
+ refuseSpelling(source);
30
+ const parser = new Parser(source);
31
+ const body = parser.parseAlt();
32
+ if (!parser.done())
33
+ throw new DialectError('a pattern has an extra ")"');
34
+ const flags = ignoreCase ? 'iu' : 'u';
35
+ return new RegExp(body.source, flags);
36
+ }
37
+ class Parser {
38
+ at = 0;
39
+ source;
40
+ constructor(source) {
41
+ this.source = source;
42
+ }
43
+ done() {
44
+ return this.at >= this.source.length;
45
+ }
46
+ parseAlt() {
47
+ const branches = [this.parseSeq()];
48
+ while (this.eat('|'))
49
+ branches.push(this.parseSeq());
50
+ return {
51
+ source: branches.map((branch) => seqSource(branch)).join('|'),
52
+ min: 1,
53
+ max: 1,
54
+ literal: false,
55
+ group: branches.length > 1,
56
+ branches,
57
+ };
58
+ }
59
+ parseSeq() {
60
+ const atoms = [];
61
+ while (!this.done() && this.peek() !== '|' && this.peek() !== ')')
62
+ atoms.push(this.parseAtom());
63
+ return atoms;
64
+ }
65
+ parseAtom() {
66
+ const piece = this.parsePiece();
67
+ const quant = this.parseQuantifier();
68
+ if (!quant)
69
+ return piece;
70
+ if (piece.group && quant.unbounded)
71
+ this.checkRepeatedGroup(piece);
72
+ return {
73
+ source: piece.source + quant.source,
74
+ min: quant.min,
75
+ max: quant.max,
76
+ literal: false,
77
+ group: piece.group,
78
+ branches: piece.branches,
79
+ };
80
+ }
81
+ // A group under `*`, `+` or `{n,}` whose first atom is optional or itself repeated
82
+ // backtracks badly. Alternation is allowed only when every branch opens on a literal.
83
+ checkRepeatedGroup(group) {
84
+ if (group.branches.length > 1) {
85
+ for (const branch of group.branches) {
86
+ const first = branch[0];
87
+ if (!first?.literal)
88
+ throw new DialectError('a repeated alternation must open every branch with a literal');
89
+ }
90
+ return;
91
+ }
92
+ if (risky(group.branches[0]?.[0]))
93
+ throw new DialectError('a repeated group starts with an optional or repeated atom');
94
+ }
95
+ parsePiece() {
96
+ const ch = this.peek();
97
+ if (ch === undefined || ch === '|' || ch === ')' || ch === '*' || ch === '+' || ch === '?' || ch === '{') {
98
+ throw new DialectError('a quantifier needs something to repeat');
99
+ }
100
+ if (ch === '(')
101
+ return this.parseGroup();
102
+ if (ch === '[')
103
+ return this.leaf(this.parseClass(), false);
104
+ if (ch === '^' || ch === '$') {
105
+ this.at++;
106
+ return this.leaf(ch, false);
107
+ }
108
+ if (ch === '.') {
109
+ this.at++;
110
+ return this.leaf('.', false);
111
+ }
112
+ if (ch === '\\')
113
+ return this.parseEscape();
114
+ if (ch === ')')
115
+ throw new DialectError('a pattern has an extra ")"');
116
+ const cp = this.readCodePoint();
117
+ return this.leaf(escapeLiteral(cp), true);
118
+ }
119
+ parseGroup() {
120
+ this.expect('(');
121
+ if (this.peek() !== '?') {
122
+ const body = this.parseAlt();
123
+ this.expect(')');
124
+ return { ...body, source: `(${body.source})`, group: true, literal: false };
125
+ }
126
+ this.expect('?');
127
+ if (this.peek() === ':') {
128
+ this.expect(':');
129
+ const body = this.parseAlt();
130
+ this.expect(')');
131
+ return { ...body, source: `(?:${body.source})`, group: true, literal: false };
132
+ }
133
+ throw new DialectError('lookaround, named groups and inline flags are not allowed');
134
+ }
135
+ parseQuantifier() {
136
+ const ch = this.peek();
137
+ if (ch !== '*' && ch !== '+' && ch !== '?' && ch !== '{')
138
+ return null;
139
+ this.at++;
140
+ let min;
141
+ let max;
142
+ let text;
143
+ let unbounded;
144
+ if (ch === '*') {
145
+ min = 0;
146
+ max = Infinity;
147
+ text = '*';
148
+ unbounded = true;
149
+ }
150
+ else if (ch === '+') {
151
+ min = 1;
152
+ max = Infinity;
153
+ text = '+';
154
+ unbounded = true;
155
+ }
156
+ else if (ch === '?') {
157
+ min = 0;
158
+ max = 1;
159
+ text = '?';
160
+ unbounded = false;
161
+ }
162
+ else {
163
+ const count = this.readCount();
164
+ min = count.min;
165
+ max = count.max;
166
+ text = count.text;
167
+ unbounded = count.max === Infinity;
168
+ }
169
+ if (this.peek() === '?') {
170
+ this.at++;
171
+ text += '?';
172
+ }
173
+ return { source: text, min, max, unbounded };
174
+ }
175
+ readCount() {
176
+ const start = this.at;
177
+ const first = this.readNumber();
178
+ if (first === null)
179
+ throw new DialectError('a "{ }" quantifier needs a number');
180
+ if (this.eat(',')) {
181
+ const second = this.readNumber();
182
+ this.expect('}');
183
+ if (second === null)
184
+ return { min: first, max: Infinity, text: this.source.slice(start - 1, this.at) };
185
+ if (second < first)
186
+ throw new DialectError('a "{n,m}" quantifier has m below n');
187
+ return { min: first, max: second, text: this.source.slice(start - 1, this.at) };
188
+ }
189
+ this.expect('}');
190
+ return { min: first, max: first, text: this.source.slice(start - 1, this.at) };
191
+ }
192
+ readNumber() {
193
+ const start = this.at;
194
+ while (this.peek() !== undefined && this.peek() >= '0' && this.peek() <= '9')
195
+ this.at++;
196
+ if (this.at === start)
197
+ return null;
198
+ const n = Number(this.source.slice(start, this.at));
199
+ if (n > MAX_QUANTIFIER)
200
+ throw new DialectError(`a quantifier is above ${MAX_QUANTIFIER}`);
201
+ return n;
202
+ }
203
+ parseClass() {
204
+ this.expect('[');
205
+ const negated = this.eat('^');
206
+ const parts = [];
207
+ let first = true;
208
+ while (!this.done() && (this.peek() !== ']' || first)) {
209
+ first = false;
210
+ if (this.peek() === '\\')
211
+ parts.push(this.parseClassEscape());
212
+ else
213
+ parts.push({ kind: 'char', cp: this.readCodePoint() });
214
+ }
215
+ this.expect(']');
216
+ return classSource(negated, foldRanges(parts));
217
+ }
218
+ parseClassEscape() {
219
+ this.expect('\\');
220
+ const ch = this.peek();
221
+ if (ch === undefined)
222
+ throw new DialectError('a pattern ends with "\\"');
223
+ this.at++;
224
+ const shorthand = shorthandSet(ch);
225
+ if (shorthand)
226
+ return shorthand;
227
+ const cp = simpleEscape(ch);
228
+ if (cp === null)
229
+ throw new DialectError(`an unsupported escape "\\${ch}"`);
230
+ return { kind: 'char', cp };
231
+ }
232
+ parseEscape() {
233
+ this.expect('\\');
234
+ const ch = this.peek();
235
+ if (ch === undefined)
236
+ throw new DialectError('a pattern ends with "\\"');
237
+ this.at++;
238
+ if (ch >= '1' && ch <= '9')
239
+ throw new DialectError('backreferences are not allowed');
240
+ if (ch === 'b')
241
+ return this.leaf('\\b', false);
242
+ const shorthand = shorthandSet(ch);
243
+ if (shorthand?.kind === 'chars')
244
+ return this.leaf(emitClass(false, shorthand.cps), false);
245
+ if (shorthand?.kind === 'not')
246
+ return this.leaf(emitClass(true, shorthand.cps), false);
247
+ const cp = simpleEscape(ch);
248
+ if (cp === null)
249
+ throw new DialectError(`an unsupported escape "\\${ch}"`);
250
+ return this.leaf(escapeLiteral(cp), true);
251
+ }
252
+ leaf(source, literal) {
253
+ return { source, min: 1, max: 1, literal, group: false, branches: [] };
254
+ }
255
+ peek() {
256
+ return this.source[this.at];
257
+ }
258
+ eat(ch) {
259
+ if (this.peek() !== ch)
260
+ return false;
261
+ this.at++;
262
+ return true;
263
+ }
264
+ expect(ch) {
265
+ if (!this.eat(ch))
266
+ throw new DialectError(`expected "${ch}"`);
267
+ }
268
+ readCodePoint() {
269
+ const cp = this.source.codePointAt(this.at);
270
+ if (cp === undefined)
271
+ throw new DialectError('a pattern ended early');
272
+ this.at += cp > 0xffff ? 2 : 1;
273
+ return cp;
274
+ }
275
+ }
276
+ function shorthandSet(ch) {
277
+ if (ch === 'd')
278
+ return { kind: 'chars', cps: DIGITS };
279
+ if (ch === 'w')
280
+ return { kind: 'chars', cps: WORD };
281
+ if (ch === 's')
282
+ return { kind: 'chars', cps: WS_CODEPOINTS };
283
+ if (ch === 'D')
284
+ return { kind: 'not', cps: DIGITS };
285
+ if (ch === 'W')
286
+ return { kind: 'not', cps: WORD };
287
+ if (ch === 'S')
288
+ return { kind: 'not', cps: WS_CODEPOINTS };
289
+ return null;
290
+ }
291
+ function simpleEscape(ch) {
292
+ if (ch === 'n')
293
+ return 0x0a;
294
+ if (ch === 'r')
295
+ return 0x0d;
296
+ if (ch === 't')
297
+ return 0x09;
298
+ if (ch === 'f')
299
+ return 0x0c;
300
+ if (ch === 'v')
301
+ return 0x0b;
302
+ if (ch === 'b')
303
+ return null;
304
+ if ('\\.^$|?*+()[]{}-/'.includes(ch))
305
+ return ch.codePointAt(0);
306
+ return null;
307
+ }
308
+ function refuseSpelling(source) {
309
+ for (let i = 0; i < source.length; i++) {
310
+ if (source[i] !== '\\')
311
+ continue;
312
+ const next = source[i + 1];
313
+ if (next === 'u' || next === 'U' || next === 'x')
314
+ throw new DialectError('a "\\u" or "\\x" escape is not allowed: write the character itself');
315
+ if (next === '\\')
316
+ i++;
317
+ }
318
+ }
319
+ // A class is a finite set, except a complement shorthand (`\S`, `\D`, `\W`), whose
320
+ // negation brings it back to a finite set. `[^\S\n]` is the whitespace set without
321
+ // a newline, which is what the working-line pattern relies on.
322
+ function classSource(negated, parts) {
323
+ const positive = new Set();
324
+ const complements = [];
325
+ for (const part of parts) {
326
+ if (part.kind === 'char')
327
+ positive.add(part.cp);
328
+ else if (part.kind === 'chars')
329
+ for (const cp of part.cps)
330
+ positive.add(cp);
331
+ else
332
+ complements.push(part.cps);
333
+ }
334
+ if (complements.length === 0)
335
+ return emitClass(negated, [...positive]);
336
+ let inter = new Set(complements[0]);
337
+ for (const extra of complements.slice(1))
338
+ inter = new Set([...inter].filter((cp) => extra.includes(cp)));
339
+ const remaining = [...inter].filter((cp) => !positive.has(cp));
340
+ return emitClass(!negated, remaining);
341
+ }
342
+ function foldRanges(parts) {
343
+ const out = [];
344
+ for (let i = 0; i < parts.length; i++) {
345
+ const part = parts[i];
346
+ const next = parts[i + 1];
347
+ const after = parts[i + 2];
348
+ if (part.kind === 'char' && next?.kind === 'char' && next.cp === 0x2d && after?.kind === 'char') {
349
+ // A hyphen between two characters is a range. A hyphen after a shorthand stays a hyphen.
350
+ if (after.cp < part.cp)
351
+ throw new DialectError('a character range is backwards');
352
+ const cps = [];
353
+ for (let cp = part.cp; cp <= after.cp; cp++)
354
+ cps.push(cp);
355
+ out.push({ kind: 'chars', cps });
356
+ i += 2;
357
+ }
358
+ else
359
+ out.push(part);
360
+ }
361
+ return out;
362
+ }
363
+ function emitClass(negated, cps) {
364
+ const sorted = [...new Set(cps)].sort((a, b) => a - b);
365
+ let body = '';
366
+ for (let i = 0; i < sorted.length;) {
367
+ const start = sorted[i];
368
+ let end = start;
369
+ while (i + 1 < sorted.length && sorted[i + 1] === end + 1) {
370
+ i++;
371
+ end = sorted[i];
372
+ }
373
+ if (end - start >= 2)
374
+ body += classChar(start) + '-' + classChar(end);
375
+ else
376
+ for (let cp = start; cp <= end; cp++)
377
+ body += classChar(cp);
378
+ i++;
379
+ }
380
+ if (!body)
381
+ return negated ? '[\\s\\S]' : '[^\\s\\S]';
382
+ return negated ? `[^${body}]` : `[${body}]`;
383
+ }
384
+ function classChar(cp) {
385
+ if (cp === 0x09)
386
+ return '\\t';
387
+ if (cp === 0x0a)
388
+ return '\\n';
389
+ if (cp === 0x0b)
390
+ return '\\v';
391
+ if (cp === 0x0c)
392
+ return '\\f';
393
+ if (cp === 0x0d)
394
+ return '\\r';
395
+ if (cp === 0x2d || cp === 0x5d || cp === 0x5e || cp === 0x5c)
396
+ return `\\${String.fromCodePoint(cp)}`;
397
+ if (cp < 0x20 || cp > 0x7e)
398
+ return `\\u${cp.toString(16).padStart(4, '0')}`;
399
+ return String.fromCodePoint(cp);
400
+ }
401
+ function escapeLiteral(cp) {
402
+ const ch = String.fromCodePoint(cp);
403
+ if ('\\.^$|?*+()[]{}'.includes(ch))
404
+ return `\\${ch}`;
405
+ if (cp < 0x20 || cp > 0x7e)
406
+ return `\\u${cp.toString(16).padStart(4, '0')}`;
407
+ return ch;
408
+ }
409
+ function seqSource(atoms) {
410
+ return atoms.map((atom) => atom.source).join('');
411
+ }
412
+ // Optional (`?`, `*`, `{0,…}`) or repeated (`+`, `*`, `{n,}` with room for more than one).
413
+ // A group that is itself exactly once still counts when its own first atom is.
414
+ function risky(atom) {
415
+ if (!atom)
416
+ return true;
417
+ if (atom.min === 0 || atom.max > 1)
418
+ return true;
419
+ if (!atom.group)
420
+ return false;
421
+ if (atom.branches.length > 1)
422
+ return atom.branches.some((branch) => !branch[0]?.literal);
423
+ return risky(atom.branches[0]?.[0]);
424
+ }
425
+ function range(from, to) {
426
+ const out = [];
427
+ for (let cp = from; cp <= to; cp++)
428
+ out.push(cp);
429
+ return out;
430
+ }
@@ -0,0 +1,11 @@
1
+ export type EndView = {
2
+ kind: 'result';
3
+ exists: boolean;
4
+ } | {
5
+ kind: 'merged';
6
+ verdict: 'merged' | 'open' | 'unproven';
7
+ detail: string;
8
+ ownNow: number;
9
+ };
10
+ /** What the watch reads for one end. A missing base cannot prove a merge. */
11
+ export declare function readEnd(root: string, until: string, base: string | null, ownBefore: boolean): EndView;
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { readMerge } from "../end/condition.js";
4
+ /** What the watch reads for one end. A missing base cannot prove a merge. */
5
+ export function readEnd(root, until, base, ownBefore) {
6
+ if (until.startsWith('result:') && until.length > 'result:'.length) {
7
+ return { kind: 'result', exists: existsSync(resolve(root, until.slice('result:'.length))) };
8
+ }
9
+ if (!until.startsWith('merged:') || until.length <= 'merged:'.length) {
10
+ return { kind: 'merged', verdict: 'unproven', ownNow: 0, detail: 'the end is not one this version reads; nothing is removed' };
11
+ }
12
+ if (!base) {
13
+ return { kind: 'merged', verdict: 'unproven', ownNow: 0, detail: 'workspace.base is required to read a merged end; nothing is removed' };
14
+ }
15
+ const read = readMerge(root, until.slice('merged:'.length), base, ownBefore);
16
+ return { kind: 'merged', verdict: read.verdict, detail: read.detail, ownNow: read.ownNow };
17
+ }
@@ -0,0 +1,38 @@
1
+ import type { TeamFile } from '../file/types.ts';
2
+ export type Machine = {
3
+ loadPerCore: number | null;
4
+ memoryFree: number | null;
5
+ diskFree: number | null;
6
+ swapFree: number | null;
7
+ swapUsed: number | null;
8
+ };
9
+ export declare function parseSwapUsage(text: string): {
10
+ total: number;
11
+ used: number;
12
+ free: number;
13
+ } | null;
14
+ export declare function parseMemoryPressure(text: string): number | null;
15
+ export declare function parseLoadavg(text: string): number | null;
16
+ export declare function parseMeminfo(text: string): {
17
+ memoryFree: number | null;
18
+ swapFree: number | null;
19
+ swapUsed: number | null;
20
+ };
21
+ export declare function readMachine(root: string, platform?: string, proc?: string): Machine;
22
+ /** A size as the file writes it: 1 GB is 1e9 bytes, the same spelling the watch uses. */
23
+ export declare function gb(bytes: number): string;
24
+ export type SwapSample = {
25
+ at: number;
26
+ used: number;
27
+ };
28
+ /**
29
+ * Records `used` and returns how far it sits above the smallest sample still inside the window.
30
+ * The watch and a launch share this, so a figure means the same thing in both places.
31
+ */
32
+ export declare function recordSwap(samples: SwapSample[], used: number, now: number, windowSeconds: number): number;
33
+ /**
34
+ * The first `machine:` start limit this reading crosses, or null. A figure that was not read
35
+ * (null) is neither fine nor a refusal. Swap growth uses the samples this command has already
36
+ * taken; one reading on its own never crosses it.
37
+ */
38
+ export declare function launchLimit(machine: Machine, limits: TeamFile['machine'], samples: SwapSample[], now: number): string | null;
@@ -0,0 +1,137 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readFileSync, statfsSync } from 'node:fs';
3
+ import { cpus, loadavg } from 'node:os';
4
+ function command(name, args) {
5
+ try {
6
+ return execFileSync(name, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 10_000 });
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ // "total = 24576.00M used = 23261.25M free = 1314.75M", as macOS's vm.swapusage prints it.
13
+ export function parseSwapUsage(text) {
14
+ const read = (name) => {
15
+ const match = new RegExp(`${name} = ([0-9.]+)([KMG])`).exec(text);
16
+ if (!match)
17
+ return null;
18
+ return Number(match[1]) * { K: 1024, M: 1024 ** 2, G: 1024 ** 3 }[match[2]];
19
+ };
20
+ const total = read('total');
21
+ const used = read('used');
22
+ const free = read('free');
23
+ return total === null || used === null || free === null ? null : { total, used, free };
24
+ }
25
+ // "System-wide memory free percentage: 44%", the last line of macOS's memory_pressure.
26
+ export function parseMemoryPressure(text) {
27
+ const match = /free percentage:\s*([0-9]+)%/.exec(text);
28
+ return match ? Number(match[1]) : null;
29
+ }
30
+ // "0.52 0.58 0.59 1/1234 5678", as /proc/loadavg holds it: the one-minute average first.
31
+ export function parseLoadavg(text) {
32
+ const match = /^\s*([0-9]+(?:\.[0-9]+)?)\s/.exec(text);
33
+ return match ? Number(match[1]) : null;
34
+ }
35
+ // One file of a /proc tree, or null when this machine doesn't have it. `proc` is the tree's root,
36
+ // a parameter so a test can read a captured one.
37
+ function procFile(proc, name) {
38
+ try {
39
+ return readFileSync(`${proc}/${name}`, 'utf8');
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export function parseMeminfo(text) {
46
+ const kb = (name) => {
47
+ const match = new RegExp(`^${name}:\\s+([0-9]+) kB`, 'm').exec(text);
48
+ return match ? Number(match[1]) * 1024 : null;
49
+ };
50
+ const total = kb('MemTotal');
51
+ const available = kb('MemAvailable');
52
+ const swapTotal = kb('SwapTotal');
53
+ const swapFree = kb('SwapFree');
54
+ return {
55
+ memoryFree: total && available !== null ? (available / total) * 100 : null,
56
+ swapFree: swapTotal ? swapFree : null,
57
+ swapUsed: swapTotal && swapFree !== null ? swapTotal - swapFree : null,
58
+ };
59
+ }
60
+ // This platform's figures: macOS's commands on darwin, Linux's /proc everywhere Linux runs.
61
+ // `platform` and `proc` are parameters for the tests; the command passes neither.
62
+ export function readMachine(root, platform = process.platform, proc = '/proc') {
63
+ const machine = { loadPerCore: null, memoryFree: null, diskFree: null, swapFree: null, swapUsed: null };
64
+ const cores = cpus().length;
65
+ try {
66
+ const volume = statfsSync(root);
67
+ machine.diskFree = volume.bavail * volume.bsize;
68
+ }
69
+ catch { }
70
+ if (platform === 'darwin') {
71
+ if (cores)
72
+ machine.loadPerCore = loadavg()[0] / cores;
73
+ const pressure = command('memory_pressure', []);
74
+ if (pressure)
75
+ machine.memoryFree = parseMemoryPressure(pressure);
76
+ const swap = parseSwapUsage(command('sysctl', ['-n', 'vm.swapusage']) ?? '');
77
+ if (swap && swap.total > 0) {
78
+ machine.swapFree = swap.free;
79
+ machine.swapUsed = swap.used;
80
+ }
81
+ }
82
+ else if (platform === 'linux') {
83
+ // /proc is always there, and reading it is what the platform's own tools do.
84
+ const load = procFile(proc, 'loadavg');
85
+ const one = load === null ? null : parseLoadavg(load);
86
+ if (cores && one !== null)
87
+ machine.loadPerCore = one / cores;
88
+ const info = procFile(proc, 'meminfo');
89
+ if (info !== null)
90
+ Object.assign(machine, parseMeminfo(info));
91
+ }
92
+ else if (cores) {
93
+ machine.loadPerCore = loadavg()[0] / cores;
94
+ }
95
+ return machine;
96
+ }
97
+ /** A size as the file writes it: 1 GB is 1e9 bytes, the same spelling the watch uses. */
98
+ export function gb(bytes) {
99
+ return `${(bytes / 1e9).toFixed(1)} GB`;
100
+ }
101
+ /**
102
+ * Records `used` and returns how far it sits above the smallest sample still inside the window.
103
+ * The watch and a launch share this, so a figure means the same thing in both places.
104
+ */
105
+ export function recordSwap(samples, used, now, windowSeconds) {
106
+ samples.push({ at: now, used });
107
+ const kept = samples.filter((sample) => now - sample.at <= windowSeconds * 1000);
108
+ samples.splice(0, samples.length, ...kept);
109
+ return used - Math.min(...kept.map((sample) => sample.used));
110
+ }
111
+ /**
112
+ * The first `machine:` start limit this reading crosses, or null. A figure that was not read
113
+ * (null) is neither fine nor a refusal. Swap growth uses the samples this command has already
114
+ * taken; one reading on its own never crosses it.
115
+ */
116
+ export function launchLimit(machine, limits, samples, now) {
117
+ if (machine.loadPerCore !== null && machine.loadPerCore > limits.loadStart) {
118
+ return `the load is ${machine.loadPerCore.toFixed(1)} per core, above ${limits.loadStart}`;
119
+ }
120
+ if (machine.memoryFree !== null && machine.memoryFree < limits.memoryStart) {
121
+ return `free memory is ${Math.round(machine.memoryFree)}%, below ${limits.memoryStart}%`;
122
+ }
123
+ if (machine.diskFree !== null && machine.diskFree < limits.diskMin) {
124
+ return `free disk is ${gb(machine.diskFree)}, below ${gb(limits.diskMin)}`;
125
+ }
126
+ if (machine.swapFree !== null && machine.swapFree < limits.swapFreeMin) {
127
+ return `free swap is ${gb(machine.swapFree)}, below ${gb(limits.swapFreeMin)}`;
128
+ }
129
+ if (machine.swapUsed !== null) {
130
+ const growth = recordSwap(samples, machine.swapUsed, now, limits.swapGrowthWindow);
131
+ const minutes = Math.floor((limits.swapGrowthWindow * 1000) / 60_000);
132
+ if (growth > limits.swapGrowthMax) {
133
+ return `swap grew by ${gb(growth)} in ${minutes} minutes, above ${gb(limits.swapGrowthMax)}`;
134
+ }
135
+ }
136
+ return null;
137
+ }
@@ -0,0 +1 @@
1
+ export declare function notify(text: string, title?: string): void;
@@ -0,0 +1,15 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { platform } from 'node:os';
3
+ // A desktop notification, best effort: a machine without one still has the log.
4
+ export function notify(text, title = 'Team watch') {
5
+ try {
6
+ if (platform() === 'darwin') {
7
+ const quote = (value) => `"${value.replace(/[\\"]/g, '\\$&')}"`;
8
+ execFileSync('osascript', ['-e', `display notification ${quote(text)} with title ${quote(title)}`], { stdio: 'ignore', timeout: 5000 });
9
+ }
10
+ else {
11
+ execFileSync('notify-send', [title, text], { stdio: 'ignore', timeout: 5000 });
12
+ }
13
+ }
14
+ catch { }
15
+ }