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.
Files changed (111) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/.arcane/governance/agent-approved-paths.md +1 -0
  3. package/dist/assets/.arcane/governance/agent-policies.md +1 -0
  4. package/dist/assets/.arcane/governance/agent-work-queue-model.md +1 -0
  5. package/dist/assets/.arcane/governance/authentication-strategy.md +1 -0
  6. package/dist/assets/.arcane/governance/cicd-standards.md +1 -0
  7. package/dist/assets/.arcane/governance/compliance-standards.md +1 -0
  8. package/dist/assets/.arcane/governance/decision-documentation-standard.md +1 -0
  9. package/dist/assets/.arcane/governance/development-methodology.md +1 -0
  10. package/dist/assets/.arcane/governance/external-verification-standards.md +1 -0
  11. package/dist/assets/.arcane/governance/framework-decisions.md +1 -0
  12. package/dist/assets/.arcane/governance/git-conventions.md +1 -0
  13. package/dist/assets/.arcane/governance/hardening-checklist.md +1 -0
  14. package/dist/assets/.arcane/governance/mobile-release-standards.md +1 -0
  15. package/dist/assets/.arcane/governance/naming-conventions.md +1 -0
  16. package/dist/assets/.arcane/governance/new-business-setup.md +1 -0
  17. package/dist/assets/.arcane/governance/poc-management-pattern.md +1 -0
  18. package/dist/assets/.arcane/governance/portable-bootstrap.md +1 -0
  19. package/dist/assets/.arcane/governance/product-excellence-standards.md +1 -0
  20. package/dist/assets/.arcane/governance/rca-process-standard.md +1 -0
  21. package/dist/assets/.arcane/governance/records-conventions.md +1 -0
  22. package/dist/assets/.arcane/governance/spell-authoring-standards.md +19 -1
  23. package/dist/assets/.arcane/governance/testing-standards.md +1 -0
  24. package/dist/assets/.arcane/governance/threat-model.md +1 -0
  25. package/dist/assets/.arcane/governance/universal-agent-rules.md +1 -0
  26. package/dist/assets/.arcane/governance/web-discoverability-standards.md +1 -0
  27. package/dist/assets/.arcane/spells/spell-address-review.md +1 -0
  28. package/dist/assets/.arcane/spells/spell-adopt-docs.md +1 -0
  29. package/dist/assets/.arcane/spells/spell-arcane-version.md +1 -0
  30. package/dist/assets/.arcane/spells/spell-architect.md +1 -0
  31. package/dist/assets/.arcane/spells/spell-brainstorm.md +1 -0
  32. package/dist/assets/.arcane/spells/spell-bug.md +1 -0
  33. package/dist/assets/.arcane/spells/spell-bump.md +1 -0
  34. package/dist/assets/.arcane/spells/spell-check-drift.md +1 -0
  35. package/dist/assets/.arcane/spells/spell-close-session.md +1 -0
  36. package/dist/assets/.arcane/spells/spell-commit-work.md +1 -0
  37. package/dist/assets/.arcane/spells/spell-compliance.md +1 -0
  38. package/dist/assets/.arcane/spells/spell-create-pull-request.md +1 -0
  39. package/dist/assets/.arcane/spells/spell-document.md +1 -0
  40. package/dist/assets/.arcane/spells/spell-dotnet-expert.md +1 -0
  41. package/dist/assets/.arcane/spells/spell-eas-store-deploy.md +1 -0
  42. package/dist/assets/.arcane/spells/spell-enchant.md +1 -0
  43. package/dist/assets/.arcane/spells/spell-explain-concept.md +1 -0
  44. package/dist/assets/.arcane/spells/spell-feedback.md +1 -0
  45. package/dist/assets/.arcane/spells/spell-full-cycle.md +1 -0
  46. package/dist/assets/.arcane/spells/spell-generate-bot-icons.md +1 -0
  47. package/dist/assets/.arcane/spells/spell-implement.md +1 -0
  48. package/dist/assets/.arcane/spells/spell-make-discoverable.md +1 -0
  49. package/dist/assets/.arcane/spells/spell-manifest.md +1 -0
  50. package/dist/assets/.arcane/spells/spell-open-session.md +1 -0
  51. package/dist/assets/.arcane/spells/spell-plan.md +1 -0
  52. package/dist/assets/.arcane/spells/spell-present-arcane.md +1 -0
  53. package/dist/assets/.arcane/spells/spell-product-review.md +1 -0
  54. package/dist/assets/.arcane/spells/spell-review-batch.md +1 -0
  55. package/dist/assets/.arcane/spells/spell-review.md +1 -0
  56. package/dist/assets/.arcane/spells/spell-save-idea.md +1 -0
  57. package/dist/assets/.arcane/spells/spell-scope.md +1 -0
  58. package/dist/assets/.arcane/spells/spell-scry.md +1 -0
  59. package/dist/assets/.arcane/spells/spell-security-review.md +1 -0
  60. package/dist/assets/.arcane/spells/spell-ship.md +1 -0
  61. package/dist/assets/.arcane/spells/spell-status.md +1 -0
  62. package/dist/assets/.arcane/spells/spell-suggest-feature.md +1 -0
  63. package/dist/assets/.arcane/spells/spell-summon-venture.md +1 -0
  64. package/dist/assets/.arcane/spells/spell-sync-pull-request.md +1 -0
  65. package/dist/assets/.arcane/spells/spell-test.md +1 -0
  66. package/dist/assets/.arcane/spells/spell-todo.md +1 -0
  67. package/dist/assets/.arcane/spells/spell-verification-ledger.md +1 -0
  68. package/dist/assets/.github/prompts/spell-address-review.prompt.md +1 -0
  69. package/dist/assets/.github/prompts/spell-adopt-docs.prompt.md +1 -0
  70. package/dist/assets/.github/prompts/spell-arcane-version.prompt.md +1 -0
  71. package/dist/assets/.github/prompts/spell-architect.prompt.md +1 -0
  72. package/dist/assets/.github/prompts/spell-brainstorm.prompt.md +1 -0
  73. package/dist/assets/.github/prompts/spell-bug.prompt.md +1 -0
  74. package/dist/assets/.github/prompts/spell-bump.prompt.md +1 -0
  75. package/dist/assets/.github/prompts/spell-check-drift.prompt.md +1 -0
  76. package/dist/assets/.github/prompts/spell-close-session.prompt.md +1 -0
  77. package/dist/assets/.github/prompts/spell-commit-work.prompt.md +1 -0
  78. package/dist/assets/.github/prompts/spell-compliance.prompt.md +1 -0
  79. package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +1 -0
  80. package/dist/assets/.github/prompts/spell-document.prompt.md +1 -0
  81. package/dist/assets/.github/prompts/spell-dotnet-expert.prompt.md +1 -0
  82. package/dist/assets/.github/prompts/spell-eas-store-deploy.prompt.md +1 -0
  83. package/dist/assets/.github/prompts/spell-enchant.prompt.md +1 -0
  84. package/dist/assets/.github/prompts/spell-explain-concept.prompt.md +1 -0
  85. package/dist/assets/.github/prompts/spell-feedback.prompt.md +1 -0
  86. package/dist/assets/.github/prompts/spell-full-cycle.prompt.md +1 -0
  87. package/dist/assets/.github/prompts/spell-generate-bot-icons.prompt.md +1 -0
  88. package/dist/assets/.github/prompts/spell-implement.prompt.md +1 -0
  89. package/dist/assets/.github/prompts/spell-make-discoverable.prompt.md +1 -0
  90. package/dist/assets/.github/prompts/spell-manifest.prompt.md +1 -0
  91. package/dist/assets/.github/prompts/spell-open-session.prompt.md +1 -0
  92. package/dist/assets/.github/prompts/spell-plan.prompt.md +1 -0
  93. package/dist/assets/.github/prompts/spell-present-arcane.prompt.md +1 -0
  94. package/dist/assets/.github/prompts/spell-product-review.prompt.md +1 -0
  95. package/dist/assets/.github/prompts/spell-review-batch.prompt.md +1 -0
  96. package/dist/assets/.github/prompts/spell-review.prompt.md +1 -0
  97. package/dist/assets/.github/prompts/spell-save-idea.prompt.md +1 -0
  98. package/dist/assets/.github/prompts/spell-scope.prompt.md +1 -0
  99. package/dist/assets/.github/prompts/spell-scry.prompt.md +1 -0
  100. package/dist/assets/.github/prompts/spell-security-review.prompt.md +1 -0
  101. package/dist/assets/.github/prompts/spell-ship.prompt.md +1 -0
  102. package/dist/assets/.github/prompts/spell-status.prompt.md +1 -0
  103. package/dist/assets/.github/prompts/spell-suggest-feature.prompt.md +1 -0
  104. package/dist/assets/.github/prompts/spell-summon-venture.prompt.md +1 -0
  105. package/dist/assets/.github/prompts/spell-sync-pull-request.prompt.md +1 -0
  106. package/dist/assets/.github/prompts/spell-test.prompt.md +1 -0
  107. package/dist/assets/.github/prompts/spell-todo.prompt.md +1 -0
  108. package/dist/assets/.github/prompts/spell-verification-ledger.prompt.md +1 -0
  109. package/dist/index.js +353 -136
  110. package/dist/index.js.map +1 -1
  111. 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 dirname13, join as join23 } from "path";
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((resolve6, reject) => {
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
- resolve6({ stdout, stderr });
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
- validateTargetPath(targetDir, relativePath);
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
- validateTargetPath(targetDir, relativePath);
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
- validateTargetPath(targetDir, relativePath);
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
- validateTargetPath(targetDir, relativePath);
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 join(homeDir, USER_TIER_DIR_NAME);
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 resolve(storeRoot, storeSpellPath(id)).replace(/\\/g, "/");
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
- join(storeRoot, storeSpellPath(id)),
1475
- ...fallbackDir ? [join(fallbackDir, canonicalSpellPath(id))] : []
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 dirname(join(homeDir, relativePath));
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 = join(options.homeDir, file.relativePath);
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(dirname(target), { recursive: true });
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 = join(options.homeDir, relativePath);
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 = join(homeDir, relativePath);
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 resolve(targetDir) === resolve(userTierRoot());
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((resolve6) => setTimeout(resolve6, ms));
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 dirname2, join as join4 } from "path";
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 join2 } from "path";
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 = join2(agentsDir, `${id}.yaml`);
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 : join2(targetDir, ".arcane");
2386
+ return scope === "user" ? targetDir : join3(targetDir, ".arcane");
2297
2387
  }
2298
2388
  async function loadRoster(targetDir, scope = "repo") {
2299
- const rosterPath = join2(agentsBaseDir(targetDir, scope), "agents.yaml");
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(join2(agentsBaseDir(targetDir, scope), "agents.yaml"));
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 join2(agentsBaseDir(targetDir, scope), "agents");
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 join3 } from "path";
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
- validateTargetPath(targetDir, relativePath);
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
- join3(assetsDir, ".arcane", "spells", "_fragments", "branch-naming.md"),
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 = join3(assetsDir, "agents");
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 = join3(openclawRoot, workspaceName);
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
- join3(workspaceDir, "IDENTITY.md"),
2705
+ join4(workspaceDir, "IDENTITY.md"),
2615
2706
  renderIdentity(def, displayName),
2616
2707
  "utf8"
2617
2708
  );
2618
2709
  await writeFile5(
2619
- join3(workspaceDir, "SOUL.md"),
2710
+ join4(workspaceDir, "SOUL.md"),
2620
2711
  renderSoul(def, displayName),
2621
2712
  "utf8"
2622
2713
  );
2623
2714
  await writeFile5(
2624
- join3(workspaceDir, "TOOLS.md"),
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 = join3(targetDir, ".arcane", "generated");
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
- join3(generatedDir, "openclaw-roster.json"),
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 = join3(targetDir, ".github", "agents");
2757
+ const agentsDir = join4(targetDir, ".github", "agents");
2666
2758
  if (!options.dryRun && !userTier) {
2667
- await mkdir3(repoOptedOut ? join3(targetDir, ".github") : agentsDir, { recursive: true });
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
- await writeFile5(join3(agentsDir, fileName), content, "utf8");
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: dirname2(targetDir),
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 = join4(baseDir, "agents");
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(join4(baseDir, "agents.yaml"), stringify(roster), "utf8");
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 = join4(assetsDir, "agents", `${roleId}.yaml`);
2926
- const dest = join4(arcaneDir, `${roleId}.yaml`);
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 join5 } from "path";
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 = join5(targetDir, businessRoot, "registry.json");
3489
+ const registryPath = join6(targetDir, businessRoot, "registry.json");
3392
3490
  if (await fileExists(registryPath)) return;
3393
- const businessRootPath = join5(targetDir, businessRoot);
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 printDryRun2(name, plan, options) {
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 === "missing") {
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 dirname6, join as join11 } from "path";
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 resolveOrphan(targetDir, file, recordedHash, prune) {
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 = dirname6(targetDir);
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, { force: true, linkTracked, conversions });
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, { force: true, linkTracked, conversions });
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 dirname8 } from "path";
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 dirname7, join as join12 } from "path";
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(dirname7(cachePath), { recursive: true });
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(dirname8(targetDir), manifest.fanout);
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 dirname9, join as join13 } from "path";
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
- console.log(
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 agentDirs = [
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 dir of agentDirs) {
6146
- await rm5(join13(targetDir, dir), { recursive: true, force: true });
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
- await stripMarkerSection(targetDir, file, { dryRun: false });
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 = dirname9(storeRoot);
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 dirname10 } from "path";
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((resolve6, reject) => {
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
- resolve6({ stdout, stderr });
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(dirname10(destPath), { recursive: true });
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
- if (created.length > 0) {
7463
- for (const file of created) {
7464
- console.log(` \u2713 Created: ${file}`);
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 resolve4 } from "path";
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(resolve4(targetDir));
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, or could not be read. Check `git config --show-scope --get core.hooksPath`."
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((resolve6, reject) => {
8327
+ await new Promise((resolve10, reject) => {
8111
8328
  server.once("error", reject);
8112
- server.listen(0, "127.0.0.1", () => resolve6());
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((resolve6, reject) => {
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") resolve6(served.result);
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(() => resolve6(result.result), Math.min(timeoutMs, 5e3));
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((resolve6) => {
8447
+ return new Promise((resolve10) => {
8231
8448
  try {
8232
- const child = execFile3(command, args, { windowsHide: true }, (error) => resolve6(!error));
8233
- child.on("error", () => resolve6(false));
8449
+ const child = execFile3(command, args, { windowsHide: true }, (error) => resolve10(!error));
8450
+ child.on("error", () => resolve10(false));
8234
8451
  } catch {
8235
- resolve6(false);
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 dirname11, join as join19 } from "path";
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(dirname11(this.filePath), { recursive: true });
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 dirname12, isAbsolute, join as join21, relative as relative3, resolve as resolve5 } from "path";
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(dirname12(planRelPath), "OPERATOR-QUEUE.md")),
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 : resolve5(rootDir, planPath);
9478
+ const planAbs = isAbsolute(planPath) ? planPath : resolve9(rootDir, planPath);
9262
9479
  const planRelPath = toPosix(relative3(rootDir, planAbs));
9263
- return programFromPlan(rootDir, basename4(dirname12(planAbs)), planRelPath);
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 : resolve5(options.rootDir, options.outDir) : resolve5(options.rootDir, dirname12(program2.planRelPath));
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(dirname13(fileURLToPath(import.meta.url)), "assets");
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)";