arcane-cli 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/.arcane/governance/decision-documentation-standard.md +37 -1
- package/dist/assets/.arcane/governance/development-methodology.md +20 -0
- package/dist/assets/.arcane/governance/spell-authoring-standards.md +32 -0
- package/dist/assets/.arcane/spells/_fragments/needs-you.md +14 -0
- package/dist/assets/.arcane/spells/spell-address-review.md +18 -0
- package/dist/assets/.arcane/spells/spell-adopt-docs.md +17 -0
- package/dist/assets/.arcane/spells/spell-arcane-version.md +26 -0
- package/dist/assets/.arcane/spells/spell-architect.md +17 -0
- package/dist/assets/.arcane/spells/spell-close-session.md +52 -2
- package/dist/assets/.arcane/spells/spell-commit-work.md +17 -0
- package/dist/assets/.arcane/spells/spell-create-pull-request.md +17 -0
- package/dist/assets/.arcane/spells/spell-eas-store-deploy.md +23 -0
- package/dist/assets/.arcane/spells/spell-feedback.md +9 -0
- package/dist/assets/.arcane/spells/spell-full-cycle.md +18 -0
- package/dist/assets/.arcane/spells/spell-make-discoverable.md +17 -0
- package/dist/assets/.arcane/spells/spell-manifest.md +18 -0
- package/dist/assets/.arcane/spells/spell-open-session.md +23 -0
- package/dist/assets/.arcane/spells/spell-plan.md +17 -0
- package/dist/assets/.arcane/spells/spell-review-batch.md +17 -0
- package/dist/assets/.arcane/spells/spell-review.md +18 -1
- package/dist/assets/.arcane/spells/spell-save-idea.md +2 -0
- package/dist/assets/.arcane/spells/spell-scope.md +17 -0
- package/dist/assets/.arcane/spells/spell-ship.md +17 -0
- package/dist/assets/.arcane/spells/spell-sync-pull-request.md +18 -0
- package/dist/assets/.arcane/spells/spell-todo.md +84 -2
- package/dist/assets/ai-context/system-prompt-context.md +1 -0
- package/dist/index.js +317 -64
- 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 createRequire2 } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as dirname11, join as
|
|
9
|
+
import { dirname as dirname11, join as join20 } 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";
|
|
@@ -322,7 +322,8 @@ var PROFILE_CONFIGS = [
|
|
|
322
322
|
"git-conventions",
|
|
323
323
|
"testing-standards",
|
|
324
324
|
"framework-decisions",
|
|
325
|
-
"session-continuity"
|
|
325
|
+
"session-continuity",
|
|
326
|
+
"line-ending-baseline"
|
|
326
327
|
]
|
|
327
328
|
},
|
|
328
329
|
{
|
|
@@ -346,7 +347,8 @@ var PROFILE_CONFIGS = [
|
|
|
346
347
|
"decision-documentation-standard",
|
|
347
348
|
"agent-work-queue-model",
|
|
348
349
|
"development-methodology",
|
|
349
|
-
"spell-authoring-standards"
|
|
350
|
+
"spell-authoring-standards",
|
|
351
|
+
"line-ending-baseline"
|
|
350
352
|
]
|
|
351
353
|
},
|
|
352
354
|
{
|
|
@@ -371,6 +373,7 @@ var PROFILE_CONFIGS = [
|
|
|
371
373
|
"spells-meta",
|
|
372
374
|
"spells-docs",
|
|
373
375
|
"docs-baseline",
|
|
376
|
+
"line-ending-baseline",
|
|
374
377
|
"records-conventions",
|
|
375
378
|
"agent-output-instructions",
|
|
376
379
|
"git-conventions",
|
|
@@ -417,7 +420,8 @@ var PROFILE_CONFIGS = [
|
|
|
417
420
|
"web-discoverability-standards",
|
|
418
421
|
"mobile-release-standards",
|
|
419
422
|
"records-conventions",
|
|
420
|
-
"compliance-standards"
|
|
423
|
+
"compliance-standards",
|
|
424
|
+
"line-ending-baseline"
|
|
421
425
|
]
|
|
422
426
|
}
|
|
423
427
|
];
|
|
@@ -546,15 +550,26 @@ var COMPONENTS = [
|
|
|
546
550
|
// non-dotfile path and mapped via sourceOverrides; see RegistryComponent.
|
|
547
551
|
{
|
|
548
552
|
name: "docs-baseline",
|
|
549
|
-
description: "
|
|
550
|
-
files: [".
|
|
553
|
+
description: "Ignore baseline for document repositories (.gitignore)",
|
|
554
|
+
files: [".gitignore"],
|
|
551
555
|
sourceOverrides: {
|
|
552
|
-
".gitattributes": "docs-baseline/gitattributes",
|
|
553
556
|
".gitignore": "docs-baseline/gitignore"
|
|
554
557
|
},
|
|
555
558
|
skipExisting: true,
|
|
556
559
|
initOnly: true
|
|
557
560
|
},
|
|
561
|
+
// Split out of docs-baseline so every profile ships it (#281, PRD D-09);
|
|
562
|
+
// same asset source. See COMPONENT_FILE_MOVES for manifests written before.
|
|
563
|
+
{
|
|
564
|
+
name: "line-ending-baseline",
|
|
565
|
+
description: "Line-ending and binary-format baseline (.gitattributes): text normalizes to LF, binary formats are never rewritten",
|
|
566
|
+
files: [".gitattributes"],
|
|
567
|
+
sourceOverrides: {
|
|
568
|
+
".gitattributes": "docs-baseline/gitattributes"
|
|
569
|
+
},
|
|
570
|
+
skipExisting: true,
|
|
571
|
+
initOnly: true
|
|
572
|
+
},
|
|
558
573
|
// Optional MCP config scaffold (BC-22/I12). skipExisting: a repo's own
|
|
559
574
|
// .mcp.json is user-owned server config Arcane must never overwrite.
|
|
560
575
|
// Not initOnly -- useful to retrofit via `spell add` on an existing repo too.
|
|
@@ -782,6 +797,14 @@ var COMPONENTS = [
|
|
|
782
797
|
".github/prompts/spell-compliance.prompt.md",
|
|
783
798
|
".claude/commands/spell-compliance.md",
|
|
784
799
|
".agents/skills/spell-compliance/SKILL.md"
|
|
800
|
+
],
|
|
801
|
+
// spell-make-discoverable cites the first two for every EV-/WD- rationale;
|
|
802
|
+
// spell-eas-store-deploy and spell-compliance consume the last two.
|
|
803
|
+
requires: [
|
|
804
|
+
"external-verification-standards",
|
|
805
|
+
"web-discoverability-standards",
|
|
806
|
+
"mobile-release-standards",
|
|
807
|
+
"compliance-standards"
|
|
785
808
|
]
|
|
786
809
|
},
|
|
787
810
|
{
|
|
@@ -886,6 +909,41 @@ var LEGACY_COMPONENT_MIGRATIONS = Object.assign(/* @__PURE__ */ Object.create(nu
|
|
|
886
909
|
"spell-prompts": LEGACY_MONOLITH_REPLACEMENTS,
|
|
887
910
|
"claude-commands": LEGACY_MONOLITH_REPLACEMENTS
|
|
888
911
|
});
|
|
912
|
+
var REGISTRY_RETIREMENTS = [
|
|
913
|
+
{
|
|
914
|
+
name: "spell-bootstrap-business",
|
|
915
|
+
kind: "spell",
|
|
916
|
+
successors: ["spell-summon-venture"],
|
|
917
|
+
reason: "renamed in 0.16.0 with no alias (ARC-008 precedent) \u2014 it became a hub-gated venture creator with per-venture books and a registry entry"
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "spell-assess",
|
|
921
|
+
kind: "spell",
|
|
922
|
+
successors: ["spell-scope"],
|
|
923
|
+
reason: "renamed to name its outcome, epic scoping and sequencing (ARC-007); no alias (ARC-008)"
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
name: "agent-files",
|
|
927
|
+
kind: "component",
|
|
928
|
+
successors: [],
|
|
929
|
+
reason: "`.github/agents/*.agent.md` files are rendered per roster by `spell agents init` / `spell agents sync` (ARC-047), which a fixed-path component cannot do"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: "spell-prompts",
|
|
933
|
+
kind: "component",
|
|
934
|
+
successors: LEGACY_MONOLITH_REPLACEMENTS,
|
|
935
|
+
reason: "split in 0.18.0 into capability-scoped spells-* components; `spell update` migrates the entry itself"
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: "claude-commands",
|
|
939
|
+
kind: "component",
|
|
940
|
+
successors: LEGACY_MONOLITH_REPLACEMENTS,
|
|
941
|
+
reason: "split in 0.18.0 into capability-scoped spells-* components; `spell update` migrates the entry itself"
|
|
942
|
+
}
|
|
943
|
+
];
|
|
944
|
+
var COMPONENT_FILE_MOVES = [
|
|
945
|
+
{ from: "docs-baseline", to: "line-ending-baseline", files: [".gitattributes"] }
|
|
946
|
+
];
|
|
889
947
|
function getComponent(name) {
|
|
890
948
|
const component = componentIndex.get(name);
|
|
891
949
|
if (!component) {
|
|
@@ -3874,8 +3932,10 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
3874
3932
|
\u2726 Arcane v${packageVersion}
|
|
3875
3933
|
`));
|
|
3876
3934
|
let repoNotFound = false;
|
|
3935
|
+
let repoHasCommits = false;
|
|
3877
3936
|
if (!options.dryRun) {
|
|
3878
3937
|
const gitState = await inspectGitRepository(targetDir);
|
|
3938
|
+
repoHasCommits = gitState.status !== "not-repository" && gitState.status !== "no-commits";
|
|
3879
3939
|
if (gitState.status === "not-repository") {
|
|
3880
3940
|
repoNotFound = true;
|
|
3881
3941
|
} else {
|
|
@@ -4228,9 +4288,15 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4228
4288
|
'Initialize Git for this project: `git init -b main` (avoids defaulting to "master" on systems where that\'s the system default)'
|
|
4229
4289
|
);
|
|
4230
4290
|
}
|
|
4291
|
+
const wroteGitattributes = installedComponents.some((c) => c.files.includes(".gitattributes"));
|
|
4292
|
+
if (wroteGitattributes && repoHasCommits) {
|
|
4293
|
+
printWarning(
|
|
4294
|
+
"Installed .gitattributes, which normalizes text to LF. In a repository with history, files committed with CRLF can show as modified once Git renormalizes them (for example after `git add --renormalize .`). Review and commit that as its own change."
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4231
4297
|
printNextStep(
|
|
4232
4298
|
step++,
|
|
4233
|
-
|
|
4299
|
+
`Commit the new files: git add .arcane .github .claude AGENTS.md CLAUDE.md .arcane.json${wroteGitattributes ? " .gitattributes" : ""}`
|
|
4234
4300
|
);
|
|
4235
4301
|
console.log();
|
|
4236
4302
|
}
|
|
@@ -4422,7 +4488,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
4422
4488
|
}
|
|
4423
4489
|
|
|
4424
4490
|
// src/commands/update.ts
|
|
4425
|
-
import { dirname as dirname5, join as
|
|
4491
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
4426
4492
|
import { readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
|
|
4427
4493
|
|
|
4428
4494
|
// src/modules/merge3.ts
|
|
@@ -4553,6 +4619,101 @@ function merge3(base, ours, theirs) {
|
|
|
4553
4619
|
return { content: resultLines.join("\n"), hasConflict };
|
|
4554
4620
|
}
|
|
4555
4621
|
|
|
4622
|
+
// src/modules/registry-changes.ts
|
|
4623
|
+
import { join as join9 } from "path";
|
|
4624
|
+
function findMissingRequires(installed, profile) {
|
|
4625
|
+
const names = new Set(installed.flatMap((c) => LEGACY_COMPONENT_MIGRATIONS[c.name] ?? [c.name]));
|
|
4626
|
+
const promised = listProfiles().find((p) => p.id === profile)?.components;
|
|
4627
|
+
const missing = /* @__PURE__ */ new Map();
|
|
4628
|
+
for (const name of names) {
|
|
4629
|
+
let requires;
|
|
4630
|
+
try {
|
|
4631
|
+
requires = getComponent(name).requires ?? [];
|
|
4632
|
+
} catch (err) {
|
|
4633
|
+
if (err instanceof ComponentNotFoundError) continue;
|
|
4634
|
+
throw err;
|
|
4635
|
+
}
|
|
4636
|
+
for (const required of requires) {
|
|
4637
|
+
if (names.has(required)) continue;
|
|
4638
|
+
if (promised && !promised.includes(required)) continue;
|
|
4639
|
+
missing.set(required, [...missing.get(required) ?? [], name]);
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
return [...missing].map(([name, requiredBy]) => ({ name, requiredBy }));
|
|
4643
|
+
}
|
|
4644
|
+
var SPELL_ID_IN_PATH = /(?:^|\/)(spell-[a-z0-9-]+)(?:\/SKILL\.md|\.prompt\.md|\.md)$/;
|
|
4645
|
+
function spellIdFromTrackedPath(file) {
|
|
4646
|
+
return SPELL_ID_IN_PATH.exec(file)?.[1];
|
|
4647
|
+
}
|
|
4648
|
+
function findTrackedRetirements(components) {
|
|
4649
|
+
const componentNames = new Set(components.map((c) => c.name));
|
|
4650
|
+
const spellIds = new Set(
|
|
4651
|
+
components.flatMap((c) => c.files).map(spellIdFromTrackedPath).filter((id) => id !== void 0)
|
|
4652
|
+
);
|
|
4653
|
+
return REGISTRY_RETIREMENTS.filter(
|
|
4654
|
+
(entry) => entry.kind === "component" ? componentNames.has(entry.name) : spellIds.has(entry.name)
|
|
4655
|
+
);
|
|
4656
|
+
}
|
|
4657
|
+
async function findNewlyAvailable(targetDir, profile, installed, spellScope) {
|
|
4658
|
+
const definition = listProfiles().find((p) => p.id === profile);
|
|
4659
|
+
if (!definition) return [];
|
|
4660
|
+
const installedNames = new Set(installed.map((c) => c.name));
|
|
4661
|
+
const found = [];
|
|
4662
|
+
for (const name of definition.components) {
|
|
4663
|
+
if (installedNames.has(name)) continue;
|
|
4664
|
+
if (spellScope === "user" && SPELL_COMPONENT_NAMES.includes(name)) continue;
|
|
4665
|
+
const component = getComponent(name);
|
|
4666
|
+
const alreadyPresent = [];
|
|
4667
|
+
if (component.skipExisting) {
|
|
4668
|
+
for (const file of component.files) {
|
|
4669
|
+
if (await fileExists(join9(targetDir, file))) alreadyPresent.push(file);
|
|
4670
|
+
}
|
|
4671
|
+
if (component.files.length > 0 && alreadyPresent.length === component.files.length) continue;
|
|
4672
|
+
}
|
|
4673
|
+
found.push({ name, description: component.description, alreadyPresent });
|
|
4674
|
+
}
|
|
4675
|
+
return found;
|
|
4676
|
+
}
|
|
4677
|
+
function formatRegistryChanges(changes) {
|
|
4678
|
+
const missingRequires = changes.missingRequires ?? [];
|
|
4679
|
+
const newlyAvailable = (changes.newlyAvailable ?? []).filter(
|
|
4680
|
+
(c) => !missingRequires.some((m) => m.name === c.name)
|
|
4681
|
+
);
|
|
4682
|
+
if (changes.retired.length === 0 && newlyAvailable.length === 0 && missingRequires.length === 0) return [];
|
|
4683
|
+
const lines = ["", "Registry changes since your install:"];
|
|
4684
|
+
if (changes.retired.length > 0) {
|
|
4685
|
+
lines.push(" Renamed or retired, still tracked here:");
|
|
4686
|
+
for (const entry of changes.retired) {
|
|
4687
|
+
const successor = entry.successors.length > 0 ? entry.successors.join(", ") : "no successor";
|
|
4688
|
+
lines.push(` ${entry.name} (${entry.kind}) \u2192 ${successor}: ${entry.reason}`);
|
|
4689
|
+
}
|
|
4690
|
+
lines.push(
|
|
4691
|
+
" Nothing is deleted because of this list: their files go through the orphan handling above, which removes an untouched one only under `spell update --prune`."
|
|
4692
|
+
);
|
|
4693
|
+
}
|
|
4694
|
+
if (newlyAvailable.length > 0) {
|
|
4695
|
+
const profile = changes.profile ? ` "${changes.profile}"` : "";
|
|
4696
|
+
lines.push(` Newly available for your${profile} profile, not installed \u2014 nothing was added:`);
|
|
4697
|
+
for (const component of newlyAvailable) {
|
|
4698
|
+
lines.push(` ${component.name} \u2014 ${component.description}`);
|
|
4699
|
+
lines.push(` spell add ${component.name}`);
|
|
4700
|
+
if (component.alreadyPresent.length > 0) {
|
|
4701
|
+
lines.push(
|
|
4702
|
+
` (already here and yours: ${component.alreadyPresent.join(", ")} \u2014 \`spell add\` stops at an existing file rather than overwrite it)`
|
|
4703
|
+
);
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
if (missingRequires.length > 0) {
|
|
4708
|
+
lines.push(" Missing prerequisites \u2014 installed components cite these, and they are not installed:");
|
|
4709
|
+
for (const { name, requiredBy } of missingRequires) {
|
|
4710
|
+
lines.push(` ${name} (required by ${requiredBy.join(", ")})`);
|
|
4711
|
+
lines.push(` spell add ${name}`);
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
return lines;
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4556
4717
|
// src/modules/npm-registry.ts
|
|
4557
4718
|
var NPM_PACKAGE_NAME = "arcane-cli";
|
|
4558
4719
|
async function fetchPublishedFile(version, relativeAssetPath) {
|
|
@@ -4634,6 +4795,44 @@ function migrateLegacyComponents(components) {
|
|
|
4634
4795
|
inherited: { files: inheritedFiles, fileHashes: inheritedHashes, carrier }
|
|
4635
4796
|
};
|
|
4636
4797
|
}
|
|
4798
|
+
function moveComponentFiles(components, present = () => true) {
|
|
4799
|
+
let out = components;
|
|
4800
|
+
for (const move of COMPONENT_FILE_MOVES) {
|
|
4801
|
+
const from = out.find((c) => c.name === move.from);
|
|
4802
|
+
const tracked = from?.files.filter((f) => move.files.includes(f)) ?? [];
|
|
4803
|
+
if (!from || tracked.length === 0) continue;
|
|
4804
|
+
const moving = tracked.filter(present);
|
|
4805
|
+
const dropped = tracked.filter((f) => !present(f));
|
|
4806
|
+
const movedHashes = {};
|
|
4807
|
+
const keptHashes = {};
|
|
4808
|
+
for (const [file, hash] of Object.entries(from.fileHashes ?? {})) {
|
|
4809
|
+
if (dropped.includes(file)) continue;
|
|
4810
|
+
(moving.includes(file) ? movedHashes : keptHashes)[file] = hash;
|
|
4811
|
+
}
|
|
4812
|
+
const newFrom = {
|
|
4813
|
+
...from,
|
|
4814
|
+
files: from.files.filter((f) => !tracked.includes(f)),
|
|
4815
|
+
...from.fileHashes ? { fileHashes: keptHashes } : {}
|
|
4816
|
+
};
|
|
4817
|
+
const existing = out.find((c) => c.name === move.to);
|
|
4818
|
+
const newTo = existing ? {
|
|
4819
|
+
...existing,
|
|
4820
|
+
files: [...existing.files, ...moving.filter((f) => !existing.files.includes(f))],
|
|
4821
|
+
fileHashes: { ...movedHashes, ...existing.fileHashes }
|
|
4822
|
+
} : {
|
|
4823
|
+
name: move.to,
|
|
4824
|
+
files: moving,
|
|
4825
|
+
installedVersion: from.installedVersion,
|
|
4826
|
+
...from.fileHashes ? { fileHashes: movedHashes } : {}
|
|
4827
|
+
};
|
|
4828
|
+
out = out.flatMap((c) => {
|
|
4829
|
+
if (c === from) return existing || moving.length === 0 ? [newFrom] : [newFrom, newTo];
|
|
4830
|
+
if (c === existing) return [newTo];
|
|
4831
|
+
return [c];
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
return out;
|
|
4835
|
+
}
|
|
4637
4836
|
async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
4638
4837
|
try {
|
|
4639
4838
|
validateTargetPath(targetDir, file);
|
|
@@ -4641,7 +4840,7 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
4641
4840
|
console.log(` ! Refused (path escapes the target directory): ${file}`);
|
|
4642
4841
|
return "reported";
|
|
4643
4842
|
}
|
|
4644
|
-
const filePath =
|
|
4843
|
+
const filePath = join10(targetDir, file);
|
|
4645
4844
|
if (!await fileExists(filePath)) return "not-found";
|
|
4646
4845
|
if (!prune) return "reported";
|
|
4647
4846
|
if (recordedHash === void 0) {
|
|
@@ -4663,7 +4862,7 @@ async function retainReportedOrphan(targetDir, file, recordedHash, into) {
|
|
|
4663
4862
|
} catch {
|
|
4664
4863
|
return;
|
|
4665
4864
|
}
|
|
4666
|
-
if (!await fileExists(
|
|
4865
|
+
if (!await fileExists(join10(targetDir, file))) return;
|
|
4667
4866
|
if (!into.files.includes(file)) into.files.push(file);
|
|
4668
4867
|
if (recordedHash !== void 0) {
|
|
4669
4868
|
into.fileHashes ??= {};
|
|
@@ -4692,7 +4891,7 @@ async function findMissingTrackedFiles(targetDir, components, scope = "repo", sp
|
|
|
4692
4891
|
const candidates = readopted.has(installed.name) ? component.files : installed.files;
|
|
4693
4892
|
for (const file of candidates) {
|
|
4694
4893
|
if (!component.files.includes(file)) continue;
|
|
4695
|
-
if (!await fileExists(
|
|
4894
|
+
if (!await fileExists(join10(targetDir, file))) missing.push(file);
|
|
4696
4895
|
}
|
|
4697
4896
|
}
|
|
4698
4897
|
return missing;
|
|
@@ -4731,6 +4930,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4731
4930
|
}
|
|
4732
4931
|
throw err;
|
|
4733
4932
|
}
|
|
4933
|
+
const movedFilesPresent = /* @__PURE__ */ new Set();
|
|
4934
|
+
for (const move of COMPONENT_FILE_MOVES) {
|
|
4935
|
+
for (const file of move.files) {
|
|
4936
|
+
if (await fileExists(join10(targetDir, file))) movedFilesPresent.add(file);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
manifest = {
|
|
4940
|
+
...manifest,
|
|
4941
|
+
components: moveComponentFiles(manifest.components, (f) => movedFilesPresent.has(f))
|
|
4942
|
+
};
|
|
4734
4943
|
const resolvedScope = resolveInstallScope(targetDir, manifest.scope, options.user);
|
|
4735
4944
|
if (resolvedScope.misplacedUserManifest) {
|
|
4736
4945
|
console.error(misplacedUserManifestMessage(targetDir));
|
|
@@ -4748,7 +4957,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4748
4957
|
return;
|
|
4749
4958
|
}
|
|
4750
4959
|
try {
|
|
4751
|
-
flagPatch = resolveManifestFlags(options.manifestFlags, manifest,
|
|
4960
|
+
flagPatch = resolveManifestFlags(options.manifestFlags, manifest, join10(targetDir, ".arcane.json"));
|
|
4752
4961
|
} catch (err) {
|
|
4753
4962
|
if (!(err instanceof ManifestFlagError)) throw err;
|
|
4754
4963
|
console.error(`
|
|
@@ -4793,12 +5002,29 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4793
5002
|
return;
|
|
4794
5003
|
}
|
|
4795
5004
|
}
|
|
5005
|
+
const registryChanges = {
|
|
5006
|
+
retired: findTrackedRetirements(manifest.components),
|
|
5007
|
+
...scope === "repo" ? {
|
|
5008
|
+
profile: manifest.profile,
|
|
5009
|
+
newlyAvailable: await findNewlyAvailable(
|
|
5010
|
+
targetDir,
|
|
5011
|
+
manifest.profile,
|
|
5012
|
+
migrateLegacyComponents(manifest.components).components,
|
|
5013
|
+
spellScope
|
|
5014
|
+
),
|
|
5015
|
+
missingRequires: findMissingRequires(manifest.components, manifest.profile)
|
|
5016
|
+
} : {}
|
|
5017
|
+
};
|
|
5018
|
+
const printRegistryChanges = () => {
|
|
5019
|
+
for (const line of formatRegistryChanges(registryChanges)) console.log(line);
|
|
5020
|
+
};
|
|
4796
5021
|
let sameVersionRestore = false;
|
|
4797
5022
|
if (manifest.version === packageVersion && manifest.components.length > 0) {
|
|
4798
5023
|
const missing = await findMissingTrackedFiles(targetDir, manifest.components, scope, spellScope, readopted);
|
|
4799
5024
|
const unmanaged = findUnmanagedTrackedFiles(manifest.components, scope, spellScope);
|
|
4800
5025
|
if (missing.length === 0 && unmanaged.length === 0) {
|
|
4801
5026
|
console.log("Already up to date.");
|
|
5027
|
+
printRegistryChanges();
|
|
4802
5028
|
if (scope === "repo" && Object.keys(flagPatch).length > 0) {
|
|
4803
5029
|
if (!options.dryRun) await writeManifest(targetDir, answered);
|
|
4804
5030
|
reportFlagPatch(flagPatch, manifest, answered, options.dryRun);
|
|
@@ -4887,8 +5113,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4887
5113
|
const updatedFiles = [];
|
|
4888
5114
|
const fileHashes = {};
|
|
4889
5115
|
for (const file of component.files) {
|
|
4890
|
-
const srcPath =
|
|
4891
|
-
const targetExists = await fileExists(
|
|
5116
|
+
const srcPath = join10(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
5117
|
+
const targetExists = await fileExists(join10(targetDir, file));
|
|
4892
5118
|
const preserveExisting = Boolean(component.skipExisting) && targetExists;
|
|
4893
5119
|
const recordedHash = installed.fileHashes?.[file];
|
|
4894
5120
|
if (component.initOnly && !targetExists) {
|
|
@@ -4908,7 +5134,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4908
5134
|
continue;
|
|
4909
5135
|
}
|
|
4910
5136
|
}
|
|
4911
|
-
const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && !await fileMatchesHash(
|
|
5137
|
+
const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && !await fileMatchesHash(join10(targetDir, file), recordedHash);
|
|
4912
5138
|
if (preserveExisting) {
|
|
4913
5139
|
console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
|
|
4914
5140
|
} else if (editedByOperator && isClientShimPath(file)) {
|
|
@@ -4935,11 +5161,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4935
5161
|
handled = true;
|
|
4936
5162
|
} else {
|
|
4937
5163
|
const [currentContent, newContent] = await Promise.all([
|
|
4938
|
-
readFile7(
|
|
5164
|
+
readFile7(join10(targetDir, file), "utf-8"),
|
|
4939
5165
|
readFile7(srcPath, "utf-8")
|
|
4940
5166
|
]);
|
|
4941
5167
|
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
4942
|
-
await writeFile9(
|
|
5168
|
+
await writeFile9(join10(targetDir, file), result.content, "utf-8");
|
|
4943
5169
|
fileHashes[file] = await hashFile(srcPath);
|
|
4944
5170
|
if (result.hasConflict) {
|
|
4945
5171
|
conflictedFiles.push(file);
|
|
@@ -4961,7 +5187,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4961
5187
|
}
|
|
4962
5188
|
}
|
|
4963
5189
|
for (const dir of component.directories ?? []) {
|
|
4964
|
-
const srcDirPath =
|
|
5190
|
+
const srcDirPath = join10(assetsDir, dir);
|
|
4965
5191
|
if (options.dryRun) {
|
|
4966
5192
|
console.log(` [dry-run] Would update directory: ${dir}/`);
|
|
4967
5193
|
} else {
|
|
@@ -5028,6 +5254,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5028
5254
|
console.log(" Run `spell update --prune` to remove the ones that are safe to delete.");
|
|
5029
5255
|
}
|
|
5030
5256
|
}
|
|
5257
|
+
printRegistryChanges();
|
|
5031
5258
|
if (customizedShims.length > 0) {
|
|
5032
5259
|
const verb = options.dryRun ? "[dry-run] Would keep" : "Kept";
|
|
5033
5260
|
console.log(
|
|
@@ -5123,7 +5350,7 @@ import { dirname as dirname7 } from "path";
|
|
|
5123
5350
|
|
|
5124
5351
|
// src/modules/version-check.ts
|
|
5125
5352
|
import { readFile as readFile8, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
|
|
5126
|
-
import { dirname as dirname6, join as
|
|
5353
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
5127
5354
|
import { homedir as homedir3 } from "os";
|
|
5128
5355
|
import { createRequire } from "module";
|
|
5129
5356
|
var require2 = createRequire(import.meta.url);
|
|
@@ -5137,7 +5364,7 @@ function getFeedUrl() {
|
|
|
5137
5364
|
}
|
|
5138
5365
|
return url;
|
|
5139
5366
|
}
|
|
5140
|
-
var DEFAULT_CACHE_PATH =
|
|
5367
|
+
var DEFAULT_CACHE_PATH = join11(homedir3(), ".arcane", "version-cache.json");
|
|
5141
5368
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5142
5369
|
async function readCache(cachePath) {
|
|
5143
5370
|
try {
|
|
@@ -5356,7 +5583,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
5356
5583
|
|
|
5357
5584
|
// src/commands/uninstall.ts
|
|
5358
5585
|
import { rm as rm5 } from "fs/promises";
|
|
5359
|
-
import { dirname as dirname8, join as
|
|
5586
|
+
import { dirname as dirname8, join as join12 } from "path";
|
|
5360
5587
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
5361
5588
|
async function runUninstall(options, targetDir) {
|
|
5362
5589
|
let manifest;
|
|
@@ -5439,16 +5666,16 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
5439
5666
|
}
|
|
5440
5667
|
}
|
|
5441
5668
|
}
|
|
5442
|
-
await rm5(
|
|
5669
|
+
await rm5(join12(targetDir, ".arcane.json"), { force: true });
|
|
5443
5670
|
const agentDirs = [
|
|
5444
5671
|
".github/agents",
|
|
5445
5672
|
".arcane/agents",
|
|
5446
5673
|
".arcane/generated"
|
|
5447
5674
|
];
|
|
5448
5675
|
for (const dir of agentDirs) {
|
|
5449
|
-
await rm5(
|
|
5676
|
+
await rm5(join12(targetDir, dir), { recursive: true, force: true });
|
|
5450
5677
|
}
|
|
5451
|
-
await rm5(
|
|
5678
|
+
await rm5(join12(targetDir, ".arcane/agents.yaml"), { force: true });
|
|
5452
5679
|
const markerFiles = [
|
|
5453
5680
|
"CLAUDE.md",
|
|
5454
5681
|
"AGENTS.md",
|
|
@@ -5510,10 +5737,10 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
5510
5737
|
console.log(` ! Skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5511
5738
|
}
|
|
5512
5739
|
}
|
|
5513
|
-
await rm5(
|
|
5514
|
-
await rm5(
|
|
5515
|
-
await rm5(
|
|
5516
|
-
await removeDirectoryIfEmpty(
|
|
5740
|
+
await rm5(join12(storeRoot, ".arcane.json"), { force: true });
|
|
5741
|
+
await rm5(join12(storeRoot, "agents"), { recursive: true, force: true });
|
|
5742
|
+
await rm5(join12(storeRoot, "agents.yaml"), { force: true });
|
|
5743
|
+
await removeDirectoryIfEmpty(join12(storeRoot, USER_TIER_SPELLS_DIR));
|
|
5517
5744
|
await removeDirectoryIfEmpty(storeRoot);
|
|
5518
5745
|
for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
|
|
5519
5746
|
const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
|
|
@@ -5526,7 +5753,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
5526
5753
|
|
|
5527
5754
|
// src/commands/doctor.ts
|
|
5528
5755
|
import { access as access4, readdir as readdir9, readFile as readFile11, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
5529
|
-
import { join as
|
|
5756
|
+
import { join as join15, dirname as dirname9 } from "path";
|
|
5530
5757
|
|
|
5531
5758
|
// src/config/incidents.ts
|
|
5532
5759
|
var INCIDENT_QUEUE = [
|
|
@@ -5595,7 +5822,7 @@ function evaluateIncidentGate(records) {
|
|
|
5595
5822
|
|
|
5596
5823
|
// src/modules/denylist-scan.ts
|
|
5597
5824
|
import { readFile as readFile9, readdir as readdir7 } from "fs/promises";
|
|
5598
|
-
import { join as
|
|
5825
|
+
import { join as join13, relative } from "path";
|
|
5599
5826
|
function escapeRegExp(value) {
|
|
5600
5827
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5601
5828
|
}
|
|
@@ -5646,7 +5873,7 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
|
|
|
5646
5873
|
return collected;
|
|
5647
5874
|
}
|
|
5648
5875
|
for (const entry of entries) {
|
|
5649
|
-
const absolutePath =
|
|
5876
|
+
const absolutePath = join13(root, entry.name);
|
|
5650
5877
|
if (entry.isDirectory()) {
|
|
5651
5878
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
5652
5879
|
collected.push(...await collectScannableFiles(absolutePath, repoRoot, extensions));
|
|
@@ -5863,7 +6090,7 @@ function parseAdoRemote(url) {
|
|
|
5863
6090
|
|
|
5864
6091
|
// src/modules/placeholders.ts
|
|
5865
6092
|
import { readdir as readdir8, readFile as readFile10 } from "fs/promises";
|
|
5866
|
-
import { join as
|
|
6093
|
+
import { join as join14 } from "path";
|
|
5867
6094
|
var PLACEHOLDER_STANDARD_FILE = "spell-authoring-standards.md";
|
|
5868
6095
|
var RUNTIME_PLACEHOLDERS_START = "<!-- runtime-placeholders:start -->";
|
|
5869
6096
|
var RUNTIME_PLACEHOLDERS_END = "<!-- runtime-placeholders:end -->";
|
|
@@ -5958,7 +6185,7 @@ function parseRuntimePlaceholders(standardContent) {
|
|
|
5958
6185
|
async function scanGovernancePlaceholders(governanceDir) {
|
|
5959
6186
|
let standard;
|
|
5960
6187
|
try {
|
|
5961
|
-
standard = await readFile10(
|
|
6188
|
+
standard = await readFile10(join14(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
|
|
5962
6189
|
} catch (err) {
|
|
5963
6190
|
if (err.code === "ENOENT") {
|
|
5964
6191
|
return { status: "skipped", reason: `${PLACEHOLDER_STANDARD_FILE} is not installed` };
|
|
@@ -5977,7 +6204,7 @@ async function scanGovernancePlaceholders(governanceDir) {
|
|
|
5977
6204
|
const templateDocs = [];
|
|
5978
6205
|
const findings = [];
|
|
5979
6206
|
for (const file of files) {
|
|
5980
|
-
const content = await readFile10(
|
|
6207
|
+
const content = await readFile10(join14(governanceDir, file), "utf8");
|
|
5981
6208
|
const status = parseFrontmatterStatus(content);
|
|
5982
6209
|
if (status === "template") templateDocs.push(file);
|
|
5983
6210
|
if (status !== "active") continue;
|
|
@@ -6008,8 +6235,8 @@ async function checkNodeVersion() {
|
|
|
6008
6235
|
async function checkVSCodeExtension(id, label, homeDir = resolveHomeDir()) {
|
|
6009
6236
|
const name = `VS Code extension: ${label}`;
|
|
6010
6237
|
const flavours = [
|
|
6011
|
-
{ dir:
|
|
6012
|
-
{ dir:
|
|
6238
|
+
{ dir: join15(homeDir, ".vscode", "extensions"), bin: "code" },
|
|
6239
|
+
{ dir: join15(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
|
|
6013
6240
|
];
|
|
6014
6241
|
let installBin = null;
|
|
6015
6242
|
for (const flavour of flavours) {
|
|
@@ -6037,11 +6264,11 @@ async function checkVSCodeExtension(id, label, homeDir = resolveHomeDir()) {
|
|
|
6037
6264
|
}
|
|
6038
6265
|
async function checkArcaneManifest(targetDir) {
|
|
6039
6266
|
const name = "Arcane manifest (.arcane.json)";
|
|
6040
|
-
const manifestPath2 =
|
|
6267
|
+
const manifestPath2 = join15(targetDir, ".arcane.json");
|
|
6041
6268
|
try {
|
|
6042
6269
|
await access4(manifestPath2);
|
|
6043
6270
|
} catch {
|
|
6044
|
-
const selfHostedManifestPath =
|
|
6271
|
+
const selfHostedManifestPath = join15(targetDir, "src", "assets", ".arcane.json");
|
|
6045
6272
|
try {
|
|
6046
6273
|
const raw = await readFile11(selfHostedManifestPath, "utf8");
|
|
6047
6274
|
const manifest = JSON.parse(raw);
|
|
@@ -6147,7 +6374,7 @@ async function checkSessionContinuity(targetDir) {
|
|
|
6147
6374
|
const results = [];
|
|
6148
6375
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
6149
6376
|
const name = `Session file: ${file}`;
|
|
6150
|
-
const filePath =
|
|
6377
|
+
const filePath = join15(targetDir, file);
|
|
6151
6378
|
try {
|
|
6152
6379
|
await access4(filePath);
|
|
6153
6380
|
results.push({ name, passed: true, message: "present" });
|
|
@@ -6165,11 +6392,11 @@ async function checkSessionContinuity(targetDir) {
|
|
|
6165
6392
|
async function fixSessionContinuity(targetDir, assetsDir) {
|
|
6166
6393
|
const created = [];
|
|
6167
6394
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
6168
|
-
const destPath =
|
|
6395
|
+
const destPath = join15(targetDir, file);
|
|
6169
6396
|
try {
|
|
6170
6397
|
await access4(destPath);
|
|
6171
6398
|
} catch {
|
|
6172
|
-
const srcPath =
|
|
6399
|
+
const srcPath = join15(assetsDir, file);
|
|
6173
6400
|
await mkdir7(dirname9(destPath), { recursive: true });
|
|
6174
6401
|
await fsCopyFile(srcPath, destPath);
|
|
6175
6402
|
created.push(file);
|
|
@@ -6297,7 +6524,7 @@ async function checkGovernancePlaceholders(targetDir) {
|
|
|
6297
6524
|
const name = "Governance placeholders (ARC-051)";
|
|
6298
6525
|
let scan;
|
|
6299
6526
|
try {
|
|
6300
|
-
scan = await scanGovernancePlaceholders(
|
|
6527
|
+
scan = await scanGovernancePlaceholders(join15(targetDir, ".arcane", "governance"));
|
|
6301
6528
|
} catch (err) {
|
|
6302
6529
|
return {
|
|
6303
6530
|
name,
|
|
@@ -6325,9 +6552,34 @@ async function checkGovernancePlaceholders(targetDir) {
|
|
|
6325
6552
|
message: `unknown placeholder(s) in status: active doc(s) \u2014 ${listed.join("; ")}. Fill each one in; or, if an agent resolves it at use time, add it to the runtime-placeholders list in .arcane/governance/${PLACEHOLDER_STANDARD_FILE}; or, if the document is meant to be filled in by hand, mark it \`status: template\`.`
|
|
6326
6553
|
};
|
|
6327
6554
|
}
|
|
6555
|
+
async function checkComponentRequires(targetDir) {
|
|
6556
|
+
const name = "Component prerequisites";
|
|
6557
|
+
let manifest;
|
|
6558
|
+
try {
|
|
6559
|
+
manifest = await readManifest(targetDir);
|
|
6560
|
+
} catch {
|
|
6561
|
+
return { name, passed: true, blocking: false, skipped: true, message: "skipped \u2014 no readable .arcane.json" };
|
|
6562
|
+
}
|
|
6563
|
+
if (manifest.scope === "user") {
|
|
6564
|
+
return { name, passed: true, blocking: false, skipped: true, message: "skipped \u2014 the user tier holds spells only" };
|
|
6565
|
+
}
|
|
6566
|
+
if (!Array.isArray(manifest.components)) {
|
|
6567
|
+
return { name, passed: true, blocking: false, skipped: true, message: "skipped \u2014 .arcane.json lists no components" };
|
|
6568
|
+
}
|
|
6569
|
+
const missing = findMissingRequires(manifest.components, manifest.profile);
|
|
6570
|
+
if (missing.length === 0) {
|
|
6571
|
+
return { name, passed: true, blocking: false, message: "every installed component's prerequisites are installed" };
|
|
6572
|
+
}
|
|
6573
|
+
return {
|
|
6574
|
+
name,
|
|
6575
|
+
passed: false,
|
|
6576
|
+
blocking: false,
|
|
6577
|
+
message: missing.map(({ name: required, requiredBy }) => `${requiredBy.join(", ")} cites ${required}, which is not installed \u2014 \`spell add ${required}\``).join("; ")
|
|
6578
|
+
};
|
|
6579
|
+
}
|
|
6328
6580
|
async function checkMcpConfig(targetDir) {
|
|
6329
6581
|
const name = "MCP config (per-server timeout, I12)";
|
|
6330
|
-
const filePath =
|
|
6582
|
+
const filePath = join15(targetDir, ".mcp.json");
|
|
6331
6583
|
let raw;
|
|
6332
6584
|
try {
|
|
6333
6585
|
raw = await readFile11(filePath, "utf8");
|
|
@@ -6383,7 +6635,7 @@ function isDelegationsFile(value) {
|
|
|
6383
6635
|
}
|
|
6384
6636
|
async function checkDelegations(targetDir) {
|
|
6385
6637
|
const name = "Standing delegations (T13)";
|
|
6386
|
-
const filePath =
|
|
6638
|
+
const filePath = join15(targetDir, ".arcane", "delegations.json");
|
|
6387
6639
|
let raw;
|
|
6388
6640
|
try {
|
|
6389
6641
|
raw = await readFile11(filePath, "utf8");
|
|
@@ -6496,7 +6748,7 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
|
|
|
6496
6748
|
let probed = false;
|
|
6497
6749
|
const probe = "spell-status.md";
|
|
6498
6750
|
try {
|
|
6499
|
-
const entries = await readdir9(
|
|
6751
|
+
const entries = await readdir9(join15(storeRoot, USER_TIER_SPELLS_DIR));
|
|
6500
6752
|
const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
|
|
6501
6753
|
spellCount = spells.length;
|
|
6502
6754
|
probed = spells.includes(probe);
|
|
@@ -6560,7 +6812,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
6560
6812
|
checkMcpConfig(targetDir),
|
|
6561
6813
|
checkUserTier(resolveHomeDir(), options.packageVersion),
|
|
6562
6814
|
checkSpellScope(targetDir),
|
|
6563
|
-
checkGovernancePlaceholders(targetDir)
|
|
6815
|
+
checkGovernancePlaceholders(targetDir),
|
|
6816
|
+
checkComponentRequires(targetDir)
|
|
6564
6817
|
]);
|
|
6565
6818
|
const sessionResults = await checkSessionContinuity(targetDir);
|
|
6566
6819
|
results.push(...sessionResults);
|
|
@@ -6608,7 +6861,7 @@ import { readFile as readFile13 } from "fs/promises";
|
|
|
6608
6861
|
|
|
6609
6862
|
// src/modules/ward.ts
|
|
6610
6863
|
import { readdir as readdir10, readFile as readFile12 } from "fs/promises";
|
|
6611
|
-
import { join as
|
|
6864
|
+
import { join as join16, relative as relative2 } from "path";
|
|
6612
6865
|
async function scanFileAllRules(absolutePath, displayPath, rules) {
|
|
6613
6866
|
const findings = [];
|
|
6614
6867
|
let content;
|
|
@@ -6671,7 +6924,7 @@ async function scanFilenames(root, rules, repoRoot = root) {
|
|
|
6671
6924
|
return findings;
|
|
6672
6925
|
}
|
|
6673
6926
|
for (const entry of entries) {
|
|
6674
|
-
const absolutePath =
|
|
6927
|
+
const absolutePath = join16(root, entry.name);
|
|
6675
6928
|
if (entry.isDirectory()) {
|
|
6676
6929
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
6677
6930
|
findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
|
|
@@ -6948,15 +7201,15 @@ async function runBlockPush(targetDir) {
|
|
|
6948
7201
|
|
|
6949
7202
|
// src/commands/report.ts
|
|
6950
7203
|
import { access as access5 } from "fs/promises";
|
|
6951
|
-
import { join as
|
|
7204
|
+
import { join as join19 } from "path";
|
|
6952
7205
|
|
|
6953
7206
|
// src/modules/show-report/generate.ts
|
|
6954
7207
|
import { mkdir as mkdir8, readdir as readdir11, readFile as readFile15, writeFile as writeFile11 } from "fs/promises";
|
|
6955
|
-
import { basename as basename3, dirname as dirname10, isAbsolute, join as
|
|
7208
|
+
import { basename as basename3, dirname as dirname10, isAbsolute, join as join18, relative as relative3, resolve as resolve4 } from "path";
|
|
6956
7209
|
|
|
6957
7210
|
// src/modules/show-report/model.ts
|
|
6958
7211
|
import { readFile as readFile14 } from "fs/promises";
|
|
6959
|
-
import { join as
|
|
7212
|
+
import { join as join17 } from "path";
|
|
6960
7213
|
|
|
6961
7214
|
// src/modules/show-report/plan-parser.ts
|
|
6962
7215
|
function parseFrontmatter(content) {
|
|
@@ -7322,11 +7575,11 @@ function buildCorrections(ledgerRows, programName) {
|
|
|
7322
7575
|
};
|
|
7323
7576
|
}
|
|
7324
7577
|
async function buildShowReportModel(options) {
|
|
7325
|
-
const planAbsPath =
|
|
7326
|
-
const queueAbsPath =
|
|
7578
|
+
const planAbsPath = join17(options.rootDir, options.planRelPath);
|
|
7579
|
+
const queueAbsPath = join17(options.rootDir, options.queueRelPath);
|
|
7327
7580
|
const ledgerRelPath = "docs/verification-ledger.md";
|
|
7328
|
-
const ledgerAbsPath =
|
|
7329
|
-
const decisionsAbsPath =
|
|
7581
|
+
const ledgerAbsPath = join17(options.rootDir, ledgerRelPath);
|
|
7582
|
+
const decisionsAbsPath = join17(options.rootDir, "DECISIONS.md");
|
|
7330
7583
|
const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
|
|
7331
7584
|
readFile14(planAbsPath, "utf8"),
|
|
7332
7585
|
readOptional(queueAbsPath),
|
|
@@ -7497,7 +7750,7 @@ function toPosix(path4) {
|
|
|
7497
7750
|
async function programFromPlan(rootDir, slug, planRelPath) {
|
|
7498
7751
|
let planContent;
|
|
7499
7752
|
try {
|
|
7500
|
-
planContent = await readFile15(
|
|
7753
|
+
planContent = await readFile15(join18(rootDir, planRelPath), "utf8");
|
|
7501
7754
|
} catch {
|
|
7502
7755
|
return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
|
|
7503
7756
|
}
|
|
@@ -7509,13 +7762,13 @@ async function programFromPlan(rootDir, slug, planRelPath) {
|
|
|
7509
7762
|
program: {
|
|
7510
7763
|
slug,
|
|
7511
7764
|
planRelPath,
|
|
7512
|
-
queueRelPath: toPosix(
|
|
7765
|
+
queueRelPath: toPosix(join18(dirname10(planRelPath), "OPERATOR-QUEUE.md")),
|
|
7513
7766
|
ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
|
|
7514
7767
|
}
|
|
7515
7768
|
};
|
|
7516
7769
|
}
|
|
7517
7770
|
async function discoverPrograms(rootDir) {
|
|
7518
|
-
const plansDir =
|
|
7771
|
+
const plansDir = join18(rootDir, "docs", "plans");
|
|
7519
7772
|
let entries;
|
|
7520
7773
|
try {
|
|
7521
7774
|
entries = (await readdir11(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
@@ -7549,7 +7802,7 @@ async function readOptionalFile(path4) {
|
|
|
7549
7802
|
}
|
|
7550
7803
|
}
|
|
7551
7804
|
async function generateReport(rootDir, program2, templateHtml) {
|
|
7552
|
-
const planContent = await readFile15(
|
|
7805
|
+
const planContent = await readFile15(join18(rootDir, program2.planRelPath), "utf8");
|
|
7553
7806
|
const frontmatter = parseFrontmatter(planContent);
|
|
7554
7807
|
const model = await buildShowReportModel({
|
|
7555
7808
|
rootDir,
|
|
@@ -7582,12 +7835,12 @@ async function runReportGeneration(options) {
|
|
|
7582
7835
|
[`${outRel}/show-report.html`, generated.html]
|
|
7583
7836
|
];
|
|
7584
7837
|
for (const [relPath, expected] of outputs) {
|
|
7585
|
-
const actual = await readOptionalFile(
|
|
7838
|
+
const actual = await readOptionalFile(join18(options.rootDir, relPath));
|
|
7586
7839
|
if (actual === null || normalizeLineEndings2(actual) !== normalizeLineEndings2(expected)) {
|
|
7587
7840
|
drifted.push(`${relPath} does not match a regeneration from its sources.`);
|
|
7588
7841
|
if (options.mode === "write") {
|
|
7589
7842
|
await mkdir8(outAbs, { recursive: true });
|
|
7590
|
-
await writeFile11(
|
|
7843
|
+
await writeFile11(join18(options.rootDir, relPath), expected, "utf8");
|
|
7591
7844
|
repaired.push(relPath);
|
|
7592
7845
|
}
|
|
7593
7846
|
}
|
|
@@ -7598,7 +7851,7 @@ async function runReportGeneration(options) {
|
|
|
7598
7851
|
|
|
7599
7852
|
// src/commands/report.ts
|
|
7600
7853
|
async function runReport(targetDir, options = {}, assetsDir) {
|
|
7601
|
-
const templatePath =
|
|
7854
|
+
const templatePath = join19(assetsDir, "report", "show-report.template.html");
|
|
7602
7855
|
try {
|
|
7603
7856
|
await access5(templatePath);
|
|
7604
7857
|
} catch {
|
|
@@ -7661,7 +7914,7 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
7661
7914
|
import { realpathSync } from "fs";
|
|
7662
7915
|
var require3 = createRequire2(import.meta.url);
|
|
7663
7916
|
var pkg = require3("../package.json");
|
|
7664
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ??
|
|
7917
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join20(dirname11(fileURLToPath(import.meta.url)), "assets");
|
|
7665
7918
|
var program = new Command();
|
|
7666
7919
|
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)";
|
|
7667
7920
|
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)";
|