enigma-cli 1.3.0 → 1.4.1

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 DELETED
@@ -1,2425 +0,0 @@
1
- #!/usr/bin/env node
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
- };
11
-
12
- // src/util.ts
13
- import { existsSync, statSync, readFileSync } from "fs";
14
- import { join, sep } from "path";
15
- function isDir(pth) {
16
- try {
17
- return statSync(pth).isDirectory();
18
- } catch {
19
- return false;
20
- }
21
- }
22
- function readJson(file) {
23
- try {
24
- return JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
25
- } catch {
26
- return null;
27
- }
28
- }
29
- function isOnPath(bin) {
30
- const dirs = (process.env.PATH || process.env.Path || "").split(sep === "\\" ? ";" : ":").filter(Boolean);
31
- const exts = sep === "\\" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()) : [""];
32
- return dirs.some((d) => exts.some((ext) => existsSync(join(d, bin + ext))));
33
- }
34
- var init_util = __esm({
35
- "src/util.ts"() {
36
- "use strict";
37
- }
38
- });
39
-
40
- // src/agents.ts
41
- import { homedir } from "os";
42
- import { join as join2 } from "path";
43
- import { existsSync as existsSync2 } from "fs";
44
- import { execFileSync } from "child_process";
45
- function isManagedProvider(provider) {
46
- return provider === MANAGED_PROVIDER || typeof provider === "string" && LEGACY_PROVIDERS.includes(provider);
47
- }
48
- function isInstalled(agent) {
49
- const det = agent.detect || {};
50
- return (det.dirs || []).some((d) => existsSync2(d)) || (det.bins || []).some((b) => isOnPath(b));
51
- }
52
- function discoverAgents() {
53
- return Object.keys(AGENTS).map((name) => {
54
- const agent = { name, ...AGENTS[name] };
55
- return { ...agent, installed: isInstalled(agent) };
56
- });
57
- }
58
- function processSnapshot() {
59
- try {
60
- if (process.platform === "win32") {
61
- return execFileSync("tasklist", ["/fo", "csv", "/nh"], { encoding: "utf8" }).toLowerCase();
62
- }
63
- return execFileSync("ps", ["-A", "-o", "comm="], { encoding: "utf8" }).toLowerCase();
64
- } catch {
65
- return null;
66
- }
67
- }
68
- function runningStatus(agents) {
69
- const snap = processSnapshot();
70
- if (snap == null) return { known: false, running: /* @__PURE__ */ new Set() };
71
- const running = /* @__PURE__ */ new Set();
72
- for (const a of agents) {
73
- const det = a.detect || {};
74
- const names = det.procs || det.bins || [a.name];
75
- if (names.some((n) => snap.includes(String(n).toLowerCase()))) running.add(a.name);
76
- }
77
- return { known: true, running };
78
- }
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
- }
118
- }
119
- };
120
- }
121
- });
122
-
123
- // src/claude.ts
124
- import { homedir as homedir2 } from "os";
125
- import { join as join4 } from "path";
126
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
127
- function claudeSettingsPath(scope) {
128
- return scope === "global" ? join4(homedir2(), ".claude", "settings.json") : join4(process.cwd(), ".claude", "settings.json");
129
- }
130
- function disableClaudeAttribution(scope) {
131
- const path = claudeSettingsPath(scope);
132
- const current = readJson(path) || {};
133
- const attribution = typeof current.attribution === "object" && current.attribution !== null ? current.attribution : {};
134
- const alreadyOff = attribution.commit === "" && attribution.pr === "" && current.includeCoAuthoredBy === false;
135
- if (alreadyOff) return false;
136
- const next = {
137
- ...current,
138
- attribution: { ...attribution, commit: "", pr: "" },
139
- includeCoAuthoredBy: false
140
- };
141
- const dir = join4(path, "..");
142
- if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
143
- writeFileSync2(path, JSON.stringify(next, null, 2) + "\n");
144
- return true;
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
- }
194
- function enableClaudeBypass(scope, dryRun) {
195
- const path = claudeSettingsPath(scope);
196
- const current = readJson(path) || {};
197
- const permissions = typeof current.permissions === "object" && current.permissions !== null ? current.permissions : {};
198
- if (permissions.defaultMode === "bypassPermissions") return { path, changed: false };
199
- if (dryRun) return { path, changed: true };
200
- const next = { ...current, permissions: { ...permissions, defaultMode: "bypassPermissions" } };
201
- writeClaudeSettings(path, next);
202
- return { path, changed: true };
203
- }
204
- function writeClaudeSettings(path, data) {
205
- const dir = join4(path, "..");
206
- if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
207
- writeFileSync2(path, JSON.stringify(data, null, 2) + "\n");
208
- }
209
- var init_claude = __esm({
210
- "src/claude.ts"() {
211
- "use strict";
212
- init_util();
213
- }
214
- });
215
-
216
- // src/permissions.ts
217
- import { homedir as homedir3 } from "os";
218
- import { join as join5 } from "path";
219
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
220
- import * as p3 from "@clack/prompts";
221
- async function resolveBypassSelection(candidates, opts, interactive) {
222
- const supported = candidates.filter((a) => BYPASS_SUPPORTED.includes(a.name));
223
- if (!supported.length || opts.noBypass) return [];
224
- const names = supported.map((a) => a.name);
225
- if (opts.bypass !== null) {
226
- const req = opts.bypass.map((s) => s.trim().toLowerCase());
227
- if (req.includes("none")) return [];
228
- if (req.includes("all")) return names;
229
- return names.filter((n) => req.includes(n));
230
- }
231
- if (!interactive) return [];
232
- const r = await p3.multiselect({
233
- message: "Bypass approval prompts for which agents? (security trade-off: the agent stops asking before acting)",
234
- options: supported.map((a) => ({
235
- value: a.name,
236
- label: a.label,
237
- hint: BYPASS_DEFAULT_ON.has(a.name) ? "recommended" : "less reliable - off by default"
238
- })),
239
- initialValues: names.filter((n) => BYPASS_DEFAULT_ON.has(n)),
240
- required: false
241
- });
242
- if (p3.isCancel(r)) return [];
243
- return r;
244
- }
245
- function applyBypass(agentNames, scope, dryRun) {
246
- for (const name of agentNames) {
247
- const res = enableFor(name, scope, dryRun);
248
- if (!res) continue;
249
- const label = AGENTS[name]?.label || name;
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
- }
254
- }
255
- function enableFor(name, scope, dryRun) {
256
- switch (name) {
257
- case "claude":
258
- return enableClaudeBypass(scope, dryRun);
259
- case "codex":
260
- return enableCodexBypass(dryRun);
261
- case "opencode":
262
- return enableOpencodeBypass(scope, dryRun);
263
- default:
264
- return null;
265
- }
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
- }
335
- function enableCodexBypass(dryRun) {
336
- const path = join5(homedir3(), ".codex", "config.toml");
337
- const before = existsSync4(path) ? readFileSync2(path, "utf8") : "";
338
- let after = setTomlTopLevelKey(before, "approval_policy", '"never"');
339
- after = setTomlTopLevelKey(after, "sandbox_mode", '"danger-full-access"');
340
- const changed = after !== before;
341
- if (changed && !dryRun) {
342
- const dir = join5(path, "..");
343
- if (!isDir(dir)) mkdirSync3(dir, { recursive: true });
344
- writeFileSync3(path, after);
345
- }
346
- return { path, changed };
347
- }
348
- function enableOpencodeBypass(scope, dryRun) {
349
- const path = opencodeConfigPath(scope);
350
- const current = readJson(path) || {};
351
- const perm = current.permission;
352
- const alreadyAllowAll = perm === "allow" || typeof perm === "object" && perm !== null && perm["*"] === "allow";
353
- if (alreadyAllowAll) return { path, changed: false };
354
- if (dryRun) return { path, changed: true };
355
- const existing = typeof perm === "object" && perm !== null ? perm : {};
356
- const rest = {};
357
- for (const k of Object.keys(existing)) if (k !== "*") rest[k] = existing[k];
358
- const next = { ...current, permission: { "*": "allow", ...rest } };
359
- const dir = join5(path, "..");
360
- if (!isDir(dir)) mkdirSync3(dir, { recursive: true });
361
- writeFileSync3(path, JSON.stringify(next, null, 2) + "\n");
362
- return { path, changed: true };
363
- }
364
- function setTomlTopLevelKey(content, key, tomlValue) {
365
- const assign = `${key} = ${tomlValue}`;
366
- if (content.trim() === "") return `${assign}
367
- `;
368
- const lines = content.split("\n");
369
- const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
370
- const scanEnd = firstTable === -1 ? lines.length : firstTable;
371
- const keyRe = new RegExp(`^\\s*${key}\\s*=`);
372
- for (let i = 0; i < scanEnd; i++) {
373
- if (!keyRe.test(lines[i])) continue;
374
- if (lines[i].trim() === assign) return content;
375
- lines[i] = assign;
376
- return normalizeTrailingNewline(lines.join("\n"));
377
- }
378
- let insertAt = scanEnd;
379
- while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
380
- const followsTable = insertAt < lines.length && /^\s*\[/.test(lines[insertAt]);
381
- lines.splice(insertAt, 0, ...followsTable ? [assign, ""] : [assign]);
382
- return normalizeTrailingNewline(lines.join("\n"));
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
- }
405
- function normalizeTrailingNewline(s) {
406
- return `${s.replace(/\s+$/, "")}
407
- `;
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, fullscreen: 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
- enigmaToggle("fullscreen", "fullscreen", "Full-screen TUI", "clear the screen for a clean TUI view; off renders inline among existing output")
495
- ]
496
- },
497
- {
498
- title: "Git & attribution",
499
- blurb: "how the coding agent attributes its work in git",
500
- settings: [
501
- {
502
- key: "claude-attribution",
503
- label: "Claude commit attribution",
504
- hint: "let Claude Code commit as its own contributor (Co-Authored-By / PR footer); enigma default: off",
505
- read: (scope) => getClaudeAttribution(scope),
506
- write: (value, scope) => ({ changed: setClaudeAttribution(scope, value) })
507
- }
508
- ]
509
- },
510
- {
511
- title: "Permissions",
512
- blurb: "approval-prompt bypass per agent (security trade-off)",
513
- settings: BYPASS_SUPPORTED.map((name) => ({
514
- key: `bypass-${name}`,
515
- label: `${AGENTS[name]?.label || name} approval bypass`,
516
- hint: name === "codex" ? "skip approval prompts (global ~/.codex only)" : "skip per-action approval prompts",
517
- globalOnly: name === "codex",
518
- read: (scope) => getBypass(name, scope),
519
- write: (value, scope) => setBypass(name, scope, value, false) || { changed: false }
520
- }))
521
- }
522
- ];
523
- ALL_SETTINGS = CATEGORIES.flatMap((c) => c.settings);
524
- }
525
- });
526
-
527
- // src/tui/settings.ts
528
- var settings_exports = {};
529
- __export(settings_exports, {
530
- buildApp: () => buildApp,
531
- runHomeTui: () => runHomeTui,
532
- runSettingsTui: () => runSettingsTui
533
- });
534
- async function runHomeTui(hub) {
535
- await runTui({ showActions: true, hub });
536
- }
537
- async function runSettingsTui() {
538
- await runTui({ showActions: false });
539
- }
540
- async function runTui(opts) {
541
- if (!process.stdout.isTTY) return;
542
- const fullscreen = readConfig().config.fullscreen;
543
- const React = (await import("react")).default;
544
- const ink = await import("ink");
545
- const { render } = ink;
546
- const h = React.createElement;
547
- const agents = opts.hub?.agents ?? [];
548
- const protections = opts.hub?.protections ?? [];
549
- const runAction = opts.hub?.runAction ?? (async () => ({ ok: false, title: "", lines: [] }));
550
- if (fullscreen) try {
551
- process.stdout.write(CLEAR_SCREEN);
552
- } catch {
553
- }
554
- const App = buildApp(React, ink, { showActions: opts.showActions, fullscreen, agents, protections, runAction });
555
- const app = render(h(App), { exitOnCtrlC: true });
556
- await app.waitUntilExit();
557
- }
558
- function buildApp(React, ink, opts) {
559
- const { useApp, useInput, useStdout } = ink;
560
- const Box = ink.Box;
561
- const Text = ink.Text;
562
- const { useState, useEffect } = React;
563
- const h = React.createElement;
564
- const fill = opts.fullscreen;
565
- const sideItems = [
566
- ...CATEGORIES.map((c, i) => ({ kind: "category", catIndex: i, title: c.title })),
567
- ...opts.showActions ? ACTION_ITEMS.map((a) => ({ kind: "action", ...a })) : []
568
- ];
569
- const actionItemKeys = (action) => action === "security" ? opts.protections.map((pr) => pr.value) : opts.agents.map((a) => a.name);
570
- return function App() {
571
- const { exit } = useApp();
572
- const { stdout } = useStdout();
573
- const [size, setSize] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
574
- const [mode, setMode] = useState("menu");
575
- const [scope, setScope] = useState("global");
576
- const [sideIndex, setSideIndex] = useState(0);
577
- const [focusRight, setFocusRight] = useState(false);
578
- const [setIndex, setSetIndex] = useState(0);
579
- const [pending, setPending] = useState({});
580
- const [confirm4, setConfirm] = useState(null);
581
- const [actCursor, setActCursor] = useState(0);
582
- const [actChecked, setActChecked] = useState({});
583
- const [busyTitle, setBusyTitle] = useState("");
584
- const [result, setResult] = useState(null);
585
- const [resultScroll, setResultScroll] = useState(0);
586
- useEffect(() => {
587
- const onResize = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
588
- stdout.on("resize", onResize);
589
- return () => {
590
- stdout.off("resize", onResize);
591
- };
592
- }, [stdout]);
593
- const current = sideItems[sideIndex];
594
- const category = current.kind === "category" ? CATEGORIES[current.catIndex] : null;
595
- const action = current.kind === "action" ? current.action : null;
596
- useEffect(() => {
597
- if (!action) return;
598
- setActCursor(0);
599
- if (action === "security") {
600
- setActChecked(Object.fromEntries(opts.protections.map((pr) => [pr.value, true])));
601
- } else {
602
- const detected = opts.agents.filter((a) => a.installed);
603
- const preselect = detected.length ? detected : opts.agents;
604
- setActChecked(Object.fromEntries(opts.agents.map((a) => [a.name, preselect.some((d) => d.name === a.name)])));
605
- }
606
- }, [action]);
607
- const resultRows = fill ? Math.max(3, size.rows - 7) : result?.lines.length ?? 0;
608
- const maxResultScroll = Math.max(0, (result?.lines.length ?? 0) - resultRows);
609
- const valueOf = (setting, sc) => {
610
- const k = stageKey(setting.key, sc);
611
- return k in pending ? pending[k] : setting.read(sc);
612
- };
613
- const isModified = (setting, sc) => {
614
- const k = stageKey(setting.key, sc);
615
- return k in pending && pending[k] !== setting.read(sc);
616
- };
617
- const dirty = Object.entries(pending).some(([k, v]) => {
618
- const { key, scope: sc } = parseStageKey(k);
619
- return SETTING_BY_KEY.get(key)?.read(sc) !== v;
620
- });
621
- const persistPending = () => {
622
- for (const [k, v] of Object.entries(pending)) {
623
- const { key, scope: sc } = parseStageKey(k);
624
- const setting = SETTING_BY_KEY.get(key);
625
- if (setting && setting.read(sc) !== v) setting.write(v, sc);
626
- }
627
- };
628
- const runChosen = (act) => {
629
- const keys = actionItemKeys(act);
630
- const chosen = keys.filter((k) => actChecked[k]);
631
- const req = act === "security" ? { action: act, protections: chosen } : { action: act, scope, agents: chosen };
632
- persistPending();
633
- setPending({});
634
- setBusyTitle(actionTitle(act));
635
- setResult(null);
636
- setResultScroll(0);
637
- setMode("running");
638
- opts.runAction(req).then((res) => {
639
- setResult(res);
640
- setMode("result");
641
- }).catch((err) => {
642
- setResult({ ok: false, title: actionTitle(act), lines: [`Error: ${err.message}`] });
643
- setMode("result");
644
- });
645
- };
646
- useInput((input, key) => {
647
- if (confirm4) {
648
- if (key.escape) {
649
- setConfirm(null);
650
- return;
651
- }
652
- if (key.upArrow || input === "k") {
653
- setConfirm((c) => c && { index: Math.max(0, c.index - 1) });
654
- return;
655
- }
656
- if (key.downArrow || input === "j") {
657
- setConfirm((c) => c && { index: Math.min(EXIT_OPTIONS.length - 1, c.index + 1) });
658
- return;
659
- }
660
- if (key.return || input === " ") {
661
- const index = confirm4.index;
662
- setConfirm(null);
663
- if (index === 2) return;
664
- if (index === 0) persistPending();
665
- setPending({});
666
- exit();
667
- }
668
- return;
669
- }
670
- if (mode === "running") return;
671
- if (mode === "result") {
672
- if (key.return || key.escape || input === " " || input === "q") {
673
- setMode("menu");
674
- return;
675
- }
676
- if (key.upArrow || input === "k") {
677
- setResultScroll((s) => Math.max(0, s - 1));
678
- return;
679
- }
680
- if (key.downArrow || input === "j") {
681
- setResultScroll((s) => Math.min(maxResultScroll, s + 1));
682
- return;
683
- }
684
- return;
685
- }
686
- if (input === "q" || key.escape) {
687
- dirty ? setConfirm({ index: 0 }) : exit();
688
- return;
689
- }
690
- if (input === "s") {
691
- persistPending();
692
- setPending({});
693
- return;
694
- }
695
- if (input === "x") {
696
- persistPending();
697
- setPending({});
698
- exit();
699
- return;
700
- }
701
- if (input === "g") {
702
- setScope((s) => s === "global" ? "local" : "global");
703
- return;
704
- }
705
- if (key.tab) {
706
- setFocusRight((f) => !f);
707
- return;
708
- }
709
- if (key.leftArrow || input === "h") {
710
- setFocusRight(false);
711
- return;
712
- }
713
- if (key.rightArrow || input === "l") {
714
- setFocusRight(true);
715
- return;
716
- }
717
- if (key.upArrow || input === "k") {
718
- if (focusRight && category) setSetIndex((i) => Math.max(0, i - 1));
719
- else if (focusRight && action) setActCursor((i) => Math.max(0, i - 1));
720
- else {
721
- setSideIndex((i) => Math.max(0, i - 1));
722
- setSetIndex(0);
723
- setFocusRight(false);
724
- }
725
- return;
726
- }
727
- if (key.downArrow || input === "j") {
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));
730
- else {
731
- setSideIndex((i) => Math.min(sideItems.length - 1, i + 1));
732
- setSetIndex(0);
733
- setFocusRight(false);
734
- }
735
- return;
736
- }
737
- if (input === " " && focusRight && action) {
738
- const k = actionItemKeys(action)[actCursor];
739
- setActChecked((c) => ({ ...c, [k]: !c[k] }));
740
- return;
741
- }
742
- if (key.return || input === " ") {
743
- if (!focusRight) {
744
- setFocusRight(true);
745
- return;
746
- }
747
- if (action) {
748
- runChosen(action);
749
- return;
750
- }
751
- const setting = category.settings[setIndex];
752
- setPending((pr) => ({ ...pr, [stageKey(setting.key, scope)]: !valueOf(setting, scope) }));
753
- }
754
- });
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(
756
- Box,
757
- {},
758
- h(Text, { dimColor: true }, "scope "),
759
- h(Text, { bold: true, color: scope === "global" ? "green" : "yellow" }, scope),
760
- h(Text, { dimColor: true }, " (g)"),
761
- dirty ? h(Text, { color: "yellow" }, " * unsaved") : null
762
- );
763
- const titleBar = h(
764
- Box,
765
- { width: size.columns, paddingX: 1, justifyContent: "space-between" },
766
- h(Text, { bold: true, color: "cyan" }, "enigma"),
767
- headerRight
768
- );
769
- let content;
770
- if (confirm4) {
771
- content = renderConfirm(h, Box, Text, confirm4.index, fill);
772
- } else if (mode === "running") {
773
- content = renderRunning(h, Box, Text, busyTitle, fill);
774
- } else if (mode === "result" && result) {
775
- content = renderResult(h, Box, Text, { res: result, scroll: Math.min(resultScroll, maxResultScroll), maxRows: resultRows, fill });
776
- } else {
777
- const sidebarWidth = Math.min(28, Math.max(20, Math.floor(size.columns * 0.3)));
778
- const panel = category ? renderCategoryPanel(h, Box, Text, { category, scope, focusRight, setIndex, valueOf, isModified, fill }) : renderChecklist(h, Box, Text, {
779
- title: actionTitle(action),
780
- blurb: action === "skills" ? `Scope ${scope} (g to change). Choose agents, then enter to install.` : "Choose what the commit guard enforces, then enter to apply.",
781
- items: action === "security" ? opts.protections.map((pr) => ({ key: pr.value, label: pr.label, hint: pr.hint })) : opts.agents.map((a) => ({ key: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
782
- cursor: actCursor,
783
- checked: actChecked,
784
- focused: focusRight,
785
- fill
786
- });
787
- content = h(Box, fill ? { flexGrow: 1 } : {}, renderSidebar(h, Box, Text, sideItems, sideIndex, focusRight, sidebarWidth), panel);
788
- }
789
- const menuFooter = focusRight && action ? action === "skills" ? "up/down move space toggle g scope enter install tab back" : "up/down move space toggle enter apply tab back" : focusRight && category ? "up/down move enter toggle g scope tab back" : `up/down move tab switch enter ${action ? "edit" : "focus"} s save x save & exit q quit`;
790
- const footerText = confirm4 ? "up/down move enter select esc cancel" : mode === "running" ? "working..." : mode === "result" ? `${maxResultScroll > 0 ? "up/down scroll " : ""}enter / esc back to menu` : menuFooter;
791
- const footer = h(Box, { width: size.columns, paddingX: 1 }, h(Text, { dimColor: true }, footerText));
792
- return h(
793
- Box,
794
- { width: size.columns, ...fill ? { height: size.rows } : {}, flexDirection: "column" },
795
- titleBar,
796
- content,
797
- footer
798
- );
799
- };
800
- }
801
- function renderSidebar(h, Box, Text, items, index, focusRight, width) {
802
- return h(Box, {
803
- flexDirection: "column",
804
- borderStyle: "round",
805
- borderColor: focusRight ? "gray" : "cyan",
806
- paddingX: 1,
807
- width,
808
- marginRight: 1
809
- }, [
810
- h(Text, { key: "__t", bold: true, dimColor: true }, "MENU"),
811
- ...items.map((it, i) => h(Text, {
812
- key: String(i),
813
- inverse: !focusRight && i === index,
814
- color: i === index ? "cyan" : void 0
815
- }, ` ${it.title} `))
816
- ]);
817
- }
818
- function renderConfirm(h, Box, Text, index, fill) {
819
- return h(
820
- Box,
821
- { justifyContent: "center", ...fill ? { flexGrow: 1, alignItems: "center" } : {} },
822
- h(Box, {
823
- flexDirection: "column",
824
- borderStyle: "round",
825
- borderColor: "yellow",
826
- paddingX: 2,
827
- paddingY: 1
828
- }, [
829
- h(Text, { key: "__t", bold: true, color: "yellow" }, "You have unsaved changes"),
830
- h(
831
- Box,
832
- { key: "__o", marginTop: 1, flexDirection: "column" },
833
- EXIT_OPTIONS.map((o, i) => h(Text, { key: o, inverse: i === index, bold: i === index }, ` ${o} `))
834
- )
835
- ])
836
- );
837
- }
838
- function renderRunning(h, Box, Text, title, fill) {
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
- }
873
- function renderChecklist(h, Box, Text, s) {
874
- const rows = s.items.map((it, i) => {
875
- const on = !!s.checked[it.key];
876
- const selected = s.focused && i === s.cursor;
877
- return h(
878
- Box,
879
- { key: it.key, justifyContent: "space-between" },
880
- h(Text, { inverse: selected, bold: selected }, ` ${on ? "[x]" : "[ ]"} ${it.label} `),
881
- h(Text, { dimColor: true }, `${it.hint} `)
882
- );
883
- });
884
- return h(Box, {
885
- flexDirection: "column",
886
- borderStyle: "round",
887
- borderColor: s.focused ? "cyan" : "gray",
888
- paddingX: 1,
889
- flexGrow: 1
890
- }, [
891
- h(Text, { key: "__t", bold: true, color: "cyan" }, s.title),
892
- h(Text, { key: "__bl", dimColor: true }, s.blurb),
893
- h(Box, { key: "__rows", marginTop: 1, flexDirection: "column" }, rows),
894
- ...s.fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : []
895
- ]);
896
- }
897
- function renderCategoryPanel(h, Box, Text, s) {
898
- const focused = s.category.settings[s.setIndex];
899
- const rows = s.category.settings.map((setting, i) => {
900
- const on = s.valueOf(setting, s.scope);
901
- const modified = s.isModified(setting, s.scope);
902
- const selected = s.focusRight && i === s.setIndex;
903
- return h(
904
- Box,
905
- { key: setting.key, justifyContent: "space-between" },
906
- h(Text, { inverse: selected, bold: selected }, ` ${setting.label}${setting.globalOnly ? " (global)" : ""} `),
907
- h(Text, { bold: true, color: modified ? "yellow" : on ? "green" : "gray" }, `${valueLabel(on)}${modified ? " *" : ""} `)
908
- );
909
- });
910
- return h(Box, {
911
- flexDirection: "column",
912
- borderStyle: "round",
913
- borderColor: s.focusRight ? "cyan" : "gray",
914
- paddingX: 1,
915
- flexGrow: 1
916
- }, [
917
- h(Text, { key: "__b", bold: true, color: "cyan" }, s.category.title),
918
- h(Text, { key: "__bl", dimColor: true }, s.category.blurb),
919
- h(Box, { key: "__rows", marginTop: 1, flexDirection: "column" }, rows),
920
- ...s.fill ? [h(Box, { key: "__grow", flexGrow: 1 })] : [],
921
- h(Text, { key: "__hint", marginTop: 1, dimColor: true, wrap: "truncate-end" }, focused.hint)
922
- ]);
923
- }
924
- var CLEAR_SCREEN, SETTING_BY_KEY, stageKey, parseStageKey, ACTION_ITEMS, actionTitle, EXIT_OPTIONS;
925
- var init_settings = __esm({
926
- "src/tui/settings.ts"() {
927
- "use strict";
928
- init_config();
929
- init_settings_registry();
930
- CLEAR_SCREEN = "\x1B[2J\x1B[H";
931
- SETTING_BY_KEY = new Map(ALL_SETTINGS.map((s) => [s.key, s]));
932
- stageKey = (key, scope) => `${scope}/${key}`;
933
- parseStageKey = (composite) => {
934
- const i = composite.indexOf("/");
935
- return { scope: composite.slice(0, i), key: composite.slice(i + 1) };
936
- };
937
- ACTION_ITEMS = [
938
- { action: "skills", title: "Install agent skills", blurb: "Claude Code, Codex, OpenCode" },
939
- { action: "security", title: "Git security hooks", blurb: "block secrets, .env, node_modules on commit" }
940
- ];
941
- actionTitle = (action) => ACTION_ITEMS.find((a) => a.action === action).title;
942
- EXIT_OPTIONS = ["Save & exit", "Exit without saving", "Cancel"];
943
- }
944
- });
945
-
946
- // src/tui/opentui.ts
947
- var opentui_exports = {};
948
- __export(opentui_exports, {
949
- runHomeTui: () => runHomeTui2
950
- });
951
- async function runHomeTui2(hub) {
952
- if (!process.stdout.isTTY) return;
953
- const React = (await import("react")).default;
954
- const { createCliRenderer, TextAttributes } = await import("@opentui/core");
955
- const { createRoot, useKeyboard, useTerminalDimensions } = await import("@opentui/react");
956
- const h = React.createElement;
957
- const { useState, useEffect } = React;
958
- const box = "box";
959
- const text = "text";
960
- const BOLD = TextAttributes.BOLD;
961
- const DIM = TextAttributes.DIM;
962
- const agents = hub.agents;
963
- const protections = hub.protections;
964
- const txt = (content, props = {}) => h(text, props, content);
965
- const selStyle = (selected, normal = {}) => selected ? { bg: SEL_BG, fg: SEL_FG, attributes: BOLD } : normal;
966
- const panelBox = (borderColor, children, extra = {}) => h(box, { border: true, borderStyle: "rounded", borderColor, flexDirection: "column", paddingLeft: 1, paddingRight: 1, flexGrow: 1, ...extra }, ...children);
967
- const renderSidebar2 = (items, index, focusRight, width) => h(
968
- box,
969
- { border: true, borderStyle: "rounded", borderColor: focusRight ? COL.gray : COL.cyan, flexDirection: "column", paddingLeft: 1, paddingRight: 1, width, marginRight: 1 },
970
- txt("MENU", { fg: COL.gray, attributes: BOLD }),
971
- ...items.map((it, i) => txt(
972
- ` ${it.title} `,
973
- !focusRight && i === index ? { bg: SEL_BG, fg: SEL_FG, attributes: BOLD } : { fg: i === index ? COL.cyan : void 0 }
974
- ))
975
- );
976
- const renderChecklist2 = (s) => panelBox(s.focused ? COL.cyan : COL.gray, [
977
- txt(s.title, { fg: COL.cyan, attributes: BOLD }),
978
- txt(s.blurb, { fg: COL.gray }),
979
- h(
980
- box,
981
- { flexDirection: "column", marginTop: 1 },
982
- ...s.items.map((it, i) => {
983
- const on = !!s.checked[it.key];
984
- const selected = s.focused && i === s.cursor;
985
- return h(
986
- box,
987
- { flexDirection: "row", justifyContent: "space-between" },
988
- txt(` ${on ? "[x]" : "[ ]"} ${it.label} `, selStyle(selected)),
989
- txt(`${it.hint} `, { fg: COL.gray })
990
- );
991
- })
992
- )
993
- ]);
994
- const renderCategoryPanel2 = (s) => {
995
- const focusedHint = s.category.settings[s.setIndex].hint;
996
- return panelBox(s.focusRight ? COL.cyan : COL.gray, [
997
- txt(s.category.title, { fg: COL.cyan, attributes: BOLD }),
998
- txt(s.category.blurb, { fg: COL.gray }),
999
- h(
1000
- box,
1001
- { flexDirection: "column", marginTop: 1 },
1002
- ...s.category.settings.map((setting, i) => {
1003
- const on = s.valueOf(setting, s.scope);
1004
- const modified = s.isModified(setting, s.scope);
1005
- const selected = s.focusRight && i === s.setIndex;
1006
- return h(
1007
- box,
1008
- { flexDirection: "row", justifyContent: "space-between" },
1009
- txt(` ${setting.label}${setting.globalOnly ? " (global)" : ""} `, selStyle(selected)),
1010
- txt(`${valueLabel(on)}${modified ? " *" : ""} `, { attributes: BOLD, fg: modified ? COL.yellow : on ? COL.green : COL.gray })
1011
- );
1012
- })
1013
- ),
1014
- h(box, { flexGrow: 1 }),
1015
- txt(focusedHint, { fg: COL.gray, marginTop: 1, truncate: true })
1016
- ]);
1017
- };
1018
- const renderConfirm2 = (index) => h(
1019
- box,
1020
- { flexGrow: 1, justifyContent: "center", alignItems: "center" },
1021
- h(
1022
- box,
1023
- { border: true, borderStyle: "rounded", borderColor: COL.yellow, flexDirection: "column", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1 },
1024
- txt("You have unsaved changes", { fg: COL.yellow, attributes: BOLD }),
1025
- h(
1026
- box,
1027
- { flexDirection: "column", marginTop: 1 },
1028
- ...EXIT_OPTIONS2.map((o, i) => txt(` ${o} `, selStyle(i === index)))
1029
- )
1030
- )
1031
- );
1032
- const renderRunning2 = (title) => panelBox(COL.cyan, [
1033
- txt(title || "Working", { fg: COL.cyan, attributes: BOLD }),
1034
- txt("Working...", { fg: COL.gray, marginTop: 1 }),
1035
- h(box, { flexGrow: 1 })
1036
- ]);
1037
- const renderResult2 = (res, scroll, maxRows) => {
1038
- const windowed = maxRows > 0 && res.lines.length > maxRows;
1039
- const start = windowed ? Math.max(0, Math.min(scroll, res.lines.length - maxRows)) : 0;
1040
- const slice = windowed ? res.lines.slice(start, start + maxRows) : res.lines;
1041
- const above = windowed && start > 0;
1042
- const below = windowed && start + maxRows < res.lines.length;
1043
- const rows = slice.length ? slice.map((line, i) => txt(` ${line} `, { key: String(start + i), truncate: true })) : [txt(" (no output) ", { fg: COL.gray })];
1044
- return panelBox(res.ok ? COL.green : COL.red, [
1045
- txt(res.title, { fg: res.ok ? COL.green : COL.red, attributes: BOLD }),
1046
- txt(above ? ` ... ${start} more above ` : " ", { fg: COL.gray }),
1047
- h(box, { flexDirection: "column" }, ...rows),
1048
- txt(below ? ` ... ${res.lines.length - start - maxRows} more below ` : " ", { fg: COL.gray }),
1049
- h(box, { flexGrow: 1 })
1050
- ]);
1051
- };
1052
- const sideItems = [
1053
- ...CATEGORIES.map((c, i) => ({ kind: "category", catIndex: i, title: c.title })),
1054
- ...ACTION_ITEMS2.map((a) => ({ kind: "action", ...a }))
1055
- ];
1056
- const actionItemKeys = (action) => action === "security" ? protections.map((p7) => p7.value) : agents.map((a) => a.name);
1057
- function App({ onExit }) {
1058
- const dims = useTerminalDimensions();
1059
- const size = { columns: dims.width || 80, rows: dims.height || 24 };
1060
- const [mode, setMode] = useState("menu");
1061
- const [scope, setScope] = useState("global");
1062
- const [sideIndex, setSideIndex] = useState(0);
1063
- const [focusRight, setFocusRight] = useState(false);
1064
- const [setIndex, setSetIndex] = useState(0);
1065
- const [pending, setPending] = useState({});
1066
- const [confirm4, setConfirm] = useState(null);
1067
- const [actCursor, setActCursor] = useState(0);
1068
- const [actChecked, setActChecked] = useState({});
1069
- const [busyTitle, setBusyTitle] = useState("");
1070
- const [result, setResult] = useState(null);
1071
- const [resultScroll, setResultScroll] = useState(0);
1072
- const current = sideItems[sideIndex];
1073
- const category = current.kind === "category" ? CATEGORIES[current.catIndex] : null;
1074
- const action = current.kind === "action" ? current.action : null;
1075
- useEffect(() => {
1076
- if (!action) return;
1077
- setActCursor(0);
1078
- if (action === "security") {
1079
- setActChecked(Object.fromEntries(protections.map((p7) => [p7.value, true])));
1080
- } else {
1081
- const detected = agents.filter((a) => a.installed);
1082
- const preselect = detected.length ? detected : agents;
1083
- setActChecked(Object.fromEntries(agents.map((a) => [a.name, preselect.some((d) => d.name === a.name)])));
1084
- }
1085
- }, [action]);
1086
- const resultRows = Math.max(3, size.rows - 7);
1087
- const maxResultScroll = Math.max(0, (result?.lines.length ?? 0) - resultRows);
1088
- const valueOf = (setting, sc) => {
1089
- const k = stageKey2(setting.key, sc);
1090
- return k in pending ? pending[k] : setting.read(sc);
1091
- };
1092
- const isModified = (setting, sc) => {
1093
- const k = stageKey2(setting.key, sc);
1094
- return k in pending && pending[k] !== setting.read(sc);
1095
- };
1096
- const dirty = Object.entries(pending).some(([k, v]) => {
1097
- const { key, scope: sc } = parseStageKey2(k);
1098
- return SETTING_BY_KEY2.get(key)?.read(sc) !== v;
1099
- });
1100
- const persistPending = () => {
1101
- for (const [k, v] of Object.entries(pending)) {
1102
- const { key, scope: sc } = parseStageKey2(k);
1103
- const setting = SETTING_BY_KEY2.get(key);
1104
- if (setting && setting.read(sc) !== v) setting.write(v, sc);
1105
- }
1106
- };
1107
- const runChosen = (act) => {
1108
- const chosen = actionItemKeys(act).filter((k) => actChecked[k]);
1109
- const req = act === "security" ? { action: act, protections: chosen } : { action: act, scope, agents: chosen };
1110
- persistPending();
1111
- setPending({});
1112
- setBusyTitle(actionTitle2(act));
1113
- setResult(null);
1114
- setResultScroll(0);
1115
- setMode("running");
1116
- hub.runAction(req).then((res) => {
1117
- setResult(res);
1118
- setMode("result");
1119
- }).catch((err) => {
1120
- setResult({ ok: false, title: actionTitle2(act), lines: [`Error: ${err.message}`] });
1121
- setMode("result");
1122
- });
1123
- };
1124
- useKeyboard((key) => {
1125
- const name = key.name;
1126
- const up = name === "up", down = name === "down", left = name === "left", right = name === "right";
1127
- const enter = name === "return", esc = name === "escape", tab = name === "tab", space = name === "space";
1128
- const ch = name && name.length === 1 ? name : "";
1129
- if (confirm4) {
1130
- if (esc) {
1131
- setConfirm(null);
1132
- return;
1133
- }
1134
- if (up || ch === "k") {
1135
- setConfirm((c) => c && { index: Math.max(0, c.index - 1) });
1136
- return;
1137
- }
1138
- if (down || ch === "j") {
1139
- setConfirm((c) => c && { index: Math.min(EXIT_OPTIONS2.length - 1, c.index + 1) });
1140
- return;
1141
- }
1142
- if (enter || space) {
1143
- const index = confirm4.index;
1144
- setConfirm(null);
1145
- if (index === 2) return;
1146
- if (index === 0) persistPending();
1147
- setPending({});
1148
- onExit();
1149
- }
1150
- return;
1151
- }
1152
- if (mode === "running") return;
1153
- if (mode === "result") {
1154
- if (enter || esc || space || ch === "q") {
1155
- setMode("menu");
1156
- return;
1157
- }
1158
- if (up || ch === "k") {
1159
- setResultScroll((s) => Math.max(0, s - 1));
1160
- return;
1161
- }
1162
- if (down || ch === "j") {
1163
- setResultScroll((s) => Math.min(maxResultScroll, s + 1));
1164
- return;
1165
- }
1166
- return;
1167
- }
1168
- if (ch === "q" || esc) {
1169
- dirty ? setConfirm({ index: 0 }) : onExit();
1170
- return;
1171
- }
1172
- if (ch === "s") {
1173
- persistPending();
1174
- setPending({});
1175
- return;
1176
- }
1177
- if (ch === "x") {
1178
- persistPending();
1179
- setPending({});
1180
- onExit();
1181
- return;
1182
- }
1183
- if (ch === "g") {
1184
- setScope((s) => s === "global" ? "local" : "global");
1185
- return;
1186
- }
1187
- if (tab) {
1188
- setFocusRight((f) => !f);
1189
- return;
1190
- }
1191
- if (left || ch === "h") {
1192
- setFocusRight(false);
1193
- return;
1194
- }
1195
- if (right || ch === "l") {
1196
- setFocusRight(true);
1197
- return;
1198
- }
1199
- if (up || ch === "k") {
1200
- if (focusRight && category) setSetIndex((i) => Math.max(0, i - 1));
1201
- else if (focusRight && action) setActCursor((i) => Math.max(0, i - 1));
1202
- else {
1203
- setSideIndex((i) => Math.max(0, i - 1));
1204
- setSetIndex(0);
1205
- setFocusRight(false);
1206
- }
1207
- return;
1208
- }
1209
- if (down || ch === "j") {
1210
- if (focusRight && category) setSetIndex((i) => Math.min(category.settings.length - 1, i + 1));
1211
- else if (focusRight && action) setActCursor((i) => Math.min(actionItemKeys(action).length - 1, i + 1));
1212
- else {
1213
- setSideIndex((i) => Math.min(sideItems.length - 1, i + 1));
1214
- setSetIndex(0);
1215
- setFocusRight(false);
1216
- }
1217
- return;
1218
- }
1219
- if (space && focusRight && action) {
1220
- const k = actionItemKeys(action)[actCursor];
1221
- setActChecked((c) => ({ ...c, [k]: !c[k] }));
1222
- return;
1223
- }
1224
- if (enter || space) {
1225
- if (!focusRight) {
1226
- setFocusRight(true);
1227
- return;
1228
- }
1229
- if (action) {
1230
- runChosen(action);
1231
- return;
1232
- }
1233
- const setting = category.settings[setIndex];
1234
- setPending((p7) => ({ ...p7, [stageKey2(setting.key, scope)]: !valueOf(setting, scope) }));
1235
- }
1236
- });
1237
- const headerRight = confirm4 ? txt("unsaved changes", { fg: COL.yellow }) : mode === "running" ? txt("working", { fg: COL.gray }) : mode === "result" ? txt("result", { fg: COL.gray }) : h(
1238
- box,
1239
- { flexDirection: "row" },
1240
- txt("scope ", { fg: COL.gray }),
1241
- txt(scope, { fg: scope === "global" ? COL.green : COL.yellow, attributes: BOLD }),
1242
- txt(" (g)", { fg: COL.gray }),
1243
- dirty ? txt(" * unsaved", { fg: COL.yellow }) : null
1244
- );
1245
- const titleBar = h(
1246
- box,
1247
- { width: size.columns, flexDirection: "row", paddingLeft: 1, paddingRight: 1, justifyContent: "space-between" },
1248
- txt("enigma", { fg: COL.cyan, attributes: BOLD }),
1249
- headerRight
1250
- );
1251
- let content;
1252
- if (confirm4) {
1253
- content = renderConfirm2(confirm4.index);
1254
- } else if (mode === "running") {
1255
- content = renderRunning2(busyTitle);
1256
- } else if (mode === "result" && result) {
1257
- content = renderResult2(result, Math.min(resultScroll, maxResultScroll), resultRows);
1258
- } else {
1259
- const sidebarWidth = Math.min(28, Math.max(20, Math.floor(size.columns * 0.3)));
1260
- const panel = category ? renderCategoryPanel2({ category, scope, focusRight, setIndex, valueOf, isModified }) : renderChecklist2({
1261
- title: actionTitle2(action),
1262
- blurb: action === "skills" ? `Scope ${scope} (g to change). Choose agents, then enter to install.` : "Choose what the commit guard enforces, then enter to apply.",
1263
- items: action === "security" ? protections.map((p7) => ({ key: p7.value, label: p7.label, hint: p7.hint })) : agents.map((a) => ({ key: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
1264
- cursor: actCursor,
1265
- checked: actChecked,
1266
- focused: focusRight
1267
- });
1268
- content = h(box, { flexGrow: 1, flexDirection: "row" }, renderSidebar2(sideItems, sideIndex, focusRight, sidebarWidth), panel);
1269
- }
1270
- const footerLine = (s) => h(box, { width: size.columns, paddingLeft: 1, paddingRight: 1 }, txt(s, { fg: COL.gray, attributes: DIM }));
1271
- const menuNav = focusRight && action ? action === "skills" ? "up/down move space toggle g scope enter install tab back" : "up/down move space toggle enter apply tab back" : focusRight && category ? "up/down move enter toggle g scope tab back" : `up/down move tab switch enter ${action ? "edit" : "focus"}`;
1272
- let footer;
1273
- if (confirm4) {
1274
- footer = footerLine("up/down move enter select esc cancel");
1275
- } else if (mode === "running") {
1276
- footer = footerLine("working...");
1277
- } else if (mode === "result") {
1278
- footer = footerLine(`${maxResultScroll > 0 ? "up/down scroll " : ""}enter / esc back to menu`);
1279
- } else {
1280
- footer = h(
1281
- box,
1282
- { width: size.columns, flexDirection: "row", justifyContent: "space-between", paddingLeft: 1, paddingRight: 1 },
1283
- txt(menuNav, { fg: COL.gray, attributes: DIM }),
1284
- txt("s save x save & exit q quit", { fg: COL.gray, attributes: DIM })
1285
- );
1286
- }
1287
- return h(box, { width: size.columns, height: size.rows, flexDirection: "column" }, titleBar, content, footer);
1288
- }
1289
- const renderer = await createCliRenderer({ exitOnCtrlC: true });
1290
- await new Promise((resolve3) => {
1291
- const root = createRoot(renderer);
1292
- const onExit = () => {
1293
- try {
1294
- root.unmount();
1295
- } catch {
1296
- }
1297
- try {
1298
- renderer.destroy();
1299
- } catch {
1300
- }
1301
- resolve3();
1302
- };
1303
- root.render(h(App, { onExit }));
1304
- });
1305
- }
1306
- var COL, SEL_BG, SEL_FG, SETTING_BY_KEY2, stageKey2, parseStageKey2, ACTION_ITEMS2, actionTitle2, EXIT_OPTIONS2;
1307
- var init_opentui = __esm({
1308
- "src/tui/opentui.ts"() {
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
-
1335
- // src/cli.ts
1336
- init_util();
1337
- import { dirname as dirname4, join as join10 } from "path";
1338
- import { fileURLToPath as fileURLToPath4 } from "url";
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
- }
1385
-
1386
- // src/skills.ts
1387
- init_util();
1388
- init_agents();
1389
- import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4, cpSync as cpSync2, mkdirSync as mkdirSync4, rmSync } from "fs";
1390
- import { dirname as dirname2, join as join6, resolve as resolve2, relative as relative2, sep as sep2 } from "path";
1391
- import { fileURLToPath as fileURLToPath2 } from "url";
1392
- import { createHash } from "crypto";
1393
- import * as p4 from "@clack/prompts";
1394
-
1395
- // src/security.ts
1396
- init_util();
1397
- import { existsSync as existsSync3, mkdirSync, cpSync, writeFileSync, chmodSync } from "fs";
1398
- import { dirname, join as join3, resolve, relative } from "path";
1399
- import { fileURLToPath } from "url";
1400
- import { execFileSync as execFileSync2 } from "child_process";
1401
- import * as p2 from "@clack/prompts";
1402
- var __dirname = dirname(fileURLToPath(import.meta.url));
1403
- function findGuardSrc() {
1404
- const candidates = [
1405
- join3(__dirname, "guard.js"),
1406
- join3(__dirname, "..", "dist", "guard.js")
1407
- ];
1408
- return candidates.find((c) => existsSync3(c)) ?? null;
1409
- }
1410
- var GUARD_PROTECTIONS = [
1411
- { value: "secrets", label: "Block committed secrets", hint: "API keys, tokens, private keys" },
1412
- { value: "envFiles", label: "Block .env files", hint: "allows .env.example / .sample / .template" },
1413
- { value: "depDirs", label: "Block dependency/cache dirs", hint: "node_modules, __pycache__, venv" },
1414
- { value: "generatedDirs", label: "Warn on generated dirs", hint: "dist, build, .next, coverage" },
1415
- { value: "junkFiles", label: "Warn on log / OS junk files", hint: ".log, .DS_Store, Thumbs.db" },
1416
- { value: "largeFiles", label: "Warn on files over 5 MB", hint: "oversized blobs" }
1417
- ];
1418
- function findGitRoot(start) {
1419
- let dir = resolve(start);
1420
- for (; ; ) {
1421
- if (existsSync3(join3(dir, ".git"))) return dir;
1422
- const parent = dirname(dir);
1423
- if (parent === dir) return null;
1424
- dir = parent;
1425
- }
1426
- }
1427
- function currentHooksPath(root) {
1428
- try {
1429
- return execFileSync2("git", ["-C", root, "config", "--get", "core.hooksPath"], { encoding: "utf8" }).trim();
1430
- } catch {
1431
- return "";
1432
- }
1433
- }
1434
- async function setupGitHooks(opts, interactive, reporter = clackReporter()) {
1435
- const root = findGitRoot(process.cwd());
1436
- if (!root) {
1437
- reporter.error("Not inside a git repository (no .git found). Run this from your project root.");
1438
- return false;
1439
- }
1440
- const guardSrc = findGuardSrc();
1441
- if (!guardSrc) {
1442
- reporter.error("Cannot find the built guard (dist/guard.js). Run 'npm run build' first.");
1443
- return false;
1444
- }
1445
- const current = currentHooksPath(root);
1446
- if (current && current !== ".githooks" && !opts.force) {
1447
- reporter.warn(`core.hooksPath is already set to '${current}'.`);
1448
- if (interactive) {
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.");
1452
- return false;
1453
- }
1454
- } else {
1455
- reporter.info("Re-run with --force to override.");
1456
- return false;
1457
- }
1458
- }
1459
- let enabled = opts.protections;
1460
- if (!enabled && interactive) {
1461
- const r = await p2.multiselect({
1462
- message: "Which protections should the commit guard enforce?",
1463
- options: GUARD_PROTECTIONS,
1464
- initialValues: GUARD_PROTECTIONS.map((o) => o.value),
1465
- required: true
1466
- });
1467
- if (p2.isCancel(r)) {
1468
- reporter.info("Left git hooks unchanged.");
1469
- return false;
1470
- }
1471
- enabled = r;
1472
- }
1473
- const config = {};
1474
- for (const o of GUARD_PROTECTIONS) config[o.value] = enabled ? enabled.includes(o.value) : true;
1475
- const hooksDir = join3(root, ".githooks");
1476
- mkdirSync(hooksDir, { recursive: true });
1477
- cpSync(guardSrc, join3(hooksDir, "guard.mjs"), { force: true });
1478
- writeFileSync(join3(hooksDir, "enigma-guard.json"), JSON.stringify(config, null, 2) + "\n");
1479
- const shimPath = join3(hooksDir, "pre-commit");
1480
- const shim = [
1481
- "#!/bin/sh",
1482
- "# Managed by enigma (enigma-cli) - blocks committed secrets, .env files, and dependency dirs.",
1483
- "# Toggle protections in .githooks/enigma-guard.json. Bypass once: git commit --no-verify",
1484
- 'exec node "$(git rev-parse --show-toplevel)/.githooks/guard.mjs" "$@"',
1485
- ""
1486
- ].join("\n");
1487
- writeFileSync(shimPath, shim);
1488
- try {
1489
- chmodSync(shimPath, 493);
1490
- } catch {
1491
- }
1492
- try {
1493
- chmodSync(join3(hooksDir, "guard.mjs"), 493);
1494
- } catch {
1495
- }
1496
- try {
1497
- execFileSync2("git", ["-C", root, "config", "core.hooksPath", ".githooks"]);
1498
- } catch (err) {
1499
- reporter.error(`Failed to set core.hooksPath: ${err.message}`);
1500
- return false;
1501
- }
1502
- const on = Object.entries(config).filter(([, v]) => v).map(([k]) => k);
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.`);
1505
- if (isOnPath("gh")) {
1506
- reporter.info("GitHub CLI (gh) detected: these hooks also run for commits made via gh, since gh uses git underneath.");
1507
- }
1508
- return true;
1509
- }
1510
- async function maybeOfferGitHooks(interactive, opts) {
1511
- if (!interactive || opts.security) return;
1512
- const root = findGitRoot(process.cwd());
1513
- if (!root) return;
1514
- if (currentHooksPath(root) === ".githooks") return;
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);
1517
- }
1518
-
1519
- // src/skills.ts
1520
- init_claude();
1521
- init_permissions();
1522
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
1523
- var PKG_ROOT = resolve2(__dirname2, "..");
1524
- var ASSETS = join6(PKG_ROOT, "assets");
1525
- var SKILLS_ROOT = join6(ASSETS, "skills");
1526
- var MEMORY_ROOT = join6(ASSETS, "memory");
1527
- function cliVersion() {
1528
- return (readJson(join6(PKG_ROOT, "package.json")) || {}).version || "0.0.0";
1529
- }
1530
- function serializeMeta(meta) {
1531
- const ordered = {};
1532
- for (const k of ["name", "version", "provider", "description", "cliVersion", "sha"]) {
1533
- if (meta[k] !== void 0) ordered[k] = meta[k];
1534
- }
1535
- for (const k of Object.keys(meta)) if (!(k in ordered)) ordered[k] = meta[k];
1536
- return JSON.stringify(ordered, null, 2) + "\n";
1537
- }
1538
- function readSkillMeta(skillDir) {
1539
- return readJson(join6(skillDir, "skill.json")) || {};
1540
- }
1541
- function listFilesRel(dir, base = dir) {
1542
- const out = [];
1543
- for (const e of readdirSync(dir)) {
1544
- const full = join6(dir, e);
1545
- if (isDir(full)) out.push(...listFilesRel(full, base));
1546
- else out.push(relative2(base, full).split(sep2).join("/"));
1547
- }
1548
- return out;
1549
- }
1550
- function computeContentSha(dir) {
1551
- const files = listFilesRel(dir).filter((f) => f !== "skill.json").sort();
1552
- const h = createHash("sha256");
1553
- for (const f of files) {
1554
- h.update(f);
1555
- h.update("\0");
1556
- h.update(readFileSync3(join6(dir, f)));
1557
- h.update("\0");
1558
- }
1559
- return h.digest("hex");
1560
- }
1561
- function skillStatus(destDir, srcMeta) {
1562
- if (!existsSync5(destDir)) return { kind: "install", from: null, to: srcMeta.version || null };
1563
- const destMeta = readSkillMeta(destDir);
1564
- const from = destMeta.version || null;
1565
- const to = srcMeta.version || null;
1566
- if (from && to && from !== to) return { kind: "update", from, to };
1567
- const recordedSha = destMeta.sha || null;
1568
- if (!recordedSha) return { kind: "reinstall", from, to };
1569
- const actualSha = computeContentSha(destDir);
1570
- if (actualSha !== recordedSha) return { kind: "tampered", from, to };
1571
- return { kind: "identical", from, to };
1572
- }
1573
- function statusLabel(st) {
1574
- switch (st.kind) {
1575
- case "install":
1576
- return st.to ? `install v${st.to}` : "install";
1577
- case "update":
1578
- return `update ${st.from} -> ${st.to}`;
1579
- case "identical":
1580
- return st.to ? `up-to-date v${st.to} (skip)` : "up-to-date (skip)";
1581
- case "tampered":
1582
- return st.to ? `MODIFIED locally v${st.to}` : "MODIFIED locally";
1583
- default:
1584
- return st.to ? `reinstall v${st.to}` : "reinstall";
1585
- }
1586
- }
1587
- function filesEqual(a, b) {
1588
- try {
1589
- return readFileSync3(a).equals(readFileSync3(b));
1590
- } catch {
1591
- return false;
1592
- }
1593
- }
1594
- function memoryStatus(srcFile, destFile) {
1595
- if (!existsSync5(destFile)) return "install";
1596
- return filesEqual(srcFile, destFile) ? "identical" : "overwrite";
1597
- }
1598
- function computePrune(destSkillsDir, sourceNames) {
1599
- if (!isDir(destSkillsDir)) return [];
1600
- return readdirSync(destSkillsDir).filter((e) => isDir(join6(destSkillsDir, e)) && existsSync5(join6(destSkillsDir, e, "SKILL.md"))).filter((e) => !sourceNames.includes(e)).map((e) => ({ name: e, dir: join6(destSkillsDir, e), meta: readSkillMeta(join6(destSkillsDir, e)) })).filter((s) => isManagedProvider(s.meta.provider));
1601
- }
1602
- function inspectSkills() {
1603
- if (!isDir(SKILLS_ROOT)) return [];
1604
- return readdirSync(SKILLS_ROOT).filter((e) => isDir(join6(SKILLS_ROOT, e)) && existsSync5(join6(SKILLS_ROOT, e, "SKILL.md"))).map((e) => ({ name: e, src: join6(SKILLS_ROOT, e), meta: readSkillMeta(join6(SKILLS_ROOT, e)) }));
1605
- }
1606
- function inspectMemory(agent) {
1607
- if (!agent.memoryFile) return [];
1608
- const src = join6(MEMORY_ROOT, agent.memoryFile);
1609
- return existsSync5(src) ? [{ name: agent.memoryFile, src }] : [];
1610
- }
1611
- function sealSources() {
1612
- if (!isDir(SKILLS_ROOT)) {
1613
- console.error(`No skills directory found at ${SKILLS_ROOT}.`);
1614
- process.exit(1);
1615
- }
1616
- const cli = cliVersion();
1617
- let sealed = 0;
1618
- for (const name of readdirSync(SKILLS_ROOT)) {
1619
- const dir = join6(SKILLS_ROOT, name);
1620
- if (!isDir(dir) || !existsSync5(join6(dir, "SKILL.md"))) continue;
1621
- const metaPath = join6(dir, "skill.json");
1622
- const meta = readJson(metaPath) || { name };
1623
- const before = JSON.stringify(meta);
1624
- meta.provider = MANAGED_PROVIDER;
1625
- meta.cliVersion = cli;
1626
- meta.sha = computeContentSha(dir);
1627
- const changed = JSON.stringify(meta) !== before;
1628
- writeFileSync4(metaPath, serializeMeta(meta));
1629
- console.log(`${changed ? "updated" : "ok "} ${name} cli=${cli} sha=${meta.sha.slice(0, 12)}`);
1630
- sealed++;
1631
- }
1632
- console.log(`
1633
- Sealed ${sealed} skill(s) at cliVersion ${cli}.`);
1634
- }
1635
- function checkSources() {
1636
- if (!isDir(SKILLS_ROOT)) {
1637
- console.error(`No skills directory found at ${SKILLS_ROOT}.`);
1638
- process.exit(1);
1639
- }
1640
- const cli = cliVersion();
1641
- const problems = [];
1642
- let checked = 0;
1643
- for (const name of readdirSync(SKILLS_ROOT)) {
1644
- const dir = join6(SKILLS_ROOT, name);
1645
- if (!isDir(dir) || !existsSync5(join6(dir, "SKILL.md"))) continue;
1646
- checked++;
1647
- const md = readFileSync3(join6(dir, "SKILL.md"), "utf8");
1648
- const fm = md.match(/^---\n([\s\S]*?)\n---/);
1649
- if (!fm) problems.push(`${name}: SKILL.md is missing YAML frontmatter`);
1650
- else {
1651
- if (!/^name:\s*\S/m.test(fm[1])) problems.push(`${name}: frontmatter missing 'name'`);
1652
- if (!/^description:\s*\S/m.test(fm[1])) problems.push(`${name}: frontmatter missing 'description'`);
1653
- }
1654
- const metaPath = join6(dir, "skill.json");
1655
- if (!existsSync5(metaPath)) {
1656
- problems.push(`${name}: missing skill.json`);
1657
- continue;
1658
- }
1659
- const meta = readJson(metaPath);
1660
- if (!meta) {
1661
- problems.push(`${name}: skill.json is not valid JSON`);
1662
- continue;
1663
- }
1664
- if (!isManagedProvider(meta.provider)) problems.push(`${name}: skill.json provider is not ${MANAGED_PROVIDER}`);
1665
- if (!meta.version) problems.push(`${name}: skill.json missing 'version'`);
1666
- if (meta.cliVersion !== cli) problems.push(`${name}: stale cliVersion (${meta.cliVersion || "none"} != ${cli}) - run 'enigma seal'`);
1667
- if (!meta.sha) problems.push(`${name}: not sealed (run 'enigma seal')`);
1668
- else if (meta.sha !== computeContentSha(dir)) problems.push(`${name}: stale sha - content changed since seal (run 'enigma seal')`);
1669
- }
1670
- if (problems.length) {
1671
- console.error(`Integrity check FAILED (${problems.length} problem(s) across ${checked} skill(s)):`);
1672
- for (const pr of problems) console.error(` - ${pr}`);
1673
- process.exit(1);
1674
- }
1675
- console.log(`Integrity check passed: ${checked} skill(s) well-formed and sealed.`);
1676
- }
1677
- async function installSkills(opts, interactive, reporter = clackReporter()) {
1678
- const available = discoverAgents();
1679
- if (available.length === 0) reporter.fatal("No installable agents known.");
1680
- let scope;
1681
- if (opts.scope) {
1682
- scope = opts.scope;
1683
- } else if (interactive) {
1684
- const r = await p4.select({
1685
- message: "Where should skills be installed?",
1686
- options: [
1687
- { value: "global", label: "Global (user)", hint: "~/.claude, ~/.codex, ~/.config/opencode" },
1688
- { value: "local", label: "Local (this project)", hint: process.cwd() }
1689
- ]
1690
- });
1691
- if (p4.isCancel(r)) {
1692
- p4.cancel("Aborted.");
1693
- return;
1694
- }
1695
- scope = r;
1696
- } else {
1697
- scope = "global";
1698
- }
1699
- const detected = available.filter((a) => a.installed);
1700
- let chosenAgents = available;
1701
- if (opts.agents.length) {
1702
- chosenAgents = available.filter((a) => opts.agents.includes(a.name));
1703
- const unknown = opts.agents.filter((n) => !available.some((a) => a.name === n));
1704
- if (unknown.length) reporter.warn(`Skipping unknown/absent agents: ${unknown.join(", ")}`);
1705
- } else if (opts.allAgents) {
1706
- chosenAgents = available;
1707
- } else if (interactive && available.length > 1) {
1708
- const preselect = (detected.length ? detected : available).map((a) => a.name);
1709
- const r = await p4.multiselect({
1710
- message: "Which agents? (detected on this system are preselected)",
1711
- options: available.map((a) => ({ value: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
1712
- initialValues: preselect,
1713
- required: true
1714
- });
1715
- if (p4.isCancel(r)) {
1716
- p4.cancel("Aborted.");
1717
- return;
1718
- }
1719
- chosenAgents = available.filter((a) => r.includes(a.name));
1720
- } else if (detected.length) {
1721
- chosenAgents = detected;
1722
- } else {
1723
- chosenAgents = available;
1724
- reporter.warn("No installed agents detected; defaulting to all supported agents.");
1725
- }
1726
- if (chosenAgents.length === 0) reporter.fatal("No matching agents selected.");
1727
- const claudeScope = chosenAgents.some((a) => a.name === "claude") ? scope : null;
1728
- const applyClaudeConfig = () => {
1729
- if (!claudeScope || opts.dryRun) return;
1730
- if (disableClaudeAttribution(claudeScope)) {
1731
- reporter.info("Claude Code: disabled Co-Authored-By and PR attribution in settings.json.");
1732
- }
1733
- };
1734
- const bypassAgents = await resolveBypassSelection(chosenAgents, opts, interactive);
1735
- const applyBypassConfig = () => applyBypass(bypassAgents, scope, opts.dryRun);
1736
- const plan = [];
1737
- for (const agent of chosenAgents) {
1738
- const target = agent.targets[scope];
1739
- if (!target) {
1740
- reporter.warn(`${agent.label} has no '${scope}' target - skipping.`);
1741
- continue;
1742
- }
1743
- const skills = inspectSkills();
1744
- const memory = inspectMemory(agent);
1745
- let chosenSkills = skills;
1746
- if (!opts.memoryOnly && opts.skills.length) {
1747
- chosenSkills = skills.filter((s2) => opts.skills.includes(s2.name));
1748
- } else if (!opts.memoryOnly && interactive && skills.length > 1) {
1749
- const r = await p4.multiselect({
1750
- message: `Skills for ${agent.label} - all selected; deselect any you don't want`,
1751
- options: skills.map((s2) => {
1752
- const st = skillStatus(join6(target.skills, s2.name), s2.meta);
1753
- const prov = s2.meta.provider ? ` ${s2.meta.provider}` : "";
1754
- return { value: s2.name, label: s2.name, hint: `${statusLabel(st)}${prov}` };
1755
- }),
1756
- initialValues: skills.map((s2) => s2.name),
1757
- required: false
1758
- });
1759
- if (p4.isCancel(r)) {
1760
- p4.cancel("Aborted.");
1761
- return;
1762
- }
1763
- chosenSkills = skills.filter((s2) => r.includes(s2.name));
1764
- }
1765
- const skillsWithStatus = (opts.memoryOnly ? [] : chosenSkills).map((s2) => ({
1766
- ...s2,
1767
- status: skillStatus(join6(target.skills, s2.name), s2.meta),
1768
- overwrite: true
1769
- }));
1770
- const prune = opts.prune && !opts.memoryOnly ? computePrune(target.skills, skills.map((s2) => s2.name)) : [];
1771
- plan.push({ agent, target, skills: skillsWithStatus, memory: opts.skillsOnly ? [] : memory, prune });
1772
- }
1773
- const tampered = plan.flatMap((x) => x.skills.filter((s2) => s2.status.kind === "tampered"));
1774
- if (tampered.length) {
1775
- if (opts.keepModified) {
1776
- for (const s2 of tampered) s2.overwrite = false;
1777
- reporter.warn(`${tampered.length} locally-modified skill(s) will be kept (--keep-modified).`);
1778
- } else if (interactive && !opts.dryRun) {
1779
- const sel = await p4.multiselect({
1780
- message: `${tampered.length} skill(s) were modified locally since install. Select which to OVERWRITE`,
1781
- options: tampered.map((s2, i) => ({ value: i, label: s2.name, hint: s2.meta.version ? `v${s2.meta.version}` : "modified" })),
1782
- initialValues: tampered.map((_, i) => i),
1783
- required: false
1784
- });
1785
- if (p4.isCancel(sel)) {
1786
- p4.cancel("Aborted.");
1787
- return;
1788
- }
1789
- tampered.forEach((s2, i) => {
1790
- s2.overwrite = sel.includes(i);
1791
- });
1792
- }
1793
- }
1794
- const willCopy = (s2) => s2.status.kind === "install" || s2.status.kind === "update" || s2.status.kind === "reinstall" || s2.status.kind === "tampered" && s2.overwrite;
1795
- let nInstall = 0, nUpdate = 0, nRemove = 0, nSkip = 0, nKept = 0;
1796
- const lines = [];
1797
- for (const x of plan) {
1798
- lines.push(`${x.agent.label} (${scope})`);
1799
- for (const s2 of x.skills) {
1800
- const prov = s2.meta.provider ? ` [${s2.meta.provider}]` : "";
1801
- let label;
1802
- if (s2.status.kind === "identical") {
1803
- nSkip++;
1804
- label = statusLabel(s2.status);
1805
- } else if (s2.status.kind === "tampered" && !s2.overwrite) {
1806
- nKept++;
1807
- label = `keep modified v${s2.meta.version || "?"}`;
1808
- } else if (s2.status.kind === "tampered") {
1809
- nUpdate++;
1810
- label = `overwrite modified v${s2.meta.version || "?"}`;
1811
- } else if (s2.status.kind === "install") {
1812
- nInstall++;
1813
- label = statusLabel(s2.status);
1814
- } else {
1815
- nUpdate++;
1816
- label = statusLabel(s2.status);
1817
- }
1818
- lines.push(` ${label.padEnd(26)} skill ${s2.name}${prov}`);
1819
- }
1820
- for (const m of x.memory) {
1821
- const ms = memoryStatus(m.src, join6(x.target.memory, m.name));
1822
- if (ms === "identical") {
1823
- nSkip++;
1824
- lines.push(` ${"up-to-date (skip)".padEnd(26)} memory ${m.name}`);
1825
- } else if (ms === "install") {
1826
- nInstall++;
1827
- lines.push(` ${"install".padEnd(26)} memory ${m.name}`);
1828
- } else {
1829
- nUpdate++;
1830
- lines.push(` ${"overwrite".padEnd(26)} memory ${m.name}`);
1831
- }
1832
- }
1833
- for (const s2 of x.prune) {
1834
- nRemove++;
1835
- const ver = s2.meta.version ? ` v${s2.meta.version}` : "";
1836
- lines.push(` ${"remove (orphaned)".padEnd(26)} skill ${s2.name} [${s2.meta.provider}${ver}]`);
1837
- }
1838
- }
1839
- if (nInstall + nUpdate + nRemove === 0) {
1840
- reporter.note(lines.join("\n"), "Nothing to do");
1841
- applyClaudeConfig();
1842
- applyBypassConfig();
1843
- await maybeOfferGitHooks(interactive, opts);
1844
- reporter.success(`Everything up-to-date - ${nSkip} item(s) unchanged${nKept ? `, ${nKept} kept modified` : ""} (${scope}).`);
1845
- return;
1846
- }
1847
- reporter.note(lines.join("\n"), opts.dryRun ? "Dry run - planned changes" : "Planned changes");
1848
- if (interactive && !opts.dryRun) {
1849
- const summary = [
1850
- nInstall && `${nInstall} to install`,
1851
- nUpdate && `${nUpdate} to update/overwrite`,
1852
- nRemove && `${nRemove} to remove`,
1853
- nSkip && `${nSkip} unchanged`
1854
- ].filter(Boolean).join(", ");
1855
- const ok = await p4.confirm({ message: `Apply: ${summary}?` });
1856
- if (p4.isCancel(ok) || !ok) {
1857
- p4.cancel("Aborted.");
1858
- return;
1859
- }
1860
- }
1861
- if (opts.dryRun) {
1862
- applyBypassConfig();
1863
- reporter.info("Dry run complete - no files written.");
1864
- return;
1865
- }
1866
- const changedAgents = plan.filter(
1867
- (x) => x.skills.some(willCopy) || x.memory.some((m) => memoryStatus(m.src, join6(x.target.memory, m.name)) !== "identical") || x.prune.length > 0
1868
- );
1869
- const s = reporter.spinner();
1870
- s.start("Installing...");
1871
- let copied = 0;
1872
- try {
1873
- for (const x of plan) {
1874
- mkdirSync4(x.target.skills, { recursive: true });
1875
- mkdirSync4(x.target.memory, { recursive: true });
1876
- for (const sk of x.skills) {
1877
- if (!willCopy(sk)) continue;
1878
- cpSync2(sk.src, join6(x.target.skills, sk.name), { recursive: true, force: true });
1879
- copied++;
1880
- }
1881
- for (const m of x.memory) {
1882
- if (memoryStatus(m.src, join6(x.target.memory, m.name)) === "identical") continue;
1883
- cpSync2(m.src, join6(x.target.memory, m.name), { force: true });
1884
- copied++;
1885
- }
1886
- for (const pr of x.prune) rmSync(pr.dir, { recursive: true, force: true });
1887
- }
1888
- } catch (err) {
1889
- s.stop("Failed.");
1890
- reporter.fatal(`Error while installing: ${err.message}`);
1891
- }
1892
- s.stop(`Wrote ${copied} item(s)${nRemove ? `, removed ${nRemove}` : ""}.`);
1893
- applyClaudeConfig();
1894
- applyBypassConfig();
1895
- await maybeOfferGitHooks(interactive, opts);
1896
- reporter.success(`${nInstall} installed, ${nUpdate} updated/overwritten` + (nRemove ? `, ${nRemove} removed` : "") + (nSkip ? `, ${nSkip} unchanged` : "") + (nKept ? `, ${nKept} kept modified` : "") + ` (${scope}).`);
1897
- if (changedAgents.length) {
1898
- const { known, running } = runningStatus(changedAgents.map((x) => x.agent));
1899
- if (running.size) {
1900
- const names = changedAgents.filter((x) => running.has(x.agent.name)).map((x) => x.agent.label);
1901
- reporter.warn(`Restart ${names.join(", ")} to apply the changes (running now).`);
1902
- } else if (!known) {
1903
- const names = changedAgents.map((x) => x.agent.label);
1904
- reporter.info(`If any of these agents are running, restart them to apply the changes: ${names.join(", ")}.`);
1905
- }
1906
- }
1907
- }
1908
-
1909
- // src/cli.ts
1910
- init_agents();
1911
-
1912
- // src/guard.ts
1913
- import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1914
- import { execFileSync as execFileSync3 } from "child_process";
1915
- import { basename, extname, dirname as dirname3, join as join7 } from "path";
1916
- import { fileURLToPath as fileURLToPath3 } from "url";
1917
- var LARGE_FILE_BYTES = 5 * 1024 * 1024;
1918
- var ENV_ALLOWED = /(example|sample|template)/i;
1919
- var isForbiddenEnv = (base) => /^\.env(\..+)?$/.test(base) && !ENV_ALLOWED.test(base);
1920
- var BLOCK_DIRS = [
1921
- /(^|\/)node_modules\//,
1922
- /(^|\/)bower_components\//,
1923
- /(^|\/)\.pnp(\/|$)/,
1924
- /(^|\/)__pycache__\//,
1925
- /(^|\/)\.venv\//,
1926
- /(^|\/)venv\//,
1927
- /(^|\/)\.mypy_cache\//,
1928
- /(^|\/)\.pytest_cache\//,
1929
- /(^|\/)\.gradle\//
1930
- ];
1931
- var WARN_DIRS = [
1932
- /(^|\/)dist\//,
1933
- /(^|\/)build\//,
1934
- /(^|\/)out\//,
1935
- /(^|\/)\.next\//,
1936
- /(^|\/)\.nuxt\//,
1937
- /(^|\/)\.svelte-kit\//,
1938
- /(^|\/)\.turbo\//,
1939
- /(^|\/)coverage\//
1940
- ];
1941
- var WARN_FILES = [/\.log$/i, /(^|\/)\.DS_Store$/, /(^|\/)Thumbs\.db$/i];
1942
- var SECRET_SKIP_EXT = /* @__PURE__ */ new Set([
1943
- ".png",
1944
- ".jpg",
1945
- ".jpeg",
1946
- ".gif",
1947
- ".webp",
1948
- ".ico",
1949
- ".pdf",
1950
- ".zip",
1951
- ".gz",
1952
- ".woff",
1953
- ".woff2",
1954
- ".ttf",
1955
- ".eot",
1956
- ".mp4",
1957
- ".mov",
1958
- ".lock"
1959
- ]);
1960
- var SECRET_PATTERNS = [
1961
- ["AWS access key id", /\bAKIA[0-9A-Z]{16}\b/],
1962
- ["GitHub token", /\bgh[pousr]_[A-Za-z0-9]{36,}\b/],
1963
- ["OpenAI API key", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/],
1964
- ["Anthropic API key", /\bsk-ant-[A-Za-z0-9_-]{20,}\b/],
1965
- ["Slack token", /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/],
1966
- ["Google API key", /\bAIza[0-9A-Za-z_-]{35}\b/],
1967
- ["Stripe secret key", /\bsk_live_[0-9A-Za-z]{24,}\b/],
1968
- ["Private key block", /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/],
1969
- ["Generic bearer secret", /\b(?:secret|token|api[_-]?key|passwd|password)\s*[:=]\s*["'][A-Za-z0-9_\-./+]{16,}["']/i]
1970
- ];
1971
- var SECRET_SKIP_PATH = [/(^|\/)node_modules\//, /(^|\/)\.git\//, /package-lock\.json$/, /guard\.[mc]?js$/];
1972
- var SELF_DIR = dirname3(fileURLToPath3(import.meta.url));
1973
- function loadConfig() {
1974
- const defaults = { secrets: true, envFiles: true, depDirs: true, generatedDirs: true, junkFiles: true, largeFiles: true };
1975
- try {
1976
- const raw = JSON.parse(readFileSync4(join7(SELF_DIR, "enigma-guard.json"), "utf8"));
1977
- return { ...defaults, ...raw };
1978
- } catch {
1979
- return defaults;
1980
- }
1981
- }
1982
- function gitFiles(all) {
1983
- const out = execFileSync3("git", all ? ["ls-files"] : ["diff", "--cached", "--name-only", "--diff-filter=ACM"], { encoding: "utf8" });
1984
- return out.split("\n").map((s) => s.trim()).filter(Boolean);
1985
- }
1986
- function scanSecrets(file, blocks) {
1987
- if (SECRET_SKIP_PATH.some((re) => re.test(file))) return;
1988
- if (SECRET_SKIP_EXT.has(extname(file).toLowerCase())) return;
1989
- let text;
1990
- try {
1991
- text = readFileSync4(file, "utf8");
1992
- } catch {
1993
- return;
1994
- }
1995
- if (text.includes("\0")) return;
1996
- text.split("\n").forEach((line, i) => {
1997
- for (const [label, re] of SECRET_PATTERNS) {
1998
- if (re.test(line)) blocks.push(`${file}:${i + 1} [secret: ${label}]`);
1999
- }
2000
- });
2001
- }
2002
- function runGuard({ all = false } = {}) {
2003
- const cfg = loadConfig();
2004
- let files;
2005
- try {
2006
- files = gitFiles(all);
2007
- } catch {
2008
- return { ok: true, blocks: [], warns: [], notRepo: true };
2009
- }
2010
- const blocks = [];
2011
- const warns = [];
2012
- for (const file of files) {
2013
- const base = basename(file);
2014
- if (cfg.envFiles && isForbiddenEnv(base)) {
2015
- blocks.push(`${file} [env file with secrets - commit .env.example/.template instead]`);
2016
- }
2017
- if (cfg.depDirs && BLOCK_DIRS.some((re) => re.test(file))) {
2018
- blocks.push(`${file} [dependency/cache dir - must not be committed]`);
2019
- } else if (cfg.generatedDirs && WARN_DIRS.some((re) => re.test(file))) {
2020
- warns.push(`${file} [looks generated - confirm you really want it tracked]`);
2021
- }
2022
- if (cfg.junkFiles && WARN_FILES.some((re) => re.test(file))) warns.push(`${file} [log / OS junk file]`);
2023
- if (cfg.largeFiles) {
2024
- try {
2025
- if (statSync2(file).size > LARGE_FILE_BYTES) warns.push(`${file} [larger than 5 MB]`);
2026
- } catch {
2027
- }
2028
- }
2029
- if (cfg.secrets) scanSecrets(file, blocks);
2030
- }
2031
- return { ok: blocks.length === 0, blocks, warns, count: files.length, all };
2032
- }
2033
- function runGuardCli(all) {
2034
- const r = runGuard({ all });
2035
- if (r.notRepo) {
2036
- console.error("enigma-guard: not a git repository; nothing to check.");
2037
- return 0;
2038
- }
2039
- if (r.warns.length) {
2040
- console.error(`enigma-guard: ${r.warns.length} warning(s):`);
2041
- for (const w of r.warns) console.error(` ! ${w}`);
2042
- }
2043
- if (r.blocks.length) {
2044
- console.error(`
2045
- enigma-guard: BLOCKED - ${r.blocks.length} problem(s) must be fixed before committing:`);
2046
- for (const b of r.blocks) console.error(` x ${b}`);
2047
- console.error("\nFix the above (move secrets to env/secret manager, gitignore generated paths).");
2048
- console.error("To bypass intentionally for one commit: git commit --no-verify");
2049
- return 1;
2050
- }
2051
- console.log(`enigma-guard: ${r.count} ${r.all ? "tracked" : "staged"} file(s) checked, no blocking problems.`);
2052
- return 0;
2053
- }
2054
- var guardEntry = process.argv[1] ?? "";
2055
- var isGuardEntry = /(^|[\\/])guard\.[mc]?[jt]s$/.test(guardEntry);
2056
- if (isGuardEntry && fileURLToPath3(import.meta.url) === guardEntry) {
2057
- process.exit(runGuardCli(process.argv.includes("--all")));
2058
- }
2059
-
2060
- // src/settings.ts
2061
- init_config();
2062
- init_settings_registry();
2063
- function printEffective() {
2064
- console.log("Effective enigma settings:\n");
2065
- for (const category of CATEGORIES) {
2066
- console.log(`${category.title}:`);
2067
- for (const s of category.settings) console.log(` ${s.key}: ${valueLabel(s.read("global"))}`);
2068
- console.log("");
2069
- }
2070
- const { sources } = readConfig();
2071
- console.log(sources.length ? `.enigma.json sources: ${sources.join(", ")}` : ".enigma.json: built-in defaults (no file found)");
2072
- console.log("Agent settings (attribution, bypass) reflect each agent's own config at the global scope.");
2073
- }
2074
- async function runConfigCli(positionals, scope, interactive) {
2075
- const [rawKey, rawValue] = positionals;
2076
- if (!rawKey) {
2077
- if (interactive) {
2078
- const { runSettingsTui: runSettingsTui2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
2079
- await runSettingsTui2();
2080
- } else {
2081
- printEffective();
2082
- }
2083
- return 0;
2084
- }
2085
- const setting = ALL_SETTINGS.find((s) => s.key === rawKey);
2086
- if (!setting) {
2087
- console.error(`Unknown config key: ${rawKey}. Known keys: ${ALL_SETTINGS.map((s) => s.key).join(", ")}.`);
2088
- return 1;
2089
- }
2090
- if (rawValue === void 0) {
2091
- console.error(`Missing value for '${rawKey}'. Usage: enigma config ${rawKey} <on|off> [-g|-l]`);
2092
- return 1;
2093
- }
2094
- const value = parseBool(rawValue);
2095
- if (value === null) {
2096
- console.error(`Invalid value '${rawValue}' for '${rawKey}'. Use on or off.`);
2097
- return 1;
2098
- }
2099
- const target = setting.globalOnly ? "global" : scope || "global";
2100
- const result = setting.write(value, target);
2101
- const where = result.path ? ` in ${result.path}` : "";
2102
- console.log(`Set ${rawKey} = ${valueLabel(value)} (${target})${where}.`);
2103
- return 0;
2104
- }
2105
-
2106
- // src/update.ts
2107
- init_util();
2108
- init_config();
2109
- import { homedir as homedir5 } from "os";
2110
- import { join as join9 } from "path";
2111
- import { writeFileSync as writeFileSync6 } from "fs";
2112
- import { spawn, spawnSync } from "child_process";
2113
- import * as p5 from "@clack/prompts";
2114
- var REGISTRY_URL = "https://registry.npmjs.org/enigma-cli/latest";
2115
- var UPDATE_COMMAND = "npm i -g enigma-cli@latest";
2116
- var CACHE_FILE = join9(homedir5(), ".enigma-update-check.json");
2117
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2118
- var CHILD_SCRIPT = [
2119
- "const fs = require('fs');",
2120
- "const ctrl = new AbortController();",
2121
- "const t = setTimeout(() => ctrl.abort(), 5000);",
2122
- "fetch(process.env.E_URL, { signal: ctrl.signal, headers: { 'user-agent': 'enigma-cli-update-check' } })",
2123
- " .then((r) => (r.ok ? r.json() : null))",
2124
- " .then((d) => { if (d && d.version) fs.writeFileSync(process.env.E_FILE, JSON.stringify({ latest: d.version, checkedAt: Date.now() })); })",
2125
- " .catch(() => {})",
2126
- " .finally(() => clearTimeout(t));"
2127
- ].join("\n");
2128
- function parseVersion(version) {
2129
- const core = String(version).trim().replace(/^v/, "").split("-")[0];
2130
- const [major, minor, patch] = core.split(".").map((n) => parseInt(n, 10) || 0);
2131
- return [major || 0, minor || 0, patch || 0];
2132
- }
2133
- function isNewer(latest, current) {
2134
- const a = parseVersion(latest);
2135
- const b = parseVersion(current);
2136
- for (let i = 0; i < 3; i++) {
2137
- if (a[i] > b[i]) return true;
2138
- if (a[i] < b[i]) return false;
2139
- }
2140
- return false;
2141
- }
2142
- function paint(text, code) {
2143
- return process.stdout.isTTY && !process.env.NO_COLOR ? `\x1B[${code}m${text}\x1B[0m` : text;
2144
- }
2145
- function renderUpdateBox(current, latest) {
2146
- const lines = [
2147
- { text: `Update available ${current} -> ${latest}`, color: "1;33" },
2148
- { text: `Run ${UPDATE_COMMAND} to update`, color: "36" }
2149
- ];
2150
- const width = Math.max(...lines.map((l) => l.text.length));
2151
- const border = `+${"-".repeat(width + 4)}+`;
2152
- const blank = `|${" ".repeat(width + 4)}|`;
2153
- const rows = lines.map((l) => `|${paint(` ${l.text}${" ".repeat(width - l.text.length)} `, l.color)}|`);
2154
- return [border, blank, ...rows, blank, border].join("\n");
2155
- }
2156
- function readCache() {
2157
- return readJson(CACHE_FILE);
2158
- }
2159
- function scheduleUpdateCheck() {
2160
- try {
2161
- const cache = readCache();
2162
- if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) return;
2163
- writeFileSync6(CACHE_FILE, JSON.stringify({ latest: cache?.latest ?? null, checkedAt: Date.now() }));
2164
- const child = spawn(process.execPath, ["-e", CHILD_SCRIPT], {
2165
- detached: true,
2166
- stdio: "ignore",
2167
- windowsHide: true,
2168
- env: { ...process.env, E_URL: REGISTRY_URL, E_FILE: CACHE_FILE }
2169
- });
2170
- child.unref();
2171
- } catch {
2172
- }
2173
- }
2174
- function runUpdate() {
2175
- try {
2176
- const result = spawnSync("npm", ["i", "-g", "enigma-cli@latest"], {
2177
- stdio: "inherit",
2178
- shell: process.platform === "win32"
2179
- });
2180
- if (result.status === 0) console.log("Updated. Re-run your command to use the new version.");
2181
- else console.log(`Update did not complete. Run '${UPDATE_COMMAND}' manually.`);
2182
- } catch {
2183
- console.log(`Could not run the update. Run '${UPDATE_COMMAND}' manually.`);
2184
- }
2185
- }
2186
- async function notifyUpdate(current, interactive) {
2187
- if (!process.stdout.isTTY || process.env.CI) return;
2188
- try {
2189
- if (!readConfig().config.updateNotifier) return;
2190
- scheduleUpdateCheck();
2191
- const cache = readCache();
2192
- const latest = cache?.latest ? String(cache.latest).replace(/[^\w.+-]/g, "") : "";
2193
- if (!latest || !isNewer(latest, current)) return;
2194
- process.stdout.write(`
2195
- ${renderUpdateBox(current, latest)}
2196
- `);
2197
- if (!interactive) return;
2198
- const ok = await p5.confirm({ message: `Update now with ${UPDATE_COMMAND}?`, initialValue: true });
2199
- if (p5.isCancel(ok) || !ok) return;
2200
- runUpdate();
2201
- } catch {
2202
- }
2203
- }
2204
-
2205
- // src/cli.ts
2206
- var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
2207
- var PKG = readJson(join10(__dirname3, "..", "package.json")) || {};
2208
- var COMMANDS = /* @__PURE__ */ new Set(["install", "security", "guard", "seal", "check", "config", "help", "version"]);
2209
- function parseArgs(argv) {
2210
- const opts = {
2211
- command: null,
2212
- positionals: [],
2213
- scope: null,
2214
- agents: [],
2215
- allAgents: false,
2216
- skills: [],
2217
- skillsOnly: false,
2218
- memoryOnly: false,
2219
- prune: true,
2220
- keepModified: false,
2221
- bypass: null,
2222
- noBypass: false,
2223
- force: false,
2224
- all: false,
2225
- yes: false,
2226
- dryRun: false,
2227
- help: false,
2228
- version: false
2229
- };
2230
- for (let i = 0; i < argv.length; i++) {
2231
- const a = argv[i];
2232
- const next = () => argv[++i];
2233
- if (i === 0 && COMMANDS.has(a)) {
2234
- opts.command = a;
2235
- continue;
2236
- }
2237
- switch (a) {
2238
- case "-g":
2239
- case "--global":
2240
- opts.scope = "global";
2241
- break;
2242
- case "-l":
2243
- case "--local":
2244
- opts.scope = "local";
2245
- break;
2246
- case "-a":
2247
- case "--agent":
2248
- opts.agents.push(...next().split(","));
2249
- break;
2250
- case "-s":
2251
- case "--skill":
2252
- opts.skills.push(...next().split(","));
2253
- break;
2254
- case "--all":
2255
- opts.allAgents = true;
2256
- opts.all = true;
2257
- break;
2258
- case "--skills-only":
2259
- opts.skillsOnly = true;
2260
- break;
2261
- case "--memory-only":
2262
- opts.memoryOnly = true;
2263
- break;
2264
- case "--no-prune":
2265
- opts.prune = false;
2266
- break;
2267
- case "--keep-modified":
2268
- opts.keepModified = true;
2269
- break;
2270
- case "--bypass":
2271
- opts.bypass = (opts.bypass || []).concat(next().split(","));
2272
- break;
2273
- case "--no-bypass":
2274
- opts.noBypass = true;
2275
- break;
2276
- case "--force":
2277
- opts.force = true;
2278
- break;
2279
- case "-y":
2280
- case "--yes":
2281
- opts.yes = true;
2282
- break;
2283
- case "--dry-run":
2284
- opts.dryRun = true;
2285
- break;
2286
- case "-h":
2287
- case "--help":
2288
- opts.help = true;
2289
- break;
2290
- case "-v":
2291
- case "--version":
2292
- opts.version = true;
2293
- break;
2294
- default:
2295
- if (a.startsWith("-")) {
2296
- console.error(`Unknown option: ${a}`);
2297
- process.exit(1);
2298
- } else if (opts.command) {
2299
- opts.positionals.push(a);
2300
- } else {
2301
- console.error(`Unknown command: ${a}`);
2302
- process.exit(1);
2303
- }
2304
- }
2305
- }
2306
- return opts;
2307
- }
2308
- function printHelp() {
2309
- console.log(`
2310
- enigma - everything you need to work with a coding agent
2311
-
2312
- Usage:
2313
- enigma [command] [options]
2314
-
2315
- Commands:
2316
- (none) Interactive hub: configure settings or set up features
2317
- install Install/update agent skills (Claude Code, Codex, OpenCode)
2318
- security Set up git security hooks in the current repo
2319
- guard [--all] Run the commit guard (staged files, or --all for every tracked file)
2320
- config [key val] Configure settings: no args opens the interactive menu;
2321
- 'config <key> <on|off> [-g|-l]' sets one (e.g. config claude-attribution on)
2322
- seal Maintenance: (re)compute skill content hashes
2323
- check Integrity gate: verify skills are well-formed and sealed
2324
- help, version
2325
-
2326
- Config keys: commit-emoji, update-notifier, fullscreen, claude-attribution,
2327
- bypass-claude, bypass-codex, bypass-opencode
2328
-
2329
- Install options:
2330
- -g, --global Install at user level
2331
- -l, --local Install into the current project
2332
- -a, --agent <name> Target agent(s) (default: auto-detect installed)
2333
- -s, --skill <name> Skill(s) to install (default: all)
2334
- --all Target every supported agent, ignoring detection
2335
- --skills-only Only skill folders --memory-only Only memory files
2336
- --no-prune Keep orphaned skills --keep-modified Don't overwrite local edits
2337
- --bypass <names> Disable approval prompts for agents (claude,codex,opencode | all | none)
2338
- --no-bypass Never configure permission bypass (skip the prompt)
2339
- --dry-run Show the plan without writing
2340
-
2341
- Security options:
2342
- --force Override an existing core.hooksPath
2343
-
2344
- Global:
2345
- -y, --yes Non-interactive -h, --help -v, --version
2346
-
2347
- Examples:
2348
- enigma # interactive
2349
- enigma install --global # skills for detected agents, user level
2350
- enigma install --all -y # every supported agent, non-interactive
2351
- enigma install -y --bypass claude,codex # also disable approval prompts (unattended)
2352
- enigma security # configure git hooks (choose protections)
2353
- enigma config # show effective runtime config
2354
- enigma config commit-emoji off # opt out of commit-message emojis (global)
2355
- `);
2356
- }
2357
- async function run(argv) {
2358
- const opts = parseArgs(argv);
2359
- const interactive = Boolean(process.stdout.isTTY) && !opts.yes;
2360
- const version = PKG.version || "0.0.0";
2361
- if (opts.help || opts.command === "help") {
2362
- printHelp();
2363
- await notifyUpdate(version, interactive);
2364
- return;
2365
- }
2366
- if (opts.version || opts.command === "version") {
2367
- console.log(version);
2368
- await notifyUpdate(version, interactive);
2369
- return;
2370
- }
2371
- if (opts.command === "seal") return sealSources();
2372
- if (opts.command === "check") return checkSources();
2373
- if (opts.command === "guard") {
2374
- process.exit(runGuardCli(opts.all));
2375
- }
2376
- if (opts.command === "config") {
2377
- process.exit(await runConfigCli(opts.positionals, opts.scope, interactive));
2378
- }
2379
- if (opts.command === "install") {
2380
- p6.intro("enigma - install agent skills");
2381
- await installSkills(opts, interactive);
2382
- p6.outro("Done.");
2383
- await notifyUpdate(version, interactive);
2384
- return;
2385
- }
2386
- if (opts.command === "security") {
2387
- p6.intro("enigma - git security hooks");
2388
- const done = await setupGitHooks(opts, interactive);
2389
- p6.outro(done ? "Git hooks configured." : "No changes made.");
2390
- await notifyUpdate(version, interactive);
2391
- return;
2392
- }
2393
- if (!interactive) {
2394
- await installSkills(opts, interactive);
2395
- await notifyUpdate(version, interactive);
2396
- return;
2397
- }
2398
- const { runHomeTui: runHomeTui3 } = isBun() ? await Promise.resolve().then(() => (init_opentui(), opentui_exports)) : await Promise.resolve().then(() => (init_settings(), settings_exports));
2399
- await runHomeTui3({
2400
- agents: discoverAgents().map((a) => ({ name: a.name, label: a.label, installed: a.installed })),
2401
- protections: GUARD_PROTECTIONS,
2402
- runAction: async (req) => {
2403
- const reporter = collectReporter();
2404
- const title = req.action === "skills" ? "Install agent skills" : "Git security hooks";
2405
- try {
2406
- if (req.action === "skills") {
2407
- await installSkills({ ...opts, scope: req.scope ?? opts.scope, agents: req.agents ?? [], allAgents: !(req.agents && req.agents.length) }, false, reporter);
2408
- return { ok: true, title, lines: reporter.lines };
2409
- }
2410
- const done = await setupGitHooks({ ...opts, protections: req.protections, force: true }, false, reporter);
2411
- return { ok: done, title, lines: reporter.lines };
2412
- } catch (err) {
2413
- reporter.error(`Error: ${err.message}`);
2414
- return { ok: false, title, lines: reporter.lines };
2415
- }
2416
- }
2417
- });
2418
- await notifyUpdate(version, interactive);
2419
- }
2420
-
2421
- // src/bin/enigma.ts
2422
- run(process.argv.slice(2)).catch((err) => {
2423
- console.error(err);
2424
- process.exit(1);
2425
- });