arcane-cli 0.39.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/assets/.agents/skills/spell-address-review/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-adopt-docs/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-arcane-version/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-architect/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-brainstorm/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-bug/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-bump/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-check-drift/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-close-session/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-commit-work/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-compliance/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-create-pull-request/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-document/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-dotnet-expert/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-eas-store-deploy/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-enchant/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-explain-concept/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-feedback/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-full-cycle/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-generate-bot-icons/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-implement/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-make-discoverable/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-manifest/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-open-session/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-plan/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-present-arcane/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-product-review/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-review/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-review-batch/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-save-idea/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-scope/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-scry/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-security-review/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-ship/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-status/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-suggest-feature/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-summon-venture/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-sync-pull-request/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-test/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-todo/SKILL.md +1 -1
- package/dist/assets/.agents/skills/spell-verification-ledger/SKILL.md +1 -1
- package/dist/assets/.arcane/governance/development-methodology.md +6 -6
- package/dist/assets/.arcane/governance/git-conventions.md +2 -2
- package/dist/assets/.arcane/governance/portable-bootstrap.md +1 -0
- package/dist/assets/.arcane/governance/product-excellence-standards.md +1 -1
- package/dist/assets/.arcane/governance/spell-authoring-standards.md +9 -1
- package/dist/assets/.arcane/governance/universal-agent-rules.md +5 -3
- package/dist/assets/.arcane/spells/spell-address-review.md +115 -0
- package/dist/assets/.arcane/spells/spell-adopt-docs.md +158 -0
- package/dist/assets/.arcane/spells/spell-arcane-version.md +87 -0
- package/dist/assets/.arcane/spells/spell-architect.md +117 -0
- package/dist/assets/.arcane/spells/spell-brainstorm.md +125 -0
- package/dist/assets/.arcane/spells/spell-bug.md +123 -0
- package/dist/assets/.arcane/spells/spell-bump.md +82 -0
- package/dist/assets/.arcane/spells/spell-check-drift.md +118 -0
- package/dist/assets/.arcane/spells/spell-close-session.md +266 -0
- package/dist/assets/.arcane/spells/spell-commit-work.md +279 -0
- package/dist/assets/.arcane/spells/spell-compliance.md +130 -0
- package/dist/assets/.arcane/spells/spell-create-pull-request.md +224 -0
- package/dist/assets/.arcane/spells/spell-document.md +84 -0
- package/dist/assets/.arcane/spells/spell-dotnet-expert.md +118 -0
- package/dist/assets/.arcane/spells/spell-eas-store-deploy.md +254 -0
- package/dist/assets/.arcane/spells/spell-enchant.md +386 -0
- package/dist/assets/.arcane/spells/spell-explain-concept.md +70 -0
- package/dist/assets/.arcane/spells/spell-feedback.md +138 -0
- package/dist/assets/.arcane/spells/spell-full-cycle.md +253 -0
- package/dist/assets/.arcane/spells/spell-generate-bot-icons.md +85 -0
- package/dist/assets/.arcane/spells/spell-implement.md +109 -0
- package/dist/assets/.arcane/spells/spell-make-discoverable.md +293 -0
- package/dist/assets/.arcane/spells/spell-manifest.md +148 -0
- package/dist/assets/.arcane/spells/spell-open-session.md +154 -0
- package/dist/assets/.arcane/spells/spell-plan.md +160 -0
- package/dist/assets/.arcane/spells/spell-present-arcane.md +189 -0
- package/dist/assets/.arcane/spells/spell-product-review.md +98 -0
- package/dist/assets/.arcane/spells/spell-review-batch.md +113 -0
- package/dist/assets/.arcane/spells/spell-review.md +142 -0
- package/dist/assets/.arcane/spells/spell-save-idea.md +117 -0
- package/dist/assets/.arcane/spells/spell-scope.md +266 -0
- package/dist/assets/.arcane/spells/spell-scry.md +124 -0
- package/dist/assets/.arcane/spells/spell-security-review.md +152 -0
- package/dist/assets/.arcane/spells/spell-ship.md +106 -0
- package/dist/assets/.arcane/spells/spell-status.md +86 -0
- package/dist/assets/.arcane/spells/spell-suggest-feature.md +122 -0
- package/dist/assets/.arcane/spells/spell-summon-venture.md +110 -0
- package/dist/assets/.arcane/spells/spell-sync-pull-request.md +161 -0
- package/dist/assets/.arcane/spells/spell-test.md +88 -0
- package/dist/assets/.arcane/spells/spell-todo.md +166 -0
- package/dist/assets/.arcane/spells/spell-verification-ledger.md +102 -0
- package/dist/assets/.claude/commands/spell-address-review.md +2 -2
- package/dist/assets/.claude/commands/spell-adopt-docs.md +2 -2
- package/dist/assets/.claude/commands/spell-arcane-version.md +2 -2
- package/dist/assets/.claude/commands/spell-architect.md +2 -2
- package/dist/assets/.claude/commands/spell-brainstorm.md +2 -2
- package/dist/assets/.claude/commands/spell-bug.md +2 -2
- package/dist/assets/.claude/commands/spell-bump.md +2 -2
- package/dist/assets/.claude/commands/spell-check-drift.md +2 -2
- package/dist/assets/.claude/commands/spell-close-session.md +2 -2
- package/dist/assets/.claude/commands/spell-commit-work.md +2 -2
- package/dist/assets/.claude/commands/spell-compliance.md +2 -2
- package/dist/assets/.claude/commands/spell-create-pull-request.md +2 -2
- package/dist/assets/.claude/commands/spell-document.md +2 -2
- package/dist/assets/.claude/commands/spell-dotnet-expert.md +2 -2
- package/dist/assets/.claude/commands/spell-eas-store-deploy.md +2 -2
- package/dist/assets/.claude/commands/spell-enchant.md +2 -2
- package/dist/assets/.claude/commands/spell-explain-concept.md +2 -2
- package/dist/assets/.claude/commands/spell-feedback.md +2 -2
- package/dist/assets/.claude/commands/spell-full-cycle.md +2 -2
- package/dist/assets/.claude/commands/spell-generate-bot-icons.md +2 -2
- package/dist/assets/.claude/commands/spell-implement.md +2 -2
- package/dist/assets/.claude/commands/spell-make-discoverable.md +2 -2
- package/dist/assets/.claude/commands/spell-manifest.md +2 -2
- package/dist/assets/.claude/commands/spell-open-session.md +2 -2
- package/dist/assets/.claude/commands/spell-plan.md +2 -2
- package/dist/assets/.claude/commands/spell-present-arcane.md +2 -2
- package/dist/assets/.claude/commands/spell-product-review.md +2 -2
- package/dist/assets/.claude/commands/spell-review-batch.md +2 -2
- package/dist/assets/.claude/commands/spell-review.md +2 -2
- package/dist/assets/.claude/commands/spell-save-idea.md +2 -2
- package/dist/assets/.claude/commands/spell-scope.md +2 -2
- package/dist/assets/.claude/commands/spell-scry.md +2 -2
- package/dist/assets/.claude/commands/spell-security-review.md +2 -2
- package/dist/assets/.claude/commands/spell-ship.md +2 -2
- package/dist/assets/.claude/commands/spell-status.md +2 -2
- package/dist/assets/.claude/commands/spell-suggest-feature.md +2 -2
- package/dist/assets/.claude/commands/spell-summon-venture.md +2 -2
- package/dist/assets/.claude/commands/spell-sync-pull-request.md +2 -2
- package/dist/assets/.claude/commands/spell-test.md +2 -2
- package/dist/assets/.claude/commands/spell-todo.md +2 -2
- package/dist/assets/.claude/commands/spell-verification-ledger.md +2 -2
- package/dist/assets/.github/instructions/agent-output.instructions.md +14 -13
- package/dist/assets/.github/prompts/spell-address-review.prompt.md +1 -107
- package/dist/assets/.github/prompts/spell-adopt-docs.prompt.md +1 -150
- package/dist/assets/.github/prompts/spell-arcane-version.prompt.md +1 -79
- package/dist/assets/.github/prompts/spell-architect.prompt.md +1 -109
- package/dist/assets/.github/prompts/spell-brainstorm.prompt.md +1 -117
- package/dist/assets/.github/prompts/spell-bug.prompt.md +1 -115
- package/dist/assets/.github/prompts/spell-bump.prompt.md +1 -74
- package/dist/assets/.github/prompts/spell-check-drift.prompt.md +1 -110
- package/dist/assets/.github/prompts/spell-close-session.prompt.md +1 -258
- package/dist/assets/.github/prompts/spell-commit-work.prompt.md +1 -270
- package/dist/assets/.github/prompts/spell-compliance.prompt.md +1 -122
- package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +1 -215
- package/dist/assets/.github/prompts/spell-document.prompt.md +1 -76
- package/dist/assets/.github/prompts/spell-dotnet-expert.prompt.md +1 -110
- package/dist/assets/.github/prompts/spell-eas-store-deploy.prompt.md +1 -246
- package/dist/assets/.github/prompts/spell-enchant.prompt.md +1 -378
- package/dist/assets/.github/prompts/spell-explain-concept.prompt.md +1 -62
- package/dist/assets/.github/prompts/spell-feedback.prompt.md +1 -130
- package/dist/assets/.github/prompts/spell-full-cycle.prompt.md +1 -245
- package/dist/assets/.github/prompts/spell-generate-bot-icons.prompt.md +1 -77
- package/dist/assets/.github/prompts/spell-implement.prompt.md +1 -101
- package/dist/assets/.github/prompts/spell-make-discoverable.prompt.md +1 -285
- package/dist/assets/.github/prompts/spell-manifest.prompt.md +1 -140
- package/dist/assets/.github/prompts/spell-open-session.prompt.md +1 -146
- package/dist/assets/.github/prompts/spell-plan.prompt.md +1 -152
- package/dist/assets/.github/prompts/spell-present-arcane.prompt.md +1 -181
- package/dist/assets/.github/prompts/spell-product-review.prompt.md +1 -90
- package/dist/assets/.github/prompts/spell-review-batch.prompt.md +1 -105
- package/dist/assets/.github/prompts/spell-review.prompt.md +1 -134
- package/dist/assets/.github/prompts/spell-save-idea.prompt.md +1 -109
- package/dist/assets/.github/prompts/spell-scope.prompt.md +1 -258
- package/dist/assets/.github/prompts/spell-scry.prompt.md +1 -116
- package/dist/assets/.github/prompts/spell-security-review.prompt.md +1 -144
- package/dist/assets/.github/prompts/spell-ship.prompt.md +1 -98
- package/dist/assets/.github/prompts/spell-status.prompt.md +1 -78
- package/dist/assets/.github/prompts/spell-suggest-feature.prompt.md +1 -114
- package/dist/assets/.github/prompts/spell-summon-venture.prompt.md +1 -102
- package/dist/assets/.github/prompts/spell-sync-pull-request.prompt.md +1 -152
- package/dist/assets/.github/prompts/spell-test.prompt.md +1 -80
- package/dist/assets/.github/prompts/spell-todo.prompt.md +1 -158
- package/dist/assets/.github/prompts/spell-verification-ledger.prompt.md +1 -94
- package/dist/index.js +1073 -246
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- /package/dist/assets/{.github/prompts → .arcane/spells}/_fragments/tracking-mode-declaration.md +0 -0
package/dist/index.js
CHANGED
|
@@ -6,11 +6,11 @@ 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
|
|
9
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { select as select2, confirm, input as input2 } from "@inquirer/prompts";
|
|
13
|
-
import { join as
|
|
13
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
14
14
|
import chalk2 from "chalk";
|
|
15
15
|
|
|
16
16
|
// src/modules/copier.ts
|
|
@@ -109,6 +109,15 @@ var VALID_TRACKING_MODES = ["internal", "external"];
|
|
|
109
109
|
var VALID_EXTERNAL_PROVIDERS = ["ado", "github", "jira", "other"];
|
|
110
110
|
var VALID_CONTENT_SENSITIVITY = ["standard", "sensitive"];
|
|
111
111
|
var VALID_PUSH_POLICIES = ["open", "guarded", "blocked"];
|
|
112
|
+
var VALID_INSTALL_SCOPES = ["repo", "user"];
|
|
113
|
+
function isValidFanoutPath(file) {
|
|
114
|
+
return file.length > 0 && !file.startsWith("/") && !/^[a-zA-Z]:/.test(file) && !file.includes("\\") && !file.split("/").includes("..") && !/[\r\n\0]/.test(file);
|
|
115
|
+
}
|
|
116
|
+
function isValidFanoutRecord(value) {
|
|
117
|
+
return isRecord(value) && Object.entries(value).every(
|
|
118
|
+
([file, hash]) => isValidFanoutPath(file) && typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
112
121
|
function isValidSubjectRoot(value) {
|
|
113
122
|
const invalid = value.trim() === "" || path2.isAbsolute(value) || // Windows drive-relative ("C:docs") and UNC-ish values also escape.
|
|
114
123
|
/^[a-zA-Z]:/.test(value) || value.startsWith("\\\\") || value.startsWith("\\") || value.split(/[\\/]/).includes("..") || // A stored subject_root is interpolated into agent prompts downstream, so
|
|
@@ -132,6 +141,9 @@ function validateSecretsScanExcludePrefixes(value, filePath) {
|
|
|
132
141
|
function manifestPath(targetDir) {
|
|
133
142
|
return path2.join(targetDir, MANIFEST_FILE);
|
|
134
143
|
}
|
|
144
|
+
function isRecord(value) {
|
|
145
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
146
|
+
}
|
|
135
147
|
function validateTrackingFields(manifest, filePath) {
|
|
136
148
|
if (manifest.tracking_mode !== void 0 && !VALID_TRACKING_MODES.includes(manifest.tracking_mode)) {
|
|
137
149
|
throw new ManifestInvalidFieldError(filePath, "tracking_mode", manifest.tracking_mode);
|
|
@@ -158,6 +170,12 @@ function validateTrackingFields(manifest, filePath) {
|
|
|
158
170
|
if (manifest.secretsScanExcludePrefixes !== void 0) {
|
|
159
171
|
validateSecretsScanExcludePrefixes(manifest.secretsScanExcludePrefixes, filePath);
|
|
160
172
|
}
|
|
173
|
+
if (manifest.scope !== void 0 && !VALID_INSTALL_SCOPES.includes(manifest.scope)) {
|
|
174
|
+
throw new ManifestInvalidFieldError(filePath, "scope", manifest.scope);
|
|
175
|
+
}
|
|
176
|
+
if (manifest.fanout !== void 0 && !isValidFanoutRecord(manifest.fanout)) {
|
|
177
|
+
throw new ManifestInvalidFieldError(filePath, "fanout", manifest.fanout);
|
|
178
|
+
}
|
|
161
179
|
}
|
|
162
180
|
async function readManifest(targetDir) {
|
|
163
181
|
const filePath = manifestPath(targetDir);
|
|
@@ -179,9 +197,6 @@ async function readManifest(targetDir) {
|
|
|
179
197
|
validateTrackingFields(manifest, filePath);
|
|
180
198
|
return manifest;
|
|
181
199
|
}
|
|
182
|
-
function isRecord(value) {
|
|
183
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
184
|
-
}
|
|
185
200
|
async function resolveSecretsScanExcludePrefixes(targetDir) {
|
|
186
201
|
try {
|
|
187
202
|
const manifest = await readManifest(targetDir);
|
|
@@ -508,31 +523,39 @@ var COMPONENTS = [
|
|
|
508
523
|
// ─── Spells ────────────────────────────────────────────────────────────────
|
|
509
524
|
// Split from the former monolithic `spell-prompts` + `claude-commands` pair
|
|
510
525
|
// so a profile can select spells by capability (see ARC-0NN). Grouping lives
|
|
511
|
-
// here and only here -- every spell file stays flat
|
|
526
|
+
// here and only here -- every spell file stays flat on disk; the one
|
|
527
|
+
// sanctioned relocation is ARC-045's canonical move (CS-03).
|
|
512
528
|
//
|
|
513
|
-
// Each component carries
|
|
514
|
-
//
|
|
529
|
+
// Each component carries, per spell, the canonical source and ALL THREE
|
|
530
|
+
// generated client shims (ARC-045 / CS-03): the client-neutral source
|
|
531
|
+
// (.arcane/spells/*.md, always listed first), the Copilot prompt shim
|
|
532
|
+
// (.github/prompts/*.prompt.md), the Claude Code command stub
|
|
515
533
|
// (.claude/commands/*.md), and the Codex skill (.agents/skills/*/SKILL.md,
|
|
516
534
|
// CS-01). They are never independently selectable -- every profile that
|
|
517
535
|
// wanted one always wanted the others -- and pairing them here makes it
|
|
518
|
-
// structurally impossible for
|
|
519
|
-
// across profiles.
|
|
536
|
+
// structurally impossible for a spell's source and its shims to drift
|
|
537
|
+
// apart across profiles.
|
|
520
538
|
{
|
|
521
539
|
name: "spells-session",
|
|
522
540
|
description: "Session lifecycle \u2014 open, close, commit, status, version",
|
|
523
541
|
files: [
|
|
542
|
+
".arcane/spells/spell-open-session.md",
|
|
524
543
|
".github/prompts/spell-open-session.prompt.md",
|
|
525
544
|
".claude/commands/spell-open-session.md",
|
|
526
545
|
".agents/skills/spell-open-session/SKILL.md",
|
|
546
|
+
".arcane/spells/spell-close-session.md",
|
|
527
547
|
".github/prompts/spell-close-session.prompt.md",
|
|
528
548
|
".claude/commands/spell-close-session.md",
|
|
529
549
|
".agents/skills/spell-close-session/SKILL.md",
|
|
550
|
+
".arcane/spells/spell-commit-work.md",
|
|
530
551
|
".github/prompts/spell-commit-work.prompt.md",
|
|
531
552
|
".claude/commands/spell-commit-work.md",
|
|
532
553
|
".agents/skills/spell-commit-work/SKILL.md",
|
|
554
|
+
".arcane/spells/spell-status.md",
|
|
533
555
|
".github/prompts/spell-status.prompt.md",
|
|
534
556
|
".claude/commands/spell-status.md",
|
|
535
557
|
".agents/skills/spell-status/SKILL.md",
|
|
558
|
+
".arcane/spells/spell-arcane-version.md",
|
|
536
559
|
".github/prompts/spell-arcane-version.prompt.md",
|
|
537
560
|
".claude/commands/spell-arcane-version.md",
|
|
538
561
|
".agents/skills/spell-arcane-version/SKILL.md"
|
|
@@ -542,27 +565,35 @@ var COMPONENTS = [
|
|
|
542
565
|
name: "spells-capture",
|
|
543
566
|
description: "Idea, todo, feedback and explanation capture",
|
|
544
567
|
files: [
|
|
568
|
+
".arcane/spells/spell-save-idea.md",
|
|
545
569
|
".github/prompts/spell-save-idea.prompt.md",
|
|
546
570
|
".claude/commands/spell-save-idea.md",
|
|
547
571
|
".agents/skills/spell-save-idea/SKILL.md",
|
|
572
|
+
".arcane/spells/spell-todo.md",
|
|
548
573
|
".github/prompts/spell-todo.prompt.md",
|
|
549
574
|
".claude/commands/spell-todo.md",
|
|
550
575
|
".agents/skills/spell-todo/SKILL.md",
|
|
576
|
+
".arcane/spells/spell-feedback.md",
|
|
551
577
|
".github/prompts/spell-feedback.prompt.md",
|
|
552
578
|
".claude/commands/spell-feedback.md",
|
|
553
579
|
".agents/skills/spell-feedback/SKILL.md",
|
|
580
|
+
".arcane/spells/spell-suggest-feature.md",
|
|
554
581
|
".github/prompts/spell-suggest-feature.prompt.md",
|
|
555
582
|
".claude/commands/spell-suggest-feature.md",
|
|
556
583
|
".agents/skills/spell-suggest-feature/SKILL.md",
|
|
584
|
+
".arcane/spells/spell-document.md",
|
|
557
585
|
".github/prompts/spell-document.prompt.md",
|
|
558
586
|
".claude/commands/spell-document.md",
|
|
559
587
|
".agents/skills/spell-document/SKILL.md",
|
|
588
|
+
".arcane/spells/spell-verification-ledger.md",
|
|
560
589
|
".github/prompts/spell-verification-ledger.prompt.md",
|
|
561
590
|
".claude/commands/spell-verification-ledger.md",
|
|
562
591
|
".agents/skills/spell-verification-ledger/SKILL.md",
|
|
592
|
+
".arcane/spells/spell-brainstorm.md",
|
|
563
593
|
".github/prompts/spell-brainstorm.prompt.md",
|
|
564
594
|
".claude/commands/spell-brainstorm.md",
|
|
565
595
|
".agents/skills/spell-brainstorm/SKILL.md",
|
|
596
|
+
".arcane/spells/spell-explain-concept.md",
|
|
566
597
|
".github/prompts/spell-explain-concept.prompt.md",
|
|
567
598
|
".claude/commands/spell-explain-concept.md",
|
|
568
599
|
".agents/skills/spell-explain-concept/SKILL.md"
|
|
@@ -572,12 +603,15 @@ var COMPONENTS = [
|
|
|
572
603
|
name: "spells-delivery",
|
|
573
604
|
description: "Pull-request lifecycle \u2014 open a PR, sync a stale one, respond to review feedback",
|
|
574
605
|
files: [
|
|
606
|
+
".arcane/spells/spell-create-pull-request.md",
|
|
575
607
|
".github/prompts/spell-create-pull-request.prompt.md",
|
|
576
608
|
".claude/commands/spell-create-pull-request.md",
|
|
577
609
|
".agents/skills/spell-create-pull-request/SKILL.md",
|
|
610
|
+
".arcane/spells/spell-sync-pull-request.md",
|
|
578
611
|
".github/prompts/spell-sync-pull-request.prompt.md",
|
|
579
612
|
".claude/commands/spell-sync-pull-request.md",
|
|
580
613
|
".agents/skills/spell-sync-pull-request/SKILL.md",
|
|
614
|
+
".arcane/spells/spell-address-review.md",
|
|
581
615
|
".github/prompts/spell-address-review.prompt.md",
|
|
582
616
|
".claude/commands/spell-address-review.md",
|
|
583
617
|
".agents/skills/spell-address-review/SKILL.md"
|
|
@@ -587,9 +621,11 @@ var COMPONENTS = [
|
|
|
587
621
|
name: "spells-review",
|
|
588
622
|
description: "Adversarial code review \u2014 requires source and tests, so excluded from docs-only profiles",
|
|
589
623
|
files: [
|
|
624
|
+
".arcane/spells/spell-review.md",
|
|
590
625
|
".github/prompts/spell-review.prompt.md",
|
|
591
626
|
".claude/commands/spell-review.md",
|
|
592
627
|
".agents/skills/spell-review/SKILL.md",
|
|
628
|
+
".arcane/spells/spell-review-batch.md",
|
|
593
629
|
".github/prompts/spell-review-batch.prompt.md",
|
|
594
630
|
".claude/commands/spell-review-batch.md",
|
|
595
631
|
".agents/skills/spell-review-batch/SKILL.md"
|
|
@@ -599,15 +635,19 @@ var COMPONENTS = [
|
|
|
599
635
|
name: "spells-planning",
|
|
600
636
|
description: "PRD, architecture, scoping and product review",
|
|
601
637
|
files: [
|
|
638
|
+
".arcane/spells/spell-plan.md",
|
|
602
639
|
".github/prompts/spell-plan.prompt.md",
|
|
603
640
|
".claude/commands/spell-plan.md",
|
|
604
641
|
".agents/skills/spell-plan/SKILL.md",
|
|
642
|
+
".arcane/spells/spell-architect.md",
|
|
605
643
|
".github/prompts/spell-architect.prompt.md",
|
|
606
644
|
".claude/commands/spell-architect.md",
|
|
607
645
|
".agents/skills/spell-architect/SKILL.md",
|
|
646
|
+
".arcane/spells/spell-scope.md",
|
|
608
647
|
".github/prompts/spell-scope.prompt.md",
|
|
609
648
|
".claude/commands/spell-scope.md",
|
|
610
649
|
".agents/skills/spell-scope/SKILL.md",
|
|
650
|
+
".arcane/spells/spell-product-review.md",
|
|
611
651
|
".github/prompts/spell-product-review.prompt.md",
|
|
612
652
|
".claude/commands/spell-product-review.md",
|
|
613
653
|
".agents/skills/spell-product-review/SKILL.md"
|
|
@@ -617,45 +657,59 @@ var COMPONENTS = [
|
|
|
617
657
|
name: "spells-build",
|
|
618
658
|
description: "Code and product delivery \u2014 implementation, tests, stack experts, release, deployment, asset tooling",
|
|
619
659
|
files: [
|
|
660
|
+
".arcane/spells/spell-implement.md",
|
|
620
661
|
".github/prompts/spell-implement.prompt.md",
|
|
621
662
|
".claude/commands/spell-implement.md",
|
|
622
663
|
".agents/skills/spell-implement/SKILL.md",
|
|
664
|
+
".arcane/spells/spell-test.md",
|
|
623
665
|
".github/prompts/spell-test.prompt.md",
|
|
624
666
|
".claude/commands/spell-test.md",
|
|
625
667
|
".agents/skills/spell-test/SKILL.md",
|
|
668
|
+
".arcane/spells/spell-full-cycle.md",
|
|
626
669
|
".github/prompts/spell-full-cycle.prompt.md",
|
|
627
670
|
".claude/commands/spell-full-cycle.md",
|
|
628
671
|
".agents/skills/spell-full-cycle/SKILL.md",
|
|
672
|
+
".arcane/spells/spell-bug.md",
|
|
629
673
|
".github/prompts/spell-bug.prompt.md",
|
|
630
674
|
".claude/commands/spell-bug.md",
|
|
631
675
|
".agents/skills/spell-bug/SKILL.md",
|
|
676
|
+
".arcane/spells/spell-bump.md",
|
|
632
677
|
".github/prompts/spell-bump.prompt.md",
|
|
633
678
|
".claude/commands/spell-bump.md",
|
|
634
679
|
".agents/skills/spell-bump/SKILL.md",
|
|
680
|
+
".arcane/spells/spell-dotnet-expert.md",
|
|
635
681
|
".github/prompts/spell-dotnet-expert.prompt.md",
|
|
636
682
|
".claude/commands/spell-dotnet-expert.md",
|
|
637
683
|
".agents/skills/spell-dotnet-expert/SKILL.md",
|
|
684
|
+
".arcane/spells/spell-security-review.md",
|
|
638
685
|
".github/prompts/spell-security-review.prompt.md",
|
|
639
686
|
".claude/commands/spell-security-review.md",
|
|
640
687
|
".agents/skills/spell-security-review/SKILL.md",
|
|
688
|
+
".arcane/spells/spell-ship.md",
|
|
641
689
|
".github/prompts/spell-ship.prompt.md",
|
|
642
690
|
".claude/commands/spell-ship.md",
|
|
643
691
|
".agents/skills/spell-ship/SKILL.md",
|
|
692
|
+
".arcane/spells/spell-enchant.md",
|
|
644
693
|
".github/prompts/spell-enchant.prompt.md",
|
|
645
694
|
".claude/commands/spell-enchant.md",
|
|
646
695
|
".agents/skills/spell-enchant/SKILL.md",
|
|
696
|
+
".arcane/spells/spell-generate-bot-icons.md",
|
|
647
697
|
".github/prompts/spell-generate-bot-icons.prompt.md",
|
|
648
698
|
".claude/commands/spell-generate-bot-icons.md",
|
|
649
699
|
".agents/skills/spell-generate-bot-icons/SKILL.md",
|
|
700
|
+
".arcane/spells/spell-make-discoverable.md",
|
|
650
701
|
".github/prompts/spell-make-discoverable.prompt.md",
|
|
651
702
|
".claude/commands/spell-make-discoverable.md",
|
|
652
703
|
".agents/skills/spell-make-discoverable/SKILL.md",
|
|
704
|
+
".arcane/spells/spell-scry.md",
|
|
653
705
|
".github/prompts/spell-scry.prompt.md",
|
|
654
706
|
".claude/commands/spell-scry.md",
|
|
655
707
|
".agents/skills/spell-scry/SKILL.md",
|
|
708
|
+
".arcane/spells/spell-eas-store-deploy.md",
|
|
656
709
|
".github/prompts/spell-eas-store-deploy.prompt.md",
|
|
657
710
|
".claude/commands/spell-eas-store-deploy.md",
|
|
658
711
|
".agents/skills/spell-eas-store-deploy/SKILL.md",
|
|
712
|
+
".arcane/spells/spell-compliance.md",
|
|
659
713
|
".github/prompts/spell-compliance.prompt.md",
|
|
660
714
|
".claude/commands/spell-compliance.md",
|
|
661
715
|
".agents/skills/spell-compliance/SKILL.md"
|
|
@@ -665,6 +719,7 @@ var COMPONENTS = [
|
|
|
665
719
|
name: "spells-docs",
|
|
666
720
|
description: "Documentation and records workflows \u2014 adopt an existing document tree",
|
|
667
721
|
files: [
|
|
722
|
+
".arcane/spells/spell-adopt-docs.md",
|
|
668
723
|
".github/prompts/spell-adopt-docs.prompt.md",
|
|
669
724
|
".claude/commands/spell-adopt-docs.md",
|
|
670
725
|
".agents/skills/spell-adopt-docs/SKILL.md"
|
|
@@ -674,9 +729,11 @@ var COMPONENTS = [
|
|
|
674
729
|
name: "spells-venture",
|
|
675
730
|
description: "Hub-only venture management",
|
|
676
731
|
files: [
|
|
732
|
+
".arcane/spells/spell-summon-venture.md",
|
|
677
733
|
".github/prompts/spell-summon-venture.prompt.md",
|
|
678
734
|
".claude/commands/spell-summon-venture.md",
|
|
679
735
|
".agents/skills/spell-summon-venture/SKILL.md",
|
|
736
|
+
".arcane/spells/spell-manifest.md",
|
|
680
737
|
".github/prompts/spell-manifest.prompt.md",
|
|
681
738
|
".claude/commands/spell-manifest.md",
|
|
682
739
|
".agents/skills/spell-manifest/SKILL.md"
|
|
@@ -686,9 +743,11 @@ var COMPONENTS = [
|
|
|
686
743
|
name: "spells-meta",
|
|
687
744
|
description: "Arcane-about-Arcane \u2014 presentation and documentation drift detection",
|
|
688
745
|
files: [
|
|
746
|
+
".arcane/spells/spell-present-arcane.md",
|
|
689
747
|
".github/prompts/spell-present-arcane.prompt.md",
|
|
690
748
|
".claude/commands/spell-present-arcane.md",
|
|
691
749
|
".agents/skills/spell-present-arcane/SKILL.md",
|
|
750
|
+
".arcane/spells/spell-check-drift.md",
|
|
692
751
|
".github/prompts/spell-check-drift.prompt.md",
|
|
693
752
|
".claude/commands/spell-check-drift.md",
|
|
694
753
|
".agents/skills/spell-check-drift/SKILL.md"
|
|
@@ -778,6 +837,400 @@ function listProfiles() {
|
|
|
778
837
|
}));
|
|
779
838
|
}
|
|
780
839
|
|
|
840
|
+
// src/modules/user-tier.ts
|
|
841
|
+
import { createHash as createHash2 } from "crypto";
|
|
842
|
+
import { lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm, rmdir, writeFile as writeFile3 } from "fs/promises";
|
|
843
|
+
import { homedir } from "os";
|
|
844
|
+
import { dirname, join, resolve } from "path";
|
|
845
|
+
|
|
846
|
+
// src/modules/spell-compiler.ts
|
|
847
|
+
import { parse as parseYaml } from "yaml";
|
|
848
|
+
var MissingFrontmatterError = class extends Error {
|
|
849
|
+
constructor(message) {
|
|
850
|
+
super(message);
|
|
851
|
+
this.name = "MissingFrontmatterError";
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
var InvalidSkillNameError = class extends Error {
|
|
855
|
+
constructor(message) {
|
|
856
|
+
super(message);
|
|
857
|
+
this.name = "InvalidSkillNameError";
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
var CANONICAL_SPELLS_DIR = ".arcane/spells";
|
|
861
|
+
function canonicalSpellPath(id) {
|
|
862
|
+
return `${CANONICAL_SPELLS_DIR}/${id}.md`;
|
|
863
|
+
}
|
|
864
|
+
var CLIENT_SHIM_PATH_PATTERNS = [
|
|
865
|
+
/^\.github\/prompts\/spell-[a-z0-9-]+\.prompt\.md$/,
|
|
866
|
+
/^\.claude\/commands\/spell-[a-z0-9-]+\.md$/,
|
|
867
|
+
/^\.agents\/skills\/spell-[a-z0-9-]+\/SKILL\.md$/
|
|
868
|
+
];
|
|
869
|
+
function isClientShimPath(path4) {
|
|
870
|
+
const normalized = path4.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
871
|
+
return CLIENT_SHIM_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
872
|
+
}
|
|
873
|
+
var FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
874
|
+
function parsePromptFrontmatter(content) {
|
|
875
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
876
|
+
if (!match) {
|
|
877
|
+
throw new MissingFrontmatterError(
|
|
878
|
+
"Prompt file has no YAML frontmatter block (expected a leading --- ... --- block)."
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
const raw = parseYaml(match[1]);
|
|
882
|
+
const name = raw?.["name"];
|
|
883
|
+
const description = raw?.["description"];
|
|
884
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
885
|
+
throw new MissingFrontmatterError('Prompt frontmatter is missing a "name" field.');
|
|
886
|
+
}
|
|
887
|
+
if (typeof description !== "string" || description.trim() === "") {
|
|
888
|
+
throw new MissingFrontmatterError('Prompt frontmatter is missing a "description" field.');
|
|
889
|
+
}
|
|
890
|
+
const claudeDescription = raw?.["claude_description"];
|
|
891
|
+
return {
|
|
892
|
+
name,
|
|
893
|
+
description,
|
|
894
|
+
claudeDescription: typeof claudeDescription === "string" ? claudeDescription : void 0
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
var SPELL_NAME_PREFIX = /^Spell\s*—\s*/;
|
|
898
|
+
function deriveStubTitle(name) {
|
|
899
|
+
return name.replace(SPELL_NAME_PREFIX, "").trim();
|
|
900
|
+
}
|
|
901
|
+
function renderClaudeCommandStub(id, frontmatter, canonicalRef = canonicalSpellPath(id)) {
|
|
902
|
+
const title = deriveStubTitle(frontmatter.name);
|
|
903
|
+
const description = frontmatter.claudeDescription ?? frontmatter.description;
|
|
904
|
+
const promptPath = canonicalRef;
|
|
905
|
+
return `---
|
|
906
|
+
description: ${description}
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
# ${title}
|
|
910
|
+
|
|
911
|
+
Invoke the Arcane \`${id}\` spell workflow.
|
|
912
|
+
|
|
913
|
+
See the full prompt at \`${promptPath}\` for the complete workflow definition.
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
@${promptPath}
|
|
918
|
+
`;
|
|
919
|
+
}
|
|
920
|
+
var SKILL_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
921
|
+
function renderCodexSkill(id, frontmatter, canonicalPath) {
|
|
922
|
+
if (!SKILL_NAME_PATTERN.test(id)) {
|
|
923
|
+
throw new InvalidSkillNameError(
|
|
924
|
+
`Skill id "${id}" must match ${SKILL_NAME_PATTERN} (lowercase letters, digits, hyphens only).`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const description = frontmatter.claudeDescription ?? frontmatter.description;
|
|
928
|
+
return `---
|
|
929
|
+
name: ${id}
|
|
930
|
+
description: ${description}
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
This skill is the Arcane \`${id}\` spell. Read \`${canonicalPath}\` and follow it as the complete workflow.
|
|
934
|
+
`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/modules/user-tier.ts
|
|
938
|
+
var USER_TIER_DIR_NAME = ".arcane";
|
|
939
|
+
var USER_TIER_SPELLS_DIR = "spells";
|
|
940
|
+
function resolveHomeDir() {
|
|
941
|
+
return homedir();
|
|
942
|
+
}
|
|
943
|
+
function userTierRoot(homeDir = resolveHomeDir()) {
|
|
944
|
+
return join(homeDir, USER_TIER_DIR_NAME);
|
|
945
|
+
}
|
|
946
|
+
var USER_TIER_COMPONENTS = SPELL_COMPONENT_NAMES;
|
|
947
|
+
var CANONICAL_SPELL_PATH_PATTERN = /^\.arcane\/spells\/(spell-[a-z0-9-]+)\.md$/;
|
|
948
|
+
var STORE_SPELL_PATH_PATTERN = /^spells\/(spell-[a-z0-9-]+)\.md$/;
|
|
949
|
+
function storeSpellPath(id) {
|
|
950
|
+
return `${USER_TIER_SPELLS_DIR}/${id}.md`;
|
|
951
|
+
}
|
|
952
|
+
function spellIdFromCanonicalPath(path4) {
|
|
953
|
+
return path4.replace(/\\/g, "/").match(CANONICAL_SPELL_PATH_PATTERN)?.[1];
|
|
954
|
+
}
|
|
955
|
+
function spellIdFromStorePath(path4) {
|
|
956
|
+
return path4.replace(/\\/g, "/").match(STORE_SPELL_PATH_PATTERN)?.[1];
|
|
957
|
+
}
|
|
958
|
+
function absoluteStoreSpellPath(storeRoot, id) {
|
|
959
|
+
return resolve(storeRoot, storeSpellPath(id)).replace(/\\/g, "/");
|
|
960
|
+
}
|
|
961
|
+
function componentForScope(component, scope) {
|
|
962
|
+
if (scope === "repo") return component;
|
|
963
|
+
const files = [];
|
|
964
|
+
const sourceOverrides = {};
|
|
965
|
+
for (const file of component.files) {
|
|
966
|
+
const id = spellIdFromCanonicalPath(file);
|
|
967
|
+
if (id === void 0) continue;
|
|
968
|
+
const installed = storeSpellPath(id);
|
|
969
|
+
files.push(installed);
|
|
970
|
+
sourceOverrides[installed] = component.sourceOverrides?.[file] ?? file;
|
|
971
|
+
}
|
|
972
|
+
return { name: component.name, description: component.description, files, sourceOverrides };
|
|
973
|
+
}
|
|
974
|
+
function spellIdsInStore(components) {
|
|
975
|
+
const ids = /* @__PURE__ */ new Set();
|
|
976
|
+
for (const component of components) {
|
|
977
|
+
for (const file of component.files) {
|
|
978
|
+
const id = spellIdFromStorePath(file);
|
|
979
|
+
if (id !== void 0) ids.add(id);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return [...ids].sort();
|
|
983
|
+
}
|
|
984
|
+
var USER_FANOUT_PATHS = {
|
|
985
|
+
codex: (id) => `.agents/skills/${id}/SKILL.md`,
|
|
986
|
+
claude: (id) => `.claude/commands/${id}.md`
|
|
987
|
+
};
|
|
988
|
+
var FANOUT_CLIENT_LABELS = {
|
|
989
|
+
codex: "Codex/Copilot skills",
|
|
990
|
+
claude: "Claude Code commands"
|
|
991
|
+
};
|
|
992
|
+
function clientOfFanoutPath(relativePath) {
|
|
993
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
994
|
+
if (normalized.startsWith(".agents/skills/")) return "codex";
|
|
995
|
+
if (normalized.startsWith(".claude/commands/")) return "claude";
|
|
996
|
+
return void 0;
|
|
997
|
+
}
|
|
998
|
+
async function readSpellSource(storeRoot, id, fallbackDir) {
|
|
999
|
+
for (const candidate of [
|
|
1000
|
+
join(storeRoot, storeSpellPath(id)),
|
|
1001
|
+
...fallbackDir ? [join(fallbackDir, canonicalSpellPath(id))] : []
|
|
1002
|
+
]) {
|
|
1003
|
+
try {
|
|
1004
|
+
return await readFile3(candidate, "utf-8");
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
if (err.code !== "ENOENT") throw err;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return void 0;
|
|
1010
|
+
}
|
|
1011
|
+
async function renderUserFanout(storeRoot, spellIds, fallbackDir) {
|
|
1012
|
+
const files = [];
|
|
1013
|
+
const missing = [];
|
|
1014
|
+
for (const id of [...spellIds].sort()) {
|
|
1015
|
+
const canonical = await readSpellSource(storeRoot, id, fallbackDir);
|
|
1016
|
+
if (canonical === void 0) {
|
|
1017
|
+
missing.push(id);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
const frontmatter = parsePromptFrontmatter(canonical);
|
|
1021
|
+
const absolutePath = absoluteStoreSpellPath(storeRoot, id);
|
|
1022
|
+
files.push({
|
|
1023
|
+
relativePath: USER_FANOUT_PATHS.codex(id),
|
|
1024
|
+
client: "codex",
|
|
1025
|
+
content: renderCodexSkill(id, frontmatter, absolutePath)
|
|
1026
|
+
});
|
|
1027
|
+
files.push({
|
|
1028
|
+
relativePath: USER_FANOUT_PATHS.claude(id),
|
|
1029
|
+
client: "claude",
|
|
1030
|
+
content: renderClaudeCommandStub(id, frontmatter, absolutePath)
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
return { files, missing };
|
|
1034
|
+
}
|
|
1035
|
+
async function classifyTarget(path4) {
|
|
1036
|
+
try {
|
|
1037
|
+
const stats = await lstat(path4);
|
|
1038
|
+
return stats.isFile() ? "file" : "other";
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
if (err.code === "ENOENT") return "absent";
|
|
1041
|
+
throw err;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function sha256(content) {
|
|
1045
|
+
return createHash2("sha256").update(content, "utf-8").digest("hex");
|
|
1046
|
+
}
|
|
1047
|
+
function perSpellDirectory(homeDir, relativePath) {
|
|
1048
|
+
if (clientOfFanoutPath(relativePath) !== "codex") return void 0;
|
|
1049
|
+
return dirname(join(homeDir, relativePath));
|
|
1050
|
+
}
|
|
1051
|
+
async function removeDirectoryIfEmpty(dir) {
|
|
1052
|
+
try {
|
|
1053
|
+
const entries = await readdir2(dir);
|
|
1054
|
+
if (entries.length === 0) await rmdir(dir);
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function syncUserTierFanout(options) {
|
|
1059
|
+
const rendered = await renderUserFanout(options.storeRoot, options.spellIds, options.fallbackDir);
|
|
1060
|
+
const desired = rendered.files;
|
|
1061
|
+
const previous = options.previous ?? {};
|
|
1062
|
+
const dryRun = Boolean(options.dryRun);
|
|
1063
|
+
const record = {};
|
|
1064
|
+
const outcomes = [];
|
|
1065
|
+
const desiredPaths = new Set(desired.map((f) => f.relativePath));
|
|
1066
|
+
for (const id of rendered.missing) {
|
|
1067
|
+
for (const client of ["codex", "claude"]) {
|
|
1068
|
+
const relativePath = USER_FANOUT_PATHS[client](id);
|
|
1069
|
+
desiredPaths.add(relativePath);
|
|
1070
|
+
if (previous[relativePath] !== void 0) record[relativePath] = previous[relativePath];
|
|
1071
|
+
outcomes.push({ relativePath, status: "unrenderable" });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const file of desired) {
|
|
1075
|
+
const target = join(options.homeDir, file.relativePath);
|
|
1076
|
+
const recorded = previous[file.relativePath];
|
|
1077
|
+
const newHash = sha256(file.content);
|
|
1078
|
+
const state = await classifyTarget(target);
|
|
1079
|
+
if (state === "absent") {
|
|
1080
|
+
if (!dryRun) {
|
|
1081
|
+
await mkdir2(dirname(target), { recursive: true });
|
|
1082
|
+
await writeFile3(target, file.content, "utf-8");
|
|
1083
|
+
}
|
|
1084
|
+
record[file.relativePath] = newHash;
|
|
1085
|
+
outcomes.push({ relativePath: file.relativePath, status: "written" });
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (state === "other" || recorded === void 0) {
|
|
1089
|
+
outcomes.push({ relativePath: file.relativePath, status: "collision" });
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const currentHash = await hashFile(target);
|
|
1093
|
+
if (currentHash !== recorded) {
|
|
1094
|
+
record[file.relativePath] = recorded;
|
|
1095
|
+
outcomes.push({ relativePath: file.relativePath, status: "customized" });
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
if (currentHash === newHash) {
|
|
1099
|
+
record[file.relativePath] = newHash;
|
|
1100
|
+
outcomes.push({ relativePath: file.relativePath, status: "unchanged" });
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
if (!dryRun) await writeFile3(target, file.content, "utf-8");
|
|
1104
|
+
record[file.relativePath] = newHash;
|
|
1105
|
+
outcomes.push({ relativePath: file.relativePath, status: "written" });
|
|
1106
|
+
}
|
|
1107
|
+
for (const [relativePath, recorded] of Object.entries(previous)) {
|
|
1108
|
+
if (desiredPaths.has(relativePath)) continue;
|
|
1109
|
+
const target = join(options.homeDir, relativePath);
|
|
1110
|
+
const state = await classifyTarget(target);
|
|
1111
|
+
if (state === "absent") {
|
|
1112
|
+
outcomes.push({ relativePath, status: "gone" });
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
if (state === "other" || await hashFile(target) !== recorded) {
|
|
1116
|
+
record[relativePath] = recorded;
|
|
1117
|
+
outcomes.push({ relativePath, status: "kept-edited" });
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (!dryRun) {
|
|
1121
|
+
await rm(target, { force: true });
|
|
1122
|
+
const spellDir = perSpellDirectory(options.homeDir, relativePath);
|
|
1123
|
+
if (spellDir !== void 0) await removeDirectoryIfEmpty(spellDir);
|
|
1124
|
+
}
|
|
1125
|
+
outcomes.push({ relativePath, status: "pruned" });
|
|
1126
|
+
}
|
|
1127
|
+
return { record, outcomes };
|
|
1128
|
+
}
|
|
1129
|
+
function summarizeFanout(outcomes) {
|
|
1130
|
+
const counts = {
|
|
1131
|
+
written: 0,
|
|
1132
|
+
unchanged: 0,
|
|
1133
|
+
customized: 0,
|
|
1134
|
+
collision: 0,
|
|
1135
|
+
pruned: 0,
|
|
1136
|
+
"kept-edited": 0,
|
|
1137
|
+
gone: 0,
|
|
1138
|
+
unrenderable: 0
|
|
1139
|
+
};
|
|
1140
|
+
for (const outcome of outcomes) counts[outcome.status]++;
|
|
1141
|
+
return counts;
|
|
1142
|
+
}
|
|
1143
|
+
function describeFanoutOutcomes(outcomes, dryRun = false) {
|
|
1144
|
+
const counts = summarizeFanout(outcomes);
|
|
1145
|
+
const lines = [];
|
|
1146
|
+
const prefix = dryRun ? "[dry-run] Would " : "";
|
|
1147
|
+
const byClient = (status) => {
|
|
1148
|
+
const n = { codex: 0, claude: 0 };
|
|
1149
|
+
for (const o of outcomes) {
|
|
1150
|
+
if (o.status !== status) continue;
|
|
1151
|
+
const client = clientOfFanoutPath(o.relativePath);
|
|
1152
|
+
if (client !== void 0) n[client]++;
|
|
1153
|
+
}
|
|
1154
|
+
return n;
|
|
1155
|
+
};
|
|
1156
|
+
const paths = (status) => outcomes.filter((o) => o.status === status).map((o) => ` ~/${o.relativePath}`).join("\n");
|
|
1157
|
+
if (counts.written > 0) {
|
|
1158
|
+
const n = byClient("written");
|
|
1159
|
+
lines.push(
|
|
1160
|
+
`${prefix}${dryRun ? "write" : "Wrote"} ${counts.written} client file(s): ${n.codex} ${FANOUT_CLIENT_LABELS.codex}, ${n.claude} ${FANOUT_CLIENT_LABELS.claude}.`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
if (counts.pruned > 0) {
|
|
1164
|
+
lines.push(`${prefix}${dryRun ? "remove" : "Removed"} ${counts.pruned} client file(s) no longer needed.`);
|
|
1165
|
+
}
|
|
1166
|
+
if (counts.customized > 0) {
|
|
1167
|
+
lines.push(
|
|
1168
|
+
`! ${dryRun ? "Would keep" : "Kept"} ${counts.customized} customized client file(s) untouched (edited since Arcane wrote them):
|
|
1169
|
+
${paths("customized")}
|
|
1170
|
+
Delete one to have it regenerated by the next \`spell update --user\`.`
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
if (counts.collision > 0) {
|
|
1174
|
+
lines.push(
|
|
1175
|
+
`! ${dryRun ? "Would leave" : "Left"} ${counts.collision} existing path(s) alone (not written by Arcane \u2014 a skill or command of the same name, a directory or a link already exists there):
|
|
1176
|
+
${paths("collision")}
|
|
1177
|
+
Move or rename it and run \`spell update --user\` to install Arcane's.`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
if (counts.unrenderable > 0) {
|
|
1181
|
+
lines.push(
|
|
1182
|
+
`! ${dryRun ? "Would keep" : "Kept"} ${counts.unrenderable} client file(s) whose spell source is missing from the store and from this CLI (left as they are):
|
|
1183
|
+
${paths("unrenderable")}
|
|
1184
|
+
Restore the spell with \`spell update --user\`, or delete these files if the spell is no longer shipped.`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
if (counts["kept-edited"] > 0) {
|
|
1188
|
+
lines.push(
|
|
1189
|
+
`! ${dryRun ? "Would keep" : "Kept"} ${counts["kept-edited"]} edited client file(s) that Arcane no longer needs (delete them yourself if unwanted):
|
|
1190
|
+
${paths("kept-edited")}`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
return lines;
|
|
1194
|
+
}
|
|
1195
|
+
async function inspectUserTierFanout(homeDir, record) {
|
|
1196
|
+
const health = {
|
|
1197
|
+
total: 0,
|
|
1198
|
+
byClient: { codex: 0, claude: 0 },
|
|
1199
|
+
missing: [],
|
|
1200
|
+
customized: []
|
|
1201
|
+
};
|
|
1202
|
+
for (const [relativePath, recorded] of Object.entries(record ?? {})) {
|
|
1203
|
+
health.total++;
|
|
1204
|
+
const client = clientOfFanoutPath(relativePath);
|
|
1205
|
+
if (client !== void 0) health.byClient[client]++;
|
|
1206
|
+
const target = join(homeDir, relativePath);
|
|
1207
|
+
const state = await classifyTarget(target);
|
|
1208
|
+
if (state === "absent") {
|
|
1209
|
+
health.missing.push(relativePath);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (state === "other" || await hashFile(target) !== recorded) health.customized.push(relativePath);
|
|
1213
|
+
}
|
|
1214
|
+
return health;
|
|
1215
|
+
}
|
|
1216
|
+
function isUserTierStore(targetDir) {
|
|
1217
|
+
return resolve(targetDir) === resolve(userTierRoot());
|
|
1218
|
+
}
|
|
1219
|
+
function resolveInstallScope(targetDir, manifestScope, userFlag) {
|
|
1220
|
+
if (userFlag) return { scope: "user", misplacedUserManifest: false };
|
|
1221
|
+
if (manifestScope !== "user") return { scope: "repo", misplacedUserManifest: false };
|
|
1222
|
+
return isUserTierStore(targetDir) ? { scope: "user", misplacedUserManifest: false } : { scope: "repo", misplacedUserManifest: true };
|
|
1223
|
+
}
|
|
1224
|
+
function misplacedUserManifestMessage(targetDir) {
|
|
1225
|
+
return `This .arcane.json belongs to the user tier (scope: "user"), but ${targetDir} is not the store (${userTierRoot()}). Run the command with --user to operate on the store, or from the store directory itself.`;
|
|
1226
|
+
}
|
|
1227
|
+
var VSCODE_USER_TIER_NOTE = [
|
|
1228
|
+
"VS Code Copilot: no settings change is needed \u2014 Copilot discovers ~/.agents/skills by default",
|
|
1229
|
+
"(chat.useAgentSkills, on by default) and lists each spell under /. Reload the window after installing.",
|
|
1230
|
+
"VS Code has deprecated chat.promptFilesLocations; the user tier does not use it."
|
|
1231
|
+
];
|
|
1232
|
+
var CLAUDE_PRECEDENCE_NOTE = "Claude Code runs a personal command over a project command of the same name, so /spell-* in a repository that still carries its own shims now runs the user tier's copy.";
|
|
1233
|
+
|
|
781
1234
|
// src/modules/banner.ts
|
|
782
1235
|
import chalk from "chalk";
|
|
783
1236
|
import gradient from "gradient-string";
|
|
@@ -800,7 +1253,7 @@ var WORDMARK = [
|
|
|
800
1253
|
var INDENT = " ";
|
|
801
1254
|
var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
|
|
802
1255
|
function sleep(ms) {
|
|
803
|
-
return new Promise((
|
|
1256
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
804
1257
|
}
|
|
805
1258
|
function isAnimatable() {
|
|
806
1259
|
return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
|
|
@@ -918,8 +1371,8 @@ function printWarning(message) {
|
|
|
918
1371
|
|
|
919
1372
|
// src/modules/agents.ts
|
|
920
1373
|
import { select, checkbox, Separator } from "@inquirer/prompts";
|
|
921
|
-
import { mkdir as
|
|
922
|
-
import { join as
|
|
1374
|
+
import { mkdir as mkdir4, writeFile as writeFile6, copyFile as copyFile2 } from "fs/promises";
|
|
1375
|
+
import { join as join4 } from "path";
|
|
923
1376
|
import { stringify } from "yaml";
|
|
924
1377
|
|
|
925
1378
|
// src/config/agent-roles.ts
|
|
@@ -1154,8 +1607,8 @@ async function applyNamingStrategy(strategy, roleIds) {
|
|
|
1154
1607
|
}
|
|
1155
1608
|
|
|
1156
1609
|
// src/modules/agent-loader.ts
|
|
1157
|
-
import { readFile as
|
|
1158
|
-
import { join } from "path";
|
|
1610
|
+
import { readFile as readFile4, readdir as readdir3, access as access2 } from "fs/promises";
|
|
1611
|
+
import { join as join2 } from "path";
|
|
1159
1612
|
import { parse } from "yaml";
|
|
1160
1613
|
|
|
1161
1614
|
// src/modules/agent-schema.ts
|
|
@@ -1335,7 +1788,7 @@ var AgentRosterNotFoundError = class extends Error {
|
|
|
1335
1788
|
this.name = "AgentRosterNotFoundError";
|
|
1336
1789
|
}
|
|
1337
1790
|
};
|
|
1338
|
-
function
|
|
1791
|
+
function parseYaml2(content, source) {
|
|
1339
1792
|
try {
|
|
1340
1793
|
return parse(content);
|
|
1341
1794
|
} catch (err) {
|
|
@@ -1344,44 +1797,44 @@ function parseYaml(content, source) {
|
|
|
1344
1797
|
}
|
|
1345
1798
|
}
|
|
1346
1799
|
async function loadAgentDefinition(agentsDir, id) {
|
|
1347
|
-
const filePath =
|
|
1800
|
+
const filePath = join2(agentsDir, `${id}.yaml`);
|
|
1348
1801
|
let content;
|
|
1349
1802
|
try {
|
|
1350
|
-
content = await
|
|
1803
|
+
content = await readFile4(filePath, "utf8");
|
|
1351
1804
|
} catch {
|
|
1352
1805
|
throw new AgentDefinitionNotFoundError(id, agentsDir);
|
|
1353
1806
|
}
|
|
1354
|
-
return validateAgentDefinition(
|
|
1807
|
+
return validateAgentDefinition(parseYaml2(content, filePath), filePath);
|
|
1355
1808
|
}
|
|
1356
1809
|
async function loadRoster(targetDir) {
|
|
1357
|
-
const rosterPath =
|
|
1810
|
+
const rosterPath = join2(targetDir, ".arcane", "agents.yaml");
|
|
1358
1811
|
let content;
|
|
1359
1812
|
try {
|
|
1360
|
-
content = await
|
|
1813
|
+
content = await readFile4(rosterPath, "utf8");
|
|
1361
1814
|
} catch {
|
|
1362
1815
|
throw new AgentRosterNotFoundError();
|
|
1363
1816
|
}
|
|
1364
|
-
return validateAgentRoster(
|
|
1817
|
+
return validateAgentRoster(parseYaml2(content, rosterPath), rosterPath);
|
|
1365
1818
|
}
|
|
1366
1819
|
async function rosterExists(targetDir) {
|
|
1367
1820
|
try {
|
|
1368
|
-
await access2(
|
|
1821
|
+
await access2(join2(targetDir, ".arcane", "agents.yaml"));
|
|
1369
1822
|
return true;
|
|
1370
1823
|
} catch {
|
|
1371
1824
|
return false;
|
|
1372
1825
|
}
|
|
1373
1826
|
}
|
|
1374
1827
|
function projectAgentsDir(targetDir) {
|
|
1375
|
-
return
|
|
1828
|
+
return join2(targetDir, ".arcane", "agents");
|
|
1376
1829
|
}
|
|
1377
1830
|
|
|
1378
1831
|
// src/modules/agent-generator.ts
|
|
1379
|
-
import { writeFile as
|
|
1380
|
-
import { join as
|
|
1381
|
-
import { homedir } from "os";
|
|
1832
|
+
import { writeFile as writeFile5, mkdir as mkdir3 } from "fs/promises";
|
|
1833
|
+
import { join as join3 } from "path";
|
|
1834
|
+
import { homedir as homedir2 } from "os";
|
|
1382
1835
|
|
|
1383
1836
|
// src/modules/merger.ts
|
|
1384
|
-
import { readFile as
|
|
1837
|
+
import { readFile as readFile5, writeFile as writeFile4, rm as rm2 } from "fs/promises";
|
|
1385
1838
|
import path3 from "path";
|
|
1386
1839
|
var MARKER_START = "<!-- arcane:start -->";
|
|
1387
1840
|
var MARKER_END = "<!-- arcane:end -->";
|
|
@@ -1396,7 +1849,7 @@ async function mergeIntoFile(targetDir, relativePath, content, opts = {}) {
|
|
|
1396
1849
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
1397
1850
|
let existing;
|
|
1398
1851
|
try {
|
|
1399
|
-
existing = await
|
|
1852
|
+
existing = await readFile5(destPath, "utf8");
|
|
1400
1853
|
} catch (err) {
|
|
1401
1854
|
const e = err;
|
|
1402
1855
|
if (e.code === "ENOENT") {
|
|
@@ -1412,7 +1865,7 @@ ${content}
|
|
|
1412
1865
|
${MARKER_END}
|
|
1413
1866
|
`;
|
|
1414
1867
|
if (!opts.dryRun) {
|
|
1415
|
-
await
|
|
1868
|
+
await writeFile4(destPath, newContent, "utf8");
|
|
1416
1869
|
}
|
|
1417
1870
|
return true;
|
|
1418
1871
|
}
|
|
@@ -1437,7 +1890,7 @@ ${MARKER_END}
|
|
|
1437
1890
|
${content}
|
|
1438
1891
|
${afterMarker}`;
|
|
1439
1892
|
if (!opts.dryRun) {
|
|
1440
|
-
await
|
|
1893
|
+
await writeFile4(destPath, updated, "utf8");
|
|
1441
1894
|
}
|
|
1442
1895
|
return true;
|
|
1443
1896
|
}
|
|
@@ -1447,7 +1900,7 @@ ${content}
|
|
|
1447
1900
|
${MARKER_END}
|
|
1448
1901
|
`;
|
|
1449
1902
|
if (!opts.dryRun) {
|
|
1450
|
-
await
|
|
1903
|
+
await writeFile4(destPath, appended, "utf8");
|
|
1451
1904
|
}
|
|
1452
1905
|
return true;
|
|
1453
1906
|
}
|
|
@@ -1455,7 +1908,7 @@ async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
|
1455
1908
|
const destPath = path3.resolve(targetDir, relativePath);
|
|
1456
1909
|
let existing;
|
|
1457
1910
|
try {
|
|
1458
|
-
existing = await
|
|
1911
|
+
existing = await readFile5(destPath, "utf8");
|
|
1459
1912
|
} catch {
|
|
1460
1913
|
return false;
|
|
1461
1914
|
}
|
|
@@ -1467,9 +1920,9 @@ async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
|
1467
1920
|
const stripped = (before + after).replace(/\n{3,}/g, "\n\n").trim();
|
|
1468
1921
|
if (!opts.dryRun) {
|
|
1469
1922
|
if (stripped.length === 0) {
|
|
1470
|
-
await
|
|
1923
|
+
await rm2(destPath, { force: true });
|
|
1471
1924
|
} else {
|
|
1472
|
-
await
|
|
1925
|
+
await writeFile4(destPath, stripped + "\n", "utf8");
|
|
1473
1926
|
}
|
|
1474
1927
|
}
|
|
1475
1928
|
return true;
|
|
@@ -1590,7 +2043,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1590
2043
|
const skipped = [];
|
|
1591
2044
|
let hasUnresolvedRoles = false;
|
|
1592
2045
|
const projectDefs = projectAgentsDir(targetDir);
|
|
1593
|
-
const bundledDefs =
|
|
2046
|
+
const bundledDefs = join3(assetsDir, "agents");
|
|
1594
2047
|
const resolved = [];
|
|
1595
2048
|
for (const entry of roster.roster) {
|
|
1596
2049
|
let def;
|
|
@@ -1614,27 +2067,27 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1614
2067
|
if (roster.openclaw.enabled && options.openclaw !== false) {
|
|
1615
2068
|
const openclawRoot = roster.openclaw.workspace_root.replace(
|
|
1616
2069
|
/^~/,
|
|
1617
|
-
|
|
2070
|
+
homedir2()
|
|
1618
2071
|
);
|
|
1619
2072
|
for (const { entry, def, displayName } of resolved) {
|
|
1620
2073
|
if (!def.clients.openclaw) continue;
|
|
1621
2074
|
const workspaceName = entry.id === "main" ? "workspace" : `workspace-${entry.id}`;
|
|
1622
|
-
const workspaceDir =
|
|
2075
|
+
const workspaceDir = join3(openclawRoot, workspaceName);
|
|
1623
2076
|
try {
|
|
1624
2077
|
if (!options.dryRun) {
|
|
1625
|
-
await
|
|
1626
|
-
await
|
|
1627
|
-
|
|
2078
|
+
await mkdir3(workspaceDir, { recursive: true });
|
|
2079
|
+
await writeFile5(
|
|
2080
|
+
join3(workspaceDir, "IDENTITY.md"),
|
|
1628
2081
|
renderIdentity(def, displayName),
|
|
1629
2082
|
"utf8"
|
|
1630
2083
|
);
|
|
1631
|
-
await
|
|
1632
|
-
|
|
2084
|
+
await writeFile5(
|
|
2085
|
+
join3(workspaceDir, "SOUL.md"),
|
|
1633
2086
|
renderSoul(def, displayName),
|
|
1634
2087
|
"utf8"
|
|
1635
2088
|
);
|
|
1636
|
-
await
|
|
1637
|
-
|
|
2089
|
+
await writeFile5(
|
|
2090
|
+
join3(workspaceDir, "TOOLS.md"),
|
|
1638
2091
|
renderTools(def, displayName),
|
|
1639
2092
|
"utf8"
|
|
1640
2093
|
);
|
|
@@ -1659,11 +2112,11 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1659
2112
|
}
|
|
1660
2113
|
}))
|
|
1661
2114
|
};
|
|
1662
|
-
const generatedDir =
|
|
2115
|
+
const generatedDir = join3(targetDir, ".arcane", "generated");
|
|
1663
2116
|
if (!options.dryRun) {
|
|
1664
|
-
await
|
|
1665
|
-
await
|
|
1666
|
-
|
|
2117
|
+
await mkdir3(generatedDir, { recursive: true });
|
|
2118
|
+
await writeFile5(
|
|
2119
|
+
join3(generatedDir, "openclaw-roster.json"),
|
|
1667
2120
|
JSON.stringify(patch, null, 2) + "\n",
|
|
1668
2121
|
"utf8"
|
|
1669
2122
|
);
|
|
@@ -1675,17 +2128,17 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
1675
2128
|
synced.push("openclaw-roster.json");
|
|
1676
2129
|
}
|
|
1677
2130
|
if (options.copilot !== false) {
|
|
1678
|
-
const agentsDir =
|
|
2131
|
+
const agentsDir = join3(targetDir, ".github", "agents");
|
|
1679
2132
|
if (!options.dryRun) {
|
|
1680
|
-
await
|
|
2133
|
+
await mkdir3(agentsDir, { recursive: true });
|
|
1681
2134
|
}
|
|
1682
2135
|
for (const { def, displayName } of resolved) {
|
|
1683
2136
|
if (!def.clients.copilot?.agent_file) continue;
|
|
1684
2137
|
const slug = displayName.toLowerCase().replace(/\s+/g, "-");
|
|
1685
2138
|
const fileName = `${slug}.agent.md`;
|
|
1686
2139
|
if (!options.dryRun) {
|
|
1687
|
-
await
|
|
1688
|
-
|
|
2140
|
+
await writeFile5(
|
|
2141
|
+
join3(agentsDir, fileName),
|
|
1689
2142
|
renderCopilotAgent(def, displayName),
|
|
1690
2143
|
"utf8"
|
|
1691
2144
|
);
|
|
@@ -1826,11 +2279,11 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
1826
2279
|
},
|
|
1827
2280
|
roster: rosterEntries
|
|
1828
2281
|
};
|
|
1829
|
-
const arcaneDir =
|
|
2282
|
+
const arcaneDir = join4(targetDir, ".arcane", "agents");
|
|
1830
2283
|
if (!options.dryRun) {
|
|
1831
|
-
await
|
|
1832
|
-
await
|
|
1833
|
-
|
|
2284
|
+
await mkdir4(arcaneDir, { recursive: true });
|
|
2285
|
+
await writeFile6(
|
|
2286
|
+
join4(targetDir, ".arcane", "agents.yaml"),
|
|
1834
2287
|
stringify(roster),
|
|
1835
2288
|
"utf8"
|
|
1836
2289
|
);
|
|
@@ -1838,8 +2291,8 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
1838
2291
|
console.log(" [dry-run] Would write: .arcane/agents.yaml");
|
|
1839
2292
|
}
|
|
1840
2293
|
for (const roleId of selectedRoleIds) {
|
|
1841
|
-
const src =
|
|
1842
|
-
const dest =
|
|
2294
|
+
const src = join4(assetsDir, "agents", `${roleId}.yaml`);
|
|
2295
|
+
const dest = join4(arcaneDir, `${roleId}.yaml`);
|
|
1843
2296
|
if (!options.dryRun) {
|
|
1844
2297
|
try {
|
|
1845
2298
|
await copyFile2(src, dest);
|
|
@@ -1973,9 +2426,9 @@ async function runAgentsList(targetDir) {
|
|
|
1973
2426
|
}
|
|
1974
2427
|
|
|
1975
2428
|
// src/modules/push-safety.ts
|
|
1976
|
-
import { mkdir as
|
|
2429
|
+
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7, chmod, access as access3, rm as rm3, readdir as readdir4 } from "fs/promises";
|
|
1977
2430
|
import { constants } from "fs";
|
|
1978
|
-
import { join as
|
|
2431
|
+
import { join as join5, resolve as resolve2, dirname as dirname2, basename } from "path";
|
|
1979
2432
|
|
|
1980
2433
|
// src/modules/git.ts
|
|
1981
2434
|
import { execFile } from "child_process";
|
|
@@ -2072,7 +2525,7 @@ function buildGitEnv() {
|
|
|
2072
2525
|
function runGit(cwd, args, options = {}) {
|
|
2073
2526
|
const commandClass = options.commandClass ?? classifyGitCommand(args);
|
|
2074
2527
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
|
|
2075
|
-
return new Promise((
|
|
2528
|
+
return new Promise((resolve5, reject) => {
|
|
2076
2529
|
const child = execFile(
|
|
2077
2530
|
"git",
|
|
2078
2531
|
args,
|
|
@@ -2106,7 +2559,7 @@ function runGit(cwd, args, options = {}) {
|
|
|
2106
2559
|
reject(error);
|
|
2107
2560
|
return;
|
|
2108
2561
|
}
|
|
2109
|
-
|
|
2562
|
+
resolve5({ stdout, stderr });
|
|
2110
2563
|
}
|
|
2111
2564
|
);
|
|
2112
2565
|
child.stdin?.end();
|
|
@@ -2224,7 +2677,7 @@ function isArcaneHooksPath(root, value) {
|
|
|
2224
2677
|
const windows = process.platform === "win32";
|
|
2225
2678
|
const normalize = (p) => {
|
|
2226
2679
|
const separated = windows ? p.replace(/\\/g, "/") : p;
|
|
2227
|
-
const absolute =
|
|
2680
|
+
const absolute = resolve2(root, separated).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2228
2681
|
return windows ? absolute.toLowerCase() : absolute;
|
|
2229
2682
|
};
|
|
2230
2683
|
return normalize(value) === normalize(ARCANE_HOOKS_DIR);
|
|
@@ -2272,7 +2725,7 @@ async function repositoryRoot(cwd) {
|
|
|
2272
2725
|
]);
|
|
2273
2726
|
const commonDir = stdout.trim();
|
|
2274
2727
|
if (commonDir !== "") {
|
|
2275
|
-
const parent =
|
|
2728
|
+
const parent = dirname2(commonDir);
|
|
2276
2729
|
if (basename(commonDir) === ".git" && parent !== "") return parent;
|
|
2277
2730
|
}
|
|
2278
2731
|
} catch {
|
|
@@ -2286,10 +2739,10 @@ async function repositoryRoot(cwd) {
|
|
|
2286
2739
|
return cwd;
|
|
2287
2740
|
}
|
|
2288
2741
|
async function hookFilePath(cwd, hookName = "pre-push") {
|
|
2289
|
-
return
|
|
2742
|
+
return join5(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, hookName);
|
|
2290
2743
|
}
|
|
2291
2744
|
async function hooksPathValue(cwd) {
|
|
2292
|
-
return
|
|
2745
|
+
return join5(await repositoryRoot(cwd), ARCANE_HOOKS_DIR).replace(/\\/g, "/");
|
|
2293
2746
|
}
|
|
2294
2747
|
async function installHook(cwd, hookName, body) {
|
|
2295
2748
|
const root = await repositoryRoot(cwd);
|
|
@@ -2308,12 +2761,12 @@ async function installHook(cwd, hookName, body) {
|
|
|
2308
2761
|
return { status: "refused-default-hooks", hooks: displaced };
|
|
2309
2762
|
}
|
|
2310
2763
|
}
|
|
2311
|
-
const hookPath =
|
|
2764
|
+
const hookPath = join5(root, ARCANE_HOOKS_DIR, hookName);
|
|
2312
2765
|
if (existing !== void 0 && await hookFileMatches(cwd, hookName, body)) {
|
|
2313
2766
|
return { status: "already-ours" };
|
|
2314
2767
|
}
|
|
2315
|
-
await
|
|
2316
|
-
await
|
|
2768
|
+
await mkdir5(join5(root, ARCANE_HOOKS_DIR), { recursive: true });
|
|
2769
|
+
await writeFile7(hookPath, body, "utf-8");
|
|
2317
2770
|
await chmod(hookPath, 493);
|
|
2318
2771
|
await runGit(cwd, ["config", "--local", "core.hooksPath", await hooksPathValue(cwd)]);
|
|
2319
2772
|
return { status: "installed", path: hookPath };
|
|
@@ -2357,12 +2810,12 @@ async function defaultHooks(cwd) {
|
|
|
2357
2810
|
let hooksDir;
|
|
2358
2811
|
try {
|
|
2359
2812
|
const { stdout } = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
|
|
2360
|
-
hooksDir =
|
|
2813
|
+
hooksDir = join5(resolve2(cwd, stdout.trim()), "hooks");
|
|
2361
2814
|
} catch {
|
|
2362
2815
|
return [];
|
|
2363
2816
|
}
|
|
2364
2817
|
try {
|
|
2365
|
-
return (await
|
|
2818
|
+
return (await readdir4(hooksDir)).filter((name) => !name.endsWith(".sample")).sort();
|
|
2366
2819
|
} catch {
|
|
2367
2820
|
return [];
|
|
2368
2821
|
}
|
|
@@ -2371,7 +2824,7 @@ async function hookFileMatches(cwd, hookName, body) {
|
|
|
2371
2824
|
const hookPath = await hookFilePath(cwd, hookName);
|
|
2372
2825
|
if (!await fileExists(hookPath)) return false;
|
|
2373
2826
|
try {
|
|
2374
|
-
if (await
|
|
2827
|
+
if (await readFile6(hookPath, "utf-8") !== body) return false;
|
|
2375
2828
|
} catch {
|
|
2376
2829
|
return false;
|
|
2377
2830
|
}
|
|
@@ -2390,13 +2843,13 @@ async function removeHook(cwd, hookName) {
|
|
|
2390
2843
|
if (!isArcaneHooksPath(root, existing.value)) return;
|
|
2391
2844
|
if (existing.scope !== "local" && existing.scope !== "worktree") return;
|
|
2392
2845
|
try {
|
|
2393
|
-
await
|
|
2846
|
+
await rm3(await hookFilePath(cwd, hookName), { force: true });
|
|
2394
2847
|
} catch {
|
|
2395
2848
|
}
|
|
2396
|
-
const hooksDir =
|
|
2849
|
+
const hooksDir = join5(root, ARCANE_HOOKS_DIR);
|
|
2397
2850
|
let remaining = [];
|
|
2398
2851
|
try {
|
|
2399
|
-
remaining = await
|
|
2852
|
+
remaining = await readdir4(hooksDir);
|
|
2400
2853
|
} catch {
|
|
2401
2854
|
remaining = [];
|
|
2402
2855
|
}
|
|
@@ -2682,13 +3135,9 @@ function isSpellComponent(name) {
|
|
|
2682
3135
|
function categorizeComponents(components) {
|
|
2683
3136
|
const groups = [];
|
|
2684
3137
|
const spellFiles = components.filter((c) => isSpellComponent(c.name)).flatMap((c) => c.files);
|
|
2685
|
-
const
|
|
2686
|
-
if (
|
|
2687
|
-
groups.push({ icon: "\u2728", label: "Copilot
|
|
2688
|
-
}
|
|
2689
|
-
const claudeCount = spellFiles.filter((f) => f.startsWith(".claude/commands/")).length;
|
|
2690
|
-
if (claudeCount > 0) {
|
|
2691
|
-
groups.push({ icon: "\u26A1", label: "Claude Spells", count: claudeCount });
|
|
3138
|
+
const spellCount = spellFiles.filter((f) => f.startsWith(".arcane/spells/")).length;
|
|
3139
|
+
if (spellCount > 0) {
|
|
3140
|
+
groups.push({ icon: "\u2728", label: "Spells (Copilot, Claude Code, Codex)", count: spellCount });
|
|
2692
3141
|
}
|
|
2693
3142
|
const governance = components.filter(
|
|
2694
3143
|
(c) => !isSpellComponent(c.name) && c.name !== "venture-template" && c.name !== "agent-definitions" && c.files.some((f) => f.startsWith(".arcane/governance/"))
|
|
@@ -2728,6 +3177,10 @@ function buildProfileDescription(components, profile) {
|
|
|
2728
3177
|
return "\n" + parts.join("\n");
|
|
2729
3178
|
}
|
|
2730
3179
|
async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
3180
|
+
if (options.user) {
|
|
3181
|
+
await runInitUser(options, targetDir, assetsDir, packageVersion);
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
2731
3184
|
if (options.profile && !VALID_PROFILES.includes(options.profile)) {
|
|
2732
3185
|
throw new Error(
|
|
2733
3186
|
`Invalid profile "${options.profile}". Valid options: ${VALID_PROFILES.join(", ")}`
|
|
@@ -2832,7 +3285,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2832
3285
|
const installedFiles = [];
|
|
2833
3286
|
const fileHashes = {};
|
|
2834
3287
|
for (const file of component.files) {
|
|
2835
|
-
const srcPath =
|
|
3288
|
+
const srcPath = join6(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
2836
3289
|
if (options.dryRun) {
|
|
2837
3290
|
printDryRun(`Would copy: ${file}`);
|
|
2838
3291
|
} else {
|
|
@@ -2849,7 +3302,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2849
3302
|
fileCount++;
|
|
2850
3303
|
}
|
|
2851
3304
|
for (const dir of component.directories ?? []) {
|
|
2852
|
-
const srcDirPath =
|
|
3305
|
+
const srcDirPath = join6(assetsDir, dir);
|
|
2853
3306
|
if (options.dryRun) {
|
|
2854
3307
|
printDryRun(`Would copy directory: ${dir}/`);
|
|
2855
3308
|
} else {
|
|
@@ -2878,7 +3331,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2878
3331
|
if (options.dryRun) {
|
|
2879
3332
|
if (hasAgents) {
|
|
2880
3333
|
const agentFiles = await import("fs").then(
|
|
2881
|
-
(fsModule) => fsModule.promises.readdir(
|
|
3334
|
+
(fsModule) => fsModule.promises.readdir(join6(assetsDir, "agents"))
|
|
2882
3335
|
);
|
|
2883
3336
|
const yamlFiles = agentFiles.filter((f) => f.endsWith(".yaml")).sort();
|
|
2884
3337
|
printDryRun("Would run agent setup (interactive):");
|
|
@@ -3120,9 +3573,110 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
3120
3573
|
);
|
|
3121
3574
|
console.log();
|
|
3122
3575
|
}
|
|
3576
|
+
async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
3577
|
+
if (options.profile) {
|
|
3578
|
+
throw new Error(
|
|
3579
|
+
'"--profile" does not apply to "--user": the user tier always installs every spell and nothing else.'
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
try {
|
|
3583
|
+
await readManifest(storeRoot);
|
|
3584
|
+
console.log(chalk2.hex("#a855f7")(`
|
|
3585
|
+
\u2726 Arcane v${packageVersion}
|
|
3586
|
+
`));
|
|
3587
|
+
console.log(
|
|
3588
|
+
`Already initialized (user tier at ${storeRoot}). Run "spell update --user" to update it.`
|
|
3589
|
+
);
|
|
3590
|
+
return;
|
|
3591
|
+
} catch (err) {
|
|
3592
|
+
if (!(err instanceof ManifestNotFoundError)) throw err;
|
|
3593
|
+
}
|
|
3594
|
+
console.log(chalk2.hex("#a855f7")(`
|
|
3595
|
+
\u2726 Arcane v${packageVersion}
|
|
3596
|
+
`));
|
|
3597
|
+
const homeDir = dirname3(storeRoot);
|
|
3598
|
+
const components = USER_TIER_COMPONENTS.map(
|
|
3599
|
+
(name) => componentForScope(getComponent(name), "user")
|
|
3600
|
+
);
|
|
3601
|
+
const spellIds = components.flatMap((c) => c.files).map((file) => spellIdFromStorePath(file)).filter((id) => id !== void 0);
|
|
3602
|
+
printSection(`\u{1F4E6} User tier \u2014 ${storeRoot}`);
|
|
3603
|
+
console.log();
|
|
3604
|
+
console.log(
|
|
3605
|
+
` \u2728 ${spellIds.length} Spells \u2192 ${storeRoot}/spells/ \xB7 \u{1F517} ${spellIds.length * 2} client files \u2192 ~/.agents/skills (Codex, Copilot), ~/.claude/commands (Claude Code)`
|
|
3606
|
+
);
|
|
3607
|
+
console.log();
|
|
3608
|
+
if (options.dryRun) {
|
|
3609
|
+
for (const component of components) {
|
|
3610
|
+
for (const file of component.files) printDryRun(`Would copy: ${file}`);
|
|
3611
|
+
}
|
|
3612
|
+
for (const id of spellIds) {
|
|
3613
|
+
for (const client of ["codex", "claude"]) {
|
|
3614
|
+
const relativePath = USER_FANOUT_PATHS[client](id);
|
|
3615
|
+
if (await fileExists(join6(homeDir, relativePath))) {
|
|
3616
|
+
printDryRun(`Would leave alone (exists, not written by Arcane): ~/${relativePath}`);
|
|
3617
|
+
} else {
|
|
3618
|
+
printDryRun(`Would write: ~/${relativePath}`);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
printDryRun(
|
|
3623
|
+
`Would initialize the user tier \u2014 ${spellIds.length} spells, ${spellIds.length * 2} client files`
|
|
3624
|
+
);
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
const installedComponents = [];
|
|
3628
|
+
let fileCount = 0;
|
|
3629
|
+
for (const component of components) {
|
|
3630
|
+
const installedFiles = [];
|
|
3631
|
+
const fileHashes = {};
|
|
3632
|
+
for (const file of component.files) {
|
|
3633
|
+
const srcPath = join6(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
3634
|
+
fileHashes[file] = await copyFile(srcPath, storeRoot, file, { force: options.force });
|
|
3635
|
+
installedFiles.push(file);
|
|
3636
|
+
fileCount++;
|
|
3637
|
+
}
|
|
3638
|
+
installedComponents.push({
|
|
3639
|
+
name: component.name,
|
|
3640
|
+
files: installedFiles,
|
|
3641
|
+
installedVersion: packageVersion,
|
|
3642
|
+
fileHashes
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
const fanout = await syncUserTierFanout({ homeDir, storeRoot, spellIds, fallbackDir: assetsDir });
|
|
3646
|
+
const manifest = {
|
|
3647
|
+
version: packageVersion,
|
|
3648
|
+
// The tier is un-profiled; "full" records the profile its component set
|
|
3649
|
+
// is drawn from, and every command reads `scope` before `profile`.
|
|
3650
|
+
profile: "full",
|
|
3651
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3652
|
+
components: installedComponents,
|
|
3653
|
+
scope: "user",
|
|
3654
|
+
fanout: fanout.record
|
|
3655
|
+
};
|
|
3656
|
+
await writeManifest(storeRoot, manifest);
|
|
3657
|
+
printStep(`\u2728 ${fileCount} Spells installed to ${storeRoot}/spells/`);
|
|
3658
|
+
for (const line of describeFanoutOutcomes(fanout.outcomes)) {
|
|
3659
|
+
if (line.startsWith("!")) printWarning(line.slice(2));
|
|
3660
|
+
else printStep(`\u{1F517} ${line}`);
|
|
3661
|
+
}
|
|
3662
|
+
printSuccess(
|
|
3663
|
+
`Initialized the user tier \u2014 ${fileCount} spells, ${Object.keys(fanout.record).length} client files recorded`
|
|
3664
|
+
);
|
|
3665
|
+
console.log();
|
|
3666
|
+
printSection("\u{1F9ED} Clients");
|
|
3667
|
+
console.log();
|
|
3668
|
+
for (const line of VSCODE_USER_TIER_NOTE) printInfo(line);
|
|
3669
|
+
printInfo(CLAUDE_PRECEDENCE_NOTE);
|
|
3670
|
+
console.log();
|
|
3671
|
+
printSection("\u{1F680} Next steps");
|
|
3672
|
+
console.log();
|
|
3673
|
+
printNextStep(1, "Reload VS Code, and start a fresh Codex or Claude Code session, so the pickers see the new files");
|
|
3674
|
+
printNextStep(2, "`spell status --user` shows the tier; `spell update --user` refreshes it after upgrading the CLI");
|
|
3675
|
+
console.log();
|
|
3676
|
+
}
|
|
3123
3677
|
|
|
3124
3678
|
// src/commands/add.ts
|
|
3125
|
-
import { join as
|
|
3679
|
+
import { join as join7 } from "path";
|
|
3126
3680
|
async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
3127
3681
|
let manifest;
|
|
3128
3682
|
try {
|
|
@@ -3158,7 +3712,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
3158
3712
|
const installedFiles = [];
|
|
3159
3713
|
const fileHashes = {};
|
|
3160
3714
|
for (const file of component.files) {
|
|
3161
|
-
const srcPath =
|
|
3715
|
+
const srcPath = join7(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
3162
3716
|
if (options.dryRun) {
|
|
3163
3717
|
console.log(` [dry-run] Would copy: ${file}`);
|
|
3164
3718
|
} else {
|
|
@@ -3168,7 +3722,7 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
3168
3722
|
fileCount++;
|
|
3169
3723
|
}
|
|
3170
3724
|
for (const dir of component.directories ?? []) {
|
|
3171
|
-
const srcDirPath =
|
|
3725
|
+
const srcDirPath = join7(assetsDir, dir);
|
|
3172
3726
|
if (options.dryRun) {
|
|
3173
3727
|
console.log(` [dry-run] Would copy directory: ${dir}/`);
|
|
3174
3728
|
} else {
|
|
@@ -3202,13 +3756,13 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
3202
3756
|
}
|
|
3203
3757
|
|
|
3204
3758
|
// src/commands/update.ts
|
|
3205
|
-
import { join as
|
|
3206
|
-
import { readFile as
|
|
3759
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
3760
|
+
import { readFile as readFile7, writeFile as writeFile9, rm as rm4 } from "fs/promises";
|
|
3207
3761
|
|
|
3208
3762
|
// src/modules/hub.ts
|
|
3209
3763
|
import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
|
|
3210
|
-
import { readdir as
|
|
3211
|
-
import { join as
|
|
3764
|
+
import { readdir as readdir5, writeFile as writeFile8 } from "fs/promises";
|
|
3765
|
+
import { join as join8 } from "path";
|
|
3212
3766
|
var MANIFEST_RETROFITS = [
|
|
3213
3767
|
{
|
|
3214
3768
|
field: "role",
|
|
@@ -3347,12 +3901,12 @@ async function runManifestRetrofits(manifest) {
|
|
|
3347
3901
|
return patch;
|
|
3348
3902
|
}
|
|
3349
3903
|
async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
3350
|
-
const registryPath =
|
|
3904
|
+
const registryPath = join8(targetDir, businessRoot, "registry.json");
|
|
3351
3905
|
if (await fileExists(registryPath)) return;
|
|
3352
|
-
const businessRootPath =
|
|
3906
|
+
const businessRootPath = join8(targetDir, businessRoot);
|
|
3353
3907
|
let ventureDirs;
|
|
3354
3908
|
try {
|
|
3355
|
-
const entries = await
|
|
3909
|
+
const entries = await readdir5(businessRootPath, { withFileTypes: true });
|
|
3356
3910
|
ventureDirs = entries.filter((e) => e.isDirectory() && e.name !== "_template").map((e) => e.name);
|
|
3357
3911
|
} catch {
|
|
3358
3912
|
return;
|
|
@@ -3387,7 +3941,7 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
|
3387
3941
|
updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3388
3942
|
ventures
|
|
3389
3943
|
};
|
|
3390
|
-
await
|
|
3944
|
+
await writeFile8(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf-8");
|
|
3391
3945
|
console.log(
|
|
3392
3946
|
` \u2713 Scaffolded ${businessRoot}/registry.json with ${Object.keys(ventures).length} venture(s).`
|
|
3393
3947
|
);
|
|
@@ -3556,7 +4110,7 @@ function migrateLegacyComponents(components) {
|
|
|
3556
4110
|
return out;
|
|
3557
4111
|
}
|
|
3558
4112
|
async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
3559
|
-
const filePath =
|
|
4113
|
+
const filePath = join9(targetDir, file);
|
|
3560
4114
|
if (!await fileExists(filePath)) return "not-found";
|
|
3561
4115
|
if (!prune) return "reported";
|
|
3562
4116
|
if (recordedHash === void 0) {
|
|
@@ -3568,10 +4122,27 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
3568
4122
|
console.log(` ! Orphaned but edited since install \u2014 not pruning: ${file}`);
|
|
3569
4123
|
return "reported";
|
|
3570
4124
|
}
|
|
3571
|
-
await
|
|
4125
|
+
await rm4(filePath, { force: true });
|
|
3572
4126
|
console.log(` Pruned orphaned file: ${file}`);
|
|
3573
4127
|
return "pruned";
|
|
3574
4128
|
}
|
|
4129
|
+
async function findMissingTrackedFiles(targetDir, components, scope = "repo") {
|
|
4130
|
+
const missing = [];
|
|
4131
|
+
for (const installed of components) {
|
|
4132
|
+
let component;
|
|
4133
|
+
try {
|
|
4134
|
+
component = componentForScope(getComponent(installed.name), scope);
|
|
4135
|
+
} catch {
|
|
4136
|
+
continue;
|
|
4137
|
+
}
|
|
4138
|
+
if (component.initOnly || component.skipExisting) continue;
|
|
4139
|
+
for (const file of installed.files) {
|
|
4140
|
+
if (!component.files.includes(file)) continue;
|
|
4141
|
+
if (!await fileExists(join9(targetDir, file))) missing.push(file);
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
return missing;
|
|
4145
|
+
}
|
|
3575
4146
|
async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
3576
4147
|
let manifest;
|
|
3577
4148
|
try {
|
|
@@ -3579,44 +4150,76 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3579
4150
|
} catch (err) {
|
|
3580
4151
|
if (err instanceof ManifestNotFoundError) {
|
|
3581
4152
|
console.error(
|
|
3582
|
-
|
|
4153
|
+
`Not initialized. Run "spell init${options.user ? " --user" : ""}" first before updating.`
|
|
3583
4154
|
);
|
|
3584
4155
|
process.exit(1);
|
|
3585
4156
|
return;
|
|
3586
4157
|
}
|
|
3587
4158
|
throw err;
|
|
3588
4159
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
console.warn(
|
|
3593
|
-
"Updates can replace managed files. A clean committed baseline is required for recovery."
|
|
3594
|
-
);
|
|
3595
|
-
const gitState = await inspectGitRepository(targetDir);
|
|
3596
|
-
if (gitState.status === "not-repository") {
|
|
3597
|
-
console.error(
|
|
3598
|
-
"Update refused: this directory is not a Git repository or Git is unavailable."
|
|
3599
|
-
);
|
|
4160
|
+
const resolvedScope = resolveInstallScope(targetDir, manifest.scope, options.user);
|
|
4161
|
+
if (resolvedScope.misplacedUserManifest) {
|
|
4162
|
+
console.error(misplacedUserManifestMessage(targetDir));
|
|
3600
4163
|
process.exit(1);
|
|
3601
4164
|
return;
|
|
3602
4165
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
4166
|
+
const scope = resolvedScope.scope;
|
|
4167
|
+
const homeDir = dirname4(targetDir);
|
|
4168
|
+
if (scope === "repo") {
|
|
4169
|
+
console.warn(
|
|
4170
|
+
`WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
|
|
3606
4171
|
);
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
}
|
|
3610
|
-
if (gitState.uncommittedChanges > 0) {
|
|
3611
|
-
console.error(
|
|
3612
|
-
`Update refused: this repository has ${gitState.uncommittedChanges} uncommitted change${gitState.uncommittedChanges === 1 ? "" : "s"}. Commit or otherwise clean the working tree before updating.`
|
|
4172
|
+
console.warn(
|
|
4173
|
+
"Updates can replace managed files. A clean committed baseline is required for recovery."
|
|
3613
4174
|
);
|
|
3614
|
-
|
|
3615
|
-
|
|
4175
|
+
const gitState = await inspectGitRepository(targetDir);
|
|
4176
|
+
if (gitState.status === "not-repository") {
|
|
4177
|
+
console.error(
|
|
4178
|
+
"Update refused: this directory is not a Git repository or Git is unavailable."
|
|
4179
|
+
);
|
|
4180
|
+
process.exit(1);
|
|
4181
|
+
return;
|
|
4182
|
+
}
|
|
4183
|
+
if (gitState.status === "no-commits") {
|
|
4184
|
+
console.error(
|
|
4185
|
+
"Update refused: this repository has no commits. Commit the current baseline before updating."
|
|
4186
|
+
);
|
|
4187
|
+
process.exit(1);
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
if (gitState.uncommittedChanges > 0) {
|
|
4191
|
+
console.error(
|
|
4192
|
+
`Update refused: this repository has ${gitState.uncommittedChanges} uncommitted change${gitState.uncommittedChanges === 1 ? "" : "s"}. Commit or otherwise clean the working tree before updating.`
|
|
4193
|
+
);
|
|
4194
|
+
process.exit(1);
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
3616
4197
|
}
|
|
4198
|
+
let sameVersionRestore = false;
|
|
3617
4199
|
if (manifest.version === packageVersion && manifest.components.length > 0) {
|
|
3618
|
-
|
|
3619
|
-
|
|
4200
|
+
const missing = await findMissingTrackedFiles(targetDir, manifest.components, scope);
|
|
4201
|
+
if (missing.length === 0) {
|
|
4202
|
+
console.log("Already up to date.");
|
|
4203
|
+
if (scope === "user") {
|
|
4204
|
+
const fanout = await syncUserTierFanout({
|
|
4205
|
+
homeDir,
|
|
4206
|
+
storeRoot: targetDir,
|
|
4207
|
+
spellIds: spellIdsInStore(manifest.components),
|
|
4208
|
+
previous: manifest.fanout,
|
|
4209
|
+
dryRun: options.dryRun,
|
|
4210
|
+
fallbackDir: assetsDir
|
|
4211
|
+
});
|
|
4212
|
+
for (const line of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) console.log(` ${line}`);
|
|
4213
|
+
if (!options.dryRun && JSON.stringify(fanout.record) !== JSON.stringify(manifest.fanout ?? {})) {
|
|
4214
|
+
await writeManifest(targetDir, { ...manifest, fanout: fanout.record });
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
sameVersionRestore = true;
|
|
4220
|
+
console.log(
|
|
4221
|
+
`${options.dryRun ? "[dry-run] " : ""}Already at v${packageVersion}, but ${missing.length} tracked file${missing.length === 1 ? " is" : "s are"} missing \u2014 restoring.`
|
|
4222
|
+
);
|
|
3620
4223
|
}
|
|
3621
4224
|
if (manifest.components.length === 0) {
|
|
3622
4225
|
console.log("No components installed. Nothing to update.");
|
|
@@ -3625,12 +4228,13 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3625
4228
|
let fileCount = 0;
|
|
3626
4229
|
const updatedComponents = [];
|
|
3627
4230
|
const conflictedFiles = [];
|
|
4231
|
+
const customizedShims = [];
|
|
3628
4232
|
const orphanReport = [];
|
|
3629
4233
|
const componentsToUpdate = migrateLegacyComponents(manifest.components);
|
|
3630
4234
|
for (const installed of componentsToUpdate) {
|
|
3631
4235
|
let component;
|
|
3632
4236
|
try {
|
|
3633
|
-
component = getComponent(installed.name);
|
|
4237
|
+
component = componentForScope(getComponent(installed.name), scope);
|
|
3634
4238
|
} catch (err) {
|
|
3635
4239
|
if (err instanceof ComponentNotFoundError) {
|
|
3636
4240
|
console.log(` ! ${installed.name} not in registry \u2014 skipping.`);
|
|
@@ -3654,52 +4258,72 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3654
4258
|
const updatedFiles = [];
|
|
3655
4259
|
const fileHashes = {};
|
|
3656
4260
|
for (const file of component.files) {
|
|
3657
|
-
const srcPath =
|
|
3658
|
-
const targetExists = await fileExists(
|
|
4261
|
+
const srcPath = join9(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
4262
|
+
const targetExists = await fileExists(join9(targetDir, file));
|
|
3659
4263
|
const preserveExisting = Boolean(component.skipExisting) && targetExists;
|
|
4264
|
+
const recordedHash = installed.fileHashes?.[file];
|
|
3660
4265
|
if (component.initOnly && !targetExists) {
|
|
3661
4266
|
console.log(
|
|
3662
4267
|
` ! Missing: ${file} \u2014 not added automatically, because doing so would change how Git treats existing files. Run "spell add ${component.name}" if you want it.`
|
|
3663
4268
|
);
|
|
3664
4269
|
continue;
|
|
3665
4270
|
}
|
|
4271
|
+
if (sameVersionRestore) {
|
|
4272
|
+
const tracked = installed.files.includes(file);
|
|
4273
|
+
const restorable = tracked && !targetExists && !component.skipExisting;
|
|
4274
|
+
if (!restorable) {
|
|
4275
|
+
if (tracked) {
|
|
4276
|
+
updatedFiles.push(file);
|
|
4277
|
+
if (recordedHash !== void 0) fileHashes[file] = recordedHash;
|
|
4278
|
+
}
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && await hashFile(join9(targetDir, file)) !== recordedHash;
|
|
3666
4283
|
if (preserveExisting) {
|
|
3667
4284
|
console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
|
|
4285
|
+
} else if (editedByOperator && isClientShimPath(file)) {
|
|
4286
|
+
customizedShims.push(file);
|
|
4287
|
+
fileHashes[file] = recordedHash;
|
|
4288
|
+
console.log(
|
|
4289
|
+
` ${options.dryRun ? "[dry-run] Would keep" : "Kept"} customized (not merged): ${file}`
|
|
4290
|
+
);
|
|
3668
4291
|
} else if (options.dryRun) {
|
|
3669
|
-
console.log(` [dry-run] Would update: ${file}`);
|
|
4292
|
+
console.log(` [dry-run] Would ${sameVersionRestore ? "restore missing" : "update"}: ${file}`);
|
|
3670
4293
|
fileCount++;
|
|
3671
4294
|
} else {
|
|
3672
|
-
const recordedHash = installed.fileHashes?.[file];
|
|
3673
4295
|
let handled = false;
|
|
3674
|
-
if (
|
|
3675
|
-
const
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
4296
|
+
if (editedByOperator) {
|
|
4297
|
+
const oldVendorContent = await fetchPublishedFile(
|
|
4298
|
+
installed.installedVersion,
|
|
4299
|
+
component.sourceOverrides?.[file] ?? file
|
|
4300
|
+
);
|
|
4301
|
+
if (oldVendorContent === void 0) {
|
|
4302
|
+
console.log(
|
|
4303
|
+
` ! Could not fetch the previously published version of ${file} to merge your edits \u2014 left your version untouched. Update it manually if you want the latest.`
|
|
4304
|
+
);
|
|
4305
|
+
fileHashes[file] = recordedHash;
|
|
4306
|
+
handled = true;
|
|
4307
|
+
} else {
|
|
4308
|
+
const [currentContent, newContent] = await Promise.all([
|
|
4309
|
+
readFile7(join9(targetDir, file), "utf-8"),
|
|
4310
|
+
readFile7(srcPath, "utf-8")
|
|
4311
|
+
]);
|
|
4312
|
+
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
4313
|
+
await writeFile9(join9(targetDir, file), result.content, "utf-8");
|
|
4314
|
+
fileHashes[file] = await hashFile(srcPath);
|
|
4315
|
+
if (result.hasConflict) {
|
|
4316
|
+
conflictedFiles.push(file);
|
|
4317
|
+
console.log(` \u26A0 Merge conflict in ${file} \u2014 resolve the <<<<<<< markers before committing.`);
|
|
3683
4318
|
} else {
|
|
3684
|
-
|
|
3685
|
-
readFile6(join8(targetDir, file), "utf-8"),
|
|
3686
|
-
readFile6(srcPath, "utf-8")
|
|
3687
|
-
]);
|
|
3688
|
-
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
3689
|
-
await writeFile8(join8(targetDir, file), result.content, "utf-8");
|
|
3690
|
-
fileHashes[file] = await hashFile(join8(targetDir, file));
|
|
3691
|
-
if (result.hasConflict) {
|
|
3692
|
-
conflictedFiles.push(file);
|
|
3693
|
-
console.log(` \u26A0 Merge conflict in ${file} \u2014 resolve the <<<<<<< markers before committing.`);
|
|
3694
|
-
} else {
|
|
3695
|
-
console.log(` Merged your edits into: ${file}`);
|
|
3696
|
-
}
|
|
3697
|
-
handled = true;
|
|
4319
|
+
console.log(` Merged your edits into: ${file}`);
|
|
3698
4320
|
}
|
|
4321
|
+
handled = true;
|
|
3699
4322
|
}
|
|
3700
4323
|
}
|
|
3701
4324
|
if (!handled) {
|
|
3702
4325
|
fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: true });
|
|
4326
|
+
if (sameVersionRestore) console.log(` Restored missing: ${file}`);
|
|
3703
4327
|
}
|
|
3704
4328
|
fileCount++;
|
|
3705
4329
|
}
|
|
@@ -3708,7 +4332,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3708
4332
|
}
|
|
3709
4333
|
}
|
|
3710
4334
|
for (const dir of component.directories ?? []) {
|
|
3711
|
-
const srcDirPath =
|
|
4335
|
+
const srcDirPath = join9(assetsDir, dir);
|
|
3712
4336
|
if (options.dryRun) {
|
|
3713
4337
|
console.log(` [dry-run] Would update directory: ${dir}/`);
|
|
3714
4338
|
} else {
|
|
@@ -3752,12 +4376,38 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
3752
4376
|
console.log(" Run `spell update --prune` to remove the ones that are safe to delete.");
|
|
3753
4377
|
}
|
|
3754
4378
|
}
|
|
4379
|
+
if (customizedShims.length > 0) {
|
|
4380
|
+
const verb = options.dryRun ? "[dry-run] Would keep" : "Kept";
|
|
4381
|
+
console.log(
|
|
4382
|
+
`
|
|
4383
|
+
! ${verb} ${customizedShims.length} customized client file(s) untouched (ARC-045): an edited Copilot prompt, Claude command or Codex skill is never merged into the new generated shim, because a shim carries no prose of its own to merge into.
|
|
4384
|
+
` + customizedShims.map((f) => ` ${f}`).join("\n") + `
|
|
4385
|
+
Each keeps working in its client as-is. To converge: move your edits into the canonical spell (.arcane/spells/<id>.md \u2014 later updates merge edits there), delete the customized file, commit, and run \`spell update\` again \u2014 it restores the generated shim at the current version.`
|
|
4386
|
+
);
|
|
4387
|
+
}
|
|
4388
|
+
let fanoutRecord;
|
|
4389
|
+
if (scope === "user") {
|
|
4390
|
+
const fanout = await syncUserTierFanout({
|
|
4391
|
+
homeDir,
|
|
4392
|
+
storeRoot: targetDir,
|
|
4393
|
+
spellIds: spellIdsInStore(updatedComponents),
|
|
4394
|
+
previous: manifest.fanout,
|
|
4395
|
+
dryRun: options.dryRun,
|
|
4396
|
+
// A dry run has restored nothing yet, so a missing store spell renders
|
|
4397
|
+
// from the vendor asset the real run would write (review finding F1).
|
|
4398
|
+
fallbackDir: assetsDir
|
|
4399
|
+
});
|
|
4400
|
+
const lines = describeFanoutOutcomes(fanout.outcomes, options.dryRun);
|
|
4401
|
+
if (lines.length > 0) console.log();
|
|
4402
|
+
for (const line of lines) console.log(` ${line}`);
|
|
4403
|
+
fanoutRecord = fanout.record;
|
|
4404
|
+
}
|
|
3755
4405
|
if (options.dryRun) {
|
|
3756
4406
|
console.log(
|
|
3757
4407
|
`
|
|
3758
4408
|
[dry-run] Would update ${fileCount} files.`
|
|
3759
4409
|
);
|
|
3760
|
-
const applicableRetrofits = MANIFEST_RETROFITS.filter((r) => r.needsRetrofit(manifest));
|
|
4410
|
+
const applicableRetrofits = scope === "repo" ? MANIFEST_RETROFITS.filter((r) => r.needsRetrofit(manifest)) : [];
|
|
3761
4411
|
if (applicableRetrofits.length > 0) {
|
|
3762
4412
|
console.log(
|
|
3763
4413
|
`[dry-run] Would ask ${applicableRetrofits.length} manifest retrofit question${applicableRetrofits.length === 1 ? "" : "s"}: ${applicableRetrofits.map((r) => r.field).join(", ")}.`
|
|
@@ -3765,9 +4415,9 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
3765
4415
|
}
|
|
3766
4416
|
return;
|
|
3767
4417
|
}
|
|
3768
|
-
const interactive = Boolean(process.stdin.isTTY);
|
|
4418
|
+
const interactive = Boolean(process.stdin.isTTY) && scope === "repo";
|
|
3769
4419
|
const retrofitPatch = interactive ? await runManifestRetrofits(manifest) : {};
|
|
3770
|
-
if (!interactive) {
|
|
4420
|
+
if (!interactive && scope === "repo") {
|
|
3771
4421
|
const pending = MANIFEST_RETROFITS.filter((r) => r.needsRetrofit(manifest));
|
|
3772
4422
|
if (pending.length > 0) {
|
|
3773
4423
|
console.log(
|
|
@@ -3779,7 +4429,8 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
3779
4429
|
...manifest,
|
|
3780
4430
|
version: packageVersion,
|
|
3781
4431
|
components: updatedComponents,
|
|
3782
|
-
...retrofitPatch
|
|
4432
|
+
...retrofitPatch,
|
|
4433
|
+
...fanoutRecord !== void 0 ? { fanout: fanoutRecord } : {}
|
|
3783
4434
|
};
|
|
3784
4435
|
await writeManifest(targetDir, updated);
|
|
3785
4436
|
console.log(
|
|
@@ -3794,15 +4445,18 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
3794
4445
|
Search for "<<<<<<< yours" and resolve before committing.`
|
|
3795
4446
|
);
|
|
3796
4447
|
}
|
|
3797
|
-
if (manifest.role !== "hub" && updated.role === "hub") {
|
|
4448
|
+
if (scope === "repo" && manifest.role !== "hub" && updated.role === "hub") {
|
|
3798
4449
|
await offerRegistryScaffold(targetDir, updated.business_root ?? "ventures");
|
|
3799
4450
|
}
|
|
3800
4451
|
}
|
|
3801
4452
|
|
|
4453
|
+
// src/commands/status.ts
|
|
4454
|
+
import { dirname as dirname6 } from "path";
|
|
4455
|
+
|
|
3802
4456
|
// src/modules/version-check.ts
|
|
3803
|
-
import { readFile as
|
|
3804
|
-
import { dirname as
|
|
3805
|
-
import { homedir as
|
|
4457
|
+
import { readFile as readFile8, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
|
|
4458
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
4459
|
+
import { homedir as homedir3 } from "os";
|
|
3806
4460
|
import { createRequire } from "module";
|
|
3807
4461
|
var require2 = createRequire(import.meta.url);
|
|
3808
4462
|
function getFeedUrl() {
|
|
@@ -3815,11 +4469,11 @@ function getFeedUrl() {
|
|
|
3815
4469
|
}
|
|
3816
4470
|
return url;
|
|
3817
4471
|
}
|
|
3818
|
-
var DEFAULT_CACHE_PATH =
|
|
4472
|
+
var DEFAULT_CACHE_PATH = join10(homedir3(), ".arcane", "version-cache.json");
|
|
3819
4473
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
3820
4474
|
async function readCache(cachePath) {
|
|
3821
4475
|
try {
|
|
3822
|
-
const content = await
|
|
4476
|
+
const content = await readFile8(cachePath, "utf-8");
|
|
3823
4477
|
const parsed = JSON.parse(content);
|
|
3824
4478
|
if (typeof parsed.latest !== "string" || typeof parsed.checkedAt !== "string") {
|
|
3825
4479
|
return null;
|
|
@@ -3830,9 +4484,9 @@ async function readCache(cachePath) {
|
|
|
3830
4484
|
}
|
|
3831
4485
|
}
|
|
3832
4486
|
async function writeCache(cachePath, latest) {
|
|
3833
|
-
await
|
|
4487
|
+
await mkdir6(dirname5(cachePath), { recursive: true });
|
|
3834
4488
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3835
|
-
await
|
|
4489
|
+
await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
3836
4490
|
}
|
|
3837
4491
|
async function getLatestVersion(feedUrl) {
|
|
3838
4492
|
const normalizedBase = feedUrl.endsWith("/") ? feedUrl : feedUrl + "/";
|
|
@@ -3911,20 +4565,27 @@ function generateVersionDriftDiagram(repoFilesVersion, installedCliVersion, npmL
|
|
|
3911
4565
|
}
|
|
3912
4566
|
|
|
3913
4567
|
// src/commands/status.ts
|
|
3914
|
-
async function runStatus(targetDir, packageVersion) {
|
|
4568
|
+
async function runStatus(targetDir, packageVersion, options = {}) {
|
|
3915
4569
|
let manifest;
|
|
3916
4570
|
try {
|
|
3917
4571
|
manifest = await readManifest(targetDir);
|
|
3918
4572
|
} catch (err) {
|
|
3919
4573
|
if (err instanceof ManifestNotFoundError) {
|
|
3920
4574
|
console.error(
|
|
3921
|
-
|
|
4575
|
+
`Not initialized. Run "spell init${options.user ? " --user" : ""}" first.`
|
|
3922
4576
|
);
|
|
3923
4577
|
process.exit(1);
|
|
3924
4578
|
return;
|
|
3925
4579
|
}
|
|
3926
4580
|
throw err;
|
|
3927
4581
|
}
|
|
4582
|
+
const resolvedScope = resolveInstallScope(targetDir, manifest.scope, options.user);
|
|
4583
|
+
if (resolvedScope.misplacedUserManifest) {
|
|
4584
|
+
console.error(misplacedUserManifestMessage(targetDir));
|
|
4585
|
+
process.exit(1);
|
|
4586
|
+
return;
|
|
4587
|
+
}
|
|
4588
|
+
const scope = resolvedScope.scope;
|
|
3928
4589
|
if (manifest.components.length === 0) {
|
|
3929
4590
|
console.log("No components installed.");
|
|
3930
4591
|
return;
|
|
@@ -3962,6 +4623,30 @@ async function runStatus(targetDir, packageVersion) {
|
|
|
3962
4623
|
);
|
|
3963
4624
|
}
|
|
3964
4625
|
console.log("");
|
|
4626
|
+
if (scope === "user") {
|
|
4627
|
+
console.log(` Scope: user \u2014 ${targetDir}`);
|
|
4628
|
+
const health = await inspectUserTierFanout(dirname6(targetDir), manifest.fanout);
|
|
4629
|
+
console.log(
|
|
4630
|
+
` Client files: ${health.total} (${health.byClient.codex} ${FANOUT_CLIENT_LABELS.codex}, ${health.byClient.claude} ${FANOUT_CLIENT_LABELS.claude})`
|
|
4631
|
+
);
|
|
4632
|
+
if (health.missing.length > 0 || health.customized.length > 0) {
|
|
4633
|
+
const parts = [];
|
|
4634
|
+
if (health.missing.length > 0) parts.push(`${health.missing.length} missing`);
|
|
4635
|
+
if (health.customized.length > 0) parts.push(`${health.customized.length} customized`);
|
|
4636
|
+
console.log(
|
|
4637
|
+
` ! ${parts.join(", ")} \u2014 \`spell update --user\` regenerates missing files; customized files are never overwritten.`
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4640
|
+
} else {
|
|
4641
|
+
console.log(" Scope: repo");
|
|
4642
|
+
try {
|
|
4643
|
+
const store = userTierRoot();
|
|
4644
|
+
const userManifest = await readManifest(store);
|
|
4645
|
+
console.log(` User tier: v${userManifest.version} at ${store} (spell status --user)`);
|
|
4646
|
+
} catch {
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
console.log("");
|
|
3965
4650
|
const latestStr = versionResult.error ? "unable to check" : versionResult.latest ?? manifest.version;
|
|
3966
4651
|
let footer = ` Installed: ${packageVersion} Latest: ${latestStr}`;
|
|
3967
4652
|
if (versionResult.updateAvailable) {
|
|
@@ -3988,11 +4673,16 @@ async function runStatus(targetDir, packageVersion) {
|
|
|
3988
4673
|
}
|
|
3989
4674
|
}
|
|
3990
4675
|
}
|
|
4676
|
+
if (scope === "user") {
|
|
4677
|
+
console.log("");
|
|
4678
|
+
for (const line of VSCODE_USER_TIER_NOTE) console.log(` ${line}`);
|
|
4679
|
+
console.log(` ${CLAUDE_PRECEDENCE_NOTE}`);
|
|
4680
|
+
}
|
|
3991
4681
|
}
|
|
3992
4682
|
|
|
3993
4683
|
// src/commands/uninstall.ts
|
|
3994
|
-
import { rm as
|
|
3995
|
-
import { join as
|
|
4684
|
+
import { rm as rm5 } from "fs/promises";
|
|
4685
|
+
import { dirname as dirname7, join as join11 } from "path";
|
|
3996
4686
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
3997
4687
|
async function runUninstall(options, targetDir) {
|
|
3998
4688
|
let manifest;
|
|
@@ -4000,12 +4690,22 @@ async function runUninstall(options, targetDir) {
|
|
|
4000
4690
|
manifest = await readManifest(targetDir);
|
|
4001
4691
|
} catch (err) {
|
|
4002
4692
|
if (err instanceof ManifestNotFoundError) {
|
|
4003
|
-
console.error(
|
|
4693
|
+
console.error(`Not initialized. Run "spell init${options.user ? " --user" : ""}" first.`);
|
|
4004
4694
|
process.exit(1);
|
|
4005
4695
|
return;
|
|
4006
4696
|
}
|
|
4007
4697
|
throw err;
|
|
4008
4698
|
}
|
|
4699
|
+
const resolvedScope = resolveInstallScope(targetDir, manifest.scope, options.user);
|
|
4700
|
+
if (resolvedScope.misplacedUserManifest) {
|
|
4701
|
+
console.error(misplacedUserManifestMessage(targetDir));
|
|
4702
|
+
process.exit(1);
|
|
4703
|
+
return;
|
|
4704
|
+
}
|
|
4705
|
+
if (resolvedScope.scope === "user") {
|
|
4706
|
+
await uninstallUserTier(options, targetDir, manifest);
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4009
4709
|
const policy = manifest.push_policy ?? "open";
|
|
4010
4710
|
const controlsInForce = await isHookEnforced(targetDir) || (await blockedRemotes(targetDir)).length > 0;
|
|
4011
4711
|
if ((policy === "blocked" || controlsInForce) && !options.dryRun) {
|
|
@@ -4055,24 +4755,24 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
4055
4755
|
let removed = 0;
|
|
4056
4756
|
for (const component of manifest.components) {
|
|
4057
4757
|
for (const file of component.files) {
|
|
4058
|
-
const filePath =
|
|
4758
|
+
const filePath = join11(targetDir, file);
|
|
4059
4759
|
try {
|
|
4060
|
-
await
|
|
4760
|
+
await rm5(filePath, { force: true });
|
|
4061
4761
|
removed++;
|
|
4062
4762
|
} catch {
|
|
4063
4763
|
}
|
|
4064
4764
|
}
|
|
4065
4765
|
}
|
|
4066
|
-
await
|
|
4766
|
+
await rm5(join11(targetDir, ".arcane.json"), { force: true });
|
|
4067
4767
|
const agentDirs = [
|
|
4068
4768
|
".github/agents",
|
|
4069
4769
|
".arcane/agents",
|
|
4070
4770
|
".arcane/generated"
|
|
4071
4771
|
];
|
|
4072
4772
|
for (const dir of agentDirs) {
|
|
4073
|
-
await
|
|
4773
|
+
await rm5(join11(targetDir, dir), { recursive: true, force: true });
|
|
4074
4774
|
}
|
|
4075
|
-
await
|
|
4775
|
+
await rm5(join11(targetDir, ".arcane/agents.yaml"), { force: true });
|
|
4076
4776
|
const markerFiles = [
|
|
4077
4777
|
"CLAUDE.md",
|
|
4078
4778
|
"AGENTS.md",
|
|
@@ -4088,10 +4788,63 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
4088
4788
|
console.log(`
|
|
4089
4789
|
\u2713 Uninstalled \u2014 ${removed} files removed.`);
|
|
4090
4790
|
}
|
|
4791
|
+
async function uninstallUserTier(options, storeRoot, manifest) {
|
|
4792
|
+
const homeDir = dirname7(storeRoot);
|
|
4793
|
+
const storeFiles = manifest.components.flatMap((c) => c.files);
|
|
4794
|
+
const recordedCount = Object.keys(manifest.fanout ?? {}).length;
|
|
4795
|
+
if (options.dryRun) {
|
|
4796
|
+
for (const file of storeFiles) console.log(`[dry-run] Would remove: ${storeRoot}/${file}`);
|
|
4797
|
+
const preview = await syncUserTierFanout({
|
|
4798
|
+
homeDir,
|
|
4799
|
+
storeRoot,
|
|
4800
|
+
spellIds: [],
|
|
4801
|
+
previous: manifest.fanout,
|
|
4802
|
+
dryRun: true
|
|
4803
|
+
});
|
|
4804
|
+
for (const line of describeFanoutOutcomes(preview.outcomes, true)) console.log(line);
|
|
4805
|
+
console.log(`[dry-run] Would remove: ${storeRoot}/.arcane.json`);
|
|
4806
|
+
console.log(
|
|
4807
|
+
`
|
|
4808
|
+
[dry-run] ${storeFiles.length} spell(s), up to ${recordedCount} client file(s) + manifest would be removed from the user tier.`
|
|
4809
|
+
);
|
|
4810
|
+
return;
|
|
4811
|
+
}
|
|
4812
|
+
if (!options.yes) {
|
|
4813
|
+
const confirmed = await confirm3({
|
|
4814
|
+
message: `This will remove ${storeFiles.length} spells, ${recordedCount} client files and .arcane.json from the user tier at ${storeRoot}. Continue?`,
|
|
4815
|
+
default: false
|
|
4816
|
+
});
|
|
4817
|
+
if (!confirmed) {
|
|
4818
|
+
console.log("Uninstall cancelled.");
|
|
4819
|
+
return;
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
const fanout = await syncUserTierFanout({
|
|
4823
|
+
homeDir,
|
|
4824
|
+
storeRoot,
|
|
4825
|
+
spellIds: [],
|
|
4826
|
+
previous: manifest.fanout
|
|
4827
|
+
});
|
|
4828
|
+
let removed = 0;
|
|
4829
|
+
for (const file of storeFiles) {
|
|
4830
|
+
await rm5(join11(storeRoot, file), { force: true });
|
|
4831
|
+
removed++;
|
|
4832
|
+
}
|
|
4833
|
+
await rm5(join11(storeRoot, ".arcane.json"), { force: true });
|
|
4834
|
+
await removeDirectoryIfEmpty(join11(storeRoot, USER_TIER_SPELLS_DIR));
|
|
4835
|
+
await removeDirectoryIfEmpty(storeRoot);
|
|
4836
|
+
for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
|
|
4837
|
+
const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
|
|
4838
|
+
const kept = fanout.outcomes.filter((o) => o.status === "kept-edited").length;
|
|
4839
|
+
console.log(
|
|
4840
|
+
`
|
|
4841
|
+
\u2713 Uninstalled the user tier \u2014 ${removed} spells and ${pruned} client files removed${kept > 0 ? `; ${kept} edited client file(s) kept` : ""}.`
|
|
4842
|
+
);
|
|
4843
|
+
}
|
|
4091
4844
|
|
|
4092
4845
|
// src/commands/doctor.ts
|
|
4093
|
-
import { access as access4, readFile as
|
|
4094
|
-
import { join as
|
|
4846
|
+
import { access as access4, readFile as readFile10, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
4847
|
+
import { join as join13, dirname as dirname8 } from "path";
|
|
4095
4848
|
import { execFile as execFile3 } from "child_process";
|
|
4096
4849
|
import { promisify as promisify2 } from "util";
|
|
4097
4850
|
|
|
@@ -4161,8 +4914,8 @@ function evaluateIncidentGate(records) {
|
|
|
4161
4914
|
}
|
|
4162
4915
|
|
|
4163
4916
|
// src/modules/denylist-scan.ts
|
|
4164
|
-
import { readFile as
|
|
4165
|
-
import { join as
|
|
4917
|
+
import { readFile as readFile9, readdir as readdir6 } from "fs/promises";
|
|
4918
|
+
import { join as join12, relative } from "path";
|
|
4166
4919
|
function escapeRegExp(value) {
|
|
4167
4920
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4168
4921
|
}
|
|
@@ -4179,7 +4932,7 @@ async function scanFile(absolutePath, displayPath, rules) {
|
|
|
4179
4932
|
const findings = [];
|
|
4180
4933
|
let content;
|
|
4181
4934
|
try {
|
|
4182
|
-
content = await
|
|
4935
|
+
content = await readFile9(absolutePath, "utf8");
|
|
4183
4936
|
} catch {
|
|
4184
4937
|
return findings;
|
|
4185
4938
|
}
|
|
@@ -4208,12 +4961,12 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
|
|
|
4208
4961
|
const collected = [];
|
|
4209
4962
|
let entries;
|
|
4210
4963
|
try {
|
|
4211
|
-
entries = await
|
|
4964
|
+
entries = await readdir6(root, { withFileTypes: true });
|
|
4212
4965
|
} catch {
|
|
4213
4966
|
return collected;
|
|
4214
4967
|
}
|
|
4215
4968
|
for (const entry of entries) {
|
|
4216
|
-
const absolutePath =
|
|
4969
|
+
const absolutePath = join12(root, entry.name);
|
|
4217
4970
|
if (entry.isDirectory()) {
|
|
4218
4971
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
4219
4972
|
collected.push(...await collectScannableFiles(absolutePath, repoRoot, extensions));
|
|
@@ -4425,13 +5178,13 @@ async function checkVSCodeExtension(id, label) {
|
|
|
4425
5178
|
const name = `VS Code extension: ${label}`;
|
|
4426
5179
|
const home = process.env["HOME"] ?? "";
|
|
4427
5180
|
const extDirs = [
|
|
4428
|
-
|
|
4429
|
-
|
|
5181
|
+
join13(home, ".vscode", "extensions"),
|
|
5182
|
+
join13(home, ".vscode-insiders", "extensions")
|
|
4430
5183
|
];
|
|
4431
5184
|
for (const dir of extDirs) {
|
|
4432
5185
|
try {
|
|
4433
|
-
const { readdir:
|
|
4434
|
-
const entries = await
|
|
5186
|
+
const { readdir: readdir9 } = await import("fs/promises");
|
|
5187
|
+
const entries = await readdir9(dir);
|
|
4435
5188
|
const prefix = id.toLowerCase() + "-";
|
|
4436
5189
|
const found = entries.some(
|
|
4437
5190
|
(e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
|
|
@@ -4460,13 +5213,13 @@ async function checkVSCodeExtension(id, label) {
|
|
|
4460
5213
|
}
|
|
4461
5214
|
async function checkArcaneManifest(targetDir) {
|
|
4462
5215
|
const name = "Arcane manifest (.arcane.json)";
|
|
4463
|
-
const manifestPath2 =
|
|
5216
|
+
const manifestPath2 = join13(targetDir, ".arcane.json");
|
|
4464
5217
|
try {
|
|
4465
5218
|
await access4(manifestPath2);
|
|
4466
5219
|
} catch {
|
|
4467
|
-
const selfHostedManifestPath =
|
|
5220
|
+
const selfHostedManifestPath = join13(targetDir, "src", "assets", ".arcane.json");
|
|
4468
5221
|
try {
|
|
4469
|
-
const raw = await
|
|
5222
|
+
const raw = await readFile10(selfHostedManifestPath, "utf8");
|
|
4470
5223
|
const manifest = JSON.parse(raw);
|
|
4471
5224
|
if (isRecord3(manifest) && manifest["selfHosted"] === true && manifest["tracking_mode"] === "internal") {
|
|
4472
5225
|
return {
|
|
@@ -4485,7 +5238,7 @@ async function checkArcaneManifest(targetDir) {
|
|
|
4485
5238
|
};
|
|
4486
5239
|
}
|
|
4487
5240
|
try {
|
|
4488
|
-
const raw = await
|
|
5241
|
+
const raw = await readFile10(manifestPath2, "utf8");
|
|
4489
5242
|
const manifest = JSON.parse(raw);
|
|
4490
5243
|
if (!isRecord3(manifest) || typeof manifest["version"] !== "string" || !manifest["version"] || !Array.isArray(manifest["components"])) {
|
|
4491
5244
|
return {
|
|
@@ -4570,7 +5323,7 @@ async function checkSessionContinuity(targetDir) {
|
|
|
4570
5323
|
const results = [];
|
|
4571
5324
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
4572
5325
|
const name = `Session file: ${file}`;
|
|
4573
|
-
const filePath =
|
|
5326
|
+
const filePath = join13(targetDir, file);
|
|
4574
5327
|
try {
|
|
4575
5328
|
await access4(filePath);
|
|
4576
5329
|
results.push({ name, passed: true, message: "present" });
|
|
@@ -4588,18 +5341,82 @@ async function checkSessionContinuity(targetDir) {
|
|
|
4588
5341
|
async function fixSessionContinuity(targetDir, assetsDir) {
|
|
4589
5342
|
const created = [];
|
|
4590
5343
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
4591
|
-
const destPath =
|
|
5344
|
+
const destPath = join13(targetDir, file);
|
|
4592
5345
|
try {
|
|
4593
5346
|
await access4(destPath);
|
|
4594
5347
|
} catch {
|
|
4595
|
-
const srcPath =
|
|
4596
|
-
await
|
|
5348
|
+
const srcPath = join13(assetsDir, file);
|
|
5349
|
+
await mkdir7(dirname8(destPath), { recursive: true });
|
|
4597
5350
|
await fsCopyFile(srcPath, destPath);
|
|
4598
5351
|
created.push(file);
|
|
4599
5352
|
}
|
|
4600
5353
|
}
|
|
4601
5354
|
return created;
|
|
4602
5355
|
}
|
|
5356
|
+
async function checkUserTier(homeDir = resolveHomeDir(), cliVersion) {
|
|
5357
|
+
const name = "User tier (ARC-045)";
|
|
5358
|
+
const storeRoot = userTierRoot(homeDir);
|
|
5359
|
+
let manifest;
|
|
5360
|
+
try {
|
|
5361
|
+
manifest = await readManifest(storeRoot);
|
|
5362
|
+
} catch (err) {
|
|
5363
|
+
if (err instanceof ManifestNotFoundError) {
|
|
5364
|
+
return {
|
|
5365
|
+
name,
|
|
5366
|
+
passed: true,
|
|
5367
|
+
blocking: false,
|
|
5368
|
+
message: "no user tier installed (optional \u2014 `spell init --user` shares every spell across the repositories on this machine)"
|
|
5369
|
+
};
|
|
5370
|
+
}
|
|
5371
|
+
return {
|
|
5372
|
+
name,
|
|
5373
|
+
passed: false,
|
|
5374
|
+
blocking: false,
|
|
5375
|
+
message: `could not read ${storeRoot}/.arcane.json (${err instanceof Error ? err.message : String(err)}) \u2014 remove it and re-run \`spell init --user\``
|
|
5376
|
+
};
|
|
5377
|
+
}
|
|
5378
|
+
try {
|
|
5379
|
+
if (typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
5380
|
+
return {
|
|
5381
|
+
name,
|
|
5382
|
+
passed: false,
|
|
5383
|
+
blocking: false,
|
|
5384
|
+
message: `${storeRoot}/.arcane.json has no version field \u2014 remove the store and re-run \`spell init --user\``
|
|
5385
|
+
};
|
|
5386
|
+
}
|
|
5387
|
+
const majorMinor = (v) => v.split(".").slice(0, 2).join(".");
|
|
5388
|
+
const problems = [];
|
|
5389
|
+
if (cliVersion !== void 0 && majorMinor(manifest.version) !== majorMinor(cliVersion)) {
|
|
5390
|
+
problems.push(`installed at v${manifest.version}, this CLI is v${cliVersion}`);
|
|
5391
|
+
}
|
|
5392
|
+
const health = await inspectUserTierFanout(homeDir, manifest.fanout);
|
|
5393
|
+
if (health.missing.length > 0) {
|
|
5394
|
+
problems.push(`${health.missing.length} of ${health.total} client file(s) missing`);
|
|
5395
|
+
}
|
|
5396
|
+
if (problems.length > 0) {
|
|
5397
|
+
return {
|
|
5398
|
+
name,
|
|
5399
|
+
passed: false,
|
|
5400
|
+
blocking: false,
|
|
5401
|
+
message: `${problems.join("; ")} \u2014 run \`spell update --user\` (regenerates missing files; never overwrites edited ones)`
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
const customized = health.customized.length > 0 ? `, ${health.customized.length} customized` : "";
|
|
5405
|
+
return {
|
|
5406
|
+
name,
|
|
5407
|
+
passed: true,
|
|
5408
|
+
blocking: false,
|
|
5409
|
+
message: `v${manifest.version} at ${storeRoot} \u2014 ${health.total} client file(s) in place${customized}`
|
|
5410
|
+
};
|
|
5411
|
+
} catch (err) {
|
|
5412
|
+
return {
|
|
5413
|
+
name,
|
|
5414
|
+
passed: false,
|
|
5415
|
+
blocking: false,
|
|
5416
|
+
message: `could not inspect the user tier at ${storeRoot} (${err instanceof Error ? err.message : String(err)}) \u2014 run \`spell update --user\`; if that fails too, remove the store and re-run \`spell init --user\``
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
4603
5420
|
async function checkPushPolicy(targetDir) {
|
|
4604
5421
|
const name = "Push policy (EF-09)";
|
|
4605
5422
|
let manifest;
|
|
@@ -4653,10 +5470,10 @@ async function checkPushPolicy(targetDir) {
|
|
|
4653
5470
|
}
|
|
4654
5471
|
async function checkMcpConfig(targetDir) {
|
|
4655
5472
|
const name = "MCP config (per-server timeout, I12)";
|
|
4656
|
-
const filePath =
|
|
5473
|
+
const filePath = join13(targetDir, ".mcp.json");
|
|
4657
5474
|
let raw;
|
|
4658
5475
|
try {
|
|
4659
|
-
raw = await
|
|
5476
|
+
raw = await readFile10(filePath, "utf8");
|
|
4660
5477
|
} catch {
|
|
4661
5478
|
return { name, passed: true, blocking: false, message: "no .mcp.json \u2014 nothing to check" };
|
|
4662
5479
|
}
|
|
@@ -4709,10 +5526,10 @@ function isDelegationsFile(value) {
|
|
|
4709
5526
|
}
|
|
4710
5527
|
async function checkDelegations(targetDir) {
|
|
4711
5528
|
const name = "Standing delegations (T13)";
|
|
4712
|
-
const filePath =
|
|
5529
|
+
const filePath = join13(targetDir, ".arcane", "delegations.json");
|
|
4713
5530
|
let raw;
|
|
4714
5531
|
try {
|
|
4715
|
-
raw = await
|
|
5532
|
+
raw = await readFile10(filePath, "utf8");
|
|
4716
5533
|
} catch {
|
|
4717
5534
|
return { name, passed: true, blocking: false, message: "no delegations recorded" };
|
|
4718
5535
|
}
|
|
@@ -4815,7 +5632,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
4815
5632
|
checkPushPolicy(targetDir),
|
|
4816
5633
|
checkPlatformBranchPolicy(targetDir),
|
|
4817
5634
|
checkDelegations(targetDir),
|
|
4818
|
-
checkMcpConfig(targetDir)
|
|
5635
|
+
checkMcpConfig(targetDir),
|
|
5636
|
+
checkUserTier(resolveHomeDir(), options.packageVersion)
|
|
4819
5637
|
]);
|
|
4820
5638
|
const sessionResults = await checkSessionContinuity(targetDir);
|
|
4821
5639
|
results.push(...sessionResults);
|
|
@@ -4860,16 +5678,16 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
4860
5678
|
}
|
|
4861
5679
|
|
|
4862
5680
|
// src/commands/ward.ts
|
|
4863
|
-
import { readFile as
|
|
5681
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
4864
5682
|
|
|
4865
5683
|
// src/modules/ward.ts
|
|
4866
|
-
import { readdir as
|
|
4867
|
-
import { join as
|
|
5684
|
+
import { readdir as readdir7, readFile as readFile11 } from "fs/promises";
|
|
5685
|
+
import { join as join14, relative as relative2 } from "path";
|
|
4868
5686
|
async function scanFileAllRules(absolutePath, displayPath, rules) {
|
|
4869
5687
|
const findings = [];
|
|
4870
5688
|
let content;
|
|
4871
5689
|
try {
|
|
4872
|
-
content = await
|
|
5690
|
+
content = await readFile11(absolutePath, "utf8");
|
|
4873
5691
|
} catch {
|
|
4874
5692
|
return findings;
|
|
4875
5693
|
}
|
|
@@ -4922,12 +5740,12 @@ async function scanFilenames(root, rules, repoRoot = root) {
|
|
|
4922
5740
|
const findings = [];
|
|
4923
5741
|
let entries;
|
|
4924
5742
|
try {
|
|
4925
|
-
entries = await
|
|
5743
|
+
entries = await readdir7(root, { withFileTypes: true });
|
|
4926
5744
|
} catch {
|
|
4927
5745
|
return findings;
|
|
4928
5746
|
}
|
|
4929
5747
|
for (const entry of entries) {
|
|
4930
|
-
const absolutePath =
|
|
5748
|
+
const absolutePath = join14(root, entry.name);
|
|
4931
5749
|
if (entry.isDirectory()) {
|
|
4932
5750
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
4933
5751
|
findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
|
|
@@ -4964,7 +5782,7 @@ async function runWard(repoRoot, options = {}) {
|
|
|
4964
5782
|
const findings2 = await scanFileAllRules(absolutePath, displayPath, allRules);
|
|
4965
5783
|
if (findings2.length > 0) {
|
|
4966
5784
|
try {
|
|
4967
|
-
fileLines.set(displayPath, (await
|
|
5785
|
+
fileLines.set(displayPath, (await readFile11(absolutePath, "utf8")).split("\n"));
|
|
4968
5786
|
} catch {
|
|
4969
5787
|
}
|
|
4970
5788
|
}
|
|
@@ -4992,7 +5810,7 @@ function parseTermsFileContent(raw) {
|
|
|
4992
5810
|
}
|
|
4993
5811
|
async function readTermsFile(filePath) {
|
|
4994
5812
|
try {
|
|
4995
|
-
return parseTermsFileContent(await
|
|
5813
|
+
return parseTermsFileContent(await readFile12(filePath, "utf8"));
|
|
4996
5814
|
} catch (error) {
|
|
4997
5815
|
console.warn(
|
|
4998
5816
|
`\u26A0 arcane: could not read --terms-file "${filePath}": ${error.message}`
|
|
@@ -5042,7 +5860,7 @@ async function runWardCli(targetDir, options = {}, exclusions = []) {
|
|
|
5042
5860
|
}
|
|
5043
5861
|
|
|
5044
5862
|
// src/commands/unblock-push.ts
|
|
5045
|
-
import { basename as basename2, resolve as
|
|
5863
|
+
import { basename as basename2, resolve as resolve3 } from "path";
|
|
5046
5864
|
import { input as input4 } from "@inquirer/prompts";
|
|
5047
5865
|
async function runUnblockPush(targetDir) {
|
|
5048
5866
|
let manifest;
|
|
@@ -5074,7 +5892,7 @@ async function runUnblockPush(targetDir) {
|
|
|
5074
5892
|
process.exit(1);
|
|
5075
5893
|
return;
|
|
5076
5894
|
}
|
|
5077
|
-
const repoName = basename2(
|
|
5895
|
+
const repoName = basename2(resolve3(targetDir));
|
|
5078
5896
|
if (repoName === "") {
|
|
5079
5897
|
console.error(
|
|
5080
5898
|
"Could not determine a repository name to confirm against (is this repository at a filesystem root?). Refusing to unblock."
|
|
@@ -5141,15 +5959,15 @@ Name did not match ("${typed.trim()}" \u2260 "${repoName}"). Nothing changed.`);
|
|
|
5141
5959
|
|
|
5142
5960
|
// src/commands/report.ts
|
|
5143
5961
|
import { access as access5 } from "fs/promises";
|
|
5144
|
-
import { join as
|
|
5962
|
+
import { join as join17 } from "path";
|
|
5145
5963
|
|
|
5146
5964
|
// src/modules/show-report/generate.ts
|
|
5147
|
-
import { mkdir as
|
|
5148
|
-
import { basename as basename3, dirname as
|
|
5965
|
+
import { mkdir as mkdir8, readdir as readdir8, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
5966
|
+
import { basename as basename3, dirname as dirname9, isAbsolute, join as join16, relative as relative3, resolve as resolve4 } from "path";
|
|
5149
5967
|
|
|
5150
5968
|
// src/modules/show-report/model.ts
|
|
5151
|
-
import { readFile as
|
|
5152
|
-
import { join as
|
|
5969
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
5970
|
+
import { join as join15 } from "path";
|
|
5153
5971
|
|
|
5154
5972
|
// src/modules/show-report/plan-parser.ts
|
|
5155
5973
|
function parseFrontmatter(content) {
|
|
@@ -5417,7 +6235,7 @@ async function getCast(cwd, fromRef, toRef) {
|
|
|
5417
6235
|
// src/modules/show-report/model.ts
|
|
5418
6236
|
async function readOptional(path4) {
|
|
5419
6237
|
try {
|
|
5420
|
-
return await
|
|
6238
|
+
return await readFile13(path4, "utf8");
|
|
5421
6239
|
} catch {
|
|
5422
6240
|
return null;
|
|
5423
6241
|
}
|
|
@@ -5513,13 +6331,13 @@ function buildCorrections(ledgerRows, programName) {
|
|
|
5513
6331
|
};
|
|
5514
6332
|
}
|
|
5515
6333
|
async function buildShowReportModel(options) {
|
|
5516
|
-
const planAbsPath =
|
|
5517
|
-
const queueAbsPath =
|
|
6334
|
+
const planAbsPath = join15(options.rootDir, options.planRelPath);
|
|
6335
|
+
const queueAbsPath = join15(options.rootDir, options.queueRelPath);
|
|
5518
6336
|
const ledgerRelPath = "docs/verification-ledger.md";
|
|
5519
|
-
const ledgerAbsPath =
|
|
5520
|
-
const decisionsAbsPath =
|
|
6337
|
+
const ledgerAbsPath = join15(options.rootDir, ledgerRelPath);
|
|
6338
|
+
const decisionsAbsPath = join15(options.rootDir, "DECISIONS.md");
|
|
5521
6339
|
const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
|
|
5522
|
-
|
|
6340
|
+
readFile13(planAbsPath, "utf8"),
|
|
5523
6341
|
readOptional(queueAbsPath),
|
|
5524
6342
|
readOptional(ledgerAbsPath),
|
|
5525
6343
|
readOptional(decisionsAbsPath)
|
|
@@ -5682,7 +6500,7 @@ function toPosix(path4) {
|
|
|
5682
6500
|
async function programFromPlan(rootDir, slug, planRelPath) {
|
|
5683
6501
|
let planContent;
|
|
5684
6502
|
try {
|
|
5685
|
-
planContent = await
|
|
6503
|
+
planContent = await readFile14(join16(rootDir, planRelPath), "utf8");
|
|
5686
6504
|
} catch {
|
|
5687
6505
|
return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
|
|
5688
6506
|
}
|
|
@@ -5694,16 +6512,16 @@ async function programFromPlan(rootDir, slug, planRelPath) {
|
|
|
5694
6512
|
program: {
|
|
5695
6513
|
slug,
|
|
5696
6514
|
planRelPath,
|
|
5697
|
-
queueRelPath: toPosix(
|
|
6515
|
+
queueRelPath: toPosix(join16(dirname9(planRelPath), "OPERATOR-QUEUE.md")),
|
|
5698
6516
|
ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
|
|
5699
6517
|
}
|
|
5700
6518
|
};
|
|
5701
6519
|
}
|
|
5702
6520
|
async function discoverPrograms(rootDir) {
|
|
5703
|
-
const plansDir =
|
|
6521
|
+
const plansDir = join16(rootDir, "docs", "plans");
|
|
5704
6522
|
let entries;
|
|
5705
6523
|
try {
|
|
5706
|
-
entries = (await
|
|
6524
|
+
entries = (await readdir8(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
5707
6525
|
} catch (error) {
|
|
5708
6526
|
if (error.code === "ENOENT") return { programs: [], skipped: [] };
|
|
5709
6527
|
throw error;
|
|
@@ -5718,23 +6536,23 @@ async function discoverPrograms(rootDir) {
|
|
|
5718
6536
|
return { programs, skipped };
|
|
5719
6537
|
}
|
|
5720
6538
|
async function programFromPlanPath(rootDir, planPath) {
|
|
5721
|
-
const planAbs = isAbsolute(planPath) ? planPath :
|
|
6539
|
+
const planAbs = isAbsolute(planPath) ? planPath : resolve4(rootDir, planPath);
|
|
5722
6540
|
const planRelPath = toPosix(relative3(rootDir, planAbs));
|
|
5723
|
-
return programFromPlan(rootDir, basename3(
|
|
6541
|
+
return programFromPlan(rootDir, basename3(dirname9(planAbs)), planRelPath);
|
|
5724
6542
|
}
|
|
5725
6543
|
function normalizeLineEndings(content) {
|
|
5726
6544
|
return content.replace(/\r\n?/g, "\n");
|
|
5727
6545
|
}
|
|
5728
6546
|
async function readOptionalFile(path4) {
|
|
5729
6547
|
try {
|
|
5730
|
-
return await
|
|
6548
|
+
return await readFile14(path4, "utf8");
|
|
5731
6549
|
} catch (error) {
|
|
5732
6550
|
if (error.code === "ENOENT") return null;
|
|
5733
6551
|
throw error;
|
|
5734
6552
|
}
|
|
5735
6553
|
}
|
|
5736
6554
|
async function generateReport(rootDir, program2, templateHtml) {
|
|
5737
|
-
const planContent = await
|
|
6555
|
+
const planContent = await readFile14(join16(rootDir, program2.planRelPath), "utf8");
|
|
5738
6556
|
const frontmatter = parseFrontmatter(planContent);
|
|
5739
6557
|
const model = await buildShowReportModel({
|
|
5740
6558
|
rootDir,
|
|
@@ -5752,7 +6570,7 @@ async function generateReport(rootDir, program2, templateHtml) {
|
|
|
5752
6570
|
};
|
|
5753
6571
|
}
|
|
5754
6572
|
async function runReportGeneration(options) {
|
|
5755
|
-
const templateHtml = await
|
|
6573
|
+
const templateHtml = await readFile14(options.templatePath, "utf8");
|
|
5756
6574
|
const drifted = [];
|
|
5757
6575
|
const repaired = [];
|
|
5758
6576
|
const unwritten = [];
|
|
@@ -5760,19 +6578,19 @@ async function runReportGeneration(options) {
|
|
|
5760
6578
|
const generated = await generateReport(options.rootDir, program2, templateHtml);
|
|
5761
6579
|
const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
|
|
5762
6580
|
if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
|
|
5763
|
-
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir :
|
|
6581
|
+
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir, dirname9(program2.planRelPath));
|
|
5764
6582
|
const outRel = toPosix(relative3(options.rootDir, outAbs));
|
|
5765
6583
|
const outputs = [
|
|
5766
6584
|
[`${outRel}/show-report.json`, generated.json],
|
|
5767
6585
|
[`${outRel}/show-report.html`, generated.html]
|
|
5768
6586
|
];
|
|
5769
6587
|
for (const [relPath, expected] of outputs) {
|
|
5770
|
-
const actual = await readOptionalFile(
|
|
6588
|
+
const actual = await readOptionalFile(join16(options.rootDir, relPath));
|
|
5771
6589
|
if (actual === null || normalizeLineEndings(actual) !== normalizeLineEndings(expected)) {
|
|
5772
6590
|
drifted.push(`${relPath} does not match a regeneration from its sources.`);
|
|
5773
6591
|
if (options.mode === "write") {
|
|
5774
|
-
await
|
|
5775
|
-
await
|
|
6592
|
+
await mkdir8(outAbs, { recursive: true });
|
|
6593
|
+
await writeFile11(join16(options.rootDir, relPath), expected, "utf8");
|
|
5776
6594
|
repaired.push(relPath);
|
|
5777
6595
|
}
|
|
5778
6596
|
}
|
|
@@ -5783,7 +6601,7 @@ async function runReportGeneration(options) {
|
|
|
5783
6601
|
|
|
5784
6602
|
// src/commands/report.ts
|
|
5785
6603
|
async function runReport(targetDir, options = {}, assetsDir) {
|
|
5786
|
-
const templatePath =
|
|
6604
|
+
const templatePath = join17(assetsDir, "report", "show-report.template.html");
|
|
5787
6605
|
try {
|
|
5788
6606
|
await access5(templatePath);
|
|
5789
6607
|
} catch {
|
|
@@ -5846,8 +6664,12 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
5846
6664
|
import { realpathSync } from "fs";
|
|
5847
6665
|
var require3 = createRequire2(import.meta.url);
|
|
5848
6666
|
var pkg = require3("../package.json");
|
|
5849
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ??
|
|
6667
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join18(dirname10(fileURLToPath(import.meta.url)), "assets");
|
|
5850
6668
|
var program = new Command();
|
|
6669
|
+
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)";
|
|
6670
|
+
function targetDirFor(opts) {
|
|
6671
|
+
return opts.user ? userTierRoot() : process.cwd();
|
|
6672
|
+
}
|
|
5851
6673
|
program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(pkg.version).action(async () => {
|
|
5852
6674
|
if (program.args.length > 0) {
|
|
5853
6675
|
const [attempted] = program.args;
|
|
@@ -5867,15 +6689,16 @@ program.name("spell").description("Arcane framework CLI \u2014 scaffold and mana
|
|
|
5867
6689
|
await printWelcome(pkg.version, process.cwd());
|
|
5868
6690
|
program.outputHelp();
|
|
5869
6691
|
});
|
|
5870
|
-
program.command("init").description("Scaffold Arcane framework files into the current repository").option("--profile <profile>", "Profile: full | lite | methodology | docs | governance-only").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be installed without making changes").action(
|
|
6692
|
+
program.command("init").description("Scaffold Arcane framework files into the current repository").option("--profile <profile>", "Profile: full | lite | methodology | docs | governance-only").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be installed without making changes").option("--user", USER_FLAG_DESCRIPTION).action(
|
|
5871
6693
|
async (opts) => {
|
|
5872
6694
|
await runInit(
|
|
5873
6695
|
{
|
|
5874
6696
|
profile: opts.profile,
|
|
5875
6697
|
force: opts.force,
|
|
5876
|
-
dryRun: opts.dryRun
|
|
6698
|
+
dryRun: opts.dryRun,
|
|
6699
|
+
user: opts.user
|
|
5877
6700
|
},
|
|
5878
|
-
|
|
6701
|
+
targetDirFor(opts),
|
|
5879
6702
|
ASSETS_DIR,
|
|
5880
6703
|
pkg.version
|
|
5881
6704
|
);
|
|
@@ -5895,25 +6718,29 @@ program.command("add <component>").description("Add a component to an existing A
|
|
|
5895
6718
|
program.command("update").description("Update installed components to the current package version").option("--dry-run", "Preview what would be updated without making changes").option(
|
|
5896
6719
|
"--prune",
|
|
5897
6720
|
"Also delete orphaned managed files (tracked before this update, no longer part of any current component). Hash-checked: an edited file is reported, never silently deleted"
|
|
5898
|
-
).action(async (opts) => {
|
|
6721
|
+
).option("--user", USER_FLAG_DESCRIPTION).action(async (opts) => {
|
|
5899
6722
|
await runUpdate(
|
|
5900
|
-
{ dryRun: opts.dryRun, prune: opts.prune },
|
|
5901
|
-
|
|
6723
|
+
{ dryRun: opts.dryRun, prune: opts.prune, user: opts.user },
|
|
6724
|
+
targetDirFor(opts),
|
|
5902
6725
|
ASSETS_DIR,
|
|
5903
6726
|
pkg.version
|
|
5904
6727
|
);
|
|
5905
6728
|
});
|
|
5906
|
-
program.command("status").description("Show installed components and check for updates").action(async () => {
|
|
5907
|
-
await runStatus(
|
|
6729
|
+
program.command("status").description("Show installed components and check for updates").option("--user", USER_FLAG_DESCRIPTION).action(async (opts) => {
|
|
6730
|
+
await runStatus(targetDirFor(opts), pkg.version, { user: opts.user });
|
|
5908
6731
|
});
|
|
5909
|
-
program.command("uninstall").description("Remove all installed Arcane framework files").option("--yes", "Skip confirmation prompt").option("--dry-run", "Preview what would be removed without deleting files").action(async (opts) => {
|
|
5910
|
-
await runUninstall({ yes: opts.yes, dryRun: opts.dryRun },
|
|
6732
|
+
program.command("uninstall").description("Remove all installed Arcane framework files").option("--yes", "Skip confirmation prompt").option("--dry-run", "Preview what would be removed without deleting files").option("--user", USER_FLAG_DESCRIPTION).action(async (opts) => {
|
|
6733
|
+
await runUninstall({ yes: opts.yes, dryRun: opts.dryRun, user: opts.user }, targetDirFor(opts));
|
|
5911
6734
|
});
|
|
5912
6735
|
program.command("unblock-push").description("Lift this repository's push block (interactive terminal only)").action(async () => {
|
|
5913
6736
|
await runUnblockPush(process.cwd());
|
|
5914
6737
|
});
|
|
5915
6738
|
program.command("doctor").description("Check your environment for Arcane prerequisites").option("--fix", "Automatically create missing session continuity files").option("--leaks", "Run only the on-demand secrets/credential scan (ARC-037), skipping the default checks").action(async (opts) => {
|
|
5916
|
-
await runDoctor(
|
|
6739
|
+
await runDoctor(
|
|
6740
|
+
process.cwd(),
|
|
6741
|
+
{ fix: opts.fix, leaks: opts.leaks, packageVersion: pkg.version },
|
|
6742
|
+
ASSETS_DIR
|
|
6743
|
+
);
|
|
5917
6744
|
});
|
|
5918
6745
|
program.command("ward").description("Scan the repo for leaked identifiers -- third-party trademarks and the operator's own personal identifiers alike (T18)").option(
|
|
5919
6746
|
"--terms <terms>",
|