create-egregore 0.18.17 → 0.19.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/assets/egregore-launcher.js +121 -51
- package/lib/local.js +237 -144
- package/lib/ui.js +465 -18
- package/package.json +2 -2
- package/runtime/codex/.codex/spec-manifest.json +7 -1
- package/runtime/codex/AGENTS.md +7 -1
- package/runtime/codex/bin/codex-render-spec.mjs +7 -0
- package/runtime/codex/bin/lib/git-sync.sh +3 -3
- package/runtime/codex/bin/lib/identity.sh +10 -0
- package/runtime/codex/bin/telemetry.sh +54 -13
package/lib/ui.js
CHANGED
|
@@ -209,7 +209,19 @@ function spinner(msg) {
|
|
|
209
209
|
|
|
210
210
|
// ── prompts ─────────────────────────────────────────────────────────
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
// Footer band — the same visual chrome as the launcher's status band:
|
|
213
|
+
// accent background (the banner's color slot), dark text, padded like a
|
|
214
|
+
// bar. Walks with the active prompt in this scrolling wizard; bg 43
|
|
215
|
+
// (yellow) marks errors/questions that need answering. Any ANSI inside
|
|
216
|
+
// the content is stripped — a RESET mid-line would kill the fill.
|
|
217
|
+
function footerBand(content, bg = 46) {
|
|
218
|
+
const plain = String(content).replace(/\x1b\[[0-9;]*m/g, "");
|
|
219
|
+
if (!COLOR) return ` ${plain}`;
|
|
220
|
+
const w = termWidth() - 3;
|
|
221
|
+
return ` ${esc(bg)}${BLACK}${pad(` ${plain}`, w)}${RESET}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function promptLegacy(question, { default: defaultValue } = {}) {
|
|
213
225
|
return new Promise((resolve) => {
|
|
214
226
|
const rl = readline.createInterface({
|
|
215
227
|
input: process.stdin,
|
|
@@ -226,9 +238,105 @@ function prompt(question, { default: defaultValue } = {}) {
|
|
|
226
238
|
});
|
|
227
239
|
}
|
|
228
240
|
|
|
241
|
+
// Shared single-line editor: the input line plus ONE dim hint line pinned
|
|
242
|
+
// below it — the dynamic footer every wizard prompt shares, instead of
|
|
243
|
+
// per-label "(esc back)" clutter. opts.back controls whether stepping
|
|
244
|
+
// backwards is possible: with back, ↑ resolves BACK (esc is a silent
|
|
245
|
+
// alias); without it, ↑/esc are no-ops. Esc NEVER quits either way —
|
|
246
|
+
// ctrl-c is the only abort. Accepts multi-char printable chunks so paste
|
|
247
|
+
// works (Telegram links, usernames).
|
|
248
|
+
function lineInput(question, { default: defaultValue, hint, back = false } = {}) {
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
let buf = "";
|
|
251
|
+
const label = defaultValue
|
|
252
|
+
? ` ${CYAN}?${RESET} ${question} ${DIM}[${defaultValue}]${RESET} `
|
|
253
|
+
: ` ${CYAN}?${RESET} ${question} `;
|
|
254
|
+
const hintLine = footerBand(hint || (back ? "enter accept · ↑ back · ctrl-c quit" : "enter accept · ctrl-c quit"));
|
|
255
|
+
const labelCols = visibleLen(label);
|
|
256
|
+
const render = () => {
|
|
257
|
+
// Input line, a blank margin row, then the footer band — the band
|
|
258
|
+
// needs breathing room or it visually fuses with the input. The real
|
|
259
|
+
// cursor parks at the end of the typed text so keystrokes land where
|
|
260
|
+
// the eye is.
|
|
261
|
+
process.stdout.write(
|
|
262
|
+
`\r\x1b[2K${label}${buf}\n` +
|
|
263
|
+
`\x1b[2K\n` +
|
|
264
|
+
`\x1b[2K${hintLine}` +
|
|
265
|
+
`\x1b[2A\x1b[${labelCols + buf.length + 1}G`,
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
const clearHint = () => {
|
|
269
|
+
process.stdout.write(`\x1b[1B\r\x1b[2K\x1b[1B\r\x1b[2K\x1b[2A\x1b[${labelCols + buf.length + 1}G`);
|
|
270
|
+
};
|
|
271
|
+
const cleanup = () => {
|
|
272
|
+
process.stdin.setRawMode(false);
|
|
273
|
+
process.stdin.pause();
|
|
274
|
+
process.stdin.removeListener("keypress", onKey);
|
|
275
|
+
};
|
|
276
|
+
const onKey = (_str, key) => {
|
|
277
|
+
if (!key) return;
|
|
278
|
+
if (key.ctrl && key.name === "c") {
|
|
279
|
+
cleanup();
|
|
280
|
+
clearHint();
|
|
281
|
+
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
282
|
+
process.exit(130);
|
|
283
|
+
}
|
|
284
|
+
if (key.name === "up" || key.name === "escape") {
|
|
285
|
+
if (!back) return; // nothing to go back to — and esc never quits
|
|
286
|
+
cleanup();
|
|
287
|
+
clearHint();
|
|
288
|
+
process.stdout.write(`\r\x1b[2K${label}${DIM}← back${RESET}\n`);
|
|
289
|
+
resolve(BACK);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (key.name === "return") {
|
|
293
|
+
cleanup();
|
|
294
|
+
clearHint();
|
|
295
|
+
process.stdout.write("\n");
|
|
296
|
+
resolve(buf.trim() || (defaultValue ?? ""));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (key.name === "backspace") {
|
|
300
|
+
buf = buf.slice(0, -1);
|
|
301
|
+
render();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (_str && !key.ctrl && !key.meta) {
|
|
305
|
+
const clean = _str.replace(/[\x00-\x1f\x7f]/g, "");
|
|
306
|
+
if (clean) {
|
|
307
|
+
buf += clean;
|
|
308
|
+
render();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
readline.emitKeypressEvents(process.stdin);
|
|
313
|
+
process.stdin.setRawMode(true);
|
|
314
|
+
process.stdin.resume();
|
|
315
|
+
process.stdin.on("keypress", onKey);
|
|
316
|
+
render();
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Standard prompt — interactive sessions get the hint-line footer, pipes
|
|
321
|
+
// and CI keep the plain readline fallback.
|
|
322
|
+
function prompt(question, opts = {}) {
|
|
323
|
+
if (!canInteract()) return promptLegacy(question, opts);
|
|
324
|
+
return lineInput(question, { ...opts, back: false });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Line input that can step backwards: ↑ resolves BACK (esc is a silent
|
|
328
|
+
// alias), enter resolves the typed value (or the default).
|
|
329
|
+
function promptOrBack(question, opts = {}) {
|
|
330
|
+
if (!canInteract()) return promptLegacy(question, opts);
|
|
331
|
+
return lineInput(question, { ...opts, back: true });
|
|
332
|
+
}
|
|
333
|
+
|
|
229
334
|
async function confirm(question, defaultYes = false) {
|
|
230
|
-
const
|
|
231
|
-
const
|
|
335
|
+
const bracket = defaultYes ? `${DIM}[Y/n]${RESET}` : `${DIM}[y/N]${RESET}`;
|
|
336
|
+
const hint = `y yes · n no · enter ${defaultYes ? "yes" : "no"} · ctrl-c quit`;
|
|
337
|
+
const answer = canInteract()
|
|
338
|
+
? await lineInput(`${question} ${bracket}`, { hint, back: false })
|
|
339
|
+
: await promptLegacy(`${question} ${bracket}`);
|
|
232
340
|
if (!answer) return defaultYes;
|
|
233
341
|
return /^y(es)?$/i.test(answer);
|
|
234
342
|
}
|
|
@@ -289,7 +397,7 @@ function renderPicker({ question, filterText, lines, help, input }) {
|
|
|
289
397
|
for (const line of lines) out.push(line);
|
|
290
398
|
if (help) {
|
|
291
399
|
out.push("");
|
|
292
|
-
out.push(
|
|
400
|
+
out.push(footerBand(help));
|
|
293
401
|
}
|
|
294
402
|
const text = out.join("\n") + "\n";
|
|
295
403
|
process.stdout.write(text);
|
|
@@ -320,24 +428,37 @@ function filterOptions(options, filterText, getText) {
|
|
|
320
428
|
return result;
|
|
321
429
|
}
|
|
322
430
|
|
|
431
|
+
// ── back navigation ─────────────────────────────────────────────────
|
|
432
|
+
//
|
|
433
|
+
// Multi-step flows (the founder setup) let the user step backwards. The
|
|
434
|
+
// advertised keys are spatial — ↑ in text fields/forms, ← in choice lists
|
|
435
|
+
// (enter descends, ← ascends) — with esc as a silent alias everywhere.
|
|
436
|
+
// Esc NEVER quits: users' terminal reflex is that esc ejects them from the
|
|
437
|
+
// CLI, so no esc press may ever reach the shell. Pickers and prompts opt
|
|
438
|
+
// in via `escAction: "back"` — back keys then resolve to the BACK sentinel
|
|
439
|
+
// and the caller's step machine rewinds. Ctrl-C is the only hard abort.
|
|
440
|
+
const BACK = Symbol.for("egregore.ui.back");
|
|
441
|
+
|
|
323
442
|
// ── choose: interactive single-select picker ────────────────────────
|
|
324
443
|
//
|
|
325
444
|
// options: [{ label, description }]
|
|
326
|
-
// returns: the picked option (same object reference as in options[])
|
|
445
|
+
// returns: the picked option (same object reference as in options[]),
|
|
446
|
+
// or BACK when opts.escAction === "back" and the user pressed esc
|
|
327
447
|
// keys:
|
|
328
448
|
// ↑ ↓ navigate
|
|
329
449
|
// enter select
|
|
330
450
|
// type filter (case-insensitive substring match on label + desc)
|
|
331
451
|
// backspace pop filter
|
|
332
|
-
// esc
|
|
452
|
+
// ← / esc go back with opts.escAction: "back" (esc never quits;
|
|
453
|
+
// a clean-state esc with no back target is a no-op)
|
|
333
454
|
// ctrl-c abort process
|
|
334
455
|
|
|
335
|
-
async function choose(question, options, hint) {
|
|
336
|
-
if (!canInteract()) return legacyChoose(question, options, hint);
|
|
337
|
-
return interactiveChoose(question, options, hint);
|
|
456
|
+
async function choose(question, options, hint, opts = {}) {
|
|
457
|
+
if (!canInteract()) return legacyChoose(question, options, hint, opts);
|
|
458
|
+
return interactiveChoose(question, options, hint, opts);
|
|
338
459
|
}
|
|
339
460
|
|
|
340
|
-
async function interactiveChoose(question, options, hint) {
|
|
461
|
+
async function interactiveChoose(question, options, hint, opts = {}) {
|
|
341
462
|
return new Promise((resolve) => {
|
|
342
463
|
let filterText = "";
|
|
343
464
|
let visible = filterOptions(options, filterText, (o) => `${o.label} ${o.description || ""}`);
|
|
@@ -394,12 +515,13 @@ async function interactiveChoose(question, options, hint) {
|
|
|
394
515
|
for (let i = start; i < end; i++) lines.push(renderRow(visible[i], i));
|
|
395
516
|
if (end < total) lines.push(` ${DIM}↓ ${total - end} more · type to filter${RESET}`);
|
|
396
517
|
}
|
|
518
|
+
const backHint = opts.escAction === "back" ? ` · ${DIM}←${RESET} back` : "";
|
|
397
519
|
if (!first) clearLines(lastLineCount);
|
|
398
520
|
lastLineCount = renderPicker({
|
|
399
521
|
question,
|
|
400
522
|
filterText,
|
|
401
523
|
lines,
|
|
402
|
-
help: hint || `${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · type to filter · ${DIM}
|
|
524
|
+
help: hint || `${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · type to filter${backHint} · ${DIM}ctrl-c${RESET} quit`,
|
|
403
525
|
});
|
|
404
526
|
};
|
|
405
527
|
|
|
@@ -416,14 +538,38 @@ async function interactiveChoose(question, options, hint) {
|
|
|
416
538
|
resolve(value);
|
|
417
539
|
};
|
|
418
540
|
|
|
541
|
+
const goBack = () => {
|
|
542
|
+
cleanup();
|
|
543
|
+
clearLines(lastLineCount);
|
|
544
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
545
|
+
resolve(BACK);
|
|
546
|
+
};
|
|
547
|
+
|
|
419
548
|
const onKey = (_str, key) => {
|
|
420
549
|
if (!key) return;
|
|
421
|
-
if (
|
|
550
|
+
if (key.name === "escape" && filterText) {
|
|
551
|
+
// fzf convention: first esc clears an active filter; only a clean
|
|
552
|
+
// state goes back / cancels.
|
|
553
|
+
filterText = "";
|
|
554
|
+
visible = filterOptions(options, filterText, (o) => `${o.label} ${o.description || ""}`);
|
|
555
|
+
cursor = firstEnabled();
|
|
556
|
+
render();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
// ← is the advertised back key in choice lists (enter descends, ←
|
|
560
|
+
// ascends); esc is a silent alias. Esc NEVER quits — a clean-state esc
|
|
561
|
+
// with no back target does nothing, ctrl-c is the only abort.
|
|
562
|
+
if ((key.name === "escape" || key.name === "left") && opts.escAction === "back") {
|
|
563
|
+
goBack();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (key.ctrl && key.name === "c") {
|
|
422
567
|
cleanup();
|
|
423
568
|
unparkCursor();
|
|
424
569
|
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
425
570
|
process.exit(130);
|
|
426
571
|
}
|
|
572
|
+
if (key.name === "escape") return;
|
|
427
573
|
if (key.name === "return") {
|
|
428
574
|
if (visible.length > 0 && !visible[cursor].opt.disabled) finish(visible[cursor].opt);
|
|
429
575
|
return;
|
|
@@ -462,7 +608,7 @@ async function interactiveChoose(question, options, hint) {
|
|
|
462
608
|
});
|
|
463
609
|
}
|
|
464
610
|
|
|
465
|
-
async function legacyChoose(question, options, hint) {
|
|
611
|
+
async function legacyChoose(question, options, hint, opts = {}) {
|
|
466
612
|
process.stdout.write(`\n ${CYAN}◆${RESET} ${BOLD}${question}${RESET}\n\n`);
|
|
467
613
|
const labelWidth = Math.min(40, Math.max(...options.map((o) => o.label.length)));
|
|
468
614
|
for (let i = 0; i < options.length; i++) {
|
|
@@ -473,8 +619,10 @@ async function legacyChoose(question, options, hint) {
|
|
|
473
619
|
process.stdout.write(` ${DIM}${num})${RESET} ${BOLD}${paddedLabel}${RESET}${desc}\n`);
|
|
474
620
|
}
|
|
475
621
|
if (hint) process.stdout.write(`\n ${DIM}${hint}${RESET}\n`);
|
|
622
|
+
if (opts.escAction === "back") process.stdout.write(` ${DIM}type "back" to go back${RESET}\n`);
|
|
476
623
|
while (true) {
|
|
477
624
|
const answer = await prompt(`pick ${DIM}[1-${options.length}]${RESET}:`);
|
|
625
|
+
if (opts.escAction === "back" && /^(b|back)$/i.test(answer)) return BACK;
|
|
478
626
|
if (!answer) { warn(`Enter a number between 1 and ${options.length}.`); continue; }
|
|
479
627
|
const n = parseInt(answer, 10);
|
|
480
628
|
if (n >= 1 && n <= options.length) {
|
|
@@ -506,7 +654,7 @@ async function legacyChoose(question, options, hint) {
|
|
|
506
654
|
// ctrl-a toggle all (respecting filter)
|
|
507
655
|
// enter confirm
|
|
508
656
|
// type filter
|
|
509
|
-
// esc
|
|
657
|
+
// ← / esc go back with opts.escAction: "back" (esc never quits)
|
|
510
658
|
|
|
511
659
|
async function multiSelect(question, options, opts = {}) {
|
|
512
660
|
// Remote search can populate an empty pool, but only interactively —
|
|
@@ -523,7 +671,9 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
523
671
|
return new Promise((resolve) => {
|
|
524
672
|
let cursor = 0;
|
|
525
673
|
let filterText = "";
|
|
526
|
-
|
|
674
|
+
// opts.preselected re-arms a previous selection — used when the step
|
|
675
|
+
// machine navigates back to the picker so choices aren't lost.
|
|
676
|
+
const picked = new Set((opts.preselected || []).filter((n) => options.some((o) => o.name === n)));
|
|
527
677
|
// The pool grows as remote results merge in; seed order is preserved so
|
|
528
678
|
// boosted entries (e.g. the user's team repos) stay on top when idle.
|
|
529
679
|
const pool = [...options];
|
|
@@ -584,13 +734,14 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
584
734
|
if (searching && total > 0) lines.push(` ${DIM}searching…${RESET}`);
|
|
585
735
|
|
|
586
736
|
const searchHint = opts.remoteSearch ? "type to search" : "type to filter";
|
|
737
|
+
const escHint = opts.escAction === "back" ? ` · ${DIM}←${RESET} back` : "";
|
|
587
738
|
if (!first) clearLines(lastLineCount);
|
|
588
739
|
lastLineCount = renderPicker({
|
|
589
740
|
question,
|
|
590
741
|
filterText,
|
|
591
742
|
lines,
|
|
592
743
|
input: { label: opts.remoteSearch ? "search" : "filter" },
|
|
593
|
-
help: `${DIM}↑↓${RESET} nav · ${DIM}space${RESET} toggle · ${DIM}ctrl-a${RESET} all · ${DIM}enter${RESET} confirm · ${searchHint}`,
|
|
744
|
+
help: `${DIM}↑↓${RESET} nav · ${DIM}space${RESET} toggle · ${DIM}ctrl-a${RESET} all · ${DIM}enter${RESET} confirm · ${searchHint}${escHint}`,
|
|
594
745
|
});
|
|
595
746
|
};
|
|
596
747
|
|
|
@@ -661,12 +812,28 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
661
812
|
|
|
662
813
|
const onKey = (_str, key) => {
|
|
663
814
|
if (!key) return;
|
|
664
|
-
if (
|
|
815
|
+
if (key.name === "escape" && filterText) {
|
|
816
|
+
filterText = "";
|
|
817
|
+
refreshVisible();
|
|
818
|
+
cursor = 0;
|
|
819
|
+
searching = false;
|
|
820
|
+
render();
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if ((key.name === "escape" || key.name === "left") && opts.escAction === "back") {
|
|
824
|
+
cleanup();
|
|
825
|
+
clearLines(lastLineCount);
|
|
826
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
827
|
+
resolve(BACK);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (key.ctrl && key.name === "c") {
|
|
665
831
|
cleanup();
|
|
666
832
|
unparkCursor();
|
|
667
833
|
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
668
834
|
process.exit(130);
|
|
669
835
|
}
|
|
836
|
+
if (key.name === "escape") return; // esc never quits — ctrl-c aborts
|
|
670
837
|
if (key.name === "return") return finish();
|
|
671
838
|
if (key.name === "up" || (key.ctrl && key.name === "p")) {
|
|
672
839
|
cursor = (cursor - 1 + visible.length) % Math.max(1, visible.length);
|
|
@@ -749,6 +916,286 @@ async function legacyMultiSelect(question, options) {
|
|
|
749
916
|
return [...picked];
|
|
750
917
|
}
|
|
751
918
|
|
|
919
|
+
// ── manifestForm: live-manifest form ────────────────────────────────
|
|
920
|
+
//
|
|
921
|
+
// A vertical form rendered as one visible block — the "what gets created"
|
|
922
|
+
// plan IS the UI. One field is active at a time; derived fields (repo
|
|
923
|
+
// names from the Egregore name) fill in live as earlier answers are typed,
|
|
924
|
+
// so the plan builds itself in front of the user. "Back" is cursor
|
|
925
|
+
// movement, not a hidden step-rewind: ↑ moves to the previous field, and
|
|
926
|
+
// at the first field resolves BACK to the caller's step machine.
|
|
927
|
+
//
|
|
928
|
+
// fields: [{
|
|
929
|
+
// key value key in the resolved object
|
|
930
|
+
// label row label (left column)
|
|
931
|
+
// note dim annotation (right column, e.g. "config + skills")
|
|
932
|
+
// hint header guidance shown while this field is active
|
|
933
|
+
// (falls back to the form-level subtitle)
|
|
934
|
+
// initial previous answer (revisit) — kept only while it differs
|
|
935
|
+
// from the live derive, so edits keep cascading
|
|
936
|
+
// derive(vals) live default computed from earlier fields
|
|
937
|
+
// normalize(raw) canonicalize input (e.g. slugify) before validate
|
|
938
|
+
// validate(v, vals) return an error string to reject, null to accept
|
|
939
|
+
// optional enter on empty = skip (value "")
|
|
940
|
+
// tree render with ├─/└─ branch glyphs (the repo plan rows)
|
|
941
|
+
// }]
|
|
942
|
+
//
|
|
943
|
+
// keys: enter accept+next (submit on last) · ↑ previous field (BACK at the
|
|
944
|
+
// first) · ↓ next · type/backspace edit · esc = silent alias of ↑ (esc
|
|
945
|
+
// never quits) · ctrl-c abort
|
|
946
|
+
//
|
|
947
|
+
// resolves { values } or BACK.
|
|
948
|
+
async function manifestForm({ title, subtitle, fields }) {
|
|
949
|
+
if (!canInteract()) {
|
|
950
|
+
// Non-interactive fallback: plain sequential prompts, same validation.
|
|
951
|
+
const values = {};
|
|
952
|
+
for (const f of fields) {
|
|
953
|
+
const def = (f.initial != null && f.initial !== "") ? f.initial : (f.derive ? f.derive(values) : "");
|
|
954
|
+
for (;;) {
|
|
955
|
+
const raw = await prompt(f.label, { default: def || undefined });
|
|
956
|
+
const val = f.normalize ? f.normalize(raw) : String(raw || "").trim();
|
|
957
|
+
if (!val && f.optional) { values[f.key] = ""; break; }
|
|
958
|
+
const err = f.validate ? f.validate(val, values) : null;
|
|
959
|
+
if (err) { warn(err); continue; }
|
|
960
|
+
values[f.key] = val;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return { values };
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return new Promise((resolve) => {
|
|
968
|
+
// entered[key] is set only when the user TYPED a value — accepting a
|
|
969
|
+
// derived ghost with enter leaves the field derived, so going back up
|
|
970
|
+
// and changing the name re-derives everything the user didn't override.
|
|
971
|
+
const entered = {};
|
|
972
|
+
let active = 0;
|
|
973
|
+
let buf = null; // null = untouched → the ghost (derive/initial) shows
|
|
974
|
+
let error = null;
|
|
975
|
+
let confirmed = -1; // highest field index confirmed with enter
|
|
976
|
+
let lastRows = 0;
|
|
977
|
+
|
|
978
|
+
for (let i = 0; i < fields.length; i++) {
|
|
979
|
+
const f = fields[i];
|
|
980
|
+
if (f.initial != null && f.initial !== "") entered[f.key] = f.initial;
|
|
981
|
+
}
|
|
982
|
+
// Revisit case: initial values that match what derivation would produce
|
|
983
|
+
// are dropped back to derived so the cascade stays live.
|
|
984
|
+
{
|
|
985
|
+
const vals = {};
|
|
986
|
+
for (const f of fields) {
|
|
987
|
+
const derived = f.derive ? f.derive(vals) : "";
|
|
988
|
+
if (entered[f.key] != null && entered[f.key] === derived) delete entered[f.key];
|
|
989
|
+
vals[f.key] = entered[f.key] != null ? entered[f.key] : derived;
|
|
990
|
+
}
|
|
991
|
+
if (Object.keys(entered).length > 0) confirmed = fields.length - 1;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Resolve every field's current value in order; the active field's
|
|
995
|
+
// in-progress buffer feeds later derives so typing the name updates the
|
|
996
|
+
// repo rows on every keystroke.
|
|
997
|
+
const computeVals = () => {
|
|
998
|
+
const vals = {};
|
|
999
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1000
|
+
const f = fields[i];
|
|
1001
|
+
let v;
|
|
1002
|
+
if (i === active && buf != null) v = f.normalize ? f.normalize(buf) : buf.trim();
|
|
1003
|
+
else if (entered[f.key] != null) v = entered[f.key];
|
|
1004
|
+
else v = f.derive ? f.derive(vals) : "";
|
|
1005
|
+
vals[f.key] = v || "";
|
|
1006
|
+
}
|
|
1007
|
+
return vals;
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
const ghostFor = (i, vals) => {
|
|
1011
|
+
const f = fields[i];
|
|
1012
|
+
if (entered[f.key] != null) return entered[f.key];
|
|
1013
|
+
return f.derive ? f.derive(vals) : "";
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const labelW = Math.max(...fields.map((f) => f.label.length + (f.tree ? 3 : 0)));
|
|
1017
|
+
const lastTreeIdx = fields.reduce((acc, f, i) => (f.tree ? i : acc), -1);
|
|
1018
|
+
|
|
1019
|
+
const rowLine = (i, vals) => {
|
|
1020
|
+
const f = fields[i];
|
|
1021
|
+
const glyph = f.tree ? (i === lastTreeIdx ? "└─ " : "├─ ") : "";
|
|
1022
|
+
const labelText = pad(`${glyph}${f.label}`, labelW);
|
|
1023
|
+
const isActive = i === active;
|
|
1024
|
+
const isDone = i <= confirmed && !isActive;
|
|
1025
|
+
const mark = isActive ? `${CYAN}❯${RESET}` : (isDone ? `${GREEN}✓${RESET}` : " ");
|
|
1026
|
+
const note = f.note ? ` ${DIM}${f.note}${RESET}` : "";
|
|
1027
|
+
let value;
|
|
1028
|
+
if (isActive) {
|
|
1029
|
+
// No drawn caret — the REAL terminal cursor is parked here after
|
|
1030
|
+
// render, so it blinks where keystrokes land.
|
|
1031
|
+
if (buf != null) {
|
|
1032
|
+
value = buf;
|
|
1033
|
+
} else {
|
|
1034
|
+
const ghost = ghostFor(i, vals);
|
|
1035
|
+
value = ghost
|
|
1036
|
+
? `${DIM}${ghost}${RESET}`
|
|
1037
|
+
: (f.optional ? `${DIM}(enter to skip)${RESET}` : "");
|
|
1038
|
+
}
|
|
1039
|
+
} else {
|
|
1040
|
+
const v = vals[f.key];
|
|
1041
|
+
if (!v) value = `${DIM}${f.optional && isDone ? "— skipped" : "·"}${RESET}`;
|
|
1042
|
+
else if (entered[f.key] != null || isDone) value = `${BOLD}${v}${RESET}`;
|
|
1043
|
+
else value = `${DIM}${v}${RESET}`;
|
|
1044
|
+
}
|
|
1045
|
+
const valueW = 26;
|
|
1046
|
+
return ` ${mark} ${DIM}${labelText}${RESET} ${pad(value, valueW)}${note}`;
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
const hintLine = () => {
|
|
1050
|
+
const backTarget = active === 0 ? "back" : "previous";
|
|
1051
|
+
return footerBand(`enter ${active === fields.length - 1 ? "finish" : "next"} · ↑ ${backTarget} · type to edit · ctrl-c quit`);
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const render = (first = false) => {
|
|
1055
|
+
const vals = computeVals();
|
|
1056
|
+
const lines = [];
|
|
1057
|
+
// Header guidance is contextual: each field can carry its own hint
|
|
1058
|
+
// ("type your preferred Egregore name or hit enter…"), so the line
|
|
1059
|
+
// always says what to do RIGHT NOW instead of describing the form.
|
|
1060
|
+
const guidance = fields[active].hint || subtitle;
|
|
1061
|
+
lines.push(` ${CYAN}◆${RESET} ${BOLD}${title}${RESET}${guidance ? ` ${DIM}${guidance}${RESET}` : ""}`);
|
|
1062
|
+
lines.push("");
|
|
1063
|
+
for (let i = 0; i < fields.length; i++) lines.push(rowLine(i, vals));
|
|
1064
|
+
lines.push("");
|
|
1065
|
+
lines.push(error ? footerBand(`! ${error}`, 43) : hintLine());
|
|
1066
|
+
if (!first) clearLines(lastRows);
|
|
1067
|
+
const width = process.stdout.columns || 80;
|
|
1068
|
+
const text = lines.join("\n") + "\n";
|
|
1069
|
+
process.stdout.write(CURSOR_HIDE + text);
|
|
1070
|
+
lastRows = lines.reduce((sum, line) => sum + visualRows(line, width), 0);
|
|
1071
|
+
// Park the REAL terminal cursor at the active field's edit point so it
|
|
1072
|
+
// blinks there — the strongest "type here" affordance a terminal has.
|
|
1073
|
+
// Row math is visual-row aware (the header hint may wrap); the active
|
|
1074
|
+
// field row itself is short and assumed unwrapped.
|
|
1075
|
+
const activeLine = 2 + active;
|
|
1076
|
+
let rowsUp = 1; // the trailing newline after the block
|
|
1077
|
+
for (let i = activeLine + 1; i < lines.length; i++) rowsUp += visualRows(lines[i], width);
|
|
1078
|
+
const col = labelW + 9 + (buf != null ? visibleLen(buf) : 0);
|
|
1079
|
+
process.stdout.write(`\x1b[${rowsUp}A\x1b[${col}G${CURSOR_SHOW}`);
|
|
1080
|
+
_parkedRows = rowsUp; // clearLines() unparks before rewinding
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const cleanup = () => {
|
|
1084
|
+
process.stdin.setRawMode(false);
|
|
1085
|
+
process.stdin.pause();
|
|
1086
|
+
process.stdin.removeListener("keypress", onKey);
|
|
1087
|
+
process.stdout.write(CURSOR_SHOW);
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// Transcript receipt: the final plan stays in scrollback as a static
|
|
1091
|
+
// tree, so the record of what was named survives the redraw region.
|
|
1092
|
+
const receipt = (vals) => {
|
|
1093
|
+
const lines = [` ${CYAN}◆${RESET} ${BOLD}${title}${RESET}`];
|
|
1094
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1095
|
+
const f = fields[i];
|
|
1096
|
+
if (!vals[f.key]) continue;
|
|
1097
|
+
const glyph = f.tree ? (i === lastTreeIdx ? "└─ " : "├─ ") : "";
|
|
1098
|
+
const note = f.note ? ` ${DIM}${f.note}${RESET}` : "";
|
|
1099
|
+
lines.push(` ${DIM}${pad(`${glyph}${f.label}`, labelW)}${RESET} ${pad(`${BOLD}${vals[f.key]}${RESET}`, 26)}${note}`);
|
|
1100
|
+
}
|
|
1101
|
+
return lines.join("\n") + "\n";
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const commitActive = () => {
|
|
1105
|
+
const f = fields[active];
|
|
1106
|
+
const vals = computeVals();
|
|
1107
|
+
const typed = buf != null && buf.trim() !== "";
|
|
1108
|
+
const value = vals[f.key];
|
|
1109
|
+
if (!value && !f.optional) {
|
|
1110
|
+
error = `${f.label} can't be empty.`;
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
if (value && f.validate) {
|
|
1114
|
+
// Validate against the other fields' values, not our own in-progress one.
|
|
1115
|
+
const others = { ...vals };
|
|
1116
|
+
delete others[f.key];
|
|
1117
|
+
const err = f.validate(value, others);
|
|
1118
|
+
if (err) { error = err; return false; }
|
|
1119
|
+
}
|
|
1120
|
+
if (typed) entered[f.key] = value;
|
|
1121
|
+
else if (buf != null && buf.trim() === "" && f.optional) entered[f.key] = "";
|
|
1122
|
+
error = null;
|
|
1123
|
+
confirmed = Math.max(confirmed, active);
|
|
1124
|
+
return true;
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const onKey = (_str, key) => {
|
|
1128
|
+
if (!key) return;
|
|
1129
|
+
if (key.ctrl && key.name === "c") {
|
|
1130
|
+
cleanup();
|
|
1131
|
+
clearLines(lastRows);
|
|
1132
|
+
process.stdout.write(` ${DIM}(cancelled)${RESET}\n`);
|
|
1133
|
+
process.exit(130);
|
|
1134
|
+
}
|
|
1135
|
+
// ↑ = previous field; at the first field it leaves the form (BACK to
|
|
1136
|
+
// the previous wizard step). Esc is a silent alias — it never quits.
|
|
1137
|
+
if (key.name === "up" || key.name === "escape") {
|
|
1138
|
+
error = null;
|
|
1139
|
+
if (active === 0) {
|
|
1140
|
+
cleanup();
|
|
1141
|
+
clearLines(lastRows);
|
|
1142
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${title}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
1143
|
+
resolve(BACK);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
buf = null;
|
|
1147
|
+
active -= 1;
|
|
1148
|
+
render();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (key.name === "return" || key.name === "down") {
|
|
1152
|
+
if (key.name === "down" && active === fields.length - 1) return;
|
|
1153
|
+
if (!commitActive()) { render(); return; }
|
|
1154
|
+
if (active === fields.length - 1) {
|
|
1155
|
+
const vals = computeVals();
|
|
1156
|
+
cleanup();
|
|
1157
|
+
clearLines(lastRows);
|
|
1158
|
+
process.stdout.write(receipt(vals));
|
|
1159
|
+
resolve({ values: vals });
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
buf = null;
|
|
1163
|
+
active += 1;
|
|
1164
|
+
render();
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (key.name === "backspace") {
|
|
1168
|
+
// Backspace on an untouched field materializes the ghost for editing.
|
|
1169
|
+
if (buf == null) {
|
|
1170
|
+
const vals = computeVals();
|
|
1171
|
+
buf = ghostFor(active, vals).slice(0, -1);
|
|
1172
|
+
} else {
|
|
1173
|
+
buf = buf.slice(0, -1);
|
|
1174
|
+
}
|
|
1175
|
+
// Deleting everything un-touches the field — the derived suggestion
|
|
1176
|
+
// returns (and downstream derives recover) instead of leaving the
|
|
1177
|
+
// row stuck empty.
|
|
1178
|
+
if (buf === "") buf = null;
|
|
1179
|
+
error = null;
|
|
1180
|
+
render();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (_str && _str.length === 1 && !key.ctrl && !key.meta && _str.charCodeAt(0) >= 32) {
|
|
1184
|
+
buf = (buf == null ? "" : buf) + _str;
|
|
1185
|
+
error = null;
|
|
1186
|
+
render();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1191
|
+
process.stdin.setRawMode(true);
|
|
1192
|
+
process.stdin.resume();
|
|
1193
|
+
process.stdin.on("keypress", onKey);
|
|
1194
|
+
process.stdout.write(CURSOR_HIDE);
|
|
1195
|
+
render(true);
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
752
1199
|
// ── commandBox ──────────────────────────────────────────────────────
|
|
753
1200
|
//
|
|
754
1201
|
// Preserved for back-compat; card() is the newer more-flexible version.
|
|
@@ -1009,7 +1456,7 @@ module.exports = {
|
|
|
1009
1456
|
banner, bannerCompact, section, note, card,
|
|
1010
1457
|
info, success, warn, error, step,
|
|
1011
1458
|
dim, bold, cyan, green, yellow, red,
|
|
1012
|
-
spinner, prompt, choose, multiSelect, confirm, canInteract,
|
|
1459
|
+
spinner, prompt, promptOrBack, manifestForm, choose, multiSelect, confirm, canInteract, BACK,
|
|
1013
1460
|
commandBox,
|
|
1014
1461
|
canShowScreens, welcomeScreen, summaryScreen,
|
|
1015
1462
|
_renderWelcomeBody, _readPkgVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code or Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build:runtime": "node scripts/build-runtime-bundle.mjs",
|
|
14
|
-
"test": "node scripts/build-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js",
|
|
14
|
+
"test": "node scripts/build-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js",
|
|
15
15
|
"test:e2e": "node --test test/e2e-local.test.js",
|
|
16
16
|
"test:all": "node scripts/build-runtime-bundle.mjs --check && node --test test/"
|
|
17
17
|
},
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
{
|
|
12
12
|
"section": "Identity & Upstream",
|
|
13
13
|
"action": "keep",
|
|
14
|
-
"sourceHash": "
|
|
14
|
+
"sourceHash": "11d07e3fcb58",
|
|
15
15
|
"note": "Kept; /update, /contribute, /save tokens rendered as $skill tokens."
|
|
16
16
|
},
|
|
17
17
|
{
|
|
@@ -62,6 +62,12 @@
|
|
|
62
62
|
"sourceHash": "d75dc9f6d88c",
|
|
63
63
|
"note": "Kept verbatim."
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
"section": "Loom Routing",
|
|
67
|
+
"action": "replace",
|
|
68
|
+
"sourceHash": "b6a6ca2bc2b7",
|
|
69
|
+
"note": "Loom delegation rides the Claude Code Agent tool; Codex has no subagent delegation — commands run inline. Preambles and bin/loom.sh calls marked skippable."
|
|
70
|
+
},
|
|
65
71
|
{
|
|
66
72
|
"section": "Git Workflow",
|
|
67
73
|
"action": "keep",
|
package/runtime/codex/AGENTS.md
CHANGED
|
@@ -63,7 +63,7 @@ This block is the Codex-native Egregore behavioral spec, rendered from CLAUDE.md
|
|
|
63
63
|
|
|
64
64
|
## Identity & Upstream
|
|
65
65
|
|
|
66
|
-
This is an Egregore instance — a downstream fork of the upstream framework at `egregore-labs/egregore`. The framework (`bin/`, `.claude/skills/`, `.claude/hooks/`, `.claude/context/`, `CLAUDE.md`, `skills/`) is synced from upstream on every session start and via `$update`. It is not authored in this repo.
|
|
66
|
+
This is an Egregore instance — a downstream fork of the upstream framework at `egregore-labs/egregore`. The framework (`bin/`, `.claude/skills/`, `.claude/hooks/`, `.claude/context/`, `.claude/agents/`, `loom/`, `CLAUDE.md`, `skills/`) is synced from upstream on every session start and via `$update`. It is not authored in this repo.
|
|
67
67
|
|
|
68
68
|
**Where changes belong:**
|
|
69
69
|
- **Framework is outdated or a skill is missing:** run `$update` first. It pulls the latest from upstream.
|
|
@@ -198,6 +198,12 @@ Always use HTTPS for git operations — `github-auth.sh` handles credential stor
|
|
|
198
198
|
|
|
199
199
|
---
|
|
200
200
|
|
|
201
|
+
## Loom Routing
|
|
202
|
+
|
|
203
|
+
Loom routes commands across model tiers on the Claude Code runtime (`loom/routes.json` + `bin/loom.sh` + a model-pinned executor subagent). Codex has no subagent delegation — every command runs inline in the current session. Ignore "Loom routing" preambles if you encounter them in a skill spec, and skip `bin/loom.sh` calls; `loom/` and `.claude/agents/` are framework files synced for the Claude runtime.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
201
207
|
## Git Workflow
|
|
202
208
|
|
|
203
209
|
`develop` branch model with deferred, topic-based branching. Users never interact with git directly.
|