enigma-cli 1.1.4 → 1.2.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.
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Backend/API architecture: controller-service-repository layering, API and request optimization, server-side caching (Redis), and Zod boundary validation.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "c442bc9e39a7710cb709ef2abb8d15ecd8aa16ed4f5c8af92b7af6877401cba4"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.1",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Ciphera code style conventions (formatting, naming, imports, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "74f638aec13e8c93257fe1ad604c28b07e9a7c456796a4ceefcc99217d9e7039"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "3d3bbe0602d5bbb4afe37648fe3c2fa39376b1bcbac5d8c441f01fad1e866ed0"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.4.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Core engineering execution policy and harness orchestration (highest-authority rules).",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "c9c69c59516794311cb7b306ed4d4ad971824de3689a39c2b86c7669c73f2e8b"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Senior database architecture policy: query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "c4617ee8d1a57d9621c81bef3093e94de91f79eec0cc0ead41f6d18dd443e623"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "14b0064c8b33a0dc85e51464b05005cf5801c756b1101789a6924b9548420f6b"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Frontend architecture: reusable components, abstraction thresholds, state management, and optimistic UI with rollback.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "33fa1e9f667ef26203a3d6c892121efe12b0cddb706c195492fa97e080fba115"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.2.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "ada4b7eb5bb7e013429e23703c271c0f34b0d76327c059efa148ea2794f96178"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), OWASP Top 10, transport/crypto baseline, secure logging, and agent/MCP/tool-use safety.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "9971e9d9127397d0152e89d24aad3191e2935e55a8483db7fd15f5d4d7a60e7a"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, and regression-first bug fixing.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "d19fa8ec7985ed231478be504d3c80360897f555d0bc0624bea19c091f459fb0"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
6
- "cliVersion": "1.1.4",
6
+ "cliVersion": "1.2.0",
7
7
  "sha": "a33622a2f810ee4cea39824cb1a7ca34b355a917d4224025df50d77dd74f0b3a"
8
8
  }
package/dist/enigma.js CHANGED
@@ -106,7 +106,7 @@ var init_agents = __esm({
106
106
  }
107
107
  },
108
108
  opencode: {
109
- label: "opencode",
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.
@@ -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,63 +531,81 @@ __export(settings_exports, {
530
531
  runHomeTui: () => runHomeTui,
531
532
  runSettingsTui: () => runSettingsTui
532
533
  });
533
- async function runHomeTui(runAction) {
534
- await runTui("home", runAction);
534
+ async function runHomeTui(hub) {
535
+ await runTui({ showActions: true, hub });
535
536
  }
536
537
  async function runSettingsTui() {
537
- await runTui("settings");
538
+ await runTui({ showActions: false });
538
539
  }
539
- async function runTui(initialView, runAction) {
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 restore = () => {
546
- try {
547
- process.stdout.write(LEAVE_ALT);
547
+ const agents = opts.hub?.agents ?? [];
548
+ const protections = opts.hub?.protections ?? [];
549
+ const clear = () => {
550
+ if (fullscreen) try {
551
+ process.stdout.write(CLEAR_SCREEN);
548
552
  } catch {
549
553
  }
550
554
  };
551
555
  for (; ; ) {
552
- const state = { leave: "quit" };
553
- const App = buildApp(React, ink, { initialView, onLeave: (a) => {
554
- state.leave = a;
556
+ const state = { intent: { action: "quit" } };
557
+ const App = buildApp(React, ink, { showActions: opts.showActions, fullscreen, agents, protections, onLeave: (i) => {
558
+ state.intent = i;
555
559
  } });
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);
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();
567
568
  continue;
568
569
  }
569
570
  break;
570
571
  }
571
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
+ });
583
+ }
572
584
  function buildApp(React, ink, opts) {
573
585
  const { useApp, useInput, useStdout } = ink;
574
586
  const Box = ink.Box;
575
587
  const Text = ink.Text;
576
588
  const { useState, useEffect } = React;
577
589
  const h = React.createElement;
578
- const canGoHome = opts.initialView === "home";
590
+ const fill = opts.fullscreen;
591
+ const sideItems = [
592
+ ...CATEGORIES.map((c, i) => ({ kind: "category", catIndex: i, title: c.title })),
593
+ ...opts.showActions ? ACTION_ITEMS.map((a) => ({ kind: "action", ...a })) : []
594
+ ];
579
595
  return function App() {
580
596
  const { exit } = useApp();
581
597
  const { stdout } = useStdout();
582
598
  const [size, setSize] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
583
- const [view, setView] = useState(opts.initialView);
584
- const [homeIndex, setHomeIndex] = useState(0);
599
+ const [mode, setMode] = useState("menu");
585
600
  const [scope, setScope] = useState("global");
586
- const [focusSettings, setFocusSettings] = useState(false);
587
- const [catIndex, setCatIndex] = useState(0);
601
+ const [sideIndex, setSideIndex] = useState(0);
602
+ const [focusRight, setFocusRight] = useState(false);
588
603
  const [setIndex, setSetIndex] = useState(0);
589
- const [, redraw] = useState(0);
604
+ const [pending, setPending] = useState({});
605
+ const [confirm4, setConfirm] = useState(null);
606
+ const [actCursor, setActCursor] = useState(0);
607
+ const [actChecked, setActChecked] = useState({});
608
+ const [actScope, setActScope] = useState("global");
590
609
  useEffect(() => {
591
610
  const onResize = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
592
611
  stdout.on("resize", onResize);
@@ -594,45 +613,112 @@ function buildApp(React, ink, opts) {
594
613
  stdout.off("resize", onResize);
595
614
  };
596
615
  }, [stdout]);
597
- const back = () => {
598
- if (canGoHome) setView("home");
599
- else {
600
- opts.onLeave("quit");
601
- exit();
616
+ const current = sideItems[sideIndex];
617
+ const category = current.kind === "category" ? CATEGORIES[current.catIndex] : null;
618
+ const valueOf = (setting, sc) => {
619
+ const k = stageKey(setting.key, sc);
620
+ return k in pending ? pending[k] : setting.read(sc);
621
+ };
622
+ const isModified = (setting, sc) => {
623
+ const k = stageKey(setting.key, sc);
624
+ return k in pending && pending[k] !== setting.read(sc);
625
+ };
626
+ const dirty = Object.entries(pending).some(([k, v]) => {
627
+ const { key, scope: sc } = parseStageKey(k);
628
+ return SETTING_BY_KEY.get(key)?.read(sc) !== v;
629
+ });
630
+ const persistPending = () => {
631
+ for (const [k, v] of Object.entries(pending)) {
632
+ const { key, scope: sc } = parseStageKey(k);
633
+ const setting = SETTING_BY_KEY.get(key);
634
+ if (setting && setting.read(sc) !== v) setting.write(v, sc);
602
635
  }
603
636
  };
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);
652
+ };
604
653
  useInput((input, key) => {
605
- if (view === "home") {
606
- if (input === "q" || key.escape) {
607
- opts.onLeave("quit");
608
- exit();
654
+ if (confirm4) {
655
+ if (key.escape) {
656
+ setConfirm(null);
609
657
  return;
610
658
  }
611
659
  if (key.upArrow || input === "k") {
612
- setHomeIndex((i) => Math.max(0, i - 1));
660
+ setConfirm((c) => c && { index: Math.max(0, c.index - 1) });
613
661
  return;
614
662
  }
615
663
  if (key.downArrow || input === "j") {
616
- setHomeIndex((i) => Math.min(HOME_ITEMS.length - 1, i + 1));
664
+ setConfirm((c) => c && { index: Math.min(EXIT_OPTIONS.length - 1, c.index + 1) });
617
665
  return;
618
666
  }
619
667
  if (key.return || input === " ") {
620
- const item = HOME_ITEMS[homeIndex];
621
- if (item.key === "settings") {
622
- setView("settings");
623
- setFocusSettings(false);
624
- } else if (item.key === "exit") {
625
- opts.onLeave("quit");
626
- exit();
627
- } else {
628
- opts.onLeave(item.key);
629
- exit();
630
- }
668
+ const index = confirm4.index;
669
+ setConfirm(null);
670
+ if (index === 2) return;
671
+ if (index === 0) persistPending();
672
+ setPending({});
673
+ leave({ action: "quit" });
674
+ }
675
+ return;
676
+ }
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") {
680
+ setMode("menu");
681
+ return;
682
+ }
683
+ if (mode === "skills" && input === "g") {
684
+ setActScope((s) => s === "global" ? "local" : "global");
685
+ return;
686
+ }
687
+ if (key.upArrow || input === "k") {
688
+ setActCursor((i) => Math.max(0, i - 1));
689
+ return;
690
+ }
691
+ 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] }));
698
+ return;
699
+ }
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 });
631
706
  }
632
707
  return;
633
708
  }
634
709
  if (input === "q" || key.escape) {
635
- back();
710
+ dirty ? setConfirm({ index: 0 }) : leave({ action: "quit" });
711
+ return;
712
+ }
713
+ if (input === "s") {
714
+ persistPending();
715
+ setPending({});
716
+ return;
717
+ }
718
+ if (input === "x") {
719
+ persistPending();
720
+ setPending({});
721
+ leave({ action: "quit" });
636
722
  return;
637
723
  }
638
724
  if (input === "g") {
@@ -640,147 +726,218 @@ function buildApp(React, ink, opts) {
640
726
  return;
641
727
  }
642
728
  if (key.tab) {
643
- setFocusSettings((f) => !f);
729
+ if (category) setFocusRight((f) => !f);
644
730
  return;
645
731
  }
646
732
  if (key.leftArrow || input === "h") {
647
- setFocusSettings(false);
733
+ setFocusRight(false);
648
734
  return;
649
735
  }
650
736
  if (key.rightArrow || input === "l") {
651
- setFocusSettings(true);
737
+ if (category) setFocusRight(true);
652
738
  return;
653
739
  }
654
740
  if (key.upArrow || input === "k") {
655
- if (focusSettings) setSetIndex((i) => Math.max(0, i - 1));
741
+ if (focusRight && category) setSetIndex((i) => Math.max(0, i - 1));
656
742
  else {
657
- setCatIndex((i) => Math.max(0, i - 1));
743
+ setSideIndex((i) => Math.max(0, i - 1));
658
744
  setSetIndex(0);
745
+ setFocusRight(false);
659
746
  }
660
747
  return;
661
748
  }
662
749
  if (key.downArrow || input === "j") {
663
- if (focusSettings) setSetIndex((i) => Math.min(CATEGORIES[catIndex].settings.length - 1, i + 1));
750
+ if (focusRight && category) setSetIndex((i) => Math.min(category.settings.length - 1, i + 1));
664
751
  else {
665
- setCatIndex((i) => Math.min(CATEGORIES.length - 1, i + 1));
752
+ setSideIndex((i) => Math.min(sideItems.length - 1, i + 1));
666
753
  setSetIndex(0);
754
+ setFocusRight(false);
667
755
  }
668
756
  return;
669
757
  }
670
758
  if (key.return || input === " ") {
671
- if (!focusSettings) {
672
- setFocusSettings(true);
759
+ if (current.kind === "action") {
760
+ openAction(current.action);
761
+ return;
762
+ }
763
+ if (!focusRight) {
764
+ setFocusRight(true);
673
765
  return;
674
766
  }
675
- const setting = CATEGORIES[catIndex].settings[setIndex];
676
- setting.write(!setting.read(scope), scope);
677
- redraw((n) => n + 1);
767
+ const setting = category.settings[setIndex];
768
+ setPending((p6) => ({ ...p6, [stageKey(setting.key, scope)]: !valueOf(setting, scope) }));
678
769
  }
679
770
  });
680
- const titleBar = (right2) => h(
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(
771
+ const headerRight = confirm4 ? h(Text, { color: "yellow" }, "unsaved changes") : mode === "menu" ? h(
693
772
  Box,
694
773
  {},
695
774
  h(Text, { dimColor: true }, "scope "),
696
775
  h(Text, { bold: true, color: scope === "global" ? "green" : "yellow" }, scope),
697
- h(Text, { dimColor: true }, " (g to change)")
776
+ h(Text, { dimColor: true }, " (g)"),
777
+ dirty ? h(Text, { color: "yellow" }, " * unsaved") : null
778
+ ) : h(Text, { dimColor: true }, mode === "skills" ? "install" : "security");
779
+ const titleBar = h(
780
+ Box,
781
+ { width: size.columns, paddingX: 1, justifyContent: "space-between" },
782
+ h(Text, { bold: true, color: "cyan" }, "enigma"),
783
+ headerRight
698
784
  );
785
+ let content;
786
+ if (confirm4) {
787
+ 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" })),
802
+ cursor: actCursor,
803
+ checked: actChecked,
804
+ fill
805
+ });
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
+ content = h(Box, fill ? { flexGrow: 1 } : {}, renderSidebar(h, Box, Text, sideItems, sideIndex, focusRight, sidebarWidth), panel);
810
+ }
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`;
812
+ const footer = h(Box, { width: size.columns, paddingX: 1 }, h(Text, { dimColor: true }, footerText));
699
813
  return h(
700
814
  Box,
701
- { width: size.columns, height: size.rows, flexDirection: "column" },
702
- titleBar(right),
703
- h(Box, { flexGrow: 1 }, body),
815
+ { width: size.columns, ...fill ? { height: size.rows } : {}, flexDirection: "column" },
816
+ titleBar,
817
+ content,
704
818
  footer
705
819
  );
706
820
  };
707
821
  }
708
- function renderHome(h, Box, Text, homeIndex) {
709
- const items = HOME_ITEMS.map((item, i) => h(
710
- Box,
711
- { key: item.key, flexDirection: "column" },
712
- h(Text, { inverse: i === homeIndex, bold: i === homeIndex }, ` ${item.label} `),
713
- i === homeIndex && item.hint ? h(Text, { key: "h", dimColor: true }, ` ${item.hint}`) : null
714
- ));
822
+ function renderSidebar(h, Box, Text, items, index, focusRight, width) {
715
823
  return h(Box, {
716
824
  flexDirection: "column",
717
825
  borderStyle: "round",
718
- borderColor: "cyan",
826
+ borderColor: focusRight ? "gray" : "cyan",
719
827
  paddingX: 1,
720
- flexGrow: 1
828
+ width,
829
+ marginRight: 1
721
830
  }, [
722
- h(Text, { key: "__t", bold: true, color: "cyan" }, "What would you like to do?"),
723
- h(Box, { key: "__sp", marginTop: 1, flexDirection: "column" }, items)
831
+ h(Text, { key: "__t", bold: true, dimColor: true }, "MENU"),
832
+ ...items.map((it, i) => h(Text, {
833
+ key: String(i),
834
+ inverse: !focusRight && i === index,
835
+ color: i === index ? "cyan" : void 0
836
+ }, ` ${it.title} `))
724
837
  ]);
725
838
  }
726
- function renderSettings(h, Box, Text, s) {
727
- const category = CATEGORIES[s.catIndex];
728
- const settings = category.settings;
729
- const focused = settings[s.setIndex];
730
- const sidebarWidth = Math.min(28, Math.max(18, Math.floor(s.size.columns * 0.3)));
731
- const sidebar = h(Box, {
839
+ function renderConfirm(h, Box, Text, index, fill) {
840
+ return h(
841
+ Box,
842
+ { justifyContent: "center", ...fill ? { flexGrow: 1, alignItems: "center" } : {} },
843
+ h(Box, {
844
+ flexDirection: "column",
845
+ borderStyle: "round",
846
+ borderColor: "yellow",
847
+ paddingX: 2,
848
+ paddingY: 1
849
+ }, [
850
+ h(Text, { key: "__t", bold: true, color: "yellow" }, "You have unsaved changes"),
851
+ h(
852
+ Box,
853
+ { key: "__o", marginTop: 1, flexDirection: "column" },
854
+ EXIT_OPTIONS.map((o, i) => h(Text, { key: o, inverse: i === index, bold: i === index }, ` ${o} `))
855
+ )
856
+ ])
857
+ );
858
+ }
859
+ function renderChecklist(h, Box, Text, s) {
860
+ const rows = s.items.map((it, i) => {
861
+ const on = !!s.checked[it.key];
862
+ const selected = i === s.cursor;
863
+ return h(
864
+ Box,
865
+ { key: it.key, justifyContent: "space-between" },
866
+ h(Text, { inverse: selected, bold: selected }, ` ${on ? "[x]" : "[ ]"} ${it.label} `),
867
+ h(Text, { dimColor: true }, `${it.hint} `)
868
+ );
869
+ });
870
+ return h(Box, {
732
871
  flexDirection: "column",
733
872
  borderStyle: "round",
734
- borderColor: s.focusSettings ? "gray" : "cyan",
873
+ borderColor: "cyan",
735
874
  paddingX: 1,
736
- width: sidebarWidth,
737
- marginRight: 1
875
+ ...s.fill ? { flexGrow: 1 } : {}
738
876
  }, [
739
- h(Text, { key: "__t", bold: true, dimColor: true }, "CATEGORIES"),
740
- ...CATEGORIES.map((c, i) => h(Text, {
741
- key: c.title,
742
- inverse: !s.focusSettings && i === s.catIndex,
743
- color: i === s.catIndex ? "cyan" : void 0
744
- }, ` ${c.title} `))
877
+ h(Text, { key: "__t", bold: true, color: "cyan" }, s.title),
878
+ h(Text, { key: "__bl", dimColor: true }, s.blurb),
879
+ h(Box, { key: "__rows", marginTop: 1, flexDirection: "column" }, rows),
880
+ ...s.fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : []
745
881
  ]);
746
- const rows = settings.map((setting, i) => {
747
- const on = setting.read(s.scope);
748
- const selected = s.focusSettings && i === s.setIndex;
882
+ }
883
+ function renderCategoryPanel(h, Box, Text, s) {
884
+ const focused = s.category.settings[s.setIndex];
885
+ const rows = s.category.settings.map((setting, i) => {
886
+ const on = s.valueOf(setting, s.scope);
887
+ const modified = s.isModified(setting, s.scope);
888
+ const selected = s.focusRight && i === s.setIndex;
749
889
  return h(
750
890
  Box,
751
891
  { key: setting.key, justifyContent: "space-between" },
752
892
  h(Text, { inverse: selected, bold: selected }, ` ${setting.label}${setting.globalOnly ? " (global)" : ""} `),
753
- h(Text, { bold: true, color: on ? "green" : "gray" }, `${valueLabel(on)} `)
893
+ h(Text, { bold: true, color: modified ? "yellow" : on ? "green" : "gray" }, `${valueLabel(on)}${modified ? " *" : ""} `)
754
894
  );
755
895
  });
756
- const panel = h(Box, {
896
+ return h(Box, {
757
897
  flexDirection: "column",
758
898
  borderStyle: "round",
759
- borderColor: s.focusSettings ? "cyan" : "gray",
899
+ borderColor: s.focusRight ? "cyan" : "gray",
760
900
  paddingX: 1,
761
901
  flexGrow: 1
762
902
  }, [
763
- h(Text, { key: "__b", bold: true, color: "cyan" }, category.title),
764
- h(Text, { key: "__bl", dimColor: true }, category.blurb),
903
+ h(Text, { key: "__b", bold: true, color: "cyan" }, s.category.title),
904
+ h(Text, { key: "__bl", dimColor: true }, s.category.blurb),
765
905
  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)
906
+ ...s.fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : [],
907
+ h(Text, { key: "__hint", marginTop: 1, dimColor: true, wrap: "truncate-end" }, focused.hint)
768
908
  ]);
769
- return h(Box, { flexGrow: 1 }, sidebar, panel);
770
909
  }
771
- var ENTER_ALT, LEAVE_ALT, HOME_ITEMS;
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;
772
924
  var init_settings = __esm({
773
925
  "src/tui/settings.ts"() {
774
926
  "use strict";
927
+ init_config();
775
928
  init_settings_registry();
776
- ENTER_ALT = "\x1B[?1049h\x1B[?25l";
777
- LEAVE_ALT = "\x1B[?25h\x1B[?1049l";
778
- HOME_ITEMS = [
779
- { key: "settings", label: "Configure settings", hint: "emoji, attribution, permission bypass, ..." },
780
- { key: "skills", label: "Install agent skills", hint: "Claude Code, Codex, opencode" },
781
- { key: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" },
782
- { key: "exit", label: "Exit", hint: "leave enigma" }
929
+ CLEAR_SCREEN = "\x1B[2J\x1B[H";
930
+ SETTING_BY_KEY = new Map(ALL_SETTINGS.map((s) => [s.key, s]));
931
+ stageKey = (key, scope) => `${scope}/${key}`;
932
+ parseStageKey = (composite) => {
933
+ const i = composite.indexOf("/");
934
+ return { scope: composite.slice(0, i), key: composite.slice(i + 1) };
935
+ };
936
+ ACTION_ITEMS = [
937
+ { action: "skills", title: "Install agent skills", blurb: "Claude Code, Codex, OpenCode" },
938
+ { action: "security", title: "Git security hooks", blurb: "block secrets, .env, node_modules on commit" }
783
939
  ];
940
+ EXIT_OPTIONS = ["Save & exit", "Exit without saving", "Cancel"];
784
941
  }
785
942
  });
786
943
 
@@ -1320,6 +1477,9 @@ async function installSkills(opts, interactive) {
1320
1477
  }
1321
1478
  }
1322
1479
 
1480
+ // src/cli.ts
1481
+ init_agents();
1482
+
1323
1483
  // src/guard.ts
1324
1484
  import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1325
1485
  import { execFileSync as execFileSync3 } from "child_process";
@@ -1725,7 +1885,7 @@ Usage:
1725
1885
 
1726
1886
  Commands:
1727
1887
  (none) Interactive hub: configure settings or set up features
1728
- install Install/update agent skills (Claude Code, Codex, opencode)
1888
+ install Install/update agent skills (Claude Code, Codex, OpenCode)
1729
1889
  security Set up git security hooks in the current repo
1730
1890
  guard [--all] Run the commit guard (staged files, or --all for every tracked file)
1731
1891
  config [key val] Configure settings: no args opens the interactive menu;
@@ -1734,7 +1894,7 @@ Commands:
1734
1894
  check Integrity gate: verify skills are well-formed and sealed
1735
1895
  help, version
1736
1896
 
1737
- Config keys: commit-emoji, update-notifier, claude-attribution,
1897
+ Config keys: commit-emoji, update-notifier, fullscreen, claude-attribution,
1738
1898
  bypass-claude, bypass-codex, bypass-opencode
1739
1899
 
1740
1900
  Install options:
@@ -1807,15 +1967,19 @@ async function run(argv) {
1807
1967
  return;
1808
1968
  }
1809
1969
  const { runHomeTui: runHomeTui2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
1810
- await runHomeTui2(async (action) => {
1811
- if (action === "skills") {
1812
- p5.intro("enigma - install agent skills");
1813
- await installSkills(opts, interactive);
1814
- p5.outro("Done.");
1815
- } else if (action === "security") {
1816
- p5.intro("enigma - git security hooks");
1817
- const done = await setupGitHooks(opts, interactive);
1818
- p5.outro(done ? "Git hooks configured." : "No changes made.");
1970
+ await runHomeTui2({
1971
+ agents: discoverAgents().map((a) => ({ name: a.name, label: a.label, installed: a.installed })),
1972
+ 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.");
1982
+ }
1819
1983
  }
1820
1984
  });
1821
1985
  await notifyUpdate(version, interactive);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {