enigma-cli 1.1.1 → 1.1.3

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.1",
6
+ "cliVersion": "1.1.3",
7
7
  "sha": "c442bc9e39a7710cb709ef2abb8d15ecd8aa16ed4f5c8af92b7af6877401cba4"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.0",
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.1",
6
+ "cliVersion": "1.1.3",
7
7
  "sha": "f8602bb79fbbe063ab39fbd59d0b7844a22c3a1583fcd11c1b4f98a2fe8ddc86"
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.1",
6
+ "cliVersion": "1.1.3",
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.1",
6
+ "cliVersion": "1.1.3",
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.1",
6
+ "cliVersion": "1.1.3",
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.1",
6
+ "cliVersion": "1.1.3",
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.1",
6
+ "cliVersion": "1.1.3",
7
7
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.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.1",
6
+ "cliVersion": "1.1.3",
7
7
  "sha": "b0355b0e15f9f528d32adf19f0722d2727cd64d6b3544307ecc7a3141338f023"
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.1",
6
+ "cliVersion": "1.1.3",
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.1",
6
+ "cliVersion": "1.1.3",
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.1",
6
+ "cliVersion": "1.1.3",
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.1",
6
+ "cliVersion": "1.1.3",
7
7
  "sha": "a33622a2f810ee4cea39824cb1a7ca34b355a917d4224025df50d77dd74f0b3a"
8
8
  }
package/dist/enigma.js CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { dirname as dirname4, join as join10 } from "path";
5
- import { fileURLToPath as fileURLToPath4 } from "url";
6
- import * as p5 from "@clack/prompts";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
7
11
 
8
12
  // src/util.ts
9
13
  import { existsSync, statSync, readFileSync } from "fs";
@@ -27,58 +31,20 @@ function isOnPath(bin) {
27
31
  const exts = sep === "\\" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()) : [""];
28
32
  return dirs.some((d) => exts.some((ext) => existsSync(join(d, bin + ext))));
29
33
  }
30
-
31
- // src/skills.ts
32
- import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4, cpSync as cpSync2, mkdirSync as mkdirSync4, rmSync } from "fs";
33
- import { dirname as dirname2, join as join6, resolve as resolve2, relative as relative2, sep as sep2 } from "path";
34
- import { fileURLToPath as fileURLToPath2 } from "url";
35
- import { createHash } from "crypto";
36
- import * as p3 from "@clack/prompts";
34
+ var init_util = __esm({
35
+ "src/util.ts"() {
36
+ "use strict";
37
+ }
38
+ });
37
39
 
38
40
  // src/agents.ts
39
41
  import { homedir } from "os";
40
42
  import { join as join2 } from "path";
41
43
  import { existsSync as existsSync2 } from "fs";
42
44
  import { execFileSync } from "child_process";
43
- var HOME = homedir();
44
- var MANAGED_PROVIDER = "FJRG2007/enigma";
45
- var LEGACY_PROVIDERS = ["FJRG2007"];
46
45
  function isManagedProvider(provider) {
47
46
  return provider === MANAGED_PROVIDER || typeof provider === "string" && LEGACY_PROVIDERS.includes(provider);
48
47
  }
49
- var AGENTS = {
50
- claude: {
51
- label: "Claude Code",
52
- memoryFile: "CLAUDE.md",
53
- detect: { bins: ["claude"], dirs: [join2(HOME, ".claude")] },
54
- targets: {
55
- global: { skills: join2(HOME, ".claude", "skills"), memory: join2(HOME, ".claude") },
56
- local: { skills: join2(process.cwd(), ".claude", "skills"), memory: process.cwd() }
57
- }
58
- },
59
- codex: {
60
- label: "OpenAI Codex",
61
- memoryFile: "AGENTS.md",
62
- // Codex reads AGENTS.md from its home (~/.codex) and project root, but
63
- // discovers skills from the shared `.agents/skills` location, not ~/.codex/skills.
64
- detect: { bins: ["codex"], dirs: [join2(HOME, ".codex")] },
65
- targets: {
66
- global: { skills: join2(HOME, ".agents", "skills"), memory: join2(HOME, ".codex") },
67
- local: { skills: join2(process.cwd(), ".agents", "skills"), memory: process.cwd() }
68
- }
69
- },
70
- opencode: {
71
- label: "opencode",
72
- memoryFile: "AGENTS.md",
73
- // opencode reads AGENTS.md from ~/.config/opencode (global) or the project
74
- // root (local); skills from ~/.config/opencode/skills and .opencode/skills.
75
- detect: { bins: ["opencode"], dirs: [join2(HOME, ".config", "opencode"), join2(HOME, ".opencode")] },
76
- targets: {
77
- global: { skills: join2(HOME, ".config", "opencode", "skills"), memory: join2(HOME, ".config", "opencode") },
78
- local: { skills: join2(process.cwd(), ".opencode", "skills"), memory: process.cwd() }
79
- }
80
- }
81
- };
82
48
  function isInstalled(agent) {
83
49
  const det = agent.detect || {};
84
50
  return (det.dirs || []).some((d) => existsSync2(d)) || (det.bins || []).some((b) => isOnPath(b));
@@ -110,129 +76,49 @@ function runningStatus(agents) {
110
76
  }
111
77
  return { known: true, running };
112
78
  }
113
-
114
- // src/security.ts
115
- import { existsSync as existsSync3, mkdirSync, cpSync, writeFileSync, chmodSync } from "fs";
116
- import { dirname, join as join3, resolve, relative } from "path";
117
- import { fileURLToPath } from "url";
118
- import { execFileSync as execFileSync2 } from "child_process";
119
- import * as p from "@clack/prompts";
120
- var __dirname = dirname(fileURLToPath(import.meta.url));
121
- function findGuardSrc() {
122
- const candidates = [
123
- join3(__dirname, "guard.js"),
124
- join3(__dirname, "..", "dist", "guard.js")
125
- ];
126
- return candidates.find((c) => existsSync3(c)) ?? null;
127
- }
128
- var GUARD_PROTECTIONS = [
129
- { value: "secrets", label: "Block committed secrets", hint: "API keys, tokens, private keys" },
130
- { value: "envFiles", label: "Block .env files", hint: "allows .env.example / .sample / .template" },
131
- { value: "depDirs", label: "Block dependency/cache dirs", hint: "node_modules, __pycache__, venv" },
132
- { value: "generatedDirs", label: "Warn on generated dirs", hint: "dist, build, .next, coverage" },
133
- { value: "junkFiles", label: "Warn on log / OS junk files", hint: ".log, .DS_Store, Thumbs.db" },
134
- { value: "largeFiles", label: "Warn on files over 5 MB", hint: "oversized blobs" }
135
- ];
136
- function findGitRoot(start) {
137
- let dir = resolve(start);
138
- for (; ; ) {
139
- if (existsSync3(join3(dir, ".git"))) return dir;
140
- const parent = dirname(dir);
141
- if (parent === dir) return null;
142
- dir = parent;
143
- }
144
- }
145
- function currentHooksPath(root) {
146
- try {
147
- return execFileSync2("git", ["-C", root, "config", "--get", "core.hooksPath"], { encoding: "utf8" }).trim();
148
- } catch {
149
- return "";
150
- }
151
- }
152
- async function setupGitHooks(opts, interactive) {
153
- const root = findGitRoot(process.cwd());
154
- if (!root) {
155
- p.log.error("Not inside a git repository (no .git found). Run this from your project root.");
156
- return false;
157
- }
158
- const guardSrc = findGuardSrc();
159
- if (!guardSrc) {
160
- p.log.error("Cannot find the built guard (dist/guard.js). Run 'npm run build' first.");
161
- return false;
162
- }
163
- const current = currentHooksPath(root);
164
- if (current && current !== ".githooks" && !opts.force) {
165
- p.log.warn(`core.hooksPath is already set to '${current}'.`);
166
- if (interactive) {
167
- const ok = await p.confirm({ message: `Override existing core.hooksPath '${current}' with '.githooks'?` });
168
- if (p.isCancel(ok) || !ok) {
169
- p.log.info("Left git hooks unchanged.");
170
- return false;
79
+ var HOME, MANAGED_PROVIDER, LEGACY_PROVIDERS, AGENTS;
80
+ var init_agents = __esm({
81
+ "src/agents.ts"() {
82
+ "use strict";
83
+ init_util();
84
+ HOME = homedir();
85
+ MANAGED_PROVIDER = "FJRG2007/enigma";
86
+ LEGACY_PROVIDERS = ["FJRG2007"];
87
+ AGENTS = {
88
+ claude: {
89
+ label: "Claude Code",
90
+ memoryFile: "CLAUDE.md",
91
+ detect: { bins: ["claude"], dirs: [join2(HOME, ".claude")] },
92
+ targets: {
93
+ global: { skills: join2(HOME, ".claude", "skills"), memory: join2(HOME, ".claude") },
94
+ local: { skills: join2(process.cwd(), ".claude", "skills"), memory: process.cwd() }
95
+ }
96
+ },
97
+ codex: {
98
+ label: "OpenAI Codex",
99
+ memoryFile: "AGENTS.md",
100
+ // Codex reads AGENTS.md from its home (~/.codex) and project root, but
101
+ // discovers skills from the shared `.agents/skills` location, not ~/.codex/skills.
102
+ detect: { bins: ["codex"], dirs: [join2(HOME, ".codex")] },
103
+ targets: {
104
+ global: { skills: join2(HOME, ".agents", "skills"), memory: join2(HOME, ".codex") },
105
+ local: { skills: join2(process.cwd(), ".agents", "skills"), memory: process.cwd() }
106
+ }
107
+ },
108
+ opencode: {
109
+ label: "opencode",
110
+ memoryFile: "AGENTS.md",
111
+ // opencode reads AGENTS.md from ~/.config/opencode (global) or the project
112
+ // root (local); skills from ~/.config/opencode/skills and .opencode/skills.
113
+ detect: { bins: ["opencode"], dirs: [join2(HOME, ".config", "opencode"), join2(HOME, ".opencode")] },
114
+ targets: {
115
+ global: { skills: join2(HOME, ".config", "opencode", "skills"), memory: join2(HOME, ".config", "opencode") },
116
+ local: { skills: join2(process.cwd(), ".opencode", "skills"), memory: process.cwd() }
117
+ }
171
118
  }
172
- } else {
173
- p.log.info("Re-run with --force to override.");
174
- return false;
175
- }
176
- }
177
- let enabled = opts.protections;
178
- if (!enabled && interactive) {
179
- const r = await p.multiselect({
180
- message: "Which protections should the commit guard enforce?",
181
- options: GUARD_PROTECTIONS,
182
- initialValues: GUARD_PROTECTIONS.map((o) => o.value),
183
- required: true
184
- });
185
- if (p.isCancel(r)) {
186
- p.log.info("Left git hooks unchanged.");
187
- return false;
188
- }
189
- enabled = r;
119
+ };
190
120
  }
191
- const config = {};
192
- for (const o of GUARD_PROTECTIONS) config[o.value] = enabled ? enabled.includes(o.value) : true;
193
- const hooksDir = join3(root, ".githooks");
194
- mkdirSync(hooksDir, { recursive: true });
195
- cpSync(guardSrc, join3(hooksDir, "guard.mjs"), { force: true });
196
- writeFileSync(join3(hooksDir, "enigma-guard.json"), JSON.stringify(config, null, 2) + "\n");
197
- const shimPath = join3(hooksDir, "pre-commit");
198
- const shim = [
199
- "#!/bin/sh",
200
- "# Managed by enigma (enigma-cli) - blocks committed secrets, .env files, and dependency dirs.",
201
- "# Toggle protections in .githooks/enigma-guard.json. Bypass once: git commit --no-verify",
202
- 'exec node "$(git rev-parse --show-toplevel)/.githooks/guard.mjs" "$@"',
203
- ""
204
- ].join("\n");
205
- writeFileSync(shimPath, shim);
206
- try {
207
- chmodSync(shimPath, 493);
208
- } catch {
209
- }
210
- try {
211
- chmodSync(join3(hooksDir, "guard.mjs"), 493);
212
- } catch {
213
- }
214
- try {
215
- execFileSync2("git", ["-C", root, "config", "core.hooksPath", ".githooks"]);
216
- } catch (err) {
217
- p.log.error(`Failed to set core.hooksPath: ${err.message}`);
218
- return false;
219
- }
220
- const on = Object.entries(config).filter(([, v]) => v).map(([k]) => k);
221
- p.log.success(`Git security hooks installed in ${relative(process.cwd(), hooksDir) || ".githooks"} (core.hooksPath set).`);
222
- p.log.info(`Enforcing: ${on.join(", ") || "nothing"}. Commit .githooks/ so your team inherits it.`);
223
- if (isOnPath("gh")) {
224
- p.log.info("GitHub CLI (gh) detected: these hooks also run for commits made via gh, since gh uses git underneath.");
225
- }
226
- return true;
227
- }
228
- async function maybeOfferGitHooks(interactive, opts) {
229
- if (!interactive || opts.security) return;
230
- const root = findGitRoot(process.cwd());
231
- if (!root) return;
232
- if (currentHooksPath(root) === ".githooks") return;
233
- const ok = await p.confirm({ message: "Set up git security hooks here too (block secrets, .env, node_modules)?" });
234
- if (!p.isCancel(ok) && ok) await setupGitHooks({ ...opts, protections: void 0 }, interactive);
235
- }
121
+ });
236
122
 
237
123
  // src/claude.ts
238
124
  import { homedir as homedir2 } from "os";
@@ -257,6 +143,54 @@ function disableClaudeAttribution(scope) {
257
143
  writeFileSync2(path, JSON.stringify(next, null, 2) + "\n");
258
144
  return true;
259
145
  }
146
+ function getClaudeAttribution(scope) {
147
+ const current = readJson(claudeSettingsPath(scope)) || {};
148
+ const attribution = current.attribution;
149
+ const disabled = Boolean(attribution) && attribution.commit === "" && attribution.pr === "" && current.includeCoAuthoredBy === false;
150
+ return !disabled;
151
+ }
152
+ function setClaudeAttribution(scope, enabled) {
153
+ if (!enabled) return disableClaudeAttribution(scope);
154
+ const path = claudeSettingsPath(scope);
155
+ const current = readJson(path) || {};
156
+ const attribution = typeof current.attribution === "object" && current.attribution !== null ? { ...current.attribution } : {};
157
+ let changed = false;
158
+ if (attribution.commit === "") {
159
+ delete attribution.commit;
160
+ changed = true;
161
+ }
162
+ if (attribution.pr === "") {
163
+ delete attribution.pr;
164
+ changed = true;
165
+ }
166
+ if (current.includeCoAuthoredBy === false) changed = true;
167
+ if (!changed) return false;
168
+ const next = { ...current };
169
+ if (Object.keys(attribution).length) next.attribution = attribution;
170
+ else delete next.attribution;
171
+ delete next.includeCoAuthoredBy;
172
+ writeClaudeSettings(path, next);
173
+ return true;
174
+ }
175
+ function getClaudeBypass(scope) {
176
+ const current = readJson(claudeSettingsPath(scope)) || {};
177
+ const permissions = current.permissions;
178
+ return Boolean(permissions) && permissions.defaultMode === "bypassPermissions";
179
+ }
180
+ function setClaudeBypass(scope, on, dryRun) {
181
+ if (on) return enableClaudeBypass(scope, dryRun);
182
+ const path = claudeSettingsPath(scope);
183
+ const current = readJson(path) || {};
184
+ const permissions = typeof current.permissions === "object" && current.permissions !== null ? { ...current.permissions } : {};
185
+ if (permissions.defaultMode !== "bypassPermissions") return { path, changed: false };
186
+ if (dryRun) return { path, changed: true };
187
+ delete permissions.defaultMode;
188
+ const next = { ...current };
189
+ if (Object.keys(permissions).length) next.permissions = permissions;
190
+ else delete next.permissions;
191
+ writeClaudeSettings(path, next);
192
+ return { path, changed: true };
193
+ }
260
194
  function enableClaudeBypass(scope, dryRun) {
261
195
  const path = claudeSettingsPath(scope);
262
196
  const current = readJson(path) || {};
@@ -264,19 +198,26 @@ function enableClaudeBypass(scope, dryRun) {
264
198
  if (permissions.defaultMode === "bypassPermissions") return { path, changed: false };
265
199
  if (dryRun) return { path, changed: true };
266
200
  const next = { ...current, permissions: { ...permissions, defaultMode: "bypassPermissions" } };
201
+ writeClaudeSettings(path, next);
202
+ return { path, changed: true };
203
+ }
204
+ function writeClaudeSettings(path, data) {
267
205
  const dir = join4(path, "..");
268
206
  if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
269
- writeFileSync2(path, JSON.stringify(next, null, 2) + "\n");
270
- return { path, changed: true };
207
+ writeFileSync2(path, JSON.stringify(data, null, 2) + "\n");
271
208
  }
209
+ var init_claude = __esm({
210
+ "src/claude.ts"() {
211
+ "use strict";
212
+ init_util();
213
+ }
214
+ });
272
215
 
273
216
  // src/permissions.ts
274
217
  import { homedir as homedir3 } from "os";
275
218
  import { join as join5 } from "path";
276
219
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
277
220
  import * as p2 from "@clack/prompts";
278
- var BYPASS_SUPPORTED = ["claude", "codex", "opencode"];
279
- var BYPASS_DEFAULT_ON = /* @__PURE__ */ new Set(["claude", "codex"]);
280
221
  async function resolveBypassSelection(candidates, opts, interactive) {
281
222
  const supported = candidates.filter((a) => BYPASS_SUPPORTED.includes(a.name));
282
223
  if (!supported.length || opts.noBypass) return [];
@@ -323,6 +264,74 @@ function enableFor(name, scope, dryRun) {
323
264
  return null;
324
265
  }
325
266
  }
267
+ function getBypass(name, scope) {
268
+ switch (name) {
269
+ case "claude":
270
+ return getClaudeBypass(scope);
271
+ case "codex":
272
+ return getCodexBypass();
273
+ case "opencode":
274
+ return getOpencodeBypass(scope);
275
+ default:
276
+ return false;
277
+ }
278
+ }
279
+ function setBypass(name, scope, on, dryRun) {
280
+ switch (name) {
281
+ case "claude":
282
+ return setClaudeBypass(scope, on, dryRun);
283
+ case "codex":
284
+ return on ? enableCodexBypass(dryRun) : disableCodexBypass(dryRun);
285
+ case "opencode":
286
+ return on ? enableOpencodeBypass(scope, dryRun) : disableOpencodeBypass(scope, dryRun);
287
+ default:
288
+ return null;
289
+ }
290
+ }
291
+ function getCodexBypass() {
292
+ const path = join5(homedir3(), ".codex", "config.toml");
293
+ const content = existsSync4(path) ? readFileSync2(path, "utf8") : "";
294
+ return getTomlTopLevelKey(content, "approval_policy") === '"never"';
295
+ }
296
+ function disableCodexBypass(dryRun) {
297
+ const path = join5(homedir3(), ".codex", "config.toml");
298
+ const before = existsSync4(path) ? readFileSync2(path, "utf8") : "";
299
+ let after = removeTomlTopLevelKey(before, "approval_policy");
300
+ after = removeTomlTopLevelKey(after, "sandbox_mode");
301
+ const changed = after !== before;
302
+ if (changed && !dryRun) writeFileSync3(path, after);
303
+ return { path, changed };
304
+ }
305
+ function getOpencodeBypass(scope) {
306
+ const path = opencodeConfigPath(scope);
307
+ const perm = (readJson(path) || {}).permission;
308
+ return perm === "allow" || typeof perm === "object" && perm !== null && perm["*"] === "allow";
309
+ }
310
+ function disableOpencodeBypass(scope, dryRun) {
311
+ const path = opencodeConfigPath(scope);
312
+ const current = readJson(path) || {};
313
+ const perm = current.permission;
314
+ if (!getOpencodeBypass(scope)) return { path, changed: false };
315
+ if (dryRun) return { path, changed: true };
316
+ const next = { ...current };
317
+ if (typeof perm === "object" && perm !== null) {
318
+ const rest = {};
319
+ for (const k of Object.keys(perm)) {
320
+ if (k !== "*") rest[k] = perm[k];
321
+ }
322
+ if (Object.keys(rest).length) next.permission = rest;
323
+ else delete next.permission;
324
+ } else {
325
+ delete next.permission;
326
+ }
327
+ const dir = join5(path, "..");
328
+ if (!isDir(dir)) mkdirSync3(dir, { recursive: true });
329
+ writeFileSync3(path, JSON.stringify(next, null, 2) + "\n");
330
+ return { path, changed: true };
331
+ }
332
+ function opencodeConfigPath(scope) {
333
+ return scope === "global" ? join5(homedir3(), ".config", "opencode", "opencode.json") : join5(process.cwd(), "opencode.json");
334
+ }
326
335
  function enableCodexBypass(dryRun) {
327
336
  const path = join5(homedir3(), ".codex", "config.toml");
328
337
  const before = existsSync4(path) ? readFileSync2(path, "utf8") : "";
@@ -337,7 +346,7 @@ function enableCodexBypass(dryRun) {
337
346
  return { path, changed };
338
347
  }
339
348
  function enableOpencodeBypass(scope, dryRun) {
340
- const path = scope === "global" ? join5(homedir3(), ".config", "opencode", "opencode.json") : join5(process.cwd(), "opencode.json");
349
+ const path = opencodeConfigPath(scope);
341
350
  const current = readJson(path) || {};
342
351
  const perm = current.permission;
343
352
  const alreadyAllowAll = perm === "allow" || typeof perm === "object" && perm !== null && perm["*"] === "allow";
@@ -372,12 +381,416 @@ function setTomlTopLevelKey(content, key, tomlValue) {
372
381
  lines.splice(insertAt, 0, ...followsTable ? [assign, ""] : [assign]);
373
382
  return normalizeTrailingNewline(lines.join("\n"));
374
383
  }
384
+ function getTomlTopLevelKey(content, key) {
385
+ const lines = content.split("\n");
386
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
387
+ const scanEnd = firstTable === -1 ? lines.length : firstTable;
388
+ const keyRe = new RegExp(`^\\s*${key}\\s*=\\s*(.+?)\\s*$`);
389
+ for (let i = 0; i < scanEnd; i++) {
390
+ const m = keyRe.exec(lines[i]);
391
+ if (m) return m[1];
392
+ }
393
+ return null;
394
+ }
395
+ function removeTomlTopLevelKey(content, key) {
396
+ if (content.trim() === "") return content;
397
+ const lines = content.split("\n");
398
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
399
+ const scanEnd = firstTable === -1 ? lines.length : firstTable;
400
+ const keyRe = new RegExp(`^\\s*${key}\\s*=`);
401
+ const kept = lines.filter((l, i) => !(i < scanEnd && keyRe.test(l)));
402
+ if (kept.length === lines.length) return content;
403
+ return normalizeTrailingNewline(kept.join("\n"));
404
+ }
375
405
  function normalizeTrailingNewline(s) {
376
406
  return `${s.replace(/\s+$/, "")}
377
407
  `;
378
408
  }
409
+ var BYPASS_SUPPORTED, BYPASS_DEFAULT_ON;
410
+ var init_permissions = __esm({
411
+ "src/permissions.ts"() {
412
+ "use strict";
413
+ init_agents();
414
+ init_util();
415
+ init_claude();
416
+ BYPASS_SUPPORTED = ["claude", "codex", "opencode"];
417
+ BYPASS_DEFAULT_ON = /* @__PURE__ */ new Set(["claude", "codex"]);
418
+ }
419
+ });
420
+
421
+ // src/config.ts
422
+ import { homedir as homedir4 } from "os";
423
+ import { join as join8 } from "path";
424
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
425
+ function configPath(scope) {
426
+ return scope === "global" ? join8(homedir4(), CONFIG_FILE) : join8(process.cwd(), CONFIG_FILE);
427
+ }
428
+ function readConfig() {
429
+ const sources = [];
430
+ let config = { ...CONFIG_DEFAULTS };
431
+ for (const scope of ["global", "local"]) {
432
+ const path = configPath(scope);
433
+ const raw = existsSync6(path) ? readJson(path) : null;
434
+ if (raw) {
435
+ config = { ...config, ...raw };
436
+ sources.push(path);
437
+ }
438
+ }
439
+ return { config, sources };
440
+ }
441
+ function setEnigmaToggle(key, value, scope) {
442
+ const path = configPath(scope);
443
+ const current = readJson(path) || {};
444
+ const next = { ...current, [key]: value };
445
+ const dir = join8(path, "..");
446
+ if (!isDir(dir)) mkdirSync5(dir, { recursive: true });
447
+ writeFileSync5(path, JSON.stringify(next, null, 2) + "\n");
448
+ return path;
449
+ }
450
+ var CONFIG_FILE, CONFIG_DEFAULTS;
451
+ var init_config = __esm({
452
+ "src/config.ts"() {
453
+ "use strict";
454
+ init_util();
455
+ CONFIG_FILE = ".enigma.json";
456
+ CONFIG_DEFAULTS = { commitEmoji: true, updateNotifier: true };
457
+ }
458
+ });
459
+
460
+ // src/settings-registry.ts
461
+ function enigmaToggle(key, field, label, hint) {
462
+ return {
463
+ key,
464
+ label,
465
+ hint,
466
+ read: () => readConfig().config[field],
467
+ write: (value, scope) => ({ path: setEnigmaToggle(field, value, scope), changed: true })
468
+ };
469
+ }
470
+ function valueLabel(on) {
471
+ return on ? "on" : "off";
472
+ }
473
+ function parseBool(value) {
474
+ const v = value.toLowerCase();
475
+ if (["on", "true", "yes", "1", "enable", "enabled"].includes(v)) return true;
476
+ if (["off", "false", "no", "0", "disable", "disabled"].includes(v)) return false;
477
+ return null;
478
+ }
479
+ var CATEGORIES, ALL_SETTINGS;
480
+ var init_settings_registry = __esm({
481
+ "src/settings-registry.ts"() {
482
+ "use strict";
483
+ init_agents();
484
+ init_config();
485
+ init_claude();
486
+ init_permissions();
487
+ CATEGORIES = [
488
+ {
489
+ title: "General",
490
+ blurb: "enigma runtime toggles (.enigma.json)",
491
+ settings: [
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")
494
+ ]
495
+ },
496
+ {
497
+ title: "Git & attribution",
498
+ blurb: "how the coding agent attributes its work in git",
499
+ settings: [
500
+ {
501
+ key: "claude-attribution",
502
+ label: "Claude commit attribution",
503
+ hint: "let Claude Code commit as its own contributor (Co-Authored-By / PR footer); enigma default: off",
504
+ read: (scope) => getClaudeAttribution(scope),
505
+ write: (value, scope) => ({ changed: setClaudeAttribution(scope, value) })
506
+ }
507
+ ]
508
+ },
509
+ {
510
+ title: "Permissions",
511
+ blurb: "approval-prompt bypass per agent (security trade-off)",
512
+ settings: BYPASS_SUPPORTED.map((name) => ({
513
+ key: `bypass-${name}`,
514
+ label: `${AGENTS[name]?.label || name} approval bypass`,
515
+ hint: name === "codex" ? "skip approval prompts (global ~/.codex only)" : "skip per-action approval prompts",
516
+ globalOnly: name === "codex",
517
+ read: (scope) => getBypass(name, scope),
518
+ write: (value, scope) => setBypass(name, scope, value, false) || { changed: false }
519
+ }))
520
+ }
521
+ ];
522
+ ALL_SETTINGS = CATEGORIES.flatMap((c) => c.settings);
523
+ }
524
+ });
525
+
526
+ // src/tui/settings.ts
527
+ var settings_exports = {};
528
+ __export(settings_exports, {
529
+ runSettingsTui: () => runSettingsTui
530
+ });
531
+ async function runSettingsTui() {
532
+ if (!process.stdout.isTTY) return;
533
+ const React = (await import("react")).default;
534
+ const ink = await import("ink");
535
+ const { render, useApp, useInput } = ink;
536
+ const Box = ink.Box;
537
+ const Text = ink.Text;
538
+ const { useState } = React;
539
+ const h = React.createElement;
540
+ function App() {
541
+ const { exit } = useApp();
542
+ const [scope, setScope] = useState("global");
543
+ const [focusSettings, setFocusSettings] = useState(false);
544
+ const [catIndex, setCatIndex] = useState(0);
545
+ const [setIndex, setSetIndex] = useState(0);
546
+ const [, bump] = useState(0);
547
+ const category = CATEGORIES[catIndex];
548
+ const settings = category.settings;
549
+ useInput((input, key) => {
550
+ if (input === "q" || key.escape || key.ctrl && input === "c") {
551
+ exit();
552
+ return;
553
+ }
554
+ if (input === "g") {
555
+ setScope((s) => s === "global" ? "local" : "global");
556
+ return;
557
+ }
558
+ if (key.tab) {
559
+ setFocusSettings((f) => !f);
560
+ return;
561
+ }
562
+ if (key.leftArrow || input === "h") {
563
+ setFocusSettings(false);
564
+ return;
565
+ }
566
+ if (key.rightArrow || input === "l") {
567
+ setFocusSettings(true);
568
+ return;
569
+ }
570
+ if (key.upArrow || input === "k") {
571
+ if (focusSettings) setSetIndex((i) => Math.max(0, i - 1));
572
+ else {
573
+ setCatIndex((i) => Math.max(0, i - 1));
574
+ setSetIndex(0);
575
+ }
576
+ return;
577
+ }
578
+ if (key.downArrow || input === "j") {
579
+ if (focusSettings) setSetIndex((i) => Math.min(settings.length - 1, i + 1));
580
+ else {
581
+ setCatIndex((i) => Math.min(CATEGORIES.length - 1, i + 1));
582
+ setSetIndex(0);
583
+ }
584
+ return;
585
+ }
586
+ if (key.return || input === " ") {
587
+ if (!focusSettings) {
588
+ setFocusSettings(true);
589
+ return;
590
+ }
591
+ const setting = settings[setIndex];
592
+ setting.write(!setting.read(scope), scope);
593
+ bump((n) => n + 1);
594
+ }
595
+ });
596
+ const header = h(
597
+ Box,
598
+ { marginBottom: 1 },
599
+ h(Text, { bold: true, color: "cyan" }, "enigma settings"),
600
+ h(Text, { dimColor: true }, " scope: "),
601
+ h(Text, { bold: true, color: scope === "global" ? "green" : "yellow" }, scope),
602
+ h(Text, { dimColor: true }, " (g to change)")
603
+ );
604
+ const left = h(Box, {
605
+ flexDirection: "column",
606
+ borderStyle: "round",
607
+ borderColor: focusSettings ? "gray" : "cyan",
608
+ paddingX: 1,
609
+ width: 26,
610
+ marginRight: 1
611
+ }, CATEGORIES.map((c, i) => h(Text, {
612
+ key: c.title,
613
+ color: i === catIndex ? "cyan" : void 0,
614
+ inverse: !focusSettings && i === catIndex
615
+ }, `${i === catIndex ? ">" : " "} ${c.title}`)));
616
+ const right = h(Box, {
617
+ flexDirection: "column",
618
+ borderStyle: "round",
619
+ borderColor: focusSettings ? "cyan" : "gray",
620
+ paddingX: 1,
621
+ flexGrow: 1
622
+ }, [
623
+ h(Text, { key: "__blurb", dimColor: true }, category.blurb),
624
+ ...settings.map((s, i) => {
625
+ const on = s.read(scope);
626
+ const selected = focusSettings && i === setIndex;
627
+ return h(
628
+ Box,
629
+ { key: s.key, justifyContent: "space-between" },
630
+ h(Text, { inverse: selected }, `${selected ? ">" : " "} ${s.label}${s.globalOnly ? " (global)" : ""}`),
631
+ h(Text, { bold: true, color: on ? "green" : "gray" }, ` ${valueLabel(on)}`)
632
+ );
633
+ })
634
+ ]);
635
+ const footer = h(Box, { marginTop: 1 }, h(
636
+ Text,
637
+ { dimColor: true },
638
+ "up/down move - tab/left/right switch pane - enter/space toggle - g scope - q quit"
639
+ ));
640
+ return h(Box, { flexDirection: "column" }, header, h(Box, {}, left, right), footer);
641
+ }
642
+ const app = render(h(App));
643
+ await app.waitUntilExit();
644
+ }
645
+ var init_settings = __esm({
646
+ "src/tui/settings.ts"() {
647
+ "use strict";
648
+ init_settings_registry();
649
+ }
650
+ });
651
+
652
+ // src/cli.ts
653
+ init_util();
654
+ import { dirname as dirname4, join as join10 } from "path";
655
+ import { fileURLToPath as fileURLToPath4 } from "url";
656
+ import * as p5 from "@clack/prompts";
379
657
 
380
658
  // src/skills.ts
659
+ init_util();
660
+ init_agents();
661
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4, cpSync as cpSync2, mkdirSync as mkdirSync4, rmSync } from "fs";
662
+ import { dirname as dirname2, join as join6, resolve as resolve2, relative as relative2, sep as sep2 } from "path";
663
+ import { fileURLToPath as fileURLToPath2 } from "url";
664
+ import { createHash } from "crypto";
665
+ import * as p3 from "@clack/prompts";
666
+
667
+ // src/security.ts
668
+ init_util();
669
+ import { existsSync as existsSync3, mkdirSync, cpSync, writeFileSync, chmodSync } from "fs";
670
+ import { dirname, join as join3, resolve, relative } from "path";
671
+ import { fileURLToPath } from "url";
672
+ import { execFileSync as execFileSync2 } from "child_process";
673
+ import * as p from "@clack/prompts";
674
+ var __dirname = dirname(fileURLToPath(import.meta.url));
675
+ function findGuardSrc() {
676
+ const candidates = [
677
+ join3(__dirname, "guard.js"),
678
+ join3(__dirname, "..", "dist", "guard.js")
679
+ ];
680
+ return candidates.find((c) => existsSync3(c)) ?? null;
681
+ }
682
+ var GUARD_PROTECTIONS = [
683
+ { value: "secrets", label: "Block committed secrets", hint: "API keys, tokens, private keys" },
684
+ { value: "envFiles", label: "Block .env files", hint: "allows .env.example / .sample / .template" },
685
+ { value: "depDirs", label: "Block dependency/cache dirs", hint: "node_modules, __pycache__, venv" },
686
+ { value: "generatedDirs", label: "Warn on generated dirs", hint: "dist, build, .next, coverage" },
687
+ { value: "junkFiles", label: "Warn on log / OS junk files", hint: ".log, .DS_Store, Thumbs.db" },
688
+ { value: "largeFiles", label: "Warn on files over 5 MB", hint: "oversized blobs" }
689
+ ];
690
+ function findGitRoot(start) {
691
+ let dir = resolve(start);
692
+ for (; ; ) {
693
+ if (existsSync3(join3(dir, ".git"))) return dir;
694
+ const parent = dirname(dir);
695
+ if (parent === dir) return null;
696
+ dir = parent;
697
+ }
698
+ }
699
+ function currentHooksPath(root) {
700
+ try {
701
+ return execFileSync2("git", ["-C", root, "config", "--get", "core.hooksPath"], { encoding: "utf8" }).trim();
702
+ } catch {
703
+ return "";
704
+ }
705
+ }
706
+ async function setupGitHooks(opts, interactive) {
707
+ const root = findGitRoot(process.cwd());
708
+ if (!root) {
709
+ p.log.error("Not inside a git repository (no .git found). Run this from your project root.");
710
+ return false;
711
+ }
712
+ const guardSrc = findGuardSrc();
713
+ if (!guardSrc) {
714
+ p.log.error("Cannot find the built guard (dist/guard.js). Run 'npm run build' first.");
715
+ return false;
716
+ }
717
+ const current = currentHooksPath(root);
718
+ if (current && current !== ".githooks" && !opts.force) {
719
+ p.log.warn(`core.hooksPath is already set to '${current}'.`);
720
+ if (interactive) {
721
+ const ok = await p.confirm({ message: `Override existing core.hooksPath '${current}' with '.githooks'?` });
722
+ if (p.isCancel(ok) || !ok) {
723
+ p.log.info("Left git hooks unchanged.");
724
+ return false;
725
+ }
726
+ } else {
727
+ p.log.info("Re-run with --force to override.");
728
+ return false;
729
+ }
730
+ }
731
+ let enabled = opts.protections;
732
+ if (!enabled && interactive) {
733
+ const r = await p.multiselect({
734
+ message: "Which protections should the commit guard enforce?",
735
+ options: GUARD_PROTECTIONS,
736
+ initialValues: GUARD_PROTECTIONS.map((o) => o.value),
737
+ required: true
738
+ });
739
+ if (p.isCancel(r)) {
740
+ p.log.info("Left git hooks unchanged.");
741
+ return false;
742
+ }
743
+ enabled = r;
744
+ }
745
+ const config = {};
746
+ for (const o of GUARD_PROTECTIONS) config[o.value] = enabled ? enabled.includes(o.value) : true;
747
+ const hooksDir = join3(root, ".githooks");
748
+ mkdirSync(hooksDir, { recursive: true });
749
+ cpSync(guardSrc, join3(hooksDir, "guard.mjs"), { force: true });
750
+ writeFileSync(join3(hooksDir, "enigma-guard.json"), JSON.stringify(config, null, 2) + "\n");
751
+ const shimPath = join3(hooksDir, "pre-commit");
752
+ const shim = [
753
+ "#!/bin/sh",
754
+ "# Managed by enigma (enigma-cli) - blocks committed secrets, .env files, and dependency dirs.",
755
+ "# Toggle protections in .githooks/enigma-guard.json. Bypass once: git commit --no-verify",
756
+ 'exec node "$(git rev-parse --show-toplevel)/.githooks/guard.mjs" "$@"',
757
+ ""
758
+ ].join("\n");
759
+ writeFileSync(shimPath, shim);
760
+ try {
761
+ chmodSync(shimPath, 493);
762
+ } catch {
763
+ }
764
+ try {
765
+ chmodSync(join3(hooksDir, "guard.mjs"), 493);
766
+ } catch {
767
+ }
768
+ try {
769
+ execFileSync2("git", ["-C", root, "config", "core.hooksPath", ".githooks"]);
770
+ } catch (err) {
771
+ p.log.error(`Failed to set core.hooksPath: ${err.message}`);
772
+ return false;
773
+ }
774
+ const on = Object.entries(config).filter(([, v]) => v).map(([k]) => k);
775
+ p.log.success(`Git security hooks installed in ${relative(process.cwd(), hooksDir) || ".githooks"} (core.hooksPath set).`);
776
+ p.log.info(`Enforcing: ${on.join(", ") || "nothing"}. Commit .githooks/ so your team inherits it.`);
777
+ if (isOnPath("gh")) {
778
+ p.log.info("GitHub CLI (gh) detected: these hooks also run for commits made via gh, since gh uses git underneath.");
779
+ }
780
+ return true;
781
+ }
782
+ async function maybeOfferGitHooks(interactive, opts) {
783
+ if (!interactive || opts.security) return;
784
+ const root = findGitRoot(process.cwd());
785
+ if (!root) return;
786
+ if (currentHooksPath(root) === ".githooks") return;
787
+ const ok = await p.confirm({ message: "Set up git security hooks here too (block secrets, .env, node_modules)?" });
788
+ if (!p.isCancel(ok) && ok) await setupGitHooks({ ...opts, protections: void 0 }, interactive);
789
+ }
790
+
791
+ // src/skills.ts
792
+ init_claude();
793
+ init_permissions();
381
794
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
382
795
  var PKG_ROOT = resolve2(__dirname2, "..");
383
796
  var ASSETS = join6(PKG_ROOT, "assets");
@@ -920,65 +1333,38 @@ if (isGuardEntry && fileURLToPath3(import.meta.url) === guardEntry) {
920
1333
  process.exit(runGuardCli(process.argv.includes("--all")));
921
1334
  }
922
1335
 
923
- // src/config.ts
924
- import { homedir as homedir4 } from "os";
925
- import { join as join8 } from "path";
926
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
927
- var CONFIG_FILE = ".enigma.json";
928
- var CONFIG_DEFAULTS = { commitEmoji: true };
929
- var BOOLEAN_KEYS = ["commitEmoji"];
930
- var CLI_KEYS = { "commit-emoji": "commitEmoji" };
931
- function configPath(scope) {
932
- return scope === "global" ? join8(homedir4(), CONFIG_FILE) : join8(process.cwd(), CONFIG_FILE);
1336
+ // src/settings.ts
1337
+ init_config();
1338
+ init_settings_registry();
1339
+ function printEffective() {
1340
+ console.log("Effective enigma settings:\n");
1341
+ for (const category of CATEGORIES) {
1342
+ console.log(`${category.title}:`);
1343
+ for (const s of category.settings) console.log(` ${s.key}: ${valueLabel(s.read("global"))}`);
1344
+ console.log("");
1345
+ }
1346
+ const { sources } = readConfig();
1347
+ console.log(sources.length ? `.enigma.json sources: ${sources.join(", ")}` : ".enigma.json: built-in defaults (no file found)");
1348
+ console.log("Agent settings (attribution, bypass) reflect each agent's own config at the global scope.");
933
1349
  }
934
- function readConfig() {
935
- const sources = [];
936
- let config = { ...CONFIG_DEFAULTS };
937
- for (const scope of ["global", "local"]) {
938
- const path = configPath(scope);
939
- const raw = existsSync6(path) ? readJson(path) : null;
940
- if (raw) {
941
- config = { ...config, ...raw };
942
- sources.push(path);
943
- }
944
- }
945
- return { config, sources };
946
- }
947
- function parseBool(value) {
948
- const v = value.toLowerCase();
949
- if (["on", "true", "yes", "1", "enable", "enabled"].includes(v)) return true;
950
- if (["off", "false", "no", "0", "disable", "disabled"].includes(v)) return false;
951
- return null;
952
- }
953
- function setValue(scope, key, value) {
954
- const path = configPath(scope);
955
- const current = readJson(path) || {};
956
- const next = { ...current, [key]: value };
957
- const dir = join8(path, "..");
958
- if (!isDir(dir)) mkdirSync5(dir, { recursive: true });
959
- writeFileSync5(path, JSON.stringify(next, null, 2) + "\n");
960
- return path;
961
- }
962
- function runConfigCli(positionals, scope) {
1350
+ async function runConfigCli(positionals, scope, interactive) {
963
1351
  const [rawKey, rawValue] = positionals;
964
1352
  if (!rawKey) {
965
- const { config, sources } = readConfig();
966
- console.log("Effective enigma config:");
967
- for (const k of BOOLEAN_KEYS) {
968
- const cliKey = Object.keys(CLI_KEYS).find((c) => CLI_KEYS[c] === k) || k;
969
- console.log(` ${cliKey}: ${config[k]}`);
1353
+ if (interactive) {
1354
+ const { runSettingsTui: runSettingsTui2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
1355
+ await runSettingsTui2();
1356
+ } else {
1357
+ printEffective();
970
1358
  }
971
- console.log(sources.length ? `
972
- From: ${sources.join(", ")}` : "\nFrom: built-in defaults (no .enigma.json found)");
973
1359
  return 0;
974
1360
  }
975
- const key = CLI_KEYS[rawKey];
976
- if (!key) {
977
- console.error(`Unknown config key: ${rawKey}. Known keys: ${Object.keys(CLI_KEYS).join(", ")}.`);
1361
+ const setting = ALL_SETTINGS.find((s) => s.key === rawKey);
1362
+ if (!setting) {
1363
+ console.error(`Unknown config key: ${rawKey}. Known keys: ${ALL_SETTINGS.map((s) => s.key).join(", ")}.`);
978
1364
  return 1;
979
1365
  }
980
1366
  if (rawValue === void 0) {
981
- console.error(`Missing value for '${rawKey}'. Usage: enigma config ${rawKey} <on|off>`);
1367
+ console.error(`Missing value for '${rawKey}'. Usage: enigma config ${rawKey} <on|off> [-g|-l]`);
982
1368
  return 1;
983
1369
  }
984
1370
  const value = parseBool(rawValue);
@@ -986,13 +1372,16 @@ From: ${sources.join(", ")}` : "\nFrom: built-in defaults (no .enigma.json found
986
1372
  console.error(`Invalid value '${rawValue}' for '${rawKey}'. Use on or off.`);
987
1373
  return 1;
988
1374
  }
989
- const target = scope || "global";
990
- const path = setValue(target, key, value);
991
- console.log(`Set ${rawKey} = ${value ? "on" : "off"} (${target}) in ${path}.`);
1375
+ const target = setting.globalOnly ? "global" : scope || "global";
1376
+ const result = setting.write(value, target);
1377
+ const where = result.path ? ` in ${result.path}` : "";
1378
+ console.log(`Set ${rawKey} = ${valueLabel(value)} (${target})${where}.`);
992
1379
  return 0;
993
1380
  }
994
1381
 
995
1382
  // src/update.ts
1383
+ init_util();
1384
+ init_config();
996
1385
  import { homedir as homedir5 } from "os";
997
1386
  import { join as join9 } from "path";
998
1387
  import { writeFileSync as writeFileSync6 } from "fs";
@@ -1073,6 +1462,7 @@ function runUpdate() {
1073
1462
  async function notifyUpdate(current, interactive) {
1074
1463
  if (!process.stdout.isTTY || process.env.CI) return;
1075
1464
  try {
1465
+ if (!readConfig().config.updateNotifier) return;
1076
1466
  scheduleUpdateCheck();
1077
1467
  const cache = readCache();
1078
1468
  const latest = cache?.latest ? String(cache.latest).replace(/[^\w.+-]/g, "") : "";
@@ -1199,15 +1589,19 @@ Usage:
1199
1589
  enigma [command] [options]
1200
1590
 
1201
1591
  Commands:
1202
- (none) Interactive menu: pick which features to set up
1592
+ (none) Interactive hub: configure settings or set up features
1203
1593
  install Install/update agent skills (Claude Code, Codex, opencode)
1204
1594
  security Set up git security hooks in the current repo
1205
1595
  guard [--all] Run the commit guard (staged files, or --all for every tracked file)
1206
- config [key val] Show/set runtime toggles (e.g. config commit-emoji off)
1596
+ config [key val] Configure settings: no args opens the interactive menu;
1597
+ 'config <key> <on|off> [-g|-l]' sets one (e.g. config claude-attribution on)
1207
1598
  seal Maintenance: (re)compute skill content hashes
1208
1599
  check Integrity gate: verify skills are well-formed and sealed
1209
1600
  help, version
1210
1601
 
1602
+ Config keys: commit-emoji, update-notifier, claude-attribution,
1603
+ bypass-claude, bypass-codex, bypass-opencode
1604
+
1211
1605
  Install options:
1212
1606
  -g, --global Install at user level
1213
1607
  -l, --local Install into the current project
@@ -1256,7 +1650,7 @@ async function run(argv) {
1256
1650
  process.exit(runGuardCli(opts.all));
1257
1651
  }
1258
1652
  if (opts.command === "config") {
1259
- process.exit(runConfigCli(opts.positionals, opts.scope));
1653
+ process.exit(await runConfigCli(opts.positionals, opts.scope, interactive));
1260
1654
  }
1261
1655
  if (opts.command === "install") {
1262
1656
  p5.intro("enigma - install agent skills");
@@ -1272,28 +1666,29 @@ async function run(argv) {
1272
1666
  await notifyUpdate(version, interactive);
1273
1667
  return;
1274
1668
  }
1669
+ if (!interactive) {
1670
+ await installSkills(opts, interactive);
1671
+ await notifyUpdate(version, interactive);
1672
+ return;
1673
+ }
1275
1674
  p5.intro("enigma");
1276
- let features;
1277
- if (interactive) {
1278
- const r = await p5.multiselect({
1279
- message: "What do you want to set up?",
1675
+ for (; ; ) {
1676
+ const action = await p5.select({
1677
+ message: "What would you like to do?",
1280
1678
  options: [
1281
- { value: "skills", label: "Agent skills", hint: "Claude Code, Codex, opencode" },
1282
- { value: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" }
1283
- ],
1284
- initialValues: ["skills"],
1285
- required: true
1679
+ { value: "config", label: "Configure settings", hint: "emoji, attribution, permission bypass, ..." },
1680
+ { value: "skills", label: "Install agent skills", hint: "Claude Code, Codex, opencode" },
1681
+ { value: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" },
1682
+ { value: "exit", label: "Exit" }
1683
+ ]
1286
1684
  });
1287
- if (p5.isCancel(r)) {
1288
- p5.cancel("Aborted.");
1289
- return;
1290
- }
1291
- features = r;
1292
- } else {
1293
- features = ["skills"];
1685
+ if (p5.isCancel(action) || action === "exit") break;
1686
+ if (action === "config") {
1687
+ const { runSettingsTui: runSettingsTui2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
1688
+ await runSettingsTui2();
1689
+ } else if (action === "skills") await installSkills(opts, interactive);
1690
+ else if (action === "security") await setupGitHooks(opts, interactive);
1294
1691
  }
1295
- if (features.includes("skills")) await installSkills(opts, interactive);
1296
- if (features.includes("security")) await setupGitHooks(opts, interactive);
1297
1692
  p5.outro("Done.");
1298
1693
  await notifyUpdate(version, interactive);
1299
1694
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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": {
@@ -9,7 +9,8 @@
9
9
  "scripts": {
10
10
  "enigma": "tsx src/bin/enigma.ts",
11
11
  "build": "tsup",
12
- "dev": "tsup --watch",
12
+ "dev": "node scripts/dev.mjs",
13
+ "dev:watch": "tsup --watch",
13
14
  "typecheck": "tsc --noEmit",
14
15
  "seal": "tsx src/bin/enigma.ts seal",
15
16
  "check": "tsx src/bin/enigma.ts check",
@@ -27,10 +28,13 @@
27
28
  "node": ">=18"
28
29
  },
29
30
  "dependencies": {
30
- "@clack/prompts": "^0.7.0"
31
+ "@clack/prompts": "^0.7.0",
32
+ "ink": "^5.2.1",
33
+ "react": "^18.3.1"
31
34
  },
32
35
  "devDependencies": {
33
36
  "@types/node": "^22.0.0",
37
+ "@types/react": "^18.3.29",
34
38
  "tsup": "^8.0.0",
35
39
  "tsx": "^4.0.0",
36
40
  "typescript": "^5.0.0"