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