enigma-cli 1.2.0 → 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/dist/enigma.js CHANGED
@@ -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 p2 from "@clack/prompts";
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 p2.multiselect({
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 (p2.isCancel(r)) return [];
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) p2.log.info(`Bypass (dry run): would enable for ${label} -> ${res.path}.`);
251
- else if (res.changed) p2.log.warn(`Bypass: enabled for ${label} (${res.path}) - approval prompts are now OFF.`);
252
- else p2.log.info(`Bypass: already enabled for ${label}.`);
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) {
@@ -546,40 +546,14 @@ async function runTui(opts) {
546
546
  const h = React.createElement;
547
547
  const agents = opts.hub?.agents ?? [];
548
548
  const protections = opts.hub?.protections ?? [];
549
- const clear = () => {
550
- if (fullscreen) try {
551
- process.stdout.write(CLEAR_SCREEN);
552
- } catch {
553
- }
554
- };
555
- for (; ; ) {
556
- const state = { intent: { action: "quit" } };
557
- const App = buildApp(React, ink, { showActions: opts.showActions, fullscreen, agents, protections, onLeave: (i) => {
558
- state.intent = i;
559
- } });
560
- clear();
561
- const app = render(h(App), { exitOnCtrlC: true });
562
- await app.waitUntilExit();
563
- const { intent } = state;
564
- if (opts.hub && (intent.action === "skills" || intent.action === "security")) {
565
- clear();
566
- await opts.hub.runAction(intent);
567
- await waitForEnter();
568
- continue;
569
- }
570
- break;
549
+ const runAction = opts.hub?.runAction ?? (async () => ({ ok: false, title: "", lines: [] }));
550
+ if (fullscreen) try {
551
+ process.stdout.write(CLEAR_SCREEN);
552
+ } catch {
571
553
  }
572
- }
573
- function waitForEnter() {
574
- return new Promise((resolve3) => {
575
- process.stdout.write("\nPress Enter to return to the menu...\n");
576
- const stdin = process.stdin;
577
- stdin.resume();
578
- stdin.once("data", () => {
579
- stdin.pause();
580
- resolve3();
581
- });
582
- });
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();
583
557
  }
584
558
  function buildApp(React, ink, opts) {
585
559
  const { useApp, useInput, useStdout } = ink;
@@ -592,6 +566,7 @@ function buildApp(React, ink, opts) {
592
566
  ...CATEGORIES.map((c, i) => ({ kind: "category", catIndex: i, title: c.title })),
593
567
  ...opts.showActions ? ACTION_ITEMS.map((a) => ({ kind: "action", ...a })) : []
594
568
  ];
569
+ const actionItemKeys = (action) => action === "security" ? opts.protections.map((pr) => pr.value) : opts.agents.map((a) => a.name);
595
570
  return function App() {
596
571
  const { exit } = useApp();
597
572
  const { stdout } = useStdout();
@@ -605,7 +580,9 @@ function buildApp(React, ink, opts) {
605
580
  const [confirm4, setConfirm] = useState(null);
606
581
  const [actCursor, setActCursor] = useState(0);
607
582
  const [actChecked, setActChecked] = useState({});
608
- const [actScope, setActScope] = useState("global");
583
+ const [busyTitle, setBusyTitle] = useState("");
584
+ const [result, setResult] = useState(null);
585
+ const [resultScroll, setResultScroll] = useState(0);
609
586
  useEffect(() => {
610
587
  const onResize = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
611
588
  stdout.on("resize", onResize);
@@ -615,6 +592,20 @@ function buildApp(React, ink, opts) {
615
592
  }, [stdout]);
616
593
  const current = sideItems[sideIndex];
617
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)])));
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);
618
609
  const valueOf = (setting, sc) => {
619
610
  const k = stageKey(setting.key, sc);
620
611
  return k in pending ? pending[k] : setting.read(sc);
@@ -634,21 +625,23 @@ function buildApp(React, ink, opts) {
634
625
  if (setting && setting.read(sc) !== v) setting.write(v, sc);
635
626
  }
636
627
  };
637
- const leave = (intent) => {
638
- opts.onLeave(intent);
639
- exit();
640
- };
641
- const openAction = (action) => {
642
- setActCursor(0);
643
- if (action === "security") {
644
- setActChecked(Object.fromEntries(opts.protections.map((p6) => [p6.value, true])));
645
- } else {
646
- const detected = opts.agents.filter((a) => a.installed);
647
- const preselect = detected.length ? detected : opts.agents;
648
- setActChecked(Object.fromEntries(opts.agents.map((a) => [a.name, preselect.some((d) => d.name === a.name)])));
649
- setActScope("global");
650
- }
651
- setMode(action);
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
+ });
652
645
  };
653
646
  useInput((input, key) => {
654
647
  if (confirm4) {
@@ -670,44 +663,28 @@ function buildApp(React, ink, opts) {
670
663
  if (index === 2) return;
671
664
  if (index === 0) persistPending();
672
665
  setPending({});
673
- leave({ action: "quit" });
666
+ exit();
674
667
  }
675
668
  return;
676
669
  }
677
- if (mode !== "menu") {
678
- const items = mode === "security" ? opts.protections.map((p6) => p6.value) : opts.agents.map((a) => a.name);
679
- if (key.escape || input === "q") {
670
+ if (mode === "running") return;
671
+ if (mode === "result") {
672
+ if (key.return || key.escape || input === " " || input === "q") {
680
673
  setMode("menu");
681
674
  return;
682
675
  }
683
- if (mode === "skills" && input === "g") {
684
- setActScope((s) => s === "global" ? "local" : "global");
685
- return;
686
- }
687
676
  if (key.upArrow || input === "k") {
688
- setActCursor((i) => Math.max(0, i - 1));
677
+ setResultScroll((s) => Math.max(0, s - 1));
689
678
  return;
690
679
  }
691
680
  if (key.downArrow || input === "j") {
692
- setActCursor((i) => Math.min(items.length - 1, i + 1));
693
- return;
694
- }
695
- if (input === " ") {
696
- const k = items[actCursor];
697
- setActChecked((c) => ({ ...c, [k]: !c[k] }));
681
+ setResultScroll((s) => Math.min(maxResultScroll, s + 1));
698
682
  return;
699
683
  }
700
- if (key.return) {
701
- const chosen = items.filter((k) => actChecked[k]);
702
- persistPending();
703
- setPending({});
704
- if (mode === "security") leave({ action: "security", protections: chosen });
705
- else leave({ action: "skills", scope: actScope, agents: chosen });
706
- }
707
684
  return;
708
685
  }
709
686
  if (input === "q" || key.escape) {
710
- dirty ? setConfirm({ index: 0 }) : leave({ action: "quit" });
687
+ dirty ? setConfirm({ index: 0 }) : exit();
711
688
  return;
712
689
  }
713
690
  if (input === "s") {
@@ -718,7 +695,7 @@ function buildApp(React, ink, opts) {
718
695
  if (input === "x") {
719
696
  persistPending();
720
697
  setPending({});
721
- leave({ action: "quit" });
698
+ exit();
722
699
  return;
723
700
  }
724
701
  if (input === "g") {
@@ -726,7 +703,7 @@ function buildApp(React, ink, opts) {
726
703
  return;
727
704
  }
728
705
  if (key.tab) {
729
- if (category) setFocusRight((f) => !f);
706
+ setFocusRight((f) => !f);
730
707
  return;
731
708
  }
732
709
  if (key.leftArrow || input === "h") {
@@ -734,11 +711,12 @@ function buildApp(React, ink, opts) {
734
711
  return;
735
712
  }
736
713
  if (key.rightArrow || input === "l") {
737
- if (category) setFocusRight(true);
714
+ setFocusRight(true);
738
715
  return;
739
716
  }
740
717
  if (key.upArrow || input === "k") {
741
718
  if (focusRight && category) setSetIndex((i) => Math.max(0, i - 1));
719
+ else if (focusRight && action) setActCursor((i) => Math.max(0, i - 1));
742
720
  else {
743
721
  setSideIndex((i) => Math.max(0, i - 1));
744
722
  setSetIndex(0);
@@ -748,6 +726,7 @@ function buildApp(React, ink, opts) {
748
726
  }
749
727
  if (key.downArrow || input === "j") {
750
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));
751
730
  else {
752
731
  setSideIndex((i) => Math.min(sideItems.length - 1, i + 1));
753
732
  setSetIndex(0);
@@ -755,27 +734,32 @@ function buildApp(React, ink, opts) {
755
734
  }
756
735
  return;
757
736
  }
737
+ if (input === " " && focusRight && action) {
738
+ const k = actionItemKeys(action)[actCursor];
739
+ setActChecked((c) => ({ ...c, [k]: !c[k] }));
740
+ return;
741
+ }
758
742
  if (key.return || input === " ") {
759
- if (current.kind === "action") {
760
- openAction(current.action);
761
- return;
762
- }
763
743
  if (!focusRight) {
764
744
  setFocusRight(true);
765
745
  return;
766
746
  }
747
+ if (action) {
748
+ runChosen(action);
749
+ return;
750
+ }
767
751
  const setting = category.settings[setIndex];
768
- setPending((p6) => ({ ...p6, [stageKey(setting.key, scope)]: !valueOf(setting, scope) }));
752
+ setPending((pr) => ({ ...pr, [stageKey(setting.key, scope)]: !valueOf(setting, scope) }));
769
753
  }
770
754
  });
771
- const headerRight = confirm4 ? h(Text, { color: "yellow" }, "unsaved changes") : mode === "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(
772
756
  Box,
773
757
  {},
774
758
  h(Text, { dimColor: true }, "scope "),
775
759
  h(Text, { bold: true, color: scope === "global" ? "green" : "yellow" }, scope),
776
760
  h(Text, { dimColor: true }, " (g)"),
777
761
  dirty ? h(Text, { color: "yellow" }, " * unsaved") : null
778
- ) : h(Text, { dimColor: true }, mode === "skills" ? "install" : "security");
762
+ );
779
763
  const titleBar = h(
780
764
  Box,
781
765
  { width: size.columns, paddingX: 1, justifyContent: "space-between" },
@@ -785,30 +769,25 @@ function buildApp(React, ink, opts) {
785
769
  let content;
786
770
  if (confirm4) {
787
771
  content = renderConfirm(h, Box, Text, confirm4.index, fill);
788
- } else if (mode === "security") {
789
- content = renderChecklist(h, Box, Text, {
790
- title: "Git security hooks",
791
- blurb: "Choose what the commit guard enforces, then press enter.",
792
- items: opts.protections.map((p6) => ({ key: p6.value, label: p6.label, hint: p6.hint })),
793
- cursor: actCursor,
794
- checked: actChecked,
795
- fill
796
- });
797
- } else if (mode === "skills") {
798
- content = renderChecklist(h, Box, Text, {
799
- title: "Install agent skills",
800
- blurb: `Scope ${actScope} (g to change). Choose agents, then press enter.`,
801
- items: opts.agents.map((a) => ({ key: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
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" })),
802
782
  cursor: actCursor,
803
783
  checked: actChecked,
784
+ focused: focusRight,
804
785
  fill
805
786
  });
806
- } else {
807
- const sidebarWidth = Math.min(28, Math.max(20, Math.floor(size.columns * 0.3)));
808
- const panel = category ? renderCategoryPanel(h, Box, Text, { category, scope, focusRight, setIndex, valueOf, isModified, fill }) : renderActionPanel(h, Box, Text, current);
809
787
  content = h(Box, fill ? { flexGrow: 1 } : {}, renderSidebar(h, Box, Text, sideItems, sideIndex, focusRight, sidebarWidth), panel);
810
788
  }
811
- const footerText = confirm4 ? "up/down move enter select esc cancel" : mode === "security" ? "up/down move space toggle enter apply esc back" : mode === "skills" ? "up/down move space toggle g scope enter install esc back" : `up/down move tab switch ${category ? "enter toggle g scope " : "enter open "}s save x save & exit q quit`;
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;
812
791
  const footer = h(Box, { width: size.columns, paddingX: 1 }, h(Text, { dimColor: true }, footerText));
813
792
  return h(
814
793
  Box,
@@ -856,10 +835,45 @@ function renderConfirm(h, Box, Text, index, fill) {
856
835
  ])
857
836
  );
858
837
  }
838
+ function renderRunning(h, Box, Text, title, fill) {
839
+ return h(Box, {
840
+ flexDirection: "column",
841
+ borderStyle: "round",
842
+ borderColor: "cyan",
843
+ paddingX: 1,
844
+ ...fill ? { flexGrow: 1 } : {}
845
+ }, [
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 })] : []
849
+ ]);
850
+ }
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, {
860
+ flexDirection: "column",
861
+ borderStyle: "round",
862
+ borderColor: res.ok ? "green" : "red",
863
+ paddingX: 1,
864
+ ...s.fill ? { flexGrow: 1 } : {}
865
+ }, [
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 })] : []
871
+ ]);
872
+ }
859
873
  function renderChecklist(h, Box, Text, s) {
860
874
  const rows = s.items.map((it, i) => {
861
875
  const on = !!s.checked[it.key];
862
- const selected = i === s.cursor;
876
+ const selected = s.focused && i === s.cursor;
863
877
  return h(
864
878
  Box,
865
879
  { key: it.key, justifyContent: "space-between" },
@@ -870,9 +884,9 @@ function renderChecklist(h, Box, Text, s) {
870
884
  return h(Box, {
871
885
  flexDirection: "column",
872
886
  borderStyle: "round",
873
- borderColor: "cyan",
887
+ borderColor: s.focused ? "cyan" : "gray",
874
888
  paddingX: 1,
875
- ...s.fill ? { flexGrow: 1 } : {}
889
+ flexGrow: 1
876
890
  }, [
877
891
  h(Text, { key: "__t", bold: true, color: "cyan" }, s.title),
878
892
  h(Text, { key: "__bl", dimColor: true }, s.blurb),
@@ -907,20 +921,7 @@ function renderCategoryPanel(h, Box, Text, s) {
907
921
  h(Text, { key: "__hint", marginTop: 1, dimColor: true, wrap: "truncate-end" }, focused.hint)
908
922
  ]);
909
923
  }
910
- function renderActionPanel(h, Box, Text, item) {
911
- return h(Box, {
912
- flexDirection: "column",
913
- borderStyle: "round",
914
- borderColor: "gray",
915
- paddingX: 1,
916
- flexGrow: 1
917
- }, [
918
- h(Text, { key: "__t", bold: true, color: "cyan" }, item.title),
919
- h(Text, { key: "__bl", dimColor: true }, item.blurb),
920
- h(Text, { key: "__h", marginTop: 1, dimColor: true }, "Press enter to open")
921
- ]);
922
- }
923
- var CLEAR_SCREEN, SETTING_BY_KEY, stageKey, parseStageKey, ACTION_ITEMS, EXIT_OPTIONS;
924
+ var CLEAR_SCREEN, SETTING_BY_KEY, stageKey, parseStageKey, ACTION_ITEMS, actionTitle, EXIT_OPTIONS;
924
925
  var init_settings = __esm({
925
926
  "src/tui/settings.ts"() {
926
927
  "use strict";
@@ -937,15 +938,450 @@ var init_settings = __esm({
937
938
  { action: "skills", title: "Install agent skills", blurb: "Claude Code, Codex, OpenCode" },
938
939
  { action: "security", title: "Git security hooks", blurb: "block secrets, .env, node_modules on commit" }
939
940
  ];
941
+ actionTitle = (action) => ACTION_ITEMS.find((a) => a.action === action).title;
940
942
  EXIT_OPTIONS = ["Save & exit", "Exit without saving", "Cancel"];
941
943
  }
942
944
  });
943
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"() {
1309
+ "use strict";
1310
+ init_settings_registry();
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" }
1329
+ ];
1330
+ actionTitle2 = (action) => ACTION_ITEMS2.find((a) => a.action === action).title;
1331
+ EXIT_OPTIONS2 = ["Save & exit", "Exit without saving", "Cancel"];
1332
+ }
1333
+ });
1334
+
944
1335
  // src/cli.ts
945
1336
  init_util();
946
1337
  import { dirname as dirname4, join as join10 } from "path";
947
1338
  import { fileURLToPath as fileURLToPath4 } from "url";
948
- import * as p5 from "@clack/prompts";
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
+ }
949
1385
 
950
1386
  // src/skills.ts
951
1387
  init_util();
@@ -954,7 +1390,7 @@ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3,
954
1390
  import { dirname as dirname2, join as join6, resolve as resolve2, relative as relative2, sep as sep2 } from "path";
955
1391
  import { fileURLToPath as fileURLToPath2 } from "url";
956
1392
  import { createHash } from "crypto";
957
- import * as p3 from "@clack/prompts";
1393
+ import * as p4 from "@clack/prompts";
958
1394
 
959
1395
  // src/security.ts
960
1396
  init_util();
@@ -962,7 +1398,7 @@ import { existsSync as existsSync3, mkdirSync, cpSync, writeFileSync, chmodSync
962
1398
  import { dirname, join as join3, resolve, relative } from "path";
963
1399
  import { fileURLToPath } from "url";
964
1400
  import { execFileSync as execFileSync2 } from "child_process";
965
- import * as p from "@clack/prompts";
1401
+ import * as p2 from "@clack/prompts";
966
1402
  var __dirname = dirname(fileURLToPath(import.meta.url));
967
1403
  function findGuardSrc() {
968
1404
  const candidates = [
@@ -995,41 +1431,41 @@ function currentHooksPath(root) {
995
1431
  return "";
996
1432
  }
997
1433
  }
998
- async function setupGitHooks(opts, interactive) {
1434
+ async function setupGitHooks(opts, interactive, reporter = clackReporter()) {
999
1435
  const root = findGitRoot(process.cwd());
1000
1436
  if (!root) {
1001
- p.log.error("Not inside a git repository (no .git found). Run this from your project root.");
1437
+ reporter.error("Not inside a git repository (no .git found). Run this from your project root.");
1002
1438
  return false;
1003
1439
  }
1004
1440
  const guardSrc = findGuardSrc();
1005
1441
  if (!guardSrc) {
1006
- p.log.error("Cannot find the built guard (dist/guard.js). Run 'npm run build' first.");
1442
+ reporter.error("Cannot find the built guard (dist/guard.js). Run 'npm run build' first.");
1007
1443
  return false;
1008
1444
  }
1009
1445
  const current = currentHooksPath(root);
1010
1446
  if (current && current !== ".githooks" && !opts.force) {
1011
- p.log.warn(`core.hooksPath is already set to '${current}'.`);
1447
+ reporter.warn(`core.hooksPath is already set to '${current}'.`);
1012
1448
  if (interactive) {
1013
- const ok = await p.confirm({ message: `Override existing core.hooksPath '${current}' with '.githooks'?` });
1014
- if (p.isCancel(ok) || !ok) {
1015
- p.log.info("Left git hooks unchanged.");
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.");
1016
1452
  return false;
1017
1453
  }
1018
1454
  } else {
1019
- p.log.info("Re-run with --force to override.");
1455
+ reporter.info("Re-run with --force to override.");
1020
1456
  return false;
1021
1457
  }
1022
1458
  }
1023
1459
  let enabled = opts.protections;
1024
1460
  if (!enabled && interactive) {
1025
- const r = await p.multiselect({
1461
+ const r = await p2.multiselect({
1026
1462
  message: "Which protections should the commit guard enforce?",
1027
1463
  options: GUARD_PROTECTIONS,
1028
1464
  initialValues: GUARD_PROTECTIONS.map((o) => o.value),
1029
1465
  required: true
1030
1466
  });
1031
- if (p.isCancel(r)) {
1032
- p.log.info("Left git hooks unchanged.");
1467
+ if (p2.isCancel(r)) {
1468
+ reporter.info("Left git hooks unchanged.");
1033
1469
  return false;
1034
1470
  }
1035
1471
  enabled = r;
@@ -1060,14 +1496,14 @@ async function setupGitHooks(opts, interactive) {
1060
1496
  try {
1061
1497
  execFileSync2("git", ["-C", root, "config", "core.hooksPath", ".githooks"]);
1062
1498
  } catch (err) {
1063
- p.log.error(`Failed to set core.hooksPath: ${err.message}`);
1499
+ reporter.error(`Failed to set core.hooksPath: ${err.message}`);
1064
1500
  return false;
1065
1501
  }
1066
1502
  const on = Object.entries(config).filter(([, v]) => v).map(([k]) => k);
1067
- p.log.success(`Git security hooks installed in ${relative(process.cwd(), hooksDir) || ".githooks"} (core.hooksPath set).`);
1068
- p.log.info(`Enforcing: ${on.join(", ") || "nothing"}. Commit .githooks/ so your team inherits it.`);
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.`);
1069
1505
  if (isOnPath("gh")) {
1070
- p.log.info("GitHub CLI (gh) detected: these hooks also run for commits made via gh, since gh uses git underneath.");
1506
+ reporter.info("GitHub CLI (gh) detected: these hooks also run for commits made via gh, since gh uses git underneath.");
1071
1507
  }
1072
1508
  return true;
1073
1509
  }
@@ -1076,8 +1512,8 @@ async function maybeOfferGitHooks(interactive, opts) {
1076
1512
  const root = findGitRoot(process.cwd());
1077
1513
  if (!root) return;
1078
1514
  if (currentHooksPath(root) === ".githooks") return;
1079
- const ok = await p.confirm({ message: "Set up git security hooks here too (block secrets, .env, node_modules)?" });
1080
- if (!p.isCancel(ok) && ok) await setupGitHooks({ ...opts, protections: void 0 }, interactive);
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);
1081
1517
  }
1082
1518
 
1083
1519
  // src/skills.ts
@@ -1238,25 +1674,22 @@ function checkSources() {
1238
1674
  }
1239
1675
  console.log(`Integrity check passed: ${checked} skill(s) well-formed and sealed.`);
1240
1676
  }
1241
- async function installSkills(opts, interactive) {
1677
+ async function installSkills(opts, interactive, reporter = clackReporter()) {
1242
1678
  const available = discoverAgents();
1243
- if (available.length === 0) {
1244
- p3.cancel("No installable agents known.");
1245
- process.exit(1);
1246
- }
1679
+ if (available.length === 0) reporter.fatal("No installable agents known.");
1247
1680
  let scope;
1248
1681
  if (opts.scope) {
1249
1682
  scope = opts.scope;
1250
1683
  } else if (interactive) {
1251
- const r = await p3.select({
1684
+ const r = await p4.select({
1252
1685
  message: "Where should skills be installed?",
1253
1686
  options: [
1254
1687
  { value: "global", label: "Global (user)", hint: "~/.claude, ~/.codex, ~/.config/opencode" },
1255
1688
  { value: "local", label: "Local (this project)", hint: process.cwd() }
1256
1689
  ]
1257
1690
  });
1258
- if (p3.isCancel(r)) {
1259
- p3.cancel("Aborted.");
1691
+ if (p4.isCancel(r)) {
1692
+ p4.cancel("Aborted.");
1260
1693
  return;
1261
1694
  }
1262
1695
  scope = r;
@@ -1268,19 +1701,19 @@ async function installSkills(opts, interactive) {
1268
1701
  if (opts.agents.length) {
1269
1702
  chosenAgents = available.filter((a) => opts.agents.includes(a.name));
1270
1703
  const unknown = opts.agents.filter((n) => !available.some((a) => a.name === n));
1271
- if (unknown.length) p3.log.warn(`Skipping unknown/absent agents: ${unknown.join(", ")}`);
1704
+ if (unknown.length) reporter.warn(`Skipping unknown/absent agents: ${unknown.join(", ")}`);
1272
1705
  } else if (opts.allAgents) {
1273
1706
  chosenAgents = available;
1274
1707
  } else if (interactive && available.length > 1) {
1275
1708
  const preselect = (detected.length ? detected : available).map((a) => a.name);
1276
- const r = await p3.multiselect({
1709
+ const r = await p4.multiselect({
1277
1710
  message: "Which agents? (detected on this system are preselected)",
1278
1711
  options: available.map((a) => ({ value: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
1279
1712
  initialValues: preselect,
1280
1713
  required: true
1281
1714
  });
1282
- if (p3.isCancel(r)) {
1283
- p3.cancel("Aborted.");
1715
+ if (p4.isCancel(r)) {
1716
+ p4.cancel("Aborted.");
1284
1717
  return;
1285
1718
  }
1286
1719
  chosenAgents = available.filter((a) => r.includes(a.name));
@@ -1288,17 +1721,14 @@ async function installSkills(opts, interactive) {
1288
1721
  chosenAgents = detected;
1289
1722
  } else {
1290
1723
  chosenAgents = available;
1291
- p3.log.warn("No installed agents detected; defaulting to all supported agents.");
1292
- }
1293
- if (chosenAgents.length === 0) {
1294
- p3.cancel("No matching agents selected.");
1295
- process.exit(1);
1724
+ reporter.warn("No installed agents detected; defaulting to all supported agents.");
1296
1725
  }
1726
+ if (chosenAgents.length === 0) reporter.fatal("No matching agents selected.");
1297
1727
  const claudeScope = chosenAgents.some((a) => a.name === "claude") ? scope : null;
1298
1728
  const applyClaudeConfig = () => {
1299
1729
  if (!claudeScope || opts.dryRun) return;
1300
1730
  if (disableClaudeAttribution(claudeScope)) {
1301
- p3.log.info("Claude Code: disabled Co-Authored-By and PR attribution in settings.json.");
1731
+ reporter.info("Claude Code: disabled Co-Authored-By and PR attribution in settings.json.");
1302
1732
  }
1303
1733
  };
1304
1734
  const bypassAgents = await resolveBypassSelection(chosenAgents, opts, interactive);
@@ -1307,7 +1737,7 @@ async function installSkills(opts, interactive) {
1307
1737
  for (const agent of chosenAgents) {
1308
1738
  const target = agent.targets[scope];
1309
1739
  if (!target) {
1310
- p3.log.warn(`${agent.label} has no '${scope}' target - skipping.`);
1740
+ reporter.warn(`${agent.label} has no '${scope}' target - skipping.`);
1311
1741
  continue;
1312
1742
  }
1313
1743
  const skills = inspectSkills();
@@ -1316,7 +1746,7 @@ async function installSkills(opts, interactive) {
1316
1746
  if (!opts.memoryOnly && opts.skills.length) {
1317
1747
  chosenSkills = skills.filter((s2) => opts.skills.includes(s2.name));
1318
1748
  } else if (!opts.memoryOnly && interactive && skills.length > 1) {
1319
- const r = await p3.multiselect({
1749
+ const r = await p4.multiselect({
1320
1750
  message: `Skills for ${agent.label} - all selected; deselect any you don't want`,
1321
1751
  options: skills.map((s2) => {
1322
1752
  const st = skillStatus(join6(target.skills, s2.name), s2.meta);
@@ -1326,8 +1756,8 @@ async function installSkills(opts, interactive) {
1326
1756
  initialValues: skills.map((s2) => s2.name),
1327
1757
  required: false
1328
1758
  });
1329
- if (p3.isCancel(r)) {
1330
- p3.cancel("Aborted.");
1759
+ if (p4.isCancel(r)) {
1760
+ p4.cancel("Aborted.");
1331
1761
  return;
1332
1762
  }
1333
1763
  chosenSkills = skills.filter((s2) => r.includes(s2.name));
@@ -1344,16 +1774,16 @@ async function installSkills(opts, interactive) {
1344
1774
  if (tampered.length) {
1345
1775
  if (opts.keepModified) {
1346
1776
  for (const s2 of tampered) s2.overwrite = false;
1347
- p3.log.warn(`${tampered.length} locally-modified skill(s) will be kept (--keep-modified).`);
1777
+ reporter.warn(`${tampered.length} locally-modified skill(s) will be kept (--keep-modified).`);
1348
1778
  } else if (interactive && !opts.dryRun) {
1349
- const sel = await p3.multiselect({
1779
+ const sel = await p4.multiselect({
1350
1780
  message: `${tampered.length} skill(s) were modified locally since install. Select which to OVERWRITE`,
1351
1781
  options: tampered.map((s2, i) => ({ value: i, label: s2.name, hint: s2.meta.version ? `v${s2.meta.version}` : "modified" })),
1352
1782
  initialValues: tampered.map((_, i) => i),
1353
1783
  required: false
1354
1784
  });
1355
- if (p3.isCancel(sel)) {
1356
- p3.cancel("Aborted.");
1785
+ if (p4.isCancel(sel)) {
1786
+ p4.cancel("Aborted.");
1357
1787
  return;
1358
1788
  }
1359
1789
  tampered.forEach((s2, i) => {
@@ -1407,14 +1837,14 @@ async function installSkills(opts, interactive) {
1407
1837
  }
1408
1838
  }
1409
1839
  if (nInstall + nUpdate + nRemove === 0) {
1410
- p3.note(lines.join("\n"), "Nothing to do");
1840
+ reporter.note(lines.join("\n"), "Nothing to do");
1411
1841
  applyClaudeConfig();
1412
1842
  applyBypassConfig();
1413
1843
  await maybeOfferGitHooks(interactive, opts);
1414
- p3.log.success(`Everything up-to-date - ${nSkip} item(s) unchanged${nKept ? `, ${nKept} kept modified` : ""} (${scope}).`);
1844
+ reporter.success(`Everything up-to-date - ${nSkip} item(s) unchanged${nKept ? `, ${nKept} kept modified` : ""} (${scope}).`);
1415
1845
  return;
1416
1846
  }
1417
- p3.note(lines.join("\n"), opts.dryRun ? "Dry run - planned changes" : "Planned changes");
1847
+ reporter.note(lines.join("\n"), opts.dryRun ? "Dry run - planned changes" : "Planned changes");
1418
1848
  if (interactive && !opts.dryRun) {
1419
1849
  const summary = [
1420
1850
  nInstall && `${nInstall} to install`,
@@ -1422,21 +1852,21 @@ async function installSkills(opts, interactive) {
1422
1852
  nRemove && `${nRemove} to remove`,
1423
1853
  nSkip && `${nSkip} unchanged`
1424
1854
  ].filter(Boolean).join(", ");
1425
- const ok = await p3.confirm({ message: `Apply: ${summary}?` });
1426
- if (p3.isCancel(ok) || !ok) {
1427
- p3.cancel("Aborted.");
1855
+ const ok = await p4.confirm({ message: `Apply: ${summary}?` });
1856
+ if (p4.isCancel(ok) || !ok) {
1857
+ p4.cancel("Aborted.");
1428
1858
  return;
1429
1859
  }
1430
1860
  }
1431
1861
  if (opts.dryRun) {
1432
1862
  applyBypassConfig();
1433
- p3.log.info("Dry run complete - no files written.");
1863
+ reporter.info("Dry run complete - no files written.");
1434
1864
  return;
1435
1865
  }
1436
1866
  const changedAgents = plan.filter(
1437
1867
  (x) => x.skills.some(willCopy) || x.memory.some((m) => memoryStatus(m.src, join6(x.target.memory, m.name)) !== "identical") || x.prune.length > 0
1438
1868
  );
1439
- const s = p3.spinner();
1869
+ const s = reporter.spinner();
1440
1870
  s.start("Installing...");
1441
1871
  let copied = 0;
1442
1872
  try {
@@ -1457,22 +1887,21 @@ async function installSkills(opts, interactive) {
1457
1887
  }
1458
1888
  } catch (err) {
1459
1889
  s.stop("Failed.");
1460
- p3.cancel(`Error while installing: ${err.message}`);
1461
- process.exit(1);
1890
+ reporter.fatal(`Error while installing: ${err.message}`);
1462
1891
  }
1463
1892
  s.stop(`Wrote ${copied} item(s)${nRemove ? `, removed ${nRemove}` : ""}.`);
1464
1893
  applyClaudeConfig();
1465
1894
  applyBypassConfig();
1466
1895
  await maybeOfferGitHooks(interactive, opts);
1467
- p3.log.success(`${nInstall} installed, ${nUpdate} updated/overwritten` + (nRemove ? `, ${nRemove} removed` : "") + (nSkip ? `, ${nSkip} unchanged` : "") + (nKept ? `, ${nKept} kept modified` : "") + ` (${scope}).`);
1896
+ reporter.success(`${nInstall} installed, ${nUpdate} updated/overwritten` + (nRemove ? `, ${nRemove} removed` : "") + (nSkip ? `, ${nSkip} unchanged` : "") + (nKept ? `, ${nKept} kept modified` : "") + ` (${scope}).`);
1468
1897
  if (changedAgents.length) {
1469
1898
  const { known, running } = runningStatus(changedAgents.map((x) => x.agent));
1470
1899
  if (running.size) {
1471
1900
  const names = changedAgents.filter((x) => running.has(x.agent.name)).map((x) => x.agent.label);
1472
- p3.log.warn(`Restart ${names.join(", ")} to apply the changes (running now).`);
1901
+ reporter.warn(`Restart ${names.join(", ")} to apply the changes (running now).`);
1473
1902
  } else if (!known) {
1474
1903
  const names = changedAgents.map((x) => x.agent.label);
1475
- p3.log.info(`If any of these agents are running, restart them to apply the changes: ${names.join(", ")}.`);
1904
+ reporter.info(`If any of these agents are running, restart them to apply the changes: ${names.join(", ")}.`);
1476
1905
  }
1477
1906
  }
1478
1907
  }
@@ -1681,7 +2110,7 @@ import { homedir as homedir5 } from "os";
1681
2110
  import { join as join9 } from "path";
1682
2111
  import { writeFileSync as writeFileSync6 } from "fs";
1683
2112
  import { spawn, spawnSync } from "child_process";
1684
- import * as p4 from "@clack/prompts";
2113
+ import * as p5 from "@clack/prompts";
1685
2114
  var REGISTRY_URL = "https://registry.npmjs.org/enigma-cli/latest";
1686
2115
  var UPDATE_COMMAND = "npm i -g enigma-cli@latest";
1687
2116
  var CACHE_FILE = join9(homedir5(), ".enigma-update-check.json");
@@ -1766,8 +2195,8 @@ async function notifyUpdate(current, interactive) {
1766
2195
  ${renderUpdateBox(current, latest)}
1767
2196
  `);
1768
2197
  if (!interactive) return;
1769
- const ok = await p4.confirm({ message: `Update now with ${UPDATE_COMMAND}?`, initialValue: true });
1770
- if (p4.isCancel(ok) || !ok) return;
2198
+ const ok = await p5.confirm({ message: `Update now with ${UPDATE_COMMAND}?`, initialValue: true });
2199
+ if (p5.isCancel(ok) || !ok) return;
1771
2200
  runUpdate();
1772
2201
  } catch {
1773
2202
  }
@@ -1948,16 +2377,16 @@ async function run(argv) {
1948
2377
  process.exit(await runConfigCli(opts.positionals, opts.scope, interactive));
1949
2378
  }
1950
2379
  if (opts.command === "install") {
1951
- p5.intro("enigma - install agent skills");
2380
+ p6.intro("enigma - install agent skills");
1952
2381
  await installSkills(opts, interactive);
1953
- p5.outro("Done.");
2382
+ p6.outro("Done.");
1954
2383
  await notifyUpdate(version, interactive);
1955
2384
  return;
1956
2385
  }
1957
2386
  if (opts.command === "security") {
1958
- p5.intro("enigma - git security hooks");
2387
+ p6.intro("enigma - git security hooks");
1959
2388
  const done = await setupGitHooks(opts, interactive);
1960
- p5.outro(done ? "Git hooks configured." : "No changes made.");
2389
+ p6.outro(done ? "Git hooks configured." : "No changes made.");
1961
2390
  await notifyUpdate(version, interactive);
1962
2391
  return;
1963
2392
  }
@@ -1966,19 +2395,23 @@ async function run(argv) {
1966
2395
  await notifyUpdate(version, interactive);
1967
2396
  return;
1968
2397
  }
1969
- const { runHomeTui: runHomeTui2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
1970
- await runHomeTui2({
2398
+ const { runHomeTui: runHomeTui3 } = isBun() ? await Promise.resolve().then(() => (init_opentui(), opentui_exports)) : await Promise.resolve().then(() => (init_settings(), settings_exports));
2399
+ await runHomeTui3({
1971
2400
  agents: discoverAgents().map((a) => ({ name: a.name, label: a.label, installed: a.installed })),
1972
2401
  protections: GUARD_PROTECTIONS,
1973
- runAction: async (intent) => {
1974
- if (intent.action === "skills") {
1975
- p5.intro("enigma - install agent skills");
1976
- await installSkills({ ...opts, scope: intent.scope ?? opts.scope, agents: intent.agents ?? [], allAgents: !(intent.agents && intent.agents.length) }, false);
1977
- p5.outro("Done.");
1978
- } else if (intent.action === "security") {
1979
- p5.intro("enigma - git security hooks");
1980
- const done = await setupGitHooks({ ...opts, protections: intent.protections, force: true }, false);
1981
- p5.outro(done ? "Git hooks configured." : "No changes made.");
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 };
1982
2415
  }
1983
2416
  }
1984
2417
  });