arcane-cli 1.17.0 → 1.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/.arcane/governance/universal-agent-rules.md +1 -1
- package/dist/assets/.arcane/spells/spell-close-session.md +4 -3
- package/dist/assets/.arcane/spells/spell-commit-work.md +7 -5
- package/dist/assets/.arcane/spells/spell-create-pull-request.md +5 -4
- package/dist/assets/.github/prompts/spell-commit-work.prompt.md +1 -1
- package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +1 -1
- package/dist/index.js +108 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -34,7 +34,7 @@ See also: [[README]], [[DECISIONS]], [[governance/git-conventions|Git Convention
|
|
|
34
34
|
|
|
35
35
|
7. **Use wiki-links for cross-references. Enforcement: explicitly advisory prose (ARC-023) — no link-checker validates wiki-link usage or resolution across this repo's docs.** (e.g., `[[DECISIONS#ADR-NNN|Short Title]]`, `[[journal/YYYY-MM-DD-topic-slug|Session Label]]`). This enables knowledge graph visualization.
|
|
36
36
|
|
|
37
|
-
8. **Use Mermaid for diagrams. Enforcement: explicitly advisory prose (ARC-023) — no lint validates diagram block type or the generated-state-diagrams determinism convention.** (` ```mermaid ` blocks) for all flow charts, architecture diagrams, and sequence diagrams. Directory trees stay as plain code blocks. Spell output describing state the spell already computed — not authored, freehand explanatory content — additionally follows the **generated state diagrams** convention ([ARC-036](https://github.com/codemagicianhq/arcane/blob/main/DECISIONS.md#arc-036--generated-state-diagrams-deterministic-mermaid-for-computed-spell-state)): a deterministic, data-derived Mermaid diagram built only from values already in scope, never modeled or invented. Skip it for one-line outputs, speed-rule spells, or any state with only a single reading — the diagram exists to make relationships between multiple already-computed values legible, not to decorate simple output.
|
|
37
|
+
8. **Use Mermaid for diagrams. Enforcement: explicitly advisory prose (ARC-023) — no lint validates diagram block type or the generated-state-diagrams determinism convention.** (` ```mermaid ` blocks) for all flow charts, architecture diagrams, and sequence diagrams. Directory trees stay as plain code blocks. Spell output describing state the spell already computed — not authored, freehand explanatory content — additionally follows the **generated state diagrams** convention ([ARC-036](https://github.com/codemagicianhq/arcane/blob/main/DECISIONS.md#arc-036--generated-state-diagrams-deterministic-mermaid-for-computed-spell-state)): a deterministic, data-derived Mermaid diagram built only from values already in scope, never modeled or invented. Skip it for one-line outputs, speed-rule spells, or any state with only a single reading — the diagram exists to make relationships between multiple already-computed values legible, not to decorate simple output. A `gitGraph` that draws commits labels every commit `<short-sha> <summary>`: the short SHA, then two to five words taken from the commit subject already in scope, never the SHA alone, so the diagram reads without the log beside it.
|
|
38
38
|
|
|
39
39
|
9. **Journal files use date-prefix naming. Enforcement: explicitly advisory prose (ARC-023) — `checkSessionContinuity` only verifies `journal/.gitkeep` exists, not that individual journal filenames follow the date-prefix pattern.** `journal/YYYY-MM-DD-topic-slug.md` for chronological sorting.
|
|
40
40
|
|
|
@@ -266,11 +266,12 @@ Output format:
|
|
|
266
266
|
|
|
267
267
|
```mermaid
|
|
268
268
|
gitGraph
|
|
269
|
-
commit id: "<short-sha 1> succeeded"
|
|
270
|
-
commit id: "<short-sha 2> pending"
|
|
269
|
+
commit id: "<short-sha 1> <summary 1> succeeded"
|
|
270
|
+
commit id: "<short-sha 2> <summary 2> pending"
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
One commit per session commit, in order, `id` carrying its short SHA
|
|
273
|
+
One commit per session commit, in order, `id` carrying its short SHA, a two-to-five-word summary of its
|
|
274
|
+
subject (rule 8) and step 1b's classification —
|
|
274
275
|
never claim `succeeded` for anything step 1b did not confirm as such.
|
|
275
276
|
|
|
276
277
|
## Journal Updates
|
|
@@ -5,7 +5,7 @@ tagline: Conventional commits, written for you
|
|
|
5
5
|
claude_description: Use PROACTIVELY whenever committing work during a session, even if the user just says 'commit this'.
|
|
6
6
|
argument-hint: Optional focus (e.g., security, prompts, agents, infrastructure)
|
|
7
7
|
agent: agent
|
|
8
|
-
last_updated: 2026-
|
|
8
|
+
last_updated: 2026-11-04
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
## Executive Summary
|
|
@@ -312,11 +312,13 @@ trunk) or no usable remote (nothing to compare against):
|
|
|
312
312
|
gitGraph
|
|
313
313
|
commit id: "<target HEAD short-sha>"
|
|
314
314
|
branch <branch-name>
|
|
315
|
-
commit id: "<short-sha 1>"
|
|
316
|
-
commit id: "<short-sha 2>"
|
|
315
|
+
commit id: "<short-sha 1> <summary 1>"
|
|
316
|
+
commit id: "<short-sha 2> <summary 2>"
|
|
317
317
|
```
|
|
318
318
|
|
|
319
|
-
One commit per entry in the gathered commit list, in the same order
|
|
320
|
-
|
|
319
|
+
One commit per entry in the gathered commit list, in the same order, each `id` carrying the short SHA and
|
|
320
|
+
then a two-to-five-word summary of that commit's subject (`<short-sha> <summary>`, rule 8): the SHA
|
|
321
|
+
identifies the point, the words say what it is, so the diagram reads without the log beside it. Omit the
|
|
322
|
+
diagram (not just the commits) if that list is empty. Use `<target>`'s short SHA as the fork-point commit id.
|
|
321
323
|
|
|
322
324
|
**Next:** Continue work or run `spell-close-session` to finalize journal and session docs.
|
|
@@ -5,7 +5,7 @@ tagline: Open a PR with full context
|
|
|
5
5
|
claude_description: Use PROACTIVELY whenever opening a pull request for the current branch, even if the user just says 'open a PR'.
|
|
6
6
|
argument-hint: '[--draft] [--reviewers <name,...>] [--target <branch>] [--docs-only]'
|
|
7
7
|
agent: agent
|
|
8
|
-
last_updated: 2026-
|
|
8
|
+
last_updated: 2026-11-04
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
## Executive Summary
|
|
@@ -263,12 +263,13 @@ so they need no edit.
|
|
|
263
263
|
gitGraph
|
|
264
264
|
commit id: "<target HEAD short-sha>"
|
|
265
265
|
branch <branch-name>
|
|
266
|
-
commit id: "<short-sha 1>"
|
|
267
|
-
commit id: "<short-sha 2>"
|
|
266
|
+
commit id: "<short-sha 1> <summary 1>"
|
|
267
|
+
commit id: "<short-sha 2> <summary 2>"
|
|
268
268
|
```
|
|
269
269
|
|
|
270
270
|
Same shape `spell-commit-work` uses for the identical concept — reference it rather than re-deriving
|
|
271
|
-
the rule (D8): one commit per gathered log entry, target's short SHA
|
|
271
|
+
the rule (D8): one commit per gathered log entry labelled `<short-sha> <summary>` (rule 8), target's short SHA
|
|
272
|
+
as the fork point.
|
|
272
273
|
|
|
273
274
|
## Rules
|
|
274
275
|
|
|
@@ -5,7 +5,7 @@ tagline: Conventional commits, written for you
|
|
|
5
5
|
claude_description: Use PROACTIVELY whenever committing work during a session, even if the user just says 'commit this'.
|
|
6
6
|
argument-hint: Optional focus (e.g., security, prompts, agents, infrastructure)
|
|
7
7
|
agent: agent
|
|
8
|
-
last_updated: 2026-
|
|
8
|
+
last_updated: 2026-11-04
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
This prompt is the Arcane `spell-commit-work` spell. Read [`.arcane/spells/spell-commit-work.md`](../../.arcane/spells/spell-commit-work.md) and follow it as the complete workflow.
|
|
@@ -5,7 +5,7 @@ tagline: Open a PR with full context
|
|
|
5
5
|
claude_description: Use PROACTIVELY whenever opening a pull request for the current branch, even if the user just says 'open a PR'.
|
|
6
6
|
argument-hint: '[--draft] [--reviewers <name,...>] [--target <branch>] [--docs-only]'
|
|
7
7
|
agent: agent
|
|
8
|
-
last_updated: 2026-
|
|
8
|
+
last_updated: 2026-11-04
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
This prompt is the Arcane `spell-create-pull-request` spell. Read [`.arcane/spells/spell-create-pull-request.md`](../../.arcane/spells/spell-create-pull-request.md) and follow it as the complete workflow.
|
package/dist/index.js
CHANGED
|
@@ -14,7 +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
|
+
import { lstatSync, readlinkSync } from "fs";
|
|
18
18
|
import {
|
|
19
19
|
mkdir,
|
|
20
20
|
access,
|
|
@@ -406,6 +406,44 @@ function validateRealTargetDirectory(targetDir, relativeDir) {
|
|
|
406
406
|
const escape = findLinkedEscape(targetDir, relativeDir);
|
|
407
407
|
if (escape !== null) throw new LinkedDirectoryError(relativeDir, escape, targetDir);
|
|
408
408
|
}
|
|
409
|
+
var LinkedFileError = class extends LinkedDirectoryError {
|
|
410
|
+
constructor(relativePath, escape, targetDir) {
|
|
411
|
+
super(relativePath, escape, targetDir);
|
|
412
|
+
this.message = `Linked file: "${relativePath}" is a link to "${escape.resolvesTo}" outside target directory "${targetDir}". Arcane writes managed files only inside the target directory: replace the link with a regular file, then run the command again.`;
|
|
413
|
+
this.name = "LinkedFileError";
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var MAX_LINK_HOPS = 40;
|
|
417
|
+
function resolveWriteDestination(absPath) {
|
|
418
|
+
let current = absPath;
|
|
419
|
+
for (let hop = 0; hop < MAX_LINK_HOPS; hop++) {
|
|
420
|
+
let isLink;
|
|
421
|
+
try {
|
|
422
|
+
isLink = lstatSync(current).isSymbolicLink();
|
|
423
|
+
} catch {
|
|
424
|
+
return resolveRealPath(current);
|
|
425
|
+
}
|
|
426
|
+
if (!isLink) return resolveRealPath(current);
|
|
427
|
+
current = path.resolve(resolveRealPath(path.dirname(current)), readlinkSync(current));
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
function findLinkedFileEscape(targetDir, relativePath) {
|
|
432
|
+
const abs = path.resolve(targetDir, relativePath);
|
|
433
|
+
try {
|
|
434
|
+
if (!lstatSync(abs).isSymbolicLink()) return null;
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
const destination = resolveWriteDestination(abs);
|
|
439
|
+
if (destination !== null && isWithin(resolveRealPath(path.resolve(targetDir)), destination)) return null;
|
|
440
|
+
return { link: relativePath.replace(/\\/g, "/"), resolvesTo: destination ?? "a loop of links" };
|
|
441
|
+
}
|
|
442
|
+
function validateRealTargetFile(targetDir, relativePath) {
|
|
443
|
+
validateRealTargetPath(targetDir, relativePath);
|
|
444
|
+
const escape = findLinkedFileEscape(targetDir, relativePath);
|
|
445
|
+
if (escape !== null) throw new LinkedFileError(relativePath, escape, targetDir);
|
|
446
|
+
}
|
|
409
447
|
function findLinkedEscapes(targetDir, files) {
|
|
410
448
|
const byDirectory = /* @__PURE__ */ new Map();
|
|
411
449
|
const byLink = /* @__PURE__ */ new Map();
|
|
@@ -709,8 +747,9 @@ async function resolveSecretsScanExcludePrefixes(targetDir) {
|
|
|
709
747
|
}
|
|
710
748
|
return [];
|
|
711
749
|
}
|
|
712
|
-
async function writeManifest(targetDir, manifest) {
|
|
750
|
+
async function writeManifest(targetDir, manifest, opts = {}) {
|
|
713
751
|
const filePath = manifestPath(targetDir);
|
|
752
|
+
if (!opts.followLinks) validateRealTargetFile(targetDir, path2.relative(path2.resolve(targetDir), filePath));
|
|
714
753
|
let existing = "";
|
|
715
754
|
try {
|
|
716
755
|
existing = await readFile2(filePath, "utf-8");
|
|
@@ -2326,6 +2365,39 @@ function validateAgentDefinition(value, source) {
|
|
|
2326
2365
|
if (issues.length > 0) throw new AgentConfigValidationError(source, issues);
|
|
2327
2366
|
return definition;
|
|
2328
2367
|
}
|
|
2368
|
+
function hasControlCharacter(value) {
|
|
2369
|
+
for (let i = 0; i < value.length; i++) {
|
|
2370
|
+
const code = value.charCodeAt(i);
|
|
2371
|
+
if (code < 32 || code === 127) return true;
|
|
2372
|
+
}
|
|
2373
|
+
return false;
|
|
2374
|
+
}
|
|
2375
|
+
var OPENCLAW_HOME = "~/.openclaw";
|
|
2376
|
+
function requirePathSegment(value, field, issues) {
|
|
2377
|
+
if (typeof value !== "string") {
|
|
2378
|
+
requireString(value, field, issues);
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const trimmed = value.trim();
|
|
2382
|
+
if (trimmed === "" || trimmed === "." || trimmed === "..") {
|
|
2383
|
+
issues.push(`${field} must name something other than "${value}"`);
|
|
2384
|
+
} else if (hasControlCharacter(value)) {
|
|
2385
|
+
issues.push(`${field} must not contain a control character such as a newline`);
|
|
2386
|
+
} else if (/[\\/]/.test(value)) {
|
|
2387
|
+
issues.push(`${field} must not contain "/" or "\\"; it becomes a file name`);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
function requireOpenClawRoot(value, field, issues) {
|
|
2391
|
+
if (typeof value !== "string") {
|
|
2392
|
+
requireString(value, field, issues);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
const segments = value.split("/");
|
|
2396
|
+
const insideHome = (value === OPENCLAW_HOME || value.startsWith(`${OPENCLAW_HOME}/`)) && !hasControlCharacter(value) && !value.includes("\\") && !segments.slice(2).some((s) => s === "." || s === "..");
|
|
2397
|
+
if (!insideHome) {
|
|
2398
|
+
issues.push(`${field} must be "${OPENCLAW_HOME}" or a folder inside it, not "${value}"`);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2329
2401
|
function validateAgentRoster(value, source) {
|
|
2330
2402
|
const issues = [];
|
|
2331
2403
|
const parsed = requireRecord(value, "roster", issues);
|
|
@@ -2352,7 +2424,7 @@ function validateAgentRoster(value, source) {
|
|
|
2352
2424
|
const openclaw = requireRecord(roster.openclaw, "openclaw", issues);
|
|
2353
2425
|
if (openclaw) {
|
|
2354
2426
|
requireBoolean(openclaw.enabled, "openclaw.enabled", issues);
|
|
2355
|
-
|
|
2427
|
+
requireOpenClawRoot(openclaw.workspace_root, "openclaw.workspace_root", issues);
|
|
2356
2428
|
}
|
|
2357
2429
|
if (!Array.isArray(roster.roster)) {
|
|
2358
2430
|
issues.push("roster must be an array");
|
|
@@ -2361,9 +2433,14 @@ function validateAgentRoster(value, source) {
|
|
|
2361
2433
|
const item = requireRecord(entry, `roster[${index}]`, issues);
|
|
2362
2434
|
if (!item) return;
|
|
2363
2435
|
requireString(item.definition, `roster[${index}].definition`, issues);
|
|
2364
|
-
if (item.name !== null)
|
|
2365
|
-
|
|
2366
|
-
if (item.epithet !== void 0)
|
|
2436
|
+
if (item.name !== null) requirePathSegment(item.name, `roster[${index}].name`, issues);
|
|
2437
|
+
requirePathSegment(item.id, `roster[${index}].id`, issues);
|
|
2438
|
+
if (item.epithet !== void 0) {
|
|
2439
|
+
requireString(item.epithet, `roster[${index}].epithet`, issues);
|
|
2440
|
+
if (typeof item.epithet === "string" && hasControlCharacter(item.epithet)) {
|
|
2441
|
+
issues.push(`roster[${index}].epithet must not contain a control character such as a newline`);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2367
2444
|
});
|
|
2368
2445
|
}
|
|
2369
2446
|
if (issues.length > 0) throw new AgentConfigValidationError(source, issues);
|
|
@@ -2453,7 +2530,7 @@ var MalformedMarkersError = class extends Error {
|
|
|
2453
2530
|
}
|
|
2454
2531
|
};
|
|
2455
2532
|
async function mergeIntoFile(targetDir, relativePath, content, opts = {}) {
|
|
2456
|
-
|
|
2533
|
+
validateRealTargetFile(targetDir, relativePath);
|
|
2457
2534
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
2458
2535
|
let existing;
|
|
2459
2536
|
try {
|
|
@@ -2513,7 +2590,7 @@ ${MARKER_END}
|
|
|
2513
2590
|
return true;
|
|
2514
2591
|
}
|
|
2515
2592
|
async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
2516
|
-
|
|
2593
|
+
validateRealTargetFile(targetDir, relativePath);
|
|
2517
2594
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
2518
2595
|
let existing;
|
|
2519
2596
|
try {
|
|
@@ -2538,6 +2615,7 @@ async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
|
2538
2615
|
}
|
|
2539
2616
|
|
|
2540
2617
|
// src/modules/agent-generator.ts
|
|
2618
|
+
var OPENCLAW_WORKSPACE_FILES = ["IDENTITY.md", "SOUL.md", "TOOLS.md"];
|
|
2541
2619
|
function renderIdentity(def, name) {
|
|
2542
2620
|
const mottosLine = def.persona.catchphrases && def.persona.catchphrases.length > 0 ? `
|
|
2543
2621
|
## Mottos
|
|
@@ -2727,6 +2805,12 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2727
2805
|
if (!def.clients.openclaw) continue;
|
|
2728
2806
|
const workspaceName = entry.id === "main" ? "workspace" : `workspace-${entry.id}`;
|
|
2729
2807
|
const workspaceDir = join4(openclawRoot, workspaceName);
|
|
2808
|
+
if (!options.dryRun) {
|
|
2809
|
+
validateRealTargetDirectory(openclawRoot, workspaceName);
|
|
2810
|
+
for (const file of OPENCLAW_WORKSPACE_FILES) {
|
|
2811
|
+
validateRealTargetFile(openclawRoot, `${workspaceName}/${file}`);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2730
2814
|
try {
|
|
2731
2815
|
if (!options.dryRun) {
|
|
2732
2816
|
await mkdir3(workspaceDir, { recursive: true });
|
|
@@ -2769,6 +2853,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2769
2853
|
const generatedDir = join4(targetDir, ".arcane", "generated");
|
|
2770
2854
|
if (!options.dryRun) {
|
|
2771
2855
|
validateRealTargetDirectory(targetDir, ".arcane/generated");
|
|
2856
|
+
validateRealTargetFile(targetDir, ".arcane/generated/openclaw-roster.json");
|
|
2772
2857
|
await mkdir3(generatedDir, { recursive: true });
|
|
2773
2858
|
await writeFile5(
|
|
2774
2859
|
join4(generatedDir, "openclaw-roster.json"),
|
|
@@ -2801,7 +2886,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2801
2886
|
}
|
|
2802
2887
|
if (repoOptedOut) continue;
|
|
2803
2888
|
if (!options.dryRun) {
|
|
2804
|
-
|
|
2889
|
+
validateRealTargetFile(targetDir, `.github/agents/${fileName}`);
|
|
2805
2890
|
await writeFile5(join4(agentsDir, fileName), content, "utf8");
|
|
2806
2891
|
} else {
|
|
2807
2892
|
console.log(` [dry-run] Would write: .github/agents/${fileName}`);
|
|
@@ -2919,7 +3004,7 @@ async function performAgentSync(targetDir, assetsDir, roster, options) {
|
|
|
2919
3004
|
console.log(` ${line}`);
|
|
2920
3005
|
}
|
|
2921
3006
|
if (!options.dryRun) {
|
|
2922
|
-
await writeManifest(targetDir, { ...manifest, fanout: fanout.record });
|
|
3007
|
+
await writeManifest(targetDir, { ...manifest, fanout: fanout.record }, { followLinks: true });
|
|
2923
3008
|
}
|
|
2924
3009
|
}
|
|
2925
3010
|
if (result.unmanagedAgentFiles.length > 0) {
|
|
@@ -3041,7 +3126,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
3041
3126
|
if (!options.dryRun) {
|
|
3042
3127
|
if (scope === "repo") {
|
|
3043
3128
|
validateRealTargetDirectory(targetDir, ".arcane/agents");
|
|
3044
|
-
|
|
3129
|
+
validateRealTargetFile(targetDir, ".arcane/agents.yaml");
|
|
3045
3130
|
}
|
|
3046
3131
|
await mkdir4(arcaneDir, { recursive: true });
|
|
3047
3132
|
await writeFile6(join5(baseDir, "agents.yaml"), stringify(roster), "utf8");
|
|
@@ -3052,6 +3137,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
3052
3137
|
const src = join5(assetsDir, "agents", `${roleId}.yaml`);
|
|
3053
3138
|
const dest = join5(arcaneDir, `${roleId}.yaml`);
|
|
3054
3139
|
if (!options.dryRun) {
|
|
3140
|
+
if (scope === "repo") validateRealTargetFile(targetDir, `.arcane/agents/${roleId}.yaml`);
|
|
3055
3141
|
try {
|
|
3056
3142
|
await copyFile2(src, dest);
|
|
3057
3143
|
} catch {
|
|
@@ -3685,6 +3771,7 @@ async function installHook(cwd, hookName, body) {
|
|
|
3685
3771
|
return { status: "already-ours" };
|
|
3686
3772
|
}
|
|
3687
3773
|
validateRealTargetDirectory(root, ARCANE_HOOKS_DIR);
|
|
3774
|
+
validateRealTargetFile(root, `${ARCANE_HOOKS_DIR}/${hookName}`);
|
|
3688
3775
|
await mkdir5(join7(root, ARCANE_HOOKS_DIR), { recursive: true });
|
|
3689
3776
|
await writeFile8(hookPath, body, "utf-8");
|
|
3690
3777
|
await chmod(hookPath, 493);
|
|
@@ -4665,7 +4752,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4665
4752
|
scope: "user",
|
|
4666
4753
|
fanout: fanout.record
|
|
4667
4754
|
};
|
|
4668
|
-
await writeManifest(storeRoot, manifest);
|
|
4755
|
+
await writeManifest(storeRoot, manifest, { followLinks: true });
|
|
4669
4756
|
printStep(`\u2728 ${fileCount} Spells installed to ${storeRoot}/spells/`);
|
|
4670
4757
|
for (const line of describeFanoutOutcomes(fanout.outcomes)) {
|
|
4671
4758
|
if (line.startsWith("!")) printWarning(line.slice(2));
|
|
@@ -5656,7 +5743,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5656
5743
|
for (const line of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) console.log(` ${line}`);
|
|
5657
5744
|
const scopePatch = defaultScopePatch(options, manifest);
|
|
5658
5745
|
if (!options.dryRun && (JSON.stringify(fanout.record) !== JSON.stringify(manifest.fanout ?? {}) || scopePatch !== void 0)) {
|
|
5659
|
-
await writeManifest(targetDir, { ...manifest, ...scopePatch, fanout: fanout.record });
|
|
5746
|
+
await writeManifest(targetDir, { ...manifest, ...scopePatch, fanout: fanout.record }, { followLinks: scope === "user" });
|
|
5660
5747
|
}
|
|
5661
5748
|
}
|
|
5662
5749
|
return;
|
|
@@ -5994,7 +6081,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5994
6081
|
...fanoutRecord !== void 0 ? { fanout: fanoutRecord } : {},
|
|
5995
6082
|
...defaultScopePatch(options, manifest)
|
|
5996
6083
|
};
|
|
5997
|
-
await writeManifest(targetDir, updated);
|
|
6084
|
+
await writeManifest(targetDir, updated, { followLinks: scope === "user" });
|
|
5998
6085
|
console.log(
|
|
5999
6086
|
`
|
|
6000
6087
|
\u2713 Updated ${fileCount} files.`
|
|
@@ -8185,6 +8272,13 @@ async function fixSessionContinuity(targetDir, assetsDir) {
|
|
|
8185
8272
|
);
|
|
8186
8273
|
continue;
|
|
8187
8274
|
}
|
|
8275
|
+
const fileEscape = findLinkedFileEscape(targetDir, file);
|
|
8276
|
+
if (fileEscape !== null) {
|
|
8277
|
+
console.log(
|
|
8278
|
+
` ! Not created: ${file} \u2014 it is a link to "${fileEscape.resolvesTo}" outside "${resolve7(targetDir)}".`
|
|
8279
|
+
);
|
|
8280
|
+
continue;
|
|
8281
|
+
}
|
|
8188
8282
|
const srcPath = join22(assetsDir, file);
|
|
8189
8283
|
await mkdir7(dirname11(destPath), { recursive: true });
|
|
8190
8284
|
await fsCopyFile(srcPath, destPath);
|