create-egregore 0.18.18 → 0.19.2
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 +155 -96
- package/lib/ui.js +381 -33
- 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/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,18 +238,36 @@ function prompt(question, { default: defaultValue } = {}) {
|
|
|
226
238
|
});
|
|
227
239
|
}
|
|
228
240
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
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 } = {}) {
|
|
235
249
|
return new Promise((resolve) => {
|
|
236
250
|
let buf = "";
|
|
237
251
|
const label = defaultValue
|
|
238
|
-
? ` ${CYAN}?${RESET} ${question} ${DIM}[${defaultValue}]${RESET}
|
|
239
|
-
: ` ${CYAN}?${RESET} ${question}
|
|
240
|
-
const
|
|
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
|
+
};
|
|
241
271
|
const cleanup = () => {
|
|
242
272
|
process.stdin.setRawMode(false);
|
|
243
273
|
process.stdin.pause();
|
|
@@ -247,17 +277,21 @@ function promptOrBack(question, { default: defaultValue } = {}) {
|
|
|
247
277
|
if (!key) return;
|
|
248
278
|
if (key.ctrl && key.name === "c") {
|
|
249
279
|
cleanup();
|
|
280
|
+
clearHint();
|
|
250
281
|
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
251
282
|
process.exit(130);
|
|
252
283
|
}
|
|
253
|
-
if (key.name === "escape") {
|
|
284
|
+
if (key.name === "up" || key.name === "escape") {
|
|
285
|
+
if (!back) return; // nothing to go back to — and esc never quits
|
|
254
286
|
cleanup();
|
|
287
|
+
clearHint();
|
|
255
288
|
process.stdout.write(`\r\x1b[2K${label}${DIM}← back${RESET}\n`);
|
|
256
289
|
resolve(BACK);
|
|
257
290
|
return;
|
|
258
291
|
}
|
|
259
292
|
if (key.name === "return") {
|
|
260
293
|
cleanup();
|
|
294
|
+
clearHint();
|
|
261
295
|
process.stdout.write("\n");
|
|
262
296
|
resolve(buf.trim() || (defaultValue ?? ""));
|
|
263
297
|
return;
|
|
@@ -267,9 +301,12 @@ function promptOrBack(question, { default: defaultValue } = {}) {
|
|
|
267
301
|
render();
|
|
268
302
|
return;
|
|
269
303
|
}
|
|
270
|
-
if (_str &&
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|
|
273
310
|
}
|
|
274
311
|
};
|
|
275
312
|
readline.emitKeypressEvents(process.stdin);
|
|
@@ -280,9 +317,26 @@ function promptOrBack(question, { default: defaultValue } = {}) {
|
|
|
280
317
|
});
|
|
281
318
|
}
|
|
282
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
|
+
|
|
283
334
|
async function confirm(question, defaultYes = false) {
|
|
284
|
-
const
|
|
285
|
-
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}`);
|
|
286
340
|
if (!answer) return defaultYes;
|
|
287
341
|
return /^y(es)?$/i.test(answer);
|
|
288
342
|
}
|
|
@@ -343,7 +397,7 @@ function renderPicker({ question, filterText, lines, help, input }) {
|
|
|
343
397
|
for (const line of lines) out.push(line);
|
|
344
398
|
if (help) {
|
|
345
399
|
out.push("");
|
|
346
|
-
out.push(
|
|
400
|
+
out.push(footerBand(help));
|
|
347
401
|
}
|
|
348
402
|
const text = out.join("\n") + "\n";
|
|
349
403
|
process.stdout.write(text);
|
|
@@ -376,10 +430,13 @@ function filterOptions(options, filterText, getText) {
|
|
|
376
430
|
|
|
377
431
|
// ── back navigation ─────────────────────────────────────────────────
|
|
378
432
|
//
|
|
379
|
-
// Multi-step flows (the founder setup) let the user step backwards
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
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.
|
|
383
440
|
const BACK = Symbol.for("egregore.ui.back");
|
|
384
441
|
|
|
385
442
|
// ── choose: interactive single-select picker ────────────────────────
|
|
@@ -392,7 +449,8 @@ const BACK = Symbol.for("egregore.ui.back");
|
|
|
392
449
|
// enter select
|
|
393
450
|
// type filter (case-insensitive substring match on label + desc)
|
|
394
451
|
// backspace pop filter
|
|
395
|
-
//
|
|
452
|
+
// ← / esc go back with opts.escAction: "back" (esc never quits;
|
|
453
|
+
// a clean-state esc with no back target is a no-op)
|
|
396
454
|
// ctrl-c abort process
|
|
397
455
|
|
|
398
456
|
async function choose(question, options, hint, opts = {}) {
|
|
@@ -457,13 +515,13 @@ async function interactiveChoose(question, options, hint, opts = {}) {
|
|
|
457
515
|
for (let i = start; i < end; i++) lines.push(renderRow(visible[i], i));
|
|
458
516
|
if (end < total) lines.push(` ${DIM}↓ ${total - end} more · type to filter${RESET}`);
|
|
459
517
|
}
|
|
460
|
-
const
|
|
518
|
+
const backHint = opts.escAction === "back" ? ` · ${DIM}←${RESET} back` : "";
|
|
461
519
|
if (!first) clearLines(lastLineCount);
|
|
462
520
|
lastLineCount = renderPicker({
|
|
463
521
|
question,
|
|
464
522
|
filterText,
|
|
465
523
|
lines,
|
|
466
|
-
help: hint || `${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · type to filter · ${
|
|
524
|
+
help: hint || `${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · type to filter${backHint} · ${DIM}ctrl-c${RESET} quit`,
|
|
467
525
|
});
|
|
468
526
|
};
|
|
469
527
|
|
|
@@ -498,16 +556,20 @@ async function interactiveChoose(question, options, hint, opts = {}) {
|
|
|
498
556
|
render();
|
|
499
557
|
return;
|
|
500
558
|
}
|
|
501
|
-
|
|
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") {
|
|
502
563
|
goBack();
|
|
503
564
|
return;
|
|
504
565
|
}
|
|
505
|
-
if (
|
|
566
|
+
if (key.ctrl && key.name === "c") {
|
|
506
567
|
cleanup();
|
|
507
568
|
unparkCursor();
|
|
508
569
|
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
509
570
|
process.exit(130);
|
|
510
571
|
}
|
|
572
|
+
if (key.name === "escape") return;
|
|
511
573
|
if (key.name === "return") {
|
|
512
574
|
if (visible.length > 0 && !visible[cursor].opt.disabled) finish(visible[cursor].opt);
|
|
513
575
|
return;
|
|
@@ -592,7 +654,7 @@ async function legacyChoose(question, options, hint, opts = {}) {
|
|
|
592
654
|
// ctrl-a toggle all (respecting filter)
|
|
593
655
|
// enter confirm
|
|
594
656
|
// type filter
|
|
595
|
-
// esc
|
|
657
|
+
// ← / esc go back with opts.escAction: "back" (esc never quits)
|
|
596
658
|
|
|
597
659
|
async function multiSelect(question, options, opts = {}) {
|
|
598
660
|
// Remote search can populate an empty pool, but only interactively —
|
|
@@ -667,19 +729,24 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
667
729
|
const shown = names.length > maxW ? names.slice(0, maxW - 1) + "…" : names;
|
|
668
730
|
lines.push(` ${GREEN}✓${RESET} ${DIM}selected (${picked.size}):${RESET} ${shown}`);
|
|
669
731
|
} else {
|
|
670
|
-
|
|
732
|
+
// Nothing picked is a valid state — this picker never enforces a
|
|
733
|
+
// minimum. Say so, so an empty Enter doesn't read as "stuck".
|
|
734
|
+
lines.push(` ${DIM}0 selected — none required${RESET}`);
|
|
671
735
|
}
|
|
672
736
|
if (searching && total > 0) lines.push(` ${DIM}searching…${RESET}`);
|
|
673
737
|
|
|
674
738
|
const searchHint = opts.remoteSearch ? "type to search" : "type to filter";
|
|
675
|
-
const escHint = opts.escAction === "back" ? ` · ${DIM}
|
|
739
|
+
const escHint = opts.escAction === "back" ? ` · ${DIM}←${RESET} back` : "";
|
|
740
|
+
// Enter always confirms the current selection, empty included — label it
|
|
741
|
+
// "skip" while nothing is picked so the exit is discoverable.
|
|
742
|
+
const enterHint = picked.size === 0 ? "skip" : "confirm";
|
|
676
743
|
if (!first) clearLines(lastLineCount);
|
|
677
744
|
lastLineCount = renderPicker({
|
|
678
745
|
question,
|
|
679
746
|
filterText,
|
|
680
747
|
lines,
|
|
681
748
|
input: { label: opts.remoteSearch ? "search" : "filter" },
|
|
682
|
-
help: `${DIM}↑↓${RESET} nav · ${DIM}space${RESET} toggle · ${DIM}ctrl-a${RESET} all · ${DIM}enter${RESET}
|
|
749
|
+
help: `${DIM}↑↓${RESET} nav · ${DIM}space${RESET} toggle · ${DIM}ctrl-a${RESET} all · ${DIM}enter${RESET} ${enterHint} · ${searchHint}${escHint}`,
|
|
683
750
|
});
|
|
684
751
|
};
|
|
685
752
|
|
|
@@ -758,19 +825,20 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
758
825
|
render();
|
|
759
826
|
return;
|
|
760
827
|
}
|
|
761
|
-
if (key.name === "escape" && opts.escAction === "back") {
|
|
828
|
+
if ((key.name === "escape" || key.name === "left") && opts.escAction === "back") {
|
|
762
829
|
cleanup();
|
|
763
830
|
clearLines(lastLineCount);
|
|
764
831
|
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
765
832
|
resolve(BACK);
|
|
766
833
|
return;
|
|
767
834
|
}
|
|
768
|
-
if (
|
|
835
|
+
if (key.ctrl && key.name === "c") {
|
|
769
836
|
cleanup();
|
|
770
837
|
unparkCursor();
|
|
771
838
|
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
772
839
|
process.exit(130);
|
|
773
840
|
}
|
|
841
|
+
if (key.name === "escape") return; // esc never quits — ctrl-c aborts
|
|
774
842
|
if (key.name === "return") return finish();
|
|
775
843
|
if (key.name === "up" || (key.ctrl && key.name === "p")) {
|
|
776
844
|
cursor = (cursor - 1 + visible.length) % Math.max(1, visible.length);
|
|
@@ -853,6 +921,286 @@ async function legacyMultiSelect(question, options) {
|
|
|
853
921
|
return [...picked];
|
|
854
922
|
}
|
|
855
923
|
|
|
924
|
+
// ── manifestForm: live-manifest form ────────────────────────────────
|
|
925
|
+
//
|
|
926
|
+
// A vertical form rendered as one visible block — the "what gets created"
|
|
927
|
+
// plan IS the UI. One field is active at a time; derived fields (repo
|
|
928
|
+
// names from the Egregore name) fill in live as earlier answers are typed,
|
|
929
|
+
// so the plan builds itself in front of the user. "Back" is cursor
|
|
930
|
+
// movement, not a hidden step-rewind: ↑ moves to the previous field, and
|
|
931
|
+
// at the first field resolves BACK to the caller's step machine.
|
|
932
|
+
//
|
|
933
|
+
// fields: [{
|
|
934
|
+
// key value key in the resolved object
|
|
935
|
+
// label row label (left column)
|
|
936
|
+
// note dim annotation (right column, e.g. "config + skills")
|
|
937
|
+
// hint header guidance shown while this field is active
|
|
938
|
+
// (falls back to the form-level subtitle)
|
|
939
|
+
// initial previous answer (revisit) — kept only while it differs
|
|
940
|
+
// from the live derive, so edits keep cascading
|
|
941
|
+
// derive(vals) live default computed from earlier fields
|
|
942
|
+
// normalize(raw) canonicalize input (e.g. slugify) before validate
|
|
943
|
+
// validate(v, vals) return an error string to reject, null to accept
|
|
944
|
+
// optional enter on empty = skip (value "")
|
|
945
|
+
// tree render with ├─/└─ branch glyphs (the repo plan rows)
|
|
946
|
+
// }]
|
|
947
|
+
//
|
|
948
|
+
// keys: enter accept+next (submit on last) · ↑ previous field (BACK at the
|
|
949
|
+
// first) · ↓ next · type/backspace edit · esc = silent alias of ↑ (esc
|
|
950
|
+
// never quits) · ctrl-c abort
|
|
951
|
+
//
|
|
952
|
+
// resolves { values } or BACK.
|
|
953
|
+
async function manifestForm({ title, subtitle, fields }) {
|
|
954
|
+
if (!canInteract()) {
|
|
955
|
+
// Non-interactive fallback: plain sequential prompts, same validation.
|
|
956
|
+
const values = {};
|
|
957
|
+
for (const f of fields) {
|
|
958
|
+
const def = (f.initial != null && f.initial !== "") ? f.initial : (f.derive ? f.derive(values) : "");
|
|
959
|
+
for (;;) {
|
|
960
|
+
const raw = await prompt(f.label, { default: def || undefined });
|
|
961
|
+
const val = f.normalize ? f.normalize(raw) : String(raw || "").trim();
|
|
962
|
+
if (!val && f.optional) { values[f.key] = ""; break; }
|
|
963
|
+
const err = f.validate ? f.validate(val, values) : null;
|
|
964
|
+
if (err) { warn(err); continue; }
|
|
965
|
+
values[f.key] = val;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return { values };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return new Promise((resolve) => {
|
|
973
|
+
// entered[key] is set only when the user TYPED a value — accepting a
|
|
974
|
+
// derived ghost with enter leaves the field derived, so going back up
|
|
975
|
+
// and changing the name re-derives everything the user didn't override.
|
|
976
|
+
const entered = {};
|
|
977
|
+
let active = 0;
|
|
978
|
+
let buf = null; // null = untouched → the ghost (derive/initial) shows
|
|
979
|
+
let error = null;
|
|
980
|
+
let confirmed = -1; // highest field index confirmed with enter
|
|
981
|
+
let lastRows = 0;
|
|
982
|
+
|
|
983
|
+
for (let i = 0; i < fields.length; i++) {
|
|
984
|
+
const f = fields[i];
|
|
985
|
+
if (f.initial != null && f.initial !== "") entered[f.key] = f.initial;
|
|
986
|
+
}
|
|
987
|
+
// Revisit case: initial values that match what derivation would produce
|
|
988
|
+
// are dropped back to derived so the cascade stays live.
|
|
989
|
+
{
|
|
990
|
+
const vals = {};
|
|
991
|
+
for (const f of fields) {
|
|
992
|
+
const derived = f.derive ? f.derive(vals) : "";
|
|
993
|
+
if (entered[f.key] != null && entered[f.key] === derived) delete entered[f.key];
|
|
994
|
+
vals[f.key] = entered[f.key] != null ? entered[f.key] : derived;
|
|
995
|
+
}
|
|
996
|
+
if (Object.keys(entered).length > 0) confirmed = fields.length - 1;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Resolve every field's current value in order; the active field's
|
|
1000
|
+
// in-progress buffer feeds later derives so typing the name updates the
|
|
1001
|
+
// repo rows on every keystroke.
|
|
1002
|
+
const computeVals = () => {
|
|
1003
|
+
const vals = {};
|
|
1004
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1005
|
+
const f = fields[i];
|
|
1006
|
+
let v;
|
|
1007
|
+
if (i === active && buf != null) v = f.normalize ? f.normalize(buf) : buf.trim();
|
|
1008
|
+
else if (entered[f.key] != null) v = entered[f.key];
|
|
1009
|
+
else v = f.derive ? f.derive(vals) : "";
|
|
1010
|
+
vals[f.key] = v || "";
|
|
1011
|
+
}
|
|
1012
|
+
return vals;
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const ghostFor = (i, vals) => {
|
|
1016
|
+
const f = fields[i];
|
|
1017
|
+
if (entered[f.key] != null) return entered[f.key];
|
|
1018
|
+
return f.derive ? f.derive(vals) : "";
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
const labelW = Math.max(...fields.map((f) => f.label.length + (f.tree ? 3 : 0)));
|
|
1022
|
+
const lastTreeIdx = fields.reduce((acc, f, i) => (f.tree ? i : acc), -1);
|
|
1023
|
+
|
|
1024
|
+
const rowLine = (i, vals) => {
|
|
1025
|
+
const f = fields[i];
|
|
1026
|
+
const glyph = f.tree ? (i === lastTreeIdx ? "└─ " : "├─ ") : "";
|
|
1027
|
+
const labelText = pad(`${glyph}${f.label}`, labelW);
|
|
1028
|
+
const isActive = i === active;
|
|
1029
|
+
const isDone = i <= confirmed && !isActive;
|
|
1030
|
+
const mark = isActive ? `${CYAN}❯${RESET}` : (isDone ? `${GREEN}✓${RESET}` : " ");
|
|
1031
|
+
const note = f.note ? ` ${DIM}${f.note}${RESET}` : "";
|
|
1032
|
+
let value;
|
|
1033
|
+
if (isActive) {
|
|
1034
|
+
// No drawn caret — the REAL terminal cursor is parked here after
|
|
1035
|
+
// render, so it blinks where keystrokes land.
|
|
1036
|
+
if (buf != null) {
|
|
1037
|
+
value = buf;
|
|
1038
|
+
} else {
|
|
1039
|
+
const ghost = ghostFor(i, vals);
|
|
1040
|
+
value = ghost
|
|
1041
|
+
? `${DIM}${ghost}${RESET}`
|
|
1042
|
+
: (f.optional ? `${DIM}(enter to skip)${RESET}` : "");
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
const v = vals[f.key];
|
|
1046
|
+
if (!v) value = `${DIM}${f.optional && isDone ? "— skipped" : "·"}${RESET}`;
|
|
1047
|
+
else if (entered[f.key] != null || isDone) value = `${BOLD}${v}${RESET}`;
|
|
1048
|
+
else value = `${DIM}${v}${RESET}`;
|
|
1049
|
+
}
|
|
1050
|
+
const valueW = 26;
|
|
1051
|
+
return ` ${mark} ${DIM}${labelText}${RESET} ${pad(value, valueW)}${note}`;
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const hintLine = () => {
|
|
1055
|
+
const backTarget = active === 0 ? "back" : "previous";
|
|
1056
|
+
return footerBand(`enter ${active === fields.length - 1 ? "finish" : "next"} · ↑ ${backTarget} · type to edit · ctrl-c quit`);
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const render = (first = false) => {
|
|
1060
|
+
const vals = computeVals();
|
|
1061
|
+
const lines = [];
|
|
1062
|
+
// Header guidance is contextual: each field can carry its own hint
|
|
1063
|
+
// ("type your preferred Egregore name or hit enter…"), so the line
|
|
1064
|
+
// always says what to do RIGHT NOW instead of describing the form.
|
|
1065
|
+
const guidance = fields[active].hint || subtitle;
|
|
1066
|
+
lines.push(` ${CYAN}◆${RESET} ${BOLD}${title}${RESET}${guidance ? ` ${DIM}${guidance}${RESET}` : ""}`);
|
|
1067
|
+
lines.push("");
|
|
1068
|
+
for (let i = 0; i < fields.length; i++) lines.push(rowLine(i, vals));
|
|
1069
|
+
lines.push("");
|
|
1070
|
+
lines.push(error ? footerBand(`! ${error}`, 43) : hintLine());
|
|
1071
|
+
if (!first) clearLines(lastRows);
|
|
1072
|
+
const width = process.stdout.columns || 80;
|
|
1073
|
+
const text = lines.join("\n") + "\n";
|
|
1074
|
+
process.stdout.write(CURSOR_HIDE + text);
|
|
1075
|
+
lastRows = lines.reduce((sum, line) => sum + visualRows(line, width), 0);
|
|
1076
|
+
// Park the REAL terminal cursor at the active field's edit point so it
|
|
1077
|
+
// blinks there — the strongest "type here" affordance a terminal has.
|
|
1078
|
+
// Row math is visual-row aware (the header hint may wrap); the active
|
|
1079
|
+
// field row itself is short and assumed unwrapped.
|
|
1080
|
+
const activeLine = 2 + active;
|
|
1081
|
+
let rowsUp = 1; // the trailing newline after the block
|
|
1082
|
+
for (let i = activeLine + 1; i < lines.length; i++) rowsUp += visualRows(lines[i], width);
|
|
1083
|
+
const col = labelW + 9 + (buf != null ? visibleLen(buf) : 0);
|
|
1084
|
+
process.stdout.write(`\x1b[${rowsUp}A\x1b[${col}G${CURSOR_SHOW}`);
|
|
1085
|
+
_parkedRows = rowsUp; // clearLines() unparks before rewinding
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const cleanup = () => {
|
|
1089
|
+
process.stdin.setRawMode(false);
|
|
1090
|
+
process.stdin.pause();
|
|
1091
|
+
process.stdin.removeListener("keypress", onKey);
|
|
1092
|
+
process.stdout.write(CURSOR_SHOW);
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// Transcript receipt: the final plan stays in scrollback as a static
|
|
1096
|
+
// tree, so the record of what was named survives the redraw region.
|
|
1097
|
+
const receipt = (vals) => {
|
|
1098
|
+
const lines = [` ${CYAN}◆${RESET} ${BOLD}${title}${RESET}`];
|
|
1099
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1100
|
+
const f = fields[i];
|
|
1101
|
+
if (!vals[f.key]) continue;
|
|
1102
|
+
const glyph = f.tree ? (i === lastTreeIdx ? "└─ " : "├─ ") : "";
|
|
1103
|
+
const note = f.note ? ` ${DIM}${f.note}${RESET}` : "";
|
|
1104
|
+
lines.push(` ${DIM}${pad(`${glyph}${f.label}`, labelW)}${RESET} ${pad(`${BOLD}${vals[f.key]}${RESET}`, 26)}${note}`);
|
|
1105
|
+
}
|
|
1106
|
+
return lines.join("\n") + "\n";
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
const commitActive = () => {
|
|
1110
|
+
const f = fields[active];
|
|
1111
|
+
const vals = computeVals();
|
|
1112
|
+
const typed = buf != null && buf.trim() !== "";
|
|
1113
|
+
const value = vals[f.key];
|
|
1114
|
+
if (!value && !f.optional) {
|
|
1115
|
+
error = `${f.label} can't be empty.`;
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
if (value && f.validate) {
|
|
1119
|
+
// Validate against the other fields' values, not our own in-progress one.
|
|
1120
|
+
const others = { ...vals };
|
|
1121
|
+
delete others[f.key];
|
|
1122
|
+
const err = f.validate(value, others);
|
|
1123
|
+
if (err) { error = err; return false; }
|
|
1124
|
+
}
|
|
1125
|
+
if (typed) entered[f.key] = value;
|
|
1126
|
+
else if (buf != null && buf.trim() === "" && f.optional) entered[f.key] = "";
|
|
1127
|
+
error = null;
|
|
1128
|
+
confirmed = Math.max(confirmed, active);
|
|
1129
|
+
return true;
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
const onKey = (_str, key) => {
|
|
1133
|
+
if (!key) return;
|
|
1134
|
+
if (key.ctrl && key.name === "c") {
|
|
1135
|
+
cleanup();
|
|
1136
|
+
clearLines(lastRows);
|
|
1137
|
+
process.stdout.write(` ${DIM}(cancelled)${RESET}\n`);
|
|
1138
|
+
process.exit(130);
|
|
1139
|
+
}
|
|
1140
|
+
// ↑ = previous field; at the first field it leaves the form (BACK to
|
|
1141
|
+
// the previous wizard step). Esc is a silent alias — it never quits.
|
|
1142
|
+
if (key.name === "up" || key.name === "escape") {
|
|
1143
|
+
error = null;
|
|
1144
|
+
if (active === 0) {
|
|
1145
|
+
cleanup();
|
|
1146
|
+
clearLines(lastRows);
|
|
1147
|
+
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${title}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
1148
|
+
resolve(BACK);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
buf = null;
|
|
1152
|
+
active -= 1;
|
|
1153
|
+
render();
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (key.name === "return" || key.name === "down") {
|
|
1157
|
+
if (key.name === "down" && active === fields.length - 1) return;
|
|
1158
|
+
if (!commitActive()) { render(); return; }
|
|
1159
|
+
if (active === fields.length - 1) {
|
|
1160
|
+
const vals = computeVals();
|
|
1161
|
+
cleanup();
|
|
1162
|
+
clearLines(lastRows);
|
|
1163
|
+
process.stdout.write(receipt(vals));
|
|
1164
|
+
resolve({ values: vals });
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
buf = null;
|
|
1168
|
+
active += 1;
|
|
1169
|
+
render();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (key.name === "backspace") {
|
|
1173
|
+
// Backspace on an untouched field materializes the ghost for editing.
|
|
1174
|
+
if (buf == null) {
|
|
1175
|
+
const vals = computeVals();
|
|
1176
|
+
buf = ghostFor(active, vals).slice(0, -1);
|
|
1177
|
+
} else {
|
|
1178
|
+
buf = buf.slice(0, -1);
|
|
1179
|
+
}
|
|
1180
|
+
// Deleting everything un-touches the field — the derived suggestion
|
|
1181
|
+
// returns (and downstream derives recover) instead of leaving the
|
|
1182
|
+
// row stuck empty.
|
|
1183
|
+
if (buf === "") buf = null;
|
|
1184
|
+
error = null;
|
|
1185
|
+
render();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (_str && _str.length === 1 && !key.ctrl && !key.meta && _str.charCodeAt(0) >= 32) {
|
|
1189
|
+
buf = (buf == null ? "" : buf) + _str;
|
|
1190
|
+
error = null;
|
|
1191
|
+
render();
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1196
|
+
process.stdin.setRawMode(true);
|
|
1197
|
+
process.stdin.resume();
|
|
1198
|
+
process.stdin.on("keypress", onKey);
|
|
1199
|
+
process.stdout.write(CURSOR_HIDE);
|
|
1200
|
+
render(true);
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
856
1204
|
// ── commandBox ──────────────────────────────────────────────────────
|
|
857
1205
|
//
|
|
858
1206
|
// Preserved for back-compat; card() is the newer more-flexible version.
|
|
@@ -1113,7 +1461,7 @@ module.exports = {
|
|
|
1113
1461
|
banner, bannerCompact, section, note, card,
|
|
1114
1462
|
info, success, warn, error, step,
|
|
1115
1463
|
dim, bold, cyan, green, yellow, red,
|
|
1116
|
-
spinner, prompt, promptOrBack, choose, multiSelect, confirm, canInteract, BACK,
|
|
1464
|
+
spinner, prompt, promptOrBack, manifestForm, choose, multiSelect, confirm, canInteract, BACK,
|
|
1117
1465
|
commandBox,
|
|
1118
1466
|
canShowScreens, welcomeScreen, summaryScreen,
|
|
1119
1467
|
_renderWelcomeBody, _readPkgVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.2",
|
|
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 test/back-navigation.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.
|
|
@@ -224,6 +224,8 @@ Codex has no structured question tool — render each batch as compact numbered
|
|
|
224
224
|
|
|
225
225
|
**Rules:** Max 4 questions per batch. When choices aren't mutually exclusive, say "pick any that apply".`;
|
|
226
226
|
|
|
227
|
+
const LOOM_ROUTING_BODY = `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.`;
|
|
228
|
+
|
|
227
229
|
const ISOLATION_BODY = `Sessions are confined to this project + memory + managed repos. On Codex this boundary is a standing instruction, not an enforced hook — hold it yourself.
|
|
228
230
|
|
|
229
231
|
- **Never modify \`~/.egregore/instances.json\`** — managed by session-start.sh
|
|
@@ -266,6 +268,11 @@ const RULES = {
|
|
|
266
268
|
'Onboarding': { action: 'keep', note: 'Kept; /onboarding rendered as $onboarding.' },
|
|
267
269
|
'Transparency Beat': { action: 'keep', note: 'Kept verbatim.' },
|
|
268
270
|
'Memory': { action: 'keep', note: 'Kept verbatim.' },
|
|
271
|
+
'Loom Routing': {
|
|
272
|
+
action: 'replace',
|
|
273
|
+
body: LOOM_ROUTING_BODY,
|
|
274
|
+
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.',
|
|
275
|
+
},
|
|
269
276
|
'Git Workflow': {
|
|
270
277
|
action: 'keep',
|
|
271
278
|
note: 'Kept; slash tokens rendered as $skill tokens. Branch diagram unchanged (code fence).',
|