@north-light/crouter 0.3.274 → 0.3.276
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/dist/builtin-memory/02-turn-lifecycle/01-terminal.md +1 -1
- package/dist/clients/attach/viewer.js +459 -459
- package/dist/commands/cron.js +3 -3
- package/dist/commands/node/create.js +3 -1
- package/dist/commands/node-worktree.js +7 -7
- package/dist/commands/pkg/plugin-manage.js +19 -8
- package/dist/commands/push.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +12 -10
- package/dist/core/__tests__/integration/worktree-land.test.js +7 -2
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +3 -16
- package/dist/core/canvas/browse/render.js +82 -67
- package/dist/core/canvas/status-glyph.d.ts +4 -3
- package/dist/core/canvas/status-glyph.js +17 -9
- package/dist/core/canvas/types.d.ts +18 -0
- package/dist/core/command-plugins/bundle.d.ts +7 -0
- package/dist/core/command-plugins/bundle.js +18 -4
- package/dist/core/git.d.ts +5 -1
- package/dist/core/git.js +18 -4
- package/dist/core/runtime/spawn.d.ts +2 -0
- package/dist/core/runtime/spawn.js +1 -1
- package/dist/core/runtime/surface-bg.d.ts +6 -4
- package/dist/core/runtime/surface-bg.js +39 -22
- package/dist/core/tui/draw.d.ts +2 -0
- package/dist/core/tui/draw.js +15 -0
- package/dist/core/worktree-sweep.js +28 -5
- package/dist/core/worktree.d.ts +1 -12
- package/dist/core/worktree.js +222 -10
- package/dist/daemon/api/handlers/crons.js +5 -4
- package/dist/daemon/api/handlers/messages.js +5 -0
- package/dist/daemon/api/handlers/nodes.js +2 -0
- package/dist/daemon/api/handlers/reports.js +11 -7
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -17,7 +17,7 @@ import { matchIndices } from '../../tui/fuzzy.js';
|
|
|
17
17
|
// Span/color primitives live in core/tui/draw.ts (one copy, shared with the
|
|
18
18
|
// `crtr view` host). Re-export the color caps so browse's importers + tests keep
|
|
19
19
|
// resolving them from this module.
|
|
20
|
-
import { clip, assemble, detectColorCaps } from '../../tui/draw.js';
|
|
20
|
+
import { clip, assemble, detectColorCaps, META_FG, RECESSIVE_FG } from '../../tui/draw.js';
|
|
21
21
|
import { renderMarkdownSpans } from '../../tui/markdown.js';
|
|
22
22
|
export { detectColorCaps };
|
|
23
23
|
// Fixed chrome heights, shared with app.ts so its viewport math never drifts
|
|
@@ -85,20 +85,29 @@ const RESET = `${ESC}0m`;
|
|
|
85
85
|
const REVERSE = `${ESC}7m`;
|
|
86
86
|
const DIM = `${ESC}2m`;
|
|
87
87
|
const BOLD = `${ESC}1m`;
|
|
88
|
-
|
|
88
|
+
// Cursor-row band. Absolute (256-color) rather than theme-derived: the browser
|
|
89
|
+
// runs in its own process that deliberately never loads pi, so it cannot read the
|
|
90
|
+
// theme. It must therefore lift off BOTH planes it can land on — a bare terminal
|
|
91
|
+
// background when launched from a shell or a tmux keybinding, and the theme's
|
|
92
|
+
// card plane when the attach viewer opens it as a painted popup (see
|
|
93
|
+
// runtime/surface-bg.ts). 236 was chosen against a bare dark terminal alone and
|
|
94
|
+
// vanishes on a card (1.01:1 on gruvbox-material); 239 reads on both (1.6–1.8:1)
|
|
95
|
+
// while leaving body text above 4.5:1 on the band itself.
|
|
96
|
+
const CURSOR_BG = `${ESC}48;5;239m`;
|
|
89
97
|
// Basic-16 ANSI fg codes used by the palette.
|
|
90
98
|
const FG_YELLOW = '33';
|
|
91
99
|
const FG_RED = '31';
|
|
92
100
|
const FG_CYAN = '36';
|
|
93
|
-
|
|
101
|
+
// META_FG / RECESSIVE_FG (imported) carry every grey in this view. Bright-black
|
|
102
|
+
// and SGR DIM are deliberately NOT used for text — see their definition in draw.ts.
|
|
103
|
+
// DIM survives for exactly one job: the whole-row base on a non-matching ancestor
|
|
104
|
+
// during a search, where it is a RELATIVE step down from whatever the row already
|
|
105
|
+
// is, not an absolute colour choice.
|
|
106
|
+
const META = `${ESC}${META_FG}m`;
|
|
107
|
+
const RECESSIVE = `${ESC}${RECESSIVE_FG}m`;
|
|
94
108
|
const FG_BRIGHT_YELLOW = '93';
|
|
95
109
|
const FG_BRIGHT_CYAN = '96'; // query-match highlight (ties to the cyan search accent)
|
|
96
110
|
const FG_MAGENTA = '35'; // pin star (distinct from the yellow ⚑ asks flag)
|
|
97
|
-
// STATUS_GLYPH + STATUS_COLOR are imported from ../status-glyph.js (the shared
|
|
98
|
-
// canvas-graph map). STATUS_COLOR there is NUMERIC SGR (e.g. 32) — wrap with
|
|
99
|
-
// `sgr()` where a string ANSI code is needed (Span fg / template literals accept
|
|
100
|
-
// the number directly).
|
|
101
|
-
const FG = (code) => String(code);
|
|
102
111
|
function fmtCtx(tokens) {
|
|
103
112
|
if (tokens <= 0)
|
|
104
113
|
return '0k';
|
|
@@ -173,17 +182,17 @@ function tabBar(active, color) {
|
|
|
173
182
|
// Keep the [ ] brackets in BOTH paths so the active tab reads without color.
|
|
174
183
|
return color ? `${BOLD}${ESC}${FG_CYAN}m[ ${t} ]${RESET}` : `${REVERSE}[ ${t} ]${RESET}`;
|
|
175
184
|
}
|
|
176
|
-
return color ? `${
|
|
185
|
+
return color ? `${META} ${t} ${RESET}` : ` ${t} `;
|
|
177
186
|
}).join('');
|
|
178
187
|
}
|
|
179
188
|
const EMPTY_HI = new Set();
|
|
180
189
|
/** Name → spans, splitting out the query-matched chars (bold + bright-cyan) so
|
|
181
|
-
* matches are scannable. Non-matched chars carry the row's name style (
|
|
182
|
-
* terminal status, bold on the cursor row). */
|
|
190
|
+
* matches are scannable. Non-matched chars carry the row's name style (the
|
|
191
|
+
* recessive grey for terminal status, bold on the cursor row). */
|
|
183
192
|
function nameSpans(name, query, style) {
|
|
184
193
|
const hi = query === '' ? EMPTY_HI : matchIndices(query, name);
|
|
185
194
|
if (hi.size === 0)
|
|
186
|
-
return [{ text: name, style: {
|
|
195
|
+
return [{ text: name, style: { fg: style.fg, bold: style.bold } }];
|
|
187
196
|
const out = [];
|
|
188
197
|
let buf = '';
|
|
189
198
|
let bufHi = false;
|
|
@@ -193,7 +202,7 @@ function nameSpans(name, query, style) {
|
|
|
193
202
|
if (bufHi)
|
|
194
203
|
out.push({ text: buf, style: { fg: FG_BRIGHT_CYAN, bold: true } });
|
|
195
204
|
else
|
|
196
|
-
out.push({ text: buf, style: {
|
|
205
|
+
out.push({ text: buf, style: { fg: style.fg, bold: style.bold } });
|
|
197
206
|
buf = '';
|
|
198
207
|
};
|
|
199
208
|
for (let i = 0; i < name.length; i++) {
|
|
@@ -229,7 +238,7 @@ function statusRail(r) {
|
|
|
229
238
|
// STATUS_W — acceptable: the rare, attention-demanding row gets a slightly
|
|
230
239
|
// wider rail (Math.max(1,…) keeps a gap), which draws the eye.
|
|
231
240
|
const word = v.word ?? ({ active: 'active', idle: 'idle', done: 'done', dead: 'dead', canceled: 'cancel' }[r.status] ?? r.status);
|
|
232
|
-
const fg =
|
|
241
|
+
const fg = v.color;
|
|
233
242
|
const text = `${v.glyph} ${word}`;
|
|
234
243
|
const pad = Math.max(1, STATUS_W - [...text].length);
|
|
235
244
|
return [
|
|
@@ -243,14 +252,14 @@ function statusRail(r) {
|
|
|
243
252
|
function metaCluster(r, now, showCwd) {
|
|
244
253
|
const ctxFg = ctxColorCode(r.ctx_tokens);
|
|
245
254
|
const out = [
|
|
246
|
-
{ text: clipPad(`${r.kind}/${r.mode}`, KM_W), style: { fg:
|
|
255
|
+
{ text: clipPad(`${r.kind}/${r.mode}`, KM_W), style: { fg: META_FG } },
|
|
247
256
|
{ text: COL_GAP },
|
|
248
|
-
{ text: fmtCtx(r.ctx_tokens).padStart(CTX_W), style: { fg: ctxFg
|
|
257
|
+
{ text: fmtCtx(r.ctx_tokens).padStart(CTX_W), style: { fg: ctxFg ?? META_FG } },
|
|
249
258
|
{ text: COL_GAP },
|
|
250
|
-
{ text: relAge(r.created, now).padStart(AGE_W), style: {
|
|
259
|
+
{ text: relAge(r.created, now).padStart(AGE_W), style: { fg: META_FG } },
|
|
251
260
|
];
|
|
252
261
|
if (showCwd) {
|
|
253
|
-
out.push({ text: COL_GAP }, { text: clipPad(`~${baseDir(r.cwd)}`, PROJ_W), style: { fg:
|
|
262
|
+
out.push({ text: COL_GAP }, { text: clipPad(`~${baseDir(r.cwd)}`, PROJ_W), style: { fg: META_FG } });
|
|
254
263
|
}
|
|
255
264
|
return out;
|
|
256
265
|
}
|
|
@@ -300,7 +309,7 @@ padToWidth = false) {
|
|
|
300
309
|
// Hanging: the fault summary travels with the name (the ⚠ + kind
|
|
301
310
|
// label live in the rail). Dim yellow so it reads as secondary detail.
|
|
302
311
|
if (r.hanging != null)
|
|
303
|
-
flags.push({ text: ` · ${faultSummary(r.hanging, now)}`, style: { fg: FG_YELLOW
|
|
312
|
+
flags.push({ text: ` · ${faultSummary(r.hanging, now)}`, style: { fg: FG_YELLOW } });
|
|
304
313
|
// Pin marker travels just before the name so a starred node is scannable in any
|
|
305
314
|
// tab/sort. Magenta keeps it distinct from the yellow ⚑ asks flag.
|
|
306
315
|
const pinMark = isPinned ? [{ text: '★ ', style: { fg: FG_MAGENTA, bold: true } }] : [];
|
|
@@ -312,11 +321,14 @@ padToWidth = false) {
|
|
|
312
321
|
const fixed = spansWidth([gutter]) + spansWidth(status) + [...treeLead].length + spansWidth(pinMark) + spansWidth(flags);
|
|
313
322
|
const rightW = spansWidth(right);
|
|
314
323
|
const nameMax = Math.max(4, width - fixed - rightW - 1);
|
|
315
|
-
|
|
324
|
+
// A finished node's name recedes to the grey ramp rather than to SGR DIM: dim
|
|
325
|
+
// is a multiplier on the theme's own foreground, and on a dark theme that lands
|
|
326
|
+
// where the canceled rows became unreadable.
|
|
327
|
+
const nameStyle = { fg: !isCursor && terminal ? RECESSIVE_FG : undefined, bold: isCursor };
|
|
316
328
|
const left = [
|
|
317
329
|
gutter,
|
|
318
330
|
...status,
|
|
319
|
-
{ text: treeLead, style: {
|
|
331
|
+
{ text: treeLead, style: { fg: RECESSIVE_FG } },
|
|
320
332
|
...pinMark,
|
|
321
333
|
...nameSpans(clip(r.name, nameMax), query, nameStyle),
|
|
322
334
|
...flags,
|
|
@@ -334,32 +346,34 @@ padToWidth = false) {
|
|
|
334
346
|
}
|
|
335
347
|
return assembleRow(left, right, width, caps.color, lineBase, fill);
|
|
336
348
|
}
|
|
337
|
-
/** The column-header row —
|
|
338
|
-
* the metadata columns read as a table.
|
|
349
|
+
/** The column-header row — grey labels aligned to the same zones the rows use, so
|
|
350
|
+
* the metadata columns read as a table. Bold + the metadata grey: a header is a
|
|
351
|
+
* landmark you scan back to, so it sits at the same weight as the column under it. */
|
|
339
352
|
function columnHeaderLine(width, showCwd, caps, showMeta = true, padToWidth = false) {
|
|
353
|
+
const hdr = { fg: META_FG, bold: true };
|
|
340
354
|
const left = [
|
|
341
355
|
{ text: ' ' }, // viewer gutter (no label)
|
|
342
|
-
{ text: 'STATUS'.padEnd(STATUS_W), style:
|
|
343
|
-
{ text: 'NAME', style:
|
|
356
|
+
{ text: 'STATUS'.padEnd(STATUS_W), style: hdr },
|
|
357
|
+
{ text: 'NAME', style: hdr },
|
|
344
358
|
];
|
|
345
359
|
if (!showMeta)
|
|
346
360
|
return assemble(left, width, caps.color, '', padToWidth);
|
|
347
361
|
const right = [
|
|
348
|
-
{ text: 'KIND/MODE'.padEnd(KM_W), style:
|
|
362
|
+
{ text: 'KIND/MODE'.padEnd(KM_W), style: hdr },
|
|
349
363
|
{ text: COL_GAP },
|
|
350
|
-
{ text: 'CTX'.padStart(CTX_W), style:
|
|
364
|
+
{ text: 'CTX'.padStart(CTX_W), style: hdr },
|
|
351
365
|
{ text: COL_GAP },
|
|
352
|
-
{ text: 'AGE'.padStart(AGE_W), style:
|
|
366
|
+
{ text: 'AGE'.padStart(AGE_W), style: hdr },
|
|
353
367
|
];
|
|
354
368
|
if (showCwd)
|
|
355
|
-
right.push({ text: COL_GAP }, { text: 'PROJECT'.padEnd(PROJ_W), style:
|
|
369
|
+
right.push({ text: COL_GAP }, { text: 'PROJECT'.padEnd(PROJ_W), style: hdr });
|
|
356
370
|
return assembleRow(left, right, width, caps.color, '', false);
|
|
357
371
|
}
|
|
358
372
|
/** The status line (always present): active profile/cwd scopes · sort mode ·
|
|
359
373
|
* committed filter. The committed-filter indicator lives here now (not its own
|
|
360
374
|
* line) so a filtered view always advertises its query without consuming a header row. */
|
|
361
375
|
function statusLine(state) {
|
|
362
|
-
const dim = (s) => `${
|
|
376
|
+
const dim = (s) => `${ESC}${META_FG}m${s}${RESET}`;
|
|
363
377
|
const profile = state.profileScope === undefined ? 'all profiles' : 'current';
|
|
364
378
|
const scope = state.cwdScope === null ? 'all dirs' : baseDir(state.cwdScope);
|
|
365
379
|
const segs = [
|
|
@@ -413,14 +427,14 @@ function markedBlock(marker, text, empty, lines, width, caps, query) {
|
|
|
413
427
|
if (lines <= 0)
|
|
414
428
|
return [];
|
|
415
429
|
const out = [];
|
|
416
|
-
const mark = caps.color ? `${
|
|
430
|
+
const mark = caps.color ? `${RECESSIVE}${marker}${RESET}` : marker;
|
|
417
431
|
const inner = Math.max(1, width - 2);
|
|
418
432
|
// No query → render the text as markdown (see blockLines for why a live query
|
|
419
433
|
// keeps the raw windowed path instead). No spacing: this panel is cramped.
|
|
420
434
|
if (query === '') {
|
|
421
435
|
const md = renderMarkdownSpans(text, inner, lines, false);
|
|
422
436
|
if (md.length === 0)
|
|
423
|
-
out.push(`${mark}${
|
|
437
|
+
out.push(`${mark}${RECESSIVE}${clip(empty, inner)}${RESET}`);
|
|
424
438
|
else
|
|
425
439
|
for (const spans of md)
|
|
426
440
|
out.push(`${out.length === 0 ? mark : ' '}${assemble(spans, inner, caps.color, '', false)}`);
|
|
@@ -430,7 +444,7 @@ function markedBlock(marker, text, empty, lines, width, caps, query) {
|
|
|
430
444
|
}
|
|
431
445
|
const snippet = previewSnippet(query, text, inner, lines);
|
|
432
446
|
if (snippet.length === 0) {
|
|
433
|
-
out.push(`${mark}${
|
|
447
|
+
out.push(`${mark}${RECESSIVE}${clip(empty, inner)}${RESET}`);
|
|
434
448
|
}
|
|
435
449
|
else {
|
|
436
450
|
for (const ln of snippet)
|
|
@@ -440,11 +454,11 @@ function markedBlock(marker, text, empty, lines, width, caps, query) {
|
|
|
440
454
|
out.push('');
|
|
441
455
|
return out;
|
|
442
456
|
}
|
|
443
|
-
/** One `Goal`/`State` briefing line: a
|
|
457
|
+
/** One `Goal`/`State` briefing line: a grey fixed-width label then the text. */
|
|
444
458
|
function recapLine(label, text, width, caps) {
|
|
445
459
|
const lbl = `${label.padEnd(6)}`;
|
|
446
460
|
const body = clip(text, Math.max(1, width - lbl.length));
|
|
447
|
-
return caps.color ? `${
|
|
461
|
+
return caps.color ? `${META}${lbl}${RESET}${BOLD}${body}${RESET}` : `${lbl}${body}`;
|
|
448
462
|
}
|
|
449
463
|
/** The bottom preview panel — exactly PREVIEW_HEIGHT lines, ordered by how fast
|
|
450
464
|
* each answers "which node was this, and can I pick it back up?":
|
|
@@ -462,7 +476,7 @@ function recapLine(label, text, width, caps) {
|
|
|
462
476
|
* The optional blocks yield their lines to the literal ones when absent, so the
|
|
463
477
|
* panel is never padded with blanks. Always full height so viewport math holds. */
|
|
464
478
|
function previewPanel(r, width, caps, now, query) {
|
|
465
|
-
const out = [`${
|
|
479
|
+
const out = [`${RECESSIVE}${'─'.repeat(width)}${RESET}`];
|
|
466
480
|
if (r === undefined) {
|
|
467
481
|
while (out.length < PREVIEW_HEIGHT)
|
|
468
482
|
out.push('');
|
|
@@ -486,7 +500,7 @@ function previewPanel(r, width, caps, now, query) {
|
|
|
486
500
|
if (r.viewed === true)
|
|
487
501
|
metaPieces.push('\uf06e viewing');
|
|
488
502
|
const metaText = clip(metaPieces.filter((p) => p !== '').join(' · '), Math.max(0, width - 2));
|
|
489
|
-
out.push(caps.color ? `${glyph} ${
|
|
503
|
+
out.push(caps.color ? `${glyph} ${META}${metaText}${RESET}` : `${glyph} ${metaText}`);
|
|
490
504
|
const alloc = previewAllocation(r.recap !== undefined, r.report !== undefined);
|
|
491
505
|
// Goal / State — the generated briefing, the fastest "what is this and where is
|
|
492
506
|
// it" in the panel. Absent for a node that never idled long enough to make one.
|
|
@@ -507,14 +521,14 @@ function previewPanel(r, width, caps, now, query) {
|
|
|
507
521
|
const age = relAge(r.report.ts, now);
|
|
508
522
|
const stamp = `${age === '' ? '' : `${age} `}${r.report.kind}`;
|
|
509
523
|
const headline = r.report.heading === '' ? r.report.body : r.report.heading;
|
|
510
|
-
const mark = caps.color ? `${
|
|
524
|
+
const mark = caps.color ? `${RECESSIVE}▣ ${RESET}` : '▣ ';
|
|
511
525
|
const inner = Math.max(1, width - 2);
|
|
512
|
-
const stampText = caps.color ? `${ESC}${FG_CYAN}m${stamp}${RESET}${
|
|
526
|
+
const stampText = caps.color ? `${ESC}${FG_CYAN}m${stamp}${RESET}${RECESSIVE} · ${RESET}` : `${stamp} · `;
|
|
513
527
|
out.push(`${mark}${stampText}${clip(headline, Math.max(1, inner - stamp.length - 3))}`);
|
|
514
528
|
for (let i = 1; i < alloc.report; i++) {
|
|
515
529
|
const detail = r.report.heading === '' ? '' : r.report.body;
|
|
516
530
|
const ln = previewSnippet('', detail, inner, alloc.report - 1)[i - 1];
|
|
517
|
-
out.push(ln === undefined ? '' : ` ${
|
|
531
|
+
out.push(ln === undefined ? '' : ` ${META}${clip(ln.text, inner)}${RESET}`);
|
|
518
532
|
}
|
|
519
533
|
}
|
|
520
534
|
return out;
|
|
@@ -537,15 +551,16 @@ function previewPanel(r, width, caps, now, query) {
|
|
|
537
551
|
// natural size, the largest gives a line back until it fits, and only then does the
|
|
538
552
|
// least-valuable section drop out entirely (report → state → goal → reply; the ask
|
|
539
553
|
// is the recognition anchor and never drops).
|
|
540
|
-
/** A
|
|
541
|
-
* suffix rides after the label in its own hue (the report's age · kind stamp).
|
|
554
|
+
/** A hairline with a small-caps section label — `── GOAL ────────`. The optional
|
|
555
|
+
* suffix rides after the label in its own hue (the report's age · kind stamp).
|
|
556
|
+
* The label is a landmark (metadata grey + bold); the rule itself recedes. */
|
|
542
557
|
function sectionRule(label, width, caps, suffix) {
|
|
543
558
|
const suf = suffix === undefined ? '' : `${suffix.text} `;
|
|
544
559
|
const tail = Math.max(0, width - 4 - label.length - suf.length);
|
|
545
560
|
const sufStyled = suffix === undefined
|
|
546
561
|
? ''
|
|
547
|
-
: caps.color ? `${ESC}${suffix.fg}m${suffix.text}${RESET}${
|
|
548
|
-
return `${
|
|
562
|
+
: caps.color ? `${ESC}${suffix.fg}m${suffix.text}${RESET}${RECESSIVE} ` : `${suffix.text} `;
|
|
563
|
+
return `${RECESSIVE}── ${META}${BOLD}${label}${RESET}${RECESSIVE} ${sufStyled}${'─'.repeat(tail)}${RESET}`;
|
|
549
564
|
}
|
|
550
565
|
/** A marked body block with a hanging indent: `marker` leads the first line, `cont`
|
|
551
566
|
* every wrapped line after it (`▌ ` keeps the accent bar running down a quote).
|
|
@@ -566,7 +581,7 @@ function blockLines(marker, cont, markerStyle, text, empty, maxLines, width, cap
|
|
|
566
581
|
if (md.length === 0) {
|
|
567
582
|
if (empty === undefined)
|
|
568
583
|
return [];
|
|
569
|
-
return [`${mk(marker, markerStyle)}${
|
|
584
|
+
return [`${mk(marker, markerStyle)}${RECESSIVE}${clip(empty, inner)}${RESET}`];
|
|
570
585
|
}
|
|
571
586
|
return md.map((spans, i) => `${mk(i === 0 ? marker : cont, markerStyle)}${assemble(spans, inner, caps.color, '', false)}`);
|
|
572
587
|
}
|
|
@@ -574,17 +589,17 @@ function blockLines(marker, cont, markerStyle, text, empty, maxLines, width, cap
|
|
|
574
589
|
if (snippet.length === 0) {
|
|
575
590
|
if (empty === undefined)
|
|
576
591
|
return [];
|
|
577
|
-
return [`${mk(marker, markerStyle)}${
|
|
592
|
+
return [`${mk(marker, markerStyle)}${RECESSIVE}${clip(empty, inner)}${RESET}`];
|
|
578
593
|
}
|
|
579
594
|
return snippet.map((ln, i) => `${mk(i === 0 ? marker : cont, markerStyle)}${snippetLine(ln, inner, caps, bodyStyle)}`);
|
|
580
595
|
}
|
|
581
|
-
/** One `label value` fact cell: a 4-col right-aligned
|
|
596
|
+
/** One `label value` fact cell: a 4-col right-aligned grey label, then the value in
|
|
582
597
|
* its own hue, padded so a two-column facts block aligns down both columns. */
|
|
583
598
|
function factCell(label, value, w, style, pad) {
|
|
584
599
|
const vw = Math.max(1, w - 5);
|
|
585
600
|
const v = clip(value, vw);
|
|
586
601
|
return [
|
|
587
|
-
{ text: `${label.padStart(4)} `, style: {
|
|
602
|
+
{ text: `${label.padStart(4)} `, style: { fg: RECESSIVE_FG } },
|
|
588
603
|
{ text: v, style },
|
|
589
604
|
...(pad && v.length < vw ? [{ text: ' '.repeat(vw - v.length) }] : []),
|
|
590
605
|
];
|
|
@@ -594,10 +609,10 @@ function factCell(label, value, w, style, pad) {
|
|
|
594
609
|
function factsBlock(r, width, now, caps) {
|
|
595
610
|
const ctxFg = ctxColorCode(r.ctx_tokens);
|
|
596
611
|
const facts = [
|
|
597
|
-
['kind', `${r.kind}/${r.mode}`, { fg:
|
|
598
|
-
['ctx', fmtCtx(r.ctx_tokens), { fg: ctxFg
|
|
599
|
-
['proj', `~${baseDir(r.cwd)}`, { fg:
|
|
600
|
-
['age', relAge(r.created, now), {
|
|
612
|
+
['kind', `${r.kind}/${r.mode}`, { fg: META_FG }],
|
|
613
|
+
['ctx', fmtCtx(r.ctx_tokens), { fg: ctxFg ?? META_FG }],
|
|
614
|
+
['proj', `~${baseDir(r.cwd)}`, { fg: META_FG }],
|
|
615
|
+
['age', relAge(r.created, now), { fg: META_FG }],
|
|
601
616
|
];
|
|
602
617
|
const out = [];
|
|
603
618
|
// Two columns at any panel width the side layout allows; the column is capped so
|
|
@@ -628,7 +643,7 @@ function panelStatusLine(r, width, caps, now) {
|
|
|
628
643
|
hanging: r.hanging ?? null,
|
|
629
644
|
});
|
|
630
645
|
const word = v.word ?? ({ active: 'active', idle: 'idle', done: 'done', dead: 'dead', canceled: 'canceled' }[r.status] ?? r.status);
|
|
631
|
-
const spans = [{ text: `${v.glyph} ${word}`, style: { fg:
|
|
646
|
+
const spans = [{ text: `${v.glyph} ${word}`, style: { fg: v.color, bold: true } }];
|
|
632
647
|
const sep = () => ({ text: ' ' });
|
|
633
648
|
if (r.asks > 0)
|
|
634
649
|
spans.push(sep(), { text: `⚑${r.asks} ${r.asks === 1 ? 'ask' : 'asks'}`, style: { fg: FG_BRIGHT_YELLOW, bold: true } });
|
|
@@ -642,13 +657,13 @@ function panelStatusLine(r, width, caps, now) {
|
|
|
642
657
|
* beneath it: the pinned star, the node name in bold, its short id flush right. */
|
|
643
658
|
function panelTitleLine(r, width, caps, isPinned) {
|
|
644
659
|
if (r === undefined)
|
|
645
|
-
return assemble([{ text: 'PREVIEW', style: {
|
|
660
|
+
return assemble([{ text: 'PREVIEW', style: { fg: META_FG, bold: true } }], width, caps.color, '', false);
|
|
646
661
|
const left = [];
|
|
647
662
|
if (isPinned)
|
|
648
663
|
left.push({ text: '★ ', style: { fg: FG_MAGENTA, bold: true } });
|
|
649
664
|
const id = r.node_id.slice(0, 8);
|
|
650
665
|
left.push({ text: clip(r.name, Math.max(4, width - id.length - 2 - spansWidth(left))), style: { bold: true } });
|
|
651
|
-
return assembleRow(left, [{ text: id, style: {
|
|
666
|
+
return assembleRow(left, [{ text: id, style: { fg: META_FG } }], width, caps.color, '', false);
|
|
652
667
|
}
|
|
653
668
|
/** The side panel's body, at most `height` lines (never padded to it). */
|
|
654
669
|
function sidePanelBody(r, width, height, caps, now, query) {
|
|
@@ -656,7 +671,7 @@ function sidePanelBody(r, width, height, caps, now, query) {
|
|
|
656
671
|
if (height <= 0)
|
|
657
672
|
return out;
|
|
658
673
|
if (r === undefined) {
|
|
659
|
-
out.push(`${
|
|
674
|
+
out.push(`${RECESSIVE}(no selection)${RESET}`);
|
|
660
675
|
return out;
|
|
661
676
|
}
|
|
662
677
|
out.push(panelStatusLine(r, width, caps, now));
|
|
@@ -669,7 +684,7 @@ function sidePanelBody(r, width, height, caps, now, query) {
|
|
|
669
684
|
frozen: isFrozenRow(r),
|
|
670
685
|
hanging: r.hanging ?? null,
|
|
671
686
|
});
|
|
672
|
-
const accent = { fg:
|
|
687
|
+
const accent = { fg: v.color };
|
|
673
688
|
const secs = [];
|
|
674
689
|
if (r.recap !== undefined && r.recap.goal !== '') {
|
|
675
690
|
const goal = r.recap.goal;
|
|
@@ -688,7 +703,7 @@ function sidePanelBody(r, width, height, caps, now, query) {
|
|
|
688
703
|
lines: (n) => blockLines('❯ ', ' ', { fg: FG_BRIGHT_CYAN, bold: true }, askText, '(no spawn prompt)', n, width, caps, query) });
|
|
689
704
|
const reply = r.lastAssistant ?? '';
|
|
690
705
|
secs.push({ label: 'REPLY', drop: 1, min: 2, want: 9,
|
|
691
|
-
lines: (n) => blockLines('↩ ', ' ', {
|
|
706
|
+
lines: (n) => blockLines('↩ ', ' ', { fg: META_FG }, reply, '(no reply yet)', n, width, caps, '') });
|
|
692
707
|
if (r.report !== undefined) {
|
|
693
708
|
const rep = r.report;
|
|
694
709
|
const age = relAge(rep.ts, now);
|
|
@@ -699,7 +714,7 @@ function sidePanelBody(r, width, height, caps, now, query) {
|
|
|
699
714
|
const headline = rep.heading === '' ? rep.body : rep.heading;
|
|
700
715
|
const head = blockLines('▣ ', ' ', { fg: FG_CYAN, bold: true }, headline, undefined, Math.min(2, n), width, caps, '', { bold: true });
|
|
701
716
|
if (rep.heading !== '' && n > head.length) {
|
|
702
|
-
head.push(...blockLines(' ', ' ', undefined, rep.body, undefined, n - head.length, width, caps, '', {
|
|
717
|
+
head.push(...blockLines(' ', ' ', undefined, rep.body, undefined, n - head.length, width, caps, '', { fg: META_FG }));
|
|
703
718
|
}
|
|
704
719
|
return head;
|
|
705
720
|
},
|
|
@@ -806,7 +821,7 @@ function renderHelp(size, caps) {
|
|
|
806
821
|
const lines = [];
|
|
807
822
|
lines.push(`${BOLD}Canvas navigator — keys${RESET}`);
|
|
808
823
|
lines.push('');
|
|
809
|
-
const dim = (s) => `${
|
|
824
|
+
const dim = (s) => `${META}${s}${RESET}`;
|
|
810
825
|
for (const [group, keys, desc] of HELP_ROWS) {
|
|
811
826
|
const g = group.padEnd(6);
|
|
812
827
|
const k = keys.padEnd(12);
|
|
@@ -840,8 +855,8 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
840
855
|
const parts = statusCounts(state.tree);
|
|
841
856
|
const summaryPlain = parts.map((p) => `${STATUS_GLYPH[p.status]}${p.count}`).join(' ');
|
|
842
857
|
const summaryColored = caps.color
|
|
843
|
-
? parts.map((p) => `${ESC}${STATUS_COLOR[p.status]}m${STATUS_GLYPH[p.status]}${RESET}${
|
|
844
|
-
: `${
|
|
858
|
+
? parts.map((p) => `${ESC}${STATUS_COLOR[p.status]}m${STATUS_GLYPH[p.status]}${RESET}${META}${p.count}${RESET}`).join(' ')
|
|
859
|
+
: `${META}${summaryPlain}${RESET}`;
|
|
845
860
|
const gap = width - titlePlainLen - summaryPlain.length;
|
|
846
861
|
lines.push(gap > 1 ? `${title}${' '.repeat(gap)}${summaryColored}` : title);
|
|
847
862
|
// line 2 — tab bar.
|
|
@@ -862,7 +877,7 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
862
877
|
const layout = previewLayout(cols, previewOn);
|
|
863
878
|
const side = layout.mode === 'side';
|
|
864
879
|
const rowsW = side ? layout.rowsW : width;
|
|
865
|
-
const gutter = `${
|
|
880
|
+
const gutter = `${RECESSIVE}${SIDE_GUTTER}${RESET}`;
|
|
866
881
|
const sel = state.visible[state.cursor];
|
|
867
882
|
const selRow = sel !== undefined ? state.tree.nodes.get(sel.id)?.row : undefined;
|
|
868
883
|
// column header (table labels, plus the panel title in side mode) + separator.
|
|
@@ -872,8 +887,8 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
872
887
|
? `${colHeader}${gutter}${panelTitleLine(selRow, layout.panelW, caps, sel !== undefined && state.pinned.has(sel.id))}`
|
|
873
888
|
: colHeader);
|
|
874
889
|
lines.push(side
|
|
875
|
-
? `${
|
|
876
|
-
: `${
|
|
890
|
+
? `${RECESSIVE}${'─'.repeat(rowsW)}─┼─${'─'.repeat(layout.panelW)}${RESET}`
|
|
891
|
+
: `${RECESSIVE}${'─'.repeat(width)}${RESET}`);
|
|
877
892
|
// body — windowed visible rows, leaving room for the (optional) bottom panel.
|
|
878
893
|
const head = lines.length; // === headerHeight(state.search)
|
|
879
894
|
const previewH = layout.mode === 'bottom' ? PREVIEW_HEIGHT : 0;
|
|
@@ -886,7 +901,7 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
886
901
|
const compose = (row, i) => (side ? `${row}${gutter}${panel[i] ?? ''}` : row);
|
|
887
902
|
const blankRow = side ? ' '.repeat(rowsW) : '';
|
|
888
903
|
if (state.visible.length === 0) {
|
|
889
|
-
lines.push(`${
|
|
904
|
+
lines.push(`${META} (no nodes match this view)${RESET}`);
|
|
890
905
|
for (let i = 1; i < viewport; i++)
|
|
891
906
|
lines.push('');
|
|
892
907
|
}
|
|
@@ -923,7 +938,7 @@ export function renderFrame(state, size, caps = { color: false, color256: false
|
|
|
923
938
|
: state.readOnly === true
|
|
924
939
|
? '↑↓ move →/← tree / search p pin v preview Tab tabs ? more q quit — read-only remote canvas'
|
|
925
940
|
: `${hangingHint}↑↓ move ⏎ open O bg / search p pin v preview m msg x close ? more q quit`;
|
|
926
|
-
lines.push(`${
|
|
941
|
+
lines.push(`${META}${clip(footer, width)}${RESET}`);
|
|
927
942
|
}
|
|
928
943
|
// Assemble: home, each line cleared to EOL, then clear below.
|
|
929
944
|
const body = lines.map((l) => `${l}${ESC}K`).join('\r\n');
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { NodeStatus } from './types.js';
|
|
2
2
|
import { type Fault, type FaultKind } from '../runtime/fault.js';
|
|
3
3
|
export declare const STATUS_GLYPH: Record<NodeStatus, string>;
|
|
4
|
-
export declare const STATUS_COLOR: Record<NodeStatus,
|
|
4
|
+
export declare const STATUS_COLOR: Record<NodeStatus, string>;
|
|
5
5
|
export declare const HANGING_GLYPH = "\u26A0";
|
|
6
|
-
export declare const HANGING_COLOR = 33;
|
|
6
|
+
export declare const HANGING_COLOR = "33";
|
|
7
7
|
/** Short label for the fault kind. The overlay keeps one glyph and lets the
|
|
8
8
|
* label carry the distinction. */
|
|
9
9
|
export declare function hangingLabel(kind: FaultKind): string;
|
|
10
10
|
export interface NodeVisual {
|
|
11
11
|
glyph: string;
|
|
12
|
-
color
|
|
12
|
+
/** SGR fg parameters, e.g. '32' or '38;5;243' — interpolate as `\x1b[${color}m`. */
|
|
13
|
+
color: string;
|
|
13
14
|
/** Overlay word (hanging kind label / 'live'); undefined for a plain status
|
|
14
15
|
* (the caller supplies its own status word). */
|
|
15
16
|
word?: string;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// because a frozen row has no process at all: any streaming read against it is
|
|
7
7
|
// a stale marker, and showing it as active is the lie the freeze state exists
|
|
8
8
|
// to stop telling.
|
|
9
|
+
import { RECESSIVE_FG } from '../tui/draw.js';
|
|
9
10
|
import { FAULT_QUIET_MS } from '../runtime/fault.js';
|
|
10
11
|
// ── Status glyphs (the mono, color-free primary encoding) ─────────────────────
|
|
11
12
|
export const STATUS_GLYPH = {
|
|
@@ -15,24 +16,31 @@ export const STATUS_GLYPH = {
|
|
|
15
16
|
dead: '✗',
|
|
16
17
|
canceled: '⊘',
|
|
17
18
|
};
|
|
18
|
-
// ── Status hue:
|
|
19
|
+
// ── Status hue: SGR fg parameters per status. ─────────────────────────────────
|
|
19
20
|
// Reinforces the glyph; the glyph SHAPE stays the NO_COLOR carrier.
|
|
21
|
+
//
|
|
22
|
+
// The four live statuses use basic-16 hues, which every terminal palette renders
|
|
23
|
+
// as a saturated colour. `canceled` cannot: it wants GREY, and basic-16 grey is
|
|
24
|
+
// bright-black — a slot themes routinely set to a value barely off their own
|
|
25
|
+
// background, which is what made every canceled row unreadable. Grey is therefore
|
|
26
|
+
// an explicit 256-colour step (see RECESSIVE_FG) chosen for measured contrast
|
|
27
|
+
// rather than delegated to the palette.
|
|
20
28
|
export const STATUS_COLOR = {
|
|
21
|
-
active: 32, // green
|
|
22
|
-
idle: 33, // yellow
|
|
23
|
-
done: 36, // cyan
|
|
24
|
-
dead: 31, // red
|
|
25
|
-
canceled:
|
|
29
|
+
active: '32', // green
|
|
30
|
+
idle: '33', // yellow
|
|
31
|
+
done: '36', // cyan
|
|
32
|
+
dead: '31', // red
|
|
33
|
+
canceled: RECESSIVE_FG,
|
|
26
34
|
};
|
|
27
35
|
// ── Streaming overlay (genuinely mid-turn) — browse's existing statusRail visual.
|
|
28
36
|
const STREAMING_GLYPH = '⟳';
|
|
29
|
-
const STREAMING_COLOR = 92; // bright green (brighter than active-status green)
|
|
37
|
+
const STREAMING_COLOR = '92'; // bright green (brighter than active-status green)
|
|
30
38
|
// ── Frozen overlay (waiting on a broker slot: marked, but no process). ───────
|
|
31
39
|
const FROZEN_GLYPH = '◌';
|
|
32
|
-
const FROZEN_COLOR = 94; // bright blue
|
|
40
|
+
const FROZEN_COLOR = '94'; // bright blue
|
|
33
41
|
// ── Active-fault overlay (one glyph, kind label carries the detail). ─────────
|
|
34
42
|
export const HANGING_GLYPH = '⚠';
|
|
35
|
-
export const HANGING_COLOR = 33; // yellow
|
|
43
|
+
export const HANGING_COLOR = '33'; // yellow
|
|
36
44
|
/** Short label for the fault kind. The overlay keeps one glyph and lets the
|
|
37
45
|
* label carry the distinction. */
|
|
38
46
|
export function hangingLabel(kind) {
|
|
@@ -106,6 +106,24 @@ export interface ManagedWorktree {
|
|
|
106
106
|
blocked: string;
|
|
107
107
|
};
|
|
108
108
|
sweep?: ManagedWorktreeSweep;
|
|
109
|
+
/** Recorded ONLY by an explicit branch-moved `close` when the checkout's
|
|
110
|
+
* actual branch has drifted from `branch` (its recorded `crtr/*` ref is
|
|
111
|
+
* gone — ordinary PR work moved it there) but that branch's content was
|
|
112
|
+
* proven, immediately before this was written, already contained in the
|
|
113
|
+
* recorded base or its upstream. Authorizes ONLY the daemon sweep's later
|
|
114
|
+
* checkout removal for this EXACT path + git common dir + observed branch
|
|
115
|
+
* + observed SHA — the sweep re-verifies all four before acting and NEVER
|
|
116
|
+
* deletes `observed_branch`, since it is not a `crtr/*` branch crouter
|
|
117
|
+
* created. A mismatch on re-verification invalidates the authorization; a
|
|
118
|
+
* fresh explicit close must re-establish it. Absent for every ordinary
|
|
119
|
+
* managed worktree, and for `abandon`'s branch-moved path (which disposes
|
|
120
|
+
* immediately instead of deferring, so it needs no persisted grant). */
|
|
121
|
+
disposal?: {
|
|
122
|
+
observed_branch: string;
|
|
123
|
+
observed_sha: string;
|
|
124
|
+
common_dir: string;
|
|
125
|
+
authorized: string;
|
|
126
|
+
};
|
|
109
127
|
}
|
|
110
128
|
/** Immutable companion provenance. The origin session/leaf/model coordinates
|
|
111
129
|
* remain canonical in the review row rather than being duplicated into node
|
|
@@ -32,6 +32,13 @@ export interface ValidatedBundle {
|
|
|
32
32
|
path: string;
|
|
33
33
|
bytes: Uint8Array;
|
|
34
34
|
}>;
|
|
35
|
+
hooks?: {
|
|
36
|
+
manifest: Uint8Array;
|
|
37
|
+
files: ReadonlyArray<{
|
|
38
|
+
path: string;
|
|
39
|
+
bytes: Uint8Array;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
35
42
|
}
|
|
36
43
|
export interface PluginBundleValidation {
|
|
37
44
|
bundle?: ValidatedBundle;
|
|
@@ -38,13 +38,13 @@ function validateMemberName(path, type) {
|
|
|
38
38
|
}
|
|
39
39
|
function validateMemberShape(path, type) {
|
|
40
40
|
if (type === 'file') {
|
|
41
|
-
if (path === 'bundle.json' || path === 'commands.json' || (path.startsWith('memory/') && path.endsWith('.md')))
|
|
41
|
+
if (path === 'bundle.json' || path === 'commands.json' || path === 'hooks.json' || (path.startsWith('memory/') && path.endsWith('.md')) || path.startsWith('hooks/'))
|
|
42
42
|
return undefined;
|
|
43
|
-
return bundleInvalid('bundle contains an unsupported file member', path, 'bundle.json, commands.json,
|
|
43
|
+
return bundleInvalid('bundle contains an unsupported file member', path, 'bundle.json, commands.json, hooks.json, memory/**/*.md, or hooks/**', 'Remove unsupported members from the archive.', path);
|
|
44
44
|
}
|
|
45
|
-
if (path === 'memory' || path.startsWith('memory/'))
|
|
45
|
+
if (path === 'memory' || path.startsWith('memory/') || path === 'hooks' || path.startsWith('hooks/'))
|
|
46
46
|
return undefined;
|
|
47
|
-
return bundleInvalid('bundle contains an unsupported directory member', path, 'memory or a directory below
|
|
47
|
+
return bundleInvalid('bundle contains an unsupported directory member', path, 'memory or hooks, or a directory below either subtree', 'Remove unsupported directory members from the archive.', path);
|
|
48
48
|
}
|
|
49
49
|
/** A member path as it will exist on disk: directories carry no trailing slash. */
|
|
50
50
|
function materializedPath(member) {
|
|
@@ -221,6 +221,14 @@ export async function validatePluginBundle(archive, options) {
|
|
|
221
221
|
issues: [bundleInvalid('bundle must contain exactly one bundle.json and one commands.json regular file', `bundle.json=${bundle !== undefined}, commands.json=${commands !== undefined}`, 'one regular bundle.json and one regular commands.json member', 'Add the required metadata members to the archive.')],
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
|
+
const hookManifest = parsed.members.find((member) => member.path === 'hooks.json' && member.type === 'file');
|
|
225
|
+
const hookFiles = parsed.members.filter((member) => member.type === 'file' && member.path.startsWith('hooks/'));
|
|
226
|
+
const carriesHooks = hookManifest !== undefined || parsed.members.some((member) => member.path === 'hooks' || member.path.startsWith('hooks/'));
|
|
227
|
+
if (carriesHooks && (hookManifest === undefined || !hookFiles.some((member) => member.path === 'hooks/dispatch'))) {
|
|
228
|
+
return {
|
|
229
|
+
issues: [bundleInvalid('bundle hook payload must contain both hooks.json and hooks/dispatch regular files', `hooks.json=${hookManifest !== undefined}, hooks/dispatch=${hookFiles.some((member) => member.path === 'hooks/dispatch')}`, 'both hooks.json and hooks/dispatch regular files', 'Add both required hook members or remove the hook payload.')],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
224
232
|
const metadata = parseBundleMetadata(bundle.bytes);
|
|
225
233
|
if (metadata.issue !== undefined)
|
|
226
234
|
return { issues: [metadata.issue] };
|
|
@@ -241,6 +249,12 @@ export async function validatePluginBundle(archive, options) {
|
|
|
241
249
|
memory: parsed.members
|
|
242
250
|
.filter((member) => member.type === 'file' && member.path.startsWith('memory/'))
|
|
243
251
|
.map((member) => ({ path: member.path, bytes: member.bytes })),
|
|
252
|
+
...(hookManifest !== undefined ? {
|
|
253
|
+
hooks: {
|
|
254
|
+
manifest: hookManifest.bytes,
|
|
255
|
+
files: hookFiles.map((member) => ({ path: member.path, bytes: member.bytes })),
|
|
256
|
+
},
|
|
257
|
+
} : {}),
|
|
244
258
|
},
|
|
245
259
|
issues: [],
|
|
246
260
|
};
|
package/dist/core/git.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ export interface GitSyncOptions {
|
|
|
11
11
|
timeoutMs?: number;
|
|
12
12
|
}
|
|
13
13
|
export declare function gitSync(args: string[], cwd?: string, input?: string, options?: GitSyncOptions): GitResult;
|
|
14
|
-
export
|
|
14
|
+
export interface GitAsyncOptions {
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
}
|
|
18
|
+
export declare function gitAsync(args: string[], cwd?: string, input?: string, options?: GitAsyncOptions): Promise<GitResult>;
|
|
15
19
|
export declare function clone(url: string, dest: string, opts?: {
|
|
16
20
|
ref?: string;
|
|
17
21
|
depth?: number;
|
package/dist/core/git.js
CHANGED
|
@@ -10,22 +10,36 @@ export function gitSync(args, cwd, input, options = {}) {
|
|
|
10
10
|
const timedOut = res.error?.code === 'ETIMEDOUT';
|
|
11
11
|
return { status, stdout, stderr, ...(completed ? {} : { executionFailed: true }), ...(processError === undefined ? {} : { processError }), ...(timedOut ? { timedOut: true } : {}) };
|
|
12
12
|
}
|
|
13
|
-
export async function gitAsync(args, cwd, input) {
|
|
13
|
+
export async function gitAsync(args, cwd, input, options = {}) {
|
|
14
14
|
return new Promise((resolve) => {
|
|
15
|
-
const child = spawn('git', args, { cwd });
|
|
15
|
+
const child = spawn('git', args, { cwd, ...(options.env === undefined ? {} : { env: options.env }) });
|
|
16
16
|
if (input === undefined)
|
|
17
17
|
child.stdin.end();
|
|
18
18
|
else
|
|
19
19
|
child.stdin.end(input);
|
|
20
20
|
let stdout = '';
|
|
21
21
|
let stderr = '';
|
|
22
|
+
let timedOut = false;
|
|
23
|
+
let settled = false;
|
|
24
|
+
const timer = options.timeoutMs === undefined ? undefined : setTimeout(() => {
|
|
25
|
+
timedOut = true;
|
|
26
|
+
child.kill('SIGTERM');
|
|
27
|
+
}, options.timeoutMs);
|
|
28
|
+
const settle = (result) => {
|
|
29
|
+
if (settled)
|
|
30
|
+
return;
|
|
31
|
+
settled = true;
|
|
32
|
+
if (timer !== undefined)
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
resolve(result);
|
|
35
|
+
};
|
|
22
36
|
child.stdout.on('data', (d) => (stdout += d.toString()));
|
|
23
37
|
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
24
38
|
child.on('close', (status) => {
|
|
25
39
|
const code = typeof status === 'number' ? status : 1;
|
|
26
|
-
|
|
40
|
+
settle({ status: code, stdout, stderr, ...(timedOut ? { timedOut: true, executionFailed: true } : {}) });
|
|
27
41
|
});
|
|
28
|
-
child.on('error', (e) =>
|
|
42
|
+
child.on('error', (e) => settle({ status: 1, stdout: '', stderr: String(e), executionFailed: true, processError: String(e) }));
|
|
29
43
|
});
|
|
30
44
|
}
|
|
31
45
|
export function clone(url, dest, opts = {}) {
|