arcane-cli 1.12.3 → 1.13.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/README.md +1 -1
- package/dist/assets/.arcane/governance/agent-approved-paths.md +1 -0
- package/dist/assets/.arcane/governance/agent-policies.md +1 -0
- package/dist/assets/.arcane/governance/agent-work-queue-model.md +1 -0
- package/dist/assets/.arcane/governance/authentication-strategy.md +1 -0
- package/dist/assets/.arcane/governance/cicd-standards.md +1 -0
- package/dist/assets/.arcane/governance/compliance-standards.md +1 -0
- package/dist/assets/.arcane/governance/decision-documentation-standard.md +1 -0
- package/dist/assets/.arcane/governance/development-methodology.md +1 -0
- package/dist/assets/.arcane/governance/external-verification-standards.md +1 -0
- package/dist/assets/.arcane/governance/framework-decisions.md +1 -0
- package/dist/assets/.arcane/governance/git-conventions.md +1 -0
- package/dist/assets/.arcane/governance/hardening-checklist.md +1 -0
- package/dist/assets/.arcane/governance/mobile-release-standards.md +1 -0
- package/dist/assets/.arcane/governance/naming-conventions.md +1 -0
- package/dist/assets/.arcane/governance/new-business-setup.md +1 -0
- package/dist/assets/.arcane/governance/poc-management-pattern.md +1 -0
- package/dist/assets/.arcane/governance/portable-bootstrap.md +1 -0
- package/dist/assets/.arcane/governance/product-excellence-standards.md +1 -0
- package/dist/assets/.arcane/governance/rca-process-standard.md +1 -0
- package/dist/assets/.arcane/governance/records-conventions.md +1 -0
- package/dist/assets/.arcane/governance/spell-authoring-standards.md +19 -1
- package/dist/assets/.arcane/governance/testing-standards.md +1 -0
- package/dist/assets/.arcane/governance/threat-model.md +1 -0
- package/dist/assets/.arcane/governance/universal-agent-rules.md +1 -0
- package/dist/assets/.arcane/governance/web-discoverability-standards.md +1 -0
- package/dist/assets/.arcane/spells/spell-address-review.md +1 -0
- package/dist/assets/.arcane/spells/spell-adopt-docs.md +1 -0
- package/dist/assets/.arcane/spells/spell-arcane-version.md +1 -0
- package/dist/assets/.arcane/spells/spell-architect.md +1 -0
- package/dist/assets/.arcane/spells/spell-brainstorm.md +1 -0
- package/dist/assets/.arcane/spells/spell-bug.md +1 -0
- package/dist/assets/.arcane/spells/spell-bump.md +1 -0
- package/dist/assets/.arcane/spells/spell-check-drift.md +1 -0
- package/dist/assets/.arcane/spells/spell-close-session.md +1 -0
- package/dist/assets/.arcane/spells/spell-commit-work.md +1 -0
- package/dist/assets/.arcane/spells/spell-compliance.md +1 -0
- package/dist/assets/.arcane/spells/spell-create-pull-request.md +1 -0
- package/dist/assets/.arcane/spells/spell-document.md +1 -0
- package/dist/assets/.arcane/spells/spell-dotnet-expert.md +1 -0
- package/dist/assets/.arcane/spells/spell-eas-store-deploy.md +1 -0
- package/dist/assets/.arcane/spells/spell-enchant.md +1 -0
- package/dist/assets/.arcane/spells/spell-explain-concept.md +1 -0
- package/dist/assets/.arcane/spells/spell-feedback.md +1 -0
- package/dist/assets/.arcane/spells/spell-full-cycle.md +1 -0
- package/dist/assets/.arcane/spells/spell-generate-bot-icons.md +1 -0
- package/dist/assets/.arcane/spells/spell-implement.md +1 -0
- package/dist/assets/.arcane/spells/spell-make-discoverable.md +1 -0
- package/dist/assets/.arcane/spells/spell-manifest.md +1 -0
- package/dist/assets/.arcane/spells/spell-open-session.md +1 -0
- package/dist/assets/.arcane/spells/spell-plan.md +1 -0
- package/dist/assets/.arcane/spells/spell-present-arcane.md +1 -0
- package/dist/assets/.arcane/spells/spell-product-review.md +1 -0
- package/dist/assets/.arcane/spells/spell-review-batch.md +1 -0
- package/dist/assets/.arcane/spells/spell-review.md +1 -0
- package/dist/assets/.arcane/spells/spell-save-idea.md +1 -0
- package/dist/assets/.arcane/spells/spell-scope.md +1 -0
- package/dist/assets/.arcane/spells/spell-scry.md +1 -0
- package/dist/assets/.arcane/spells/spell-security-review.md +1 -0
- package/dist/assets/.arcane/spells/spell-ship.md +1 -0
- package/dist/assets/.arcane/spells/spell-status.md +1 -0
- package/dist/assets/.arcane/spells/spell-suggest-feature.md +1 -0
- package/dist/assets/.arcane/spells/spell-summon-venture.md +1 -0
- package/dist/assets/.arcane/spells/spell-sync-pull-request.md +1 -0
- package/dist/assets/.arcane/spells/spell-test.md +1 -0
- package/dist/assets/.arcane/spells/spell-todo.md +1 -0
- package/dist/assets/.arcane/spells/spell-verification-ledger.md +1 -0
- package/dist/assets/.github/prompts/spell-address-review.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-adopt-docs.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-arcane-version.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-architect.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-brainstorm.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-bug.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-bump.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-check-drift.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-close-session.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-commit-work.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-compliance.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-document.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-dotnet-expert.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-eas-store-deploy.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-enchant.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-explain-concept.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-feedback.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-full-cycle.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-generate-bot-icons.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-implement.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-make-discoverable.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-manifest.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-open-session.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-plan.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-present-arcane.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-product-review.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-review-batch.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-review.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-save-idea.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-scope.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-scry.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-security-review.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-ship.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-status.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-suggest-feature.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-summon-venture.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-sync-pull-request.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-test.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-todo.prompt.md +1 -0
- package/dist/assets/.github/prompts/spell-verification-ledger.prompt.md +1 -0
- package/dist/index.js +353 -136
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const require = __createRequire(import.meta.url);
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { createRequire as createRequire3 } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as
|
|
9
|
+
import { dirname as dirname14, join as join23 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { select as select3, confirm as confirm2, input as input3 } from "@inquirer/prompts";
|
|
@@ -14,6 +14,7 @@ import { dirname as dirname5, join as join8 } from "path";
|
|
|
14
14
|
import chalk2 from "chalk";
|
|
15
15
|
|
|
16
16
|
// src/modules/copier.ts
|
|
17
|
+
import { lstatSync } from "fs";
|
|
17
18
|
import {
|
|
18
19
|
mkdir,
|
|
19
20
|
access,
|
|
@@ -123,7 +124,7 @@ function buildGitEnv() {
|
|
|
123
124
|
function runGit(cwd, args, options = {}) {
|
|
124
125
|
const commandClass = options.commandClass ?? classifyGitCommand(args);
|
|
125
126
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
|
|
126
|
-
return new Promise((
|
|
127
|
+
return new Promise((resolve10, reject) => {
|
|
127
128
|
const child = execFile(
|
|
128
129
|
"git",
|
|
129
130
|
args,
|
|
@@ -157,7 +158,7 @@ function runGit(cwd, args, options = {}) {
|
|
|
157
158
|
reject(error);
|
|
158
159
|
return;
|
|
159
160
|
}
|
|
160
|
-
|
|
161
|
+
resolve10({ stdout, stderr });
|
|
161
162
|
}
|
|
162
163
|
);
|
|
163
164
|
child.stdin?.end();
|
|
@@ -295,6 +296,24 @@ async function isUneditedTrackedFile(cwd, relativePath) {
|
|
|
295
296
|
return { verdict: "unedited", detail: "tracked, clean, and added by its only commit" };
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
// src/modules/real-path.ts
|
|
300
|
+
import { realpathSync } from "fs";
|
|
301
|
+
import { basename, dirname, join, resolve } from "path";
|
|
302
|
+
function resolveRealPath(path4) {
|
|
303
|
+
const missing = [];
|
|
304
|
+
let existing = path4;
|
|
305
|
+
for (; ; ) {
|
|
306
|
+
try {
|
|
307
|
+
return join(realpathSync.native(existing), ...missing);
|
|
308
|
+
} catch {
|
|
309
|
+
const parent = dirname(existing);
|
|
310
|
+
if (parent === existing) return resolve(path4);
|
|
311
|
+
missing.unshift(basename(existing));
|
|
312
|
+
existing = parent;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
298
317
|
// src/modules/copier.ts
|
|
299
318
|
function normalizeLineEndings(content) {
|
|
300
319
|
if (!content.includes(13)) return content;
|
|
@@ -332,6 +351,77 @@ function validateTargetPath(targetDir, relativePath) {
|
|
|
332
351
|
);
|
|
333
352
|
}
|
|
334
353
|
}
|
|
354
|
+
var LinkedDirectoryError = class extends Error {
|
|
355
|
+
constructor(relativePath, escape, targetDir) {
|
|
356
|
+
super(
|
|
357
|
+
`Linked directory: "${relativePath}" ${escape.link === relativePath ? "is" : `is under "${escape.link}",`} a link to "${escape.resolvesTo}" outside target directory "${targetDir}". Arcane writes and deletes managed files only inside the target directory: replace the link with a directory, then run the command again.`
|
|
358
|
+
);
|
|
359
|
+
this.relativePath = relativePath;
|
|
360
|
+
this.escape = escape;
|
|
361
|
+
this.name = "LinkedDirectoryError";
|
|
362
|
+
}
|
|
363
|
+
relativePath;
|
|
364
|
+
escape;
|
|
365
|
+
};
|
|
366
|
+
function isWithin(base, candidate) {
|
|
367
|
+
return candidate === base || candidate.startsWith(base.endsWith(path.sep) ? base : base + path.sep);
|
|
368
|
+
}
|
|
369
|
+
function findLinkedEscape(targetDir, relativeDir) {
|
|
370
|
+
const base = path.resolve(targetDir);
|
|
371
|
+
const dir = path.resolve(base, relativeDir);
|
|
372
|
+
if (!isWithin(base, dir) || dir === base) return null;
|
|
373
|
+
const realBase = resolveRealPath(base);
|
|
374
|
+
const realDir = resolveRealPath(dir);
|
|
375
|
+
if (isWithin(realBase, realDir)) return null;
|
|
376
|
+
const parts = path.relative(base, dir).split(path.sep);
|
|
377
|
+
let current = base;
|
|
378
|
+
for (let i = 0; i < parts.length; i++) {
|
|
379
|
+
current = path.join(current, parts[i]);
|
|
380
|
+
let isLink;
|
|
381
|
+
try {
|
|
382
|
+
isLink = lstatSync(current).isSymbolicLink();
|
|
383
|
+
} catch {
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
if (!isLink) continue;
|
|
387
|
+
const resolvesTo = resolveRealPath(current);
|
|
388
|
+
if (!isWithin(realBase, resolvesTo)) return { link: parts.slice(0, i + 1).join("/"), resolvesTo };
|
|
389
|
+
}
|
|
390
|
+
return { link: parts.join("/"), resolvesTo: realDir };
|
|
391
|
+
}
|
|
392
|
+
function validateRealTargetPath(targetDir, relativePath) {
|
|
393
|
+
validateTargetPath(targetDir, relativePath);
|
|
394
|
+
const escape = findLinkedEscape(targetDir, path.dirname(relativePath));
|
|
395
|
+
if (escape !== null) throw new LinkedDirectoryError(relativePath, escape, targetDir);
|
|
396
|
+
}
|
|
397
|
+
function validateRealTargetDirectory(targetDir, relativeDir) {
|
|
398
|
+
validateTargetPath(targetDir, relativeDir);
|
|
399
|
+
const escape = findLinkedEscape(targetDir, relativeDir);
|
|
400
|
+
if (escape !== null) throw new LinkedDirectoryError(relativeDir, escape, targetDir);
|
|
401
|
+
}
|
|
402
|
+
function findLinkedEscapes(targetDir, files) {
|
|
403
|
+
const byDirectory = /* @__PURE__ */ new Map();
|
|
404
|
+
const byLink = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
const dir = path.dirname(file);
|
|
407
|
+
if (!byDirectory.has(dir)) byDirectory.set(dir, findLinkedEscape(targetDir, dir));
|
|
408
|
+
const escape = byDirectory.get(dir);
|
|
409
|
+
if (escape == null) continue;
|
|
410
|
+
const seen = byLink.get(escape.link);
|
|
411
|
+
if (seen === void 0) byLink.set(escape.link, { ...escape, count: 1 });
|
|
412
|
+
else seen.count++;
|
|
413
|
+
}
|
|
414
|
+
return [...byLink.values()];
|
|
415
|
+
}
|
|
416
|
+
function describeLinkedEscapes(targetDir, escapes) {
|
|
417
|
+
return escapes.map(
|
|
418
|
+
(e) => ` \u2717 "${e.link}" is a link to "${e.resolvesTo}", outside "${path.resolve(targetDir)}": ${e.count} managed path${e.count === 1 ? "" : "s"} under it.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
function validateManagedPath(targetDir, relativePath, opts) {
|
|
422
|
+
if (opts.followDirectoryLinks) validateTargetPath(targetDir, relativePath);
|
|
423
|
+
else validateRealTargetPath(targetDir, relativePath);
|
|
424
|
+
}
|
|
335
425
|
async function ensureDir(dirPath) {
|
|
336
426
|
await mkdir(dirPath, { recursive: true });
|
|
337
427
|
}
|
|
@@ -380,7 +470,7 @@ async function managedPathLinkShape(targetDir, relativePath, linkTracked, bodyHe
|
|
|
380
470
|
}
|
|
381
471
|
}
|
|
382
472
|
async function writeManagedFile(targetDir, relativePath, content, opts = {}) {
|
|
383
|
-
|
|
473
|
+
validateManagedPath(targetDir, relativePath, opts);
|
|
384
474
|
const destPath = path.resolve(targetDir, relativePath);
|
|
385
475
|
await ensureDir(path.dirname(destPath));
|
|
386
476
|
const shape = await replaceSymlinkedDestination(destPath, relativePath, opts);
|
|
@@ -389,7 +479,7 @@ async function writeManagedFile(targetDir, relativePath, content, opts = {}) {
|
|
|
389
479
|
if (shape !== null) opts.conversions?.push({ file: relativePath, shape });
|
|
390
480
|
}
|
|
391
481
|
async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
|
|
392
|
-
|
|
482
|
+
validateManagedPath(targetDir, relativePath, opts);
|
|
393
483
|
const destPath = path.resolve(targetDir, relativePath);
|
|
394
484
|
await ensureDir(path.dirname(destPath));
|
|
395
485
|
if (!opts.force && await fileExists(destPath)) {
|
|
@@ -402,12 +492,12 @@ async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
|
|
|
402
492
|
await writeManagedFile(targetDir, relativePath, content, opts);
|
|
403
493
|
return hash;
|
|
404
494
|
}
|
|
405
|
-
async function removeWithin(targetDir, relativePath) {
|
|
406
|
-
|
|
495
|
+
async function removeWithin(targetDir, relativePath, opts = {}) {
|
|
496
|
+
validateManagedPath(targetDir, relativePath, opts);
|
|
407
497
|
await rm(path.resolve(targetDir, relativePath), { force: true });
|
|
408
498
|
}
|
|
409
|
-
async function removeEmptyAncestors(targetDir, relativePath) {
|
|
410
|
-
|
|
499
|
+
async function removeEmptyAncestors(targetDir, relativePath, opts = {}) {
|
|
500
|
+
validateManagedPath(targetDir, relativePath, opts);
|
|
411
501
|
const base = path.resolve(targetDir);
|
|
412
502
|
let dir = path.dirname(path.resolve(targetDir, relativePath));
|
|
413
503
|
while (dir !== base && dir.startsWith(base + path.sep)) {
|
|
@@ -1294,7 +1384,7 @@ function listProfiles() {
|
|
|
1294
1384
|
// src/modules/user-tier.ts
|
|
1295
1385
|
import { lstat as lstat2, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, rmdir as rmdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1296
1386
|
import { homedir } from "os";
|
|
1297
|
-
import { dirname, join, resolve } from "path";
|
|
1387
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
1298
1388
|
|
|
1299
1389
|
// src/modules/spell-compiler.ts
|
|
1300
1390
|
import { parse as parseYaml } from "yaml";
|
|
@@ -1395,7 +1485,7 @@ function resolveHomeDir() {
|
|
|
1395
1485
|
return homedir();
|
|
1396
1486
|
}
|
|
1397
1487
|
function userTierRoot(homeDir = resolveHomeDir()) {
|
|
1398
|
-
return
|
|
1488
|
+
return join2(homeDir, USER_TIER_DIR_NAME);
|
|
1399
1489
|
}
|
|
1400
1490
|
var USER_TIER_COMPONENTS = SPELL_COMPONENT_NAMES;
|
|
1401
1491
|
var CANONICAL_SPELL_PATH_PATTERN = /^\.arcane\/spells\/(spell-[a-z0-9-]+)\.md$/;
|
|
@@ -1410,7 +1500,7 @@ function spellIdFromStorePath(path4) {
|
|
|
1410
1500
|
return path4.replace(/\\/g, "/").match(STORE_SPELL_PATH_PATTERN)?.[1];
|
|
1411
1501
|
}
|
|
1412
1502
|
function absoluteStoreSpellPath(storeRoot, id) {
|
|
1413
|
-
return
|
|
1503
|
+
return resolve2(storeRoot, storeSpellPath(id)).replace(/\\/g, "/");
|
|
1414
1504
|
}
|
|
1415
1505
|
function componentForScope(component, scope) {
|
|
1416
1506
|
if (scope === "repo") return component;
|
|
@@ -1471,8 +1561,8 @@ function clientOfFanoutPath(relativePath) {
|
|
|
1471
1561
|
}
|
|
1472
1562
|
async function readSpellSource(storeRoot, id, fallbackDir) {
|
|
1473
1563
|
for (const candidate of [
|
|
1474
|
-
|
|
1475
|
-
...fallbackDir ? [
|
|
1564
|
+
join2(storeRoot, storeSpellPath(id)),
|
|
1565
|
+
...fallbackDir ? [join2(fallbackDir, canonicalSpellPath(id))] : []
|
|
1476
1566
|
]) {
|
|
1477
1567
|
try {
|
|
1478
1568
|
return await readFile3(candidate, "utf-8");
|
|
@@ -1517,7 +1607,7 @@ async function classifyTarget(path4) {
|
|
|
1517
1607
|
}
|
|
1518
1608
|
function perSpellDirectory(homeDir, relativePath) {
|
|
1519
1609
|
if (clientOfFanoutPath(relativePath) !== "codex") return void 0;
|
|
1520
|
-
return
|
|
1610
|
+
return dirname2(join2(homeDir, relativePath));
|
|
1521
1611
|
}
|
|
1522
1612
|
async function removeDirectoryIfEmpty(dir) {
|
|
1523
1613
|
try {
|
|
@@ -1546,13 +1636,13 @@ async function syncUserTierFanout(options) {
|
|
|
1546
1636
|
}
|
|
1547
1637
|
for (const file of desired) {
|
|
1548
1638
|
validateTargetPath(options.homeDir, file.relativePath);
|
|
1549
|
-
const target =
|
|
1639
|
+
const target = join2(options.homeDir, file.relativePath);
|
|
1550
1640
|
const recorded = previous[file.relativePath];
|
|
1551
1641
|
const newHash = hashContent(file.content);
|
|
1552
1642
|
const state = await classifyTarget(target);
|
|
1553
1643
|
if (state === "absent") {
|
|
1554
1644
|
if (!dryRun) {
|
|
1555
|
-
await mkdir2(
|
|
1645
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
1556
1646
|
await writeFile3(target, file.content, "utf-8");
|
|
1557
1647
|
}
|
|
1558
1648
|
record[file.relativePath] = newHash;
|
|
@@ -1585,7 +1675,7 @@ async function syncUserTierFanout(options) {
|
|
|
1585
1675
|
continue;
|
|
1586
1676
|
}
|
|
1587
1677
|
validateTargetPath(options.homeDir, relativePath);
|
|
1588
|
-
const target =
|
|
1678
|
+
const target = join2(options.homeDir, relativePath);
|
|
1589
1679
|
const state = await classifyTarget(target);
|
|
1590
1680
|
if (state === "absent") {
|
|
1591
1681
|
outcomes.push({ relativePath, status: "gone" });
|
|
@@ -1683,7 +1773,7 @@ async function inspectUserTierFanout(homeDir, record) {
|
|
|
1683
1773
|
health.total++;
|
|
1684
1774
|
const client2 = clientOfFanoutPath(relativePath);
|
|
1685
1775
|
if (client2 !== void 0) health.byClient[client2]++;
|
|
1686
|
-
const target =
|
|
1776
|
+
const target = join2(homeDir, relativePath);
|
|
1687
1777
|
const state = await classifyTarget(target);
|
|
1688
1778
|
if (state === "absent") {
|
|
1689
1779
|
health.missing.push(relativePath);
|
|
@@ -1694,7 +1784,7 @@ async function inspectUserTierFanout(homeDir, record) {
|
|
|
1694
1784
|
return health;
|
|
1695
1785
|
}
|
|
1696
1786
|
function isUserTierStore(targetDir) {
|
|
1697
|
-
return
|
|
1787
|
+
return resolve2(targetDir) === resolve2(userTierRoot());
|
|
1698
1788
|
}
|
|
1699
1789
|
function resolveInstallScope(targetDir, manifestScope, userFlag) {
|
|
1700
1790
|
if (userFlag) return { scope: "user", misplacedUserManifest: false };
|
|
@@ -1733,7 +1823,7 @@ var WORDMARK = [
|
|
|
1733
1823
|
var INDENT = " ";
|
|
1734
1824
|
var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
|
|
1735
1825
|
function sleep(ms) {
|
|
1736
|
-
return new Promise((
|
|
1826
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
1737
1827
|
}
|
|
1738
1828
|
function isAnimatable() {
|
|
1739
1829
|
return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
|
|
@@ -1852,7 +1942,7 @@ function printWarning(message) {
|
|
|
1852
1942
|
// src/modules/agents.ts
|
|
1853
1943
|
import { select, checkbox, Separator } from "@inquirer/prompts";
|
|
1854
1944
|
import { mkdir as mkdir4, writeFile as writeFile6, copyFile as copyFile2 } from "fs/promises";
|
|
1855
|
-
import { dirname as
|
|
1945
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1856
1946
|
import { stringify } from "yaml";
|
|
1857
1947
|
|
|
1858
1948
|
// src/config/agent-roles.ts
|
|
@@ -2088,7 +2178,7 @@ async function applyNamingStrategy(strategy, roleIds) {
|
|
|
2088
2178
|
|
|
2089
2179
|
// src/modules/agent-loader.ts
|
|
2090
2180
|
import { readFile as readFile4, readdir as readdir3, access as access2 } from "fs/promises";
|
|
2091
|
-
import { join as
|
|
2181
|
+
import { join as join3 } from "path";
|
|
2092
2182
|
import { parse } from "yaml";
|
|
2093
2183
|
|
|
2094
2184
|
// src/modules/agent-schema.ts
|
|
@@ -2283,7 +2373,7 @@ function parseYaml2(content, source) {
|
|
|
2283
2373
|
}
|
|
2284
2374
|
}
|
|
2285
2375
|
async function loadAgentDefinition(agentsDir, id) {
|
|
2286
|
-
const filePath =
|
|
2376
|
+
const filePath = join3(agentsDir, `${id}.yaml`);
|
|
2287
2377
|
let content;
|
|
2288
2378
|
try {
|
|
2289
2379
|
content = await readFile4(filePath, "utf8");
|
|
@@ -2293,10 +2383,10 @@ async function loadAgentDefinition(agentsDir, id) {
|
|
|
2293
2383
|
return validateAgentDefinition(parseYaml2(content, filePath), filePath);
|
|
2294
2384
|
}
|
|
2295
2385
|
function agentsBaseDir(targetDir, scope = "repo") {
|
|
2296
|
-
return scope === "user" ? targetDir :
|
|
2386
|
+
return scope === "user" ? targetDir : join3(targetDir, ".arcane");
|
|
2297
2387
|
}
|
|
2298
2388
|
async function loadRoster(targetDir, scope = "repo") {
|
|
2299
|
-
const rosterPath =
|
|
2389
|
+
const rosterPath = join3(agentsBaseDir(targetDir, scope), "agents.yaml");
|
|
2300
2390
|
let content;
|
|
2301
2391
|
try {
|
|
2302
2392
|
content = await readFile4(rosterPath, "utf8");
|
|
@@ -2307,19 +2397,19 @@ async function loadRoster(targetDir, scope = "repo") {
|
|
|
2307
2397
|
}
|
|
2308
2398
|
async function rosterExists(targetDir, scope = "repo") {
|
|
2309
2399
|
try {
|
|
2310
|
-
await access2(
|
|
2400
|
+
await access2(join3(agentsBaseDir(targetDir, scope), "agents.yaml"));
|
|
2311
2401
|
return true;
|
|
2312
2402
|
} catch {
|
|
2313
2403
|
return false;
|
|
2314
2404
|
}
|
|
2315
2405
|
}
|
|
2316
2406
|
function projectAgentsDir(targetDir, scope = "repo") {
|
|
2317
|
-
return
|
|
2407
|
+
return join3(agentsBaseDir(targetDir, scope), "agents");
|
|
2318
2408
|
}
|
|
2319
2409
|
|
|
2320
2410
|
// src/modules/agent-generator.ts
|
|
2321
2411
|
import { writeFile as writeFile5, mkdir as mkdir3, readdir as readdir4, readFile as readFile6 } from "fs/promises";
|
|
2322
|
-
import { join as
|
|
2412
|
+
import { join as join4 } from "path";
|
|
2323
2413
|
import { homedir as homedir2 } from "os";
|
|
2324
2414
|
|
|
2325
2415
|
// src/modules/merger.ts
|
|
@@ -2334,7 +2424,7 @@ var MalformedMarkersError = class extends Error {
|
|
|
2334
2424
|
}
|
|
2335
2425
|
};
|
|
2336
2426
|
async function mergeIntoFile(targetDir, relativePath, content, opts = {}) {
|
|
2337
|
-
|
|
2427
|
+
validateRealTargetPath(targetDir, relativePath);
|
|
2338
2428
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
2339
2429
|
let existing;
|
|
2340
2430
|
try {
|
|
@@ -2394,6 +2484,7 @@ ${MARKER_END}
|
|
|
2394
2484
|
return true;
|
|
2395
2485
|
}
|
|
2396
2486
|
async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
2487
|
+
validateRealTargetPath(targetDir, relativePath);
|
|
2397
2488
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
2398
2489
|
let existing;
|
|
2399
2490
|
try {
|
|
@@ -2532,7 +2623,7 @@ ${userTierRule}`;
|
|
|
2532
2623
|
async function loadBranchNamingFragment(assetsDir) {
|
|
2533
2624
|
try {
|
|
2534
2625
|
const fragment = await readFile6(
|
|
2535
|
-
|
|
2626
|
+
join4(assetsDir, ".arcane", "spells", "_fragments", "branch-naming.md"),
|
|
2536
2627
|
"utf8"
|
|
2537
2628
|
);
|
|
2538
2629
|
return fragment.trim();
|
|
@@ -2577,7 +2668,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2577
2668
|
const userAgentFiles = [];
|
|
2578
2669
|
const unmanagedAgentFiles = [];
|
|
2579
2670
|
const projectDefs = options.definitionsDir ?? projectAgentsDir(targetDir, userTier ? "user" : "repo");
|
|
2580
|
-
const bundledDefs =
|
|
2671
|
+
const bundledDefs = join4(assetsDir, "agents");
|
|
2581
2672
|
const resolved = [];
|
|
2582
2673
|
for (const entry of roster.roster) {
|
|
2583
2674
|
let def;
|
|
@@ -2606,22 +2697,22 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2606
2697
|
for (const { entry, def, displayName } of resolved) {
|
|
2607
2698
|
if (!def.clients.openclaw) continue;
|
|
2608
2699
|
const workspaceName = entry.id === "main" ? "workspace" : `workspace-${entry.id}`;
|
|
2609
|
-
const workspaceDir =
|
|
2700
|
+
const workspaceDir = join4(openclawRoot, workspaceName);
|
|
2610
2701
|
try {
|
|
2611
2702
|
if (!options.dryRun) {
|
|
2612
2703
|
await mkdir3(workspaceDir, { recursive: true });
|
|
2613
2704
|
await writeFile5(
|
|
2614
|
-
|
|
2705
|
+
join4(workspaceDir, "IDENTITY.md"),
|
|
2615
2706
|
renderIdentity(def, displayName),
|
|
2616
2707
|
"utf8"
|
|
2617
2708
|
);
|
|
2618
2709
|
await writeFile5(
|
|
2619
|
-
|
|
2710
|
+
join4(workspaceDir, "SOUL.md"),
|
|
2620
2711
|
renderSoul(def, displayName),
|
|
2621
2712
|
"utf8"
|
|
2622
2713
|
);
|
|
2623
2714
|
await writeFile5(
|
|
2624
|
-
|
|
2715
|
+
join4(workspaceDir, "TOOLS.md"),
|
|
2625
2716
|
renderTools(def, displayName),
|
|
2626
2717
|
"utf8"
|
|
2627
2718
|
);
|
|
@@ -2646,11 +2737,12 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2646
2737
|
}
|
|
2647
2738
|
}))
|
|
2648
2739
|
};
|
|
2649
|
-
const generatedDir =
|
|
2740
|
+
const generatedDir = join4(targetDir, ".arcane", "generated");
|
|
2650
2741
|
if (!options.dryRun) {
|
|
2742
|
+
validateRealTargetDirectory(targetDir, ".arcane/generated");
|
|
2651
2743
|
await mkdir3(generatedDir, { recursive: true });
|
|
2652
2744
|
await writeFile5(
|
|
2653
|
-
|
|
2745
|
+
join4(generatedDir, "openclaw-roster.json"),
|
|
2654
2746
|
JSON.stringify(patch, null, 2) + "\n",
|
|
2655
2747
|
"utf8"
|
|
2656
2748
|
);
|
|
@@ -2662,9 +2754,10 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2662
2754
|
synced.push("openclaw-roster.json");
|
|
2663
2755
|
}
|
|
2664
2756
|
if (options.copilot !== false) {
|
|
2665
|
-
const agentsDir =
|
|
2757
|
+
const agentsDir = join4(targetDir, ".github", "agents");
|
|
2666
2758
|
if (!options.dryRun && !userTier) {
|
|
2667
|
-
|
|
2759
|
+
validateRealTargetDirectory(targetDir, repoOptedOut ? ".github" : ".github/agents");
|
|
2760
|
+
await mkdir3(repoOptedOut ? join4(targetDir, ".github") : agentsDir, { recursive: true });
|
|
2668
2761
|
}
|
|
2669
2762
|
for (const { def, displayName } of resolved) {
|
|
2670
2763
|
if (!def.clients.copilot?.agent_file) continue;
|
|
@@ -2679,7 +2772,8 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2679
2772
|
}
|
|
2680
2773
|
if (repoOptedOut) continue;
|
|
2681
2774
|
if (!options.dryRun) {
|
|
2682
|
-
|
|
2775
|
+
validateRealTargetPath(targetDir, `.github/agents/${fileName}`);
|
|
2776
|
+
await writeFile5(join4(agentsDir, fileName), content, "utf8");
|
|
2683
2777
|
} else {
|
|
2684
2778
|
console.log(` [dry-run] Would write: .github/agents/${fileName}`);
|
|
2685
2779
|
}
|
|
@@ -2783,7 +2877,7 @@ async function performAgentSync(targetDir, assetsDir, roster, options) {
|
|
|
2783
2877
|
if (scope === "user") {
|
|
2784
2878
|
const manifest = await requireUserStore(targetDir);
|
|
2785
2879
|
const fanout = await syncUserTierFanout({
|
|
2786
|
-
homeDir:
|
|
2880
|
+
homeDir: dirname3(targetDir),
|
|
2787
2881
|
storeRoot: targetDir,
|
|
2788
2882
|
spellIds: [],
|
|
2789
2883
|
extraFiles: result.userAgentFiles,
|
|
@@ -2914,16 +3008,20 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
2914
3008
|
},
|
|
2915
3009
|
roster: rosterEntries
|
|
2916
3010
|
};
|
|
2917
|
-
const arcaneDir =
|
|
3011
|
+
const arcaneDir = join5(baseDir, "agents");
|
|
2918
3012
|
if (!options.dryRun) {
|
|
3013
|
+
if (scope === "repo") {
|
|
3014
|
+
validateRealTargetDirectory(targetDir, ".arcane/agents");
|
|
3015
|
+
validateRealTargetPath(targetDir, ".arcane/agents.yaml");
|
|
3016
|
+
}
|
|
2919
3017
|
await mkdir4(arcaneDir, { recursive: true });
|
|
2920
|
-
await writeFile6(
|
|
3018
|
+
await writeFile6(join5(baseDir, "agents.yaml"), stringify(roster), "utf8");
|
|
2921
3019
|
} else {
|
|
2922
3020
|
console.log(` [dry-run] Would write: ${label2}/agents.yaml`);
|
|
2923
3021
|
}
|
|
2924
3022
|
for (const roleId of selectedRoleIds) {
|
|
2925
|
-
const src =
|
|
2926
|
-
const dest =
|
|
3023
|
+
const src = join5(assetsDir, "agents", `${roleId}.yaml`);
|
|
3024
|
+
const dest = join5(arcaneDir, `${roleId}.yaml`);
|
|
2927
3025
|
if (!options.dryRun) {
|
|
2928
3026
|
try {
|
|
2929
3027
|
await copyFile2(src, dest);
|
|
@@ -3071,7 +3169,7 @@ async function runAgentsList(targetDir, scope = "repo") {
|
|
|
3071
3169
|
// src/modules/hub.ts
|
|
3072
3170
|
import { confirm, select as select2, input as input2 } from "@inquirer/prompts";
|
|
3073
3171
|
import { readdir as readdir5, writeFile as writeFile7 } from "fs/promises";
|
|
3074
|
-
import { join as
|
|
3172
|
+
import { join as join6 } from "path";
|
|
3075
3173
|
|
|
3076
3174
|
// src/modules/ado-context.ts
|
|
3077
3175
|
function remoteUrlHost(url) {
|
|
@@ -3388,9 +3486,9 @@ function describeFlagPatch(patch) {
|
|
|
3388
3486
|
return Object.entries(patch).filter(([field]) => field in FLAG_NAMES).map(([field, value]) => `${field}: ${JSON.stringify(value)}`);
|
|
3389
3487
|
}
|
|
3390
3488
|
async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
3391
|
-
const registryPath =
|
|
3489
|
+
const registryPath = join6(targetDir, businessRoot, "registry.json");
|
|
3392
3490
|
if (await fileExists(registryPath)) return;
|
|
3393
|
-
const businessRootPath =
|
|
3491
|
+
const businessRootPath = join6(targetDir, businessRoot);
|
|
3394
3492
|
let ventureDirs;
|
|
3395
3493
|
try {
|
|
3396
3494
|
const entries = await readdir5(businessRootPath, { withFileTypes: true });
|
|
@@ -3438,26 +3536,6 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
|
3438
3536
|
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8, chmod, access as access3, rm as rm4, readdir as readdir6 } from "fs/promises";
|
|
3439
3537
|
import { constants } from "fs";
|
|
3440
3538
|
import { join as join7, resolve as resolve3, dirname as dirname4, basename as basename2 } from "path";
|
|
3441
|
-
|
|
3442
|
-
// src/modules/real-path.ts
|
|
3443
|
-
import { realpathSync } from "fs";
|
|
3444
|
-
import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "path";
|
|
3445
|
-
function resolveRealPath(path4) {
|
|
3446
|
-
const missing = [];
|
|
3447
|
-
let existing = path4;
|
|
3448
|
-
for (; ; ) {
|
|
3449
|
-
try {
|
|
3450
|
-
return join6(realpathSync.native(existing), ...missing);
|
|
3451
|
-
} catch {
|
|
3452
|
-
const parent = dirname3(existing);
|
|
3453
|
-
if (parent === existing) return resolve2(path4);
|
|
3454
|
-
missing.unshift(basename(existing));
|
|
3455
|
-
existing = parent;
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
|
|
3460
|
-
// src/modules/push-safety.ts
|
|
3461
3539
|
var DISABLED_PUSH_URL = "arcane-push-blocked-run-spell-unblock-push://this-repository";
|
|
3462
3540
|
var NO_PUSH_URL_MARKER = (remote) => `remote.${remote}.arcaneHadNoPushUrl`;
|
|
3463
3541
|
var ARCANE_HOOKS_DIR = ".arcane/hooks";
|
|
@@ -3577,6 +3655,7 @@ async function installHook(cwd, hookName, body) {
|
|
|
3577
3655
|
if (existing !== void 0 && await hookFileMatches(cwd, hookName, body)) {
|
|
3578
3656
|
return { status: "already-ours" };
|
|
3579
3657
|
}
|
|
3658
|
+
validateRealTargetDirectory(root, ARCANE_HOOKS_DIR);
|
|
3580
3659
|
await mkdir5(join7(root, ARCANE_HOOKS_DIR), { recursive: true });
|
|
3581
3660
|
await writeFile8(hookPath, body, "utf-8");
|
|
3582
3661
|
await chmod(hookPath, 493);
|
|
@@ -3654,6 +3733,7 @@ async function removeHook(cwd, hookName) {
|
|
|
3654
3733
|
const root = await repositoryRoot(cwd);
|
|
3655
3734
|
if (!isArcaneHooksPath(root, existing.value)) return;
|
|
3656
3735
|
if (existing.scope !== "local" && existing.scope !== "worktree") return;
|
|
3736
|
+
if (findLinkedEscape(root, ARCANE_HOOKS_DIR) !== null) return;
|
|
3657
3737
|
try {
|
|
3658
3738
|
await rm4(await hookFilePath(cwd, hookName), { force: true });
|
|
3659
3739
|
} catch {
|
|
@@ -4168,6 +4248,22 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4168
4248
|
const components = profileComponents.map(
|
|
4169
4249
|
(component) => componentForSpellScope(component, spell_scope ?? "repo")
|
|
4170
4250
|
);
|
|
4251
|
+
const plannedFiles = [];
|
|
4252
|
+
for (const component of components) {
|
|
4253
|
+
plannedFiles.push(...component.files);
|
|
4254
|
+
for (const dir of component.directories ?? []) {
|
|
4255
|
+
plannedFiles.push(...await listDirectoryFiles(join8(assetsDir, dir), dir));
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
const escapes = findLinkedEscapes(targetDir, plannedFiles);
|
|
4259
|
+
if (escapes.length > 0) {
|
|
4260
|
+
for (const line of describeLinkedEscapes(targetDir, escapes)) console.error(line);
|
|
4261
|
+
console.error(
|
|
4262
|
+
" \u2717 Init refused: nothing was installed. Arcane writes managed files only inside its target directory: replace each link named above with a directory, then run `spell init` again."
|
|
4263
|
+
);
|
|
4264
|
+
process.exit(1);
|
|
4265
|
+
return;
|
|
4266
|
+
}
|
|
4171
4267
|
const groups = categorizeComponents(components);
|
|
4172
4268
|
console.log();
|
|
4173
4269
|
printSection(`\u{1F4E6} Profile: ${profile}`);
|
|
@@ -4518,7 +4614,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4518
4614
|
const fileHashes = {};
|
|
4519
4615
|
for (const file of component.files) {
|
|
4520
4616
|
const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
4521
|
-
fileHashes[file] = await copyFile(srcPath, storeRoot, file, { force: options.force });
|
|
4617
|
+
fileHashes[file] = await copyFile(srcPath, storeRoot, file, { force: options.force, followDirectoryLinks: true });
|
|
4522
4618
|
installedFiles.push(file);
|
|
4523
4619
|
fileCount++;
|
|
4524
4620
|
}
|
|
@@ -4562,12 +4658,17 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4562
4658
|
console.log();
|
|
4563
4659
|
}
|
|
4564
4660
|
|
|
4661
|
+
// src/commands/add.ts
|
|
4662
|
+
import { resolve as resolve4 } from "path";
|
|
4663
|
+
|
|
4565
4664
|
// src/modules/component-install.ts
|
|
4566
4665
|
import { readFile as readFile8, stat, writeFile as writeFile9 } from "fs/promises";
|
|
4567
|
-
import { join as join9 } from "path";
|
|
4666
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
4568
4667
|
async function planComponentInstall(component, targetDir, assetsDir, opts = {}) {
|
|
4569
4668
|
const files = [];
|
|
4570
4669
|
const stateOf = async (file, src) => {
|
|
4670
|
+
const escape = findLinkedEscape(targetDir, dirname6(file));
|
|
4671
|
+
if (escape !== null) return { file, src, state: "linked", escape };
|
|
4571
4672
|
const dest = join9(targetDir, file);
|
|
4572
4673
|
if (!await fileExists(dest)) return { file, src, state: "missing" };
|
|
4573
4674
|
if ((await stat(dest)).isDirectory()) return { file, src, state: "directory" };
|
|
@@ -4585,12 +4686,12 @@ async function planComponentInstall(component, targetDir, assetsDir, opts = {})
|
|
|
4585
4686
|
}
|
|
4586
4687
|
const of = (...states) => files.filter((f) => states.includes(f.state));
|
|
4587
4688
|
if (opts.force) {
|
|
4588
|
-
return { component, files, toCopy: of("missing", "identical", "differs"), kept: [], conflicts: of("directory") };
|
|
4689
|
+
return { component, files, toCopy: of("missing", "identical", "differs"), kept: [], conflicts: of("directory", "linked") };
|
|
4589
4690
|
}
|
|
4590
4691
|
if (component.skipExisting) {
|
|
4591
|
-
return { component, files, toCopy: of("missing"), kept: of("identical", "differs", "directory"), conflicts:
|
|
4692
|
+
return { component, files, toCopy: of("missing"), kept: of("identical", "differs", "directory"), conflicts: of("linked") };
|
|
4592
4693
|
}
|
|
4593
|
-
return { component, files, toCopy: of("missing", "identical"), kept: [], conflicts: of("differs", "directory") };
|
|
4694
|
+
return { component, files, toCopy: of("missing", "identical"), kept: [], conflicts: of("differs", "directory", "linked") };
|
|
4594
4695
|
}
|
|
4595
4696
|
async function executeInstallPlan(plan, targetDir, opts = {}) {
|
|
4596
4697
|
const files = [];
|
|
@@ -4631,9 +4732,17 @@ function describeExisting(f) {
|
|
|
4631
4732
|
function directoryConflict(plan) {
|
|
4632
4733
|
return plan.conflicts.find((f) => f.state === "directory");
|
|
4633
4734
|
}
|
|
4634
|
-
function
|
|
4735
|
+
function linkedConflict(plan) {
|
|
4736
|
+
return plan.conflicts.find((f) => f.state === "linked");
|
|
4737
|
+
}
|
|
4738
|
+
function describeLink(f, targetDir) {
|
|
4739
|
+
return `under "${f.escape?.link}", a link to "${f.escape?.resolvesTo}" outside "${resolve4(targetDir)}"`;
|
|
4740
|
+
}
|
|
4741
|
+
function printDryRun2(name, plan, options, targetDir) {
|
|
4635
4742
|
for (const f of plan.files) {
|
|
4636
|
-
if (f.state === "
|
|
4743
|
+
if (f.state === "linked") {
|
|
4744
|
+
console.log(` [dry-run] Would refuse: ${f.file} (${describeLink(f, targetDir)})`);
|
|
4745
|
+
} else if (f.state === "missing") {
|
|
4637
4746
|
console.log(` [dry-run] Would copy: ${f.file}`);
|
|
4638
4747
|
} else if (f.state === "directory" && !(plan.component.skipExisting && !options.force)) {
|
|
4639
4748
|
console.log(` [dry-run] Would refuse: ${f.file} (exists, is a directory) \u2014 remove it first`);
|
|
@@ -4647,6 +4756,12 @@ function printDryRun2(name, plan, options) {
|
|
|
4647
4756
|
console.log(` [dry-run] Would refuse: ${f.file} (exists, ${describeExisting(f)}) \u2014 needs --force`);
|
|
4648
4757
|
}
|
|
4649
4758
|
}
|
|
4759
|
+
const linked = linkedConflict(plan);
|
|
4760
|
+
if (linked) {
|
|
4761
|
+
console.log(`
|
|
4762
|
+
[dry-run] Would refuse component "${name}": "${linked.file}" is ${describeLink(linked, targetDir)}. Replace the link with a directory first.`);
|
|
4763
|
+
return;
|
|
4764
|
+
}
|
|
4650
4765
|
const dir = directoryConflict(plan);
|
|
4651
4766
|
if (dir) {
|
|
4652
4767
|
console.log(`
|
|
@@ -4683,9 +4798,16 @@ async function addOne(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
4683
4798
|
}
|
|
4684
4799
|
const plan = await planComponentInstall(component, targetDir, assetsDir, { force: options.force });
|
|
4685
4800
|
if (options.dryRun) {
|
|
4686
|
-
printDryRun2(name, plan, options);
|
|
4801
|
+
printDryRun2(name, plan, options, targetDir);
|
|
4687
4802
|
return plan.conflicts.length > 0 ? "failed" : "added";
|
|
4688
4803
|
}
|
|
4804
|
+
const linked = linkedConflict(plan);
|
|
4805
|
+
if (linked) {
|
|
4806
|
+
console.error(
|
|
4807
|
+
`Cannot add "${name}": "${linked.file}" is ${describeLink(linked, targetDir)}. Nothing was written. Replace the link with a directory, then run spell add again.`
|
|
4808
|
+
);
|
|
4809
|
+
return "failed";
|
|
4810
|
+
}
|
|
4689
4811
|
const dir = directoryConflict(plan);
|
|
4690
4812
|
if (dir) {
|
|
4691
4813
|
console.error(
|
|
@@ -4752,7 +4874,7 @@ async function runAdd(names, options, targetDir, assetsDir, packageVersion) {
|
|
|
4752
4874
|
}
|
|
4753
4875
|
|
|
4754
4876
|
// src/commands/update.ts
|
|
4755
|
-
import { dirname as
|
|
4877
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
4756
4878
|
import { readFile as readFile9 } from "fs/promises";
|
|
4757
4879
|
|
|
4758
4880
|
// src/modules/merge3.ts
|
|
@@ -4984,6 +5106,7 @@ function formatRegistryChanges(changes) {
|
|
|
4984
5106
|
}
|
|
4985
5107
|
|
|
4986
5108
|
// src/modules/update-installs.ts
|
|
5109
|
+
import { resolve as resolve5 } from "path";
|
|
4987
5110
|
import { checkbox as checkbox2 } from "@inquirer/prompts";
|
|
4988
5111
|
function offerableNewComponents(newlyAvailable, missingRequires) {
|
|
4989
5112
|
return newlyAvailable.filter(
|
|
@@ -5012,7 +5135,7 @@ async function planUpdateInstalls(args) {
|
|
|
5012
5135
|
if (component.files.length === 0) continue;
|
|
5013
5136
|
const plan = await planComponentInstall(component, args.targetDir, args.assetsDir, {});
|
|
5014
5137
|
if (plan.conflicts.length > 0) {
|
|
5015
|
-
leftAlone.push({ name, kind: "prerequisite", reason: conflictReason(plan) });
|
|
5138
|
+
leftAlone.push({ name, kind: "prerequisite", reason: conflictReason(plan, args.targetDir) });
|
|
5016
5139
|
continue;
|
|
5017
5140
|
}
|
|
5018
5141
|
items.push({ plan, kind: "prerequisite", requiredBy });
|
|
@@ -5022,14 +5145,18 @@ async function planUpdateInstalls(args) {
|
|
|
5022
5145
|
if (component.initOnly || component.files.length === 0) continue;
|
|
5023
5146
|
const plan = await planComponentInstall(component, args.targetDir, args.assetsDir, {});
|
|
5024
5147
|
if (plan.conflicts.length > 0) {
|
|
5025
|
-
leftAlone.push({ name: candidate.name, kind: "new", reason: conflictReason(plan) });
|
|
5148
|
+
leftAlone.push({ name: candidate.name, kind: "new", reason: conflictReason(plan, args.targetDir) });
|
|
5026
5149
|
continue;
|
|
5027
5150
|
}
|
|
5028
5151
|
items.push({ plan, kind: "new", requiredBy: [] });
|
|
5029
5152
|
}
|
|
5030
5153
|
return { items, leftAlone };
|
|
5031
5154
|
}
|
|
5032
|
-
function conflictReason(plan) {
|
|
5155
|
+
function conflictReason(plan, targetDir) {
|
|
5156
|
+
const linked = plan.conflicts.find((f) => f.state === "linked");
|
|
5157
|
+
if (linked) {
|
|
5158
|
+
return `${linked.file} is under "${linked.escape?.link}", a link to "${linked.escape?.resolvesTo}" outside "${resolve5(targetDir)}", and update writes nothing there`;
|
|
5159
|
+
}
|
|
5033
5160
|
const first = plan.conflicts[0];
|
|
5034
5161
|
if (first.state === "directory") return `${first.file} is a directory, which update does not replace`;
|
|
5035
5162
|
const more = plan.conflicts.length > 1 ? ` (and ${plan.conflicts.length - 1} more)` : "";
|
|
@@ -5182,13 +5309,40 @@ function moveComponentFiles(components, present = () => true) {
|
|
|
5182
5309
|
}
|
|
5183
5310
|
return out;
|
|
5184
5311
|
}
|
|
5185
|
-
async function
|
|
5312
|
+
async function plannedUpdateWrites(installed, scope, spellScope, assetsDir) {
|
|
5313
|
+
const files = [];
|
|
5314
|
+
for (const entry of migrateLegacyComponents(installed).components) {
|
|
5315
|
+
let component;
|
|
5316
|
+
try {
|
|
5317
|
+
component = componentForSpellScope(
|
|
5318
|
+
componentForScope(getComponent(entry.name), scope),
|
|
5319
|
+
scope === "repo" ? spellScope : "repo"
|
|
5320
|
+
);
|
|
5321
|
+
} catch (err) {
|
|
5322
|
+
if (err instanceof ComponentNotFoundError) continue;
|
|
5323
|
+
throw err;
|
|
5324
|
+
}
|
|
5325
|
+
files.push(...component.files);
|
|
5326
|
+
for (const dir of component.directories ?? []) {
|
|
5327
|
+
files.push(...await listDirectoryFiles(join11(assetsDir, dir), dir));
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
return files;
|
|
5331
|
+
}
|
|
5332
|
+
async function resolveOrphan(targetDir, file, recordedHash, prune, followDirectoryLinks = false) {
|
|
5186
5333
|
try {
|
|
5187
5334
|
validateTargetPath(targetDir, file);
|
|
5188
5335
|
} catch {
|
|
5189
5336
|
console.log(` ! Refused (path escapes the target directory): ${file}`);
|
|
5190
5337
|
return "reported";
|
|
5191
5338
|
}
|
|
5339
|
+
const escape = followDirectoryLinks ? null : findLinkedEscape(targetDir, dirname7(file));
|
|
5340
|
+
if (escape !== null) {
|
|
5341
|
+
console.log(
|
|
5342
|
+
` ! Refused (under "${escape.link}", a link to "${escape.resolvesTo}" outside the target directory): ${file}`
|
|
5343
|
+
);
|
|
5344
|
+
return "reported";
|
|
5345
|
+
}
|
|
5192
5346
|
const filePath = join11(targetDir, file);
|
|
5193
5347
|
if (!await fileExists(filePath)) return "not-found";
|
|
5194
5348
|
if (!prune) return "reported";
|
|
@@ -5200,8 +5354,8 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
5200
5354
|
);
|
|
5201
5355
|
return "reported";
|
|
5202
5356
|
}
|
|
5203
|
-
await removeWithin(targetDir, file);
|
|
5204
|
-
await removeEmptyAncestors(targetDir, file);
|
|
5357
|
+
await removeWithin(targetDir, file, { followDirectoryLinks });
|
|
5358
|
+
await removeEmptyAncestors(targetDir, file, { followDirectoryLinks });
|
|
5205
5359
|
console.log(` Pruned orphaned file (no recorded hash; git shows it ${evidence.detail}): ${file}`);
|
|
5206
5360
|
return "pruned";
|
|
5207
5361
|
}
|
|
@@ -5209,8 +5363,8 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
5209
5363
|
console.log(` ! Orphaned but edited since install \u2014 not pruning: ${file}`);
|
|
5210
5364
|
return "reported";
|
|
5211
5365
|
}
|
|
5212
|
-
await removeWithin(targetDir, file);
|
|
5213
|
-
await removeEmptyAncestors(targetDir, file);
|
|
5366
|
+
await removeWithin(targetDir, file, { followDirectoryLinks });
|
|
5367
|
+
await removeEmptyAncestors(targetDir, file, { followDirectoryLinks });
|
|
5214
5368
|
console.log(` Pruned orphaned file: ${file}`);
|
|
5215
5369
|
return "pruned";
|
|
5216
5370
|
}
|
|
@@ -5346,7 +5500,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5346
5500
|
const answered = { ...manifest, ...flagPatch };
|
|
5347
5501
|
const spellScope = effectiveSpellScope(manifest);
|
|
5348
5502
|
const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
|
|
5349
|
-
const homeDir =
|
|
5503
|
+
const homeDir = dirname7(targetDir);
|
|
5350
5504
|
const tierGate = await missingTierWarning(scope, spellScope);
|
|
5351
5505
|
if (tierGate.length > 0) {
|
|
5352
5506
|
for (const line of tierGate) console.error(line);
|
|
@@ -5388,6 +5542,18 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5388
5542
|
}
|
|
5389
5543
|
const linkTracked = scope === "repo" ? await listSymlinkTrackedPaths(targetDir) : /* @__PURE__ */ new Set();
|
|
5390
5544
|
const conversions = [];
|
|
5545
|
+
const followDirectoryLinks = scope === "user";
|
|
5546
|
+
const linkedEscapes = followDirectoryLinks || manifest.components.length === 0 ? [] : findLinkedEscapes(targetDir, await plannedUpdateWrites(manifest.components, scope, spellScope, assetsDir));
|
|
5547
|
+
const refusedForLinks = () => {
|
|
5548
|
+
if (linkedEscapes.length === 0) return false;
|
|
5549
|
+
for (const line of describeLinkedEscapes(targetDir, linkedEscapes)) console.error(line);
|
|
5550
|
+
console.error(
|
|
5551
|
+
" \u2717 Update refused: nothing was written. Arcane writes and deletes managed files only inside its target directory: replace each link named above with a directory, then run `spell update` again."
|
|
5552
|
+
);
|
|
5553
|
+
process.exit(1);
|
|
5554
|
+
return true;
|
|
5555
|
+
};
|
|
5556
|
+
if (manifest.version !== packageVersion && refusedForLinks()) return;
|
|
5391
5557
|
const registryChanges = {
|
|
5392
5558
|
retired: findTrackedRetirements(manifest.components),
|
|
5393
5559
|
...scope === "repo" ? {
|
|
@@ -5466,6 +5632,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5466
5632
|
}
|
|
5467
5633
|
return;
|
|
5468
5634
|
}
|
|
5635
|
+
if (refusedForLinks()) return;
|
|
5469
5636
|
sameVersionRestore = true;
|
|
5470
5637
|
const prefix = options.dryRun ? "[dry-run] " : "";
|
|
5471
5638
|
if (missing.length > 0) {
|
|
@@ -5528,7 +5695,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5528
5695
|
targetDir,
|
|
5529
5696
|
file,
|
|
5530
5697
|
recordedHash,
|
|
5531
|
-
Boolean(options.prune) && !options.dryRun
|
|
5698
|
+
Boolean(options.prune) && !options.dryRun,
|
|
5699
|
+
followDirectoryLinks
|
|
5532
5700
|
);
|
|
5533
5701
|
orphanReport.push({ file, status });
|
|
5534
5702
|
if (status === "reported") stillTracked.push(file);
|
|
@@ -5618,7 +5786,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5618
5786
|
readFile9(srcPath, "utf-8")
|
|
5619
5787
|
]);
|
|
5620
5788
|
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
5621
|
-
await writeManagedFile(targetDir, file, result.content, { linkTracked, conversions });
|
|
5789
|
+
await writeManagedFile(targetDir, file, result.content, { linkTracked, conversions, followDirectoryLinks });
|
|
5622
5790
|
fileHashes[file] = await hashFile(srcPath);
|
|
5623
5791
|
if (result.hasConflict) {
|
|
5624
5792
|
conflictedFiles.push(file);
|
|
@@ -5630,7 +5798,12 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5630
5798
|
}
|
|
5631
5799
|
}
|
|
5632
5800
|
if (!handled) {
|
|
5633
|
-
fileHashes[file] = await copyFile(srcPath, targetDir, file, {
|
|
5801
|
+
fileHashes[file] = await copyFile(srcPath, targetDir, file, {
|
|
5802
|
+
force: true,
|
|
5803
|
+
linkTracked,
|
|
5804
|
+
conversions,
|
|
5805
|
+
followDirectoryLinks
|
|
5806
|
+
});
|
|
5634
5807
|
if (linkShape === "path-body") conversions.push({ file, shape: "path-body" });
|
|
5635
5808
|
if (sameVersionRestore) console.log(` Restored missing: ${file}`);
|
|
5636
5809
|
}
|
|
@@ -5645,7 +5818,12 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5645
5818
|
if (options.dryRun) {
|
|
5646
5819
|
console.log(` [dry-run] Would update directory: ${dir}/`);
|
|
5647
5820
|
} else {
|
|
5648
|
-
const copied = await copyDirectory(srcDirPath, targetDir, dir, {
|
|
5821
|
+
const copied = await copyDirectory(srcDirPath, targetDir, dir, {
|
|
5822
|
+
force: true,
|
|
5823
|
+
linkTracked,
|
|
5824
|
+
conversions,
|
|
5825
|
+
followDirectoryLinks
|
|
5826
|
+
});
|
|
5649
5827
|
for (const { path: copiedPath, hash } of copied) {
|
|
5650
5828
|
updatedFiles.push(copiedPath);
|
|
5651
5829
|
fileHashes[copiedPath] = hash;
|
|
@@ -5678,7 +5856,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5678
5856
|
targetDir,
|
|
5679
5857
|
file,
|
|
5680
5858
|
recordedHash,
|
|
5681
|
-
Boolean(options.prune) && !options.dryRun
|
|
5859
|
+
Boolean(options.prune) && !options.dryRun,
|
|
5860
|
+
followDirectoryLinks
|
|
5682
5861
|
);
|
|
5683
5862
|
orphanReport.push({ file, status });
|
|
5684
5863
|
if (status !== "reported") continue;
|
|
@@ -5816,11 +5995,11 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5816
5995
|
}
|
|
5817
5996
|
|
|
5818
5997
|
// src/commands/status.ts
|
|
5819
|
-
import { dirname as
|
|
5998
|
+
import { dirname as dirname9 } from "path";
|
|
5820
5999
|
|
|
5821
6000
|
// src/modules/version-check.ts
|
|
5822
6001
|
import { readFile as readFile10, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
|
|
5823
|
-
import { dirname as
|
|
6002
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
5824
6003
|
import { homedir as homedir3 } from "os";
|
|
5825
6004
|
import { createRequire } from "module";
|
|
5826
6005
|
var require2 = createRequire(import.meta.url);
|
|
@@ -5849,7 +6028,7 @@ async function readCache(cachePath) {
|
|
|
5849
6028
|
}
|
|
5850
6029
|
}
|
|
5851
6030
|
async function writeCache(cachePath, latest) {
|
|
5852
|
-
await mkdir6(
|
|
6031
|
+
await mkdir6(dirname8(cachePath), { recursive: true });
|
|
5853
6032
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5854
6033
|
await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
5855
6034
|
}
|
|
@@ -5990,7 +6169,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
5990
6169
|
console.log("");
|
|
5991
6170
|
if (scope === "user") {
|
|
5992
6171
|
console.log(` Scope: user \u2014 ${targetDir}`);
|
|
5993
|
-
const health = await inspectUserTierFanout(
|
|
6172
|
+
const health = await inspectUserTierFanout(dirname9(targetDir), manifest.fanout);
|
|
5994
6173
|
console.log(
|
|
5995
6174
|
` Client files: ${health.total} (${health.byClient.codex} ${FANOUT_CLIENT_LABELS.codex}, ${health.byClient.claude} ${FANOUT_CLIENT_LABELS.claude})`
|
|
5996
6175
|
);
|
|
@@ -6053,7 +6232,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
6053
6232
|
|
|
6054
6233
|
// src/commands/uninstall.ts
|
|
6055
6234
|
import { rm as rm5 } from "fs/promises";
|
|
6056
|
-
import { dirname as
|
|
6235
|
+
import { dirname as dirname10, join as join13, resolve as resolve6 } from "path";
|
|
6057
6236
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
6058
6237
|
async function runUninstall(options, targetDir) {
|
|
6059
6238
|
let manifest;
|
|
@@ -6124,35 +6303,48 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
6124
6303
|
}
|
|
6125
6304
|
}
|
|
6126
6305
|
let removed = 0;
|
|
6306
|
+
let leftBehindLinks = 0;
|
|
6307
|
+
const skip = (path4, err) => {
|
|
6308
|
+
if (err instanceof LinkedDirectoryError) leftBehindLinks++;
|
|
6309
|
+
console.log(` ! Skipped ${path4}: ${skipReason(err, targetDir)}`);
|
|
6310
|
+
};
|
|
6127
6311
|
for (const component of manifest.components) {
|
|
6128
6312
|
for (const file of component.files) {
|
|
6129
6313
|
try {
|
|
6130
6314
|
await removeWithin(targetDir, file);
|
|
6131
6315
|
removed++;
|
|
6132
6316
|
} catch (err) {
|
|
6133
|
-
|
|
6134
|
-
` ! Skipped ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
6135
|
-
);
|
|
6317
|
+
skip(file, err);
|
|
6136
6318
|
}
|
|
6137
6319
|
}
|
|
6138
6320
|
}
|
|
6139
6321
|
await rm5(join13(targetDir, ".arcane.json"), { force: true });
|
|
6140
|
-
const
|
|
6322
|
+
const agentOutputs = [
|
|
6141
6323
|
".github/agents",
|
|
6142
6324
|
".arcane/agents",
|
|
6143
|
-
".arcane/generated"
|
|
6325
|
+
".arcane/generated",
|
|
6326
|
+
".arcane/agents.yaml"
|
|
6144
6327
|
];
|
|
6145
|
-
for (const
|
|
6146
|
-
|
|
6328
|
+
for (const output of agentOutputs) {
|
|
6329
|
+
try {
|
|
6330
|
+
validateRealTargetPath(targetDir, output);
|
|
6331
|
+
} catch (err) {
|
|
6332
|
+
if (await fileExists(join13(targetDir, output))) skip(output, err);
|
|
6333
|
+
continue;
|
|
6334
|
+
}
|
|
6335
|
+
await rm5(join13(targetDir, output), { recursive: true, force: true });
|
|
6147
6336
|
}
|
|
6148
|
-
await rm5(join13(targetDir, ".arcane/agents.yaml"), { force: true });
|
|
6149
6337
|
const markerFiles = [
|
|
6150
6338
|
"CLAUDE.md",
|
|
6151
6339
|
"AGENTS.md",
|
|
6152
6340
|
".github/copilot-instructions.md"
|
|
6153
6341
|
];
|
|
6154
6342
|
for (const file of markerFiles) {
|
|
6155
|
-
|
|
6343
|
+
try {
|
|
6344
|
+
await stripMarkerSection(targetDir, file, { dryRun: false });
|
|
6345
|
+
} catch (err) {
|
|
6346
|
+
skip(file, err);
|
|
6347
|
+
}
|
|
6156
6348
|
}
|
|
6157
6349
|
try {
|
|
6158
6350
|
await removeSecretsPrecommitHook(targetDir);
|
|
@@ -6160,9 +6352,20 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
6160
6352
|
}
|
|
6161
6353
|
console.log(`
|
|
6162
6354
|
\u2713 Uninstalled \u2014 ${removed} files removed.`);
|
|
6355
|
+
if (leftBehindLinks > 0) {
|
|
6356
|
+
console.log(
|
|
6357
|
+
` ! Left ${leftBehindLinks} path(s) where a link outside "${resolve6(targetDir)}" points (named above): Arcane deletes nothing outside it.`
|
|
6358
|
+
);
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
function skipReason(err, targetDir) {
|
|
6362
|
+
if (err instanceof LinkedDirectoryError) {
|
|
6363
|
+
return `it is under "${err.escape.link}", a link to "${err.escape.resolvesTo}" outside "${resolve6(targetDir)}"; nothing there is deleted.`;
|
|
6364
|
+
}
|
|
6365
|
+
return err instanceof Error ? err.message : String(err);
|
|
6163
6366
|
}
|
|
6164
6367
|
async function uninstallUserTier(options, storeRoot, manifest) {
|
|
6165
|
-
const homeDir =
|
|
6368
|
+
const homeDir = dirname10(storeRoot);
|
|
6166
6369
|
const storeFiles = manifest.components.flatMap((c) => c.files);
|
|
6167
6370
|
const recordedCount = Object.keys(manifest.fanout ?? {}).length;
|
|
6168
6371
|
if (options.dryRun) {
|
|
@@ -6201,7 +6404,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
6201
6404
|
let removed = 0;
|
|
6202
6405
|
for (const file of storeFiles) {
|
|
6203
6406
|
try {
|
|
6204
|
-
await removeWithin(storeRoot, file);
|
|
6407
|
+
await removeWithin(storeRoot, file, { followDirectoryLinks: true });
|
|
6205
6408
|
removed++;
|
|
6206
6409
|
} catch (err) {
|
|
6207
6410
|
console.log(` ! Skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -6223,7 +6426,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
6223
6426
|
|
|
6224
6427
|
// src/commands/doctor.ts
|
|
6225
6428
|
import { access as access4, readdir as readdir9, readFile as readFile13, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
6226
|
-
import { join as join16, dirname as
|
|
6429
|
+
import { join as join16, dirname as dirname11, resolve as resolve7 } from "path";
|
|
6227
6430
|
|
|
6228
6431
|
// src/config/incidents.ts
|
|
6229
6432
|
var INCIDENT_QUEUE = [
|
|
@@ -6370,7 +6573,7 @@ import { existsSync, readFileSync } from "fs";
|
|
|
6370
6573
|
import { win32 } from "path";
|
|
6371
6574
|
var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
|
|
6372
6575
|
function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
6373
|
-
return new Promise((
|
|
6576
|
+
return new Promise((resolve10, reject) => {
|
|
6374
6577
|
const child = execFile2(
|
|
6375
6578
|
file,
|
|
6376
6579
|
args,
|
|
@@ -6386,7 +6589,7 @@ function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
|
6386
6589
|
reject(error);
|
|
6387
6590
|
return;
|
|
6388
6591
|
}
|
|
6389
|
-
|
|
6592
|
+
resolve10({ stdout, stderr });
|
|
6390
6593
|
}
|
|
6391
6594
|
);
|
|
6392
6595
|
child.stdin?.end();
|
|
@@ -6910,8 +7113,15 @@ async function fixSessionContinuity(targetDir, assetsDir) {
|
|
|
6910
7113
|
try {
|
|
6911
7114
|
await access4(destPath);
|
|
6912
7115
|
} catch {
|
|
7116
|
+
const escape = findLinkedEscape(targetDir, dirname11(file));
|
|
7117
|
+
if (escape !== null) {
|
|
7118
|
+
console.log(
|
|
7119
|
+
` ! Not created: ${file} \u2014 it is under "${escape.link}", a link to "${escape.resolvesTo}" outside "${resolve7(targetDir)}".`
|
|
7120
|
+
);
|
|
7121
|
+
continue;
|
|
7122
|
+
}
|
|
6913
7123
|
const srcPath = join16(assetsDir, file);
|
|
6914
|
-
await mkdir7(
|
|
7124
|
+
await mkdir7(dirname11(destPath), { recursive: true });
|
|
6915
7125
|
await fsCopyFile(srcPath, destPath);
|
|
6916
7126
|
created.push(file);
|
|
6917
7127
|
}
|
|
@@ -7459,10 +7669,17 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
7459
7669
|
if (options.fix && hasMissingSession && assetsDir) {
|
|
7460
7670
|
console.log(" \u{1F527} Fixing missing session continuity files...\n");
|
|
7461
7671
|
const created = await fixSessionContinuity(targetDir, assetsDir);
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7672
|
+
for (const file of created) {
|
|
7673
|
+
console.log(` \u2713 Created: ${file}`);
|
|
7674
|
+
}
|
|
7675
|
+
const stillMissing = (await checkSessionContinuity(targetDir)).filter((r) => !r.passed).length;
|
|
7676
|
+
if (stillMissing > 0) {
|
|
7677
|
+
console.log(
|
|
7678
|
+
`
|
|
7679
|
+
${created.length} file(s) created; ${stillMissing} still missing (named above). Repo is not close-session-ready yet.
|
|
7680
|
+
`
|
|
7681
|
+
);
|
|
7682
|
+
} else if (created.length > 0) {
|
|
7466
7683
|
console.log(`
|
|
7467
7684
|
${created.length} file(s) created. Repo is now close-session-ready.
|
|
7468
7685
|
`);
|
|
@@ -7657,7 +7874,7 @@ async function runWardCli(targetDir, options = {}, exclusions = []) {
|
|
|
7657
7874
|
}
|
|
7658
7875
|
|
|
7659
7876
|
// src/commands/unblock-push.ts
|
|
7660
|
-
import { basename as basename3, resolve as
|
|
7877
|
+
import { basename as basename3, resolve as resolve8 } from "path";
|
|
7661
7878
|
import { input as input4 } from "@inquirer/prompts";
|
|
7662
7879
|
async function runUnblockPush(targetDir) {
|
|
7663
7880
|
let manifest;
|
|
@@ -7689,7 +7906,7 @@ async function runUnblockPush(targetDir) {
|
|
|
7689
7906
|
process.exit(1);
|
|
7690
7907
|
return;
|
|
7691
7908
|
}
|
|
7692
|
-
const repoName = basename3(
|
|
7909
|
+
const repoName = basename3(resolve8(targetDir));
|
|
7693
7910
|
if (repoName === "") {
|
|
7694
7911
|
console.error(
|
|
7695
7912
|
"Could not determine a repository name to confirm against (is this repository at a filesystem root?). Refusing to unblock."
|
|
@@ -7736,7 +7953,7 @@ Name did not match ("${typed.trim()}" \u2260 "${repoName}"). Nothing changed.`);
|
|
|
7736
7953
|
}
|
|
7737
7954
|
if (hookStillOn) {
|
|
7738
7955
|
printWarning(
|
|
7739
|
-
"The pre-push hook is still in force. Arcane did not remove it because `core.hooksPath` is not one it owns,
|
|
7956
|
+
"The pre-push hook is still in force. Arcane did not remove it because `core.hooksPath` is not one it owns, could not be read, or names a directory that a link takes outside this repository. Check `git config --show-scope --get core.hooksPath`."
|
|
7740
7957
|
);
|
|
7741
7958
|
}
|
|
7742
7959
|
if (failed.length > 0) {
|
|
@@ -8107,9 +8324,9 @@ async function browserLogin(config, options) {
|
|
|
8107
8324
|
const state = client.randomState();
|
|
8108
8325
|
const nonce = client.randomNonce();
|
|
8109
8326
|
const server = createServer();
|
|
8110
|
-
await new Promise((
|
|
8327
|
+
await new Promise((resolve10, reject) => {
|
|
8111
8328
|
server.once("error", reject);
|
|
8112
|
-
server.listen(0, "127.0.0.1", () =>
|
|
8329
|
+
server.listen(0, "127.0.0.1", () => resolve10());
|
|
8113
8330
|
});
|
|
8114
8331
|
const address = server.address();
|
|
8115
8332
|
if (address === null || typeof address === "string") {
|
|
@@ -8135,7 +8352,7 @@ async function browserLogin(config, options) {
|
|
|
8135
8352
|
server.closeAllConnections?.();
|
|
8136
8353
|
server.close();
|
|
8137
8354
|
};
|
|
8138
|
-
const tokens = new Promise((
|
|
8355
|
+
const tokens = new Promise((resolve10, reject) => {
|
|
8139
8356
|
server.on("request", (req, res) => {
|
|
8140
8357
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
8141
8358
|
if (RESULT_PATHS.has(url.pathname)) {
|
|
@@ -8146,7 +8363,7 @@ async function browserLogin(config, options) {
|
|
|
8146
8363
|
const served = browserResult;
|
|
8147
8364
|
const page = pages[served.kind];
|
|
8148
8365
|
const settle = () => {
|
|
8149
|
-
if (served.kind === "success")
|
|
8366
|
+
if (served.kind === "success") resolve10(served.result);
|
|
8150
8367
|
else reject(served.error);
|
|
8151
8368
|
};
|
|
8152
8369
|
res.once("finish", settle).once("close", settle);
|
|
@@ -8175,7 +8392,7 @@ async function browserLogin(config, options) {
|
|
|
8175
8392
|
}).end();
|
|
8176
8393
|
if (result.kind === "success") {
|
|
8177
8394
|
if (timer) clearTimeout(timer);
|
|
8178
|
-
timer = setTimeout(() =>
|
|
8395
|
+
timer = setTimeout(() => resolve10(result.result), Math.min(timeoutMs, 5e3));
|
|
8179
8396
|
}
|
|
8180
8397
|
};
|
|
8181
8398
|
const errorCode = url.searchParams.get("error");
|
|
@@ -8227,12 +8444,12 @@ async function refreshSession(config, refreshToken, scopes) {
|
|
|
8227
8444
|
}
|
|
8228
8445
|
function openInDefaultBrowser(url) {
|
|
8229
8446
|
const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
8230
|
-
return new Promise((
|
|
8447
|
+
return new Promise((resolve10) => {
|
|
8231
8448
|
try {
|
|
8232
|
-
const child = execFile3(command, args, { windowsHide: true }, (error) =>
|
|
8233
|
-
child.on("error", () =>
|
|
8449
|
+
const child = execFile3(command, args, { windowsHide: true }, (error) => resolve10(!error));
|
|
8450
|
+
child.on("error", () => resolve10(false));
|
|
8234
8451
|
} catch {
|
|
8235
|
-
|
|
8452
|
+
resolve10(false);
|
|
8236
8453
|
}
|
|
8237
8454
|
});
|
|
8238
8455
|
}
|
|
@@ -8241,7 +8458,7 @@ function openInDefaultBrowser(url) {
|
|
|
8241
8458
|
import { execFileSync } from "child_process";
|
|
8242
8459
|
import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
8243
8460
|
import { userInfo } from "os";
|
|
8244
|
-
import { dirname as
|
|
8461
|
+
import { dirname as dirname12, join as join19 } from "path";
|
|
8245
8462
|
|
|
8246
8463
|
// src/modules/keychain-backend.ts
|
|
8247
8464
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -8330,7 +8547,7 @@ var FileSecretBackend = class {
|
|
|
8330
8547
|
rmSync(this.filePath, { force: true });
|
|
8331
8548
|
return;
|
|
8332
8549
|
}
|
|
8333
|
-
mkdirSync(
|
|
8550
|
+
mkdirSync(dirname12(this.filePath), { recursive: true });
|
|
8334
8551
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
8335
8552
|
writeFileSync(tmp, JSON.stringify(entries), { encoding: "utf8", mode: 384 });
|
|
8336
8553
|
try {
|
|
@@ -8647,7 +8864,7 @@ import { join as join22 } from "path";
|
|
|
8647
8864
|
|
|
8648
8865
|
// src/modules/show-report/generate.ts
|
|
8649
8866
|
import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
|
|
8650
|
-
import { basename as basename4, dirname as
|
|
8867
|
+
import { basename as basename4, dirname as dirname13, isAbsolute, join as join21, relative as relative3, resolve as resolve9 } from "path";
|
|
8651
8868
|
|
|
8652
8869
|
// src/modules/show-report/model.ts
|
|
8653
8870
|
import { readFile as readFile17 } from "fs/promises";
|
|
@@ -9234,7 +9451,7 @@ async function programFromPlan(rootDir, slug, planRelPath) {
|
|
|
9234
9451
|
program: {
|
|
9235
9452
|
slug,
|
|
9236
9453
|
planRelPath,
|
|
9237
|
-
queueRelPath: toPosix(join21(
|
|
9454
|
+
queueRelPath: toPosix(join21(dirname13(planRelPath), "OPERATOR-QUEUE.md")),
|
|
9238
9455
|
ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
|
|
9239
9456
|
}
|
|
9240
9457
|
};
|
|
@@ -9258,9 +9475,9 @@ async function discoverPrograms(rootDir) {
|
|
|
9258
9475
|
return { programs, skipped };
|
|
9259
9476
|
}
|
|
9260
9477
|
async function programFromPlanPath(rootDir, planPath) {
|
|
9261
|
-
const planAbs = isAbsolute(planPath) ? planPath :
|
|
9478
|
+
const planAbs = isAbsolute(planPath) ? planPath : resolve9(rootDir, planPath);
|
|
9262
9479
|
const planRelPath = toPosix(relative3(rootDir, planAbs));
|
|
9263
|
-
return programFromPlan(rootDir, basename4(
|
|
9480
|
+
return programFromPlan(rootDir, basename4(dirname13(planAbs)), planRelPath);
|
|
9264
9481
|
}
|
|
9265
9482
|
function normalizeLineEndings2(content) {
|
|
9266
9483
|
return content.replace(/\r\n?/g, "\n");
|
|
@@ -9305,7 +9522,7 @@ async function runReportGeneration(options) {
|
|
|
9305
9522
|
if (!generated.themeApplied) themeSkipped.push(program2.slug);
|
|
9306
9523
|
const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
|
|
9307
9524
|
if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
|
|
9308
|
-
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir :
|
|
9525
|
+
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve9(options.rootDir, options.outDir) : resolve9(options.rootDir, dirname13(program2.planRelPath));
|
|
9309
9526
|
const outRel = toPosix(relative3(options.rootDir, outAbs));
|
|
9310
9527
|
const outputs = [
|
|
9311
9528
|
[`${outRel}/show-report.json`, generated.json],
|
|
@@ -9405,7 +9622,7 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
9405
9622
|
import { realpathSync as realpathSync2 } from "fs";
|
|
9406
9623
|
var require3 = createRequire3(import.meta.url);
|
|
9407
9624
|
var pkg = require3("../package.json");
|
|
9408
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join23(
|
|
9625
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join23(dirname14(fileURLToPath(import.meta.url)), "assets");
|
|
9409
9626
|
var program = new Command();
|
|
9410
9627
|
var USER_FLAG_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 spells shared by every repository on this machine, fanned out to ~/.agents/skills (Codex, Copilot) and ~/.claude/commands (Claude Code)";
|
|
9411
9628
|
var USER_TIER_AGENTS_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 one agent roster shared by every repository on this machine, rendered to ~/.copilot/agents (VS Code agent modes)";
|