arcane-cli 0.30.0 → 0.32.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.
- package/dist/index.js +440 -108
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,11 +16,17 @@ import chalk2 from "chalk";
|
|
|
16
16
|
// src/modules/copier.ts
|
|
17
17
|
import {
|
|
18
18
|
mkdir,
|
|
19
|
-
copyFile as fsCopyFile,
|
|
20
19
|
access,
|
|
21
|
-
readdir
|
|
20
|
+
readdir,
|
|
21
|
+
readFile,
|
|
22
|
+
writeFile
|
|
22
23
|
} from "fs/promises";
|
|
24
|
+
import { createHash } from "crypto";
|
|
23
25
|
import path from "path";
|
|
26
|
+
async function hashFile(filePath) {
|
|
27
|
+
const content = await readFile(filePath);
|
|
28
|
+
return createHash("sha256").update(content).digest("hex");
|
|
29
|
+
}
|
|
24
30
|
function validateTargetPath(targetDir, relativePath) {
|
|
25
31
|
const resolvedBase = path.resolve(targetDir);
|
|
26
32
|
const resolvedFull = path.resolve(targetDir, relativePath);
|
|
@@ -50,7 +56,10 @@ async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
|
|
|
50
56
|
`Destination already exists: "${relativePath}". Use --force to overwrite.`
|
|
51
57
|
);
|
|
52
58
|
}
|
|
53
|
-
await
|
|
59
|
+
const content = await readFile(srcPath);
|
|
60
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
61
|
+
await writeFile(destPath, content);
|
|
62
|
+
return hash;
|
|
54
63
|
}
|
|
55
64
|
async function copyDirectory(srcDir, targetDir, relativeDir, opts = {}) {
|
|
56
65
|
validateTargetPath(targetDir, relativeDir);
|
|
@@ -63,15 +72,15 @@ async function copyDirectory(srcDir, targetDir, relativeDir, opts = {}) {
|
|
|
63
72
|
const subFiles = await copyDirectory(srcEntryPath, targetDir, relEntryPath, opts);
|
|
64
73
|
copiedFiles.push(...subFiles);
|
|
65
74
|
} else {
|
|
66
|
-
await copyFile(srcEntryPath, targetDir, relEntryPath, opts);
|
|
67
|
-
copiedFiles.push(relEntryPath);
|
|
75
|
+
const hash = await copyFile(srcEntryPath, targetDir, relEntryPath, opts);
|
|
76
|
+
copiedFiles.push({ path: relEntryPath, hash });
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
79
|
return copiedFiles;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
// src/modules/manifest.ts
|
|
74
|
-
import { readFile, writeFile } from "fs/promises";
|
|
83
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
75
84
|
import path2 from "path";
|
|
76
85
|
var ManifestNotFoundError = class extends Error {
|
|
77
86
|
constructor(manifestPath2) {
|
|
@@ -154,7 +163,7 @@ async function readManifest(targetDir) {
|
|
|
154
163
|
const filePath = manifestPath(targetDir);
|
|
155
164
|
let content;
|
|
156
165
|
try {
|
|
157
|
-
content = await
|
|
166
|
+
content = await readFile2(filePath, "utf-8");
|
|
158
167
|
} catch (err) {
|
|
159
168
|
if (err.code === "ENOENT") {
|
|
160
169
|
throw new ManifestNotFoundError(filePath);
|
|
@@ -180,7 +189,7 @@ async function resolveSecretsScanExcludePrefixes(targetDir) {
|
|
|
180
189
|
} catch {
|
|
181
190
|
}
|
|
182
191
|
try {
|
|
183
|
-
const raw = await
|
|
192
|
+
const raw = await readFile2(path2.join(targetDir, "src", "assets", ".arcane.json"), "utf-8");
|
|
184
193
|
const parsed = JSON.parse(raw);
|
|
185
194
|
if (isRecord(parsed) && parsed["selfHosted"] === true && parsed["tracking_mode"] === "internal") {
|
|
186
195
|
const prefixes = parsed["secretsScanExcludePrefixes"];
|
|
@@ -192,7 +201,7 @@ async function resolveSecretsScanExcludePrefixes(targetDir) {
|
|
|
192
201
|
}
|
|
193
202
|
async function writeManifest(targetDir, manifest) {
|
|
194
203
|
const filePath = manifestPath(targetDir);
|
|
195
|
-
await
|
|
204
|
+
await writeFile2(filePath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
196
205
|
}
|
|
197
206
|
function addComponent(manifest, component) {
|
|
198
207
|
return {
|
|
@@ -866,7 +875,7 @@ function printWarning(message) {
|
|
|
866
875
|
|
|
867
876
|
// src/modules/agents.ts
|
|
868
877
|
import { select, checkbox, Separator } from "@inquirer/prompts";
|
|
869
|
-
import { mkdir as mkdir3, writeFile as
|
|
878
|
+
import { mkdir as mkdir3, writeFile as writeFile5, copyFile as copyFile2 } from "fs/promises";
|
|
870
879
|
import { join as join3 } from "path";
|
|
871
880
|
import { stringify } from "yaml";
|
|
872
881
|
|
|
@@ -1102,7 +1111,7 @@ async function applyNamingStrategy(strategy, roleIds) {
|
|
|
1102
1111
|
}
|
|
1103
1112
|
|
|
1104
1113
|
// src/modules/agent-loader.ts
|
|
1105
|
-
import { readFile as
|
|
1114
|
+
import { readFile as readFile3, readdir as readdir2, access as access2 } from "fs/promises";
|
|
1106
1115
|
import { join } from "path";
|
|
1107
1116
|
import { parse } from "yaml";
|
|
1108
1117
|
|
|
@@ -1295,7 +1304,7 @@ async function loadAgentDefinition(agentsDir, id) {
|
|
|
1295
1304
|
const filePath = join(agentsDir, `${id}.yaml`);
|
|
1296
1305
|
let content;
|
|
1297
1306
|
try {
|
|
1298
|
-
content = await
|
|
1307
|
+
content = await readFile3(filePath, "utf8");
|
|
1299
1308
|
} catch {
|
|
1300
1309
|
throw new AgentDefinitionNotFoundError(id, agentsDir);
|
|
1301
1310
|
}
|
|
@@ -1305,7 +1314,7 @@ async function loadRoster(targetDir) {
|
|
|
1305
1314
|
const rosterPath = join(targetDir, ".arcane", "agents.yaml");
|
|
1306
1315
|
let content;
|
|
1307
1316
|
try {
|
|
1308
|
-
content = await
|
|
1317
|
+
content = await readFile3(rosterPath, "utf8");
|
|
1309
1318
|
} catch {
|
|
1310
1319
|
throw new AgentRosterNotFoundError();
|
|
1311
1320
|
}
|
|
@@ -1324,12 +1333,12 @@ function projectAgentsDir(targetDir) {
|
|
|
1324
1333
|
}
|
|
1325
1334
|
|
|
1326
1335
|
// src/modules/agent-generator.ts
|
|
1327
|
-
import { writeFile as
|
|
1336
|
+
import { writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
|
|
1328
1337
|
import { join as join2 } from "path";
|
|
1329
1338
|
import { homedir } from "os";
|
|
1330
1339
|
|
|
1331
1340
|
// src/modules/merger.ts
|
|
1332
|
-
import { readFile as
|
|
1341
|
+
import { readFile as readFile4, writeFile as writeFile3, rm } from "fs/promises";
|
|
1333
1342
|
import path3 from "path";
|
|
1334
1343
|
var MARKER_START = "<!-- arcane:start -->";
|
|
1335
1344
|
var MARKER_END = "<!-- arcane:end -->";
|
|
@@ -1344,7 +1353,7 @@ async function mergeIntoFile(targetDir, relativePath, content, opts = {}) {
|
|
|
1344
1353
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
1345
1354
|
let existing;
|
|
1346
1355
|
try {
|
|
1347
|
-
existing = await
|
|
1356
|
+
existing = await readFile4(destPath, "utf8");
|
|
1348
1357
|
} catch (err) {
|
|
1349
1358
|
const e = err;
|
|
1350
1359
|
if (e.code === "ENOENT") {
|
|
@@ -1360,7 +1369,7 @@ ${content}
|
|
|
1360
1369
|
${MARKER_END}
|
|
1361
1370
|
`;
|
|
1362
1371
|
if (!opts.dryRun) {
|
|
1363
|
-
await
|
|
1372
|
+
await writeFile3(destPath, newContent, "utf8");
|
|
1364
1373
|
}
|
|
1365
1374
|
return true;
|
|
1366
1375
|
}
|
|
@@ -1385,7 +1394,7 @@ ${MARKER_END}
|
|
|
1385
1394
|
${content}
|
|
1386
1395
|
${afterMarker}`;
|
|
1387
1396
|
if (!opts.dryRun) {
|
|
1388
|
-
await
|
|
1397
|
+
await writeFile3(destPath, updated, "utf8");
|
|
1389
1398
|
}
|
|
1390
1399
|
return true;
|
|
1391
1400
|
}
|
|
@@ -1395,7 +1404,7 @@ ${content}
|
|
|
1395
1404
|
${MARKER_END}
|
|
1396
1405
|
`;
|
|
1397
1406
|
if (!opts.dryRun) {
|
|
1398
|
-
await
|
|
1407
|
+
await writeFile3(destPath, appended, "utf8");
|
|
1399
1408
|
}
|
|
1400
1409
|
return true;
|
|
1401
1410
|
}
|
|
@@ -1403,7 +1412,7 @@ async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
|
1403
1412
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
1404
1413
|
let existing;
|
|
1405
1414
|
try {
|
|
1406
|
-
existing = await
|
|
1415
|
+
existing = await readFile4(destPath, "utf8");
|
|
1407
1416
|
} catch {
|
|
1408
1417
|
return false;
|
|
1409
1418
|
}
|
|
@@ -1417,7 +1426,7 @@ async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
|
1417
1426
|
if (stripped.length === 0) {
|
|
1418
1427
|
await rm(destPath, { force: true });
|
|
1419
1428
|
} else {
|
|
1420
|
-
await
|
|
1429
|
+
await writeFile3(destPath, stripped + "\n", "utf8");
|
|
1421
1430
|
}
|
|
1422
1431
|
}
|
|
1423
1432
|
return true;
|
|
@@ -1571,17 +1580,17 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1571
1580
|
try {
|
|
1572
1581
|
if (!options.dryRun) {
|
|
1573
1582
|
await mkdir2(workspaceDir, { recursive: true });
|
|
1574
|
-
await
|
|
1583
|
+
await writeFile4(
|
|
1575
1584
|
join2(workspaceDir, "IDENTITY.md"),
|
|
1576
1585
|
renderIdentity(def, displayName),
|
|
1577
1586
|
"utf8"
|
|
1578
1587
|
);
|
|
1579
|
-
await
|
|
1588
|
+
await writeFile4(
|
|
1580
1589
|
join2(workspaceDir, "SOUL.md"),
|
|
1581
1590
|
renderSoul(def, displayName),
|
|
1582
1591
|
"utf8"
|
|
1583
1592
|
);
|
|
1584
|
-
await
|
|
1593
|
+
await writeFile4(
|
|
1585
1594
|
join2(workspaceDir, "TOOLS.md"),
|
|
1586
1595
|
renderTools(def, displayName),
|
|
1587
1596
|
"utf8"
|
|
@@ -1610,7 +1619,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1610
1619
|
const generatedDir = join2(targetDir, ".arcane", "generated");
|
|
1611
1620
|
if (!options.dryRun) {
|
|
1612
1621
|
await mkdir2(generatedDir, { recursive: true });
|
|
1613
|
-
await
|
|
1622
|
+
await writeFile4(
|
|
1614
1623
|
join2(generatedDir, "openclaw-roster.json"),
|
|
1615
1624
|
JSON.stringify(patch, null, 2) + "\n",
|
|
1616
1625
|
"utf8"
|
|
@@ -1632,7 +1641,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1632
1641
|
const slug = displayName.toLowerCase().replace(/\s+/g, "-");
|
|
1633
1642
|
const fileName = `${slug}.agent.md`;
|
|
1634
1643
|
if (!options.dryRun) {
|
|
1635
|
-
await
|
|
1644
|
+
await writeFile4(
|
|
1636
1645
|
join2(agentsDir, fileName),
|
|
1637
1646
|
renderCopilotAgent(def, displayName),
|
|
1638
1647
|
"utf8"
|
|
@@ -1777,7 +1786,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
1777
1786
|
const arcaneDir = join3(targetDir, ".arcane", "agents");
|
|
1778
1787
|
if (!options.dryRun) {
|
|
1779
1788
|
await mkdir3(arcaneDir, { recursive: true });
|
|
1780
|
-
await
|
|
1789
|
+
await writeFile5(
|
|
1781
1790
|
join3(targetDir, ".arcane", "agents.yaml"),
|
|
1782
1791
|
stringify(roster),
|
|
1783
1792
|
"utf8"
|
|
@@ -1921,7 +1930,7 @@ async function runAgentsList(targetDir) {
|
|
|
1921
1930
|
}
|
|
1922
1931
|
|
|
1923
1932
|
// src/modules/push-safety.ts
|
|
1924
|
-
import { mkdir as mkdir4, readFile as
|
|
1933
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile6, chmod, access as access3, rm as rm2, readdir as readdir3 } from "fs/promises";
|
|
1925
1934
|
import { constants } from "fs";
|
|
1926
1935
|
import { join as join4, resolve, dirname, basename } from "path";
|
|
1927
1936
|
|
|
@@ -2177,7 +2186,7 @@ function isArcaneHooksPath(root, value) {
|
|
|
2177
2186
|
};
|
|
2178
2187
|
return normalize(value) === normalize(ARCANE_HOOKS_DIR);
|
|
2179
2188
|
}
|
|
2180
|
-
var
|
|
2189
|
+
var PRE_PUSH_HOOK_BODY = `#!/bin/sh
|
|
2181
2190
|
# Installed by Arcane because this repository's .arcane.json sets
|
|
2182
2191
|
# push_policy: "blocked".
|
|
2183
2192
|
#
|
|
@@ -2233,13 +2242,13 @@ async function repositoryRoot(cwd) {
|
|
|
2233
2242
|
}
|
|
2234
2243
|
return cwd;
|
|
2235
2244
|
}
|
|
2236
|
-
async function hookFilePath(cwd) {
|
|
2237
|
-
return join4(await repositoryRoot(cwd), ARCANE_HOOKS_DIR,
|
|
2245
|
+
async function hookFilePath(cwd, hookName = "pre-push") {
|
|
2246
|
+
return join4(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, hookName);
|
|
2238
2247
|
}
|
|
2239
2248
|
async function hooksPathValue(cwd) {
|
|
2240
2249
|
return join4(await repositoryRoot(cwd), ARCANE_HOOKS_DIR).replace(/\\/g, "/");
|
|
2241
2250
|
}
|
|
2242
|
-
async function
|
|
2251
|
+
async function installHook(cwd, hookName, body) {
|
|
2243
2252
|
const root = await repositoryRoot(cwd);
|
|
2244
2253
|
const existing = await readHooksPath(cwd);
|
|
2245
2254
|
if (existing?.unreadable === true) return { status: "refused-unreadable-config" };
|
|
@@ -2256,19 +2265,27 @@ async function installPrePushHook(cwd) {
|
|
|
2256
2265
|
return { status: "refused-default-hooks", hooks: displaced };
|
|
2257
2266
|
}
|
|
2258
2267
|
}
|
|
2259
|
-
const hookPath = join4(root, ARCANE_HOOKS_DIR,
|
|
2260
|
-
if (existing !== void 0 && await
|
|
2268
|
+
const hookPath = join4(root, ARCANE_HOOKS_DIR, hookName);
|
|
2269
|
+
if (existing !== void 0 && await hookFileMatches(cwd, hookName, body)) {
|
|
2270
|
+
return { status: "already-ours" };
|
|
2271
|
+
}
|
|
2261
2272
|
await mkdir4(join4(root, ARCANE_HOOKS_DIR), { recursive: true });
|
|
2262
|
-
await
|
|
2273
|
+
await writeFile6(hookPath, body, "utf-8");
|
|
2263
2274
|
await chmod(hookPath, 493);
|
|
2264
2275
|
await runGit(cwd, ["config", "--local", "core.hooksPath", await hooksPathValue(cwd)]);
|
|
2265
2276
|
return { status: "installed", path: hookPath };
|
|
2266
2277
|
}
|
|
2267
|
-
async function
|
|
2278
|
+
async function installPrePushHook(cwd) {
|
|
2279
|
+
return installHook(cwd, "pre-push", PRE_PUSH_HOOK_BODY);
|
|
2280
|
+
}
|
|
2281
|
+
async function isHookInstalled(cwd, hookName, body) {
|
|
2268
2282
|
const hooksPath = await readHooksPath(cwd);
|
|
2269
2283
|
if (hooksPath === void 0 || hooksPath.unreadable === true) return false;
|
|
2270
2284
|
if (!isArcaneHooksPath(await repositoryRoot(cwd), hooksPath.value)) return false;
|
|
2271
|
-
return
|
|
2285
|
+
return hookFileMatches(cwd, hookName, body);
|
|
2286
|
+
}
|
|
2287
|
+
async function isHookEnforced(cwd) {
|
|
2288
|
+
return isHookInstalled(cwd, "pre-push", PRE_PUSH_HOOK_BODY);
|
|
2272
2289
|
}
|
|
2273
2290
|
async function defaultHooks(cwd) {
|
|
2274
2291
|
let hooksDir;
|
|
@@ -2284,11 +2301,11 @@ async function defaultHooks(cwd) {
|
|
|
2284
2301
|
return [];
|
|
2285
2302
|
}
|
|
2286
2303
|
}
|
|
2287
|
-
async function
|
|
2288
|
-
const hookPath = await hookFilePath(cwd);
|
|
2304
|
+
async function hookFileMatches(cwd, hookName, body) {
|
|
2305
|
+
const hookPath = await hookFilePath(cwd, hookName);
|
|
2289
2306
|
if (!await fileExists(hookPath)) return false;
|
|
2290
2307
|
try {
|
|
2291
|
-
if (await
|
|
2308
|
+
if (await readFile5(hookPath, "utf-8") !== body) return false;
|
|
2292
2309
|
} catch {
|
|
2293
2310
|
return false;
|
|
2294
2311
|
}
|
|
@@ -2300,21 +2317,33 @@ async function hookBodyMatches(cwd) {
|
|
|
2300
2317
|
return false;
|
|
2301
2318
|
}
|
|
2302
2319
|
}
|
|
2303
|
-
async function
|
|
2320
|
+
async function removeHook(cwd, hookName) {
|
|
2304
2321
|
const existing = await readHooksPath(cwd);
|
|
2305
2322
|
if (existing === void 0 || existing.unreadable === true) return;
|
|
2306
|
-
|
|
2323
|
+
const root = await repositoryRoot(cwd);
|
|
2324
|
+
if (!isArcaneHooksPath(root, existing.value)) return;
|
|
2307
2325
|
if (existing.scope !== "local" && existing.scope !== "worktree") return;
|
|
2308
|
-
const scopeFlag = existing.scope === "worktree" ? "--worktree" : "--local";
|
|
2309
2326
|
try {
|
|
2310
|
-
await
|
|
2327
|
+
await rm2(await hookFilePath(cwd, hookName), { force: true });
|
|
2328
|
+
} catch {
|
|
2329
|
+
}
|
|
2330
|
+
const hooksDir = join4(root, ARCANE_HOOKS_DIR);
|
|
2331
|
+
let remaining = [];
|
|
2332
|
+
try {
|
|
2333
|
+
remaining = await readdir3(hooksDir);
|
|
2311
2334
|
} catch {
|
|
2335
|
+
remaining = [];
|
|
2312
2336
|
}
|
|
2337
|
+
if (remaining.length > 0) return;
|
|
2338
|
+
const scopeFlag = existing.scope === "worktree" ? "--worktree" : "--local";
|
|
2313
2339
|
try {
|
|
2314
|
-
await
|
|
2340
|
+
await runGit(cwd, ["config", scopeFlag, "--unset", "core.hooksPath"]);
|
|
2315
2341
|
} catch {
|
|
2316
2342
|
}
|
|
2317
2343
|
}
|
|
2344
|
+
async function removePrePushHook(cwd) {
|
|
2345
|
+
return removeHook(cwd, "pre-push");
|
|
2346
|
+
}
|
|
2318
2347
|
async function listRemotes(cwd) {
|
|
2319
2348
|
try {
|
|
2320
2349
|
const { stdout } = await runGit(cwd, ["remote"]);
|
|
@@ -2522,6 +2551,58 @@ function describeError(error) {
|
|
|
2522
2551
|
return message.split("\n")[0].trim();
|
|
2523
2552
|
}
|
|
2524
2553
|
|
|
2554
|
+
// src/modules/secrets-scan.ts
|
|
2555
|
+
var SECRETS_RULES = [
|
|
2556
|
+
{ label: "api-key", pattern: /API[_-]KEY\s*[:=]\s*\S+/i },
|
|
2557
|
+
// Case-sensitive and requiring an all-caps/underscore identifier (SECRET,
|
|
2558
|
+
// MY_SECRET, API_SECRET_KEY...) -- the real config/env-var convention for a
|
|
2559
|
+
// credential-holding key. Widening this repository-wide (ARC-037 decision 3)
|
|
2560
|
+
// surfaced that the original case-insensitive form matched ANY lowercase
|
|
2561
|
+
// `secret`/`token` code identifier (e.g. `const token = value.trim();`),
|
|
2562
|
+
// which is overwhelmingly a parsed/resolved value, never a literal
|
|
2563
|
+
// credential. The `(?!\$?\{)` guard additionally excludes any
|
|
2564
|
+
// brace-delimited placeholder -- GitHub Actions/templating references
|
|
2565
|
+
// (`TOKEN: ${{ secrets.X }}`, a reference to a secret store, never the
|
|
2566
|
+
// secret itself) and this codebase's own documented `{UPPER_SNAKE}`
|
|
2567
|
+
// placeholder convention (denylist-scan.ts's `scanFile` already strips
|
|
2568
|
+
// those before matching for the sibling org-token scan; a bare single
|
|
2569
|
+
// brace, with no `scanFile`-style stripping in the copy-time call path,
|
|
2570
|
+
// needed the same exclusion here rather than only the double-brace case).
|
|
2571
|
+
// A minimum length of 8 further excludes short non-secret values like
|
|
2572
|
+
// `write` (`id-token: write`).
|
|
2573
|
+
{ label: "generic-secret", pattern: /\b[A-Z0-9_]*SECRET[A-Z0-9_]*\s*[:=]\s*(?!\$?\{)\S{8,}/ },
|
|
2574
|
+
{ label: "generic-token", pattern: /\b[A-Z0-9_]*TOKEN[A-Z0-9_]*\s*[:=]\s*(?!\$?\{)\S{8,}/ },
|
|
2575
|
+
{ label: "bearer-token", pattern: /Bearer\s+[A-Za-z0-9._~+/-]{20,}/ },
|
|
2576
|
+
// real tokens are long
|
|
2577
|
+
{ label: "openai-key", pattern: /sk-[A-Za-z0-9]{20,}/ },
|
|
2578
|
+
{ label: "pem-key", pattern: /-----BEGIN [A-Z ]+-----/ },
|
|
2579
|
+
{ label: "slack-token", pattern: /xox[bpars]-[A-Za-z0-9-]+/ },
|
|
2580
|
+
{ label: "aws-access-key", pattern: /AKIA[0-9A-Z]{16}/ },
|
|
2581
|
+
{ label: "github-pat", pattern: /ghp_[A-Za-z0-9]{36}/ }
|
|
2582
|
+
];
|
|
2583
|
+
var SECRETS_PATTERNS = SECRETS_RULES.map((rule) => rule.pattern);
|
|
2584
|
+
var SECRETS_PRECOMMIT_HOOK_NAME = "pre-commit";
|
|
2585
|
+
var SECRETS_PRECOMMIT_HOOK_BODY = `#!/bin/sh
|
|
2586
|
+
# Installed by Arcane (ARC-037) to scan staged content for accidentally
|
|
2587
|
+
# committed credentials before they enter history.
|
|
2588
|
+
#
|
|
2589
|
+
# This is deliberately not tamper-proof: \`git commit --no-verify\` skips it
|
|
2590
|
+
# entirely. The real defense is the repository-wide scan Arcane's own build
|
|
2591
|
+
# runs in CI on every push -- this hook exists to catch a leak locally, for
|
|
2592
|
+
# free, before it ever reaches a commit.
|
|
2593
|
+
if ! command -v spell >/dev/null 2>&1; then
|
|
2594
|
+
echo "arcane: 'spell' not found on PATH -- skipping secrets scan (see: npm install -g arcane-cli)" >&2
|
|
2595
|
+
exit 0
|
|
2596
|
+
fi
|
|
2597
|
+
spell doctor --leaks
|
|
2598
|
+
`;
|
|
2599
|
+
async function installSecretsPrecommitHook(cwd) {
|
|
2600
|
+
return installHook(cwd, SECRETS_PRECOMMIT_HOOK_NAME, SECRETS_PRECOMMIT_HOOK_BODY);
|
|
2601
|
+
}
|
|
2602
|
+
async function removeSecretsPrecommitHook(cwd) {
|
|
2603
|
+
return removeHook(cwd, SECRETS_PRECOMMIT_HOOK_NAME);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2525
2606
|
// src/commands/init.ts
|
|
2526
2607
|
var VALID_PROFILES = ["full", "lite", "governance-only", "methodology", "docs"];
|
|
2527
2608
|
var PROFILE_ICONS = {
|
|
@@ -2683,13 +2764,14 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2683
2764
|
let fileCount = 0;
|
|
2684
2765
|
for (const component of components) {
|
|
2685
2766
|
const installedFiles = [];
|
|
2767
|
+
const fileHashes = {};
|
|
2686
2768
|
for (const file of component.files) {
|
|
2687
2769
|
const srcPath = join5(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
2688
2770
|
if (options.dryRun) {
|
|
2689
2771
|
printDryRun(`Would copy: ${file}`);
|
|
2690
2772
|
} else {
|
|
2691
2773
|
try {
|
|
2692
|
-
await copyFile(srcPath, targetDir, file, { force: options.force });
|
|
2774
|
+
fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: options.force });
|
|
2693
2775
|
} catch (err) {
|
|
2694
2776
|
if (component.skipExisting && err instanceof Error && err.message.includes("already exists")) {
|
|
2695
2777
|
continue;
|
|
@@ -2706,7 +2788,10 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2706
2788
|
printDryRun(`Would copy directory: ${dir}/`);
|
|
2707
2789
|
} else {
|
|
2708
2790
|
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: options.force });
|
|
2709
|
-
|
|
2791
|
+
for (const { path: copiedPath, hash } of copied) {
|
|
2792
|
+
installedFiles.push(copiedPath);
|
|
2793
|
+
fileHashes[copiedPath] = hash;
|
|
2794
|
+
}
|
|
2710
2795
|
fileCount += copied.length;
|
|
2711
2796
|
}
|
|
2712
2797
|
}
|
|
@@ -2714,7 +2799,8 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2714
2799
|
installedComponents.push({
|
|
2715
2800
|
name: component.name,
|
|
2716
2801
|
files: installedFiles,
|
|
2717
|
-
installedVersion: packageVersion
|
|
2802
|
+
installedVersion: packageVersion,
|
|
2803
|
+
fileHashes
|
|
2718
2804
|
});
|
|
2719
2805
|
}
|
|
2720
2806
|
}
|
|
@@ -2894,6 +2980,28 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2894
2980
|
);
|
|
2895
2981
|
}
|
|
2896
2982
|
}
|
|
2983
|
+
try {
|
|
2984
|
+
const hook = await installSecretsPrecommitHook(targetDir);
|
|
2985
|
+
if (hook.status === "installed") {
|
|
2986
|
+
printInfo("Installed a pre-commit hook that scans staged content for leaked credentials.");
|
|
2987
|
+
} else if (hook.status === "refused-unreadable-config") {
|
|
2988
|
+
printWarning(
|
|
2989
|
+
"Did not install the secrets pre-commit hook: git could not report the current core.hooksPath (unreadable config, or a git too old for `--show-scope`)."
|
|
2990
|
+
);
|
|
2991
|
+
} else if (hook.status === "refused-default-hooks") {
|
|
2992
|
+
printWarning(
|
|
2993
|
+
`Did not install the secrets pre-commit hook: this repository has hooks in git's default directory (${hook.hooks.join(", ")}). Setting core.hooksPath would silently stop them running. Move them under \`${ARCANE_HOOKS_DIR}\` yourself and re-run.`
|
|
2994
|
+
);
|
|
2995
|
+
} else if (hook.status === "refused-foreign-hooks-path") {
|
|
2996
|
+
printWarning(
|
|
2997
|
+
`Did not install the secrets pre-commit hook: core.hooksPath is already "${hook.existing}" (${describeConfigScope(hook.scope)}), so another hook manager owns it.`
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
printWarning(
|
|
3002
|
+
`Could not install the secrets pre-commit hook: ${err instanceof Error ? err.message : String(err)}. Run \`spell doctor --leaks\` directly to scan on demand instead.`
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
2897
3005
|
let agentsConfigured = false;
|
|
2898
3006
|
if (hasAgents && !options.profile) {
|
|
2899
3007
|
console.log();
|
|
@@ -2973,12 +3081,13 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
2973
3081
|
}
|
|
2974
3082
|
let fileCount = 0;
|
|
2975
3083
|
const installedFiles = [];
|
|
3084
|
+
const fileHashes = {};
|
|
2976
3085
|
for (const file of component.files) {
|
|
2977
3086
|
const srcPath = join6(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
2978
3087
|
if (options.dryRun) {
|
|
2979
3088
|
console.log(` [dry-run] Would copy: ${file}`);
|
|
2980
3089
|
} else {
|
|
2981
|
-
await copyFile(srcPath, targetDir, file, { force: options.force });
|
|
3090
|
+
fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: options.force });
|
|
2982
3091
|
}
|
|
2983
3092
|
installedFiles.push(file);
|
|
2984
3093
|
fileCount++;
|
|
@@ -2989,7 +3098,10 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
2989
3098
|
console.log(` [dry-run] Would copy directory: ${dir}/`);
|
|
2990
3099
|
} else {
|
|
2991
3100
|
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: options.force });
|
|
2992
|
-
|
|
3101
|
+
for (const { path: copiedPath, hash } of copied) {
|
|
3102
|
+
installedFiles.push(copiedPath);
|
|
3103
|
+
fileHashes[copiedPath] = hash;
|
|
3104
|
+
}
|
|
2993
3105
|
fileCount += copied.length;
|
|
2994
3106
|
}
|
|
2995
3107
|
}
|
|
@@ -3003,7 +3115,8 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
3003
3115
|
const newComponent = {
|
|
3004
3116
|
name,
|
|
3005
3117
|
files: installedFiles,
|
|
3006
|
-
installedVersion: packageVersion
|
|
3118
|
+
installedVersion: packageVersion,
|
|
3119
|
+
fileHashes
|
|
3007
3120
|
};
|
|
3008
3121
|
const updated = addComponent(manifest, newComponent);
|
|
3009
3122
|
await writeManifest(targetDir, updated);
|
|
@@ -3015,10 +3128,11 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
3015
3128
|
|
|
3016
3129
|
// src/commands/update.ts
|
|
3017
3130
|
import { join as join8 } from "path";
|
|
3131
|
+
import { readFile as readFile6, writeFile as writeFile8, rm as rm3 } from "fs/promises";
|
|
3018
3132
|
|
|
3019
3133
|
// src/modules/hub.ts
|
|
3020
3134
|
import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
|
|
3021
|
-
import { readdir as readdir4, writeFile as
|
|
3135
|
+
import { readdir as readdir4, writeFile as writeFile7 } from "fs/promises";
|
|
3022
3136
|
import { join as join7 } from "path";
|
|
3023
3137
|
var MANIFEST_RETROFITS = [
|
|
3024
3138
|
{
|
|
@@ -3198,12 +3312,154 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
|
3198
3312
|
updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3199
3313
|
ventures
|
|
3200
3314
|
};
|
|
3201
|
-
await
|
|
3315
|
+
await writeFile7(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
3202
3316
|
console.log(
|
|
3203
3317
|
` \u2713 Scaffolded ${businessRoot}/registry.json with ${Object.keys(ventures).length} venture(s).`
|
|
3204
3318
|
);
|
|
3205
3319
|
}
|
|
3206
3320
|
|
|
3321
|
+
// src/modules/merge3.ts
|
|
3322
|
+
var CONFLICT_START = "<<<<<<< yours (edited)";
|
|
3323
|
+
var CONFLICT_MID = "=======";
|
|
3324
|
+
var CONFLICT_END = ">>>>>>> incoming (vendor update)";
|
|
3325
|
+
function computeLcsDiff(base, other) {
|
|
3326
|
+
const n = base.length;
|
|
3327
|
+
const m = other.length;
|
|
3328
|
+
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
3329
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
3330
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
3331
|
+
dp[i2][j2] = base[i2] === other[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
const ops = [];
|
|
3335
|
+
let i = 0;
|
|
3336
|
+
let j = 0;
|
|
3337
|
+
let pendingBaseStart = 0;
|
|
3338
|
+
let pendingOtherStart = 0;
|
|
3339
|
+
const flushReplace = (baseEnd, otherEnd) => {
|
|
3340
|
+
if (baseEnd > pendingBaseStart || otherEnd > pendingOtherStart) {
|
|
3341
|
+
ops.push({
|
|
3342
|
+
type: "replace",
|
|
3343
|
+
baseStart: pendingBaseStart,
|
|
3344
|
+
baseEnd,
|
|
3345
|
+
otherStart: pendingOtherStart,
|
|
3346
|
+
otherEnd
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
while (i < n && j < m) {
|
|
3351
|
+
if (base[i] === other[j]) {
|
|
3352
|
+
flushReplace(i, j);
|
|
3353
|
+
const eqBaseStart = i;
|
|
3354
|
+
const eqOtherStart = j;
|
|
3355
|
+
while (i < n && j < m && base[i] === other[j]) {
|
|
3356
|
+
i++;
|
|
3357
|
+
j++;
|
|
3358
|
+
}
|
|
3359
|
+
ops.push({ type: "equal", baseStart: eqBaseStart, baseEnd: i, otherStart: eqOtherStart, otherEnd: j });
|
|
3360
|
+
pendingBaseStart = i;
|
|
3361
|
+
pendingOtherStart = j;
|
|
3362
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
3363
|
+
i++;
|
|
3364
|
+
} else {
|
|
3365
|
+
j++;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
flushReplace(n, m);
|
|
3369
|
+
return ops;
|
|
3370
|
+
}
|
|
3371
|
+
function arraysEqual(a, b) {
|
|
3372
|
+
if (a.length !== b.length) return false;
|
|
3373
|
+
return a.every((line, idx) => line === b[idx]);
|
|
3374
|
+
}
|
|
3375
|
+
var MAX_LINES_FOR_LCS = 4e3;
|
|
3376
|
+
function splitLines(text) {
|
|
3377
|
+
return text.split("\n");
|
|
3378
|
+
}
|
|
3379
|
+
function merge3(base, ours, theirs) {
|
|
3380
|
+
const baseLines = splitLines(base);
|
|
3381
|
+
const oursLines = splitLines(ours);
|
|
3382
|
+
const theirsLines = splitLines(theirs);
|
|
3383
|
+
if (baseLines.length > MAX_LINES_FOR_LCS || oursLines.length > MAX_LINES_FOR_LCS || theirsLines.length > MAX_LINES_FOR_LCS) {
|
|
3384
|
+
return {
|
|
3385
|
+
content: [CONFLICT_START, ours, CONFLICT_MID, theirs, CONFLICT_END].join("\n"),
|
|
3386
|
+
hasConflict: true
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
const oursOps = computeLcsDiff(baseLines, oursLines).filter((op) => op.type === "replace");
|
|
3390
|
+
const theirsOps = computeLcsDiff(baseLines, theirsLines).filter((op) => op.type === "replace");
|
|
3391
|
+
if (oursOps.length === 0 && theirsOps.length === 0) {
|
|
3392
|
+
return { content: base, hasConflict: false };
|
|
3393
|
+
}
|
|
3394
|
+
const intervals = [
|
|
3395
|
+
...oursOps.map((op) => ({
|
|
3396
|
+
baseStart: op.baseStart,
|
|
3397
|
+
baseEnd: op.baseEnd,
|
|
3398
|
+
source: "ours",
|
|
3399
|
+
lines: oursLines.slice(op.otherStart, op.otherEnd)
|
|
3400
|
+
})),
|
|
3401
|
+
...theirsOps.map((op) => ({
|
|
3402
|
+
baseStart: op.baseStart,
|
|
3403
|
+
baseEnd: op.baseEnd,
|
|
3404
|
+
source: "theirs",
|
|
3405
|
+
lines: theirsLines.slice(op.otherStart, op.otherEnd)
|
|
3406
|
+
}))
|
|
3407
|
+
].sort((a, b) => a.baseStart - b.baseStart || a.baseEnd - b.baseEnd);
|
|
3408
|
+
const groups = [];
|
|
3409
|
+
let groupMaxEnd = -1;
|
|
3410
|
+
for (const iv of intervals) {
|
|
3411
|
+
const current = groups[groups.length - 1];
|
|
3412
|
+
if (current && iv.baseStart < groupMaxEnd) {
|
|
3413
|
+
current.push(iv);
|
|
3414
|
+
groupMaxEnd = Math.max(groupMaxEnd, iv.baseEnd);
|
|
3415
|
+
} else {
|
|
3416
|
+
groups.push([iv]);
|
|
3417
|
+
groupMaxEnd = iv.baseEnd;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
const resultLines = [];
|
|
3421
|
+
let hasConflict = false;
|
|
3422
|
+
let basePos = 0;
|
|
3423
|
+
for (const group of groups) {
|
|
3424
|
+
const groupStart = Math.min(...group.map((g) => g.baseStart));
|
|
3425
|
+
const groupEnd = Math.max(...group.map((g) => g.baseEnd));
|
|
3426
|
+
resultLines.push(...baseLines.slice(basePos, groupStart));
|
|
3427
|
+
const oursInGroup = group.filter((g) => g.source === "ours");
|
|
3428
|
+
const theirsInGroup = group.filter((g) => g.source === "theirs");
|
|
3429
|
+
if (oursInGroup.length > 0 && theirsInGroup.length === 0) {
|
|
3430
|
+
for (const g of oursInGroup) resultLines.push(...g.lines);
|
|
3431
|
+
} else if (theirsInGroup.length > 0 && oursInGroup.length === 0) {
|
|
3432
|
+
for (const g of theirsInGroup) resultLines.push(...g.lines);
|
|
3433
|
+
} else {
|
|
3434
|
+
const oursGroupLines = oursInGroup.flatMap((g) => g.lines);
|
|
3435
|
+
const theirsGroupLines = theirsInGroup.flatMap((g) => g.lines);
|
|
3436
|
+
if (arraysEqual(oursGroupLines, theirsGroupLines)) {
|
|
3437
|
+
resultLines.push(...oursGroupLines);
|
|
3438
|
+
} else {
|
|
3439
|
+
hasConflict = true;
|
|
3440
|
+
resultLines.push(CONFLICT_START, ...oursGroupLines, CONFLICT_MID, ...theirsGroupLines, CONFLICT_END);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
basePos = groupEnd;
|
|
3444
|
+
}
|
|
3445
|
+
resultLines.push(...baseLines.slice(basePos));
|
|
3446
|
+
return { content: resultLines.join("\n"), hasConflict };
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// src/modules/npm-registry.ts
|
|
3450
|
+
var NPM_PACKAGE_NAME = "arcane-cli";
|
|
3451
|
+
async function fetchPublishedFile(version, relativeAssetPath) {
|
|
3452
|
+
const normalized = relativeAssetPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
3453
|
+
const url = `https://unpkg.com/${NPM_PACKAGE_NAME}@${version}/dist/assets/${normalized}`;
|
|
3454
|
+
try {
|
|
3455
|
+
const response = await fetch(url);
|
|
3456
|
+
if (!response.ok) return void 0;
|
|
3457
|
+
return await response.text();
|
|
3458
|
+
} catch {
|
|
3459
|
+
return void 0;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3207
3463
|
// src/commands/update.ts
|
|
3208
3464
|
function migrateLegacyComponents(components) {
|
|
3209
3465
|
const out = [];
|
|
@@ -3224,6 +3480,23 @@ function migrateLegacyComponents(components) {
|
|
|
3224
3480
|
}
|
|
3225
3481
|
return out;
|
|
3226
3482
|
}
|
|
3483
|
+
async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
3484
|
+
const filePath = join8(targetDir, file);
|
|
3485
|
+
if (!await fileExists(filePath)) return "not-found";
|
|
3486
|
+
if (!prune) return "reported";
|
|
3487
|
+
if (recordedHash === void 0) {
|
|
3488
|
+
console.log(` ! Orphaned, not pruned (no recorded hash to verify it's untouched): ${file}`);
|
|
3489
|
+
return "reported";
|
|
3490
|
+
}
|
|
3491
|
+
const currentHash = await hashFile(filePath);
|
|
3492
|
+
if (currentHash !== recordedHash) {
|
|
3493
|
+
console.log(` ! Orphaned but edited since install \u2014 not pruning: ${file}`);
|
|
3494
|
+
return "reported";
|
|
3495
|
+
}
|
|
3496
|
+
await rm3(filePath, { force: true });
|
|
3497
|
+
console.log(` Pruned orphaned file: ${file}`);
|
|
3498
|
+
return "pruned";
|
|
3499
|
+
}
|
|
3227
3500
|
async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
3228
3501
|
let manifest;
|
|
3229
3502
|
try {
|
|
@@ -3276,6 +3549,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3276
3549
|
}
|
|
3277
3550
|
let fileCount = 0;
|
|
3278
3551
|
const updatedComponents = [];
|
|
3552
|
+
const conflictedFiles = [];
|
|
3553
|
+
const orphanReport = [];
|
|
3279
3554
|
const componentsToUpdate = migrateLegacyComponents(manifest.components);
|
|
3280
3555
|
for (const installed of componentsToUpdate) {
|
|
3281
3556
|
let component;
|
|
@@ -3284,12 +3559,25 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3284
3559
|
} catch (err) {
|
|
3285
3560
|
if (err instanceof ComponentNotFoundError) {
|
|
3286
3561
|
console.log(` ! ${installed.name} not in registry \u2014 skipping.`);
|
|
3562
|
+
for (const file of installed.files) {
|
|
3563
|
+
const status = await resolveOrphan(
|
|
3564
|
+
targetDir,
|
|
3565
|
+
file,
|
|
3566
|
+
installed.fileHashes?.[file],
|
|
3567
|
+
Boolean(options.prune) && !options.dryRun
|
|
3568
|
+
);
|
|
3569
|
+
orphanReport.push({ file, status });
|
|
3570
|
+
}
|
|
3571
|
+
if (options.prune && !options.dryRun && installed.files.every((f) => orphanReport.find((o) => o.file === f)?.status !== "reported")) {
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3287
3574
|
updatedComponents.push(installed);
|
|
3288
3575
|
continue;
|
|
3289
3576
|
}
|
|
3290
3577
|
throw err;
|
|
3291
3578
|
}
|
|
3292
3579
|
const updatedFiles = [];
|
|
3580
|
+
const fileHashes = {};
|
|
3293
3581
|
for (const file of component.files) {
|
|
3294
3582
|
const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
3295
3583
|
const targetExists = await fileExists(join8(targetDir, file));
|
|
@@ -3306,7 +3594,38 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3306
3594
|
console.log(` [dry-run] Would update: ${file}`);
|
|
3307
3595
|
fileCount++;
|
|
3308
3596
|
} else {
|
|
3309
|
-
|
|
3597
|
+
const recordedHash = installed.fileHashes?.[file];
|
|
3598
|
+
let handled = false;
|
|
3599
|
+
if (recordedHash !== void 0 && targetExists) {
|
|
3600
|
+
const currentHash = await hashFile(join8(targetDir, file));
|
|
3601
|
+
if (currentHash !== recordedHash) {
|
|
3602
|
+
const oldVendorContent = await fetchPublishedFile(installed.installedVersion, file);
|
|
3603
|
+
if (oldVendorContent === void 0) {
|
|
3604
|
+
console.log(
|
|
3605
|
+
` ! Could not fetch the previously published version of ${file} to merge your edits \u2014 left your version untouched. Update it manually if you want the latest.`
|
|
3606
|
+
);
|
|
3607
|
+
handled = true;
|
|
3608
|
+
} else {
|
|
3609
|
+
const [currentContent, newContent] = await Promise.all([
|
|
3610
|
+
readFile6(join8(targetDir, file), "utf-8"),
|
|
3611
|
+
readFile6(srcPath, "utf-8")
|
|
3612
|
+
]);
|
|
3613
|
+
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
3614
|
+
await writeFile8(join8(targetDir, file), result.content, "utf-8");
|
|
3615
|
+
fileHashes[file] = await hashFile(join8(targetDir, file));
|
|
3616
|
+
if (result.hasConflict) {
|
|
3617
|
+
conflictedFiles.push(file);
|
|
3618
|
+
console.log(` \u26A0 Merge conflict in ${file} \u2014 resolve the <<<<<<< markers before committing.`);
|
|
3619
|
+
} else {
|
|
3620
|
+
console.log(` Merged your edits into: ${file}`);
|
|
3621
|
+
}
|
|
3622
|
+
handled = true;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
if (!handled) {
|
|
3627
|
+
fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: true });
|
|
3628
|
+
}
|
|
3310
3629
|
fileCount++;
|
|
3311
3630
|
}
|
|
3312
3631
|
if (!preserveExisting || installed.files.includes(file)) {
|
|
@@ -3319,16 +3638,45 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3319
3638
|
console.log(` [dry-run] Would update directory: ${dir}/`);
|
|
3320
3639
|
} else {
|
|
3321
3640
|
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true });
|
|
3322
|
-
|
|
3641
|
+
for (const { path: copiedPath, hash } of copied) {
|
|
3642
|
+
updatedFiles.push(copiedPath);
|
|
3643
|
+
fileHashes[copiedPath] = hash;
|
|
3644
|
+
}
|
|
3323
3645
|
fileCount += copied.length;
|
|
3324
3646
|
}
|
|
3325
3647
|
}
|
|
3648
|
+
for (const file of installed.files) {
|
|
3649
|
+
if (updatedFiles.includes(file)) continue;
|
|
3650
|
+
const status = await resolveOrphan(
|
|
3651
|
+
targetDir,
|
|
3652
|
+
file,
|
|
3653
|
+
installed.fileHashes?.[file],
|
|
3654
|
+
Boolean(options.prune) && !options.dryRun
|
|
3655
|
+
);
|
|
3656
|
+
orphanReport.push({ file, status });
|
|
3657
|
+
}
|
|
3326
3658
|
updatedComponents.push({
|
|
3327
3659
|
...installed,
|
|
3328
3660
|
files: updatedFiles,
|
|
3329
|
-
installedVersion: packageVersion
|
|
3661
|
+
installedVersion: packageVersion,
|
|
3662
|
+
fileHashes
|
|
3330
3663
|
});
|
|
3331
3664
|
}
|
|
3665
|
+
const orphansToReport = orphanReport.filter((o) => o.status !== "not-found");
|
|
3666
|
+
if (orphansToReport.length > 0) {
|
|
3667
|
+
const verb = options.dryRun ? "[dry-run] Found" : "Found";
|
|
3668
|
+
console.log(
|
|
3669
|
+
`
|
|
3670
|
+
${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update, no longer part of any current component):`
|
|
3671
|
+
);
|
|
3672
|
+
for (const { file, status } of orphansToReport) {
|
|
3673
|
+
const marker = status === "pruned" ? "pruned" : options.prune ? "kept (see reason above)" : "not removed";
|
|
3674
|
+
console.log(` ${file} \u2014 ${marker}`);
|
|
3675
|
+
}
|
|
3676
|
+
if (!options.prune) {
|
|
3677
|
+
console.log(" Run `spell update --prune` to remove the ones that are safe to delete.");
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3332
3680
|
if (options.dryRun) {
|
|
3333
3681
|
console.log(
|
|
3334
3682
|
`
|
|
@@ -3363,13 +3711,21 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3363
3711
|
`
|
|
3364
3712
|
\u2713 Updated ${fileCount} files.`
|
|
3365
3713
|
);
|
|
3714
|
+
if (conflictedFiles.length > 0) {
|
|
3715
|
+
console.log(
|
|
3716
|
+
`
|
|
3717
|
+
\u26A0 ${conflictedFiles.length} file(s) have unresolved merge conflicts:
|
|
3718
|
+
` + conflictedFiles.map((f) => ` ${f}`).join("\n") + `
|
|
3719
|
+
Search for "<<<<<<< yours" and resolve before committing.`
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3366
3722
|
if (manifest.role !== "hub" && updated.role === "hub") {
|
|
3367
3723
|
await offerRegistryScaffold(targetDir, updated.business_root ?? "ventures");
|
|
3368
3724
|
}
|
|
3369
3725
|
}
|
|
3370
3726
|
|
|
3371
3727
|
// src/modules/version-check.ts
|
|
3372
|
-
import { readFile as
|
|
3728
|
+
import { readFile as readFile7, writeFile as writeFile9, mkdir as mkdir5 } from "fs/promises";
|
|
3373
3729
|
import { dirname as dirname2, join as join9 } from "path";
|
|
3374
3730
|
import { homedir as homedir2 } from "os";
|
|
3375
3731
|
import { createRequire } from "module";
|
|
@@ -3388,7 +3744,7 @@ var DEFAULT_CACHE_PATH = join9(homedir2(), ".arcane", "version-cache.json");
|
|
|
3388
3744
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3389
3745
|
async function readCache(cachePath) {
|
|
3390
3746
|
try {
|
|
3391
|
-
const content = await
|
|
3747
|
+
const content = await readFile7(cachePath, "utf-8");
|
|
3392
3748
|
const parsed = JSON.parse(content);
|
|
3393
3749
|
if (typeof parsed.latest !== "string" || typeof parsed.checkedAt !== "string") {
|
|
3394
3750
|
return null;
|
|
@@ -3401,7 +3757,7 @@ async function readCache(cachePath) {
|
|
|
3401
3757
|
async function writeCache(cachePath, latest) {
|
|
3402
3758
|
await mkdir5(dirname2(cachePath), { recursive: true });
|
|
3403
3759
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3404
|
-
await
|
|
3760
|
+
await writeFile9(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
3405
3761
|
}
|
|
3406
3762
|
async function getLatestVersion(feedUrl) {
|
|
3407
3763
|
const normalizedBase = feedUrl.endsWith("/") ? feedUrl : feedUrl + "/";
|
|
@@ -3560,7 +3916,7 @@ async function runStatus(targetDir, packageVersion) {
|
|
|
3560
3916
|
}
|
|
3561
3917
|
|
|
3562
3918
|
// src/commands/uninstall.ts
|
|
3563
|
-
import { rm as
|
|
3919
|
+
import { rm as rm4 } from "fs/promises";
|
|
3564
3920
|
import { join as join10 } from "path";
|
|
3565
3921
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
3566
3922
|
async function runUninstall(options, targetDir) {
|
|
@@ -3626,22 +3982,22 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
3626
3982
|
for (const file of component.files) {
|
|
3627
3983
|
const filePath = join10(targetDir, file);
|
|
3628
3984
|
try {
|
|
3629
|
-
await
|
|
3985
|
+
await rm4(filePath, { force: true });
|
|
3630
3986
|
removed++;
|
|
3631
3987
|
} catch {
|
|
3632
3988
|
}
|
|
3633
3989
|
}
|
|
3634
3990
|
}
|
|
3635
|
-
await
|
|
3991
|
+
await rm4(join10(targetDir, ".arcane.json"), { force: true });
|
|
3636
3992
|
const agentDirs = [
|
|
3637
3993
|
".github/agents",
|
|
3638
3994
|
".arcane/agents",
|
|
3639
3995
|
".arcane/generated"
|
|
3640
3996
|
];
|
|
3641
3997
|
for (const dir of agentDirs) {
|
|
3642
|
-
await
|
|
3998
|
+
await rm4(join10(targetDir, dir), { recursive: true, force: true });
|
|
3643
3999
|
}
|
|
3644
|
-
await
|
|
4000
|
+
await rm4(join10(targetDir, ".arcane/agents.yaml"), { force: true });
|
|
3645
4001
|
const markerFiles = [
|
|
3646
4002
|
"CLAUDE.md",
|
|
3647
4003
|
"AGENTS.md",
|
|
@@ -3650,12 +4006,16 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
3650
4006
|
for (const file of markerFiles) {
|
|
3651
4007
|
await stripMarkerSection(targetDir, file, { dryRun: false });
|
|
3652
4008
|
}
|
|
4009
|
+
try {
|
|
4010
|
+
await removeSecretsPrecommitHook(targetDir);
|
|
4011
|
+
} catch {
|
|
4012
|
+
}
|
|
3653
4013
|
console.log(`
|
|
3654
4014
|
\u2713 Uninstalled \u2014 ${removed} files removed.`);
|
|
3655
4015
|
}
|
|
3656
4016
|
|
|
3657
4017
|
// src/commands/doctor.ts
|
|
3658
|
-
import { access as access4, readFile as
|
|
4018
|
+
import { access as access4, readFile as readFile9, mkdir as mkdir6, copyFile as fsCopyFile } from "fs/promises";
|
|
3659
4019
|
import { join as join12, dirname as dirname3 } from "path";
|
|
3660
4020
|
import { execFile as execFile3 } from "child_process";
|
|
3661
4021
|
import { promisify as promisify2 } from "util";
|
|
@@ -3726,7 +4086,7 @@ function evaluateIncidentGate(records) {
|
|
|
3726
4086
|
}
|
|
3727
4087
|
|
|
3728
4088
|
// src/modules/denylist-scan.ts
|
|
3729
|
-
import { readFile as
|
|
4089
|
+
import { readFile as readFile8, readdir as readdir5 } from "fs/promises";
|
|
3730
4090
|
import { join as join11, relative } from "path";
|
|
3731
4091
|
function escapeRegExp(value) {
|
|
3732
4092
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -3744,7 +4104,7 @@ async function scanFile(absolutePath, displayPath, rules) {
|
|
|
3744
4104
|
const findings = [];
|
|
3745
4105
|
let content;
|
|
3746
4106
|
try {
|
|
3747
|
-
content = await
|
|
4107
|
+
content = await readFile8(absolutePath, "utf8");
|
|
3748
4108
|
} catch {
|
|
3749
4109
|
return findings;
|
|
3750
4110
|
}
|
|
@@ -3799,37 +4159,6 @@ async function scanRepository(repoRoot, rules, extensions, excludePrefixes = [])
|
|
|
3799
4159
|
return findings;
|
|
3800
4160
|
}
|
|
3801
4161
|
|
|
3802
|
-
// src/modules/secrets-scan.ts
|
|
3803
|
-
var SECRETS_RULES = [
|
|
3804
|
-
{ label: "api-key", pattern: /API[_-]KEY\s*[:=]\s*\S+/i },
|
|
3805
|
-
// Case-sensitive and requiring an all-caps/underscore identifier (SECRET,
|
|
3806
|
-
// MY_SECRET, API_SECRET_KEY...) -- the real config/env-var convention for a
|
|
3807
|
-
// credential-holding key. Widening this repository-wide (ARC-037 decision 3)
|
|
3808
|
-
// surfaced that the original case-insensitive form matched ANY lowercase
|
|
3809
|
-
// `secret`/`token` code identifier (e.g. `const token = value.trim();`),
|
|
3810
|
-
// which is overwhelmingly a parsed/resolved value, never a literal
|
|
3811
|
-
// credential. The `(?!\$?\{)` guard additionally excludes any
|
|
3812
|
-
// brace-delimited placeholder -- GitHub Actions/templating references
|
|
3813
|
-
// (`TOKEN: ${{ secrets.X }}`, a reference to a secret store, never the
|
|
3814
|
-
// secret itself) and this codebase's own documented `{UPPER_SNAKE}`
|
|
3815
|
-
// placeholder convention (denylist-scan.ts's `scanFile` already strips
|
|
3816
|
-
// those before matching for the sibling org-token scan; a bare single
|
|
3817
|
-
// brace, with no `scanFile`-style stripping in the copy-time call path,
|
|
3818
|
-
// needed the same exclusion here rather than only the double-brace case).
|
|
3819
|
-
// A minimum length of 8 further excludes short non-secret values like
|
|
3820
|
-
// `write` (`id-token: write`).
|
|
3821
|
-
{ label: "generic-secret", pattern: /\b[A-Z0-9_]*SECRET[A-Z0-9_]*\s*[:=]\s*(?!\$?\{)\S{8,}/ },
|
|
3822
|
-
{ label: "generic-token", pattern: /\b[A-Z0-9_]*TOKEN[A-Z0-9_]*\s*[:=]\s*(?!\$?\{)\S{8,}/ },
|
|
3823
|
-
{ label: "bearer-token", pattern: /Bearer\s+[A-Za-z0-9._~+/-]{20,}/ },
|
|
3824
|
-
// real tokens are long
|
|
3825
|
-
{ label: "openai-key", pattern: /sk-[A-Za-z0-9]{20,}/ },
|
|
3826
|
-
{ label: "pem-key", pattern: /-----BEGIN [A-Z ]+-----/ },
|
|
3827
|
-
{ label: "slack-token", pattern: /xox[bpars]-[A-Za-z0-9-]+/ },
|
|
3828
|
-
{ label: "aws-access-key", pattern: /AKIA[0-9A-Z]{16}/ },
|
|
3829
|
-
{ label: "github-pat", pattern: /ghp_[A-Za-z0-9]{36}/ }
|
|
3830
|
-
];
|
|
3831
|
-
var SECRETS_PATTERNS = SECRETS_RULES.map((rule) => rule.pattern);
|
|
3832
|
-
|
|
3833
4162
|
// src/modules/platform-policy.ts
|
|
3834
4163
|
import { execFile as execFile2 } from "child_process";
|
|
3835
4164
|
import { promisify } from "util";
|
|
@@ -4062,7 +4391,7 @@ async function checkArcaneManifest(targetDir) {
|
|
|
4062
4391
|
} catch {
|
|
4063
4392
|
const selfHostedManifestPath = join12(targetDir, "src", "assets", ".arcane.json");
|
|
4064
4393
|
try {
|
|
4065
|
-
const raw = await
|
|
4394
|
+
const raw = await readFile9(selfHostedManifestPath, "utf8");
|
|
4066
4395
|
const manifest = JSON.parse(raw);
|
|
4067
4396
|
if (isRecord3(manifest) && manifest["selfHosted"] === true && manifest["tracking_mode"] === "internal") {
|
|
4068
4397
|
return {
|
|
@@ -4081,7 +4410,7 @@ async function checkArcaneManifest(targetDir) {
|
|
|
4081
4410
|
};
|
|
4082
4411
|
}
|
|
4083
4412
|
try {
|
|
4084
|
-
const raw = await
|
|
4413
|
+
const raw = await readFile9(manifestPath2, "utf8");
|
|
4085
4414
|
const manifest = JSON.parse(raw);
|
|
4086
4415
|
if (!isRecord3(manifest) || typeof manifest["version"] !== "string" || !manifest["version"] || !Array.isArray(manifest["components"])) {
|
|
4087
4416
|
return {
|
|
@@ -4190,7 +4519,7 @@ async function fixSessionContinuity(targetDir, assetsDir) {
|
|
|
4190
4519
|
} catch {
|
|
4191
4520
|
const srcPath = join12(assetsDir, file);
|
|
4192
4521
|
await mkdir6(dirname3(destPath), { recursive: true });
|
|
4193
|
-
await
|
|
4522
|
+
await fsCopyFile(srcPath, destPath);
|
|
4194
4523
|
created.push(file);
|
|
4195
4524
|
}
|
|
4196
4525
|
}
|
|
@@ -4252,7 +4581,7 @@ async function checkMcpConfig(targetDir) {
|
|
|
4252
4581
|
const filePath = join12(targetDir, ".mcp.json");
|
|
4253
4582
|
let raw;
|
|
4254
4583
|
try {
|
|
4255
|
-
raw = await
|
|
4584
|
+
raw = await readFile9(filePath, "utf8");
|
|
4256
4585
|
} catch {
|
|
4257
4586
|
return { name, passed: true, blocking: false, message: "no .mcp.json \u2014 nothing to check" };
|
|
4258
4587
|
}
|
|
@@ -4308,7 +4637,7 @@ async function checkDelegations(targetDir) {
|
|
|
4308
4637
|
const filePath = join12(targetDir, ".arcane", "delegations.json");
|
|
4309
4638
|
let raw;
|
|
4310
4639
|
try {
|
|
4311
|
-
raw = await
|
|
4640
|
+
raw = await readFile9(filePath, "utf8");
|
|
4312
4641
|
} catch {
|
|
4313
4642
|
return { name, passed: true, blocking: false, message: "no delegations recorded" };
|
|
4314
4643
|
}
|
|
@@ -4456,13 +4785,13 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
4456
4785
|
}
|
|
4457
4786
|
|
|
4458
4787
|
// src/modules/ward.ts
|
|
4459
|
-
import { readdir as readdir6, readFile as
|
|
4788
|
+
import { readdir as readdir6, readFile as readFile10 } from "fs/promises";
|
|
4460
4789
|
import { join as join13, relative as relative2 } from "path";
|
|
4461
4790
|
async function scanFileAllRules(absolutePath, displayPath, rules) {
|
|
4462
4791
|
const findings = [];
|
|
4463
4792
|
let content;
|
|
4464
4793
|
try {
|
|
4465
|
-
content = await
|
|
4794
|
+
content = await readFile10(absolutePath, "utf8");
|
|
4466
4795
|
} catch {
|
|
4467
4796
|
return findings;
|
|
4468
4797
|
}
|
|
@@ -4557,7 +4886,7 @@ async function runWard(repoRoot, options = {}) {
|
|
|
4557
4886
|
const findings2 = await scanFileAllRules(absolutePath, displayPath, allRules);
|
|
4558
4887
|
if (findings2.length > 0) {
|
|
4559
4888
|
try {
|
|
4560
|
-
fileLines.set(displayPath, (await
|
|
4889
|
+
fileLines.set(displayPath, (await readFile10(absolutePath, "utf8")).split("\n"));
|
|
4561
4890
|
} catch {
|
|
4562
4891
|
}
|
|
4563
4892
|
}
|
|
@@ -4766,9 +5095,12 @@ program.command("add <component>").description("Add a component to an existing A
|
|
|
4766
5095
|
);
|
|
4767
5096
|
}
|
|
4768
5097
|
);
|
|
4769
|
-
program.command("update").description("Update installed components to the current package version").option("--dry-run", "Preview what would be updated without making changes").
|
|
5098
|
+
program.command("update").description("Update installed components to the current package version").option("--dry-run", "Preview what would be updated without making changes").option(
|
|
5099
|
+
"--prune",
|
|
5100
|
+
"Also delete orphaned managed files (tracked before this update, no longer part of any current component). Hash-checked: an edited file is reported, never silently deleted"
|
|
5101
|
+
).action(async (opts) => {
|
|
4770
5102
|
await runUpdate(
|
|
4771
|
-
{ dryRun: opts.dryRun },
|
|
5103
|
+
{ dryRun: opts.dryRun, prune: opts.prune },
|
|
4772
5104
|
process.cwd(),
|
|
4773
5105
|
ASSETS_DIR,
|
|
4774
5106
|
pkg.version
|