enigma-cli 1.1.4 → 1.3.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/skills/backend-policy/skill.json +1 -1
- package/assets/skills/ciphera-style-policy/skill.json +1 -1
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/skill.json +1 -1
- package/assets/skills/database-expert/skill.json +1 -1
- package/assets/skills/debugging-policy/skill.json +1 -1
- package/assets/skills/dependency-policy/skill.json +1 -1
- package/assets/skills/frontend-policy/skill.json +1 -1
- package/assets/skills/git-policy/skill.json +1 -1
- package/assets/skills/security-policy/skill.json +1 -1
- package/assets/skills/testing-policy/skill.json +1 -1
- package/assets/skills/validation-policy/skill.json +1 -1
- package/dist/enigma.js +814 -217
- package/package.json +9 -4
package/dist/enigma.js
CHANGED
|
@@ -106,7 +106,7 @@ var init_agents = __esm({
|
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
opencode: {
|
|
109
|
-
label: "
|
|
109
|
+
label: "OpenCode",
|
|
110
110
|
memoryFile: "AGENTS.md",
|
|
111
111
|
// opencode reads AGENTS.md from ~/.config/opencode (global) or the project
|
|
112
112
|
// root (local); skills from ~/.config/opencode/skills and .opencode/skills.
|
|
@@ -217,7 +217,7 @@ var init_claude = __esm({
|
|
|
217
217
|
import { homedir as homedir3 } from "os";
|
|
218
218
|
import { join as join5 } from "path";
|
|
219
219
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
220
|
-
import * as
|
|
220
|
+
import * as p3 from "@clack/prompts";
|
|
221
221
|
async function resolveBypassSelection(candidates, opts, interactive) {
|
|
222
222
|
const supported = candidates.filter((a) => BYPASS_SUPPORTED.includes(a.name));
|
|
223
223
|
if (!supported.length || opts.noBypass) return [];
|
|
@@ -229,7 +229,7 @@ async function resolveBypassSelection(candidates, opts, interactive) {
|
|
|
229
229
|
return names.filter((n) => req.includes(n));
|
|
230
230
|
}
|
|
231
231
|
if (!interactive) return [];
|
|
232
|
-
const r = await
|
|
232
|
+
const r = await p3.multiselect({
|
|
233
233
|
message: "Bypass approval prompts for which agents? (security trade-off: the agent stops asking before acting)",
|
|
234
234
|
options: supported.map((a) => ({
|
|
235
235
|
value: a.name,
|
|
@@ -239,7 +239,7 @@ async function resolveBypassSelection(candidates, opts, interactive) {
|
|
|
239
239
|
initialValues: names.filter((n) => BYPASS_DEFAULT_ON.has(n)),
|
|
240
240
|
required: false
|
|
241
241
|
});
|
|
242
|
-
if (
|
|
242
|
+
if (p3.isCancel(r)) return [];
|
|
243
243
|
return r;
|
|
244
244
|
}
|
|
245
245
|
function applyBypass(agentNames, scope, dryRun) {
|
|
@@ -247,9 +247,9 @@ function applyBypass(agentNames, scope, dryRun) {
|
|
|
247
247
|
const res = enableFor(name, scope, dryRun);
|
|
248
248
|
if (!res) continue;
|
|
249
249
|
const label = AGENTS[name]?.label || name;
|
|
250
|
-
if (dryRun)
|
|
251
|
-
else if (res.changed)
|
|
252
|
-
else
|
|
250
|
+
if (dryRun) p3.log.info(`Bypass (dry run): would enable for ${label} -> ${res.path}.`);
|
|
251
|
+
else if (res.changed) p3.log.warn(`Bypass: enabled for ${label} (${res.path}) - approval prompts are now OFF.`);
|
|
252
|
+
else p3.log.info(`Bypass: already enabled for ${label}.`);
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
function enableFor(name, scope, dryRun) {
|
|
@@ -453,7 +453,7 @@ var init_config = __esm({
|
|
|
453
453
|
"use strict";
|
|
454
454
|
init_util();
|
|
455
455
|
CONFIG_FILE = ".enigma.json";
|
|
456
|
-
CONFIG_DEFAULTS = { commitEmoji: true, updateNotifier: true };
|
|
456
|
+
CONFIG_DEFAULTS = { commitEmoji: true, updateNotifier: true, fullscreen: true };
|
|
457
457
|
}
|
|
458
458
|
});
|
|
459
459
|
|
|
@@ -490,7 +490,8 @@ var init_settings_registry = __esm({
|
|
|
490
490
|
blurb: "enigma runtime toggles (.enigma.json)",
|
|
491
491
|
settings: [
|
|
492
492
|
enigmaToggle("commit-emoji", "commitEmoji", "Commit subject emoji", "leading gitmoji on commit subjects"),
|
|
493
|
-
enigmaToggle("update-notifier", "updateNotifier", "Update notifications", "notify when a newer enigma-cli is published")
|
|
493
|
+
enigmaToggle("update-notifier", "updateNotifier", "Update notifications", "notify when a newer enigma-cli is published"),
|
|
494
|
+
enigmaToggle("fullscreen", "fullscreen", "Full-screen TUI", "clear the screen for a clean TUI view; off renders inline among existing output")
|
|
494
495
|
]
|
|
495
496
|
},
|
|
496
497
|
{
|
|
@@ -530,44 +531,29 @@ __export(settings_exports, {
|
|
|
530
531
|
runHomeTui: () => runHomeTui,
|
|
531
532
|
runSettingsTui: () => runSettingsTui
|
|
532
533
|
});
|
|
533
|
-
async function runHomeTui(
|
|
534
|
-
await runTui(
|
|
534
|
+
async function runHomeTui(hub) {
|
|
535
|
+
await runTui({ showActions: true, hub });
|
|
535
536
|
}
|
|
536
537
|
async function runSettingsTui() {
|
|
537
|
-
await runTui(
|
|
538
|
+
await runTui({ showActions: false });
|
|
538
539
|
}
|
|
539
|
-
async function runTui(
|
|
540
|
+
async function runTui(opts) {
|
|
540
541
|
if (!process.stdout.isTTY) return;
|
|
542
|
+
const fullscreen = readConfig().config.fullscreen;
|
|
541
543
|
const React = (await import("react")).default;
|
|
542
544
|
const ink = await import("ink");
|
|
543
545
|
const { render } = ink;
|
|
544
546
|
const h = React.createElement;
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
for (; ; ) {
|
|
552
|
-
const state = { leave: "quit" };
|
|
553
|
-
const App = buildApp(React, ink, { initialView, onLeave: (a) => {
|
|
554
|
-
state.leave = a;
|
|
555
|
-
} });
|
|
556
|
-
process.stdout.write(ENTER_ALT);
|
|
557
|
-
process.on("exit", restore);
|
|
558
|
-
try {
|
|
559
|
-
const app = render(h(App), { exitOnCtrlC: true });
|
|
560
|
-
await app.waitUntilExit();
|
|
561
|
-
} finally {
|
|
562
|
-
process.removeListener("exit", restore);
|
|
563
|
-
restore();
|
|
564
|
-
}
|
|
565
|
-
if (runAction && (state.leave === "skills" || state.leave === "security")) {
|
|
566
|
-
await runAction(state.leave);
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
break;
|
|
547
|
+
const agents = opts.hub?.agents ?? [];
|
|
548
|
+
const protections = opts.hub?.protections ?? [];
|
|
549
|
+
const runAction = opts.hub?.runAction ?? (async () => ({ ok: false, title: "", lines: [] }));
|
|
550
|
+
if (fullscreen) try {
|
|
551
|
+
process.stdout.write(CLEAR_SCREEN);
|
|
552
|
+
} catch {
|
|
570
553
|
}
|
|
554
|
+
const App = buildApp(React, ink, { showActions: opts.showActions, fullscreen, agents, protections, runAction });
|
|
555
|
+
const app = render(h(App), { exitOnCtrlC: true });
|
|
556
|
+
await app.waitUntilExit();
|
|
571
557
|
}
|
|
572
558
|
function buildApp(React, ink, opts) {
|
|
573
559
|
const { useApp, useInput, useStdout } = ink;
|
|
@@ -575,18 +561,28 @@ function buildApp(React, ink, opts) {
|
|
|
575
561
|
const Text = ink.Text;
|
|
576
562
|
const { useState, useEffect } = React;
|
|
577
563
|
const h = React.createElement;
|
|
578
|
-
const
|
|
564
|
+
const fill = opts.fullscreen;
|
|
565
|
+
const sideItems = [
|
|
566
|
+
...CATEGORIES.map((c, i) => ({ kind: "category", catIndex: i, title: c.title })),
|
|
567
|
+
...opts.showActions ? ACTION_ITEMS.map((a) => ({ kind: "action", ...a })) : []
|
|
568
|
+
];
|
|
569
|
+
const actionItemKeys = (action) => action === "security" ? opts.protections.map((pr) => pr.value) : opts.agents.map((a) => a.name);
|
|
579
570
|
return function App() {
|
|
580
571
|
const { exit } = useApp();
|
|
581
572
|
const { stdout } = useStdout();
|
|
582
573
|
const [size, setSize] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
583
|
-
const [
|
|
584
|
-
const [homeIndex, setHomeIndex] = useState(0);
|
|
574
|
+
const [mode, setMode] = useState("menu");
|
|
585
575
|
const [scope, setScope] = useState("global");
|
|
586
|
-
const [
|
|
587
|
-
const [
|
|
576
|
+
const [sideIndex, setSideIndex] = useState(0);
|
|
577
|
+
const [focusRight, setFocusRight] = useState(false);
|
|
588
578
|
const [setIndex, setSetIndex] = useState(0);
|
|
589
|
-
const [,
|
|
579
|
+
const [pending, setPending] = useState({});
|
|
580
|
+
const [confirm4, setConfirm] = useState(null);
|
|
581
|
+
const [actCursor, setActCursor] = useState(0);
|
|
582
|
+
const [actChecked, setActChecked] = useState({});
|
|
583
|
+
const [busyTitle, setBusyTitle] = useState("");
|
|
584
|
+
const [result, setResult] = useState(null);
|
|
585
|
+
const [resultScroll, setResultScroll] = useState(0);
|
|
590
586
|
useEffect(() => {
|
|
591
587
|
const onResize = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
592
588
|
stdout.on("resize", onResize);
|
|
@@ -594,45 +590,112 @@ function buildApp(React, ink, opts) {
|
|
|
594
590
|
stdout.off("resize", onResize);
|
|
595
591
|
};
|
|
596
592
|
}, [stdout]);
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
593
|
+
const current = sideItems[sideIndex];
|
|
594
|
+
const category = current.kind === "category" ? CATEGORIES[current.catIndex] : null;
|
|
595
|
+
const action = current.kind === "action" ? current.action : null;
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
if (!action) return;
|
|
598
|
+
setActCursor(0);
|
|
599
|
+
if (action === "security") {
|
|
600
|
+
setActChecked(Object.fromEntries(opts.protections.map((pr) => [pr.value, true])));
|
|
601
|
+
} else {
|
|
602
|
+
const detected = opts.agents.filter((a) => a.installed);
|
|
603
|
+
const preselect = detected.length ? detected : opts.agents;
|
|
604
|
+
setActChecked(Object.fromEntries(opts.agents.map((a) => [a.name, preselect.some((d) => d.name === a.name)])));
|
|
602
605
|
}
|
|
606
|
+
}, [action]);
|
|
607
|
+
const resultRows = fill ? Math.max(3, size.rows - 7) : result?.lines.length ?? 0;
|
|
608
|
+
const maxResultScroll = Math.max(0, (result?.lines.length ?? 0) - resultRows);
|
|
609
|
+
const valueOf = (setting, sc) => {
|
|
610
|
+
const k = stageKey(setting.key, sc);
|
|
611
|
+
return k in pending ? pending[k] : setting.read(sc);
|
|
612
|
+
};
|
|
613
|
+
const isModified = (setting, sc) => {
|
|
614
|
+
const k = stageKey(setting.key, sc);
|
|
615
|
+
return k in pending && pending[k] !== setting.read(sc);
|
|
616
|
+
};
|
|
617
|
+
const dirty = Object.entries(pending).some(([k, v]) => {
|
|
618
|
+
const { key, scope: sc } = parseStageKey(k);
|
|
619
|
+
return SETTING_BY_KEY.get(key)?.read(sc) !== v;
|
|
620
|
+
});
|
|
621
|
+
const persistPending = () => {
|
|
622
|
+
for (const [k, v] of Object.entries(pending)) {
|
|
623
|
+
const { key, scope: sc } = parseStageKey(k);
|
|
624
|
+
const setting = SETTING_BY_KEY.get(key);
|
|
625
|
+
if (setting && setting.read(sc) !== v) setting.write(v, sc);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
const runChosen = (act) => {
|
|
629
|
+
const keys = actionItemKeys(act);
|
|
630
|
+
const chosen = keys.filter((k) => actChecked[k]);
|
|
631
|
+
const req = act === "security" ? { action: act, protections: chosen } : { action: act, scope, agents: chosen };
|
|
632
|
+
persistPending();
|
|
633
|
+
setPending({});
|
|
634
|
+
setBusyTitle(actionTitle(act));
|
|
635
|
+
setResult(null);
|
|
636
|
+
setResultScroll(0);
|
|
637
|
+
setMode("running");
|
|
638
|
+
opts.runAction(req).then((res) => {
|
|
639
|
+
setResult(res);
|
|
640
|
+
setMode("result");
|
|
641
|
+
}).catch((err) => {
|
|
642
|
+
setResult({ ok: false, title: actionTitle(act), lines: [`Error: ${err.message}`] });
|
|
643
|
+
setMode("result");
|
|
644
|
+
});
|
|
603
645
|
};
|
|
604
646
|
useInput((input, key) => {
|
|
605
|
-
if (
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
exit();
|
|
647
|
+
if (confirm4) {
|
|
648
|
+
if (key.escape) {
|
|
649
|
+
setConfirm(null);
|
|
609
650
|
return;
|
|
610
651
|
}
|
|
611
652
|
if (key.upArrow || input === "k") {
|
|
612
|
-
|
|
653
|
+
setConfirm((c) => c && { index: Math.max(0, c.index - 1) });
|
|
613
654
|
return;
|
|
614
655
|
}
|
|
615
656
|
if (key.downArrow || input === "j") {
|
|
616
|
-
|
|
657
|
+
setConfirm((c) => c && { index: Math.min(EXIT_OPTIONS.length - 1, c.index + 1) });
|
|
617
658
|
return;
|
|
618
659
|
}
|
|
619
660
|
if (key.return || input === " ") {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
661
|
+
const index = confirm4.index;
|
|
662
|
+
setConfirm(null);
|
|
663
|
+
if (index === 2) return;
|
|
664
|
+
if (index === 0) persistPending();
|
|
665
|
+
setPending({});
|
|
666
|
+
exit();
|
|
667
|
+
}
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (mode === "running") return;
|
|
671
|
+
if (mode === "result") {
|
|
672
|
+
if (key.return || key.escape || input === " " || input === "q") {
|
|
673
|
+
setMode("menu");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (key.upArrow || input === "k") {
|
|
677
|
+
setResultScroll((s) => Math.max(0, s - 1));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (key.downArrow || input === "j") {
|
|
681
|
+
setResultScroll((s) => Math.min(maxResultScroll, s + 1));
|
|
682
|
+
return;
|
|
631
683
|
}
|
|
632
684
|
return;
|
|
633
685
|
}
|
|
634
686
|
if (input === "q" || key.escape) {
|
|
635
|
-
|
|
687
|
+
dirty ? setConfirm({ index: 0 }) : exit();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (input === "s") {
|
|
691
|
+
persistPending();
|
|
692
|
+
setPending({});
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (input === "x") {
|
|
696
|
+
persistPending();
|
|
697
|
+
setPending({});
|
|
698
|
+
exit();
|
|
636
699
|
return;
|
|
637
700
|
}
|
|
638
701
|
if (input === "g") {
|
|
@@ -640,147 +703,632 @@ function buildApp(React, ink, opts) {
|
|
|
640
703
|
return;
|
|
641
704
|
}
|
|
642
705
|
if (key.tab) {
|
|
643
|
-
|
|
706
|
+
setFocusRight((f) => !f);
|
|
644
707
|
return;
|
|
645
708
|
}
|
|
646
709
|
if (key.leftArrow || input === "h") {
|
|
647
|
-
|
|
710
|
+
setFocusRight(false);
|
|
648
711
|
return;
|
|
649
712
|
}
|
|
650
713
|
if (key.rightArrow || input === "l") {
|
|
651
|
-
|
|
714
|
+
setFocusRight(true);
|
|
652
715
|
return;
|
|
653
716
|
}
|
|
654
717
|
if (key.upArrow || input === "k") {
|
|
655
|
-
if (
|
|
718
|
+
if (focusRight && category) setSetIndex((i) => Math.max(0, i - 1));
|
|
719
|
+
else if (focusRight && action) setActCursor((i) => Math.max(0, i - 1));
|
|
656
720
|
else {
|
|
657
|
-
|
|
721
|
+
setSideIndex((i) => Math.max(0, i - 1));
|
|
658
722
|
setSetIndex(0);
|
|
723
|
+
setFocusRight(false);
|
|
659
724
|
}
|
|
660
725
|
return;
|
|
661
726
|
}
|
|
662
727
|
if (key.downArrow || input === "j") {
|
|
663
|
-
if (
|
|
728
|
+
if (focusRight && category) setSetIndex((i) => Math.min(category.settings.length - 1, i + 1));
|
|
729
|
+
else if (focusRight && action) setActCursor((i) => Math.min(actionItemKeys(action).length - 1, i + 1));
|
|
664
730
|
else {
|
|
665
|
-
|
|
731
|
+
setSideIndex((i) => Math.min(sideItems.length - 1, i + 1));
|
|
666
732
|
setSetIndex(0);
|
|
733
|
+
setFocusRight(false);
|
|
667
734
|
}
|
|
668
735
|
return;
|
|
669
736
|
}
|
|
737
|
+
if (input === " " && focusRight && action) {
|
|
738
|
+
const k = actionItemKeys(action)[actCursor];
|
|
739
|
+
setActChecked((c) => ({ ...c, [k]: !c[k] }));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
670
742
|
if (key.return || input === " ") {
|
|
671
|
-
if (!
|
|
672
|
-
|
|
743
|
+
if (!focusRight) {
|
|
744
|
+
setFocusRight(true);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (action) {
|
|
748
|
+
runChosen(action);
|
|
673
749
|
return;
|
|
674
750
|
}
|
|
675
|
-
const setting =
|
|
676
|
-
|
|
677
|
-
redraw((n) => n + 1);
|
|
751
|
+
const setting = category.settings[setIndex];
|
|
752
|
+
setPending((pr) => ({ ...pr, [stageKey(setting.key, scope)]: !valueOf(setting, scope) }));
|
|
678
753
|
}
|
|
679
754
|
});
|
|
680
|
-
const
|
|
681
|
-
Box,
|
|
682
|
-
{ width: size.columns, paddingX: 1, justifyContent: "space-between" },
|
|
683
|
-
h(Text, { bold: true, color: "cyan" }, "enigma"),
|
|
684
|
-
right2
|
|
685
|
-
);
|
|
686
|
-
const body = view === "home" ? renderHome(h, Box, Text, homeIndex) : renderSettings(h, Box, Text, { size, scope, focusSettings, catIndex, setIndex });
|
|
687
|
-
const footer = h(Box, { width: size.columns, paddingX: 1 }, h(
|
|
688
|
-
Text,
|
|
689
|
-
{ dimColor: true },
|
|
690
|
-
view === "home" ? "up/down move enter select q quit" : `up/down move tab/left/right switch enter toggle g scope ${canGoHome ? "esc back" : "q quit"}`
|
|
691
|
-
));
|
|
692
|
-
const right = view === "home" ? h(Text, { dimColor: true }, "main menu") : h(
|
|
755
|
+
const headerRight = confirm4 ? h(Text, { color: "yellow" }, "unsaved changes") : mode === "running" ? h(Text, { dimColor: true }, "working") : mode === "result" ? h(Text, { dimColor: true }, "result") : h(
|
|
693
756
|
Box,
|
|
694
757
|
{},
|
|
695
758
|
h(Text, { dimColor: true }, "scope "),
|
|
696
759
|
h(Text, { bold: true, color: scope === "global" ? "green" : "yellow" }, scope),
|
|
697
|
-
h(Text, { dimColor: true }, "
|
|
760
|
+
h(Text, { dimColor: true }, " (g)"),
|
|
761
|
+
dirty ? h(Text, { color: "yellow" }, " * unsaved") : null
|
|
698
762
|
);
|
|
763
|
+
const titleBar = h(
|
|
764
|
+
Box,
|
|
765
|
+
{ width: size.columns, paddingX: 1, justifyContent: "space-between" },
|
|
766
|
+
h(Text, { bold: true, color: "cyan" }, "enigma"),
|
|
767
|
+
headerRight
|
|
768
|
+
);
|
|
769
|
+
let content;
|
|
770
|
+
if (confirm4) {
|
|
771
|
+
content = renderConfirm(h, Box, Text, confirm4.index, fill);
|
|
772
|
+
} else if (mode === "running") {
|
|
773
|
+
content = renderRunning(h, Box, Text, busyTitle, fill);
|
|
774
|
+
} else if (mode === "result" && result) {
|
|
775
|
+
content = renderResult(h, Box, Text, { res: result, scroll: Math.min(resultScroll, maxResultScroll), maxRows: resultRows, fill });
|
|
776
|
+
} else {
|
|
777
|
+
const sidebarWidth = Math.min(28, Math.max(20, Math.floor(size.columns * 0.3)));
|
|
778
|
+
const panel = category ? renderCategoryPanel(h, Box, Text, { category, scope, focusRight, setIndex, valueOf, isModified, fill }) : renderChecklist(h, Box, Text, {
|
|
779
|
+
title: actionTitle(action),
|
|
780
|
+
blurb: action === "skills" ? `Scope ${scope} (g to change). Choose agents, then enter to install.` : "Choose what the commit guard enforces, then enter to apply.",
|
|
781
|
+
items: action === "security" ? opts.protections.map((pr) => ({ key: pr.value, label: pr.label, hint: pr.hint })) : opts.agents.map((a) => ({ key: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
|
|
782
|
+
cursor: actCursor,
|
|
783
|
+
checked: actChecked,
|
|
784
|
+
focused: focusRight,
|
|
785
|
+
fill
|
|
786
|
+
});
|
|
787
|
+
content = h(Box, fill ? { flexGrow: 1 } : {}, renderSidebar(h, Box, Text, sideItems, sideIndex, focusRight, sidebarWidth), panel);
|
|
788
|
+
}
|
|
789
|
+
const menuFooter = focusRight && action ? action === "skills" ? "up/down move space toggle g scope enter install tab back" : "up/down move space toggle enter apply tab back" : focusRight && category ? "up/down move enter toggle g scope tab back" : `up/down move tab switch enter ${action ? "edit" : "focus"} s save x save & exit q quit`;
|
|
790
|
+
const footerText = confirm4 ? "up/down move enter select esc cancel" : mode === "running" ? "working..." : mode === "result" ? `${maxResultScroll > 0 ? "up/down scroll " : ""}enter / esc back to menu` : menuFooter;
|
|
791
|
+
const footer = h(Box, { width: size.columns, paddingX: 1 }, h(Text, { dimColor: true }, footerText));
|
|
699
792
|
return h(
|
|
700
793
|
Box,
|
|
701
|
-
{ width: size.columns, height: size.rows, flexDirection: "column" },
|
|
702
|
-
titleBar
|
|
703
|
-
|
|
794
|
+
{ width: size.columns, ...fill ? { height: size.rows } : {}, flexDirection: "column" },
|
|
795
|
+
titleBar,
|
|
796
|
+
content,
|
|
704
797
|
footer
|
|
705
798
|
);
|
|
706
799
|
};
|
|
707
800
|
}
|
|
708
|
-
function
|
|
709
|
-
|
|
801
|
+
function renderSidebar(h, Box, Text, items, index, focusRight, width) {
|
|
802
|
+
return h(Box, {
|
|
803
|
+
flexDirection: "column",
|
|
804
|
+
borderStyle: "round",
|
|
805
|
+
borderColor: focusRight ? "gray" : "cyan",
|
|
806
|
+
paddingX: 1,
|
|
807
|
+
width,
|
|
808
|
+
marginRight: 1
|
|
809
|
+
}, [
|
|
810
|
+
h(Text, { key: "__t", bold: true, dimColor: true }, "MENU"),
|
|
811
|
+
...items.map((it, i) => h(Text, {
|
|
812
|
+
key: String(i),
|
|
813
|
+
inverse: !focusRight && i === index,
|
|
814
|
+
color: i === index ? "cyan" : void 0
|
|
815
|
+
}, ` ${it.title} `))
|
|
816
|
+
]);
|
|
817
|
+
}
|
|
818
|
+
function renderConfirm(h, Box, Text, index, fill) {
|
|
819
|
+
return h(
|
|
710
820
|
Box,
|
|
711
|
-
{
|
|
712
|
-
h(
|
|
713
|
-
|
|
714
|
-
|
|
821
|
+
{ justifyContent: "center", ...fill ? { flexGrow: 1, alignItems: "center" } : {} },
|
|
822
|
+
h(Box, {
|
|
823
|
+
flexDirection: "column",
|
|
824
|
+
borderStyle: "round",
|
|
825
|
+
borderColor: "yellow",
|
|
826
|
+
paddingX: 2,
|
|
827
|
+
paddingY: 1
|
|
828
|
+
}, [
|
|
829
|
+
h(Text, { key: "__t", bold: true, color: "yellow" }, "You have unsaved changes"),
|
|
830
|
+
h(
|
|
831
|
+
Box,
|
|
832
|
+
{ key: "__o", marginTop: 1, flexDirection: "column" },
|
|
833
|
+
EXIT_OPTIONS.map((o, i) => h(Text, { key: o, inverse: i === index, bold: i === index }, ` ${o} `))
|
|
834
|
+
)
|
|
835
|
+
])
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
function renderRunning(h, Box, Text, title, fill) {
|
|
715
839
|
return h(Box, {
|
|
716
840
|
flexDirection: "column",
|
|
717
841
|
borderStyle: "round",
|
|
718
842
|
borderColor: "cyan",
|
|
719
843
|
paddingX: 1,
|
|
720
|
-
flexGrow: 1
|
|
844
|
+
...fill ? { flexGrow: 1 } : {}
|
|
721
845
|
}, [
|
|
722
|
-
h(Text, { key: "__t", bold: true, color: "cyan" },
|
|
723
|
-
h(
|
|
846
|
+
h(Text, { key: "__t", bold: true, color: "cyan" }, title || "Working"),
|
|
847
|
+
h(Text, { key: "__w", marginTop: 1, dimColor: true }, "Working..."),
|
|
848
|
+
...fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : []
|
|
724
849
|
]);
|
|
725
850
|
}
|
|
726
|
-
function
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
const
|
|
851
|
+
function renderResult(h, Box, Text, s) {
|
|
852
|
+
const { res, maxRows } = s;
|
|
853
|
+
const windowed = maxRows > 0 && res.lines.length > maxRows;
|
|
854
|
+
const start = windowed ? Math.max(0, Math.min(s.scroll, res.lines.length - maxRows)) : 0;
|
|
855
|
+
const slice = windowed ? res.lines.slice(start, start + maxRows) : res.lines;
|
|
856
|
+
const rows = slice.length ? slice.map((line, i) => h(Text, { key: String(start + i), wrap: "truncate-end" }, ` ${line} `)) : [h(Text, { key: "__none", dimColor: true }, " (no output) ")];
|
|
857
|
+
const above = windowed && start > 0;
|
|
858
|
+
const below = windowed && start + maxRows < res.lines.length;
|
|
859
|
+
return h(Box, {
|
|
732
860
|
flexDirection: "column",
|
|
733
861
|
borderStyle: "round",
|
|
734
|
-
borderColor:
|
|
862
|
+
borderColor: res.ok ? "green" : "red",
|
|
735
863
|
paddingX: 1,
|
|
736
|
-
|
|
737
|
-
marginRight: 1
|
|
864
|
+
...s.fill ? { flexGrow: 1 } : {}
|
|
738
865
|
}, [
|
|
739
|
-
h(Text, { key: "__t", bold: true,
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}, ` ${c.title} `))
|
|
866
|
+
h(Text, { key: "__t", bold: true, color: res.ok ? "green" : "red" }, res.title),
|
|
867
|
+
h(Text, { key: "__up", dimColor: true }, above ? ` ... ${start} more above ` : " "),
|
|
868
|
+
h(Box, { key: "__rows", flexDirection: "column" }, rows),
|
|
869
|
+
h(Text, { key: "__dn", dimColor: true }, below ? ` ... ${res.lines.length - start - maxRows} more below ` : " "),
|
|
870
|
+
...s.fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : []
|
|
745
871
|
]);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
872
|
+
}
|
|
873
|
+
function renderChecklist(h, Box, Text, s) {
|
|
874
|
+
const rows = s.items.map((it, i) => {
|
|
875
|
+
const on = !!s.checked[it.key];
|
|
876
|
+
const selected = s.focused && i === s.cursor;
|
|
877
|
+
return h(
|
|
878
|
+
Box,
|
|
879
|
+
{ key: it.key, justifyContent: "space-between" },
|
|
880
|
+
h(Text, { inverse: selected, bold: selected }, ` ${on ? "[x]" : "[ ]"} ${it.label} `),
|
|
881
|
+
h(Text, { dimColor: true }, `${it.hint} `)
|
|
882
|
+
);
|
|
883
|
+
});
|
|
884
|
+
return h(Box, {
|
|
885
|
+
flexDirection: "column",
|
|
886
|
+
borderStyle: "round",
|
|
887
|
+
borderColor: s.focused ? "cyan" : "gray",
|
|
888
|
+
paddingX: 1,
|
|
889
|
+
flexGrow: 1
|
|
890
|
+
}, [
|
|
891
|
+
h(Text, { key: "__t", bold: true, color: "cyan" }, s.title),
|
|
892
|
+
h(Text, { key: "__bl", dimColor: true }, s.blurb),
|
|
893
|
+
h(Box, { key: "__rows", marginTop: 1, flexDirection: "column" }, rows),
|
|
894
|
+
...s.fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : []
|
|
895
|
+
]);
|
|
896
|
+
}
|
|
897
|
+
function renderCategoryPanel(h, Box, Text, s) {
|
|
898
|
+
const focused = s.category.settings[s.setIndex];
|
|
899
|
+
const rows = s.category.settings.map((setting, i) => {
|
|
900
|
+
const on = s.valueOf(setting, s.scope);
|
|
901
|
+
const modified = s.isModified(setting, s.scope);
|
|
902
|
+
const selected = s.focusRight && i === s.setIndex;
|
|
749
903
|
return h(
|
|
750
904
|
Box,
|
|
751
905
|
{ key: setting.key, justifyContent: "space-between" },
|
|
752
906
|
h(Text, { inverse: selected, bold: selected }, ` ${setting.label}${setting.globalOnly ? " (global)" : ""} `),
|
|
753
|
-
h(Text, { bold: true, color: on ? "green" : "gray" }, `${valueLabel(on)} `)
|
|
907
|
+
h(Text, { bold: true, color: modified ? "yellow" : on ? "green" : "gray" }, `${valueLabel(on)}${modified ? " *" : ""} `)
|
|
754
908
|
);
|
|
755
909
|
});
|
|
756
|
-
|
|
910
|
+
return h(Box, {
|
|
757
911
|
flexDirection: "column",
|
|
758
912
|
borderStyle: "round",
|
|
759
|
-
borderColor: s.
|
|
913
|
+
borderColor: s.focusRight ? "cyan" : "gray",
|
|
760
914
|
paddingX: 1,
|
|
761
915
|
flexGrow: 1
|
|
762
916
|
}, [
|
|
763
|
-
h(Text, { key: "__b", bold: true, color: "cyan" }, category.title),
|
|
764
|
-
h(Text, { key: "__bl", dimColor: true }, category.blurb),
|
|
917
|
+
h(Text, { key: "__b", bold: true, color: "cyan" }, s.category.title),
|
|
918
|
+
h(Text, { key: "__bl", dimColor: true }, s.category.blurb),
|
|
765
919
|
h(Box, { key: "__rows", marginTop: 1, flexDirection: "column" }, rows),
|
|
766
|
-
h(Box, { key: "__grow", flexGrow: 1 }),
|
|
767
|
-
h(Text, { key: "__hint", dimColor: true, wrap: "truncate-end" }, focused.hint)
|
|
920
|
+
...s.fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : [],
|
|
921
|
+
h(Text, { key: "__hint", marginTop: 1, dimColor: true, wrap: "truncate-end" }, focused.hint)
|
|
768
922
|
]);
|
|
769
|
-
return h(Box, { flexGrow: 1 }, sidebar, panel);
|
|
770
923
|
}
|
|
771
|
-
var
|
|
924
|
+
var CLEAR_SCREEN, SETTING_BY_KEY, stageKey, parseStageKey, ACTION_ITEMS, actionTitle, EXIT_OPTIONS;
|
|
772
925
|
var init_settings = __esm({
|
|
773
926
|
"src/tui/settings.ts"() {
|
|
927
|
+
"use strict";
|
|
928
|
+
init_config();
|
|
929
|
+
init_settings_registry();
|
|
930
|
+
CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
931
|
+
SETTING_BY_KEY = new Map(ALL_SETTINGS.map((s) => [s.key, s]));
|
|
932
|
+
stageKey = (key, scope) => `${scope}/${key}`;
|
|
933
|
+
parseStageKey = (composite) => {
|
|
934
|
+
const i = composite.indexOf("/");
|
|
935
|
+
return { scope: composite.slice(0, i), key: composite.slice(i + 1) };
|
|
936
|
+
};
|
|
937
|
+
ACTION_ITEMS = [
|
|
938
|
+
{ action: "skills", title: "Install agent skills", blurb: "Claude Code, Codex, OpenCode" },
|
|
939
|
+
{ action: "security", title: "Git security hooks", blurb: "block secrets, .env, node_modules on commit" }
|
|
940
|
+
];
|
|
941
|
+
actionTitle = (action) => ACTION_ITEMS.find((a) => a.action === action).title;
|
|
942
|
+
EXIT_OPTIONS = ["Save & exit", "Exit without saving", "Cancel"];
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// src/tui/opentui.ts
|
|
947
|
+
var opentui_exports = {};
|
|
948
|
+
__export(opentui_exports, {
|
|
949
|
+
runHomeTui: () => runHomeTui2
|
|
950
|
+
});
|
|
951
|
+
async function runHomeTui2(hub) {
|
|
952
|
+
if (!process.stdout.isTTY) return;
|
|
953
|
+
const React = (await import("react")).default;
|
|
954
|
+
const { createCliRenderer, TextAttributes } = await import("@opentui/core");
|
|
955
|
+
const { createRoot, useKeyboard, useTerminalDimensions } = await import("@opentui/react");
|
|
956
|
+
const h = React.createElement;
|
|
957
|
+
const { useState, useEffect } = React;
|
|
958
|
+
const box = "box";
|
|
959
|
+
const text = "text";
|
|
960
|
+
const BOLD = TextAttributes.BOLD;
|
|
961
|
+
const DIM = TextAttributes.DIM;
|
|
962
|
+
const agents = hub.agents;
|
|
963
|
+
const protections = hub.protections;
|
|
964
|
+
const txt = (content, props = {}) => h(text, props, content);
|
|
965
|
+
const selStyle = (selected, normal = {}) => selected ? { bg: SEL_BG, fg: SEL_FG, attributes: BOLD } : normal;
|
|
966
|
+
const panelBox = (borderColor, children, extra = {}) => h(box, { border: true, borderStyle: "rounded", borderColor, flexDirection: "column", paddingLeft: 1, paddingRight: 1, flexGrow: 1, ...extra }, ...children);
|
|
967
|
+
const renderSidebar2 = (items, index, focusRight, width) => h(
|
|
968
|
+
box,
|
|
969
|
+
{ border: true, borderStyle: "rounded", borderColor: focusRight ? COL.gray : COL.cyan, flexDirection: "column", paddingLeft: 1, paddingRight: 1, width, marginRight: 1 },
|
|
970
|
+
txt("MENU", { fg: COL.gray, attributes: BOLD }),
|
|
971
|
+
...items.map((it, i) => txt(
|
|
972
|
+
` ${it.title} `,
|
|
973
|
+
!focusRight && i === index ? { bg: SEL_BG, fg: SEL_FG, attributes: BOLD } : { fg: i === index ? COL.cyan : void 0 }
|
|
974
|
+
))
|
|
975
|
+
);
|
|
976
|
+
const renderChecklist2 = (s) => panelBox(s.focused ? COL.cyan : COL.gray, [
|
|
977
|
+
txt(s.title, { fg: COL.cyan, attributes: BOLD }),
|
|
978
|
+
txt(s.blurb, { fg: COL.gray }),
|
|
979
|
+
h(
|
|
980
|
+
box,
|
|
981
|
+
{ flexDirection: "column", marginTop: 1 },
|
|
982
|
+
...s.items.map((it, i) => {
|
|
983
|
+
const on = !!s.checked[it.key];
|
|
984
|
+
const selected = s.focused && i === s.cursor;
|
|
985
|
+
return h(
|
|
986
|
+
box,
|
|
987
|
+
{ flexDirection: "row", justifyContent: "space-between" },
|
|
988
|
+
txt(` ${on ? "[x]" : "[ ]"} ${it.label} `, selStyle(selected)),
|
|
989
|
+
txt(`${it.hint} `, { fg: COL.gray })
|
|
990
|
+
);
|
|
991
|
+
})
|
|
992
|
+
)
|
|
993
|
+
]);
|
|
994
|
+
const renderCategoryPanel2 = (s) => {
|
|
995
|
+
const focusedHint = s.category.settings[s.setIndex].hint;
|
|
996
|
+
return panelBox(s.focusRight ? COL.cyan : COL.gray, [
|
|
997
|
+
txt(s.category.title, { fg: COL.cyan, attributes: BOLD }),
|
|
998
|
+
txt(s.category.blurb, { fg: COL.gray }),
|
|
999
|
+
h(
|
|
1000
|
+
box,
|
|
1001
|
+
{ flexDirection: "column", marginTop: 1 },
|
|
1002
|
+
...s.category.settings.map((setting, i) => {
|
|
1003
|
+
const on = s.valueOf(setting, s.scope);
|
|
1004
|
+
const modified = s.isModified(setting, s.scope);
|
|
1005
|
+
const selected = s.focusRight && i === s.setIndex;
|
|
1006
|
+
return h(
|
|
1007
|
+
box,
|
|
1008
|
+
{ flexDirection: "row", justifyContent: "space-between" },
|
|
1009
|
+
txt(` ${setting.label}${setting.globalOnly ? " (global)" : ""} `, selStyle(selected)),
|
|
1010
|
+
txt(`${valueLabel(on)}${modified ? " *" : ""} `, { attributes: BOLD, fg: modified ? COL.yellow : on ? COL.green : COL.gray })
|
|
1011
|
+
);
|
|
1012
|
+
})
|
|
1013
|
+
),
|
|
1014
|
+
h(box, { flexGrow: 1 }),
|
|
1015
|
+
txt(focusedHint, { fg: COL.gray, marginTop: 1, truncate: true })
|
|
1016
|
+
]);
|
|
1017
|
+
};
|
|
1018
|
+
const renderConfirm2 = (index) => h(
|
|
1019
|
+
box,
|
|
1020
|
+
{ flexGrow: 1, justifyContent: "center", alignItems: "center" },
|
|
1021
|
+
h(
|
|
1022
|
+
box,
|
|
1023
|
+
{ border: true, borderStyle: "rounded", borderColor: COL.yellow, flexDirection: "column", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1 },
|
|
1024
|
+
txt("You have unsaved changes", { fg: COL.yellow, attributes: BOLD }),
|
|
1025
|
+
h(
|
|
1026
|
+
box,
|
|
1027
|
+
{ flexDirection: "column", marginTop: 1 },
|
|
1028
|
+
...EXIT_OPTIONS2.map((o, i) => txt(` ${o} `, selStyle(i === index)))
|
|
1029
|
+
)
|
|
1030
|
+
)
|
|
1031
|
+
);
|
|
1032
|
+
const renderRunning2 = (title) => panelBox(COL.cyan, [
|
|
1033
|
+
txt(title || "Working", { fg: COL.cyan, attributes: BOLD }),
|
|
1034
|
+
txt("Working...", { fg: COL.gray, marginTop: 1 }),
|
|
1035
|
+
h(box, { flexGrow: 1 })
|
|
1036
|
+
]);
|
|
1037
|
+
const renderResult2 = (res, scroll, maxRows) => {
|
|
1038
|
+
const windowed = maxRows > 0 && res.lines.length > maxRows;
|
|
1039
|
+
const start = windowed ? Math.max(0, Math.min(scroll, res.lines.length - maxRows)) : 0;
|
|
1040
|
+
const slice = windowed ? res.lines.slice(start, start + maxRows) : res.lines;
|
|
1041
|
+
const above = windowed && start > 0;
|
|
1042
|
+
const below = windowed && start + maxRows < res.lines.length;
|
|
1043
|
+
const rows = slice.length ? slice.map((line, i) => txt(` ${line} `, { key: String(start + i), truncate: true })) : [txt(" (no output) ", { fg: COL.gray })];
|
|
1044
|
+
return panelBox(res.ok ? COL.green : COL.red, [
|
|
1045
|
+
txt(res.title, { fg: res.ok ? COL.green : COL.red, attributes: BOLD }),
|
|
1046
|
+
txt(above ? ` ... ${start} more above ` : " ", { fg: COL.gray }),
|
|
1047
|
+
h(box, { flexDirection: "column" }, ...rows),
|
|
1048
|
+
txt(below ? ` ... ${res.lines.length - start - maxRows} more below ` : " ", { fg: COL.gray }),
|
|
1049
|
+
h(box, { flexGrow: 1 })
|
|
1050
|
+
]);
|
|
1051
|
+
};
|
|
1052
|
+
const sideItems = [
|
|
1053
|
+
...CATEGORIES.map((c, i) => ({ kind: "category", catIndex: i, title: c.title })),
|
|
1054
|
+
...ACTION_ITEMS2.map((a) => ({ kind: "action", ...a }))
|
|
1055
|
+
];
|
|
1056
|
+
const actionItemKeys = (action) => action === "security" ? protections.map((p7) => p7.value) : agents.map((a) => a.name);
|
|
1057
|
+
function App({ onExit }) {
|
|
1058
|
+
const dims = useTerminalDimensions();
|
|
1059
|
+
const size = { columns: dims.width || 80, rows: dims.height || 24 };
|
|
1060
|
+
const [mode, setMode] = useState("menu");
|
|
1061
|
+
const [scope, setScope] = useState("global");
|
|
1062
|
+
const [sideIndex, setSideIndex] = useState(0);
|
|
1063
|
+
const [focusRight, setFocusRight] = useState(false);
|
|
1064
|
+
const [setIndex, setSetIndex] = useState(0);
|
|
1065
|
+
const [pending, setPending] = useState({});
|
|
1066
|
+
const [confirm4, setConfirm] = useState(null);
|
|
1067
|
+
const [actCursor, setActCursor] = useState(0);
|
|
1068
|
+
const [actChecked, setActChecked] = useState({});
|
|
1069
|
+
const [busyTitle, setBusyTitle] = useState("");
|
|
1070
|
+
const [result, setResult] = useState(null);
|
|
1071
|
+
const [resultScroll, setResultScroll] = useState(0);
|
|
1072
|
+
const current = sideItems[sideIndex];
|
|
1073
|
+
const category = current.kind === "category" ? CATEGORIES[current.catIndex] : null;
|
|
1074
|
+
const action = current.kind === "action" ? current.action : null;
|
|
1075
|
+
useEffect(() => {
|
|
1076
|
+
if (!action) return;
|
|
1077
|
+
setActCursor(0);
|
|
1078
|
+
if (action === "security") {
|
|
1079
|
+
setActChecked(Object.fromEntries(protections.map((p7) => [p7.value, true])));
|
|
1080
|
+
} else {
|
|
1081
|
+
const detected = agents.filter((a) => a.installed);
|
|
1082
|
+
const preselect = detected.length ? detected : agents;
|
|
1083
|
+
setActChecked(Object.fromEntries(agents.map((a) => [a.name, preselect.some((d) => d.name === a.name)])));
|
|
1084
|
+
}
|
|
1085
|
+
}, [action]);
|
|
1086
|
+
const resultRows = Math.max(3, size.rows - 7);
|
|
1087
|
+
const maxResultScroll = Math.max(0, (result?.lines.length ?? 0) - resultRows);
|
|
1088
|
+
const valueOf = (setting, sc) => {
|
|
1089
|
+
const k = stageKey2(setting.key, sc);
|
|
1090
|
+
return k in pending ? pending[k] : setting.read(sc);
|
|
1091
|
+
};
|
|
1092
|
+
const isModified = (setting, sc) => {
|
|
1093
|
+
const k = stageKey2(setting.key, sc);
|
|
1094
|
+
return k in pending && pending[k] !== setting.read(sc);
|
|
1095
|
+
};
|
|
1096
|
+
const dirty = Object.entries(pending).some(([k, v]) => {
|
|
1097
|
+
const { key, scope: sc } = parseStageKey2(k);
|
|
1098
|
+
return SETTING_BY_KEY2.get(key)?.read(sc) !== v;
|
|
1099
|
+
});
|
|
1100
|
+
const persistPending = () => {
|
|
1101
|
+
for (const [k, v] of Object.entries(pending)) {
|
|
1102
|
+
const { key, scope: sc } = parseStageKey2(k);
|
|
1103
|
+
const setting = SETTING_BY_KEY2.get(key);
|
|
1104
|
+
if (setting && setting.read(sc) !== v) setting.write(v, sc);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
const runChosen = (act) => {
|
|
1108
|
+
const chosen = actionItemKeys(act).filter((k) => actChecked[k]);
|
|
1109
|
+
const req = act === "security" ? { action: act, protections: chosen } : { action: act, scope, agents: chosen };
|
|
1110
|
+
persistPending();
|
|
1111
|
+
setPending({});
|
|
1112
|
+
setBusyTitle(actionTitle2(act));
|
|
1113
|
+
setResult(null);
|
|
1114
|
+
setResultScroll(0);
|
|
1115
|
+
setMode("running");
|
|
1116
|
+
hub.runAction(req).then((res) => {
|
|
1117
|
+
setResult(res);
|
|
1118
|
+
setMode("result");
|
|
1119
|
+
}).catch((err) => {
|
|
1120
|
+
setResult({ ok: false, title: actionTitle2(act), lines: [`Error: ${err.message}`] });
|
|
1121
|
+
setMode("result");
|
|
1122
|
+
});
|
|
1123
|
+
};
|
|
1124
|
+
useKeyboard((key) => {
|
|
1125
|
+
const name = key.name;
|
|
1126
|
+
const up = name === "up", down = name === "down", left = name === "left", right = name === "right";
|
|
1127
|
+
const enter = name === "return", esc = name === "escape", tab = name === "tab", space = name === "space";
|
|
1128
|
+
const ch = name && name.length === 1 ? name : "";
|
|
1129
|
+
if (confirm4) {
|
|
1130
|
+
if (esc) {
|
|
1131
|
+
setConfirm(null);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (up || ch === "k") {
|
|
1135
|
+
setConfirm((c) => c && { index: Math.max(0, c.index - 1) });
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (down || ch === "j") {
|
|
1139
|
+
setConfirm((c) => c && { index: Math.min(EXIT_OPTIONS2.length - 1, c.index + 1) });
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (enter || space) {
|
|
1143
|
+
const index = confirm4.index;
|
|
1144
|
+
setConfirm(null);
|
|
1145
|
+
if (index === 2) return;
|
|
1146
|
+
if (index === 0) persistPending();
|
|
1147
|
+
setPending({});
|
|
1148
|
+
onExit();
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (mode === "running") return;
|
|
1153
|
+
if (mode === "result") {
|
|
1154
|
+
if (enter || esc || space || ch === "q") {
|
|
1155
|
+
setMode("menu");
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (up || ch === "k") {
|
|
1159
|
+
setResultScroll((s) => Math.max(0, s - 1));
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (down || ch === "j") {
|
|
1163
|
+
setResultScroll((s) => Math.min(maxResultScroll, s + 1));
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (ch === "q" || esc) {
|
|
1169
|
+
dirty ? setConfirm({ index: 0 }) : onExit();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
if (ch === "s") {
|
|
1173
|
+
persistPending();
|
|
1174
|
+
setPending({});
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (ch === "x") {
|
|
1178
|
+
persistPending();
|
|
1179
|
+
setPending({});
|
|
1180
|
+
onExit();
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (ch === "g") {
|
|
1184
|
+
setScope((s) => s === "global" ? "local" : "global");
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (tab) {
|
|
1188
|
+
setFocusRight((f) => !f);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (left || ch === "h") {
|
|
1192
|
+
setFocusRight(false);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
if (right || ch === "l") {
|
|
1196
|
+
setFocusRight(true);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (up || ch === "k") {
|
|
1200
|
+
if (focusRight && category) setSetIndex((i) => Math.max(0, i - 1));
|
|
1201
|
+
else if (focusRight && action) setActCursor((i) => Math.max(0, i - 1));
|
|
1202
|
+
else {
|
|
1203
|
+
setSideIndex((i) => Math.max(0, i - 1));
|
|
1204
|
+
setSetIndex(0);
|
|
1205
|
+
setFocusRight(false);
|
|
1206
|
+
}
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (down || ch === "j") {
|
|
1210
|
+
if (focusRight && category) setSetIndex((i) => Math.min(category.settings.length - 1, i + 1));
|
|
1211
|
+
else if (focusRight && action) setActCursor((i) => Math.min(actionItemKeys(action).length - 1, i + 1));
|
|
1212
|
+
else {
|
|
1213
|
+
setSideIndex((i) => Math.min(sideItems.length - 1, i + 1));
|
|
1214
|
+
setSetIndex(0);
|
|
1215
|
+
setFocusRight(false);
|
|
1216
|
+
}
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (space && focusRight && action) {
|
|
1220
|
+
const k = actionItemKeys(action)[actCursor];
|
|
1221
|
+
setActChecked((c) => ({ ...c, [k]: !c[k] }));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (enter || space) {
|
|
1225
|
+
if (!focusRight) {
|
|
1226
|
+
setFocusRight(true);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (action) {
|
|
1230
|
+
runChosen(action);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const setting = category.settings[setIndex];
|
|
1234
|
+
setPending((p7) => ({ ...p7, [stageKey2(setting.key, scope)]: !valueOf(setting, scope) }));
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
const headerRight = confirm4 ? txt("unsaved changes", { fg: COL.yellow }) : mode === "running" ? txt("working", { fg: COL.gray }) : mode === "result" ? txt("result", { fg: COL.gray }) : h(
|
|
1238
|
+
box,
|
|
1239
|
+
{ flexDirection: "row" },
|
|
1240
|
+
txt("scope ", { fg: COL.gray }),
|
|
1241
|
+
txt(scope, { fg: scope === "global" ? COL.green : COL.yellow, attributes: BOLD }),
|
|
1242
|
+
txt(" (g)", { fg: COL.gray }),
|
|
1243
|
+
dirty ? txt(" * unsaved", { fg: COL.yellow }) : null
|
|
1244
|
+
);
|
|
1245
|
+
const titleBar = h(
|
|
1246
|
+
box,
|
|
1247
|
+
{ width: size.columns, flexDirection: "row", paddingLeft: 1, paddingRight: 1, justifyContent: "space-between" },
|
|
1248
|
+
txt("enigma", { fg: COL.cyan, attributes: BOLD }),
|
|
1249
|
+
headerRight
|
|
1250
|
+
);
|
|
1251
|
+
let content;
|
|
1252
|
+
if (confirm4) {
|
|
1253
|
+
content = renderConfirm2(confirm4.index);
|
|
1254
|
+
} else if (mode === "running") {
|
|
1255
|
+
content = renderRunning2(busyTitle);
|
|
1256
|
+
} else if (mode === "result" && result) {
|
|
1257
|
+
content = renderResult2(result, Math.min(resultScroll, maxResultScroll), resultRows);
|
|
1258
|
+
} else {
|
|
1259
|
+
const sidebarWidth = Math.min(28, Math.max(20, Math.floor(size.columns * 0.3)));
|
|
1260
|
+
const panel = category ? renderCategoryPanel2({ category, scope, focusRight, setIndex, valueOf, isModified }) : renderChecklist2({
|
|
1261
|
+
title: actionTitle2(action),
|
|
1262
|
+
blurb: action === "skills" ? `Scope ${scope} (g to change). Choose agents, then enter to install.` : "Choose what the commit guard enforces, then enter to apply.",
|
|
1263
|
+
items: action === "security" ? protections.map((p7) => ({ key: p7.value, label: p7.label, hint: p7.hint })) : agents.map((a) => ({ key: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
|
|
1264
|
+
cursor: actCursor,
|
|
1265
|
+
checked: actChecked,
|
|
1266
|
+
focused: focusRight
|
|
1267
|
+
});
|
|
1268
|
+
content = h(box, { flexGrow: 1, flexDirection: "row" }, renderSidebar2(sideItems, sideIndex, focusRight, sidebarWidth), panel);
|
|
1269
|
+
}
|
|
1270
|
+
const footerLine = (s) => h(box, { width: size.columns, paddingLeft: 1, paddingRight: 1 }, txt(s, { fg: COL.gray, attributes: DIM }));
|
|
1271
|
+
const menuNav = focusRight && action ? action === "skills" ? "up/down move space toggle g scope enter install tab back" : "up/down move space toggle enter apply tab back" : focusRight && category ? "up/down move enter toggle g scope tab back" : `up/down move tab switch enter ${action ? "edit" : "focus"}`;
|
|
1272
|
+
let footer;
|
|
1273
|
+
if (confirm4) {
|
|
1274
|
+
footer = footerLine("up/down move enter select esc cancel");
|
|
1275
|
+
} else if (mode === "running") {
|
|
1276
|
+
footer = footerLine("working...");
|
|
1277
|
+
} else if (mode === "result") {
|
|
1278
|
+
footer = footerLine(`${maxResultScroll > 0 ? "up/down scroll " : ""}enter / esc back to menu`);
|
|
1279
|
+
} else {
|
|
1280
|
+
footer = h(
|
|
1281
|
+
box,
|
|
1282
|
+
{ width: size.columns, flexDirection: "row", justifyContent: "space-between", paddingLeft: 1, paddingRight: 1 },
|
|
1283
|
+
txt(menuNav, { fg: COL.gray, attributes: DIM }),
|
|
1284
|
+
txt("s save x save & exit q quit", { fg: COL.gray, attributes: DIM })
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
return h(box, { width: size.columns, height: size.rows, flexDirection: "column" }, titleBar, content, footer);
|
|
1288
|
+
}
|
|
1289
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
1290
|
+
await new Promise((resolve3) => {
|
|
1291
|
+
const root = createRoot(renderer);
|
|
1292
|
+
const onExit = () => {
|
|
1293
|
+
try {
|
|
1294
|
+
root.unmount();
|
|
1295
|
+
} catch {
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
renderer.destroy();
|
|
1299
|
+
} catch {
|
|
1300
|
+
}
|
|
1301
|
+
resolve3();
|
|
1302
|
+
};
|
|
1303
|
+
root.render(h(App, { onExit }));
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
var COL, SEL_BG, SEL_FG, SETTING_BY_KEY2, stageKey2, parseStageKey2, ACTION_ITEMS2, actionTitle2, EXIT_OPTIONS2;
|
|
1307
|
+
var init_opentui = __esm({
|
|
1308
|
+
"src/tui/opentui.ts"() {
|
|
774
1309
|
"use strict";
|
|
775
1310
|
init_settings_registry();
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1311
|
+
COL = {
|
|
1312
|
+
cyan: "#22d3ee",
|
|
1313
|
+
green: "#22c55e",
|
|
1314
|
+
yellow: "#eab308",
|
|
1315
|
+
red: "#ef4444",
|
|
1316
|
+
gray: "#6b7280"
|
|
1317
|
+
};
|
|
1318
|
+
SEL_BG = "#155e75";
|
|
1319
|
+
SEL_FG = "#ffffff";
|
|
1320
|
+
SETTING_BY_KEY2 = new Map(ALL_SETTINGS.map((s) => [s.key, s]));
|
|
1321
|
+
stageKey2 = (key, scope) => `${scope}/${key}`;
|
|
1322
|
+
parseStageKey2 = (composite) => {
|
|
1323
|
+
const i = composite.indexOf("/");
|
|
1324
|
+
return { scope: composite.slice(0, i), key: composite.slice(i + 1) };
|
|
1325
|
+
};
|
|
1326
|
+
ACTION_ITEMS2 = [
|
|
1327
|
+
{ action: "skills", title: "Install agent skills", blurb: "Claude Code, Codex, OpenCode" },
|
|
1328
|
+
{ action: "security", title: "Git security hooks", blurb: "block secrets, .env, node_modules on commit" }
|
|
783
1329
|
];
|
|
1330
|
+
actionTitle2 = (action) => ACTION_ITEMS2.find((a) => a.action === action).title;
|
|
1331
|
+
EXIT_OPTIONS2 = ["Save & exit", "Exit without saving", "Cancel"];
|
|
784
1332
|
}
|
|
785
1333
|
});
|
|
786
1334
|
|
|
@@ -788,7 +1336,52 @@ var init_settings = __esm({
|
|
|
788
1336
|
init_util();
|
|
789
1337
|
import { dirname as dirname4, join as join10 } from "path";
|
|
790
1338
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
791
|
-
import * as
|
|
1339
|
+
import * as p6 from "@clack/prompts";
|
|
1340
|
+
|
|
1341
|
+
// src/runtime.ts
|
|
1342
|
+
var isBun = () => typeof process.versions.bun === "string";
|
|
1343
|
+
|
|
1344
|
+
// src/reporter.ts
|
|
1345
|
+
import * as p from "@clack/prompts";
|
|
1346
|
+
function clackReporter() {
|
|
1347
|
+
return {
|
|
1348
|
+
info: (m) => p.log.info(m),
|
|
1349
|
+
success: (m) => p.log.success(m),
|
|
1350
|
+
warn: (m) => p.log.warn(m),
|
|
1351
|
+
error: (m) => p.log.error(m),
|
|
1352
|
+
note: (body, title) => p.note(body, title),
|
|
1353
|
+
spinner: () => {
|
|
1354
|
+
const s = p.spinner();
|
|
1355
|
+
return { start: (m) => s.start(m), stop: (m) => s.stop(m) };
|
|
1356
|
+
},
|
|
1357
|
+
fatal: (m) => {
|
|
1358
|
+
p.cancel(m);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function collectReporter() {
|
|
1364
|
+
const lines = [];
|
|
1365
|
+
const push = (prefix, message) => {
|
|
1366
|
+
for (const line of message.split("\n")) lines.push(prefix ? `${prefix} ${line}` : line);
|
|
1367
|
+
};
|
|
1368
|
+
return {
|
|
1369
|
+
lines,
|
|
1370
|
+
info: (m) => push("", m),
|
|
1371
|
+
success: (m) => push("", m),
|
|
1372
|
+
warn: (m) => push("!", m),
|
|
1373
|
+
error: (m) => push("x", m),
|
|
1374
|
+
note: (body, title) => {
|
|
1375
|
+
if (title) lines.push(title);
|
|
1376
|
+
push(" ", body);
|
|
1377
|
+
},
|
|
1378
|
+
spinner: () => ({ start: () => {
|
|
1379
|
+
}, stop: (m) => push("", m) }),
|
|
1380
|
+
fatal: (m) => {
|
|
1381
|
+
throw new Error(m);
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
792
1385
|
|
|
793
1386
|
// src/skills.ts
|
|
794
1387
|
init_util();
|
|
@@ -797,7 +1390,7 @@ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3,
|
|
|
797
1390
|
import { dirname as dirname2, join as join6, resolve as resolve2, relative as relative2, sep as sep2 } from "path";
|
|
798
1391
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
799
1392
|
import { createHash } from "crypto";
|
|
800
|
-
import * as
|
|
1393
|
+
import * as p4 from "@clack/prompts";
|
|
801
1394
|
|
|
802
1395
|
// src/security.ts
|
|
803
1396
|
init_util();
|
|
@@ -805,7 +1398,7 @@ import { existsSync as existsSync3, mkdirSync, cpSync, writeFileSync, chmodSync
|
|
|
805
1398
|
import { dirname, join as join3, resolve, relative } from "path";
|
|
806
1399
|
import { fileURLToPath } from "url";
|
|
807
1400
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
808
|
-
import * as
|
|
1401
|
+
import * as p2 from "@clack/prompts";
|
|
809
1402
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
810
1403
|
function findGuardSrc() {
|
|
811
1404
|
const candidates = [
|
|
@@ -838,41 +1431,41 @@ function currentHooksPath(root) {
|
|
|
838
1431
|
return "";
|
|
839
1432
|
}
|
|
840
1433
|
}
|
|
841
|
-
async function setupGitHooks(opts, interactive) {
|
|
1434
|
+
async function setupGitHooks(opts, interactive, reporter = clackReporter()) {
|
|
842
1435
|
const root = findGitRoot(process.cwd());
|
|
843
1436
|
if (!root) {
|
|
844
|
-
|
|
1437
|
+
reporter.error("Not inside a git repository (no .git found). Run this from your project root.");
|
|
845
1438
|
return false;
|
|
846
1439
|
}
|
|
847
1440
|
const guardSrc = findGuardSrc();
|
|
848
1441
|
if (!guardSrc) {
|
|
849
|
-
|
|
1442
|
+
reporter.error("Cannot find the built guard (dist/guard.js). Run 'npm run build' first.");
|
|
850
1443
|
return false;
|
|
851
1444
|
}
|
|
852
1445
|
const current = currentHooksPath(root);
|
|
853
1446
|
if (current && current !== ".githooks" && !opts.force) {
|
|
854
|
-
|
|
1447
|
+
reporter.warn(`core.hooksPath is already set to '${current}'.`);
|
|
855
1448
|
if (interactive) {
|
|
856
|
-
const ok = await
|
|
857
|
-
if (
|
|
858
|
-
|
|
1449
|
+
const ok = await p2.confirm({ message: `Override existing core.hooksPath '${current}' with '.githooks'?` });
|
|
1450
|
+
if (p2.isCancel(ok) || !ok) {
|
|
1451
|
+
reporter.info("Left git hooks unchanged.");
|
|
859
1452
|
return false;
|
|
860
1453
|
}
|
|
861
1454
|
} else {
|
|
862
|
-
|
|
1455
|
+
reporter.info("Re-run with --force to override.");
|
|
863
1456
|
return false;
|
|
864
1457
|
}
|
|
865
1458
|
}
|
|
866
1459
|
let enabled = opts.protections;
|
|
867
1460
|
if (!enabled && interactive) {
|
|
868
|
-
const r = await
|
|
1461
|
+
const r = await p2.multiselect({
|
|
869
1462
|
message: "Which protections should the commit guard enforce?",
|
|
870
1463
|
options: GUARD_PROTECTIONS,
|
|
871
1464
|
initialValues: GUARD_PROTECTIONS.map((o) => o.value),
|
|
872
1465
|
required: true
|
|
873
1466
|
});
|
|
874
|
-
if (
|
|
875
|
-
|
|
1467
|
+
if (p2.isCancel(r)) {
|
|
1468
|
+
reporter.info("Left git hooks unchanged.");
|
|
876
1469
|
return false;
|
|
877
1470
|
}
|
|
878
1471
|
enabled = r;
|
|
@@ -903,14 +1496,14 @@ async function setupGitHooks(opts, interactive) {
|
|
|
903
1496
|
try {
|
|
904
1497
|
execFileSync2("git", ["-C", root, "config", "core.hooksPath", ".githooks"]);
|
|
905
1498
|
} catch (err) {
|
|
906
|
-
|
|
1499
|
+
reporter.error(`Failed to set core.hooksPath: ${err.message}`);
|
|
907
1500
|
return false;
|
|
908
1501
|
}
|
|
909
1502
|
const on = Object.entries(config).filter(([, v]) => v).map(([k]) => k);
|
|
910
|
-
|
|
911
|
-
|
|
1503
|
+
reporter.success(`Git security hooks installed in ${relative(process.cwd(), hooksDir) || ".githooks"} (core.hooksPath set).`);
|
|
1504
|
+
reporter.info(`Enforcing: ${on.join(", ") || "nothing"}. Commit .githooks/ so your team inherits it.`);
|
|
912
1505
|
if (isOnPath("gh")) {
|
|
913
|
-
|
|
1506
|
+
reporter.info("GitHub CLI (gh) detected: these hooks also run for commits made via gh, since gh uses git underneath.");
|
|
914
1507
|
}
|
|
915
1508
|
return true;
|
|
916
1509
|
}
|
|
@@ -919,8 +1512,8 @@ async function maybeOfferGitHooks(interactive, opts) {
|
|
|
919
1512
|
const root = findGitRoot(process.cwd());
|
|
920
1513
|
if (!root) return;
|
|
921
1514
|
if (currentHooksPath(root) === ".githooks") return;
|
|
922
|
-
const ok = await
|
|
923
|
-
if (!
|
|
1515
|
+
const ok = await p2.confirm({ message: "Set up git security hooks here too (block secrets, .env, node_modules)?" });
|
|
1516
|
+
if (!p2.isCancel(ok) && ok) await setupGitHooks({ ...opts, protections: void 0 }, interactive);
|
|
924
1517
|
}
|
|
925
1518
|
|
|
926
1519
|
// src/skills.ts
|
|
@@ -1081,25 +1674,22 @@ function checkSources() {
|
|
|
1081
1674
|
}
|
|
1082
1675
|
console.log(`Integrity check passed: ${checked} skill(s) well-formed and sealed.`);
|
|
1083
1676
|
}
|
|
1084
|
-
async function installSkills(opts, interactive) {
|
|
1677
|
+
async function installSkills(opts, interactive, reporter = clackReporter()) {
|
|
1085
1678
|
const available = discoverAgents();
|
|
1086
|
-
if (available.length === 0)
|
|
1087
|
-
p3.cancel("No installable agents known.");
|
|
1088
|
-
process.exit(1);
|
|
1089
|
-
}
|
|
1679
|
+
if (available.length === 0) reporter.fatal("No installable agents known.");
|
|
1090
1680
|
let scope;
|
|
1091
1681
|
if (opts.scope) {
|
|
1092
1682
|
scope = opts.scope;
|
|
1093
1683
|
} else if (interactive) {
|
|
1094
|
-
const r = await
|
|
1684
|
+
const r = await p4.select({
|
|
1095
1685
|
message: "Where should skills be installed?",
|
|
1096
1686
|
options: [
|
|
1097
1687
|
{ value: "global", label: "Global (user)", hint: "~/.claude, ~/.codex, ~/.config/opencode" },
|
|
1098
1688
|
{ value: "local", label: "Local (this project)", hint: process.cwd() }
|
|
1099
1689
|
]
|
|
1100
1690
|
});
|
|
1101
|
-
if (
|
|
1102
|
-
|
|
1691
|
+
if (p4.isCancel(r)) {
|
|
1692
|
+
p4.cancel("Aborted.");
|
|
1103
1693
|
return;
|
|
1104
1694
|
}
|
|
1105
1695
|
scope = r;
|
|
@@ -1111,19 +1701,19 @@ async function installSkills(opts, interactive) {
|
|
|
1111
1701
|
if (opts.agents.length) {
|
|
1112
1702
|
chosenAgents = available.filter((a) => opts.agents.includes(a.name));
|
|
1113
1703
|
const unknown = opts.agents.filter((n) => !available.some((a) => a.name === n));
|
|
1114
|
-
if (unknown.length)
|
|
1704
|
+
if (unknown.length) reporter.warn(`Skipping unknown/absent agents: ${unknown.join(", ")}`);
|
|
1115
1705
|
} else if (opts.allAgents) {
|
|
1116
1706
|
chosenAgents = available;
|
|
1117
1707
|
} else if (interactive && available.length > 1) {
|
|
1118
1708
|
const preselect = (detected.length ? detected : available).map((a) => a.name);
|
|
1119
|
-
const r = await
|
|
1709
|
+
const r = await p4.multiselect({
|
|
1120
1710
|
message: "Which agents? (detected on this system are preselected)",
|
|
1121
1711
|
options: available.map((a) => ({ value: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
|
|
1122
1712
|
initialValues: preselect,
|
|
1123
1713
|
required: true
|
|
1124
1714
|
});
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1715
|
+
if (p4.isCancel(r)) {
|
|
1716
|
+
p4.cancel("Aborted.");
|
|
1127
1717
|
return;
|
|
1128
1718
|
}
|
|
1129
1719
|
chosenAgents = available.filter((a) => r.includes(a.name));
|
|
@@ -1131,17 +1721,14 @@ async function installSkills(opts, interactive) {
|
|
|
1131
1721
|
chosenAgents = detected;
|
|
1132
1722
|
} else {
|
|
1133
1723
|
chosenAgents = available;
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
if (chosenAgents.length === 0) {
|
|
1137
|
-
p3.cancel("No matching agents selected.");
|
|
1138
|
-
process.exit(1);
|
|
1724
|
+
reporter.warn("No installed agents detected; defaulting to all supported agents.");
|
|
1139
1725
|
}
|
|
1726
|
+
if (chosenAgents.length === 0) reporter.fatal("No matching agents selected.");
|
|
1140
1727
|
const claudeScope = chosenAgents.some((a) => a.name === "claude") ? scope : null;
|
|
1141
1728
|
const applyClaudeConfig = () => {
|
|
1142
1729
|
if (!claudeScope || opts.dryRun) return;
|
|
1143
1730
|
if (disableClaudeAttribution(claudeScope)) {
|
|
1144
|
-
|
|
1731
|
+
reporter.info("Claude Code: disabled Co-Authored-By and PR attribution in settings.json.");
|
|
1145
1732
|
}
|
|
1146
1733
|
};
|
|
1147
1734
|
const bypassAgents = await resolveBypassSelection(chosenAgents, opts, interactive);
|
|
@@ -1150,7 +1737,7 @@ async function installSkills(opts, interactive) {
|
|
|
1150
1737
|
for (const agent of chosenAgents) {
|
|
1151
1738
|
const target = agent.targets[scope];
|
|
1152
1739
|
if (!target) {
|
|
1153
|
-
|
|
1740
|
+
reporter.warn(`${agent.label} has no '${scope}' target - skipping.`);
|
|
1154
1741
|
continue;
|
|
1155
1742
|
}
|
|
1156
1743
|
const skills = inspectSkills();
|
|
@@ -1159,7 +1746,7 @@ async function installSkills(opts, interactive) {
|
|
|
1159
1746
|
if (!opts.memoryOnly && opts.skills.length) {
|
|
1160
1747
|
chosenSkills = skills.filter((s2) => opts.skills.includes(s2.name));
|
|
1161
1748
|
} else if (!opts.memoryOnly && interactive && skills.length > 1) {
|
|
1162
|
-
const r = await
|
|
1749
|
+
const r = await p4.multiselect({
|
|
1163
1750
|
message: `Skills for ${agent.label} - all selected; deselect any you don't want`,
|
|
1164
1751
|
options: skills.map((s2) => {
|
|
1165
1752
|
const st = skillStatus(join6(target.skills, s2.name), s2.meta);
|
|
@@ -1169,8 +1756,8 @@ async function installSkills(opts, interactive) {
|
|
|
1169
1756
|
initialValues: skills.map((s2) => s2.name),
|
|
1170
1757
|
required: false
|
|
1171
1758
|
});
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1759
|
+
if (p4.isCancel(r)) {
|
|
1760
|
+
p4.cancel("Aborted.");
|
|
1174
1761
|
return;
|
|
1175
1762
|
}
|
|
1176
1763
|
chosenSkills = skills.filter((s2) => r.includes(s2.name));
|
|
@@ -1187,16 +1774,16 @@ async function installSkills(opts, interactive) {
|
|
|
1187
1774
|
if (tampered.length) {
|
|
1188
1775
|
if (opts.keepModified) {
|
|
1189
1776
|
for (const s2 of tampered) s2.overwrite = false;
|
|
1190
|
-
|
|
1777
|
+
reporter.warn(`${tampered.length} locally-modified skill(s) will be kept (--keep-modified).`);
|
|
1191
1778
|
} else if (interactive && !opts.dryRun) {
|
|
1192
|
-
const sel = await
|
|
1779
|
+
const sel = await p4.multiselect({
|
|
1193
1780
|
message: `${tampered.length} skill(s) were modified locally since install. Select which to OVERWRITE`,
|
|
1194
1781
|
options: tampered.map((s2, i) => ({ value: i, label: s2.name, hint: s2.meta.version ? `v${s2.meta.version}` : "modified" })),
|
|
1195
1782
|
initialValues: tampered.map((_, i) => i),
|
|
1196
1783
|
required: false
|
|
1197
1784
|
});
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1785
|
+
if (p4.isCancel(sel)) {
|
|
1786
|
+
p4.cancel("Aborted.");
|
|
1200
1787
|
return;
|
|
1201
1788
|
}
|
|
1202
1789
|
tampered.forEach((s2, i) => {
|
|
@@ -1250,14 +1837,14 @@ async function installSkills(opts, interactive) {
|
|
|
1250
1837
|
}
|
|
1251
1838
|
}
|
|
1252
1839
|
if (nInstall + nUpdate + nRemove === 0) {
|
|
1253
|
-
|
|
1840
|
+
reporter.note(lines.join("\n"), "Nothing to do");
|
|
1254
1841
|
applyClaudeConfig();
|
|
1255
1842
|
applyBypassConfig();
|
|
1256
1843
|
await maybeOfferGitHooks(interactive, opts);
|
|
1257
|
-
|
|
1844
|
+
reporter.success(`Everything up-to-date - ${nSkip} item(s) unchanged${nKept ? `, ${nKept} kept modified` : ""} (${scope}).`);
|
|
1258
1845
|
return;
|
|
1259
1846
|
}
|
|
1260
|
-
|
|
1847
|
+
reporter.note(lines.join("\n"), opts.dryRun ? "Dry run - planned changes" : "Planned changes");
|
|
1261
1848
|
if (interactive && !opts.dryRun) {
|
|
1262
1849
|
const summary = [
|
|
1263
1850
|
nInstall && `${nInstall} to install`,
|
|
@@ -1265,21 +1852,21 @@ async function installSkills(opts, interactive) {
|
|
|
1265
1852
|
nRemove && `${nRemove} to remove`,
|
|
1266
1853
|
nSkip && `${nSkip} unchanged`
|
|
1267
1854
|
].filter(Boolean).join(", ");
|
|
1268
|
-
const ok = await
|
|
1269
|
-
if (
|
|
1270
|
-
|
|
1855
|
+
const ok = await p4.confirm({ message: `Apply: ${summary}?` });
|
|
1856
|
+
if (p4.isCancel(ok) || !ok) {
|
|
1857
|
+
p4.cancel("Aborted.");
|
|
1271
1858
|
return;
|
|
1272
1859
|
}
|
|
1273
1860
|
}
|
|
1274
1861
|
if (opts.dryRun) {
|
|
1275
1862
|
applyBypassConfig();
|
|
1276
|
-
|
|
1863
|
+
reporter.info("Dry run complete - no files written.");
|
|
1277
1864
|
return;
|
|
1278
1865
|
}
|
|
1279
1866
|
const changedAgents = plan.filter(
|
|
1280
1867
|
(x) => x.skills.some(willCopy) || x.memory.some((m) => memoryStatus(m.src, join6(x.target.memory, m.name)) !== "identical") || x.prune.length > 0
|
|
1281
1868
|
);
|
|
1282
|
-
const s =
|
|
1869
|
+
const s = reporter.spinner();
|
|
1283
1870
|
s.start("Installing...");
|
|
1284
1871
|
let copied = 0;
|
|
1285
1872
|
try {
|
|
@@ -1300,26 +1887,28 @@ async function installSkills(opts, interactive) {
|
|
|
1300
1887
|
}
|
|
1301
1888
|
} catch (err) {
|
|
1302
1889
|
s.stop("Failed.");
|
|
1303
|
-
|
|
1304
|
-
process.exit(1);
|
|
1890
|
+
reporter.fatal(`Error while installing: ${err.message}`);
|
|
1305
1891
|
}
|
|
1306
1892
|
s.stop(`Wrote ${copied} item(s)${nRemove ? `, removed ${nRemove}` : ""}.`);
|
|
1307
1893
|
applyClaudeConfig();
|
|
1308
1894
|
applyBypassConfig();
|
|
1309
1895
|
await maybeOfferGitHooks(interactive, opts);
|
|
1310
|
-
|
|
1896
|
+
reporter.success(`${nInstall} installed, ${nUpdate} updated/overwritten` + (nRemove ? `, ${nRemove} removed` : "") + (nSkip ? `, ${nSkip} unchanged` : "") + (nKept ? `, ${nKept} kept modified` : "") + ` (${scope}).`);
|
|
1311
1897
|
if (changedAgents.length) {
|
|
1312
1898
|
const { known, running } = runningStatus(changedAgents.map((x) => x.agent));
|
|
1313
1899
|
if (running.size) {
|
|
1314
1900
|
const names = changedAgents.filter((x) => running.has(x.agent.name)).map((x) => x.agent.label);
|
|
1315
|
-
|
|
1901
|
+
reporter.warn(`Restart ${names.join(", ")} to apply the changes (running now).`);
|
|
1316
1902
|
} else if (!known) {
|
|
1317
1903
|
const names = changedAgents.map((x) => x.agent.label);
|
|
1318
|
-
|
|
1904
|
+
reporter.info(`If any of these agents are running, restart them to apply the changes: ${names.join(", ")}.`);
|
|
1319
1905
|
}
|
|
1320
1906
|
}
|
|
1321
1907
|
}
|
|
1322
1908
|
|
|
1909
|
+
// src/cli.ts
|
|
1910
|
+
init_agents();
|
|
1911
|
+
|
|
1323
1912
|
// src/guard.ts
|
|
1324
1913
|
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
1325
1914
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
@@ -1521,7 +2110,7 @@ import { homedir as homedir5 } from "os";
|
|
|
1521
2110
|
import { join as join9 } from "path";
|
|
1522
2111
|
import { writeFileSync as writeFileSync6 } from "fs";
|
|
1523
2112
|
import { spawn, spawnSync } from "child_process";
|
|
1524
|
-
import * as
|
|
2113
|
+
import * as p5 from "@clack/prompts";
|
|
1525
2114
|
var REGISTRY_URL = "https://registry.npmjs.org/enigma-cli/latest";
|
|
1526
2115
|
var UPDATE_COMMAND = "npm i -g enigma-cli@latest";
|
|
1527
2116
|
var CACHE_FILE = join9(homedir5(), ".enigma-update-check.json");
|
|
@@ -1606,8 +2195,8 @@ async function notifyUpdate(current, interactive) {
|
|
|
1606
2195
|
${renderUpdateBox(current, latest)}
|
|
1607
2196
|
`);
|
|
1608
2197
|
if (!interactive) return;
|
|
1609
|
-
const ok = await
|
|
1610
|
-
if (
|
|
2198
|
+
const ok = await p5.confirm({ message: `Update now with ${UPDATE_COMMAND}?`, initialValue: true });
|
|
2199
|
+
if (p5.isCancel(ok) || !ok) return;
|
|
1611
2200
|
runUpdate();
|
|
1612
2201
|
} catch {
|
|
1613
2202
|
}
|
|
@@ -1725,7 +2314,7 @@ Usage:
|
|
|
1725
2314
|
|
|
1726
2315
|
Commands:
|
|
1727
2316
|
(none) Interactive hub: configure settings or set up features
|
|
1728
|
-
install Install/update agent skills (Claude Code, Codex,
|
|
2317
|
+
install Install/update agent skills (Claude Code, Codex, OpenCode)
|
|
1729
2318
|
security Set up git security hooks in the current repo
|
|
1730
2319
|
guard [--all] Run the commit guard (staged files, or --all for every tracked file)
|
|
1731
2320
|
config [key val] Configure settings: no args opens the interactive menu;
|
|
@@ -1734,7 +2323,7 @@ Commands:
|
|
|
1734
2323
|
check Integrity gate: verify skills are well-formed and sealed
|
|
1735
2324
|
help, version
|
|
1736
2325
|
|
|
1737
|
-
Config keys: commit-emoji, update-notifier, claude-attribution,
|
|
2326
|
+
Config keys: commit-emoji, update-notifier, fullscreen, claude-attribution,
|
|
1738
2327
|
bypass-claude, bypass-codex, bypass-opencode
|
|
1739
2328
|
|
|
1740
2329
|
Install options:
|
|
@@ -1788,16 +2377,16 @@ async function run(argv) {
|
|
|
1788
2377
|
process.exit(await runConfigCli(opts.positionals, opts.scope, interactive));
|
|
1789
2378
|
}
|
|
1790
2379
|
if (opts.command === "install") {
|
|
1791
|
-
|
|
2380
|
+
p6.intro("enigma - install agent skills");
|
|
1792
2381
|
await installSkills(opts, interactive);
|
|
1793
|
-
|
|
2382
|
+
p6.outro("Done.");
|
|
1794
2383
|
await notifyUpdate(version, interactive);
|
|
1795
2384
|
return;
|
|
1796
2385
|
}
|
|
1797
2386
|
if (opts.command === "security") {
|
|
1798
|
-
|
|
2387
|
+
p6.intro("enigma - git security hooks");
|
|
1799
2388
|
const done = await setupGitHooks(opts, interactive);
|
|
1800
|
-
|
|
2389
|
+
p6.outro(done ? "Git hooks configured." : "No changes made.");
|
|
1801
2390
|
await notifyUpdate(version, interactive);
|
|
1802
2391
|
return;
|
|
1803
2392
|
}
|
|
@@ -1806,16 +2395,24 @@ async function run(argv) {
|
|
|
1806
2395
|
await notifyUpdate(version, interactive);
|
|
1807
2396
|
return;
|
|
1808
2397
|
}
|
|
1809
|
-
const { runHomeTui:
|
|
1810
|
-
await
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2398
|
+
const { runHomeTui: runHomeTui3 } = isBun() ? await Promise.resolve().then(() => (init_opentui(), opentui_exports)) : await Promise.resolve().then(() => (init_settings(), settings_exports));
|
|
2399
|
+
await runHomeTui3({
|
|
2400
|
+
agents: discoverAgents().map((a) => ({ name: a.name, label: a.label, installed: a.installed })),
|
|
2401
|
+
protections: GUARD_PROTECTIONS,
|
|
2402
|
+
runAction: async (req) => {
|
|
2403
|
+
const reporter = collectReporter();
|
|
2404
|
+
const title = req.action === "skills" ? "Install agent skills" : "Git security hooks";
|
|
2405
|
+
try {
|
|
2406
|
+
if (req.action === "skills") {
|
|
2407
|
+
await installSkills({ ...opts, scope: req.scope ?? opts.scope, agents: req.agents ?? [], allAgents: !(req.agents && req.agents.length) }, false, reporter);
|
|
2408
|
+
return { ok: true, title, lines: reporter.lines };
|
|
2409
|
+
}
|
|
2410
|
+
const done = await setupGitHooks({ ...opts, protections: req.protections, force: true }, false, reporter);
|
|
2411
|
+
return { ok: done, title, lines: reporter.lines };
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
reporter.error(`Error: ${err.message}`);
|
|
2414
|
+
return { ok: false, title, lines: reporter.lines };
|
|
2415
|
+
}
|
|
1819
2416
|
}
|
|
1820
2417
|
});
|
|
1821
2418
|
await notifyUpdate(version, interactive);
|