enigma-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +87 -0
  3. package/assets/memory/AGENTS.md +270 -0
  4. package/assets/memory/CLAUDE.md +270 -0
  5. package/assets/skills/backend-policy/SKILL.md +84 -0
  6. package/assets/skills/backend-policy/skill.json +8 -0
  7. package/assets/skills/ciphera-style-policy/SKILL.md +136 -0
  8. package/assets/skills/ciphera-style-policy/skill.json +8 -0
  9. package/assets/skills/code-review-policy/SKILL.md +68 -0
  10. package/assets/skills/code-review-policy/skill.json +8 -0
  11. package/assets/skills/core-engineering-policy/SKILL.md +277 -0
  12. package/assets/skills/core-engineering-policy/skill.json +8 -0
  13. package/assets/skills/database-expert/SKILL.md +224 -0
  14. package/assets/skills/database-expert/skill.json +8 -0
  15. package/assets/skills/debugging-policy/SKILL.md +59 -0
  16. package/assets/skills/debugging-policy/skill.json +8 -0
  17. package/assets/skills/dependency-policy/SKILL.md +61 -0
  18. package/assets/skills/dependency-policy/skill.json +8 -0
  19. package/assets/skills/frontend-policy/SKILL.md +117 -0
  20. package/assets/skills/frontend-policy/skill.json +8 -0
  21. package/assets/skills/git-policy/SKILL.md +192 -0
  22. package/assets/skills/git-policy/skill.json +8 -0
  23. package/assets/skills/security-policy/SKILL.md +86 -0
  24. package/assets/skills/security-policy/skill.json +8 -0
  25. package/assets/skills/testing-policy/SKILL.md +76 -0
  26. package/assets/skills/testing-policy/skill.json +8 -0
  27. package/assets/skills/validation-policy/SKILL.md +77 -0
  28. package/assets/skills/validation-policy/skill.json +8 -0
  29. package/dist/enigma.js +1068 -0
  30. package/dist/guard.js +153 -0
  31. package/package.json +65 -0
package/dist/enigma.js ADDED
@@ -0,0 +1,1068 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { dirname as dirname4, join as join8 } from "path";
5
+ import { fileURLToPath as fileURLToPath4 } from "url";
6
+ import * as p3 from "@clack/prompts";
7
+
8
+ // src/util.ts
9
+ import { existsSync, statSync, readFileSync } from "fs";
10
+ import { join, sep } from "path";
11
+ function isDir(pth) {
12
+ try {
13
+ return statSync(pth).isDirectory();
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+ function readJson(file) {
19
+ try {
20
+ return JSON.parse(readFileSync(file, "utf8"));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ function isOnPath(bin) {
26
+ const dirs = (process.env.PATH || process.env.Path || "").split(sep === "\\" ? ";" : ":").filter(Boolean);
27
+ const exts = sep === "\\" ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()) : [""];
28
+ return dirs.some((d) => exts.some((ext) => existsSync(join(d, bin + ext))));
29
+ }
30
+
31
+ // src/skills.ts
32
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3, cpSync as cpSync2, mkdirSync as mkdirSync3, rmSync } from "fs";
33
+ import { dirname as dirname2, join as join5, resolve as resolve2, relative as relative2, sep as sep2 } from "path";
34
+ import { fileURLToPath as fileURLToPath2 } from "url";
35
+ import { createHash } from "crypto";
36
+ import * as p2 from "@clack/prompts";
37
+
38
+ // src/agents.ts
39
+ import { homedir } from "os";
40
+ import { join as join2 } from "path";
41
+ import { existsSync as existsSync2 } from "fs";
42
+ import { execFileSync } from "child_process";
43
+ var HOME = homedir();
44
+ var MANAGED_PROVIDER = "FJRG2007/enigma";
45
+ var LEGACY_PROVIDERS = ["FJRG2007"];
46
+ function isManagedProvider(provider) {
47
+ return provider === MANAGED_PROVIDER || typeof provider === "string" && LEGACY_PROVIDERS.includes(provider);
48
+ }
49
+ var AGENTS = {
50
+ claude: {
51
+ label: "Claude Code",
52
+ memoryFile: "CLAUDE.md",
53
+ detect: { bins: ["claude"], dirs: [join2(HOME, ".claude")] },
54
+ targets: {
55
+ global: { skills: join2(HOME, ".claude", "skills"), memory: join2(HOME, ".claude") },
56
+ local: { skills: join2(process.cwd(), ".claude", "skills"), memory: process.cwd() }
57
+ }
58
+ },
59
+ codex: {
60
+ label: "OpenAI Codex",
61
+ memoryFile: "AGENTS.md",
62
+ // Codex reads AGENTS.md from its home (~/.codex) and project root, but
63
+ // discovers skills from the shared `.agents/skills` location, not ~/.codex/skills.
64
+ detect: { bins: ["codex"], dirs: [join2(HOME, ".codex")] },
65
+ targets: {
66
+ global: { skills: join2(HOME, ".agents", "skills"), memory: join2(HOME, ".codex") },
67
+ local: { skills: join2(process.cwd(), ".agents", "skills"), memory: process.cwd() }
68
+ }
69
+ },
70
+ opencode: {
71
+ label: "opencode",
72
+ memoryFile: "AGENTS.md",
73
+ // opencode reads AGENTS.md from ~/.config/opencode (global) or the project
74
+ // root (local); skills from ~/.config/opencode/skills and .opencode/skills.
75
+ detect: { bins: ["opencode"], dirs: [join2(HOME, ".config", "opencode"), join2(HOME, ".opencode")] },
76
+ targets: {
77
+ global: { skills: join2(HOME, ".config", "opencode", "skills"), memory: join2(HOME, ".config", "opencode") },
78
+ local: { skills: join2(process.cwd(), ".opencode", "skills"), memory: process.cwd() }
79
+ }
80
+ }
81
+ };
82
+ function isInstalled(agent) {
83
+ const det = agent.detect || {};
84
+ return (det.dirs || []).some((d) => existsSync2(d)) || (det.bins || []).some((b) => isOnPath(b));
85
+ }
86
+ function discoverAgents() {
87
+ return Object.keys(AGENTS).map((name) => {
88
+ const agent = { name, ...AGENTS[name] };
89
+ return { ...agent, installed: isInstalled(agent) };
90
+ });
91
+ }
92
+ function processSnapshot() {
93
+ try {
94
+ if (process.platform === "win32") {
95
+ return execFileSync("tasklist", ["/fo", "csv", "/nh"], { encoding: "utf8" }).toLowerCase();
96
+ }
97
+ return execFileSync("ps", ["-A", "-o", "comm="], { encoding: "utf8" }).toLowerCase();
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function runningStatus(agents) {
103
+ const snap = processSnapshot();
104
+ if (snap == null) return { known: false, running: /* @__PURE__ */ new Set() };
105
+ const running = /* @__PURE__ */ new Set();
106
+ for (const a of agents) {
107
+ const det = a.detect || {};
108
+ const names = det.procs || det.bins || [a.name];
109
+ if (names.some((n) => snap.includes(String(n).toLowerCase()))) running.add(a.name);
110
+ }
111
+ return { known: true, running };
112
+ }
113
+
114
+ // src/security.ts
115
+ import { existsSync as existsSync3, mkdirSync, cpSync, writeFileSync, chmodSync } from "fs";
116
+ import { dirname, join as join3, resolve, relative } from "path";
117
+ import { fileURLToPath } from "url";
118
+ import { execFileSync as execFileSync2 } from "child_process";
119
+ import * as p from "@clack/prompts";
120
+ var __dirname = dirname(fileURLToPath(import.meta.url));
121
+ function findGuardSrc() {
122
+ const candidates = [
123
+ join3(__dirname, "guard.js"),
124
+ join3(__dirname, "..", "dist", "guard.js")
125
+ ];
126
+ return candidates.find((c) => existsSync3(c)) ?? null;
127
+ }
128
+ var GUARD_PROTECTIONS = [
129
+ { value: "secrets", label: "Block committed secrets", hint: "API keys, tokens, private keys" },
130
+ { value: "envFiles", label: "Block .env files", hint: "allows .env.example / .sample / .template" },
131
+ { value: "depDirs", label: "Block dependency/cache dirs", hint: "node_modules, __pycache__, venv" },
132
+ { value: "generatedDirs", label: "Warn on generated dirs", hint: "dist, build, .next, coverage" },
133
+ { value: "junkFiles", label: "Warn on log / OS junk files", hint: ".log, .DS_Store, Thumbs.db" },
134
+ { value: "largeFiles", label: "Warn on files over 5 MB", hint: "oversized blobs" }
135
+ ];
136
+ function findGitRoot(start) {
137
+ let dir = resolve(start);
138
+ for (; ; ) {
139
+ if (existsSync3(join3(dir, ".git"))) return dir;
140
+ const parent = dirname(dir);
141
+ if (parent === dir) return null;
142
+ dir = parent;
143
+ }
144
+ }
145
+ function currentHooksPath(root) {
146
+ try {
147
+ return execFileSync2("git", ["-C", root, "config", "--get", "core.hooksPath"], { encoding: "utf8" }).trim();
148
+ } catch {
149
+ return "";
150
+ }
151
+ }
152
+ async function setupGitHooks(opts, interactive) {
153
+ const root = findGitRoot(process.cwd());
154
+ if (!root) {
155
+ p.log.error("Not inside a git repository (no .git found). Run this from your project root.");
156
+ return false;
157
+ }
158
+ const guardSrc = findGuardSrc();
159
+ if (!guardSrc) {
160
+ p.log.error("Cannot find the built guard (dist/guard.js). Run 'npm run build' first.");
161
+ return false;
162
+ }
163
+ const current = currentHooksPath(root);
164
+ if (current && current !== ".githooks" && !opts.force) {
165
+ p.log.warn(`core.hooksPath is already set to '${current}'.`);
166
+ if (interactive) {
167
+ const ok = await p.confirm({ message: `Override existing core.hooksPath '${current}' with '.githooks'?` });
168
+ if (p.isCancel(ok) || !ok) {
169
+ p.log.info("Left git hooks unchanged.");
170
+ return false;
171
+ }
172
+ } else {
173
+ p.log.info("Re-run with --force to override.");
174
+ return false;
175
+ }
176
+ }
177
+ let enabled = opts.protections;
178
+ if (!enabled && interactive) {
179
+ const r = await p.multiselect({
180
+ message: "Which protections should the commit guard enforce?",
181
+ options: GUARD_PROTECTIONS,
182
+ initialValues: GUARD_PROTECTIONS.map((o) => o.value),
183
+ required: true
184
+ });
185
+ if (p.isCancel(r)) {
186
+ p.log.info("Left git hooks unchanged.");
187
+ return false;
188
+ }
189
+ enabled = r;
190
+ }
191
+ const config = {};
192
+ for (const o of GUARD_PROTECTIONS) config[o.value] = enabled ? enabled.includes(o.value) : true;
193
+ const hooksDir = join3(root, ".githooks");
194
+ mkdirSync(hooksDir, { recursive: true });
195
+ cpSync(guardSrc, join3(hooksDir, "guard.mjs"), { force: true });
196
+ writeFileSync(join3(hooksDir, "enigma-guard.json"), JSON.stringify(config, null, 2) + "\n");
197
+ const shimPath = join3(hooksDir, "pre-commit");
198
+ const shim = [
199
+ "#!/bin/sh",
200
+ "# Managed by enigma (enigma-cli) - blocks committed secrets, .env files, and dependency dirs.",
201
+ "# Toggle protections in .githooks/enigma-guard.json. Bypass once: git commit --no-verify",
202
+ 'exec node "$(git rev-parse --show-toplevel)/.githooks/guard.mjs" "$@"',
203
+ ""
204
+ ].join("\n");
205
+ writeFileSync(shimPath, shim);
206
+ try {
207
+ chmodSync(shimPath, 493);
208
+ } catch {
209
+ }
210
+ try {
211
+ chmodSync(join3(hooksDir, "guard.mjs"), 493);
212
+ } catch {
213
+ }
214
+ try {
215
+ execFileSync2("git", ["-C", root, "config", "core.hooksPath", ".githooks"]);
216
+ } catch (err) {
217
+ p.log.error(`Failed to set core.hooksPath: ${err.message}`);
218
+ return false;
219
+ }
220
+ const on = Object.entries(config).filter(([, v]) => v).map(([k]) => k);
221
+ p.log.success(`Git security hooks installed in ${relative(process.cwd(), hooksDir) || ".githooks"} (core.hooksPath set).`);
222
+ p.log.info(`Enforcing: ${on.join(", ") || "nothing"}. Commit .githooks/ so your team inherits it.`);
223
+ if (isOnPath("gh")) {
224
+ p.log.info("GitHub CLI (gh) detected: these hooks also run for commits made via gh, since gh uses git underneath.");
225
+ }
226
+ return true;
227
+ }
228
+ async function maybeOfferGitHooks(interactive, opts) {
229
+ if (!interactive || opts.security) return;
230
+ const root = findGitRoot(process.cwd());
231
+ if (!root) return;
232
+ if (currentHooksPath(root) === ".githooks") return;
233
+ const ok = await p.confirm({ message: "Set up git security hooks here too (block secrets, .env, node_modules)?" });
234
+ if (!p.isCancel(ok) && ok) await setupGitHooks({ ...opts, protections: void 0 }, interactive);
235
+ }
236
+
237
+ // src/claude.ts
238
+ import { homedir as homedir2 } from "os";
239
+ import { join as join4 } from "path";
240
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
241
+ function claudeSettingsPath(scope) {
242
+ return scope === "global" ? join4(homedir2(), ".claude", "settings.json") : join4(process.cwd(), ".claude", "settings.json");
243
+ }
244
+ function disableClaudeAttribution(scope) {
245
+ const path = claudeSettingsPath(scope);
246
+ const current = readJson(path) || {};
247
+ const attribution = typeof current.attribution === "object" && current.attribution !== null ? current.attribution : {};
248
+ const alreadyOff = attribution.commit === "" && attribution.pr === "" && current.includeCoAuthoredBy === false;
249
+ if (alreadyOff) return false;
250
+ const next = {
251
+ ...current,
252
+ attribution: { ...attribution, commit: "", pr: "" },
253
+ includeCoAuthoredBy: false
254
+ };
255
+ const dir = join4(path, "..");
256
+ if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
257
+ writeFileSync2(path, JSON.stringify(next, null, 2) + "\n");
258
+ return true;
259
+ }
260
+
261
+ // src/skills.ts
262
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
263
+ var PKG_ROOT = resolve2(__dirname2, "..");
264
+ var ASSETS = join5(PKG_ROOT, "assets");
265
+ var SKILLS_ROOT = join5(ASSETS, "skills");
266
+ var MEMORY_ROOT = join5(ASSETS, "memory");
267
+ function cliVersion() {
268
+ return (readJson(join5(PKG_ROOT, "package.json")) || {}).version || "0.0.0";
269
+ }
270
+ function serializeMeta(meta) {
271
+ const ordered = {};
272
+ for (const k of ["name", "version", "provider", "description", "cliVersion", "sha"]) {
273
+ if (meta[k] !== void 0) ordered[k] = meta[k];
274
+ }
275
+ for (const k of Object.keys(meta)) if (!(k in ordered)) ordered[k] = meta[k];
276
+ return JSON.stringify(ordered, null, 2) + "\n";
277
+ }
278
+ function readSkillMeta(skillDir) {
279
+ return readJson(join5(skillDir, "skill.json")) || {};
280
+ }
281
+ function listFilesRel(dir, base = dir) {
282
+ const out = [];
283
+ for (const e of readdirSync(dir)) {
284
+ const full = join5(dir, e);
285
+ if (isDir(full)) out.push(...listFilesRel(full, base));
286
+ else out.push(relative2(base, full).split(sep2).join("/"));
287
+ }
288
+ return out;
289
+ }
290
+ function computeContentSha(dir) {
291
+ const files = listFilesRel(dir).filter((f) => f !== "skill.json").sort();
292
+ const h = createHash("sha256");
293
+ for (const f of files) {
294
+ h.update(f);
295
+ h.update("\0");
296
+ h.update(readFileSync2(join5(dir, f)));
297
+ h.update("\0");
298
+ }
299
+ return h.digest("hex");
300
+ }
301
+ function skillStatus(destDir, srcMeta) {
302
+ if (!existsSync4(destDir)) return { kind: "install", from: null, to: srcMeta.version || null };
303
+ const destMeta = readSkillMeta(destDir);
304
+ const from = destMeta.version || null;
305
+ const to = srcMeta.version || null;
306
+ if (from && to && from !== to) return { kind: "update", from, to };
307
+ const recordedSha = destMeta.sha || null;
308
+ if (!recordedSha) return { kind: "reinstall", from, to };
309
+ const actualSha = computeContentSha(destDir);
310
+ if (actualSha !== recordedSha) return { kind: "tampered", from, to };
311
+ return { kind: "identical", from, to };
312
+ }
313
+ function statusLabel(st) {
314
+ switch (st.kind) {
315
+ case "install":
316
+ return st.to ? `install v${st.to}` : "install";
317
+ case "update":
318
+ return `update ${st.from} -> ${st.to}`;
319
+ case "identical":
320
+ return st.to ? `up-to-date v${st.to} (skip)` : "up-to-date (skip)";
321
+ case "tampered":
322
+ return st.to ? `MODIFIED locally v${st.to}` : "MODIFIED locally";
323
+ default:
324
+ return st.to ? `reinstall v${st.to}` : "reinstall";
325
+ }
326
+ }
327
+ function filesEqual(a, b) {
328
+ try {
329
+ return readFileSync2(a).equals(readFileSync2(b));
330
+ } catch {
331
+ return false;
332
+ }
333
+ }
334
+ function memoryStatus(srcFile, destFile) {
335
+ if (!existsSync4(destFile)) return "install";
336
+ return filesEqual(srcFile, destFile) ? "identical" : "overwrite";
337
+ }
338
+ function computePrune(destSkillsDir, sourceNames) {
339
+ if (!isDir(destSkillsDir)) return [];
340
+ return readdirSync(destSkillsDir).filter((e) => isDir(join5(destSkillsDir, e)) && existsSync4(join5(destSkillsDir, e, "SKILL.md"))).filter((e) => !sourceNames.includes(e)).map((e) => ({ name: e, dir: join5(destSkillsDir, e), meta: readSkillMeta(join5(destSkillsDir, e)) })).filter((s) => isManagedProvider(s.meta.provider));
341
+ }
342
+ function inspectSkills() {
343
+ if (!isDir(SKILLS_ROOT)) return [];
344
+ return readdirSync(SKILLS_ROOT).filter((e) => isDir(join5(SKILLS_ROOT, e)) && existsSync4(join5(SKILLS_ROOT, e, "SKILL.md"))).map((e) => ({ name: e, src: join5(SKILLS_ROOT, e), meta: readSkillMeta(join5(SKILLS_ROOT, e)) }));
345
+ }
346
+ function inspectMemory(agent) {
347
+ if (!agent.memoryFile) return [];
348
+ const src = join5(MEMORY_ROOT, agent.memoryFile);
349
+ return existsSync4(src) ? [{ name: agent.memoryFile, src }] : [];
350
+ }
351
+ function sealSources() {
352
+ if (!isDir(SKILLS_ROOT)) {
353
+ console.error(`No skills directory found at ${SKILLS_ROOT}.`);
354
+ process.exit(1);
355
+ }
356
+ const cli = cliVersion();
357
+ let sealed = 0;
358
+ for (const name of readdirSync(SKILLS_ROOT)) {
359
+ const dir = join5(SKILLS_ROOT, name);
360
+ if (!isDir(dir) || !existsSync4(join5(dir, "SKILL.md"))) continue;
361
+ const metaPath = join5(dir, "skill.json");
362
+ const meta = readJson(metaPath) || { name };
363
+ const before = JSON.stringify(meta);
364
+ meta.provider = MANAGED_PROVIDER;
365
+ meta.cliVersion = cli;
366
+ meta.sha = computeContentSha(dir);
367
+ const changed = JSON.stringify(meta) !== before;
368
+ writeFileSync3(metaPath, serializeMeta(meta));
369
+ console.log(`${changed ? "updated" : "ok "} ${name} cli=${cli} sha=${meta.sha.slice(0, 12)}`);
370
+ sealed++;
371
+ }
372
+ console.log(`
373
+ Sealed ${sealed} skill(s) at cliVersion ${cli}.`);
374
+ }
375
+ function checkSources() {
376
+ if (!isDir(SKILLS_ROOT)) {
377
+ console.error(`No skills directory found at ${SKILLS_ROOT}.`);
378
+ process.exit(1);
379
+ }
380
+ const cli = cliVersion();
381
+ const problems = [];
382
+ let checked = 0;
383
+ for (const name of readdirSync(SKILLS_ROOT)) {
384
+ const dir = join5(SKILLS_ROOT, name);
385
+ if (!isDir(dir) || !existsSync4(join5(dir, "SKILL.md"))) continue;
386
+ checked++;
387
+ const md = readFileSync2(join5(dir, "SKILL.md"), "utf8");
388
+ const fm = md.match(/^---\n([\s\S]*?)\n---/);
389
+ if (!fm) problems.push(`${name}: SKILL.md is missing YAML frontmatter`);
390
+ else {
391
+ if (!/^name:\s*\S/m.test(fm[1])) problems.push(`${name}: frontmatter missing 'name'`);
392
+ if (!/^description:\s*\S/m.test(fm[1])) problems.push(`${name}: frontmatter missing 'description'`);
393
+ }
394
+ const metaPath = join5(dir, "skill.json");
395
+ if (!existsSync4(metaPath)) {
396
+ problems.push(`${name}: missing skill.json`);
397
+ continue;
398
+ }
399
+ const meta = readJson(metaPath);
400
+ if (!meta) {
401
+ problems.push(`${name}: skill.json is not valid JSON`);
402
+ continue;
403
+ }
404
+ if (!isManagedProvider(meta.provider)) problems.push(`${name}: skill.json provider is not ${MANAGED_PROVIDER}`);
405
+ if (!meta.version) problems.push(`${name}: skill.json missing 'version'`);
406
+ if (meta.cliVersion !== cli) problems.push(`${name}: stale cliVersion (${meta.cliVersion || "none"} != ${cli}) - run 'enigma seal'`);
407
+ if (!meta.sha) problems.push(`${name}: not sealed (run 'enigma seal')`);
408
+ else if (meta.sha !== computeContentSha(dir)) problems.push(`${name}: stale sha - content changed since seal (run 'enigma seal')`);
409
+ }
410
+ if (problems.length) {
411
+ console.error(`Integrity check FAILED (${problems.length} problem(s) across ${checked} skill(s)):`);
412
+ for (const pr of problems) console.error(` - ${pr}`);
413
+ process.exit(1);
414
+ }
415
+ console.log(`Integrity check passed: ${checked} skill(s) well-formed and sealed.`);
416
+ }
417
+ async function installSkills(opts, interactive) {
418
+ const available = discoverAgents();
419
+ if (available.length === 0) {
420
+ p2.cancel("No installable agents known.");
421
+ process.exit(1);
422
+ }
423
+ let scope;
424
+ if (opts.scope) {
425
+ scope = opts.scope;
426
+ } else if (interactive) {
427
+ const r = await p2.select({
428
+ message: "Where should skills be installed?",
429
+ options: [
430
+ { value: "global", label: "Global (user)", hint: "~/.claude, ~/.codex, ~/.config/opencode" },
431
+ { value: "local", label: "Local (this project)", hint: process.cwd() }
432
+ ]
433
+ });
434
+ if (p2.isCancel(r)) {
435
+ p2.cancel("Aborted.");
436
+ return;
437
+ }
438
+ scope = r;
439
+ } else {
440
+ scope = "global";
441
+ }
442
+ const detected = available.filter((a) => a.installed);
443
+ let chosenAgents = available;
444
+ if (opts.agents.length) {
445
+ chosenAgents = available.filter((a) => opts.agents.includes(a.name));
446
+ const unknown = opts.agents.filter((n) => !available.some((a) => a.name === n));
447
+ if (unknown.length) p2.log.warn(`Skipping unknown/absent agents: ${unknown.join(", ")}`);
448
+ } else if (opts.allAgents) {
449
+ chosenAgents = available;
450
+ } else if (interactive && available.length > 1) {
451
+ const preselect = (detected.length ? detected : available).map((a) => a.name);
452
+ const r = await p2.multiselect({
453
+ message: "Which agents? (detected on this system are preselected)",
454
+ options: available.map((a) => ({ value: a.name, label: a.label, hint: a.installed ? "detected" : "not detected" })),
455
+ initialValues: preselect,
456
+ required: true
457
+ });
458
+ if (p2.isCancel(r)) {
459
+ p2.cancel("Aborted.");
460
+ return;
461
+ }
462
+ chosenAgents = available.filter((a) => r.includes(a.name));
463
+ } else if (detected.length) {
464
+ chosenAgents = detected;
465
+ } else {
466
+ chosenAgents = available;
467
+ p2.log.warn("No installed agents detected; defaulting to all supported agents.");
468
+ }
469
+ if (chosenAgents.length === 0) {
470
+ p2.cancel("No matching agents selected.");
471
+ process.exit(1);
472
+ }
473
+ const claudeScope = chosenAgents.some((a) => a.name === "claude") ? scope : null;
474
+ const applyClaudeConfig = () => {
475
+ if (!claudeScope || opts.dryRun) return;
476
+ if (disableClaudeAttribution(claudeScope)) {
477
+ p2.log.info("Claude Code: disabled Co-Authored-By and PR attribution in settings.json.");
478
+ }
479
+ };
480
+ const plan = [];
481
+ for (const agent of chosenAgents) {
482
+ const target = agent.targets[scope];
483
+ if (!target) {
484
+ p2.log.warn(`${agent.label} has no '${scope}' target - skipping.`);
485
+ continue;
486
+ }
487
+ const skills = inspectSkills();
488
+ const memory = inspectMemory(agent);
489
+ let chosenSkills = skills;
490
+ if (!opts.memoryOnly && opts.skills.length) {
491
+ chosenSkills = skills.filter((s2) => opts.skills.includes(s2.name));
492
+ } else if (!opts.memoryOnly && interactive && skills.length > 1) {
493
+ const r = await p2.multiselect({
494
+ message: `Skills for ${agent.label} - all selected; deselect any you don't want`,
495
+ options: skills.map((s2) => {
496
+ const st = skillStatus(join5(target.skills, s2.name), s2.meta);
497
+ const prov = s2.meta.provider ? ` ${s2.meta.provider}` : "";
498
+ return { value: s2.name, label: s2.name, hint: `${statusLabel(st)}${prov}` };
499
+ }),
500
+ initialValues: skills.map((s2) => s2.name),
501
+ required: false
502
+ });
503
+ if (p2.isCancel(r)) {
504
+ p2.cancel("Aborted.");
505
+ return;
506
+ }
507
+ chosenSkills = skills.filter((s2) => r.includes(s2.name));
508
+ }
509
+ const skillsWithStatus = (opts.memoryOnly ? [] : chosenSkills).map((s2) => ({
510
+ ...s2,
511
+ status: skillStatus(join5(target.skills, s2.name), s2.meta),
512
+ overwrite: true
513
+ }));
514
+ const prune = opts.prune && !opts.memoryOnly ? computePrune(target.skills, skills.map((s2) => s2.name)) : [];
515
+ plan.push({ agent, target, skills: skillsWithStatus, memory: opts.skillsOnly ? [] : memory, prune });
516
+ }
517
+ const tampered = plan.flatMap((x) => x.skills.filter((s2) => s2.status.kind === "tampered"));
518
+ if (tampered.length) {
519
+ if (opts.keepModified) {
520
+ for (const s2 of tampered) s2.overwrite = false;
521
+ p2.log.warn(`${tampered.length} locally-modified skill(s) will be kept (--keep-modified).`);
522
+ } else if (interactive && !opts.dryRun) {
523
+ const sel = await p2.multiselect({
524
+ message: `${tampered.length} skill(s) were modified locally since install. Select which to OVERWRITE`,
525
+ options: tampered.map((s2, i) => ({ value: i, label: s2.name, hint: s2.meta.version ? `v${s2.meta.version}` : "modified" })),
526
+ initialValues: tampered.map((_, i) => i),
527
+ required: false
528
+ });
529
+ if (p2.isCancel(sel)) {
530
+ p2.cancel("Aborted.");
531
+ return;
532
+ }
533
+ tampered.forEach((s2, i) => {
534
+ s2.overwrite = sel.includes(i);
535
+ });
536
+ }
537
+ }
538
+ const willCopy = (s2) => s2.status.kind === "install" || s2.status.kind === "update" || s2.status.kind === "reinstall" || s2.status.kind === "tampered" && s2.overwrite;
539
+ let nInstall = 0, nUpdate = 0, nRemove = 0, nSkip = 0, nKept = 0;
540
+ const lines = [];
541
+ for (const x of plan) {
542
+ lines.push(`${x.agent.label} (${scope})`);
543
+ for (const s2 of x.skills) {
544
+ const prov = s2.meta.provider ? ` [${s2.meta.provider}]` : "";
545
+ let label;
546
+ if (s2.status.kind === "identical") {
547
+ nSkip++;
548
+ label = statusLabel(s2.status);
549
+ } else if (s2.status.kind === "tampered" && !s2.overwrite) {
550
+ nKept++;
551
+ label = `keep modified v${s2.meta.version || "?"}`;
552
+ } else if (s2.status.kind === "tampered") {
553
+ nUpdate++;
554
+ label = `overwrite modified v${s2.meta.version || "?"}`;
555
+ } else if (s2.status.kind === "install") {
556
+ nInstall++;
557
+ label = statusLabel(s2.status);
558
+ } else {
559
+ nUpdate++;
560
+ label = statusLabel(s2.status);
561
+ }
562
+ lines.push(` ${label.padEnd(26)} skill ${s2.name}${prov}`);
563
+ }
564
+ for (const m of x.memory) {
565
+ const ms = memoryStatus(m.src, join5(x.target.memory, m.name));
566
+ if (ms === "identical") {
567
+ nSkip++;
568
+ lines.push(` ${"up-to-date (skip)".padEnd(26)} memory ${m.name}`);
569
+ } else if (ms === "install") {
570
+ nInstall++;
571
+ lines.push(` ${"install".padEnd(26)} memory ${m.name}`);
572
+ } else {
573
+ nUpdate++;
574
+ lines.push(` ${"overwrite".padEnd(26)} memory ${m.name}`);
575
+ }
576
+ }
577
+ for (const s2 of x.prune) {
578
+ nRemove++;
579
+ const ver = s2.meta.version ? ` v${s2.meta.version}` : "";
580
+ lines.push(` ${"remove (orphaned)".padEnd(26)} skill ${s2.name} [${s2.meta.provider}${ver}]`);
581
+ }
582
+ }
583
+ if (nInstall + nUpdate + nRemove === 0) {
584
+ p2.note(lines.join("\n"), "Nothing to do");
585
+ applyClaudeConfig();
586
+ await maybeOfferGitHooks(interactive, opts);
587
+ p2.log.success(`Everything up-to-date - ${nSkip} item(s) unchanged${nKept ? `, ${nKept} kept modified` : ""} (${scope}).`);
588
+ return;
589
+ }
590
+ p2.note(lines.join("\n"), opts.dryRun ? "Dry run - planned changes" : "Planned changes");
591
+ if (interactive && !opts.dryRun) {
592
+ const summary = [
593
+ nInstall && `${nInstall} to install`,
594
+ nUpdate && `${nUpdate} to update/overwrite`,
595
+ nRemove && `${nRemove} to remove`,
596
+ nSkip && `${nSkip} unchanged`
597
+ ].filter(Boolean).join(", ");
598
+ const ok = await p2.confirm({ message: `Apply: ${summary}?` });
599
+ if (p2.isCancel(ok) || !ok) {
600
+ p2.cancel("Aborted.");
601
+ return;
602
+ }
603
+ }
604
+ if (opts.dryRun) {
605
+ p2.log.info("Dry run complete - no files written.");
606
+ return;
607
+ }
608
+ const changedAgents = plan.filter(
609
+ (x) => x.skills.some(willCopy) || x.memory.some((m) => memoryStatus(m.src, join5(x.target.memory, m.name)) !== "identical") || x.prune.length > 0
610
+ );
611
+ const s = p2.spinner();
612
+ s.start("Installing...");
613
+ let copied = 0;
614
+ try {
615
+ for (const x of plan) {
616
+ mkdirSync3(x.target.skills, { recursive: true });
617
+ mkdirSync3(x.target.memory, { recursive: true });
618
+ for (const sk of x.skills) {
619
+ if (!willCopy(sk)) continue;
620
+ cpSync2(sk.src, join5(x.target.skills, sk.name), { recursive: true, force: true });
621
+ copied++;
622
+ }
623
+ for (const m of x.memory) {
624
+ if (memoryStatus(m.src, join5(x.target.memory, m.name)) === "identical") continue;
625
+ cpSync2(m.src, join5(x.target.memory, m.name), { force: true });
626
+ copied++;
627
+ }
628
+ for (const pr of x.prune) rmSync(pr.dir, { recursive: true, force: true });
629
+ }
630
+ } catch (err) {
631
+ s.stop("Failed.");
632
+ p2.cancel(`Error while installing: ${err.message}`);
633
+ process.exit(1);
634
+ }
635
+ s.stop(`Wrote ${copied} item(s)${nRemove ? `, removed ${nRemove}` : ""}.`);
636
+ applyClaudeConfig();
637
+ await maybeOfferGitHooks(interactive, opts);
638
+ p2.log.success(`${nInstall} installed, ${nUpdate} updated/overwritten` + (nRemove ? `, ${nRemove} removed` : "") + (nSkip ? `, ${nSkip} unchanged` : "") + (nKept ? `, ${nKept} kept modified` : "") + ` (${scope}).`);
639
+ if (changedAgents.length) {
640
+ const { known, running } = runningStatus(changedAgents.map((x) => x.agent));
641
+ if (running.size) {
642
+ const names = changedAgents.filter((x) => running.has(x.agent.name)).map((x) => x.agent.label);
643
+ p2.log.warn(`Restart ${names.join(", ")} to apply the changes (running now).`);
644
+ } else if (!known) {
645
+ const names = changedAgents.map((x) => x.agent.label);
646
+ p2.log.info(`If any of these agents are running, restart them to apply the changes: ${names.join(", ")}.`);
647
+ }
648
+ }
649
+ }
650
+
651
+ // src/guard.ts
652
+ import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
653
+ import { execFileSync as execFileSync3 } from "child_process";
654
+ import { basename, extname, dirname as dirname3, join as join6 } from "path";
655
+ import { fileURLToPath as fileURLToPath3 } from "url";
656
+ var LARGE_FILE_BYTES = 5 * 1024 * 1024;
657
+ var ENV_ALLOWED = /(example|sample|template)/i;
658
+ var isForbiddenEnv = (base) => /^\.env(\..+)?$/.test(base) && !ENV_ALLOWED.test(base);
659
+ var BLOCK_DIRS = [
660
+ /(^|\/)node_modules\//,
661
+ /(^|\/)bower_components\//,
662
+ /(^|\/)\.pnp(\/|$)/,
663
+ /(^|\/)__pycache__\//,
664
+ /(^|\/)\.venv\//,
665
+ /(^|\/)venv\//,
666
+ /(^|\/)\.mypy_cache\//,
667
+ /(^|\/)\.pytest_cache\//,
668
+ /(^|\/)\.gradle\//
669
+ ];
670
+ var WARN_DIRS = [
671
+ /(^|\/)dist\//,
672
+ /(^|\/)build\//,
673
+ /(^|\/)out\//,
674
+ /(^|\/)\.next\//,
675
+ /(^|\/)\.nuxt\//,
676
+ /(^|\/)\.svelte-kit\//,
677
+ /(^|\/)\.turbo\//,
678
+ /(^|\/)coverage\//
679
+ ];
680
+ var WARN_FILES = [/\.log$/i, /(^|\/)\.DS_Store$/, /(^|\/)Thumbs\.db$/i];
681
+ var SECRET_SKIP_EXT = /* @__PURE__ */ new Set([
682
+ ".png",
683
+ ".jpg",
684
+ ".jpeg",
685
+ ".gif",
686
+ ".webp",
687
+ ".ico",
688
+ ".pdf",
689
+ ".zip",
690
+ ".gz",
691
+ ".woff",
692
+ ".woff2",
693
+ ".ttf",
694
+ ".eot",
695
+ ".mp4",
696
+ ".mov",
697
+ ".lock"
698
+ ]);
699
+ var SECRET_PATTERNS = [
700
+ ["AWS access key id", /\bAKIA[0-9A-Z]{16}\b/],
701
+ ["GitHub token", /\bgh[pousr]_[A-Za-z0-9]{36,}\b/],
702
+ ["OpenAI API key", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/],
703
+ ["Anthropic API key", /\bsk-ant-[A-Za-z0-9_-]{20,}\b/],
704
+ ["Slack token", /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/],
705
+ ["Google API key", /\bAIza[0-9A-Za-z_-]{35}\b/],
706
+ ["Stripe secret key", /\bsk_live_[0-9A-Za-z]{24,}\b/],
707
+ ["Private key block", /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/],
708
+ ["Generic bearer secret", /\b(?:secret|token|api[_-]?key|passwd|password)\s*[:=]\s*["'][A-Za-z0-9_\-./+]{16,}["']/i]
709
+ ];
710
+ var SECRET_SKIP_PATH = [/(^|\/)node_modules\//, /(^|\/)\.git\//, /package-lock\.json$/, /guard\.[mc]?js$/];
711
+ var SELF_DIR = dirname3(fileURLToPath3(import.meta.url));
712
+ function loadConfig() {
713
+ const defaults = { secrets: true, envFiles: true, depDirs: true, generatedDirs: true, junkFiles: true, largeFiles: true };
714
+ try {
715
+ const raw = JSON.parse(readFileSync3(join6(SELF_DIR, "enigma-guard.json"), "utf8"));
716
+ return { ...defaults, ...raw };
717
+ } catch {
718
+ return defaults;
719
+ }
720
+ }
721
+ function gitFiles(all) {
722
+ const out = execFileSync3("git", all ? ["ls-files"] : ["diff", "--cached", "--name-only", "--diff-filter=ACM"], { encoding: "utf8" });
723
+ return out.split("\n").map((s) => s.trim()).filter(Boolean);
724
+ }
725
+ function scanSecrets(file, blocks) {
726
+ if (SECRET_SKIP_PATH.some((re) => re.test(file))) return;
727
+ if (SECRET_SKIP_EXT.has(extname(file).toLowerCase())) return;
728
+ let text;
729
+ try {
730
+ text = readFileSync3(file, "utf8");
731
+ } catch {
732
+ return;
733
+ }
734
+ if (text.includes("\0")) return;
735
+ text.split("\n").forEach((line, i) => {
736
+ for (const [label, re] of SECRET_PATTERNS) {
737
+ if (re.test(line)) blocks.push(`${file}:${i + 1} [secret: ${label}]`);
738
+ }
739
+ });
740
+ }
741
+ function runGuard({ all = false } = {}) {
742
+ const cfg = loadConfig();
743
+ let files;
744
+ try {
745
+ files = gitFiles(all);
746
+ } catch {
747
+ return { ok: true, blocks: [], warns: [], notRepo: true };
748
+ }
749
+ const blocks = [];
750
+ const warns = [];
751
+ for (const file of files) {
752
+ const base = basename(file);
753
+ if (cfg.envFiles && isForbiddenEnv(base)) {
754
+ blocks.push(`${file} [env file with secrets - commit .env.example/.template instead]`);
755
+ }
756
+ if (cfg.depDirs && BLOCK_DIRS.some((re) => re.test(file))) {
757
+ blocks.push(`${file} [dependency/cache dir - must not be committed]`);
758
+ } else if (cfg.generatedDirs && WARN_DIRS.some((re) => re.test(file))) {
759
+ warns.push(`${file} [looks generated - confirm you really want it tracked]`);
760
+ }
761
+ if (cfg.junkFiles && WARN_FILES.some((re) => re.test(file))) warns.push(`${file} [log / OS junk file]`);
762
+ if (cfg.largeFiles) {
763
+ try {
764
+ if (statSync2(file).size > LARGE_FILE_BYTES) warns.push(`${file} [larger than 5 MB]`);
765
+ } catch {
766
+ }
767
+ }
768
+ if (cfg.secrets) scanSecrets(file, blocks);
769
+ }
770
+ return { ok: blocks.length === 0, blocks, warns, count: files.length, all };
771
+ }
772
+ function runGuardCli(all) {
773
+ const r = runGuard({ all });
774
+ if (r.notRepo) {
775
+ console.error("enigma-guard: not a git repository; nothing to check.");
776
+ return 0;
777
+ }
778
+ if (r.warns.length) {
779
+ console.error(`enigma-guard: ${r.warns.length} warning(s):`);
780
+ for (const w of r.warns) console.error(` ! ${w}`);
781
+ }
782
+ if (r.blocks.length) {
783
+ console.error(`
784
+ enigma-guard: BLOCKED - ${r.blocks.length} problem(s) must be fixed before committing:`);
785
+ for (const b of r.blocks) console.error(` x ${b}`);
786
+ console.error("\nFix the above (move secrets to env/secret manager, gitignore generated paths).");
787
+ console.error("To bypass intentionally for one commit: git commit --no-verify");
788
+ return 1;
789
+ }
790
+ console.log(`enigma-guard: ${r.count} ${r.all ? "tracked" : "staged"} file(s) checked, no blocking problems.`);
791
+ return 0;
792
+ }
793
+ var guardEntry = process.argv[1] ?? "";
794
+ var isGuardEntry = /(^|[\\/])guard\.[mc]?[jt]s$/.test(guardEntry);
795
+ if (isGuardEntry && fileURLToPath3(import.meta.url) === guardEntry) {
796
+ process.exit(runGuardCli(process.argv.includes("--all")));
797
+ }
798
+
799
+ // src/config.ts
800
+ import { homedir as homedir3 } from "os";
801
+ import { join as join7 } from "path";
802
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
803
+ var CONFIG_FILE = ".enigma.json";
804
+ var CONFIG_DEFAULTS = { commitEmoji: true };
805
+ var BOOLEAN_KEYS = ["commitEmoji"];
806
+ var CLI_KEYS = { "commit-emoji": "commitEmoji" };
807
+ function configPath(scope) {
808
+ return scope === "global" ? join7(homedir3(), CONFIG_FILE) : join7(process.cwd(), CONFIG_FILE);
809
+ }
810
+ function readConfig() {
811
+ const sources = [];
812
+ let config = { ...CONFIG_DEFAULTS };
813
+ for (const scope of ["global", "local"]) {
814
+ const path = configPath(scope);
815
+ const raw = existsSync5(path) ? readJson(path) : null;
816
+ if (raw) {
817
+ config = { ...config, ...raw };
818
+ sources.push(path);
819
+ }
820
+ }
821
+ return { config, sources };
822
+ }
823
+ function parseBool(value) {
824
+ const v = value.toLowerCase();
825
+ if (["on", "true", "yes", "1", "enable", "enabled"].includes(v)) return true;
826
+ if (["off", "false", "no", "0", "disable", "disabled"].includes(v)) return false;
827
+ return null;
828
+ }
829
+ function setValue(scope, key, value) {
830
+ const path = configPath(scope);
831
+ const current = readJson(path) || {};
832
+ const next = { ...current, [key]: value };
833
+ const dir = join7(path, "..");
834
+ if (!isDir(dir)) mkdirSync4(dir, { recursive: true });
835
+ writeFileSync4(path, JSON.stringify(next, null, 2) + "\n");
836
+ return path;
837
+ }
838
+ function runConfigCli(positionals, scope) {
839
+ const [rawKey, rawValue] = positionals;
840
+ if (!rawKey) {
841
+ const { config, sources } = readConfig();
842
+ console.log("Effective enigma config:");
843
+ for (const k of BOOLEAN_KEYS) {
844
+ const cliKey = Object.keys(CLI_KEYS).find((c) => CLI_KEYS[c] === k) || k;
845
+ console.log(` ${cliKey}: ${config[k]}`);
846
+ }
847
+ console.log(sources.length ? `
848
+ From: ${sources.join(", ")}` : "\nFrom: built-in defaults (no .enigma.json found)");
849
+ return 0;
850
+ }
851
+ const key = CLI_KEYS[rawKey];
852
+ if (!key) {
853
+ console.error(`Unknown config key: ${rawKey}. Known keys: ${Object.keys(CLI_KEYS).join(", ")}.`);
854
+ return 1;
855
+ }
856
+ if (rawValue === void 0) {
857
+ console.error(`Missing value for '${rawKey}'. Usage: enigma config ${rawKey} <on|off>`);
858
+ return 1;
859
+ }
860
+ const value = parseBool(rawValue);
861
+ if (value === null) {
862
+ console.error(`Invalid value '${rawValue}' for '${rawKey}'. Use on or off.`);
863
+ return 1;
864
+ }
865
+ const target = scope || "global";
866
+ const path = setValue(target, key, value);
867
+ console.log(`Set ${rawKey} = ${value ? "on" : "off"} (${target}) in ${path}.`);
868
+ return 0;
869
+ }
870
+
871
+ // src/cli.ts
872
+ var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
873
+ var PKG = readJson(join8(__dirname3, "..", "package.json")) || {};
874
+ var COMMANDS = /* @__PURE__ */ new Set(["install", "security", "guard", "seal", "check", "config", "help", "version"]);
875
+ function parseArgs(argv) {
876
+ const opts = {
877
+ command: null,
878
+ positionals: [],
879
+ scope: null,
880
+ agents: [],
881
+ allAgents: false,
882
+ skills: [],
883
+ skillsOnly: false,
884
+ memoryOnly: false,
885
+ prune: true,
886
+ keepModified: false,
887
+ force: false,
888
+ all: false,
889
+ yes: false,
890
+ dryRun: false,
891
+ help: false,
892
+ version: false
893
+ };
894
+ for (let i = 0; i < argv.length; i++) {
895
+ const a = argv[i];
896
+ const next = () => argv[++i];
897
+ if (i === 0 && COMMANDS.has(a)) {
898
+ opts.command = a;
899
+ continue;
900
+ }
901
+ switch (a) {
902
+ case "-g":
903
+ case "--global":
904
+ opts.scope = "global";
905
+ break;
906
+ case "-l":
907
+ case "--local":
908
+ opts.scope = "local";
909
+ break;
910
+ case "-a":
911
+ case "--agent":
912
+ opts.agents.push(...next().split(","));
913
+ break;
914
+ case "-s":
915
+ case "--skill":
916
+ opts.skills.push(...next().split(","));
917
+ break;
918
+ case "--all":
919
+ opts.allAgents = true;
920
+ opts.all = true;
921
+ break;
922
+ case "--skills-only":
923
+ opts.skillsOnly = true;
924
+ break;
925
+ case "--memory-only":
926
+ opts.memoryOnly = true;
927
+ break;
928
+ case "--no-prune":
929
+ opts.prune = false;
930
+ break;
931
+ case "--keep-modified":
932
+ opts.keepModified = true;
933
+ break;
934
+ case "--force":
935
+ opts.force = true;
936
+ break;
937
+ case "-y":
938
+ case "--yes":
939
+ opts.yes = true;
940
+ break;
941
+ case "--dry-run":
942
+ opts.dryRun = true;
943
+ break;
944
+ case "-h":
945
+ case "--help":
946
+ opts.help = true;
947
+ break;
948
+ case "-v":
949
+ case "--version":
950
+ opts.version = true;
951
+ break;
952
+ default:
953
+ if (a.startsWith("-")) {
954
+ console.error(`Unknown option: ${a}`);
955
+ process.exit(1);
956
+ } else if (opts.command) {
957
+ opts.positionals.push(a);
958
+ } else {
959
+ console.error(`Unknown command: ${a}`);
960
+ process.exit(1);
961
+ }
962
+ }
963
+ }
964
+ return opts;
965
+ }
966
+ function printHelp() {
967
+ console.log(`
968
+ enigma - everything you need to work with a coding agent
969
+
970
+ Usage:
971
+ enigma [command] [options]
972
+
973
+ Commands:
974
+ (none) Interactive menu: pick which features to set up
975
+ install Install/update agent skills (Claude Code, Codex, opencode)
976
+ security Set up git security hooks in the current repo
977
+ guard [--all] Run the commit guard (staged files, or --all for every tracked file)
978
+ config [key val] Show/set runtime toggles (e.g. config commit-emoji off)
979
+ seal Maintenance: (re)compute skill content hashes
980
+ check Integrity gate: verify skills are well-formed and sealed
981
+ help, version
982
+
983
+ Install options:
984
+ -g, --global Install at user level
985
+ -l, --local Install into the current project
986
+ -a, --agent <name> Target agent(s) (default: auto-detect installed)
987
+ -s, --skill <name> Skill(s) to install (default: all)
988
+ --all Target every supported agent, ignoring detection
989
+ --skills-only Only skill folders --memory-only Only memory files
990
+ --no-prune Keep orphaned skills --keep-modified Don't overwrite local edits
991
+ --dry-run Show the plan without writing
992
+
993
+ Security options:
994
+ --force Override an existing core.hooksPath
995
+
996
+ Global:
997
+ -y, --yes Non-interactive -h, --help -v, --version
998
+
999
+ Examples:
1000
+ enigma # interactive
1001
+ enigma install --global # skills for detected agents, user level
1002
+ enigma install --all -y # every supported agent, non-interactive
1003
+ enigma security # configure git hooks (choose protections)
1004
+ enigma config # show effective runtime config
1005
+ enigma config commit-emoji off # opt out of commit-message emojis (global)
1006
+ `);
1007
+ }
1008
+ async function run(argv) {
1009
+ const opts = parseArgs(argv);
1010
+ if (opts.help || opts.command === "help") {
1011
+ printHelp();
1012
+ return;
1013
+ }
1014
+ if (opts.version || opts.command === "version") {
1015
+ console.log(PKG.version || "0.0.0");
1016
+ return;
1017
+ }
1018
+ if (opts.command === "seal") return sealSources();
1019
+ if (opts.command === "check") return checkSources();
1020
+ if (opts.command === "guard") {
1021
+ process.exit(runGuardCli(opts.all));
1022
+ }
1023
+ if (opts.command === "config") {
1024
+ process.exit(runConfigCli(opts.positionals, opts.scope));
1025
+ }
1026
+ const interactive = Boolean(process.stdout.isTTY) && !opts.yes;
1027
+ if (opts.command === "install") {
1028
+ p3.intro("enigma - install agent skills");
1029
+ await installSkills(opts, interactive);
1030
+ p3.outro("Done.");
1031
+ return;
1032
+ }
1033
+ if (opts.command === "security") {
1034
+ p3.intro("enigma - git security hooks");
1035
+ const done = await setupGitHooks(opts, interactive);
1036
+ p3.outro(done ? "Git hooks configured." : "No changes made.");
1037
+ return;
1038
+ }
1039
+ p3.intro("enigma");
1040
+ let features;
1041
+ if (interactive) {
1042
+ const r = await p3.multiselect({
1043
+ message: "What do you want to set up?",
1044
+ options: [
1045
+ { value: "skills", label: "Agent skills", hint: "Claude Code, Codex, opencode" },
1046
+ { value: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" }
1047
+ ],
1048
+ initialValues: ["skills"],
1049
+ required: true
1050
+ });
1051
+ if (p3.isCancel(r)) {
1052
+ p3.cancel("Aborted.");
1053
+ return;
1054
+ }
1055
+ features = r;
1056
+ } else {
1057
+ features = ["skills"];
1058
+ }
1059
+ if (features.includes("skills")) await installSkills(opts, interactive);
1060
+ if (features.includes("security")) await setupGitHooks(opts, interactive);
1061
+ p3.outro("Done.");
1062
+ }
1063
+
1064
+ // src/bin/enigma.ts
1065
+ run(process.argv.slice(2)).catch((err) => {
1066
+ console.error(err);
1067
+ process.exit(1);
1068
+ });