arcane-cli 0.14.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/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/assets/.arcane/agents/agent-policies-template.md +414 -0
- package/dist/assets/.arcane/governance/agent-approved-paths.md +60 -0
- package/dist/assets/.arcane/governance/agent-policies.md +414 -0
- package/dist/assets/.arcane/governance/agent-work-queue-model.md +61 -0
- package/dist/assets/.arcane/governance/authentication-strategy.md +220 -0
- package/dist/assets/.arcane/governance/cicd-standards.md +227 -0
- package/dist/assets/.arcane/governance/decision-documentation-standard.md +335 -0
- package/dist/assets/.arcane/governance/development-methodology.md +384 -0
- package/dist/assets/.arcane/governance/git-conventions.md +759 -0
- package/dist/assets/.arcane/governance/hardening-checklist.md +117 -0
- package/dist/assets/.arcane/governance/naming-conventions.md +142 -0
- package/dist/assets/.arcane/governance/new-business-setup.md +182 -0
- package/dist/assets/.arcane/governance/poc-management-pattern.md +190 -0
- package/dist/assets/.arcane/governance/portable-bootstrap.md +101 -0
- package/dist/assets/.arcane/governance/product-excellence-standards.md +211 -0
- package/dist/assets/.arcane/governance/rca-process-standard.md +157 -0
- package/dist/assets/.arcane/governance/spell-authoring-standards.md +129 -0
- package/dist/assets/.arcane/governance/testing-standards.md +158 -0
- package/dist/assets/.arcane/governance/threat-model.md +104 -0
- package/dist/assets/.arcane/governance/universal-agent-rules.md +108 -0
- package/dist/assets/.arcane/image-prompts/adelaide-image-prompt.md +140 -0
- package/dist/assets/.arcane/image-prompts/alexander-image-prompt.md +138 -0
- package/dist/assets/.arcane/image-prompts/bess-image-prompt.md +139 -0
- package/dist/assets/.arcane/image-prompts/circe-image-prompt.md +143 -0
- package/dist/assets/.arcane/image-prompts/custodio-image-prompt.md +140 -0
- package/dist/assets/.arcane/image-prompts/iris-image-prompt.md +139 -0
- package/dist/assets/.arcane/image-prompts/kellar-image-prompt.md +141 -0
- package/dist/assets/.arcane/image-prompts/lafayette-image-prompt.md +140 -0
- package/dist/assets/.arcane/image-prompts/lince-image-prompt.md +142 -0
- package/dist/assets/.arcane/image-prompts/mercurio-image-prompt.md +140 -0
- package/dist/assets/.arcane/image-prompts/merlin-image-prompt.md +139 -0
- package/dist/assets/.arcane/image-prompts/prospero-image-prompt.md +141 -0
- package/dist/assets/.arcane/templates/venture-template/overview.md +66 -0
- package/dist/assets/.claude/commands/spell-address-review.md +9 -0
- package/dist/assets/.claude/commands/spell-arcane-version.md +9 -0
- package/dist/assets/.claude/commands/spell-architect.md +9 -0
- package/dist/assets/.claude/commands/spell-bootstrap-business.md +9 -0
- package/dist/assets/.claude/commands/spell-brainstorm.md +9 -0
- package/dist/assets/.claude/commands/spell-bug.md +9 -0
- package/dist/assets/.claude/commands/spell-bump.md +9 -0
- package/dist/assets/.claude/commands/spell-check-drift.md +9 -0
- package/dist/assets/.claude/commands/spell-close-session.md +9 -0
- package/dist/assets/.claude/commands/spell-commit-work.md +9 -0
- package/dist/assets/.claude/commands/spell-create-pull-request.md +9 -0
- package/dist/assets/.claude/commands/spell-document.md +9 -0
- package/dist/assets/.claude/commands/spell-dotnet-expert.md +9 -0
- package/dist/assets/.claude/commands/spell-eas-ios-deploy.md +9 -0
- package/dist/assets/.claude/commands/spell-enchant.md +9 -0
- package/dist/assets/.claude/commands/spell-explain-concept.md +9 -0
- package/dist/assets/.claude/commands/spell-feedback.md +9 -0
- package/dist/assets/.claude/commands/spell-full-cycle.md +9 -0
- package/dist/assets/.claude/commands/spell-generate-bot-icons.md +9 -0
- package/dist/assets/.claude/commands/spell-implement.md +9 -0
- package/dist/assets/.claude/commands/spell-open-session.md +9 -0
- package/dist/assets/.claude/commands/spell-plan.md +9 -0
- package/dist/assets/.claude/commands/spell-present-arcane.md +9 -0
- package/dist/assets/.claude/commands/spell-product-review.md +9 -0
- package/dist/assets/.claude/commands/spell-review-batch.md +9 -0
- package/dist/assets/.claude/commands/spell-review.md +9 -0
- package/dist/assets/.claude/commands/spell-save-idea.md +9 -0
- package/dist/assets/.claude/commands/spell-scope.md +9 -0
- package/dist/assets/.claude/commands/spell-security-review.md +9 -0
- package/dist/assets/.claude/commands/spell-ship.md +9 -0
- package/dist/assets/.claude/commands/spell-status.md +9 -0
- package/dist/assets/.claude/commands/spell-suggest-feature.md +9 -0
- package/dist/assets/.claude/commands/spell-test.md +9 -0
- package/dist/assets/.claude/commands/spell-todo.md +9 -0
- package/dist/assets/.github/agents/adelaide.agent.md +44 -0
- package/dist/assets/.github/agents/alexander.agent.md +43 -0
- package/dist/assets/.github/agents/bess.agent.md +42 -0
- package/dist/assets/.github/agents/circe.agent.md +43 -0
- package/dist/assets/.github/agents/custodio.agent.md +44 -0
- package/dist/assets/.github/agents/iris.agent.md +43 -0
- package/dist/assets/.github/agents/kellar.agent.md +43 -0
- package/dist/assets/.github/agents/lafayette.agent.md +42 -0
- package/dist/assets/.github/agents/lince.agent.md +43 -0
- package/dist/assets/.github/agents/mercurio.agent.md +43 -0
- package/dist/assets/.github/agents/merlin.agent.md +44 -0
- package/dist/assets/.github/agents/prospero.agent.md +43 -0
- package/dist/assets/.github/instructions/agent-output.instructions.md +48 -0
- package/dist/assets/.github/prompts/spell-address-review.prompt.md +114 -0
- package/dist/assets/.github/prompts/spell-arcane-version.prompt.md +82 -0
- package/dist/assets/.github/prompts/spell-architect.prompt.md +116 -0
- package/dist/assets/.github/prompts/spell-bootstrap-business.prompt.md +73 -0
- package/dist/assets/.github/prompts/spell-brainstorm.prompt.md +124 -0
- package/dist/assets/.github/prompts/spell-bug.prompt.md +109 -0
- package/dist/assets/.github/prompts/spell-bump.prompt.md +73 -0
- package/dist/assets/.github/prompts/spell-check-drift.prompt.md +97 -0
- package/dist/assets/.github/prompts/spell-close-session.prompt.md +189 -0
- package/dist/assets/.github/prompts/spell-commit-work.prompt.md +211 -0
- package/dist/assets/.github/prompts/spell-create-pull-request.prompt.md +170 -0
- package/dist/assets/.github/prompts/spell-document.prompt.md +83 -0
- package/dist/assets/.github/prompts/spell-dotnet-expert.prompt.md +117 -0
- package/dist/assets/.github/prompts/spell-eas-ios-deploy.prompt.md +183 -0
- package/dist/assets/.github/prompts/spell-enchant.prompt.md +385 -0
- package/dist/assets/.github/prompts/spell-explain-concept.prompt.md +69 -0
- package/dist/assets/.github/prompts/spell-feedback.prompt.md +113 -0
- package/dist/assets/.github/prompts/spell-full-cycle.prompt.md +201 -0
- package/dist/assets/.github/prompts/spell-generate-bot-icons.prompt.md +84 -0
- package/dist/assets/.github/prompts/spell-implement.prompt.md +101 -0
- package/dist/assets/.github/prompts/spell-open-session.prompt.md +116 -0
- package/dist/assets/.github/prompts/spell-plan.prompt.md +145 -0
- package/dist/assets/.github/prompts/spell-present-arcane.prompt.md +188 -0
- package/dist/assets/.github/prompts/spell-product-review.prompt.md +97 -0
- package/dist/assets/.github/prompts/spell-review-batch.prompt.md +94 -0
- package/dist/assets/.github/prompts/spell-review.prompt.md +131 -0
- package/dist/assets/.github/prompts/spell-save-idea.prompt.md +83 -0
- package/dist/assets/.github/prompts/spell-scope.prompt.md +263 -0
- package/dist/assets/.github/prompts/spell-security-review.prompt.md +129 -0
- package/dist/assets/.github/prompts/spell-ship.prompt.md +104 -0
- package/dist/assets/.github/prompts/spell-status.prompt.md +85 -0
- package/dist/assets/.github/prompts/spell-suggest-feature.prompt.md +110 -0
- package/dist/assets/.github/prompts/spell-test.prompt.md +87 -0
- package/dist/assets/.github/prompts/spell-todo.prompt.md +126 -0
- package/dist/assets/DECISIONS.md +19 -0
- package/dist/assets/TODO.md +7 -0
- package/dist/assets/agents/architecture-lead.yaml +61 -0
- package/dist/assets/agents/collaborator.yaml +59 -0
- package/dist/assets/agents/devops.yaml +60 -0
- package/dist/assets/agents/frontend-dev.yaml +60 -0
- package/dist/assets/agents/fullstack-dev.yaml +59 -0
- package/dist/assets/agents/marketing-strategist.yaml +60 -0
- package/dist/assets/agents/mobile-dev.yaml +59 -0
- package/dist/assets/agents/operations-comms.yaml +58 -0
- package/dist/assets/agents/orchestrator.yaml +60 -0
- package/dist/assets/agents/qa-lead.yaml +60 -0
- package/dist/assets/agents/research-analyst.yaml +59 -0
- package/dist/assets/agents/security-ops.yaml +61 -0
- package/dist/assets/ai-context/system-prompt-context.md +32 -0
- package/dist/assets/journal/.gitkeep +0 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +2313 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createRequire } from "node:module";
|
|
3
|
+
const require = __createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { createRequire as createRequire2 } from "module";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { dirname as dirname3, join as join10 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/commands/init.ts
|
|
12
|
+
import { select as select2, confirm } from "@inquirer/prompts";
|
|
13
|
+
import { join as join4 } from "path";
|
|
14
|
+
import chalk2 from "chalk";
|
|
15
|
+
|
|
16
|
+
// src/modules/copier.ts
|
|
17
|
+
import {
|
|
18
|
+
mkdir,
|
|
19
|
+
copyFile as fsCopyFile,
|
|
20
|
+
access,
|
|
21
|
+
readdir
|
|
22
|
+
} from "fs/promises";
|
|
23
|
+
import path from "path";
|
|
24
|
+
function validateTargetPath(targetDir, relativePath) {
|
|
25
|
+
const resolvedBase = path.resolve(targetDir);
|
|
26
|
+
const resolvedFull = path.resolve(targetDir, relativePath);
|
|
27
|
+
if (resolvedFull !== resolvedBase && !resolvedFull.startsWith(resolvedBase + path.sep)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Path traversal detected: "${relativePath}" escapes target directory "${targetDir}"`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function ensureDir(dirPath) {
|
|
34
|
+
await mkdir(dirPath, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
async function fileExists(filePath) {
|
|
37
|
+
try {
|
|
38
|
+
await access(filePath);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
|
|
45
|
+
validateTargetPath(targetDir, relativePath);
|
|
46
|
+
const destPath = path.resolve(targetDir, relativePath);
|
|
47
|
+
await ensureDir(path.dirname(destPath));
|
|
48
|
+
if (!opts.force && await fileExists(destPath)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Destination already exists: "${relativePath}". Use --force to overwrite.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
await fsCopyFile(srcPath, destPath);
|
|
54
|
+
}
|
|
55
|
+
async function copyDirectory(srcDir, targetDir, relativeDir, opts = {}) {
|
|
56
|
+
validateTargetPath(targetDir, relativeDir);
|
|
57
|
+
const copiedFiles = [];
|
|
58
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const srcEntryPath = path.join(srcDir, entry.name);
|
|
61
|
+
const relEntryPath = `${relativeDir}/${entry.name}`;
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
const subFiles = await copyDirectory(srcEntryPath, targetDir, relEntryPath, opts);
|
|
64
|
+
copiedFiles.push(...subFiles);
|
|
65
|
+
} else {
|
|
66
|
+
await copyFile(srcEntryPath, targetDir, relEntryPath, opts);
|
|
67
|
+
copiedFiles.push(relEntryPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return copiedFiles;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/modules/manifest.ts
|
|
74
|
+
import { readFile, writeFile } from "fs/promises";
|
|
75
|
+
import path2 from "path";
|
|
76
|
+
var ManifestNotFoundError = class extends Error {
|
|
77
|
+
constructor(manifestPath2) {
|
|
78
|
+
super(`Manifest not found at "${manifestPath2}". Run "spell init" first.`);
|
|
79
|
+
this.name = "ManifestNotFoundError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ManifestCorruptError = class extends Error {
|
|
83
|
+
constructor(manifestPath2) {
|
|
84
|
+
super(
|
|
85
|
+
`Manifest at "${manifestPath2}" contains invalid JSON. It may be corrupted.`
|
|
86
|
+
);
|
|
87
|
+
this.name = "ManifestCorruptError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var MANIFEST_FILE = ".arcane.json";
|
|
91
|
+
function manifestPath(targetDir) {
|
|
92
|
+
return path2.join(targetDir, MANIFEST_FILE);
|
|
93
|
+
}
|
|
94
|
+
async function readManifest(targetDir) {
|
|
95
|
+
const filePath = manifestPath(targetDir);
|
|
96
|
+
let content;
|
|
97
|
+
try {
|
|
98
|
+
content = await readFile(filePath, "utf-8");
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err.code === "ENOENT") {
|
|
101
|
+
throw new ManifestNotFoundError(filePath);
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(content);
|
|
107
|
+
} catch {
|
|
108
|
+
throw new ManifestCorruptError(filePath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function writeManifest(targetDir, manifest) {
|
|
112
|
+
const filePath = manifestPath(targetDir);
|
|
113
|
+
await writeFile(filePath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
114
|
+
}
|
|
115
|
+
function addComponent(manifest, component) {
|
|
116
|
+
return {
|
|
117
|
+
...manifest,
|
|
118
|
+
components: [...manifest.components, component]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function hasComponent(manifest, name) {
|
|
122
|
+
return manifest.components.some((c) => c.name === name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/config/profiles.ts
|
|
126
|
+
var PROFILE_CONFIGS = [
|
|
127
|
+
{
|
|
128
|
+
id: "full",
|
|
129
|
+
displayName: "Full Suite",
|
|
130
|
+
description: "All components \u2014 spells, governance, agents, templates",
|
|
131
|
+
components: "*"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "lite",
|
|
135
|
+
displayName: "Essentials",
|
|
136
|
+
description: "Spells + core governance \u2014 fast start",
|
|
137
|
+
components: ["spell-prompts", "claude-commands", "agent-output-instructions", "git-conventions", "testing-standards", "session-continuity"]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "methodology",
|
|
141
|
+
displayName: "Methodology Suite",
|
|
142
|
+
description: "Spells + governance \u2014 full methodology without security/infra docs",
|
|
143
|
+
components: [
|
|
144
|
+
"spell-prompts",
|
|
145
|
+
"claude-commands",
|
|
146
|
+
"agent-output-instructions",
|
|
147
|
+
"git-conventions",
|
|
148
|
+
"testing-standards",
|
|
149
|
+
"session-continuity",
|
|
150
|
+
"decision-documentation-standard",
|
|
151
|
+
"agent-work-queue-model",
|
|
152
|
+
"development-methodology",
|
|
153
|
+
"spell-authoring-standards"
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "governance-only",
|
|
158
|
+
displayName: "Governance Only",
|
|
159
|
+
description: "Standards docs only \u2014 no spells or agents",
|
|
160
|
+
components: [
|
|
161
|
+
"agent-output-instructions",
|
|
162
|
+
"git-conventions",
|
|
163
|
+
"testing-standards",
|
|
164
|
+
"decision-documentation-standard",
|
|
165
|
+
"agent-work-queue-model",
|
|
166
|
+
"naming-conventions",
|
|
167
|
+
"agent-policies",
|
|
168
|
+
"threat-model",
|
|
169
|
+
"hardening-checklist",
|
|
170
|
+
"authentication-strategy",
|
|
171
|
+
"new-business-setup",
|
|
172
|
+
"agent-approved-paths",
|
|
173
|
+
"portable-bootstrap",
|
|
174
|
+
"development-methodology",
|
|
175
|
+
"cicd-standards",
|
|
176
|
+
"poc-management-pattern",
|
|
177
|
+
"product-excellence-standards",
|
|
178
|
+
"rca-process-standard",
|
|
179
|
+
"universal-agent-rules",
|
|
180
|
+
"spell-authoring-standards"
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
// src/modules/registry.ts
|
|
186
|
+
var ComponentNotFoundError = class extends Error {
|
|
187
|
+
constructor(name, available) {
|
|
188
|
+
super(
|
|
189
|
+
`Component "${name}" not found. Available components: ${available.join(", ")}`
|
|
190
|
+
);
|
|
191
|
+
this.name = "ComponentNotFoundError";
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var COMPONENTS = [
|
|
195
|
+
// Governance
|
|
196
|
+
{
|
|
197
|
+
name: "git-conventions",
|
|
198
|
+
description: "Git branching, commit message, and PR conventions",
|
|
199
|
+
files: [".arcane/governance/git-conventions.md"]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "testing-standards",
|
|
203
|
+
description: "Testing framework selection, coverage thresholds, and CI gates",
|
|
204
|
+
files: [".arcane/governance/testing-standards.md"]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "decision-documentation-standard",
|
|
208
|
+
description: "ADR format and decision-logging conventions",
|
|
209
|
+
files: [".arcane/governance/decision-documentation-standard.md"]
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "agent-work-queue-model",
|
|
213
|
+
description: "Work queue model for AI agent task assignment and tracking",
|
|
214
|
+
files: [".arcane/governance/agent-work-queue-model.md"]
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "naming-conventions",
|
|
218
|
+
description: "Machine, agent, and service naming rules (three-tier model)",
|
|
219
|
+
files: [".arcane/governance/naming-conventions.md"]
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "agent-policies",
|
|
223
|
+
description: "Agent autonomy levels, power levels, and operational constraints",
|
|
224
|
+
files: [".arcane/governance/agent-policies.md"]
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "threat-model",
|
|
228
|
+
description: "Security threat model covering STRIDE categories and mitigations",
|
|
229
|
+
files: [".arcane/governance/threat-model.md"]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "hardening-checklist",
|
|
233
|
+
description: "Production hardening checklist for infrastructure and agents",
|
|
234
|
+
files: [".arcane/governance/hardening-checklist.md"]
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "authentication-strategy",
|
|
238
|
+
description: "Authentication patterns, token management, and identity strategy",
|
|
239
|
+
files: [".arcane/governance/authentication-strategy.md"]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "new-business-setup",
|
|
243
|
+
description: "Step-by-step playbook for bootstrapping a new business unit",
|
|
244
|
+
files: [".arcane/governance/new-business-setup.md"]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "agent-approved-paths",
|
|
248
|
+
description: "Approved repository paths and access boundaries for agents",
|
|
249
|
+
files: [".arcane/governance/agent-approved-paths.md"]
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "portable-bootstrap",
|
|
253
|
+
description: "Portable AI context bootstrap for onboarding new AI clients",
|
|
254
|
+
files: [".arcane/governance/portable-bootstrap.md"]
|
|
255
|
+
},
|
|
256
|
+
// Governance — methodology
|
|
257
|
+
{
|
|
258
|
+
name: "development-methodology",
|
|
259
|
+
description: "Spell Loop development methodology",
|
|
260
|
+
files: [".arcane/governance/development-methodology.md"]
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "cicd-standards",
|
|
264
|
+
description: "CI/CD pipeline standards and quality gates",
|
|
265
|
+
files: [".arcane/governance/cicd-standards.md"]
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "poc-management-pattern",
|
|
269
|
+
description: "POC lifecycle management pattern",
|
|
270
|
+
files: [".arcane/governance/poc-management-pattern.md"]
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "product-excellence-standards",
|
|
274
|
+
description: "Product quality gate standards and spell-enchant requirements",
|
|
275
|
+
files: [".arcane/governance/product-excellence-standards.md"]
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "spell-authoring-standards",
|
|
279
|
+
description: "Spell Quality Rubric \u2014 the authoring bar for spell prompts (8 dimensions)",
|
|
280
|
+
files: [".arcane/governance/spell-authoring-standards.md"]
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "rca-process-standard",
|
|
284
|
+
description: "Root cause analysis process standard",
|
|
285
|
+
files: [".arcane/governance/rca-process-standard.md"]
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "universal-agent-rules",
|
|
289
|
+
description: "Universal rules for all AI agents operating in any Arcane-managed repo",
|
|
290
|
+
files: [".arcane/governance/universal-agent-rules.md"]
|
|
291
|
+
},
|
|
292
|
+
// VS Code Copilot instructions — installed to .github/instructions/
|
|
293
|
+
{
|
|
294
|
+
name: "agent-output-instructions",
|
|
295
|
+
description: "Copilot enforcement rules: PR link format, merge strategy, and branch naming",
|
|
296
|
+
files: [".github/instructions/agent-output.instructions.md"]
|
|
297
|
+
},
|
|
298
|
+
// Spell prompts — installed to .github/prompts/ (VS Code Copilot convention)
|
|
299
|
+
{
|
|
300
|
+
name: "spell-prompts",
|
|
301
|
+
description: "Full Arcane spell prompt library (33 spells)",
|
|
302
|
+
files: [
|
|
303
|
+
".github/prompts/spell-arcane-version.prompt.md",
|
|
304
|
+
".github/prompts/spell-architect.prompt.md",
|
|
305
|
+
".github/prompts/spell-brainstorm.prompt.md",
|
|
306
|
+
".github/prompts/spell-scope.prompt.md",
|
|
307
|
+
".github/prompts/spell-bootstrap-business.prompt.md",
|
|
308
|
+
".github/prompts/spell-bug.prompt.md",
|
|
309
|
+
".github/prompts/spell-bump.prompt.md",
|
|
310
|
+
".github/prompts/spell-check-drift.prompt.md",
|
|
311
|
+
".github/prompts/spell-close-session.prompt.md",
|
|
312
|
+
".github/prompts/spell-commit-work.prompt.md",
|
|
313
|
+
".github/prompts/spell-dotnet-expert.prompt.md",
|
|
314
|
+
".github/prompts/spell-enchant.prompt.md",
|
|
315
|
+
".github/prompts/spell-explain-concept.prompt.md",
|
|
316
|
+
".github/prompts/spell-full-cycle.prompt.md",
|
|
317
|
+
".github/prompts/spell-generate-bot-icons.prompt.md",
|
|
318
|
+
".github/prompts/spell-implement.prompt.md",
|
|
319
|
+
".github/prompts/spell-open-session.prompt.md",
|
|
320
|
+
".github/prompts/spell-plan.prompt.md",
|
|
321
|
+
".github/prompts/spell-present-arcane.prompt.md",
|
|
322
|
+
".github/prompts/spell-product-review.prompt.md",
|
|
323
|
+
".github/prompts/spell-review.prompt.md",
|
|
324
|
+
".github/prompts/spell-security-review.prompt.md",
|
|
325
|
+
".github/prompts/spell-ship.prompt.md",
|
|
326
|
+
".github/prompts/spell-suggest-feature.prompt.md",
|
|
327
|
+
".github/prompts/spell-test.prompt.md",
|
|
328
|
+
".github/prompts/spell-todo.prompt.md",
|
|
329
|
+
// Workflow spell additions — PR lifecycle, status, capture, feedback
|
|
330
|
+
".github/prompts/spell-address-review.prompt.md",
|
|
331
|
+
".github/prompts/spell-create-pull-request.prompt.md",
|
|
332
|
+
".github/prompts/spell-document.prompt.md",
|
|
333
|
+
".github/prompts/spell-feedback.prompt.md",
|
|
334
|
+
".github/prompts/spell-save-idea.prompt.md",
|
|
335
|
+
".github/prompts/spell-status.prompt.md",
|
|
336
|
+
".github/prompts/spell-review-batch.prompt.md"
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
// Templates
|
|
340
|
+
{
|
|
341
|
+
name: "venture-template",
|
|
342
|
+
description: "Starter template for a new business venture folder",
|
|
343
|
+
files: [".arcane/templates/venture-template/overview.md"]
|
|
344
|
+
},
|
|
345
|
+
// Session continuity — files required for spell-close-session / spell-open-session
|
|
346
|
+
{
|
|
347
|
+
name: "session-continuity",
|
|
348
|
+
description: "Session lifecycle files (TODO.md, DECISIONS.md, ai-context/, journal/) for close/open session spells",
|
|
349
|
+
skipExisting: true,
|
|
350
|
+
files: [
|
|
351
|
+
"TODO.md",
|
|
352
|
+
"DECISIONS.md",
|
|
353
|
+
"ai-context/system-prompt-context.md",
|
|
354
|
+
"journal/.gitkeep"
|
|
355
|
+
]
|
|
356
|
+
},
|
|
357
|
+
// Agent definitions
|
|
358
|
+
// NOTE: the full portable agent system is managed by `spell agents init` / sync / list.
|
|
359
|
+
// This component only installs the policy reference template for governance-only setups.
|
|
360
|
+
{
|
|
361
|
+
name: "agent-definitions",
|
|
362
|
+
description: "Agent policy reference template (for governance-only setups \u2014 use `spell agents init` for the full agent system)",
|
|
363
|
+
files: [".arcane/agents/agent-policies-template.md"]
|
|
364
|
+
},
|
|
365
|
+
// NOTE: the static `agent-files` component was retired — `.github/agents/*.agent.md`
|
|
366
|
+
// files are generated per-roster by `spell agents init` / `spell agents sync`, so
|
|
367
|
+
// installing a fixed-name set via `spell init` shipped stale names and collided
|
|
368
|
+
// with the generated output.
|
|
369
|
+
// Claude commands
|
|
370
|
+
{
|
|
371
|
+
name: "claude-commands",
|
|
372
|
+
description: "Claude Code slash command wrappers for all 33 Arcane spells",
|
|
373
|
+
files: [
|
|
374
|
+
".claude/commands/spell-arcane-version.md",
|
|
375
|
+
".claude/commands/spell-architect.md",
|
|
376
|
+
".claude/commands/spell-brainstorm.md",
|
|
377
|
+
".claude/commands/spell-scope.md",
|
|
378
|
+
".claude/commands/spell-bootstrap-business.md",
|
|
379
|
+
".claude/commands/spell-bug.md",
|
|
380
|
+
".claude/commands/spell-bump.md",
|
|
381
|
+
".claude/commands/spell-check-drift.md",
|
|
382
|
+
".claude/commands/spell-close-session.md",
|
|
383
|
+
".claude/commands/spell-commit-work.md",
|
|
384
|
+
".claude/commands/spell-dotnet-expert.md",
|
|
385
|
+
".claude/commands/spell-enchant.md",
|
|
386
|
+
".claude/commands/spell-explain-concept.md",
|
|
387
|
+
".claude/commands/spell-full-cycle.md",
|
|
388
|
+
".claude/commands/spell-generate-bot-icons.md",
|
|
389
|
+
".claude/commands/spell-implement.md",
|
|
390
|
+
".claude/commands/spell-open-session.md",
|
|
391
|
+
".claude/commands/spell-plan.md",
|
|
392
|
+
".claude/commands/spell-present-arcane.md",
|
|
393
|
+
".claude/commands/spell-product-review.md",
|
|
394
|
+
".claude/commands/spell-review.md",
|
|
395
|
+
".claude/commands/spell-security-review.md",
|
|
396
|
+
".claude/commands/spell-ship.md",
|
|
397
|
+
".claude/commands/spell-suggest-feature.md",
|
|
398
|
+
".claude/commands/spell-test.md",
|
|
399
|
+
".claude/commands/spell-todo.md",
|
|
400
|
+
// Workflow spell additions — PR lifecycle, status, capture, feedback
|
|
401
|
+
".claude/commands/spell-address-review.md",
|
|
402
|
+
".claude/commands/spell-create-pull-request.md",
|
|
403
|
+
".claude/commands/spell-document.md",
|
|
404
|
+
".claude/commands/spell-feedback.md",
|
|
405
|
+
".claude/commands/spell-save-idea.md",
|
|
406
|
+
".claude/commands/spell-status.md",
|
|
407
|
+
".claude/commands/spell-review-batch.md"
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
];
|
|
411
|
+
function buildProfileMap() {
|
|
412
|
+
const allNames = COMPONENTS.map((c) => c.name);
|
|
413
|
+
const map = {};
|
|
414
|
+
for (const config of PROFILE_CONFIGS) {
|
|
415
|
+
map[config.id] = config.components === "*" ? allNames : config.components;
|
|
416
|
+
}
|
|
417
|
+
return map;
|
|
418
|
+
}
|
|
419
|
+
var PROFILE_MAP = buildProfileMap();
|
|
420
|
+
var componentIndex = new Map(COMPONENTS.map((c) => [c.name, c]));
|
|
421
|
+
function getComponent(name) {
|
|
422
|
+
const component = componentIndex.get(name);
|
|
423
|
+
if (!component) {
|
|
424
|
+
throw new ComponentNotFoundError(name, Array.from(componentIndex.keys()));
|
|
425
|
+
}
|
|
426
|
+
return component;
|
|
427
|
+
}
|
|
428
|
+
function getProfile(profile) {
|
|
429
|
+
return PROFILE_MAP[profile].map((name) => getComponent(name));
|
|
430
|
+
}
|
|
431
|
+
function listProfiles() {
|
|
432
|
+
const allNames = COMPONENTS.map((c) => c.name);
|
|
433
|
+
return PROFILE_CONFIGS.map((config) => ({
|
|
434
|
+
id: config.id,
|
|
435
|
+
displayName: config.displayName,
|
|
436
|
+
description: config.description,
|
|
437
|
+
components: config.components === "*" ? [...allNames] : [...config.components]
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/modules/banner.ts
|
|
442
|
+
import chalk from "chalk";
|
|
443
|
+
import gradient from "gradient-string";
|
|
444
|
+
var TAGLINE = "Cast spells to ship software";
|
|
445
|
+
var STEEL = "#6f9fd8";
|
|
446
|
+
var STEEL_DEEP = "#3a6ea5";
|
|
447
|
+
var STEEL_DIM = "#5d6f93";
|
|
448
|
+
var INK_LABEL = "#c7d2ee";
|
|
449
|
+
var FAINT = "#2c3658";
|
|
450
|
+
var STOPS = ["#2a4d7d", "#4a7bb5", "#6f9fd8", "#a9c8ec"];
|
|
451
|
+
var arcaneGradient = gradient(STOPS);
|
|
452
|
+
var WORDMARK = [
|
|
453
|
+
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
454
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
|
|
455
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
456
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D",
|
|
457
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
458
|
+
"\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
459
|
+
];
|
|
460
|
+
var INDENT = " ";
|
|
461
|
+
var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
|
|
462
|
+
function sleep(ms) {
|
|
463
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
464
|
+
}
|
|
465
|
+
function isAnimatable() {
|
|
466
|
+
return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
|
|
467
|
+
}
|
|
468
|
+
function scale(hex, f) {
|
|
469
|
+
const v = (i) => Math.max(0, Math.min(255, Math.round(parseInt(hex.slice(i, i + 2), 16) * f)));
|
|
470
|
+
return "#" + [v(1), v(3), v(5)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
471
|
+
}
|
|
472
|
+
function lockupLines(f = 1) {
|
|
473
|
+
const g = f >= 1 ? arcaneGradient : gradient(STOPS.map((c) => scale(c, f)));
|
|
474
|
+
return WORDMARK.map((line) => INDENT + g(line));
|
|
475
|
+
}
|
|
476
|
+
function printFooter(version) {
|
|
477
|
+
const taglineText = ` ${TAGLINE} \xB7 agent-native SDLC \xB7 v${version}`;
|
|
478
|
+
const width = Math.max(LOCKUP_WIDTH, taglineText.length);
|
|
479
|
+
const border = chalk.hex(STEEL_DEEP)("\u2500".repeat(width));
|
|
480
|
+
console.log(border);
|
|
481
|
+
console.log(
|
|
482
|
+
chalk.hex(STEEL)(` ${TAGLINE}`),
|
|
483
|
+
chalk.hex(STEEL_DIM)(`\xB7 agent-native SDLC \xB7 v${version}`)
|
|
484
|
+
);
|
|
485
|
+
console.log(border);
|
|
486
|
+
}
|
|
487
|
+
function installSummary(components) {
|
|
488
|
+
const files = components.flatMap(
|
|
489
|
+
(c) => c.files.map((f) => f.replace(/\\/g, "/"))
|
|
490
|
+
);
|
|
491
|
+
const spells = new Set(
|
|
492
|
+
files.filter((f) => /\/(prompts|commands)\/spell-.+\.md$/.test(f)).map((f) => f.split("/").pop().replace(/\.prompt\.md$|\.md$/, ""))
|
|
493
|
+
).size;
|
|
494
|
+
const agents = files.filter((f) => /\/agents\/[^/]+\.agent\.md$/.test(f)).length;
|
|
495
|
+
const standards = files.filter(
|
|
496
|
+
(f) => /\/governance\/[^/]+\.md$/.test(f)
|
|
497
|
+
).length;
|
|
498
|
+
const parts = [];
|
|
499
|
+
if (spells) parts.push(`${spells} spell${spells === 1 ? "" : "s"}`);
|
|
500
|
+
if (agents) parts.push(`${agents} agent${agents === 1 ? "" : "s"}`);
|
|
501
|
+
if (standards) parts.push(`${standards} standard${standards === 1 ? "" : "s"}`);
|
|
502
|
+
return parts;
|
|
503
|
+
}
|
|
504
|
+
async function printWelcome(version, cwd) {
|
|
505
|
+
console.log();
|
|
506
|
+
if (isAnimatable()) {
|
|
507
|
+
const draw = (lines, redraw) => {
|
|
508
|
+
if (redraw) process.stdout.write(`\x1B[${lines.length}A`);
|
|
509
|
+
for (const line of lines) process.stdout.write(`\r\x1B[2K${line}
|
|
510
|
+
`);
|
|
511
|
+
};
|
|
512
|
+
const fade = [0.1, 0.22, 0.36, 0.52, 0.68, 0.84, 1];
|
|
513
|
+
for (let i = 0; i < fade.length; i++) {
|
|
514
|
+
draw(lockupLines(fade[i]), i > 0);
|
|
515
|
+
await sleep(90);
|
|
516
|
+
}
|
|
517
|
+
const pulse = [1, 0.9, 0.82, 0.9, 1];
|
|
518
|
+
for (let i = 0; i < pulse.length; i++) {
|
|
519
|
+
draw(lockupLines(pulse[i]), true);
|
|
520
|
+
await sleep(140);
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
for (const line of lockupLines()) console.log(line);
|
|
524
|
+
}
|
|
525
|
+
printFooter(version);
|
|
526
|
+
console.log();
|
|
527
|
+
const label = (s) => chalk.hex(STEEL_DIM)(s.padEnd(11));
|
|
528
|
+
const dot = chalk.hex(FAINT)("\xB7");
|
|
529
|
+
let repoLine;
|
|
530
|
+
try {
|
|
531
|
+
const manifest = await readManifest(cwd);
|
|
532
|
+
const parts = installSummary(manifest.components);
|
|
533
|
+
const summary = parts.length ? parts : [`${manifest.components.length} components`];
|
|
534
|
+
repoLine = `${chalk.hex(STEEL)("\u2713")} Arcane ${chalk.hex(INK_LABEL)(`\xAB${manifest.profile}\xBB`)} ${dot} ` + summary.map((p) => chalk.hex(INK_LABEL)(p)).join(` ${dot} `) + ` ${dot} ${chalk.hex(STEEL_DIM)(`v${manifest.version}`)}`;
|
|
535
|
+
} catch {
|
|
536
|
+
repoLine = `${chalk.hex(STEEL_DIM)("\u25CB")} not initialized ${chalk.hex(FAINT)("\u2014")} run ${chalk.hex(STEEL).inverse(" spell init ")} ${chalk.hex(STEEL_DIM)("to scaffold")}`;
|
|
537
|
+
}
|
|
538
|
+
console.log(` ${label("This repo")}${repoLine}`);
|
|
539
|
+
const phases = ["plan", "architect", "implement", "test", "review", "ship"].join(
|
|
540
|
+
chalk.hex(STEEL_DEEP)(" \u2192 ")
|
|
541
|
+
);
|
|
542
|
+
console.log(` ${label("Spell Loop")}${chalk.hex("#dbe3f7")(phases)}`);
|
|
543
|
+
const flat = ["plan", "architect", "implement", "test", "review", "ship"].join(" \u2192 ");
|
|
544
|
+
const loopStart = 13 + flat.indexOf("implement");
|
|
545
|
+
const loopEnd = 13 + flat.indexOf("review") + 5;
|
|
546
|
+
const inner = loopEnd - loopStart - 1;
|
|
547
|
+
const lp = Math.floor((inner - 19) / 2);
|
|
548
|
+
const rp = inner - 19 - lp;
|
|
549
|
+
const dash = (n) => chalk.hex(STEEL_DEEP)("\u2500".repeat(Math.max(0, n)));
|
|
550
|
+
console.log(
|
|
551
|
+
" ".repeat(loopStart) + chalk.hex(STEEL_DEEP)("\u2570") + dash(lp) + chalk.hex(STEEL)(" \u21BA ") + chalk.hex(STEEL_DIM)("until it passes") + " " + dash(rp) + chalk.hex(STEEL_DEEP)("\u256F")
|
|
552
|
+
);
|
|
553
|
+
console.log();
|
|
554
|
+
}
|
|
555
|
+
function printSuccess(message) {
|
|
556
|
+
console.log(`
|
|
557
|
+
${chalk.hex(STEEL)("\u2726")} ${chalk.bold(message)}`);
|
|
558
|
+
}
|
|
559
|
+
function printStep(message) {
|
|
560
|
+
console.log(` ${chalk.hex(STEEL_DEEP)("\u25B8")} ${message}`);
|
|
561
|
+
}
|
|
562
|
+
function printDryRun(message) {
|
|
563
|
+
console.log(` ${chalk.yellow("\u25CC")} ${chalk.dim(message)}`);
|
|
564
|
+
}
|
|
565
|
+
function printSection(title) {
|
|
566
|
+
console.log(`
|
|
567
|
+
${chalk.hex(STEEL)("\u2500")} ${chalk.bold(title)}`);
|
|
568
|
+
}
|
|
569
|
+
function printInfo(message) {
|
|
570
|
+
console.log(` ${chalk.dim(message)}`);
|
|
571
|
+
}
|
|
572
|
+
function printNextStep(step, message) {
|
|
573
|
+
console.log(` ${chalk.hex(STEEL_DEEP)(`${step}.`)} ${message}`);
|
|
574
|
+
}
|
|
575
|
+
function printWarning(message) {
|
|
576
|
+
console.log(` ${chalk.yellow("\u26A0")} ${chalk.yellow(message)}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/modules/agents.ts
|
|
580
|
+
import { select, checkbox, Separator } from "@inquirer/prompts";
|
|
581
|
+
import { mkdir as mkdir3, writeFile as writeFile4, copyFile as copyFile2 } from "fs/promises";
|
|
582
|
+
import { join as join3 } from "path";
|
|
583
|
+
import { stringify } from "yaml";
|
|
584
|
+
|
|
585
|
+
// src/config/agent-roles.ts
|
|
586
|
+
var AGENT_ROLES = [
|
|
587
|
+
{
|
|
588
|
+
id: "orchestrator",
|
|
589
|
+
role: "Product Operations Manager",
|
|
590
|
+
category: "operations"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
id: "architecture-lead",
|
|
594
|
+
role: "CTO / Architecture Lead",
|
|
595
|
+
category: "engineering"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
id: "fullstack-dev",
|
|
599
|
+
role: "Full-Stack Developer",
|
|
600
|
+
category: "engineering"
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
id: "qa-lead",
|
|
604
|
+
role: "QA Lead",
|
|
605
|
+
category: "quality"
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "devops",
|
|
609
|
+
role: "DevOps / CI/CD Engineer",
|
|
610
|
+
category: "engineering"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
id: "frontend-dev",
|
|
614
|
+
role: "Frontend Developer",
|
|
615
|
+
category: "engineering"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
id: "mobile-dev",
|
|
619
|
+
role: "Mobile Developer",
|
|
620
|
+
category: "engineering"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
id: "research-analyst",
|
|
624
|
+
role: "Research & Backlog Analyst",
|
|
625
|
+
category: "research"
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
id: "marketing-strategist",
|
|
629
|
+
role: "Marketing Strategist",
|
|
630
|
+
category: "operations"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
id: "operations-comms",
|
|
634
|
+
role: "Operations Communications",
|
|
635
|
+
category: "operations"
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: "collaborator",
|
|
639
|
+
role: "External Collaboration Lead",
|
|
640
|
+
category: "operations"
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
id: "security-ops",
|
|
644
|
+
role: "Security Operations",
|
|
645
|
+
category: "quality"
|
|
646
|
+
}
|
|
647
|
+
];
|
|
648
|
+
function getAgentRole(id) {
|
|
649
|
+
return AGENT_ROLES.find((r) => r.id === id);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/config/agent-profiles.ts
|
|
653
|
+
var AGENT_PROFILE_CONFIGS = [
|
|
654
|
+
{
|
|
655
|
+
id: "base",
|
|
656
|
+
displayName: "Base",
|
|
657
|
+
description: "Core team: orchestrator, architecture-lead, fullstack-dev, qa-lead",
|
|
658
|
+
roles: ["orchestrator", "architecture-lead", "fullstack-dev", "qa-lead"]
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
id: "full",
|
|
662
|
+
displayName: "Full",
|
|
663
|
+
description: "Complete roster \u2014 all 12 roles",
|
|
664
|
+
roles: [
|
|
665
|
+
"orchestrator",
|
|
666
|
+
"architecture-lead",
|
|
667
|
+
"fullstack-dev",
|
|
668
|
+
"qa-lead",
|
|
669
|
+
"devops",
|
|
670
|
+
"frontend-dev",
|
|
671
|
+
"mobile-dev",
|
|
672
|
+
"research-analyst",
|
|
673
|
+
"marketing-strategist",
|
|
674
|
+
"operations-comms",
|
|
675
|
+
"collaborator",
|
|
676
|
+
"security-ops"
|
|
677
|
+
]
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
id: "custom",
|
|
681
|
+
displayName: "Custom",
|
|
682
|
+
description: "Select roles interactively",
|
|
683
|
+
roles: []
|
|
684
|
+
// populated at runtime via checkbox prompt
|
|
685
|
+
}
|
|
686
|
+
];
|
|
687
|
+
function getAgentProfileConfig(id) {
|
|
688
|
+
const config = AGENT_PROFILE_CONFIGS.find((p) => p.id === id);
|
|
689
|
+
if (!config) throw new Error(`Unknown agent profile: "${id}"`);
|
|
690
|
+
return config;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/modules/naming.ts
|
|
694
|
+
import { input } from "@inquirer/prompts";
|
|
695
|
+
var GENERIC_NAME_MAP = {
|
|
696
|
+
"orchestrator": "Orchestrator",
|
|
697
|
+
"architecture-lead": "Architect",
|
|
698
|
+
"fullstack-dev": "Developer",
|
|
699
|
+
"qa-lead": "QA Lead",
|
|
700
|
+
"devops": "DevOps",
|
|
701
|
+
"frontend-dev": "Frontend",
|
|
702
|
+
"mobile-dev": "Mobile",
|
|
703
|
+
"research-analyst": "Researcher",
|
|
704
|
+
"marketing-strategist": "Marketer",
|
|
705
|
+
"operations-comms": "Comms",
|
|
706
|
+
"collaborator": "Collaborator",
|
|
707
|
+
"security-ops": "Security"
|
|
708
|
+
};
|
|
709
|
+
var ARCANOS_NAME_MAP = {
|
|
710
|
+
"orchestrator": "Kellar",
|
|
711
|
+
"architecture-lead": "Merlin",
|
|
712
|
+
"fullstack-dev": "Lafayette",
|
|
713
|
+
"qa-lead": "Lince",
|
|
714
|
+
"devops": "Prospero",
|
|
715
|
+
"frontend-dev": "Adelaide",
|
|
716
|
+
"mobile-dev": "Mercurio",
|
|
717
|
+
"research-analyst": "Alexander",
|
|
718
|
+
"marketing-strategist": "Circe",
|
|
719
|
+
"operations-comms": "Bess",
|
|
720
|
+
"collaborator": "Iris",
|
|
721
|
+
"security-ops": "Custodio"
|
|
722
|
+
};
|
|
723
|
+
var RANDOM_NAME_POOL = [
|
|
724
|
+
"Aiden",
|
|
725
|
+
"Blake",
|
|
726
|
+
"Casey",
|
|
727
|
+
"Dana",
|
|
728
|
+
"Ellis",
|
|
729
|
+
"Finley",
|
|
730
|
+
"Gray",
|
|
731
|
+
"Harper",
|
|
732
|
+
"Indigo",
|
|
733
|
+
"Jordan",
|
|
734
|
+
"Kai",
|
|
735
|
+
"Lane",
|
|
736
|
+
"Morgan",
|
|
737
|
+
"Nova",
|
|
738
|
+
"Owen",
|
|
739
|
+
"Parker",
|
|
740
|
+
"Quinn",
|
|
741
|
+
"River",
|
|
742
|
+
"Sage",
|
|
743
|
+
"Taylor",
|
|
744
|
+
"Uma",
|
|
745
|
+
"Vale",
|
|
746
|
+
"Wren",
|
|
747
|
+
"Xander",
|
|
748
|
+
"Yael",
|
|
749
|
+
"Zara",
|
|
750
|
+
"Avery",
|
|
751
|
+
"Brett",
|
|
752
|
+
"Charlie",
|
|
753
|
+
"Devon"
|
|
754
|
+
];
|
|
755
|
+
function applyGenericNames(roleIds) {
|
|
756
|
+
return roleIds.map((id) => ({
|
|
757
|
+
definition: id,
|
|
758
|
+
name: GENERIC_NAME_MAP[id] ?? id
|
|
759
|
+
}));
|
|
760
|
+
}
|
|
761
|
+
function applyArcanosNames(roleIds) {
|
|
762
|
+
return roleIds.map((id) => ({
|
|
763
|
+
definition: id,
|
|
764
|
+
name: ARCANOS_NAME_MAP[id] ?? GENERIC_NAME_MAP[id] ?? id
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
function applyRandomNames(roleIds) {
|
|
768
|
+
const shuffled = [...RANDOM_NAME_POOL].sort(() => Math.random() - 0.5);
|
|
769
|
+
return roleIds.map((id, i) => ({
|
|
770
|
+
definition: id,
|
|
771
|
+
name: shuffled[i % shuffled.length]
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
async function promptCustomNames(roleIds) {
|
|
775
|
+
const results = [];
|
|
776
|
+
for (const id of roleIds) {
|
|
777
|
+
const role = getAgentRole(id);
|
|
778
|
+
const defaultName = GENERIC_NAME_MAP[id] ?? id;
|
|
779
|
+
const name = await input({
|
|
780
|
+
message: `Name for ${role?.role ?? id}:`,
|
|
781
|
+
default: defaultName,
|
|
782
|
+
validate: (v) => v.trim().length > 0 ? true : "Name cannot be empty"
|
|
783
|
+
});
|
|
784
|
+
results.push({ definition: id, name: name.trim() });
|
|
785
|
+
}
|
|
786
|
+
return results;
|
|
787
|
+
}
|
|
788
|
+
async function applyNamingStrategy(strategy, roleIds) {
|
|
789
|
+
switch (strategy) {
|
|
790
|
+
case "arcanos":
|
|
791
|
+
return applyArcanosNames(roleIds);
|
|
792
|
+
case "generic":
|
|
793
|
+
return applyGenericNames(roleIds);
|
|
794
|
+
case "random":
|
|
795
|
+
return applyRandomNames(roleIds);
|
|
796
|
+
case "custom":
|
|
797
|
+
return promptCustomNames(roleIds);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/modules/agent-loader.ts
|
|
802
|
+
import { readFile as readFile2, readdir as readdir2, access as access2 } from "fs/promises";
|
|
803
|
+
import { join } from "path";
|
|
804
|
+
import { parse } from "yaml";
|
|
805
|
+
var AgentDefinitionNotFoundError = class extends Error {
|
|
806
|
+
constructor(id, searchDir) {
|
|
807
|
+
super(
|
|
808
|
+
`Agent definition "${id}" not found in "${searchDir}". Check that ${id}.yaml exists or run "spell agents add ${id}".`
|
|
809
|
+
);
|
|
810
|
+
this.name = "AgentDefinitionNotFoundError";
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
var AgentRosterNotFoundError = class extends Error {
|
|
814
|
+
constructor() {
|
|
815
|
+
super(
|
|
816
|
+
'No agent roster found at .arcane/agents.yaml. Run "spell agents init" to create one.'
|
|
817
|
+
);
|
|
818
|
+
this.name = "AgentRosterNotFoundError";
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
async function loadAgentDefinition(agentsDir, id) {
|
|
822
|
+
const filePath = join(agentsDir, `${id}.yaml`);
|
|
823
|
+
let content;
|
|
824
|
+
try {
|
|
825
|
+
content = await readFile2(filePath, "utf8");
|
|
826
|
+
} catch {
|
|
827
|
+
throw new AgentDefinitionNotFoundError(id, agentsDir);
|
|
828
|
+
}
|
|
829
|
+
return parse(content);
|
|
830
|
+
}
|
|
831
|
+
async function loadRoster(targetDir) {
|
|
832
|
+
const rosterPath = join(targetDir, ".arcane", "agents.yaml");
|
|
833
|
+
let content;
|
|
834
|
+
try {
|
|
835
|
+
content = await readFile2(rosterPath, "utf8");
|
|
836
|
+
} catch {
|
|
837
|
+
throw new AgentRosterNotFoundError();
|
|
838
|
+
}
|
|
839
|
+
return parse(content);
|
|
840
|
+
}
|
|
841
|
+
async function rosterExists(targetDir) {
|
|
842
|
+
try {
|
|
843
|
+
await access2(join(targetDir, ".arcane", "agents.yaml"));
|
|
844
|
+
return true;
|
|
845
|
+
} catch {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function projectAgentsDir(targetDir) {
|
|
850
|
+
return join(targetDir, ".arcane", "agents");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/modules/agent-generator.ts
|
|
854
|
+
import { writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
855
|
+
import { join as join2 } from "path";
|
|
856
|
+
import { homedir } from "os";
|
|
857
|
+
|
|
858
|
+
// src/modules/merger.ts
|
|
859
|
+
import { readFile as readFile3, writeFile as writeFile2, rm } from "fs/promises";
|
|
860
|
+
import path3 from "path";
|
|
861
|
+
var MARKER_START = "<!-- arcane:start -->";
|
|
862
|
+
var MARKER_END = "<!-- arcane:end -->";
|
|
863
|
+
var MalformedMarkersError = class extends Error {
|
|
864
|
+
constructor(message) {
|
|
865
|
+
super(message);
|
|
866
|
+
this.name = "MalformedMarkersError";
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
async function mergeIntoFile(targetDir, relativePath, content, opts = {}) {
|
|
870
|
+
validateTargetPath(targetDir, relativePath);
|
|
871
|
+
const destPath = path3.resolve(targetDir, relativePath);
|
|
872
|
+
let existing;
|
|
873
|
+
try {
|
|
874
|
+
existing = await readFile3(destPath, "utf8");
|
|
875
|
+
} catch (err) {
|
|
876
|
+
const e = err;
|
|
877
|
+
if (e.code === "ENOENT") {
|
|
878
|
+
existing = null;
|
|
879
|
+
} else {
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (existing === null) {
|
|
884
|
+
if (!opts.force) return false;
|
|
885
|
+
const newContent = `${MARKER_START}
|
|
886
|
+
${content}
|
|
887
|
+
${MARKER_END}
|
|
888
|
+
`;
|
|
889
|
+
if (!opts.dryRun) {
|
|
890
|
+
await writeFile2(destPath, newContent, "utf8");
|
|
891
|
+
}
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
895
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
896
|
+
const hasStart = startIdx !== -1;
|
|
897
|
+
const hasEnd = endIdx !== -1;
|
|
898
|
+
if (hasStart !== hasEnd) {
|
|
899
|
+
throw new MalformedMarkersError(
|
|
900
|
+
`File "${relativePath}" has mismatched arcane markers: ${hasStart ? "start found" : "start missing"}, ${hasEnd ? "end found" : "end missing"}.`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (hasStart && hasEnd) {
|
|
904
|
+
if (startIdx > endIdx) {
|
|
905
|
+
throw new MalformedMarkersError(
|
|
906
|
+
`File "${relativePath}" has arcane markers in wrong order (end before start).`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const beforeMarker = existing.slice(0, startIdx + MARKER_START.length);
|
|
910
|
+
const afterMarker = existing.slice(endIdx);
|
|
911
|
+
const updated = `${beforeMarker}
|
|
912
|
+
${content}
|
|
913
|
+
${afterMarker}`;
|
|
914
|
+
if (!opts.dryRun) {
|
|
915
|
+
await writeFile2(destPath, updated, "utf8");
|
|
916
|
+
}
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
if (!opts.force) return false;
|
|
920
|
+
const appended = `${existing}${existing.endsWith("\n") ? "" : "\n"}${MARKER_START}
|
|
921
|
+
${content}
|
|
922
|
+
${MARKER_END}
|
|
923
|
+
`;
|
|
924
|
+
if (!opts.dryRun) {
|
|
925
|
+
await writeFile2(destPath, appended, "utf8");
|
|
926
|
+
}
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
async function stripMarkerSection(targetDir, relativePath, opts = {}) {
|
|
930
|
+
const destPath = path3.resolve(targetDir, relativePath);
|
|
931
|
+
let existing;
|
|
932
|
+
try {
|
|
933
|
+
existing = await readFile3(destPath, "utf8");
|
|
934
|
+
} catch {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
938
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
939
|
+
if (startIdx === -1 || endIdx === -1) return false;
|
|
940
|
+
const before = existing.slice(0, startIdx);
|
|
941
|
+
const after = existing.slice(endIdx + MARKER_END.length);
|
|
942
|
+
const stripped = (before + after).replace(/\n{3,}/g, "\n\n").trim();
|
|
943
|
+
if (!opts.dryRun) {
|
|
944
|
+
if (stripped.length === 0) {
|
|
945
|
+
await rm(destPath, { force: true });
|
|
946
|
+
} else {
|
|
947
|
+
await writeFile2(destPath, stripped + "\n", "utf8");
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/modules/agent-generator.ts
|
|
954
|
+
function renderIdentity(def, name) {
|
|
955
|
+
const mottosLine = def.persona.catchphrases && def.persona.catchphrases.length > 0 ? `
|
|
956
|
+
## Mottos
|
|
957
|
+
|
|
958
|
+
${def.persona.catchphrases.map((c) => `- "${c}"`).join("\n")}
|
|
959
|
+
` : "";
|
|
960
|
+
return `# ${name} \u2014 ${def.role}
|
|
961
|
+
|
|
962
|
+
## Role
|
|
963
|
+
|
|
964
|
+
${def.persona.description.trim()}
|
|
965
|
+
|
|
966
|
+
## Category
|
|
967
|
+
|
|
968
|
+
${def.category}
|
|
969
|
+
${mottosLine}`;
|
|
970
|
+
}
|
|
971
|
+
function renderSoul(def, name) {
|
|
972
|
+
const rules = def.persona.behavioral_rules.map((r) => `- ${r}`).join("\n");
|
|
973
|
+
return `# ${name} \u2014 Behavioral Directives
|
|
974
|
+
|
|
975
|
+
## Rules
|
|
976
|
+
|
|
977
|
+
${rules}
|
|
978
|
+
|
|
979
|
+
## Autonomy
|
|
980
|
+
|
|
981
|
+
- **Default Power Level:** ${def.autonomy.default_power_level}
|
|
982
|
+
- **Exec Allowed:** ${def.autonomy.exec_allowed ? "Yes" : "No"}
|
|
983
|
+
`;
|
|
984
|
+
}
|
|
985
|
+
function renderTools(def, name) {
|
|
986
|
+
const allowed = def.tools.allowed.map((t) => `- \`${t}\``).join("\n");
|
|
987
|
+
const denied = def.tools.denied.map((t) => `- \`${t}\``).join("\n");
|
|
988
|
+
return `# ${name} \u2014 Tool Capabilities
|
|
989
|
+
|
|
990
|
+
## Allowed
|
|
991
|
+
|
|
992
|
+
${allowed}
|
|
993
|
+
|
|
994
|
+
## Denied
|
|
995
|
+
|
|
996
|
+
${denied}
|
|
997
|
+
`;
|
|
998
|
+
}
|
|
999
|
+
function renderCopilotAgent(def, name) {
|
|
1000
|
+
const firstLine = def.persona.description.split("\n")[0]?.trim() ?? def.role;
|
|
1001
|
+
const rules = def.persona.behavioral_rules.map((r) => `- ${r}`).join("\n");
|
|
1002
|
+
const personalityBlock = def.persona.personality ? `
|
|
1003
|
+
## Personality
|
|
1004
|
+
|
|
1005
|
+
${def.persona.personality.trim()}
|
|
1006
|
+
` : "";
|
|
1007
|
+
const voiceBlock = def.persona.voice ? `
|
|
1008
|
+
## Voice
|
|
1009
|
+
|
|
1010
|
+
${def.persona.voice.trim()}
|
|
1011
|
+
` : "";
|
|
1012
|
+
const mottosBlock = def.persona.catchphrases && def.persona.catchphrases.length > 0 ? `
|
|
1013
|
+
## Mottos
|
|
1014
|
+
|
|
1015
|
+
${def.persona.catchphrases.map((c) => `- "${c}"`).join("\n")}
|
|
1016
|
+
` : "";
|
|
1017
|
+
return `---
|
|
1018
|
+
name: ${name}
|
|
1019
|
+
description: ${def.role} \u2014 ${firstLine}
|
|
1020
|
+
---
|
|
1021
|
+
${mottosBlock}
|
|
1022
|
+
## Role
|
|
1023
|
+
|
|
1024
|
+
${def.persona.description.trim()}
|
|
1025
|
+
${personalityBlock}${voiceBlock}
|
|
1026
|
+
## Behavioral Rules
|
|
1027
|
+
|
|
1028
|
+
${rules}
|
|
1029
|
+
|
|
1030
|
+
## Tools
|
|
1031
|
+
|
|
1032
|
+
**Allowed:** ${def.tools.allowed.join(", ")}
|
|
1033
|
+
**Denied:** ${def.tools.denied.join(", ")}
|
|
1034
|
+
`;
|
|
1035
|
+
}
|
|
1036
|
+
function renderAgentRosterSection(entries) {
|
|
1037
|
+
const rows = entries.map(
|
|
1038
|
+
({ def, displayName }) => `| **${displayName}** | ${def.role} | ${def.category} |`
|
|
1039
|
+
).join("\n");
|
|
1040
|
+
return `## Agent Roster
|
|
1041
|
+
|
|
1042
|
+
| Agent | Role | Category |
|
|
1043
|
+
|-------|------|----------|
|
|
1044
|
+
${rows}
|
|
1045
|
+
`;
|
|
1046
|
+
}
|
|
1047
|
+
async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
1048
|
+
const synced = [];
|
|
1049
|
+
const skipped = [];
|
|
1050
|
+
const projectDefs = projectAgentsDir(targetDir);
|
|
1051
|
+
const bundledDefs = join2(assetsDir, "agents");
|
|
1052
|
+
const resolved = [];
|
|
1053
|
+
for (const entry of roster.roster) {
|
|
1054
|
+
let def;
|
|
1055
|
+
try {
|
|
1056
|
+
def = await loadAgentDefinition(projectDefs, entry.definition);
|
|
1057
|
+
} catch {
|
|
1058
|
+
try {
|
|
1059
|
+
def = await loadAgentDefinition(bundledDefs, entry.definition);
|
|
1060
|
+
} catch {
|
|
1061
|
+
skipped.push(`${entry.definition} (definition not found)`);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
resolved.push({
|
|
1066
|
+
entry,
|
|
1067
|
+
def,
|
|
1068
|
+
displayName: entry.name ?? entry.definition
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
if (roster.openclaw.enabled && options.openclaw !== false) {
|
|
1072
|
+
const openclawRoot = roster.openclaw.workspace_root.replace(
|
|
1073
|
+
/^~/,
|
|
1074
|
+
homedir()
|
|
1075
|
+
);
|
|
1076
|
+
for (const { entry, def, displayName } of resolved) {
|
|
1077
|
+
if (!def.clients.openclaw) continue;
|
|
1078
|
+
const workspaceName = entry.id === "main" ? "workspace" : `workspace-${entry.id}`;
|
|
1079
|
+
const workspaceDir = join2(openclawRoot, workspaceName);
|
|
1080
|
+
try {
|
|
1081
|
+
if (!options.dryRun) {
|
|
1082
|
+
await mkdir2(workspaceDir, { recursive: true });
|
|
1083
|
+
await writeFile3(
|
|
1084
|
+
join2(workspaceDir, "IDENTITY.md"),
|
|
1085
|
+
renderIdentity(def, displayName),
|
|
1086
|
+
"utf8"
|
|
1087
|
+
);
|
|
1088
|
+
await writeFile3(
|
|
1089
|
+
join2(workspaceDir, "SOUL.md"),
|
|
1090
|
+
renderSoul(def, displayName),
|
|
1091
|
+
"utf8"
|
|
1092
|
+
);
|
|
1093
|
+
await writeFile3(
|
|
1094
|
+
join2(workspaceDir, "TOOLS.md"),
|
|
1095
|
+
renderTools(def, displayName),
|
|
1096
|
+
"utf8"
|
|
1097
|
+
);
|
|
1098
|
+
} else {
|
|
1099
|
+
console.log(` [dry-run] Would write: ${workspaceDir}/IDENTITY.md`);
|
|
1100
|
+
console.log(` [dry-run] Would write: ${workspaceDir}/SOUL.md`);
|
|
1101
|
+
console.log(` [dry-run] Would write: ${workspaceDir}/TOOLS.md`);
|
|
1102
|
+
}
|
|
1103
|
+
synced.push(`openclaw:${entry.id}:IDENTITY+SOUL+TOOLS`);
|
|
1104
|
+
} catch {
|
|
1105
|
+
skipped.push(`openclaw:${entry.id} (workspace not accessible)`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const patch = {
|
|
1109
|
+
$comment: "Apply this to your openclaw.json agents section. Do not edit manually \u2014 regenerated by `spell agents sync`.",
|
|
1110
|
+
agents: resolved.map(({ entry, def }) => ({
|
|
1111
|
+
id: entry.id,
|
|
1112
|
+
identity: {
|
|
1113
|
+
name: entry.name ?? entry.definition,
|
|
1114
|
+
role: def.role
|
|
1115
|
+
}
|
|
1116
|
+
}))
|
|
1117
|
+
};
|
|
1118
|
+
const generatedDir = join2(targetDir, ".arcane", "generated");
|
|
1119
|
+
if (!options.dryRun) {
|
|
1120
|
+
await mkdir2(generatedDir, { recursive: true });
|
|
1121
|
+
await writeFile3(
|
|
1122
|
+
join2(generatedDir, "openclaw-roster.json"),
|
|
1123
|
+
JSON.stringify(patch, null, 2) + "\n",
|
|
1124
|
+
"utf8"
|
|
1125
|
+
);
|
|
1126
|
+
} else {
|
|
1127
|
+
console.log(
|
|
1128
|
+
" [dry-run] Would write: .arcane/generated/openclaw-roster.json"
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
synced.push("openclaw-roster.json");
|
|
1132
|
+
}
|
|
1133
|
+
if (options.copilot !== false) {
|
|
1134
|
+
const agentsDir = join2(targetDir, ".github", "agents");
|
|
1135
|
+
if (!options.dryRun) {
|
|
1136
|
+
await mkdir2(agentsDir, { recursive: true });
|
|
1137
|
+
}
|
|
1138
|
+
for (const { def, displayName } of resolved) {
|
|
1139
|
+
if (!def.clients.copilot?.agent_file) continue;
|
|
1140
|
+
const slug = displayName.toLowerCase().replace(/\s+/g, "-");
|
|
1141
|
+
const fileName = `${slug}.agent.md`;
|
|
1142
|
+
if (!options.dryRun) {
|
|
1143
|
+
await writeFile3(
|
|
1144
|
+
join2(agentsDir, fileName),
|
|
1145
|
+
renderCopilotAgent(def, displayName),
|
|
1146
|
+
"utf8"
|
|
1147
|
+
);
|
|
1148
|
+
} else {
|
|
1149
|
+
console.log(` [dry-run] Would write: .github/agents/${fileName}`);
|
|
1150
|
+
}
|
|
1151
|
+
synced.push(`.github/agents/${fileName}`);
|
|
1152
|
+
}
|
|
1153
|
+
const rosterSection = renderAgentRosterSection(resolved);
|
|
1154
|
+
const copilotMerged = await mergeIntoFile(
|
|
1155
|
+
targetDir,
|
|
1156
|
+
".github/copilot-instructions.md",
|
|
1157
|
+
rosterSection,
|
|
1158
|
+
{ force: true, dryRun: options.dryRun }
|
|
1159
|
+
);
|
|
1160
|
+
if (copilotMerged)
|
|
1161
|
+
synced.push(".github/copilot-instructions.md (agents section)");
|
|
1162
|
+
}
|
|
1163
|
+
if (options.claude !== false) {
|
|
1164
|
+
const rosterSection = renderAgentRosterSection(resolved);
|
|
1165
|
+
const claudeMerged = await mergeIntoFile(
|
|
1166
|
+
targetDir,
|
|
1167
|
+
"CLAUDE.md",
|
|
1168
|
+
rosterSection,
|
|
1169
|
+
{ force: true, dryRun: options.dryRun }
|
|
1170
|
+
);
|
|
1171
|
+
if (claudeMerged) synced.push("CLAUDE.md (agents section)");
|
|
1172
|
+
}
|
|
1173
|
+
if (options.codex !== false) {
|
|
1174
|
+
const rosterSection = renderAgentRosterSection(resolved);
|
|
1175
|
+
const codexMerged = await mergeIntoFile(
|
|
1176
|
+
targetDir,
|
|
1177
|
+
"AGENTS.md",
|
|
1178
|
+
rosterSection,
|
|
1179
|
+
{ force: true, dryRun: options.dryRun }
|
|
1180
|
+
);
|
|
1181
|
+
if (codexMerged) synced.push("AGENTS.md (agents section)");
|
|
1182
|
+
}
|
|
1183
|
+
return { synced, skipped };
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/modules/agents.ts
|
|
1187
|
+
async function runAgentsInit(options, targetDir, assetsDir) {
|
|
1188
|
+
if (!options.force && await rosterExists(targetDir)) {
|
|
1189
|
+
console.log(
|
|
1190
|
+
'Agent roster already exists at .arcane/agents.yaml. Run "spell agents sync" to regenerate outputs, or use --force to reinitialize.'
|
|
1191
|
+
);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
console.log();
|
|
1195
|
+
let profileId = options.profile ?? "full";
|
|
1196
|
+
if (!options.profile) {
|
|
1197
|
+
const baseRoles = getAgentProfileConfig("base").roles;
|
|
1198
|
+
const baseRoleNames = baseRoles.map((id) => {
|
|
1199
|
+
const role = AGENT_ROLES.find((r) => r.id === id);
|
|
1200
|
+
return role ? role.role : id;
|
|
1201
|
+
});
|
|
1202
|
+
const allCategories = [...new Set(AGENT_ROLES.map((r) => r.category))];
|
|
1203
|
+
profileId = await select({
|
|
1204
|
+
message: "Select an agent profile:",
|
|
1205
|
+
choices: AGENT_PROFILE_CONFIGS.map((p) => ({
|
|
1206
|
+
value: p.id,
|
|
1207
|
+
name: `${p.id === "base" ? "\u{1F3AF}" : p.id === "full" ? "\u{1F9D9}" : "\u{1F3A8}"} ${p.displayName}`,
|
|
1208
|
+
description: p.id === "base" ? `
|
|
1209
|
+
4 core roles: ${baseRoleNames.join(", ")}` : p.id === "full" ? `
|
|
1210
|
+
All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from the full roster"
|
|
1211
|
+
}))
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
let selectedRoleIds;
|
|
1215
|
+
if (profileId === "custom") {
|
|
1216
|
+
const categories = [...new Set(AGENT_ROLES.map((r) => r.category))];
|
|
1217
|
+
const choicesWithSeparators = [];
|
|
1218
|
+
for (const cat of categories) {
|
|
1219
|
+
choicesWithSeparators.push(new Separator(`\u2500\u2500 ${cat.charAt(0).toUpperCase() + cat.slice(1)} \u2500\u2500`));
|
|
1220
|
+
for (const r of AGENT_ROLES.filter((r2) => r2.category === cat)) {
|
|
1221
|
+
choicesWithSeparators.push({
|
|
1222
|
+
value: r.id,
|
|
1223
|
+
name: `${r.id} \u2014 ${r.role}`,
|
|
1224
|
+
checked: true
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
console.log();
|
|
1229
|
+
selectedRoleIds = await checkbox({
|
|
1230
|
+
message: "Select roles to install:",
|
|
1231
|
+
choices: choicesWithSeparators,
|
|
1232
|
+
validate: (choices) => choices.length > 0 ? true : "Select at least one role"
|
|
1233
|
+
});
|
|
1234
|
+
} else {
|
|
1235
|
+
selectedRoleIds = getAgentProfileConfig(profileId).roles;
|
|
1236
|
+
}
|
|
1237
|
+
let namingStrategy = options.naming ?? "arcanos";
|
|
1238
|
+
if (!options.naming) {
|
|
1239
|
+
console.log();
|
|
1240
|
+
namingStrategy = await select({
|
|
1241
|
+
message: "Choose a naming strategy for your agents:",
|
|
1242
|
+
choices: [
|
|
1243
|
+
{
|
|
1244
|
+
value: "arcanos",
|
|
1245
|
+
name: "\u{1F0CF} Arcanos",
|
|
1246
|
+
description: "\nPersona names: Kellar, Merlin, Lafayette, Lince, Prospero..."
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
value: "generic",
|
|
1250
|
+
name: "\u{1F3F7}\uFE0F Generic",
|
|
1251
|
+
description: "\nRole-derived labels: Orchestrator, Architect, Developer, QA Lead..."
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
value: "random",
|
|
1255
|
+
name: "\u{1F3B2} Random",
|
|
1256
|
+
description: "\nCommon first names shuffled randomly: Blake, Morgan, Casey..."
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
value: "custom",
|
|
1260
|
+
name: "\u270F\uFE0F Custom",
|
|
1261
|
+
description: "\nType a unique name for each role interactively"
|
|
1262
|
+
}
|
|
1263
|
+
]
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
printStep("Assigning names...");
|
|
1267
|
+
const named = await applyNamingStrategy(namingStrategy, selectedRoleIds);
|
|
1268
|
+
const rosterEntries = named.map(({ definition, name }) => {
|
|
1269
|
+
const id = definition === "orchestrator" ? "main" : name.toLowerCase().replace(/\s+/g, "-");
|
|
1270
|
+
return { definition, name, id };
|
|
1271
|
+
});
|
|
1272
|
+
const roster = {
|
|
1273
|
+
schema_version: 2,
|
|
1274
|
+
naming_strategy: namingStrategy,
|
|
1275
|
+
agent_profile: profileId,
|
|
1276
|
+
openclaw: {
|
|
1277
|
+
enabled: true,
|
|
1278
|
+
workspace_root: "~/.openclaw"
|
|
1279
|
+
},
|
|
1280
|
+
roster: rosterEntries
|
|
1281
|
+
};
|
|
1282
|
+
const arcaneDir = join3(targetDir, ".arcane", "agents");
|
|
1283
|
+
if (!options.dryRun) {
|
|
1284
|
+
await mkdir3(arcaneDir, { recursive: true });
|
|
1285
|
+
await writeFile4(
|
|
1286
|
+
join3(targetDir, ".arcane", "agents.yaml"),
|
|
1287
|
+
stringify(roster),
|
|
1288
|
+
"utf8"
|
|
1289
|
+
);
|
|
1290
|
+
} else {
|
|
1291
|
+
console.log(" [dry-run] Would write: .arcane/agents.yaml");
|
|
1292
|
+
}
|
|
1293
|
+
for (const roleId of selectedRoleIds) {
|
|
1294
|
+
const src = join3(assetsDir, "agents", `${roleId}.yaml`);
|
|
1295
|
+
const dest = join3(arcaneDir, `${roleId}.yaml`);
|
|
1296
|
+
if (!options.dryRun) {
|
|
1297
|
+
try {
|
|
1298
|
+
await copyFile2(src, dest);
|
|
1299
|
+
} catch {
|
|
1300
|
+
console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
|
|
1301
|
+
}
|
|
1302
|
+
} else {
|
|
1303
|
+
console.log(` [dry-run] Would copy: .arcane/agents/${roleId}.yaml`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (options.dryRun) {
|
|
1307
|
+
console.log(
|
|
1308
|
+
`
|
|
1309
|
+
[dry-run] Would initialize roster with ${selectedRoleIds.length} agents`
|
|
1310
|
+
);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
printStep("Syncing agent definitions to all clients...");
|
|
1314
|
+
const { synced, skipped } = await syncAgents(
|
|
1315
|
+
targetDir,
|
|
1316
|
+
assetsDir,
|
|
1317
|
+
roster,
|
|
1318
|
+
{ dryRun: false }
|
|
1319
|
+
);
|
|
1320
|
+
printSuccess("Agent roster initialized");
|
|
1321
|
+
console.log(` \u{1F4CB} Profile: ${profileId} \xB7 Naming: ${namingStrategy} \xB7 Agents: ${rosterEntries.length}`);
|
|
1322
|
+
console.log(` \u{1F504} Synced: ${synced.length} outputs (Copilot, Claude, Codex, OpenClaw)`);
|
|
1323
|
+
if (skipped.length > 0) {
|
|
1324
|
+
console.log(` \u26A0\uFE0F Skipped: ${skipped.length} (${skipped.join(", ")})`);
|
|
1325
|
+
}
|
|
1326
|
+
console.log();
|
|
1327
|
+
console.log(` \u{1F4A1} Customize: edit .arcane/agents.yaml \u2192 spell agents sync`);
|
|
1328
|
+
console.log();
|
|
1329
|
+
}
|
|
1330
|
+
async function runAgentsSync(options, targetDir, assetsDir) {
|
|
1331
|
+
let roster;
|
|
1332
|
+
try {
|
|
1333
|
+
roster = await loadRoster(targetDir);
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
if (err instanceof AgentRosterNotFoundError) {
|
|
1336
|
+
console.error(err.message);
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
throw err;
|
|
1340
|
+
}
|
|
1341
|
+
if (options.dryRun) {
|
|
1342
|
+
console.log("\n [dry-run] Would sync agents from roster:\n");
|
|
1343
|
+
} else {
|
|
1344
|
+
console.log("\n Syncing agents...\n");
|
|
1345
|
+
}
|
|
1346
|
+
const { synced, skipped } = await syncAgents(
|
|
1347
|
+
targetDir,
|
|
1348
|
+
assetsDir,
|
|
1349
|
+
roster,
|
|
1350
|
+
options
|
|
1351
|
+
);
|
|
1352
|
+
if (options.dryRun) {
|
|
1353
|
+
console.log(`
|
|
1354
|
+
[dry-run] ${synced.length} outputs would be generated`);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
console.log(`
|
|
1358
|
+
\u2713 Synced ${synced.length} outputs`);
|
|
1359
|
+
for (const item of synced) {
|
|
1360
|
+
console.log(` + ${item}`);
|
|
1361
|
+
}
|
|
1362
|
+
if (skipped.length > 0) {
|
|
1363
|
+
console.log(`
|
|
1364
|
+
! Skipped ${skipped.length}:`);
|
|
1365
|
+
for (const item of skipped) {
|
|
1366
|
+
console.log(` - ${item}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
console.log();
|
|
1370
|
+
}
|
|
1371
|
+
async function runAgentsList(targetDir) {
|
|
1372
|
+
let roster;
|
|
1373
|
+
try {
|
|
1374
|
+
roster = await loadRoster(targetDir);
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
if (err instanceof AgentRosterNotFoundError) {
|
|
1377
|
+
console.error(err.message);
|
|
1378
|
+
process.exit(1);
|
|
1379
|
+
}
|
|
1380
|
+
throw err;
|
|
1381
|
+
}
|
|
1382
|
+
const nameWidth = Math.max(
|
|
1383
|
+
"Name".length,
|
|
1384
|
+
...roster.roster.map((e) => (e.name ?? e.definition).length)
|
|
1385
|
+
);
|
|
1386
|
+
const defWidth = Math.max(
|
|
1387
|
+
"Role ID".length,
|
|
1388
|
+
...roster.roster.map((e) => e.definition.length)
|
|
1389
|
+
);
|
|
1390
|
+
const idWidth = Math.max(
|
|
1391
|
+
"OpenClaw ID".length,
|
|
1392
|
+
...roster.roster.map((e) => e.id.length)
|
|
1393
|
+
);
|
|
1394
|
+
const pad = (s, n) => s.padEnd(n);
|
|
1395
|
+
console.log(
|
|
1396
|
+
`
|
|
1397
|
+
Profile: ${roster.agent_profile} | Naming: ${roster.naming_strategy} | Agents: ${roster.roster.length}
|
|
1398
|
+
`
|
|
1399
|
+
);
|
|
1400
|
+
console.log(
|
|
1401
|
+
` ${pad("Name", nameWidth)} ${pad("Role ID", defWidth)} ${pad("OpenClaw ID", idWidth)}`
|
|
1402
|
+
);
|
|
1403
|
+
console.log(
|
|
1404
|
+
` ${"-".repeat(nameWidth)} ${"-".repeat(defWidth)} ${"-".repeat(idWidth)}`
|
|
1405
|
+
);
|
|
1406
|
+
for (const entry of roster.roster) {
|
|
1407
|
+
console.log(
|
|
1408
|
+
` ${pad(entry.name ?? "(unnamed)", nameWidth)} ${pad(entry.definition, defWidth)} ${pad(entry.id, idWidth)}`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
console.log();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/modules/git.ts
|
|
1415
|
+
import { execFile } from "child_process";
|
|
1416
|
+
import { promisify } from "util";
|
|
1417
|
+
var execFileAsync = promisify(execFile);
|
|
1418
|
+
async function countUncommittedChanges(cwd) {
|
|
1419
|
+
try {
|
|
1420
|
+
const { stdout } = await execFileAsync("git", ["status", "--porcelain", "-uall"], { cwd });
|
|
1421
|
+
const lines = stdout.trim();
|
|
1422
|
+
return lines.length === 0 ? 0 : lines.split("\n").length;
|
|
1423
|
+
} catch {
|
|
1424
|
+
return 0;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/commands/init.ts
|
|
1429
|
+
var VALID_PROFILES = ["full", "lite", "governance-only", "methodology"];
|
|
1430
|
+
function categorizeComponents(components) {
|
|
1431
|
+
const groups = [];
|
|
1432
|
+
const prompts = components.find((c) => c.name === "spell-prompts");
|
|
1433
|
+
if (prompts) {
|
|
1434
|
+
groups.push({
|
|
1435
|
+
icon: "\u2728",
|
|
1436
|
+
label: "Copilot Spells",
|
|
1437
|
+
count: prompts.files.length
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
const claude = components.find((c) => c.name === "claude-commands");
|
|
1441
|
+
if (claude) {
|
|
1442
|
+
groups.push({
|
|
1443
|
+
icon: "\u26A1",
|
|
1444
|
+
label: "Claude Spells",
|
|
1445
|
+
count: claude.files.length
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
const governance = components.filter(
|
|
1449
|
+
(c) => c.name !== "spell-prompts" && c.name !== "claude-commands" && c.name !== "venture-template" && c.name !== "agent-definitions" && c.files.some((f) => f.startsWith(".arcane/governance/"))
|
|
1450
|
+
);
|
|
1451
|
+
if (governance.length > 0) {
|
|
1452
|
+
groups.push({
|
|
1453
|
+
icon: "\u{1F4CB}",
|
|
1454
|
+
label: "Governance Docs",
|
|
1455
|
+
count: governance.reduce((n, c) => n + c.files.length, 0)
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
const agentDef = components.find((c) => c.name === "agent-definitions");
|
|
1459
|
+
if (agentDef) {
|
|
1460
|
+
groups.push({
|
|
1461
|
+
icon: "\u{1F6E1}\uFE0F",
|
|
1462
|
+
label: "Agent Policies",
|
|
1463
|
+
count: agentDef.files.length
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
const templates = components.find((c) => c.name === "venture-template");
|
|
1467
|
+
if (templates) {
|
|
1468
|
+
groups.push({
|
|
1469
|
+
icon: "\u{1F4C4}",
|
|
1470
|
+
label: "Templates",
|
|
1471
|
+
count: templates.files.length
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
return groups;
|
|
1475
|
+
}
|
|
1476
|
+
function buildProfileDescription(components, profile) {
|
|
1477
|
+
const groups = categorizeComponents(components);
|
|
1478
|
+
const parts = groups.map((g) => `${g.icon} ${g.count} ${g.label}`);
|
|
1479
|
+
if (profile === "full" || profile === "lite") {
|
|
1480
|
+
const govIndex = groups.findIndex((g) => g.label === "Governance Docs");
|
|
1481
|
+
parts.splice(govIndex >= 0 ? govIndex : parts.length, 0, "\u{1F916} 12 Agent Roles");
|
|
1482
|
+
}
|
|
1483
|
+
return "\n" + parts.join("\n");
|
|
1484
|
+
}
|
|
1485
|
+
async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
1486
|
+
if (options.profile && !VALID_PROFILES.includes(options.profile)) {
|
|
1487
|
+
throw new Error(
|
|
1488
|
+
`Invalid profile "${options.profile}". Valid options: ${VALID_PROFILES.join(", ")}`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
await readManifest(targetDir);
|
|
1493
|
+
console.log(chalk2.hex("#a855f7")(`
|
|
1494
|
+
\u2726 Arcane v${packageVersion}
|
|
1495
|
+
`));
|
|
1496
|
+
console.log('Already initialized. Run "spell update" to update existing files.');
|
|
1497
|
+
return;
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
if (!(err instanceof ManifestNotFoundError)) throw err;
|
|
1500
|
+
}
|
|
1501
|
+
console.log(chalk2.hex("#a855f7")(`
|
|
1502
|
+
\u2726 Arcane v${packageVersion}
|
|
1503
|
+
`));
|
|
1504
|
+
if (!options.dryRun) {
|
|
1505
|
+
const dirtyCount = await countUncommittedChanges(targetDir);
|
|
1506
|
+
if (dirtyCount > 0) {
|
|
1507
|
+
printWarning(`This repo has ${dirtyCount} uncommitted change${dirtyCount === 1 ? "" : "s"}.`);
|
|
1508
|
+
printInfo("Running spell init will add new files to your working tree.");
|
|
1509
|
+
console.log();
|
|
1510
|
+
const continueAnyway = await confirm({
|
|
1511
|
+
message: "Continue anyway?",
|
|
1512
|
+
default: true
|
|
1513
|
+
});
|
|
1514
|
+
if (!continueAnyway) {
|
|
1515
|
+
console.log("\nCancelled. Commit or stash your changes first.");
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
console.log();
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
let profile = options.profile;
|
|
1522
|
+
if (!profile) {
|
|
1523
|
+
const profiles = listProfiles();
|
|
1524
|
+
profile = await select2({
|
|
1525
|
+
message: "Select an installation profile:\n",
|
|
1526
|
+
choices: profiles.map((p) => {
|
|
1527
|
+
const profileComponents = getProfile(p.id);
|
|
1528
|
+
return {
|
|
1529
|
+
value: p.id,
|
|
1530
|
+
name: `${p.id === "full" ? "\u{1F9D9}" : p.id === "lite" ? "\u26A1" : "\u{1F4CB}"} ${p.displayName} \u2014 ${p.description}`,
|
|
1531
|
+
description: buildProfileDescription(profileComponents, p.id)
|
|
1532
|
+
};
|
|
1533
|
+
})
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
const components = getProfile(profile);
|
|
1537
|
+
const groups = categorizeComponents(components);
|
|
1538
|
+
console.log();
|
|
1539
|
+
printSection(`\u{1F4E6} Profile: ${profile}`);
|
|
1540
|
+
console.log();
|
|
1541
|
+
const hasAgents = profile === "full" || profile === "lite";
|
|
1542
|
+
const previewParts = groups.map((g) => `${g.icon} ${g.count} ${g.label}`);
|
|
1543
|
+
if (hasAgents) {
|
|
1544
|
+
const govIndex = groups.findIndex((g) => g.label === "Governance Docs");
|
|
1545
|
+
previewParts.splice(govIndex >= 0 ? govIndex : previewParts.length, 0, "\u{1F916} 12 Agent Roles");
|
|
1546
|
+
}
|
|
1547
|
+
console.log(` ${previewParts.join(" \xB7 ")}`);
|
|
1548
|
+
console.log();
|
|
1549
|
+
if (!options.dryRun && !options.profile) {
|
|
1550
|
+
const proceed = await confirm({
|
|
1551
|
+
message: "Proceed with installation?",
|
|
1552
|
+
default: true
|
|
1553
|
+
});
|
|
1554
|
+
if (!proceed) {
|
|
1555
|
+
console.log("\nCancelled.");
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const installedComponents = [];
|
|
1560
|
+
let fileCount = 0;
|
|
1561
|
+
for (const component of components) {
|
|
1562
|
+
const installedFiles = [];
|
|
1563
|
+
for (const file of component.files) {
|
|
1564
|
+
const srcPath = join4(assetsDir, file);
|
|
1565
|
+
if (options.dryRun) {
|
|
1566
|
+
printDryRun(`Would copy: ${file}`);
|
|
1567
|
+
} else {
|
|
1568
|
+
try {
|
|
1569
|
+
await copyFile(srcPath, targetDir, file, { force: options.force });
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
if (component.skipExisting && err instanceof Error && err.message.includes("already exists")) {
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
throw err;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
installedFiles.push(file);
|
|
1578
|
+
fileCount++;
|
|
1579
|
+
}
|
|
1580
|
+
for (const dir of component.directories ?? []) {
|
|
1581
|
+
const srcDirPath = join4(assetsDir, dir);
|
|
1582
|
+
if (options.dryRun) {
|
|
1583
|
+
printDryRun(`Would copy directory: ${dir}/`);
|
|
1584
|
+
} else {
|
|
1585
|
+
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: options.force });
|
|
1586
|
+
installedFiles.push(...copied);
|
|
1587
|
+
fileCount += copied.length;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
if (!options.dryRun) {
|
|
1591
|
+
installedComponents.push({
|
|
1592
|
+
name: component.name,
|
|
1593
|
+
files: installedFiles,
|
|
1594
|
+
installedVersion: packageVersion
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (!options.dryRun) {
|
|
1599
|
+
for (const group of groups) {
|
|
1600
|
+
printStep(`${group.icon} ${group.count} ${group.label}`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (options.dryRun) {
|
|
1604
|
+
if (hasAgents) {
|
|
1605
|
+
const agentFiles = await import("fs").then(
|
|
1606
|
+
(fsModule) => fsModule.promises.readdir(join4(assetsDir, "agents"))
|
|
1607
|
+
);
|
|
1608
|
+
const yamlFiles = agentFiles.filter((f) => f.endsWith(".yaml")).sort();
|
|
1609
|
+
printDryRun("Would run agent setup (interactive):");
|
|
1610
|
+
printDryRun("Would write: .arcane/agents.yaml");
|
|
1611
|
+
for (const f of yamlFiles) {
|
|
1612
|
+
printDryRun(`Would copy: .arcane/agents/${f}`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
printDryRun(`Would initialize with profile "${profile}" \u2014 ${fileCount} files`);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const manifest = {
|
|
1619
|
+
version: packageVersion,
|
|
1620
|
+
profile,
|
|
1621
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1622
|
+
components: installedComponents
|
|
1623
|
+
};
|
|
1624
|
+
await writeManifest(targetDir, manifest);
|
|
1625
|
+
printSuccess(`Initialized with profile "${profile}" \u2014 ${fileCount} files installed`);
|
|
1626
|
+
let agentsConfigured = false;
|
|
1627
|
+
if (hasAgents && !options.profile) {
|
|
1628
|
+
console.log();
|
|
1629
|
+
const setupAgents = await confirm({
|
|
1630
|
+
message: "\u{1F916} Set up your agent team? (names, roles, AI tool configs)",
|
|
1631
|
+
default: true
|
|
1632
|
+
});
|
|
1633
|
+
if (setupAgents) {
|
|
1634
|
+
await runAgentsInit({}, targetDir, assetsDir);
|
|
1635
|
+
agentsConfigured = true;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
console.log();
|
|
1639
|
+
printSection("\u{1F680} Next steps");
|
|
1640
|
+
console.log();
|
|
1641
|
+
let step = 1;
|
|
1642
|
+
if (!agentsConfigured && profile !== "governance-only") {
|
|
1643
|
+
printNextStep(step++, "Run `spell agents init` to set up your agent team");
|
|
1644
|
+
}
|
|
1645
|
+
if (agentsConfigured) {
|
|
1646
|
+
printNextStep(
|
|
1647
|
+
step++,
|
|
1648
|
+
"Edit .arcane/agents.yaml to customize roles \u2192 `spell agents sync` to regenerate AI tool configs"
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
if (profile !== "governance-only") {
|
|
1652
|
+
printNextStep(
|
|
1653
|
+
step++,
|
|
1654
|
+
"Review .arcane/governance/ docs and customize for your project"
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
printNextStep(
|
|
1658
|
+
step++,
|
|
1659
|
+
"Commit the new files: git add .arcane .github .claude AGENTS.md CLAUDE.md .arcane.json"
|
|
1660
|
+
);
|
|
1661
|
+
console.log();
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/commands/add.ts
|
|
1665
|
+
import { join as join5 } from "path";
|
|
1666
|
+
async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
1667
|
+
let manifest;
|
|
1668
|
+
try {
|
|
1669
|
+
manifest = await readManifest(targetDir);
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
if (err instanceof ManifestNotFoundError) {
|
|
1672
|
+
console.error(
|
|
1673
|
+
'Not initialized. Run "spell init" first before adding components.'
|
|
1674
|
+
);
|
|
1675
|
+
process.exit(1);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
throw err;
|
|
1679
|
+
}
|
|
1680
|
+
let component;
|
|
1681
|
+
try {
|
|
1682
|
+
component = getComponent(name);
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
if (err instanceof ComponentNotFoundError) {
|
|
1685
|
+
console.error(err.message);
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
throw err;
|
|
1690
|
+
}
|
|
1691
|
+
if (hasComponent(manifest, name)) {
|
|
1692
|
+
console.log(
|
|
1693
|
+
`Component "${name}" is already installed. Use "spell update" to update it.`
|
|
1694
|
+
);
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
let fileCount = 0;
|
|
1698
|
+
const installedFiles = [];
|
|
1699
|
+
for (const file of component.files) {
|
|
1700
|
+
const srcPath = join5(assetsDir, file);
|
|
1701
|
+
if (options.dryRun) {
|
|
1702
|
+
console.log(` [dry-run] Would copy: ${file}`);
|
|
1703
|
+
} else {
|
|
1704
|
+
await copyFile(srcPath, targetDir, file, { force: options.force });
|
|
1705
|
+
}
|
|
1706
|
+
installedFiles.push(file);
|
|
1707
|
+
fileCount++;
|
|
1708
|
+
}
|
|
1709
|
+
for (const dir of component.directories ?? []) {
|
|
1710
|
+
const srcDirPath = join5(assetsDir, dir);
|
|
1711
|
+
if (options.dryRun) {
|
|
1712
|
+
console.log(` [dry-run] Would copy directory: ${dir}/`);
|
|
1713
|
+
} else {
|
|
1714
|
+
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: options.force });
|
|
1715
|
+
installedFiles.push(...copied);
|
|
1716
|
+
fileCount += copied.length;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (options.dryRun) {
|
|
1720
|
+
console.log(
|
|
1721
|
+
`
|
|
1722
|
+
[dry-run] Would add component "${name}" \u2014 ${fileCount} files`
|
|
1723
|
+
);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const newComponent = {
|
|
1727
|
+
name,
|
|
1728
|
+
files: installedFiles,
|
|
1729
|
+
installedVersion: packageVersion
|
|
1730
|
+
};
|
|
1731
|
+
const updated = addComponent(manifest, newComponent);
|
|
1732
|
+
await writeManifest(targetDir, updated);
|
|
1733
|
+
console.log(
|
|
1734
|
+
`
|
|
1735
|
+
\u2713 Added component "${name}" \u2014 ${fileCount} files copied`
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/commands/update.ts
|
|
1740
|
+
import { join as join6 } from "path";
|
|
1741
|
+
async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
1742
|
+
let manifest;
|
|
1743
|
+
try {
|
|
1744
|
+
manifest = await readManifest(targetDir);
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
if (err instanceof ManifestNotFoundError) {
|
|
1747
|
+
console.error(
|
|
1748
|
+
'Not initialized. Run "spell init" first before updating.'
|
|
1749
|
+
);
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
throw err;
|
|
1754
|
+
}
|
|
1755
|
+
if (manifest.version === packageVersion && manifest.components.length > 0) {
|
|
1756
|
+
console.log("Already up to date.");
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (manifest.components.length === 0) {
|
|
1760
|
+
console.log("No components installed. Nothing to update.");
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
let fileCount = 0;
|
|
1764
|
+
const updatedComponents = [];
|
|
1765
|
+
for (const installed of manifest.components) {
|
|
1766
|
+
let component;
|
|
1767
|
+
try {
|
|
1768
|
+
component = getComponent(installed.name);
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
if (err instanceof ComponentNotFoundError) {
|
|
1771
|
+
console.log(` ! ${installed.name} not in registry \u2014 skipping.`);
|
|
1772
|
+
updatedComponents.push(installed);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
throw err;
|
|
1776
|
+
}
|
|
1777
|
+
const updatedFiles = [];
|
|
1778
|
+
for (const file of component.files) {
|
|
1779
|
+
const srcPath = join6(assetsDir, file);
|
|
1780
|
+
if (options.dryRun) {
|
|
1781
|
+
console.log(` [dry-run] Would update: ${file}`);
|
|
1782
|
+
} else {
|
|
1783
|
+
await copyFile(srcPath, targetDir, file, { force: true });
|
|
1784
|
+
}
|
|
1785
|
+
updatedFiles.push(file);
|
|
1786
|
+
fileCount++;
|
|
1787
|
+
}
|
|
1788
|
+
for (const dir of component.directories ?? []) {
|
|
1789
|
+
const srcDirPath = join6(assetsDir, dir);
|
|
1790
|
+
if (options.dryRun) {
|
|
1791
|
+
console.log(` [dry-run] Would update directory: ${dir}/`);
|
|
1792
|
+
} else {
|
|
1793
|
+
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true });
|
|
1794
|
+
updatedFiles.push(...copied);
|
|
1795
|
+
fileCount += copied.length;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
updatedComponents.push({
|
|
1799
|
+
...installed,
|
|
1800
|
+
files: updatedFiles,
|
|
1801
|
+
installedVersion: packageVersion
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
if (options.dryRun) {
|
|
1805
|
+
console.log(
|
|
1806
|
+
`
|
|
1807
|
+
[dry-run] Would update ${fileCount} files.`
|
|
1808
|
+
);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const updated = { ...manifest, version: packageVersion, components: updatedComponents };
|
|
1812
|
+
await writeManifest(targetDir, updated);
|
|
1813
|
+
console.log(
|
|
1814
|
+
`
|
|
1815
|
+
\u2713 Updated ${fileCount} files.`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// src/modules/version-check.ts
|
|
1820
|
+
import { readFile as readFile4, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
|
|
1821
|
+
import { dirname, join as join7 } from "path";
|
|
1822
|
+
import { homedir as homedir2 } from "os";
|
|
1823
|
+
import { createRequire } from "module";
|
|
1824
|
+
var require2 = createRequire(import.meta.url);
|
|
1825
|
+
function getFeedUrl() {
|
|
1826
|
+
const pkg2 = require2("../../package.json");
|
|
1827
|
+
const url = pkg2.publishConfig?.registry;
|
|
1828
|
+
if (!url) {
|
|
1829
|
+
throw new Error(
|
|
1830
|
+
"publishConfig.registry is not set in package.json. Cannot resolve npm feed URL."
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
return url;
|
|
1834
|
+
}
|
|
1835
|
+
var DEFAULT_CACHE_PATH = join7(homedir2(), ".arcane", "version-cache.json");
|
|
1836
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1837
|
+
async function readCache(cachePath) {
|
|
1838
|
+
try {
|
|
1839
|
+
const content = await readFile4(cachePath, "utf-8");
|
|
1840
|
+
const parsed = JSON.parse(content);
|
|
1841
|
+
if (typeof parsed.latest !== "string" || typeof parsed.checkedAt !== "string") {
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
return parsed;
|
|
1845
|
+
} catch {
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
async function writeCache(cachePath, latest) {
|
|
1850
|
+
await mkdir4(dirname(cachePath), { recursive: true });
|
|
1851
|
+
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1852
|
+
await writeFile5(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
1853
|
+
}
|
|
1854
|
+
async function getLatestVersion(feedUrl) {
|
|
1855
|
+
const normalizedBase = feedUrl.endsWith("/") ? feedUrl : feedUrl + "/";
|
|
1856
|
+
const url = `${normalizedBase}arcane-cli`;
|
|
1857
|
+
try {
|
|
1858
|
+
const response = await fetch(url, {
|
|
1859
|
+
headers: { Accept: "application/json" },
|
|
1860
|
+
signal: AbortSignal.timeout(5e3)
|
|
1861
|
+
});
|
|
1862
|
+
if (!response.ok) return null;
|
|
1863
|
+
const body = await response.json();
|
|
1864
|
+
return body["dist-tags"]?.latest ?? null;
|
|
1865
|
+
} catch {
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async function checkForUpdate(currentVersion, feedUrl, cachePath = DEFAULT_CACHE_PATH) {
|
|
1870
|
+
try {
|
|
1871
|
+
let latest = null;
|
|
1872
|
+
const cached = await readCache(cachePath);
|
|
1873
|
+
if (cached) {
|
|
1874
|
+
const age = Date.now() - new Date(cached.checkedAt).getTime();
|
|
1875
|
+
if (age < CACHE_TTL_MS) {
|
|
1876
|
+
latest = cached.latest;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (latest === null) {
|
|
1880
|
+
latest = await getLatestVersion(feedUrl);
|
|
1881
|
+
if (latest !== null) {
|
|
1882
|
+
try {
|
|
1883
|
+
await writeCache(cachePath, latest);
|
|
1884
|
+
} catch {
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return {
|
|
1889
|
+
current: currentVersion,
|
|
1890
|
+
latest,
|
|
1891
|
+
updateAvailable: latest !== null && latest !== currentVersion
|
|
1892
|
+
};
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
return {
|
|
1895
|
+
current: currentVersion,
|
|
1896
|
+
latest: null,
|
|
1897
|
+
updateAvailable: false,
|
|
1898
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/commands/status.ts
|
|
1904
|
+
async function runStatus(targetDir, packageVersion) {
|
|
1905
|
+
let manifest;
|
|
1906
|
+
try {
|
|
1907
|
+
manifest = await readManifest(targetDir);
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
if (err instanceof ManifestNotFoundError) {
|
|
1910
|
+
console.error(
|
|
1911
|
+
'Not initialized. Run "spell init" first.'
|
|
1912
|
+
);
|
|
1913
|
+
process.exit(1);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
throw err;
|
|
1917
|
+
}
|
|
1918
|
+
if (manifest.components.length === 0) {
|
|
1919
|
+
console.log("No components installed.");
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
let feedUrl;
|
|
1923
|
+
try {
|
|
1924
|
+
feedUrl = getFeedUrl();
|
|
1925
|
+
} catch {
|
|
1926
|
+
feedUrl = "";
|
|
1927
|
+
}
|
|
1928
|
+
const versionResult = feedUrl ? await checkForUpdate(packageVersion, feedUrl) : { current: packageVersion, latest: null, updateAvailable: false, error: "Feed URL unavailable" };
|
|
1929
|
+
const nameWidth = Math.max(
|
|
1930
|
+
"Component".length,
|
|
1931
|
+
...manifest.components.map((c) => c.name.length)
|
|
1932
|
+
);
|
|
1933
|
+
const filesWidth = "Files".length;
|
|
1934
|
+
const versionWidth = Math.max(
|
|
1935
|
+
"Version".length,
|
|
1936
|
+
...manifest.components.map((c) => c.installedVersion.length)
|
|
1937
|
+
);
|
|
1938
|
+
const pad = (s, n) => s.padEnd(n);
|
|
1939
|
+
const rpad = (s, n) => s.padStart(n);
|
|
1940
|
+
console.log(
|
|
1941
|
+
` ${pad("Component", nameWidth)} ${rpad("Files", filesWidth)} ${pad("Version", versionWidth)}`
|
|
1942
|
+
);
|
|
1943
|
+
console.log(
|
|
1944
|
+
` ${"-".repeat(nameWidth)} ${"-".repeat(filesWidth)} ${"-".repeat(versionWidth)}`
|
|
1945
|
+
);
|
|
1946
|
+
for (const component of manifest.components) {
|
|
1947
|
+
const label = component.name;
|
|
1948
|
+
const fileCount = component.files.length.toString();
|
|
1949
|
+
const displayVersion = component.installedVersion;
|
|
1950
|
+
console.log(
|
|
1951
|
+
` ${pad(label, nameWidth)} ${rpad(fileCount, filesWidth)} ${displayVersion}`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
console.log("");
|
|
1955
|
+
const latestStr = versionResult.error ? "unable to check" : versionResult.latest ?? manifest.version;
|
|
1956
|
+
let footer = ` Installed: ${packageVersion} Latest: ${latestStr}`;
|
|
1957
|
+
if (versionResult.updateAvailable) {
|
|
1958
|
+
footer += " (spell update available)";
|
|
1959
|
+
}
|
|
1960
|
+
console.log(footer);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// src/commands/uninstall.ts
|
|
1964
|
+
import { rm as rm2 } from "fs/promises";
|
|
1965
|
+
import { join as join8 } from "path";
|
|
1966
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1967
|
+
async function runUninstall(options, targetDir) {
|
|
1968
|
+
let manifest;
|
|
1969
|
+
try {
|
|
1970
|
+
manifest = await readManifest(targetDir);
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
if (err instanceof ManifestNotFoundError) {
|
|
1973
|
+
console.error('Not initialized. Run "spell init" first.');
|
|
1974
|
+
process.exit(1);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
throw err;
|
|
1978
|
+
}
|
|
1979
|
+
const allFiles = manifest.components.flatMap((c) => c.files);
|
|
1980
|
+
const totalFiles = allFiles.length;
|
|
1981
|
+
if (options.dryRun) {
|
|
1982
|
+
for (const file of allFiles) {
|
|
1983
|
+
console.log(`[dry-run] Would remove: ${file}`);
|
|
1984
|
+
}
|
|
1985
|
+
console.log(`[dry-run] Would remove: .arcane.json`);
|
|
1986
|
+
console.log(`[dry-run] Would remove: .github/agents/ (directory)`);
|
|
1987
|
+
console.log(`[dry-run] Would remove: .arcane/agents/ (directory)`);
|
|
1988
|
+
console.log(`[dry-run] Would remove: .arcane/generated/ (directory)`);
|
|
1989
|
+
console.log(`[dry-run] Would remove: .arcane/agents.yaml`);
|
|
1990
|
+
console.log(`[dry-run] Would strip arcane sections from: CLAUDE.md, AGENTS.md, copilot-instructions.md`);
|
|
1991
|
+
console.log(`
|
|
1992
|
+
[dry-run] ${totalFiles} file(s) + manifest + agent outputs would be removed.`);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
if (!options.yes) {
|
|
1996
|
+
const confirmed = await confirm2({
|
|
1997
|
+
message: `This will remove ${totalFiles} files and .arcane.json. Continue?`,
|
|
1998
|
+
default: false
|
|
1999
|
+
});
|
|
2000
|
+
if (!confirmed) {
|
|
2001
|
+
console.log("Uninstall cancelled.");
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
let removed = 0;
|
|
2006
|
+
for (const component of manifest.components) {
|
|
2007
|
+
for (const file of component.files) {
|
|
2008
|
+
const filePath = join8(targetDir, file);
|
|
2009
|
+
try {
|
|
2010
|
+
await rm2(filePath, { force: true });
|
|
2011
|
+
removed++;
|
|
2012
|
+
} catch {
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
await rm2(join8(targetDir, ".arcane.json"), { force: true });
|
|
2017
|
+
const agentDirs = [
|
|
2018
|
+
".github/agents",
|
|
2019
|
+
".arcane/agents",
|
|
2020
|
+
".arcane/generated"
|
|
2021
|
+
];
|
|
2022
|
+
for (const dir of agentDirs) {
|
|
2023
|
+
await rm2(join8(targetDir, dir), { recursive: true, force: true });
|
|
2024
|
+
}
|
|
2025
|
+
await rm2(join8(targetDir, ".arcane/agents.yaml"), { force: true });
|
|
2026
|
+
const markerFiles = [
|
|
2027
|
+
"CLAUDE.md",
|
|
2028
|
+
"AGENTS.md",
|
|
2029
|
+
".github/copilot-instructions.md"
|
|
2030
|
+
];
|
|
2031
|
+
for (const file of markerFiles) {
|
|
2032
|
+
await stripMarkerSection(targetDir, file, { dryRun: false });
|
|
2033
|
+
}
|
|
2034
|
+
console.log(`
|
|
2035
|
+
\u2713 Uninstalled \u2014 ${removed} files removed.`);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// src/commands/doctor.ts
|
|
2039
|
+
import { access as access3, readFile as readFile5, mkdir as mkdir5, copyFile as fsCopyFile2 } from "fs/promises";
|
|
2040
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
2041
|
+
import { execFile as execFile2 } from "child_process";
|
|
2042
|
+
import { promisify as promisify2 } from "util";
|
|
2043
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
2044
|
+
async function checkNodeVersion() {
|
|
2045
|
+
const name = "Node.js version";
|
|
2046
|
+
const raw = process.version;
|
|
2047
|
+
const major = parseInt(raw.replace(/^v/, "").split(".")[0] ?? "0", 10);
|
|
2048
|
+
if (major >= 18) {
|
|
2049
|
+
return { name, passed: true, message: `${raw} (>=18 required)` };
|
|
2050
|
+
}
|
|
2051
|
+
return {
|
|
2052
|
+
name,
|
|
2053
|
+
passed: false,
|
|
2054
|
+
message: `${raw} \u2014 Node.js 18 or later is required`
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
async function checkVSCodeExtension(id, label) {
|
|
2058
|
+
const name = `VS Code extension: ${label}`;
|
|
2059
|
+
const home = process.env["HOME"] ?? "";
|
|
2060
|
+
const extDirs = [
|
|
2061
|
+
join9(home, ".vscode", "extensions"),
|
|
2062
|
+
join9(home, ".vscode-insiders", "extensions")
|
|
2063
|
+
];
|
|
2064
|
+
for (const dir of extDirs) {
|
|
2065
|
+
try {
|
|
2066
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
2067
|
+
const entries = await readdir3(dir);
|
|
2068
|
+
const prefix = id.toLowerCase() + "-";
|
|
2069
|
+
const found = entries.some(
|
|
2070
|
+
(e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
|
|
2071
|
+
);
|
|
2072
|
+
if (found) {
|
|
2073
|
+
return { name, passed: true, message: `${id} is installed` };
|
|
2074
|
+
}
|
|
2075
|
+
} catch {
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
let installBin = "code";
|
|
2079
|
+
for (const bin of ["code", "code-insiders"]) {
|
|
2080
|
+
try {
|
|
2081
|
+
await execFileAsync2(bin, ["--version"]);
|
|
2082
|
+
installBin = bin;
|
|
2083
|
+
break;
|
|
2084
|
+
} catch {
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
name,
|
|
2089
|
+
passed: false,
|
|
2090
|
+
message: `${id} not found. Install via: ${installBin} --install-extension ${id}`,
|
|
2091
|
+
blocking: false
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
async function checkArcaneManifest(targetDir) {
|
|
2095
|
+
const name = "Arcane manifest (.arcane.json)";
|
|
2096
|
+
const manifestPath2 = join9(targetDir, ".arcane.json");
|
|
2097
|
+
try {
|
|
2098
|
+
await access3(manifestPath2);
|
|
2099
|
+
const raw = await readFile5(manifestPath2, "utf8");
|
|
2100
|
+
const manifest = JSON.parse(raw);
|
|
2101
|
+
if (!manifest.version || !Array.isArray(manifest.components)) {
|
|
2102
|
+
return {
|
|
2103
|
+
name,
|
|
2104
|
+
passed: false,
|
|
2105
|
+
message: ".arcane.json is present but missing required fields (version, components). Re-run `spell init`."
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
return {
|
|
2109
|
+
name,
|
|
2110
|
+
passed: true,
|
|
2111
|
+
message: `v${manifest.version} \u2014 ${manifest.components.length} component(s) installed`
|
|
2112
|
+
};
|
|
2113
|
+
} catch {
|
|
2114
|
+
return {
|
|
2115
|
+
name,
|
|
2116
|
+
passed: false,
|
|
2117
|
+
message: "No .arcane.json found. Run `spell init` to initialize Arcane in this project.",
|
|
2118
|
+
blocking: false
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
var SESSION_CONTINUITY_FILES = [
|
|
2123
|
+
"TODO.md",
|
|
2124
|
+
"DECISIONS.md",
|
|
2125
|
+
"ai-context/system-prompt-context.md",
|
|
2126
|
+
"journal/.gitkeep"
|
|
2127
|
+
];
|
|
2128
|
+
async function checkSessionContinuity(targetDir) {
|
|
2129
|
+
const results = [];
|
|
2130
|
+
for (const file of SESSION_CONTINUITY_FILES) {
|
|
2131
|
+
const name = `Session file: ${file}`;
|
|
2132
|
+
const filePath = join9(targetDir, file);
|
|
2133
|
+
try {
|
|
2134
|
+
await access3(filePath);
|
|
2135
|
+
results.push({ name, passed: true, message: "present" });
|
|
2136
|
+
} catch {
|
|
2137
|
+
results.push({
|
|
2138
|
+
name,
|
|
2139
|
+
passed: false,
|
|
2140
|
+
message: `Missing. Required for spell-close-session. Run \`spell doctor --fix\` to create it.`,
|
|
2141
|
+
blocking: false
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return results;
|
|
2146
|
+
}
|
|
2147
|
+
async function fixSessionContinuity(targetDir, assetsDir) {
|
|
2148
|
+
const created = [];
|
|
2149
|
+
for (const file of SESSION_CONTINUITY_FILES) {
|
|
2150
|
+
const destPath = join9(targetDir, file);
|
|
2151
|
+
try {
|
|
2152
|
+
await access3(destPath);
|
|
2153
|
+
} catch {
|
|
2154
|
+
const srcPath = join9(assetsDir, file);
|
|
2155
|
+
await mkdir5(dirname2(destPath), { recursive: true });
|
|
2156
|
+
await fsCopyFile2(srcPath, destPath);
|
|
2157
|
+
created.push(file);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return created;
|
|
2161
|
+
}
|
|
2162
|
+
async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
2163
|
+
console.log("\nspell doctor \u2014 checking your Arcane environment\n");
|
|
2164
|
+
const results = await Promise.all([
|
|
2165
|
+
checkNodeVersion(),
|
|
2166
|
+
checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)"),
|
|
2167
|
+
checkArcaneManifest(targetDir)
|
|
2168
|
+
]);
|
|
2169
|
+
const sessionResults = await checkSessionContinuity(targetDir);
|
|
2170
|
+
results.push(...sessionResults);
|
|
2171
|
+
let allPassed = true;
|
|
2172
|
+
for (const result of results) {
|
|
2173
|
+
const icon = result.passed ? "\u2713" : result.blocking === false ? "\u26A0" : "\u2717";
|
|
2174
|
+
const label = result.passed ? "pass" : result.blocking === false ? "warn" : "FAIL";
|
|
2175
|
+
console.log(` ${icon} [${label}] ${result.name}`);
|
|
2176
|
+
if (!result.passed) {
|
|
2177
|
+
console.log(` ${result.message}`);
|
|
2178
|
+
}
|
|
2179
|
+
if (!result.passed && result.blocking !== false) {
|
|
2180
|
+
allPassed = false;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
console.log();
|
|
2184
|
+
if (allPassed) {
|
|
2185
|
+
console.log(" All checks passed. Your environment is ready.\n");
|
|
2186
|
+
} else {
|
|
2187
|
+
console.log(" One or more checks failed. Fix the issues above before proceeding.\n");
|
|
2188
|
+
const hasMissingSession = sessionResults.some((r) => !r.passed);
|
|
2189
|
+
if (options.fix && hasMissingSession && assetsDir) {
|
|
2190
|
+
console.log(" \u{1F527} Fixing missing session continuity files...\n");
|
|
2191
|
+
const created = await fixSessionContinuity(targetDir, assetsDir);
|
|
2192
|
+
if (created.length > 0) {
|
|
2193
|
+
for (const file of created) {
|
|
2194
|
+
console.log(` \u2713 Created: ${file}`);
|
|
2195
|
+
}
|
|
2196
|
+
console.log(`
|
|
2197
|
+
${created.length} file(s) created. Repo is now close-session-ready.
|
|
2198
|
+
`);
|
|
2199
|
+
} else {
|
|
2200
|
+
console.log(" No files needed creation (all present).\n");
|
|
2201
|
+
}
|
|
2202
|
+
} else if (hasMissingSession && !options.fix) {
|
|
2203
|
+
console.log(" \u{1F4A1} Run `spell doctor --fix` to create missing session files.\n");
|
|
2204
|
+
}
|
|
2205
|
+
if (!allPassed) {
|
|
2206
|
+
process.exitCode = 1;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// src/index.ts
|
|
2212
|
+
import { realpathSync } from "fs";
|
|
2213
|
+
var require3 = createRequire2(import.meta.url);
|
|
2214
|
+
var pkg = require3("../package.json");
|
|
2215
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join10(dirname3(fileURLToPath(import.meta.url)), "assets");
|
|
2216
|
+
var program = new Command();
|
|
2217
|
+
program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(pkg.version).action(async () => {
|
|
2218
|
+
await printWelcome(pkg.version, process.cwd());
|
|
2219
|
+
program.outputHelp();
|
|
2220
|
+
});
|
|
2221
|
+
program.command("init").description("Scaffold Arcane framework files into the current repository").option("--profile <profile>", "Profile: full | lite | methodology | governance-only").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be installed without making changes").action(
|
|
2222
|
+
async (opts) => {
|
|
2223
|
+
await runInit(
|
|
2224
|
+
{
|
|
2225
|
+
profile: opts.profile,
|
|
2226
|
+
force: opts.force,
|
|
2227
|
+
dryRun: opts.dryRun
|
|
2228
|
+
},
|
|
2229
|
+
process.cwd(),
|
|
2230
|
+
ASSETS_DIR,
|
|
2231
|
+
pkg.version
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
);
|
|
2235
|
+
program.command("add <component>").description("Add a component to an existing Arcane installation").option("--force", "Overwrite existing files without error").option("--dry-run", "Preview what would be added without making changes").action(
|
|
2236
|
+
async (component, opts) => {
|
|
2237
|
+
await runAdd(
|
|
2238
|
+
component,
|
|
2239
|
+
{ force: opts.force, dryRun: opts.dryRun },
|
|
2240
|
+
process.cwd(),
|
|
2241
|
+
ASSETS_DIR,
|
|
2242
|
+
pkg.version
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
);
|
|
2246
|
+
program.command("update").description("Update installed components to the current package version").option("--dry-run", "Preview what would be updated without making changes").action(async (opts) => {
|
|
2247
|
+
await runUpdate(
|
|
2248
|
+
{ dryRun: opts.dryRun },
|
|
2249
|
+
process.cwd(),
|
|
2250
|
+
ASSETS_DIR,
|
|
2251
|
+
pkg.version
|
|
2252
|
+
);
|
|
2253
|
+
});
|
|
2254
|
+
program.command("status").description("Show installed components and check for updates").action(async () => {
|
|
2255
|
+
await runStatus(process.cwd(), pkg.version);
|
|
2256
|
+
});
|
|
2257
|
+
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) => {
|
|
2258
|
+
await runUninstall({ yes: opts.yes, dryRun: opts.dryRun }, process.cwd());
|
|
2259
|
+
});
|
|
2260
|
+
program.command("doctor").description("Check your environment for Arcane prerequisites").option("--fix", "Automatically create missing session continuity files").action(async (opts) => {
|
|
2261
|
+
await runDoctor(process.cwd(), { fix: opts.fix }, ASSETS_DIR);
|
|
2262
|
+
});
|
|
2263
|
+
var agentsCommand = program.command("agents").description("Manage the portable agent system (init, sync, list)");
|
|
2264
|
+
agentsCommand.command("init").description(
|
|
2265
|
+
"Interactive agent roster setup \u2014 choose profile, naming strategy, and install definitions"
|
|
2266
|
+
).option(
|
|
2267
|
+
"--profile <profile>",
|
|
2268
|
+
"Agent profile: base | full | custom"
|
|
2269
|
+
).option(
|
|
2270
|
+
"--naming <strategy>",
|
|
2271
|
+
"Naming strategy: arcanos | generic | random | custom"
|
|
2272
|
+
).option("--force", "Reinitialize even if roster already exists").option("--dry-run", "Preview what would be created without writing files").action(
|
|
2273
|
+
async (opts) => {
|
|
2274
|
+
await runAgentsInit(
|
|
2275
|
+
{
|
|
2276
|
+
profile: opts.profile,
|
|
2277
|
+
naming: opts.naming,
|
|
2278
|
+
force: opts.force,
|
|
2279
|
+
dryRun: opts.dryRun
|
|
2280
|
+
},
|
|
2281
|
+
process.cwd(),
|
|
2282
|
+
ASSETS_DIR
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
);
|
|
2286
|
+
agentsCommand.command("sync").description(
|
|
2287
|
+
"Regenerate all client instruction files from canonical agent definitions"
|
|
2288
|
+
).option("--dry-run", "Preview what would be generated without writing files").option("--no-openclaw", "Skip OpenClaw workspace output").option("--no-copilot", "Skip Copilot output").option("--no-claude", "Skip Claude output").option("--no-codex", "Skip Codex output").action(
|
|
2289
|
+
async (opts) => {
|
|
2290
|
+
await runAgentsSync(
|
|
2291
|
+
{
|
|
2292
|
+
dryRun: opts.dryRun,
|
|
2293
|
+
openclaw: opts.openclaw,
|
|
2294
|
+
copilot: opts.copilot,
|
|
2295
|
+
claude: opts.claude,
|
|
2296
|
+
codex: opts.codex
|
|
2297
|
+
},
|
|
2298
|
+
process.cwd(),
|
|
2299
|
+
ASSETS_DIR
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
);
|
|
2303
|
+
agentsCommand.command("list").description("Show the current agent roster with assigned names and roles").action(async () => {
|
|
2304
|
+
await runAgentsList(process.cwd());
|
|
2305
|
+
});
|
|
2306
|
+
if (realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
2307
|
+
await program.parseAsync(process.argv);
|
|
2308
|
+
}
|
|
2309
|
+
export {
|
|
2310
|
+
ASSETS_DIR,
|
|
2311
|
+
program
|
|
2312
|
+
};
|
|
2313
|
+
//# sourceMappingURL=index.js.map
|