create-egregore 0.18.18 → 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 +93 -79
- package/lib/ui.js +374 -31
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.
|
|
2
|
+
// egregore-launcher-version: 0.6.0
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -30,7 +30,7 @@ const https = require("node:https");
|
|
|
30
30
|
const { spawnSync, spawn } = require("node:child_process");
|
|
31
31
|
const readline = require("node:readline");
|
|
32
32
|
|
|
33
|
-
const VERSION = "0.
|
|
33
|
+
const VERSION = "0.6.0";
|
|
34
34
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
35
35
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
36
36
|
const PUBLIC_NPX_PACKAGE = "create-egregore";
|
|
@@ -424,7 +424,7 @@ function runtimePickerLines(inst, runtimes, stored, cursor) {
|
|
|
424
424
|
const labelWidth = Math.max(...runtimes.map((r) => runtimeLabel(r).length));
|
|
425
425
|
const lines = [
|
|
426
426
|
"",
|
|
427
|
-
` ${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · ${DIM}1-${runtimes.length}${RESET} quick select · ${DIM}
|
|
427
|
+
` ${DIM}↑↓${RESET} navigate · ${DIM}enter${RESET} select · ${DIM}1-${runtimes.length}${RESET} quick select · ${DIM}q${RESET} cancel`,
|
|
428
428
|
` ${CYAN}◆${RESET} ${BOLD}Launch ${title} with which harness?${RESET}`,
|
|
429
429
|
"",
|
|
430
430
|
];
|
|
@@ -494,7 +494,9 @@ function pickRuntimeInteractive(inst, runtimes, stored) {
|
|
|
494
494
|
cleanup();
|
|
495
495
|
process.exit(130);
|
|
496
496
|
}
|
|
497
|
-
|
|
497
|
+
// Esc never quits (it reads as "back", and there's nowhere back to go
|
|
498
|
+
// from here) — q is the only cancel key besides Ctrl+C.
|
|
499
|
+
if (key.name === "q") return finish(null);
|
|
498
500
|
if (key.name === "up" || (key.ctrl && key.name === "p") || key.name === "k") {
|
|
499
501
|
cursor = (cursor - 1 + runtimes.length) % runtimes.length;
|
|
500
502
|
render();
|
|
@@ -832,10 +834,9 @@ function cmdHelp() {
|
|
|
832
834
|
` ${DIM}n / +${RESET} create a new Egregore`,
|
|
833
835
|
` ${DIM}u${RESET} set selected as default`,
|
|
834
836
|
` ${DIM}i${RESET} invite a GitHub user to selected instance`,
|
|
835
|
-
` ${DIM}
|
|
836
|
-
` ${DIM}p${RESET} prune entries with missing paths (with confirm)`,
|
|
837
|
+
` ${DIM}d${RESET} delete selected entry (offers cleanup of ${RED}✗${RESET} missing ones)`,
|
|
837
838
|
` ${DIM}?${RESET} toggle help overlay`,
|
|
838
|
-
` ${DIM}q
|
|
839
|
+
` ${DIM}q${RESET} quit browser (esc goes back — it never quits)`,
|
|
839
840
|
``,
|
|
840
841
|
` ${BOLD}REGISTRY${RESET}`,
|
|
841
842
|
` ${DIM}instances:${RESET} ~/.egregore/instances.json`,
|
|
@@ -1463,7 +1464,7 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
|
|
|
1463
1464
|
}
|
|
1464
1465
|
|
|
1465
1466
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1466
|
-
lines.push(boxLine(`${DIM}↑↓ navigate · enter select · 1-${runtimes.length} quick select ·
|
|
1467
|
+
lines.push(boxLine(`${DIM}↑↓ navigate · enter select · 1-${runtimes.length} quick select · ← back · q quit${RESET}`, innerWidth, margin));
|
|
1467
1468
|
lines.push(`${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`);
|
|
1468
1469
|
return lines;
|
|
1469
1470
|
}
|
|
@@ -1482,6 +1483,15 @@ async function browser(args = {}) {
|
|
|
1482
1483
|
return numberedPicker(registry, args.runtimeOverride);
|
|
1483
1484
|
}
|
|
1484
1485
|
|
|
1486
|
+
// Entries whose directory is gone are dead registry rows. They stay listed
|
|
1487
|
+
// (marked ✗ missing) so the problem is visible in place — cleanup is offered
|
|
1488
|
+
// through `d`, never done automatically. Cached per session; refreshed after
|
|
1489
|
+
// every registry mutation instead of stat-ing on each keypress render.
|
|
1490
|
+
let stalePaths = new Set(registry.filter((e) => !fs.existsSync(e.path)).map((e) => e.path));
|
|
1491
|
+
const refreshStale = () => {
|
|
1492
|
+
stalePaths = new Set(registry.filter((e) => !fs.existsSync(e.path)).map((e) => e.path));
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1485
1495
|
// State
|
|
1486
1496
|
const state = {
|
|
1487
1497
|
filter: "",
|
|
@@ -1493,7 +1503,7 @@ async function browser(args = {}) {
|
|
|
1493
1503
|
runtimeCursor: 0,
|
|
1494
1504
|
cursor: initialCursorForCwd(registry),
|
|
1495
1505
|
showHelp: false,
|
|
1496
|
-
modal: null, // { type: "remove" | "
|
|
1506
|
+
modal: null, // { type: "remove" | "sweep", onYes, onNo }
|
|
1497
1507
|
tick: 0,
|
|
1498
1508
|
message: null, // ephemeral info line (e.g. "default set to X")
|
|
1499
1509
|
messageKind: "info", // "info" | "success" | "warn"
|
|
@@ -1585,7 +1595,10 @@ async function browser(args = {}) {
|
|
|
1585
1595
|
out.push(ANSI.clearLine + "\n");
|
|
1586
1596
|
|
|
1587
1597
|
// Layout: list takes top 60%, preview takes rest, footer is last 2 lines
|
|
1588
|
-
|
|
1598
|
+
// (3 while stale entries exist — the cleanup notice gets its own row so
|
|
1599
|
+
// the layout doesn't jump when modals or messages come and go).
|
|
1600
|
+
const staleCount = stalePaths.size;
|
|
1601
|
+
const footerRows = staleCount > 0 ? 3 : 2;
|
|
1589
1602
|
const usedRows = heroLines.length + 1;
|
|
1590
1603
|
const availableRows = Math.max(8, h - usedRows - footerRows - 2);
|
|
1591
1604
|
const listRows = Math.max(4, Math.floor(availableRows * 0.48));
|
|
@@ -1631,13 +1644,21 @@ async function browser(args = {}) {
|
|
|
1631
1644
|
void origIdx;
|
|
1632
1645
|
const isCursor = idx === state.cursor;
|
|
1633
1646
|
const isDefault = e.slug === def;
|
|
1647
|
+
const isMissing = stalePaths.has(e.path);
|
|
1634
1648
|
const cursorMark = isCursor ? `${CYAN}❯${RESET}` : " ";
|
|
1635
1649
|
const defMark = isDefault ? `${GREEN}●${RESET}` : " ";
|
|
1636
1650
|
const slug = isCursor
|
|
1637
1651
|
? `${BOLD}${CYAN}${pad(e.slug, slugW)}${RESET}`
|
|
1638
|
-
: (isDefault
|
|
1639
|
-
|
|
1640
|
-
|
|
1652
|
+
: (isDefault
|
|
1653
|
+
? `${BOLD}${pad(e.slug, slugW)}${RESET}`
|
|
1654
|
+
: (isMissing ? `${DIM}${pad(e.slug, slugW)}${RESET}` : pad(e.slug, slugW)));
|
|
1655
|
+
const name = isMissing && !isCursor
|
|
1656
|
+
? `${DIM}${pad(e.name || "", nameW)}${RESET}`
|
|
1657
|
+
: pad(e.name || "", nameW);
|
|
1658
|
+
const pathAvail = Math.max(0, listAvail - slugW - nameW - 10);
|
|
1659
|
+
const pathDim = isMissing
|
|
1660
|
+
? `${RED}✗${RESET} ${DIM}${truncate(e.path, Math.max(8, pathAvail - 12))}${RESET} ${RED}(missing)${RESET}`
|
|
1661
|
+
: `${DIM}${truncate(e.path, pathAvail)}${RESET}`;
|
|
1641
1662
|
const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${pathDim}`;
|
|
1642
1663
|
out.push(M + " ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
|
|
1643
1664
|
}
|
|
@@ -1661,7 +1682,9 @@ async function browser(args = {}) {
|
|
|
1661
1682
|
// value to blockWidth-15 so every line's right edge lands two cols inside
|
|
1662
1683
|
// the box border, level with the box inner content (gutter+blockWidth-2).
|
|
1663
1684
|
const valW = Math.max(16, blockWidth - 15);
|
|
1664
|
-
previewLines.push(
|
|
1685
|
+
previewLines.push(stalePaths.has(selected.path)
|
|
1686
|
+
? ` ${label("path")} ${RED}✗${RESET} ${truncate(selected.path, Math.max(8, valW - 12))} ${RED}(missing)${RESET}`
|
|
1687
|
+
: ` ${label("path")} ${truncate(selected.path, valW)}`);
|
|
1665
1688
|
if (pv.org) previewLines.push(` ${label("org")} ${truncate(`${pv.org} ${DIM}(${pv.githubOrg || "?"})${RESET}`, valW)}`);
|
|
1666
1689
|
if (pv.branch) previewLines.push(` ${label("branch")} ${BOLD}${pv.branch}${RESET}`);
|
|
1667
1690
|
if (pv.lastCommit) previewLines.push(` ${label("last")} ${DIM}${truncate(pv.lastCommit, valW)}${RESET}`);
|
|
@@ -1690,32 +1713,54 @@ async function browser(args = {}) {
|
|
|
1690
1713
|
// Divider
|
|
1691
1714
|
out.push(DIM + M + "─".repeat(blockWidth) + RESET + ANSI.clearLine + "\n");
|
|
1692
1715
|
|
|
1693
|
-
//
|
|
1716
|
+
// Notice row — persistent while dead entries exist, quiet otherwise. The
|
|
1717
|
+
// row is reserved either way (footerRows above) so toggling modals/filter
|
|
1718
|
+
// doesn't shift the layout.
|
|
1719
|
+
if (staleCount > 0) {
|
|
1720
|
+
const noticeIdle = !state.modal && !state.filterMode && !state.inviteMode;
|
|
1721
|
+
const noun = staleCount === 1 ? "entry points to a missing folder" : "entries point to missing folders";
|
|
1722
|
+
out.push(noticeIdle
|
|
1723
|
+
? `${M} ${YELLOW}!${RESET} ${staleCount} ${noun} — ${BOLD}d${RESET} ${DIM}to clean up${RESET}${ANSI.clearLine}\n`
|
|
1724
|
+
: ANSI.clearLine + "\n");
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Footer — a status band in the banner's accent color (the terminal's
|
|
1728
|
+
// cyan slot) anchors the keys; the plain dim strip was easy to miss.
|
|
1729
|
+
// Band color doubles as a mode signal: accent = browsing, yellow = a
|
|
1730
|
+
// question needs answering, green = success flash. Input states
|
|
1731
|
+
// (filter, invite) stay unbanded so the caret keeps its contrast.
|
|
1732
|
+
// Content on the band is fg-black with BOLD/UNBOLD only — a RESET
|
|
1733
|
+
// mid-line would kill the background fill.
|
|
1734
|
+
const UNBOLD = E(22);
|
|
1735
|
+
const band = (content, bg) => colorOn
|
|
1736
|
+
? `${E(bg)}${E(30)}${pad(` ${content}`, blockWidth)}${RESET}${ANSI.clearLine}`
|
|
1737
|
+
: ` ${stripAnsi(content)}${ANSI.clearLine}`;
|
|
1738
|
+
const key = (k, label) => `${BOLD}${k}${UNBOLD} ${label}`;
|
|
1694
1739
|
let footer;
|
|
1695
1740
|
if (state.modal) {
|
|
1696
1741
|
const m = state.modal;
|
|
1697
1742
|
const prompt = m.type === "remove"
|
|
1698
|
-
? `remove ${BOLD}${m.inst.slug}${
|
|
1699
|
-
: `
|
|
1700
|
-
footer =
|
|
1743
|
+
? `remove ${BOLD}${m.inst.slug}${UNBOLD}${m.missing ? " (folder missing)" : ""}? [y/N]`
|
|
1744
|
+
: `remove ${BOLD}${m.count}${UNBOLD} ${m.count === 1 ? "entry" : "entries"} with missing folders? [y/N]`;
|
|
1745
|
+
footer = band(`? ${prompt}`, 43);
|
|
1701
1746
|
} else if (state.filterMode) {
|
|
1702
1747
|
footer = ` ${DIM}typing filter · enter/esc to exit filter mode${RESET}${ANSI.clearLine}`;
|
|
1703
1748
|
} else if (state.inviteMode) {
|
|
1704
1749
|
const target = state.inviteInst?.slug || "selected";
|
|
1705
1750
|
footer = ` ${CYAN}/invite${RESET} ${DIM}${target}:${RESET} ${state.inviteUsername}${CYAN}▌${RESET} ${DIM}enter send · esc cancel${RESET}${ANSI.clearLine}`;
|
|
1706
1751
|
} else if (state.showHelp) {
|
|
1707
|
-
footer =
|
|
1752
|
+
footer = band(`${key("?", "toggles this hint")} ${key("q", "quits")}`, 46);
|
|
1708
1753
|
} else if (state.message && Date.now() < state.messageUntil) {
|
|
1709
|
-
const
|
|
1710
|
-
footer =
|
|
1754
|
+
const bg = state.messageKind === "success" ? 42 : state.messageKind === "warn" ? 43 : 46;
|
|
1755
|
+
footer = band(`● ${stripAnsi(state.message)}`, bg);
|
|
1711
1756
|
} else {
|
|
1712
1757
|
footer = blockWidth < 96
|
|
1713
|
-
?
|
|
1714
|
-
:
|
|
1758
|
+
? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("i", "invite")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46)
|
|
1759
|
+
: band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("/", "filter")} ${key("i", "invite")} ${key("u", "default")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46);
|
|
1715
1760
|
}
|
|
1716
1761
|
out.push(M + footer + "\n");
|
|
1717
1762
|
if (state.showHelp) {
|
|
1718
|
-
out.push(
|
|
1763
|
+
out.push(M + band("● default · ❯ cursor · ✗ folder missing · d deletes an entry · esc never quits", 46));
|
|
1719
1764
|
} else {
|
|
1720
1765
|
out.push(ANSI.clearLine);
|
|
1721
1766
|
}
|
|
@@ -1788,7 +1833,9 @@ async function browser(args = {}) {
|
|
|
1788
1833
|
if (state.runtimeInst) {
|
|
1789
1834
|
const runtimes = runtimeChoicesFor(state.runtimeInst, args.runtimeOverride);
|
|
1790
1835
|
if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
|
|
1791
|
-
|
|
1836
|
+
// ← is the advertised back key (enter descends, ← ascends); esc is a
|
|
1837
|
+
// silent alias for the reflex — it goes back, never quits.
|
|
1838
|
+
if (key.name === "escape" || key.name === "left") {
|
|
1792
1839
|
state.runtimeInst = null;
|
|
1793
1840
|
state.runtimeCursor = 0;
|
|
1794
1841
|
render();
|
|
@@ -1827,16 +1874,23 @@ async function browser(args = {}) {
|
|
|
1827
1874
|
return;
|
|
1828
1875
|
}
|
|
1829
1876
|
|
|
1830
|
-
// Modal handling takes priority
|
|
1877
|
+
// Modal handling takes priority. The modal is cleared BEFORE its
|
|
1878
|
+
// callback runs so a callback can chain a follow-up modal (the delete →
|
|
1879
|
+
// sweep offer). Esc is a full cancel: it closes the modal and skips the
|
|
1880
|
+
// chain — n/enter decline this modal but still allow the follow-up.
|
|
1831
1881
|
if (state.modal) {
|
|
1882
|
+
const m = state.modal;
|
|
1832
1883
|
if ((chunk && /^[yY]/.test(chunk))) {
|
|
1833
|
-
state.modal.onYes();
|
|
1834
1884
|
state.modal = null;
|
|
1885
|
+
m.onYes();
|
|
1835
1886
|
render();
|
|
1836
|
-
} else if (key.name === "escape"
|
|
1837
|
-
state.modal.onNo?.();
|
|
1887
|
+
} else if (key.name === "escape") {
|
|
1838
1888
|
state.modal = null;
|
|
1839
1889
|
render();
|
|
1890
|
+
} else if ((chunk && /^[nN]/.test(chunk)) || key.name === "return") {
|
|
1891
|
+
state.modal = null;
|
|
1892
|
+
m.onNo?.();
|
|
1893
|
+
render();
|
|
1840
1894
|
}
|
|
1841
1895
|
return;
|
|
1842
1896
|
}
|
|
@@ -1908,13 +1962,20 @@ async function browser(args = {}) {
|
|
|
1908
1962
|
|
|
1909
1963
|
// Normal mode
|
|
1910
1964
|
if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
|
|
1911
|
-
|
|
1965
|
+
// Esc never quits: at the root it only clears an active filter. Users
|
|
1966
|
+
// press esc expecting either "back" or "abort" — both are safe when it
|
|
1967
|
+
// can't eject them to the shell. A flash points at the real quit key.
|
|
1968
|
+
if (key.name === "escape") {
|
|
1912
1969
|
if (state.filter) {
|
|
1913
1970
|
state.filter = "";
|
|
1914
1971
|
clampCursor();
|
|
1915
1972
|
render();
|
|
1916
1973
|
return;
|
|
1917
1974
|
}
|
|
1975
|
+
flashMessage(`${BOLD}q${RESET} quits — esc never does`, "info");
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (key.name === "q") {
|
|
1918
1979
|
cleanup();
|
|
1919
1980
|
resolve();
|
|
1920
1981
|
return;
|
|
@@ -2000,40 +2061,49 @@ async function browser(args = {}) {
|
|
|
2000
2061
|
flashMessage(`default set to ${BOLD}${pick.slug}${RESET}`, "success");
|
|
2001
2062
|
return;
|
|
2002
2063
|
}
|
|
2003
|
-
|
|
2064
|
+
// d = the one delete verb (r kept as an unadvertised alias for muscle
|
|
2065
|
+
// memory). Deleting confirms first; when other entries with missing
|
|
2066
|
+
// folders remain, a sweep offer chains after — whether the first modal
|
|
2067
|
+
// was accepted or declined, so pressing d anywhere still reaches the
|
|
2068
|
+
// cleanup the notice line promises. Esc cancels without chaining.
|
|
2069
|
+
if (chunk === "d" || chunk === "r") {
|
|
2004
2070
|
const pick = visible()[state.cursor]?.e;
|
|
2005
2071
|
if (!pick) return;
|
|
2072
|
+
const offerSweep = () => {
|
|
2073
|
+
const stale = registry.filter((e) => !fs.existsSync(e.path));
|
|
2074
|
+
if (stale.length === 0) return;
|
|
2075
|
+
state.modal = {
|
|
2076
|
+
type: "sweep",
|
|
2077
|
+
count: stale.length,
|
|
2078
|
+
onYes: () => {
|
|
2079
|
+
const bak = backupRegistry();
|
|
2080
|
+
registry = registry.filter((e) => fs.existsSync(e.path));
|
|
2081
|
+
writeRegistry(registry);
|
|
2082
|
+
refreshStale();
|
|
2083
|
+
clampCursor();
|
|
2084
|
+
flashMessage(`removed ${stale.length} · ${registry.length} remain · backup: ${path.basename(bak)}`, "success");
|
|
2085
|
+
},
|
|
2086
|
+
};
|
|
2087
|
+
};
|
|
2088
|
+
const pickMissing = !fs.existsSync(pick.path);
|
|
2006
2089
|
state.modal = {
|
|
2007
2090
|
type: "remove",
|
|
2008
2091
|
inst: pick,
|
|
2092
|
+
missing: pickMissing,
|
|
2009
2093
|
onYes: () => {
|
|
2010
2094
|
const bak = backupRegistry();
|
|
2011
2095
|
registry = registry.filter((e) => e.path !== pick.path);
|
|
2012
2096
|
writeRegistry(registry);
|
|
2013
2097
|
previewCache.delete(pick.path);
|
|
2098
|
+
refreshStale();
|
|
2014
2099
|
clampCursor();
|
|
2015
2100
|
flashMessage(`removed ${BOLD}${pick.slug}${RESET} · backup: ${path.basename(bak)}`, "success");
|
|
2101
|
+
offerSweep();
|
|
2016
2102
|
},
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (chunk === "p") {
|
|
2022
|
-
const stale = registry.filter((e) => !fs.existsSync(e.path));
|
|
2023
|
-
if (stale.length === 0) {
|
|
2024
|
-
flashMessage(`registry is clean — all ${registry.length} paths exist.`, "success");
|
|
2025
|
-
return;
|
|
2026
|
-
}
|
|
2027
|
-
state.modal = {
|
|
2028
|
-
type: "prune",
|
|
2029
|
-
count: stale.length,
|
|
2030
|
-
onYes: () => {
|
|
2031
|
-
const bak = backupRegistry();
|
|
2032
|
-
registry = registry.filter((e) => fs.existsSync(e.path));
|
|
2033
|
-
writeRegistry(registry);
|
|
2034
|
-
clampCursor();
|
|
2035
|
-
flashMessage(`pruned ${stale.length} · ${registry.length} remain · backup: ${path.basename(bak)}`, "success");
|
|
2036
|
-
},
|
|
2103
|
+
// Declining a healthy row still routes to cleanup (the notice's d
|
|
2104
|
+
// lands here); declining a missing row means "leave it" — don't
|
|
2105
|
+
// immediately re-offer the same removal.
|
|
2106
|
+
onNo: () => { if (!pickMissing) offerSweep(); },
|
|
2037
2107
|
};
|
|
2038
2108
|
render();
|
|
2039
2109
|
return;
|
package/lib/local.js
CHANGED
|
@@ -539,14 +539,14 @@ async function maybeSetupTelegramGroup(ui, {
|
|
|
539
539
|
ui.info(`Bot add link: ${ui.cyan(botLink)}`);
|
|
540
540
|
openBrowser(botLink);
|
|
541
541
|
|
|
542
|
-
await ui.prompt("Press Enter here after the bot is in the group");
|
|
542
|
+
await ui.prompt("Press Enter here after the bot is in the group", { hint: "enter continue · ctrl-c quit" });
|
|
543
543
|
ui.note([
|
|
544
544
|
"Step 4: In Telegram, copy a group invite link.",
|
|
545
545
|
"Paste it below so teammate invites include the Telegram group too.",
|
|
546
546
|
"If you skip this, Egregore will still work, but invites will not include Telegram.",
|
|
547
547
|
]);
|
|
548
548
|
|
|
549
|
-
const groupLink = await ui.prompt("Telegram group join link (paste here, or Enter to skip)");
|
|
549
|
+
const groupLink = await ui.prompt("Telegram group join link (paste here, or Enter to skip)", { hint: "paste the link · enter skip" });
|
|
550
550
|
const cleaned = groupLink.trim();
|
|
551
551
|
let storedGroupLink = "";
|
|
552
552
|
if (cleaned && !looksLikeUrl(cleaned)) {
|
|
@@ -725,52 +725,92 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
725
725
|
return true;
|
|
726
726
|
};
|
|
727
727
|
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
"
|
|
744
|
-
"
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
728
|
+
const stepNameManifest = async () => {
|
|
729
|
+
// One live manifest instead of scattered naming prompts: name the thing
|
|
730
|
+
// first, watch the repo plan derive from it in place, override any line.
|
|
731
|
+
// Every collision check runs against names VISIBLE on screen — the old
|
|
732
|
+
// flow rejected a code-repo name for colliding with an Egregore repo
|
|
733
|
+
// name the user had never been shown.
|
|
734
|
+
console.log("");
|
|
735
|
+
const isNew = st.isNewProject;
|
|
736
|
+
const validSlug = (v) => (slugifyName(v)
|
|
737
|
+
? null
|
|
738
|
+
: "That doesn't produce a valid repo name. Try letters, numbers, and dashes.");
|
|
739
|
+
const fields = [
|
|
740
|
+
{
|
|
741
|
+
key: "orgName",
|
|
742
|
+
label: "Egregore",
|
|
743
|
+
note: "team or group name",
|
|
744
|
+
hint: "type your preferred Egregore name or hit enter to pick the naming below",
|
|
745
|
+
initial: st.orgName || undefined,
|
|
746
|
+
derive: () => st.githubOrg,
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
key: "repoName",
|
|
750
|
+
label: "Egregore repo",
|
|
751
|
+
note: "config + skills",
|
|
752
|
+
hint: `enter keeps this name · type to change · created in ${st.githubOrg}`,
|
|
753
|
+
tree: true,
|
|
754
|
+
initial: st.repoName || undefined,
|
|
755
|
+
derive: (v) => slugifyName(v.orgName) || "egregore",
|
|
756
|
+
normalize: (raw) => slugifyName(raw),
|
|
757
|
+
validate: (v) => validSlug(v),
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
key: "memoryRepoName",
|
|
761
|
+
label: "memory repo",
|
|
762
|
+
note: "shared memory",
|
|
763
|
+
hint: "enter keeps this name · type to change",
|
|
764
|
+
tree: true,
|
|
765
|
+
initial: st.memoryRepoName || undefined,
|
|
766
|
+
derive: (v) => `${v.repoName || "egregore"}-memory`,
|
|
767
|
+
normalize: (raw) => slugifyName(raw),
|
|
768
|
+
validate: (v, others) => validSlug(v)
|
|
769
|
+
|| (v === others.repoName ? "Memory repo can't share the Egregore repo's name." : null),
|
|
770
|
+
},
|
|
771
|
+
...(isNew ? [{
|
|
772
|
+
key: "codeRepo",
|
|
773
|
+
label: "code repo",
|
|
774
|
+
note: "your project · optional",
|
|
775
|
+
hint: "add a repo for your code project — or enter to skip",
|
|
776
|
+
tree: true,
|
|
777
|
+
optional: true,
|
|
778
|
+
initial: st.newProjectRepo || undefined,
|
|
779
|
+
normalize: (raw) => slugifyName(raw),
|
|
780
|
+
validate: (v, others) => validSlug(v)
|
|
781
|
+
|| (v === others.repoName ? `"${v}" is the Egregore repo above. Pick a different name — say, ${v}-app.` : null)
|
|
782
|
+
|| (v === others.memoryRepoName ? `"${v}" is the memory repo above. Pick a different name.` : null),
|
|
783
|
+
}] : []),
|
|
784
|
+
];
|
|
785
|
+
const result = await ui.manifestForm({
|
|
786
|
+
title: "Name it",
|
|
787
|
+
subtitle: `repos land in ${st.githubOrg}`,
|
|
788
|
+
fields,
|
|
789
|
+
});
|
|
790
|
+
if (result === BACK) return BACK;
|
|
791
|
+
const v = result.values;
|
|
792
|
+
st.orgName = v.orgName;
|
|
793
|
+
st.repoName = v.repoName;
|
|
794
|
+
st.memoryRepoName = v.memoryRepoName;
|
|
795
|
+
st.newProjectRepo = isNew ? (v.codeRepo || null) : null;
|
|
796
|
+
return true;
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// The one-liner is repo metadata, not a naming decision — it lives OUTSIDE
|
|
800
|
+
// the manifest so enter on the name lands directly in the naming tree
|
|
801
|
+
// (the manifest's hint promises exactly that).
|
|
802
|
+
const stepDescription = async () => {
|
|
803
|
+
if (!st.isNewProject) {
|
|
771
804
|
st.description = "";
|
|
772
|
-
|
|
805
|
+
return SKIP;
|
|
773
806
|
}
|
|
807
|
+
console.log("");
|
|
808
|
+
const desc = await ui.promptOrBack("what are you building? (one line)", {
|
|
809
|
+
default: st.description || undefined,
|
|
810
|
+
hint: "one line on what you're making · enter to skip · ↑ back",
|
|
811
|
+
});
|
|
812
|
+
if (desc === BACK) return BACK;
|
|
813
|
+
st.description = desc;
|
|
774
814
|
return true;
|
|
775
815
|
};
|
|
776
816
|
|
|
@@ -891,41 +931,12 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
891
931
|
return true;
|
|
892
932
|
};
|
|
893
933
|
|
|
894
|
-
const stepRepoNames = async () => {
|
|
895
|
-
// Derived names shown as editable prompts — creating repos with silently
|
|
896
|
-
// derived names was a repeated field complaint.
|
|
897
|
-
const derivedSlug = slugifyName(st.orgName) || "egregore";
|
|
898
|
-
console.log("");
|
|
899
|
-
ui.note([
|
|
900
|
-
`Two repos will be created in ${ui.bold(st.githubOrg)}: the Egregore repo`,
|
|
901
|
-
"(config + skills) and its shared-memory repo. Enter keeps the defaults.",
|
|
902
|
-
]);
|
|
903
|
-
for (;;) {
|
|
904
|
-
const answer = await ui.promptOrBack("Egregore repo name", { default: st.repoName || derivedSlug });
|
|
905
|
-
if (answer === BACK) return BACK;
|
|
906
|
-
const candidate = slugifyName(answer);
|
|
907
|
-
if (!candidate) { ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes."); continue; }
|
|
908
|
-
if (st.newProjectRepo && candidate === st.newProjectRepo) { ui.warn(`"${candidate}" is already your project repo. Pick a different name.`); continue; }
|
|
909
|
-
st.repoName = candidate;
|
|
910
|
-
break;
|
|
911
|
-
}
|
|
912
|
-
for (;;) {
|
|
913
|
-
const answer = await ui.promptOrBack("memory repo name", { default: st.memoryRepoName || `${st.repoName}-memory` });
|
|
914
|
-
if (answer === BACK) return BACK;
|
|
915
|
-
const candidate = slugifyName(answer);
|
|
916
|
-
if (!candidate) { ui.warn("That doesn't produce a valid repo name. Try letters, numbers, and dashes."); continue; }
|
|
917
|
-
if (candidate === st.repoName) { ui.warn("Memory repo can't share the Egregore repo's name."); continue; }
|
|
918
|
-
if (st.newProjectRepo && candidate === st.newProjectRepo) { ui.warn(`"${candidate}" is already your project repo. Pick a different name.`); continue; }
|
|
919
|
-
st.memoryRepoName = candidate;
|
|
920
|
-
break;
|
|
921
|
-
}
|
|
922
|
-
return true;
|
|
923
|
-
};
|
|
924
|
-
|
|
925
934
|
// Direction-aware driver: BACK rewinds one step; SKIP (deterministic
|
|
926
935
|
// steps) passes through in whichever direction we're moving, so backing
|
|
927
936
|
// over an auto-answered step doesn't bounce the user forward again.
|
|
928
|
-
|
|
937
|
+
// ALL naming (Egregore + repo plan) happens in the manifest BEFORE the
|
|
938
|
+
// existing-project repo picker — name the thing first, then attach repos.
|
|
939
|
+
const steps = [stepHome, stepWorkType, stepNameManifest, stepDescription, stepRepos];
|
|
929
940
|
let stepIdx = 0;
|
|
930
941
|
let stepDir = 1;
|
|
931
942
|
while (stepIdx < steps.length) {
|
|
@@ -1146,7 +1157,10 @@ async function localFounderFlow(ui, ctx = {}) {
|
|
|
1146
1157
|
|
|
1147
1158
|
// 14. "What should we call you?" + write state
|
|
1148
1159
|
ui.section("Your name", "this is how teammates will see you in handoffs");
|
|
1149
|
-
const displayName = await ui.prompt("what should we call you?", {
|
|
1160
|
+
const displayName = await ui.prompt("what should we call you?", {
|
|
1161
|
+
default: user.login,
|
|
1162
|
+
hint: `type a display name · enter keeps ${user.login}`,
|
|
1163
|
+
});
|
|
1150
1164
|
const memberRole = await ensureInstallerMemberRole(
|
|
1151
1165
|
{ usage_type: "founder_group", member_role: ctx.memberRole },
|
|
1152
1166
|
{},
|
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 —
|
|
@@ -672,7 +734,7 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
672
734
|
if (searching && total > 0) lines.push(` ${DIM}searching…${RESET}`);
|
|
673
735
|
|
|
674
736
|
const searchHint = opts.remoteSearch ? "type to search" : "type to filter";
|
|
675
|
-
const escHint = opts.escAction === "back" ? ` · ${DIM}
|
|
737
|
+
const escHint = opts.escAction === "back" ? ` · ${DIM}←${RESET} back` : "";
|
|
676
738
|
if (!first) clearLines(lastLineCount);
|
|
677
739
|
lastLineCount = renderPicker({
|
|
678
740
|
question,
|
|
@@ -758,19 +820,20 @@ async function interactiveMultiSelect(question, options, opts = {}) {
|
|
|
758
820
|
render();
|
|
759
821
|
return;
|
|
760
822
|
}
|
|
761
|
-
if (key.name === "escape" && opts.escAction === "back") {
|
|
823
|
+
if ((key.name === "escape" || key.name === "left") && opts.escAction === "back") {
|
|
762
824
|
cleanup();
|
|
763
825
|
clearLines(lastLineCount);
|
|
764
826
|
process.stdout.write(` ${CYAN}◆${RESET} ${BOLD}${question}${RESET} ${DIM}→ ← back${RESET}\n`);
|
|
765
827
|
resolve(BACK);
|
|
766
828
|
return;
|
|
767
829
|
}
|
|
768
|
-
if (
|
|
830
|
+
if (key.ctrl && key.name === "c") {
|
|
769
831
|
cleanup();
|
|
770
832
|
unparkCursor();
|
|
771
833
|
process.stdout.write(`\n ${DIM}(cancelled)${RESET}\n`);
|
|
772
834
|
process.exit(130);
|
|
773
835
|
}
|
|
836
|
+
if (key.name === "escape") return; // esc never quits — ctrl-c aborts
|
|
774
837
|
if (key.name === "return") return finish();
|
|
775
838
|
if (key.name === "up" || (key.ctrl && key.name === "p")) {
|
|
776
839
|
cursor = (cursor - 1 + visible.length) % Math.max(1, visible.length);
|
|
@@ -853,6 +916,286 @@ async function legacyMultiSelect(question, options) {
|
|
|
853
916
|
return [...picked];
|
|
854
917
|
}
|
|
855
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
|
+
|
|
856
1199
|
// ── commandBox ──────────────────────────────────────────────────────
|
|
857
1200
|
//
|
|
858
1201
|
// Preserved for back-compat; card() is the newer more-flexible version.
|
|
@@ -1113,7 +1456,7 @@ module.exports = {
|
|
|
1113
1456
|
banner, bannerCompact, section, note, card,
|
|
1114
1457
|
info, success, warn, error, step,
|
|
1115
1458
|
dim, bold, cyan, green, yellow, red,
|
|
1116
|
-
spinner, prompt, promptOrBack, choose, multiSelect, confirm, canInteract, BACK,
|
|
1459
|
+
spinner, prompt, promptOrBack, manifestForm, choose, multiSelect, confirm, canInteract, BACK,
|
|
1117
1460
|
commandBox,
|
|
1118
1461
|
canShowScreens, welcomeScreen, summaryScreen,
|
|
1119
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 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).',
|
|
@@ -72,12 +72,12 @@ _apply_framework_update() {
|
|
|
72
72
|
fi
|
|
73
73
|
# Apply upstream changes — checkout is idempotent, skip diff check
|
|
74
74
|
# (git diff with variable path lists breaks in zsh — no word-splitting)
|
|
75
|
-
for _p in bin/ .claude/commands/ .claude/skills/ .claude/hooks/ .claude/context/ CLAUDE.md skills/; do
|
|
75
|
+
for _p in bin/ .claude/commands/ .claude/skills/ .claude/hooks/ .claude/context/ .claude/agents/ loom/ CLAUDE.md skills/; do
|
|
76
76
|
git checkout upstream/main -- "$_p" 2>/dev/null || true
|
|
77
77
|
done
|
|
78
78
|
# Only commit if there are actual changes
|
|
79
|
-
if [ -n "$(git status --porcelain bin/ .claude/ CLAUDE.md skills/ 2>/dev/null | head -1)" ]; then
|
|
80
|
-
git add bin/ .claude/ CLAUDE.md skills/ 2>/dev/null
|
|
79
|
+
if [ -n "$(git status --porcelain bin/ .claude/ loom/ CLAUDE.md skills/ 2>/dev/null | head -1)" ]; then
|
|
80
|
+
git add bin/ .claude/ loom/ CLAUDE.md skills/ 2>/dev/null
|
|
81
81
|
EGREGORE_FRAMEWORK_UPDATE=1 git commit -m "Auto-update Egregore framework" --quiet 2>/dev/null || true
|
|
82
82
|
FRAMEWORK_UPDATED="true"
|
|
83
83
|
fi
|