claude-code-arcane 2.3.0 → 2.5.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/CHANGELOG.md +23 -0
- package/README.md +131 -41
- package/agents/game/ue-blueprint-specialist.md +125 -0
- package/agents/game/ue-gas-specialist.md +133 -0
- package/agents/game/ue-umg-specialist.md +131 -0
- package/dist/cli.js +642 -478
- package/docs/SKILLS-CATALOG.md +42 -3
- package/docs/USER-GUIDE.md +28 -54
- package/hooks/detect-unreal.sh +90 -0
- package/package.json +2 -1
- package/profiles/agile.yaml +1 -1
- package/profiles/ai.yaml +1 -1
- package/profiles/android-native.yaml +1 -1
- package/profiles/audio.yaml +1 -1
- package/profiles/backend-dotnet.yaml +1 -1
- package/profiles/backend-go.yaml +1 -1
- package/profiles/backend-nestjs.yaml +1 -1
- package/profiles/backend-nextjs.yaml +1 -1
- package/profiles/backend-ts.yaml +1 -1
- package/profiles/business.yaml +1 -1
- package/profiles/clevel.yaml +1 -1
- package/profiles/clickup.yaml +1 -1
- package/profiles/core.yaml +1 -1
- package/profiles/database.yaml +1 -1
- package/profiles/design.yaml +1 -1
- package/profiles/docs.yaml +1 -1
- package/profiles/ecommerce.yaml +1 -1
- package/profiles/finance.yaml +1 -1
- package/profiles/flutter.yaml +1 -1
- package/profiles/freelance.yaml +1 -1
- package/profiles/frontend.yaml +1 -1
- package/profiles/infra.yaml +1 -1
- package/profiles/integrations.yaml +1 -1
- package/profiles/ios-native.yaml +1 -1
- package/profiles/jira.yaml +1 -1
- package/profiles/job-hunt.yaml +1 -1
- package/profiles/marketing.yaml +1 -1
- package/profiles/mobile.yaml +1 -1
- package/profiles/regulatory.yaml +1 -1
- package/profiles/second-brain.yaml +1 -1
- package/profiles/security.yaml +1 -1
- package/profiles/self-improving.yaml +1 -1
- package/profiles/statusline.yaml +1 -1
- package/profiles/testing.yaml +1 -1
- package/profiles/unity-design.yaml +1 -1
- package/profiles/unity-dev.yaml +1 -1
- package/profiles/unreal-dev.yaml +97 -0
- package/profiles/visual-novel.yaml +1 -1
- package/rules/gamedev/unreal-code.md +125 -0
- package/skills/arcane-list/SKILL.md +12 -7
- package/skills/install-mcp/SKILL.md +135 -46
- package/skills/install-mcp/references/unreal-operations.md +55 -0
- package/skills/ue-actor-component-architecture/SKILL.md +509 -0
- package/skills/ue-actor-component-architecture/references/actor-lifecycle.md +301 -0
- package/skills/ue-actor-component-architecture/references/component-types.md +592 -0
- package/skills/ue-ai-navigation/SKILL.md +526 -0
- package/skills/ue-ai-navigation/references/behavior-tree-patterns.md +392 -0
- package/skills/ue-ai-navigation/references/behavior-tree-workflow.md +107 -0
- package/skills/ue-ai-navigation/references/custom-bttask.md +104 -0
- package/skills/ue-ai-navigation/references/eqs-reference.md +423 -0
- package/skills/ue-animation-system/SKILL.md +521 -0
- package/skills/ue-animation-system/references/anim-notify-reference.md +486 -0
- package/skills/ue-animation-system/references/locomotion-setup.md +550 -0
- package/skills/ue-async-threading/SKILL.md +433 -0
- package/skills/ue-async-threading/references/thread-safety-guide.md +213 -0
- package/skills/ue-async-threading/references/threading-patterns.md +345 -0
- package/skills/ue-audio-system/SKILL.md +527 -0
- package/skills/ue-audio-system/references/audio-setup-patterns.md +700 -0
- package/skills/ue-blueprints/SKILL.md +134 -0
- package/skills/ue-blueprints/references/communication.md +82 -0
- package/skills/ue-character-movement/SKILL.md +475 -0
- package/skills/ue-character-movement/references/cmc-extension-patterns.md +335 -0
- package/skills/ue-character-movement/references/movement-pipeline.md +189 -0
- package/skills/ue-cpp-foundations/SKILL.md +533 -0
- package/skills/ue-cpp-foundations/references/components-and-gc.md +107 -0
- package/skills/ue-cpp-foundations/references/container-patterns.md +544 -0
- package/skills/ue-cpp-foundations/references/delegate-patterns.md +497 -0
- package/skills/ue-cpp-foundations/references/gameplay-classes-cpp.md +136 -0
- package/skills/ue-cpp-foundations/references/property-specifiers.md +306 -0
- package/skills/ue-data-assets-tables/SKILL.md +527 -0
- package/skills/ue-data-assets-tables/references/asset-loading-patterns.md +306 -0
- package/skills/ue-data-assets-tables/references/data-driven-design.md +378 -0
- package/skills/ue-editor-tools/SKILL.md +523 -0
- package/skills/ue-editor-tools/references/detail-customization-patterns.md +395 -0
- package/skills/ue-editor-tools/references/editor-module-setup.md +480 -0
- package/skills/ue-game-features/SKILL.md +477 -0
- package/skills/ue-game-features/references/experience-system.md +222 -0
- package/skills/ue-game-features/references/game-feature-patterns.md +308 -0
- package/skills/ue-gameplay-abilities/SKILL.md +518 -0
- package/skills/ue-gameplay-abilities/references/ability-task-reference.md +391 -0
- package/skills/ue-gameplay-abilities/references/gameplay-effect-reference.md +397 -0
- package/skills/ue-gameplay-abilities/references/gas-setup-patterns.md +369 -0
- package/skills/ue-gameplay-framework/SKILL.md +466 -0
- package/skills/ue-gameplay-framework/references/framework-class-map.md +243 -0
- package/skills/ue-input-system/SKILL.md +394 -0
- package/skills/ue-input-system/references/cpp-setup.md +131 -0
- package/skills/ue-input-system/references/enhanced-input-workflow.md +117 -0
- package/skills/ue-input-system/references/input-action-reference.md +731 -0
- package/skills/ue-mass-entity/SKILL.md +476 -0
- package/skills/ue-mass-entity/references/mass-entity-patterns.md +319 -0
- package/skills/ue-mass-entity/references/mass-fragment-reference.md +119 -0
- package/skills/ue-materials-rendering/SKILL.md +526 -0
- package/skills/ue-materials-rendering/references/material-parameter-reference.md +216 -0
- package/skills/ue-materials-rendering/references/post-process-settings.md +383 -0
- package/skills/ue-module-build-system/SKILL.md +487 -0
- package/skills/ue-module-build-system/references/build-cs-reference.md +418 -0
- package/skills/ue-module-build-system/references/common-build-errors.md +381 -0
- package/skills/ue-naming-conventions/SKILL.md +142 -0
- package/skills/ue-naming-conventions/references/asset-name-modifiers.md +233 -0
- package/skills/ue-naming-conventions/references/content-structure.md +83 -0
- package/skills/ue-networking-replication/SKILL.md +452 -0
- package/skills/ue-networking-replication/references/replication-patterns.md +526 -0
- package/skills/ue-networking-replication/references/rpc-decision-guide.md +393 -0
- package/skills/ue-niagara-effects/SKILL.md +531 -0
- package/skills/ue-niagara-effects/references/niagara-data-interfaces.md +491 -0
- package/skills/ue-niagara-effects/references/niagara-parameter-types.md +153 -0
- package/skills/ue-niagara-effects/references/niagara-workflow.md +115 -0
- package/skills/ue-packaging/SKILL.md +116 -0
- package/skills/ue-physics-collision/SKILL.md +423 -0
- package/skills/ue-physics-collision/references/collision-channel-setup.md +218 -0
- package/skills/ue-physics-collision/references/trace-patterns.md +499 -0
- package/skills/ue-procedural-generation/SKILL.md +505 -0
- package/skills/ue-procedural-generation/references/pcg-node-reference.md +381 -0
- package/skills/ue-procedural-generation/references/procedural-mesh-patterns.md +940 -0
- package/skills/ue-project-context/SKILL.md +305 -0
- package/skills/ue-sequencer-cinematics/SKILL.md +506 -0
- package/skills/ue-sequencer-cinematics/references/sequencer-patterns.md +629 -0
- package/skills/ue-serialization-savegames/SKILL.md +524 -0
- package/skills/ue-serialization-savegames/references/save-system-architecture.md +705 -0
- package/skills/ue-state-trees/SKILL.md +489 -0
- package/skills/ue-state-trees/references/state-tree-mass-integration.md +184 -0
- package/skills/ue-state-trees/references/state-tree-patterns.md +321 -0
- package/skills/ue-testing-debugging/SKILL.md +520 -0
- package/skills/ue-testing-debugging/references/automation-test-patterns.md +387 -0
- package/skills/ue-testing-debugging/references/profiling-commands.md +357 -0
- package/skills/ue-ui-umg-slate/SKILL.md +431 -0
- package/skills/ue-ui-umg-slate/references/common-ui-setup.md +484 -0
- package/skills/ue-ui-umg-slate/references/widget-types.md +487 -0
- package/skills/ue-world-level-streaming/SKILL.md +518 -0
- package/skills/ue-world-level-streaming/references/streaming-patterns.md +414 -0
- package/templates/gamedev/unreal-project-context.md +107 -0
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/install.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import path11 from "path";
|
|
8
|
+
import chalk2 from "chalk";
|
|
9
9
|
|
|
10
10
|
// src/profiles.ts
|
|
11
11
|
import fs from "fs";
|
|
@@ -18,7 +18,7 @@ function parseProfile(filePath) {
|
|
|
18
18
|
return {
|
|
19
19
|
name: String(raw.name ?? path.basename(filePath, ".yaml")),
|
|
20
20
|
description: String(raw.description ?? ""),
|
|
21
|
-
|
|
21
|
+
category: String(raw.category ?? "other"),
|
|
22
22
|
skills: raw.skills ?? [],
|
|
23
23
|
rules: {
|
|
24
24
|
universal: rules.universal ?? [],
|
|
@@ -32,6 +32,29 @@ function parseProfile(filePath) {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
var CATEGORY_ORDER = [
|
|
36
|
+
{ id: "backend", label: "Backend" },
|
|
37
|
+
{ id: "frontend", label: "Frontend & Design" },
|
|
38
|
+
{ id: "mobile", label: "Mobile" },
|
|
39
|
+
{ id: "gamedev", label: "Gamedev" },
|
|
40
|
+
{ id: "platform", label: "Platform & Quality" },
|
|
41
|
+
{ id: "management", label: "Project Management" },
|
|
42
|
+
{ id: "business", label: "Business" },
|
|
43
|
+
{ id: "personal", label: "Personal" },
|
|
44
|
+
{ id: "utilities", label: "Utilities" }
|
|
45
|
+
];
|
|
46
|
+
function groupByCategory(profiles) {
|
|
47
|
+
const known = new Set(CATEGORY_ORDER.map((c) => c.id));
|
|
48
|
+
const groups = CATEGORY_ORDER.map((c) => ({
|
|
49
|
+
...c,
|
|
50
|
+
profiles: profiles.filter((p2) => p2.category === c.id)
|
|
51
|
+
}));
|
|
52
|
+
const rest = profiles.filter((p2) => !known.has(p2.category));
|
|
53
|
+
if (rest.length > 0) {
|
|
54
|
+
groups.push({ id: "other", label: "Other", profiles: rest });
|
|
55
|
+
}
|
|
56
|
+
return groups.filter((g) => g.profiles.length > 0);
|
|
57
|
+
}
|
|
35
58
|
function listProfiles(profilesDir) {
|
|
36
59
|
if (!fs.existsSync(profilesDir)) return [];
|
|
37
60
|
return fs.readdirSync(profilesDir).filter((f) => f.endsWith(".yaml") && f !== "core.yaml").map((f) => parseProfile(path.join(profilesDir, f))).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -105,77 +128,215 @@ function mergeProfiles(profilesDir, profileNames) {
|
|
|
105
128
|
};
|
|
106
129
|
}
|
|
107
130
|
|
|
131
|
+
// src/detect.ts
|
|
132
|
+
import fs2 from "fs";
|
|
133
|
+
import path2 from "path";
|
|
134
|
+
function hasFile(dir, rel) {
|
|
135
|
+
return fs2.existsSync(path2.join(dir, rel));
|
|
136
|
+
}
|
|
137
|
+
function topLevelMatch(dir, suffix) {
|
|
138
|
+
try {
|
|
139
|
+
return fs2.readdirSync(dir).find((f) => f.endsWith(suffix));
|
|
140
|
+
} catch {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function readPackageDeps(dir) {
|
|
145
|
+
try {
|
|
146
|
+
const raw = JSON.parse(
|
|
147
|
+
fs2.readFileSync(path2.join(dir, "package.json"), "utf-8")
|
|
148
|
+
);
|
|
149
|
+
return {
|
|
150
|
+
...raw.dependencies ?? {},
|
|
151
|
+
...raw.devDependencies ?? {}
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function detectStack(dir) {
|
|
158
|
+
const out = [];
|
|
159
|
+
const isUnity = hasFile(dir, "ProjectSettings/ProjectVersion.txt");
|
|
160
|
+
if (isUnity) {
|
|
161
|
+
out.push({ profile: "unity-dev", reason: "ProjectSettings/ProjectVersion.txt" });
|
|
162
|
+
}
|
|
163
|
+
const uproject = topLevelMatch(dir, ".uproject");
|
|
164
|
+
if (uproject) {
|
|
165
|
+
out.push({ profile: "unreal-dev", reason: uproject });
|
|
166
|
+
}
|
|
167
|
+
const isFlutter = hasFile(dir, "pubspec.yaml");
|
|
168
|
+
if (isFlutter) {
|
|
169
|
+
out.push({ profile: "flutter", reason: "pubspec.yaml" });
|
|
170
|
+
}
|
|
171
|
+
if (hasFile(dir, "go.mod")) {
|
|
172
|
+
out.push({ profile: "backend-go", reason: "go.mod" });
|
|
173
|
+
}
|
|
174
|
+
let isReactNative = false;
|
|
175
|
+
if (hasFile(dir, "package.json")) {
|
|
176
|
+
const deps = readPackageDeps(dir);
|
|
177
|
+
if (deps["@nestjs/core"]) {
|
|
178
|
+
out.push({ profile: "backend-nestjs", reason: "package.json: @nestjs/core" });
|
|
179
|
+
} else if (deps["next"]) {
|
|
180
|
+
out.push({ profile: "backend-nextjs", reason: "package.json: next" });
|
|
181
|
+
} else if (deps["react-native"] || deps["expo"]) {
|
|
182
|
+
isReactNative = true;
|
|
183
|
+
out.push({ profile: "mobile", reason: "package.json: react-native/expo" });
|
|
184
|
+
} else if (deps["react"] || deps["vue"] || deps["svelte"]) {
|
|
185
|
+
out.push({ profile: "frontend", reason: "package.json: react/vue/svelte" });
|
|
186
|
+
} else if (deps["fastify"] || deps["express"] || deps["koa"] || deps["hono"]) {
|
|
187
|
+
out.push({ profile: "backend-ts", reason: "package.json: server framework" });
|
|
188
|
+
} else if (deps["typescript"]) {
|
|
189
|
+
out.push({ profile: "backend-ts", reason: "package.json: typescript" });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!isUnity) {
|
|
193
|
+
const dotnet = topLevelMatch(dir, ".sln") ?? topLevelMatch(dir, ".csproj");
|
|
194
|
+
if (dotnet) {
|
|
195
|
+
out.push({ profile: "backend-dotnet", reason: dotnet });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!isFlutter && !isReactNative) {
|
|
199
|
+
if ((hasFile(dir, "settings.gradle") || hasFile(dir, "settings.gradle.kts")) && hasFile(dir, "app")) {
|
|
200
|
+
out.push({ profile: "android-native", reason: "settings.gradle + app/" });
|
|
201
|
+
}
|
|
202
|
+
const ios = topLevelMatch(dir, ".xcodeproj") ?? topLevelMatch(dir, ".xcworkspace") ?? (hasFile(dir, "Podfile") ? "Podfile" : void 0);
|
|
203
|
+
if (ios) {
|
|
204
|
+
out.push({ profile: "ios-native", reason: ios });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/wizard.ts
|
|
211
|
+
import * as p from "@clack/prompts";
|
|
212
|
+
import chalk from "chalk";
|
|
213
|
+
async function runInstallWizard(profiles, detections) {
|
|
214
|
+
p.intro(chalk.cyan("claude-code-arcane install"));
|
|
215
|
+
const available = new Set(profiles.map((pr) => pr.name));
|
|
216
|
+
const suggested = detections.filter((d) => available.has(d.profile));
|
|
217
|
+
if (suggested.length > 0) {
|
|
218
|
+
p.note(
|
|
219
|
+
suggested.map((d) => `${chalk.green(d.profile)} ${chalk.dim(`(${d.reason})`)}`).join("\n"),
|
|
220
|
+
"Detected stack \u2014 pre-selected below"
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
p.note(
|
|
224
|
+
"No stack detected in this directory.\nPick whatever fits your work \u2014 profiles combine freely.",
|
|
225
|
+
"No suggestions"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const options = {};
|
|
229
|
+
for (const group of groupByCategory(profiles)) {
|
|
230
|
+
options[group.label] = group.profiles.map((pr) => ({
|
|
231
|
+
value: pr.name,
|
|
232
|
+
label: pr.name,
|
|
233
|
+
hint: pr.description
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
const selected = await p.groupMultiselect({
|
|
237
|
+
message: "Select profiles (space to toggle, enter to confirm)",
|
|
238
|
+
options,
|
|
239
|
+
initialValues: suggested.map((d) => d.profile),
|
|
240
|
+
required: false
|
|
241
|
+
});
|
|
242
|
+
if (p.isCancel(selected)) {
|
|
243
|
+
p.cancel("Install cancelled.");
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const names = selected;
|
|
247
|
+
if (names.length === 0) {
|
|
248
|
+
p.outro(
|
|
249
|
+
chalk.dim(
|
|
250
|
+
"Nothing selected. Run again, or install directly: npx claude-code-arcane install backend-ts+testing"
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const expr = names.join("+");
|
|
256
|
+
const ok = await p.confirm({
|
|
257
|
+
message: `Install ${chalk.cyan(expr)} (plus core, always included)?`
|
|
258
|
+
});
|
|
259
|
+
if (p.isCancel(ok) || !ok) {
|
|
260
|
+
p.cancel("Install cancelled.");
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
p.outro(
|
|
264
|
+
chalk.dim(`Next time, skip the menu: npx claude-code-arcane install ${expr}`)
|
|
265
|
+
);
|
|
266
|
+
return names;
|
|
267
|
+
}
|
|
268
|
+
|
|
108
269
|
// src/installer.ts
|
|
109
|
-
import
|
|
110
|
-
import
|
|
270
|
+
import fs6 from "fs";
|
|
271
|
+
import path7 from "path";
|
|
111
272
|
|
|
112
273
|
// src/utils.ts
|
|
113
274
|
import { fileURLToPath } from "url";
|
|
114
|
-
import
|
|
115
|
-
import
|
|
275
|
+
import path3 from "path";
|
|
276
|
+
import fs3 from "fs";
|
|
116
277
|
function getPackageRoot() {
|
|
117
278
|
const thisFile = fileURLToPath(import.meta.url);
|
|
118
|
-
return
|
|
279
|
+
return path3.resolve(path3.dirname(thisFile), "..");
|
|
119
280
|
}
|
|
120
281
|
var COPY_SKIP = /* @__PURE__ */ new Set(["__pycache__", ".pytest_cache"]);
|
|
121
282
|
function isBuildArtifact(name) {
|
|
122
283
|
return COPY_SKIP.has(name) || name.endsWith(".pyc");
|
|
123
284
|
}
|
|
124
285
|
function copyDirSync(src, dest) {
|
|
125
|
-
|
|
126
|
-
for (const entry of
|
|
286
|
+
fs3.mkdirSync(dest, { recursive: true });
|
|
287
|
+
for (const entry of fs3.readdirSync(src, { withFileTypes: true })) {
|
|
127
288
|
if (isBuildArtifact(entry.name)) continue;
|
|
128
|
-
const srcPath =
|
|
129
|
-
const destPath =
|
|
289
|
+
const srcPath = path3.join(src, entry.name);
|
|
290
|
+
const destPath = path3.join(dest, entry.name);
|
|
130
291
|
if (entry.isDirectory()) {
|
|
131
292
|
copyDirSync(srcPath, destPath);
|
|
132
293
|
} else {
|
|
133
|
-
|
|
294
|
+
fs3.copyFileSync(srcPath, destPath);
|
|
134
295
|
}
|
|
135
296
|
}
|
|
136
297
|
}
|
|
137
298
|
function ensureDir(dir) {
|
|
138
|
-
|
|
299
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
139
300
|
}
|
|
140
|
-
function fileExists(
|
|
141
|
-
return
|
|
301
|
+
function fileExists(p2) {
|
|
302
|
+
return fs3.existsSync(p2);
|
|
142
303
|
}
|
|
143
|
-
function readJsonSync(
|
|
144
|
-
return JSON.parse(
|
|
304
|
+
function readJsonSync(p2) {
|
|
305
|
+
return JSON.parse(fs3.readFileSync(p2, "utf-8"));
|
|
145
306
|
}
|
|
146
|
-
function writeJsonSync(
|
|
147
|
-
|
|
307
|
+
function writeJsonSync(p2, data) {
|
|
308
|
+
fs3.writeFileSync(p2, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
148
309
|
}
|
|
149
|
-
function isSymlinkOrJunction(
|
|
310
|
+
function isSymlinkOrJunction(p2) {
|
|
150
311
|
try {
|
|
151
|
-
return
|
|
312
|
+
return fs3.lstatSync(p2).isSymbolicLink();
|
|
152
313
|
} catch {
|
|
153
314
|
return false;
|
|
154
315
|
}
|
|
155
316
|
}
|
|
156
317
|
function getPackageVersion() {
|
|
157
|
-
const pkgPath =
|
|
318
|
+
const pkgPath = path3.join(getPackageRoot(), "package.json");
|
|
158
319
|
const pkg = readJsonSync(pkgPath);
|
|
159
320
|
return pkg.version;
|
|
160
321
|
}
|
|
161
|
-
function safeRemove(
|
|
162
|
-
if (isSymlinkOrJunction(
|
|
163
|
-
|
|
164
|
-
} else if (
|
|
165
|
-
|
|
322
|
+
function safeRemove(p2) {
|
|
323
|
+
if (isSymlinkOrJunction(p2)) {
|
|
324
|
+
fs3.unlinkSync(p2);
|
|
325
|
+
} else if (fs3.existsSync(p2)) {
|
|
326
|
+
fs3.rmSync(p2, { recursive: true, force: true });
|
|
166
327
|
}
|
|
167
328
|
}
|
|
168
329
|
|
|
169
330
|
// src/manifest.ts
|
|
170
|
-
import
|
|
331
|
+
import path4 from "path";
|
|
171
332
|
var MANIFEST_FILE = "arcane-manifest.json";
|
|
172
333
|
function manifestPath(target) {
|
|
173
|
-
return
|
|
334
|
+
return path4.join(target, ".claude", MANIFEST_FILE);
|
|
174
335
|
}
|
|
175
336
|
function readManifest(target) {
|
|
176
|
-
const
|
|
177
|
-
if (!fileExists(
|
|
178
|
-
return readJsonSync(
|
|
337
|
+
const p2 = manifestPath(target);
|
|
338
|
+
if (!fileExists(p2)) return null;
|
|
339
|
+
return readJsonSync(p2);
|
|
179
340
|
}
|
|
180
341
|
function writeManifest(target, merged, profileCommand, packageRoot, worktreeOrOpts) {
|
|
181
342
|
let opts = {};
|
|
@@ -224,8 +385,8 @@ function updateManifestFields(target, updates) {
|
|
|
224
385
|
|
|
225
386
|
// src/worktree.ts
|
|
226
387
|
import { execSync } from "child_process";
|
|
227
|
-
import
|
|
228
|
-
import
|
|
388
|
+
import fs4 from "fs";
|
|
389
|
+
import path5 from "path";
|
|
229
390
|
function getWorktreeInfo(target) {
|
|
230
391
|
try {
|
|
231
392
|
execSync("git rev-parse --is-inside-work-tree", {
|
|
@@ -246,10 +407,10 @@ function getWorktreeInfo(target) {
|
|
|
246
407
|
encoding: "utf-8",
|
|
247
408
|
stdio: "pipe"
|
|
248
409
|
}).trim();
|
|
249
|
-
const resolvedCommon =
|
|
250
|
-
const resolvedGit =
|
|
410
|
+
const resolvedCommon = path5.resolve(target, gitCommonDir);
|
|
411
|
+
const resolvedGit = path5.resolve(target, gitDir);
|
|
251
412
|
const isWorktree = resolvedCommon !== resolvedGit;
|
|
252
|
-
const mainWorktreePath = isWorktree ?
|
|
413
|
+
const mainWorktreePath = isWorktree ? path5.resolve(resolvedCommon, "..") : target;
|
|
253
414
|
let currentBranch = "HEAD";
|
|
254
415
|
try {
|
|
255
416
|
currentBranch = execSync("git branch --show-current", {
|
|
@@ -264,7 +425,7 @@ function getWorktreeInfo(target) {
|
|
|
264
425
|
isWorktree,
|
|
265
426
|
mainWorktreePath,
|
|
266
427
|
currentBranch,
|
|
267
|
-
worktreeId:
|
|
428
|
+
worktreeId: path5.basename(target),
|
|
268
429
|
allWorktrees
|
|
269
430
|
};
|
|
270
431
|
}
|
|
@@ -300,17 +461,17 @@ function parseWorktreeList(cwd) {
|
|
|
300
461
|
}
|
|
301
462
|
}
|
|
302
463
|
function findMainArcaneInstall(mainPath) {
|
|
303
|
-
return
|
|
304
|
-
|
|
464
|
+
return fs4.existsSync(
|
|
465
|
+
path5.join(mainPath, ".claude", "arcane-manifest.json")
|
|
305
466
|
);
|
|
306
467
|
}
|
|
307
468
|
function linkOrCopyDir(src, dest, mode) {
|
|
308
469
|
if (mode === "symlink") {
|
|
309
470
|
try {
|
|
310
|
-
const parentDir =
|
|
311
|
-
|
|
471
|
+
const parentDir = path5.dirname(dest);
|
|
472
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
312
473
|
const type = process.platform === "win32" ? "junction" : "dir";
|
|
313
|
-
|
|
474
|
+
fs4.symlinkSync(src, dest, type);
|
|
314
475
|
return "linked";
|
|
315
476
|
} catch {
|
|
316
477
|
}
|
|
@@ -322,9 +483,9 @@ function sanitizeBranchForPath(branch) {
|
|
|
322
483
|
return branch.replace(/^(feat|fix|feature|hotfix)\//, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
323
484
|
}
|
|
324
485
|
function defaultWorktreePath(repoRoot, branch) {
|
|
325
|
-
const repoName =
|
|
486
|
+
const repoName = path5.basename(repoRoot);
|
|
326
487
|
const sanitized = sanitizeBranchForPath(branch);
|
|
327
|
-
return
|
|
488
|
+
return path5.resolve(repoRoot, "..", `${repoName}-${sanitized}`);
|
|
328
489
|
}
|
|
329
490
|
function createGitWorktree(repoRoot, worktreePath, branch, base) {
|
|
330
491
|
const branchExists = checkBranchExists(repoRoot, branch);
|
|
@@ -366,20 +527,20 @@ function detectPackageManager(dir) {
|
|
|
366
527
|
{ lockfile: "package-lock.json", command: "npm install" }
|
|
367
528
|
];
|
|
368
529
|
for (const m of managers) {
|
|
369
|
-
if (
|
|
530
|
+
if (fs4.existsSync(path5.join(dir, m.lockfile))) return m;
|
|
370
531
|
}
|
|
371
|
-
if (
|
|
532
|
+
if (fs4.existsSync(path5.join(dir, "package.json"))) {
|
|
372
533
|
return { lockfile: "package.json", command: "npm install" };
|
|
373
534
|
}
|
|
374
535
|
return null;
|
|
375
536
|
}
|
|
376
537
|
|
|
377
538
|
// src/content-hash.ts
|
|
378
|
-
import
|
|
379
|
-
import
|
|
539
|
+
import fs5 from "fs";
|
|
540
|
+
import path6 from "path";
|
|
380
541
|
import { createHash } from "crypto";
|
|
381
542
|
function hashFile(filePath) {
|
|
382
|
-
const content =
|
|
543
|
+
const content = fs5.readFileSync(filePath);
|
|
383
544
|
return createHash("sha256").update(content).digest("hex");
|
|
384
545
|
}
|
|
385
546
|
function hashDirectory(dirPath) {
|
|
@@ -388,10 +549,10 @@ function hashDirectory(dirPath) {
|
|
|
388
549
|
return hashes;
|
|
389
550
|
}
|
|
390
551
|
function walkDir(base, current, hashes) {
|
|
391
|
-
for (const entry of
|
|
552
|
+
for (const entry of fs5.readdirSync(current, { withFileTypes: true })) {
|
|
392
553
|
if (isBuildArtifact(entry.name)) continue;
|
|
393
|
-
const fullPath =
|
|
394
|
-
const relPath =
|
|
554
|
+
const fullPath = path6.join(current, entry.name);
|
|
555
|
+
const relPath = path6.relative(base, fullPath).split(path6.sep).join("/");
|
|
395
556
|
if (entry.isDirectory()) {
|
|
396
557
|
walkDir(base, fullPath, hashes);
|
|
397
558
|
} else {
|
|
@@ -406,39 +567,39 @@ function computeContentHashes(claudeDir) {
|
|
|
406
567
|
agents: {},
|
|
407
568
|
hooks: {}
|
|
408
569
|
};
|
|
409
|
-
const skillsDir =
|
|
410
|
-
if (
|
|
411
|
-
for (const entry of
|
|
570
|
+
const skillsDir = path6.join(claudeDir, "skills");
|
|
571
|
+
if (fs5.existsSync(skillsDir)) {
|
|
572
|
+
for (const entry of fs5.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
412
573
|
if (entry.isDirectory()) {
|
|
413
|
-
const dirHash = hashDirectory(
|
|
574
|
+
const dirHash = hashDirectory(path6.join(skillsDir, entry.name));
|
|
414
575
|
const combined = createHash("sha256").update(JSON.stringify(dirHash)).digest("hex");
|
|
415
576
|
hashes.skills[entry.name] = combined;
|
|
416
577
|
}
|
|
417
578
|
}
|
|
418
579
|
}
|
|
419
|
-
const rulesDir =
|
|
420
|
-
if (
|
|
421
|
-
for (const entry of
|
|
580
|
+
const rulesDir = path6.join(claudeDir, "rules");
|
|
581
|
+
if (fs5.existsSync(rulesDir)) {
|
|
582
|
+
for (const entry of fs5.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
422
583
|
if (!entry.isDirectory()) {
|
|
423
|
-
hashes.rules[entry.name] = hashFile(
|
|
584
|
+
hashes.rules[entry.name] = hashFile(path6.join(rulesDir, entry.name));
|
|
424
585
|
}
|
|
425
586
|
}
|
|
426
587
|
}
|
|
427
|
-
const agentsDir =
|
|
428
|
-
if (
|
|
429
|
-
for (const entry of
|
|
588
|
+
const agentsDir = path6.join(claudeDir, "agents");
|
|
589
|
+
if (fs5.existsSync(agentsDir)) {
|
|
590
|
+
for (const entry of fs5.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
430
591
|
if (entry.isDirectory()) {
|
|
431
|
-
const dirHash = hashDirectory(
|
|
592
|
+
const dirHash = hashDirectory(path6.join(agentsDir, entry.name));
|
|
432
593
|
const combined = createHash("sha256").update(JSON.stringify(dirHash)).digest("hex");
|
|
433
594
|
hashes.agents[entry.name] = combined;
|
|
434
595
|
}
|
|
435
596
|
}
|
|
436
597
|
}
|
|
437
|
-
const hooksDir =
|
|
438
|
-
if (
|
|
439
|
-
for (const entry of
|
|
598
|
+
const hooksDir = path6.join(claudeDir, "hooks");
|
|
599
|
+
if (fs5.existsSync(hooksDir)) {
|
|
600
|
+
for (const entry of fs5.readdirSync(hooksDir, { withFileTypes: true })) {
|
|
440
601
|
if (!entry.isDirectory()) {
|
|
441
|
-
hashes.hooks[entry.name] = hashFile(
|
|
602
|
+
hashes.hooks[entry.name] = hashFile(path6.join(hooksDir, entry.name));
|
|
442
603
|
}
|
|
443
604
|
}
|
|
444
605
|
}
|
|
@@ -452,8 +613,8 @@ function computeSourceHashes(packageRoot, skills, rules, agents) {
|
|
|
452
613
|
hooks: {}
|
|
453
614
|
};
|
|
454
615
|
for (const skill of skills) {
|
|
455
|
-
const skillDir =
|
|
456
|
-
if (
|
|
616
|
+
const skillDir = path6.join(packageRoot, "skills", skill);
|
|
617
|
+
if (fs5.existsSync(skillDir)) {
|
|
457
618
|
const dirHash = hashDirectory(skillDir);
|
|
458
619
|
const combined = createHash("sha256").update(JSON.stringify(dirHash)).digest("hex");
|
|
459
620
|
hashes.skills[skill] = combined;
|
|
@@ -461,29 +622,29 @@ function computeSourceHashes(packageRoot, skills, rules, agents) {
|
|
|
461
622
|
}
|
|
462
623
|
for (const rule of rules) {
|
|
463
624
|
const ruleName = rule.endsWith(".md") ? rule : `${rule}.md`;
|
|
464
|
-
const rulePath =
|
|
465
|
-
if (
|
|
625
|
+
const rulePath = path6.join(packageRoot, "rules", ruleName);
|
|
626
|
+
if (fs5.existsSync(rulePath)) {
|
|
466
627
|
hashes.rules[ruleName] = hashFile(rulePath);
|
|
467
628
|
} else {
|
|
468
|
-
const gamedevPath =
|
|
469
|
-
if (
|
|
629
|
+
const gamedevPath = path6.join(packageRoot, "rules", "gamedev", ruleName);
|
|
630
|
+
if (fs5.existsSync(gamedevPath)) {
|
|
470
631
|
hashes.rules[ruleName] = hashFile(gamedevPath);
|
|
471
632
|
}
|
|
472
633
|
}
|
|
473
634
|
}
|
|
474
635
|
for (const agent of agents) {
|
|
475
|
-
const agentDir =
|
|
476
|
-
if (
|
|
636
|
+
const agentDir = path6.join(packageRoot, "agents", agent);
|
|
637
|
+
if (fs5.existsSync(agentDir)) {
|
|
477
638
|
const dirHash = hashDirectory(agentDir);
|
|
478
639
|
const combined = createHash("sha256").update(JSON.stringify(dirHash)).digest("hex");
|
|
479
640
|
hashes.agents[agent] = combined;
|
|
480
641
|
}
|
|
481
642
|
}
|
|
482
|
-
const hooksDir =
|
|
483
|
-
if (
|
|
484
|
-
for (const entry of
|
|
643
|
+
const hooksDir = path6.join(packageRoot, "hooks");
|
|
644
|
+
if (fs5.existsSync(hooksDir)) {
|
|
645
|
+
for (const entry of fs5.readdirSync(hooksDir, { withFileTypes: true })) {
|
|
485
646
|
if (!entry.isDirectory()) {
|
|
486
|
-
hashes.hooks[entry.name] = hashFile(
|
|
647
|
+
hashes.hooks[entry.name] = hashFile(path6.join(hooksDir, entry.name));
|
|
487
648
|
}
|
|
488
649
|
}
|
|
489
650
|
}
|
|
@@ -509,6 +670,11 @@ var SETTINGS_TEMPLATE = {
|
|
|
509
670
|
command: "bash .claude/hooks/detect-division.sh",
|
|
510
671
|
timeout: 10
|
|
511
672
|
},
|
|
673
|
+
{
|
|
674
|
+
type: "command",
|
|
675
|
+
command: "bash .claude/hooks/detect-unreal.sh",
|
|
676
|
+
timeout: 10
|
|
677
|
+
},
|
|
512
678
|
{
|
|
513
679
|
type: "command",
|
|
514
680
|
command: "bash .claude/hooks/detect-gaps.sh",
|
|
@@ -647,7 +813,7 @@ var Installer = class {
|
|
|
647
813
|
constructor(merged, opts) {
|
|
648
814
|
this.root = opts.contentRoot ?? getPackageRoot();
|
|
649
815
|
this.target = opts.target;
|
|
650
|
-
this.claudeDir =
|
|
816
|
+
this.claudeDir = path7.join(opts.target, ".claude");
|
|
651
817
|
this.merged = merged;
|
|
652
818
|
this.dryRun = opts.dryRun;
|
|
653
819
|
this.shareFrom = opts.shareFrom;
|
|
@@ -679,22 +845,22 @@ var Installer = class {
|
|
|
679
845
|
return this.logs;
|
|
680
846
|
}
|
|
681
847
|
backup() {
|
|
682
|
-
if (!
|
|
683
|
-
const backup =
|
|
848
|
+
if (!fs6.existsSync(this.claudeDir)) return;
|
|
849
|
+
const backup = path7.join(this.target, ".claude.bak");
|
|
684
850
|
if (this.dryRun) {
|
|
685
851
|
this.log(" [dry-run] Would backup .claude/ -> .claude.bak/");
|
|
686
852
|
return;
|
|
687
853
|
}
|
|
688
|
-
if (
|
|
689
|
-
|
|
854
|
+
if (fs6.existsSync(backup)) {
|
|
855
|
+
fs6.rmSync(backup, { recursive: true, force: true });
|
|
690
856
|
}
|
|
691
|
-
|
|
857
|
+
fs6.renameSync(this.claudeDir, backup);
|
|
692
858
|
this.log(" Backed up .claude/ -> .claude.bak/");
|
|
693
859
|
}
|
|
694
860
|
createDirs() {
|
|
695
861
|
if (this.dryRun) return;
|
|
696
862
|
for (const d of ["skills", "rules", "agents", "docs"]) {
|
|
697
|
-
ensureDir(
|
|
863
|
+
ensureDir(path7.join(this.claudeDir, d));
|
|
698
864
|
}
|
|
699
865
|
}
|
|
700
866
|
get hasStatusline() {
|
|
@@ -702,7 +868,7 @@ var Installer = class {
|
|
|
702
868
|
}
|
|
703
869
|
copyHooks() {
|
|
704
870
|
this.log("\nHooks:");
|
|
705
|
-
const hooksDir =
|
|
871
|
+
const hooksDir = path7.join(this.claudeDir, "hooks");
|
|
706
872
|
if (this.dryRun) {
|
|
707
873
|
if (this.shareFrom) {
|
|
708
874
|
this.log(" [dry-run] hooks/ -> symlink from main worktree");
|
|
@@ -713,8 +879,8 @@ var Installer = class {
|
|
|
713
879
|
return;
|
|
714
880
|
}
|
|
715
881
|
if (this.shareFrom) {
|
|
716
|
-
const sharedHooks =
|
|
717
|
-
if (
|
|
882
|
+
const sharedHooks = path7.join(this.shareFrom, ".claude", "hooks");
|
|
883
|
+
if (fs6.existsSync(sharedHooks)) {
|
|
718
884
|
const result = linkOrCopyDir(sharedHooks, hooksDir, "symlink");
|
|
719
885
|
if (result === "linked") {
|
|
720
886
|
this.sharedDirs.push("hooks");
|
|
@@ -726,17 +892,17 @@ var Installer = class {
|
|
|
726
892
|
return;
|
|
727
893
|
}
|
|
728
894
|
}
|
|
729
|
-
const hooksSrc =
|
|
730
|
-
if (
|
|
895
|
+
const hooksSrc = path7.join(this.root, "hooks");
|
|
896
|
+
if (fs6.existsSync(hooksSrc)) {
|
|
731
897
|
ensureDir(hooksDir);
|
|
732
|
-
for (const entry of
|
|
898
|
+
for (const entry of fs6.readdirSync(hooksSrc, { withFileTypes: true })) {
|
|
733
899
|
if (entry.name === "statusline.sh") continue;
|
|
734
|
-
const src =
|
|
735
|
-
const dst =
|
|
900
|
+
const src = path7.join(hooksSrc, entry.name);
|
|
901
|
+
const dst = path7.join(hooksDir, entry.name);
|
|
736
902
|
if (entry.isDirectory()) {
|
|
737
903
|
copyDirSync(src, dst);
|
|
738
904
|
} else {
|
|
739
|
-
|
|
905
|
+
fs6.copyFileSync(src, dst);
|
|
740
906
|
}
|
|
741
907
|
}
|
|
742
908
|
this.log(" [ok] hooks/");
|
|
@@ -745,24 +911,24 @@ var Installer = class {
|
|
|
745
911
|
}
|
|
746
912
|
copyStatusline() {
|
|
747
913
|
if (!this.hasStatusline) return;
|
|
748
|
-
const statusline =
|
|
749
|
-
if (
|
|
750
|
-
|
|
914
|
+
const statusline = path7.join(this.root, "hooks", "statusline.sh");
|
|
915
|
+
if (fs6.existsSync(statusline)) {
|
|
916
|
+
fs6.copyFileSync(
|
|
751
917
|
statusline,
|
|
752
|
-
|
|
918
|
+
path7.join(this.claudeDir, "statusline.sh")
|
|
753
919
|
);
|
|
754
|
-
this.log(" [ok] statusline.sh (
|
|
920
|
+
this.log(" [ok] statusline.sh (statusline profile)");
|
|
755
921
|
}
|
|
756
922
|
}
|
|
757
923
|
copySkills() {
|
|
758
924
|
this.log("\nSkills:");
|
|
759
925
|
for (const skill of this.merged.skills) {
|
|
760
|
-
const src =
|
|
761
|
-
if (!
|
|
926
|
+
const src = path7.join(this.root, "skills", skill);
|
|
927
|
+
if (!fs6.existsSync(src)) {
|
|
762
928
|
this.log(` WARN: Skill '${skill}' not found in skills/`);
|
|
763
929
|
continue;
|
|
764
930
|
}
|
|
765
|
-
const dst =
|
|
931
|
+
const dst = path7.join(this.claudeDir, "skills", skill);
|
|
766
932
|
if (this.dryRun) {
|
|
767
933
|
this.log(` [dry-run] ${skill}`);
|
|
768
934
|
} else {
|
|
@@ -772,9 +938,9 @@ var Installer = class {
|
|
|
772
938
|
}
|
|
773
939
|
const hasGamedev = this.merged.rules.gamedev.length > 0;
|
|
774
940
|
if (hasGamedev) {
|
|
775
|
-
const tplSrc =
|
|
776
|
-
if (
|
|
777
|
-
const tplDst =
|
|
941
|
+
const tplSrc = path7.join(this.root, "templates", "gamedev");
|
|
942
|
+
if (fs6.existsSync(tplSrc)) {
|
|
943
|
+
const tplDst = path7.join(this.claudeDir, "skills", "_templates");
|
|
778
944
|
if (this.dryRun) {
|
|
779
945
|
this.log(" [dry-run] _templates/");
|
|
780
946
|
} else {
|
|
@@ -787,33 +953,33 @@ var Installer = class {
|
|
|
787
953
|
copyRules() {
|
|
788
954
|
this.log("\nRules:");
|
|
789
955
|
for (const rule of this.merged.rules.universal) {
|
|
790
|
-
const src =
|
|
791
|
-
if (!
|
|
956
|
+
const src = path7.join(this.root, "rules", `${rule}.md`);
|
|
957
|
+
if (!fs6.existsSync(src)) {
|
|
792
958
|
this.log(` WARN: Rule '${rule}.md' not found`);
|
|
793
959
|
continue;
|
|
794
960
|
}
|
|
795
961
|
if (this.dryRun) {
|
|
796
962
|
this.log(` [dry-run] ${rule}.md`);
|
|
797
963
|
} else {
|
|
798
|
-
|
|
964
|
+
fs6.copyFileSync(
|
|
799
965
|
src,
|
|
800
|
-
|
|
966
|
+
path7.join(this.claudeDir, "rules", `${rule}.md`)
|
|
801
967
|
);
|
|
802
968
|
this.log(` [ok] ${rule}.md`);
|
|
803
969
|
}
|
|
804
970
|
}
|
|
805
971
|
for (const rule of this.merged.rules.gamedev) {
|
|
806
|
-
const src =
|
|
807
|
-
if (!
|
|
972
|
+
const src = path7.join(this.root, "rules", "gamedev", `${rule}.md`);
|
|
973
|
+
if (!fs6.existsSync(src)) {
|
|
808
974
|
this.log(` WARN: Rule '${rule}.md' not found`);
|
|
809
975
|
continue;
|
|
810
976
|
}
|
|
811
977
|
if (this.dryRun) {
|
|
812
978
|
this.log(` [dry-run] ${rule}.md`);
|
|
813
979
|
} else {
|
|
814
|
-
|
|
980
|
+
fs6.copyFileSync(
|
|
815
981
|
src,
|
|
816
|
-
|
|
982
|
+
path7.join(this.claudeDir, "rules", `${rule}.md`)
|
|
817
983
|
);
|
|
818
984
|
this.log(` [ok] ${rule}.md`);
|
|
819
985
|
}
|
|
@@ -823,25 +989,25 @@ var Installer = class {
|
|
|
823
989
|
if (this.merged.agents.length === 0) return;
|
|
824
990
|
this.log("\nAgents:");
|
|
825
991
|
for (const agentDir of this.merged.agents) {
|
|
826
|
-
const src =
|
|
827
|
-
if (!
|
|
992
|
+
const src = path7.join(this.root, "agents", agentDir);
|
|
993
|
+
if (!fs6.existsSync(src)) {
|
|
828
994
|
this.log(` WARN: Agent dir '${agentDir}' not found`);
|
|
829
995
|
continue;
|
|
830
996
|
}
|
|
831
|
-
const dst =
|
|
997
|
+
const dst = path7.join(this.claudeDir, "agents", agentDir);
|
|
832
998
|
if (this.dryRun) {
|
|
833
999
|
this.log(` [dry-run] agents/${agentDir}/`);
|
|
834
1000
|
} else {
|
|
835
1001
|
copyDirSync(src, dst);
|
|
836
|
-
const count =
|
|
1002
|
+
const count = fs6.readdirSync(dst, { recursive: true }).filter((f) => String(f).endsWith(".md")).length;
|
|
837
1003
|
this.log(` [ok] agents/${agentDir}/ (${count} agents)`);
|
|
838
1004
|
}
|
|
839
1005
|
}
|
|
840
1006
|
}
|
|
841
1007
|
copyDocs() {
|
|
842
|
-
const docsDir =
|
|
843
|
-
const docsSrc =
|
|
844
|
-
if (!
|
|
1008
|
+
const docsDir = path7.join(this.claudeDir, "docs");
|
|
1009
|
+
const docsSrc = path7.join(this.root, "docs");
|
|
1010
|
+
if (!fs6.existsSync(docsSrc)) return;
|
|
845
1011
|
this.log("\nDocs:");
|
|
846
1012
|
if (this.dryRun) {
|
|
847
1013
|
if (this.shareFrom) {
|
|
@@ -852,8 +1018,8 @@ var Installer = class {
|
|
|
852
1018
|
return;
|
|
853
1019
|
}
|
|
854
1020
|
if (this.shareFrom) {
|
|
855
|
-
const sharedDocs =
|
|
856
|
-
if (
|
|
1021
|
+
const sharedDocs = path7.join(this.shareFrom, ".claude", "docs");
|
|
1022
|
+
if (fs6.existsSync(sharedDocs)) {
|
|
857
1023
|
const result = linkOrCopyDir(sharedDocs, docsDir, "symlink");
|
|
858
1024
|
if (result === "linked") {
|
|
859
1025
|
this.sharedDirs.push("docs");
|
|
@@ -884,8 +1050,8 @@ var Installer = class {
|
|
|
884
1050
|
);
|
|
885
1051
|
return;
|
|
886
1052
|
}
|
|
887
|
-
const settingsPath =
|
|
888
|
-
|
|
1053
|
+
const settingsPath = path7.join(this.claudeDir, "settings.json");
|
|
1054
|
+
fs6.writeFileSync(
|
|
889
1055
|
settingsPath,
|
|
890
1056
|
JSON.stringify(settings, null, 2) + "\n",
|
|
891
1057
|
"utf-8"
|
|
@@ -894,65 +1060,65 @@ var Installer = class {
|
|
|
894
1060
|
}
|
|
895
1061
|
};
|
|
896
1062
|
function clean(target) {
|
|
897
|
-
const claudeDir =
|
|
898
|
-
const backup =
|
|
899
|
-
if (!
|
|
1063
|
+
const claudeDir = path7.join(target, ".claude");
|
|
1064
|
+
const backup = path7.join(target, ".claude.bak");
|
|
1065
|
+
if (!fs6.existsSync(claudeDir)) {
|
|
900
1066
|
console.log(`No .claude/ found in ${target} \u2014 nothing to clean.`);
|
|
901
1067
|
return;
|
|
902
1068
|
}
|
|
903
|
-
const manifestFile =
|
|
904
|
-
if (
|
|
905
|
-
const data = JSON.parse(
|
|
1069
|
+
const manifestFile = path7.join(claudeDir, "arcane-manifest.json");
|
|
1070
|
+
if (fs6.existsSync(manifestFile)) {
|
|
1071
|
+
const data = JSON.parse(fs6.readFileSync(manifestFile, "utf-8"));
|
|
906
1072
|
const profiles = (data.profiles ?? []).filter(
|
|
907
|
-
(
|
|
1073
|
+
(p2) => p2 !== "core"
|
|
908
1074
|
);
|
|
909
1075
|
console.log(` Current profile: ${profiles.join(" + ")}`);
|
|
910
1076
|
console.log(` Installed: ${data.installed_at ?? "unknown"}`);
|
|
911
1077
|
const shared = data.worktree?.shared_dirs ?? [];
|
|
912
1078
|
for (const dir of shared) {
|
|
913
|
-
const
|
|
914
|
-
if (isSymlinkOrJunction(
|
|
915
|
-
|
|
1079
|
+
const p2 = path7.join(claudeDir, dir);
|
|
1080
|
+
if (isSymlinkOrJunction(p2)) {
|
|
1081
|
+
fs6.unlinkSync(p2);
|
|
916
1082
|
console.log(` [ok] Unlinked shared ${dir}/`);
|
|
917
1083
|
}
|
|
918
1084
|
}
|
|
919
1085
|
}
|
|
920
1086
|
for (const sub of ["hooks", "docs", "skills", "agents", "rules"]) {
|
|
921
|
-
const
|
|
922
|
-
safeRemove(
|
|
1087
|
+
const p2 = path7.join(claudeDir, sub);
|
|
1088
|
+
safeRemove(p2);
|
|
923
1089
|
}
|
|
924
|
-
const remaining =
|
|
1090
|
+
const remaining = fs6.readdirSync(claudeDir);
|
|
925
1091
|
for (const f of remaining) {
|
|
926
|
-
const
|
|
927
|
-
safeRemove(
|
|
1092
|
+
const p2 = path7.join(claudeDir, f);
|
|
1093
|
+
safeRemove(p2);
|
|
928
1094
|
}
|
|
929
|
-
if (
|
|
930
|
-
|
|
1095
|
+
if (fs6.existsSync(claudeDir)) {
|
|
1096
|
+
fs6.rmSync(claudeDir, { recursive: true, force: true });
|
|
931
1097
|
}
|
|
932
|
-
if (
|
|
933
|
-
|
|
1098
|
+
if (fs6.existsSync(backup)) {
|
|
1099
|
+
fs6.rmSync(backup, { recursive: true, force: true });
|
|
934
1100
|
}
|
|
935
1101
|
console.log(`
|
|
936
1102
|
[ok] Removed .claude/ from ${target}`);
|
|
937
1103
|
}
|
|
938
1104
|
|
|
939
1105
|
// src/content-source.ts
|
|
940
|
-
import
|
|
941
|
-
import
|
|
1106
|
+
import fs8 from "fs";
|
|
1107
|
+
import path9 from "path";
|
|
942
1108
|
import os2 from "os";
|
|
943
1109
|
|
|
944
1110
|
// src/cache.ts
|
|
945
|
-
import
|
|
946
|
-
import
|
|
1111
|
+
import fs7 from "fs";
|
|
1112
|
+
import path8 from "path";
|
|
947
1113
|
import os from "os";
|
|
948
|
-
var ARCANE_DIR =
|
|
949
|
-
var CACHE_DIR =
|
|
1114
|
+
var ARCANE_DIR = path8.join(os.homedir(), ".arcane");
|
|
1115
|
+
var CACHE_DIR = path8.join(ARCANE_DIR, "cache");
|
|
950
1116
|
function getCachePath(version) {
|
|
951
|
-
return
|
|
1117
|
+
return path8.join(CACHE_DIR, version);
|
|
952
1118
|
}
|
|
953
1119
|
function isCached(version) {
|
|
954
1120
|
const versionDir = getCachePath(version);
|
|
955
|
-
return
|
|
1121
|
+
return fs7.existsSync(versionDir) && fs7.existsSync(path8.join(versionDir, ".cache-meta.json"));
|
|
956
1122
|
}
|
|
957
1123
|
function getCachedContentRoot(version) {
|
|
958
1124
|
if (!isCached(version)) return null;
|
|
@@ -960,34 +1126,34 @@ function getCachedContentRoot(version) {
|
|
|
960
1126
|
}
|
|
961
1127
|
function storeInCache(version, contentDir) {
|
|
962
1128
|
const versionDir = getCachePath(version);
|
|
963
|
-
if (
|
|
964
|
-
|
|
1129
|
+
if (fs7.existsSync(versionDir)) {
|
|
1130
|
+
fs7.rmSync(versionDir, { recursive: true, force: true });
|
|
965
1131
|
}
|
|
966
|
-
|
|
1132
|
+
fs7.mkdirSync(versionDir, { recursive: true });
|
|
967
1133
|
for (const dir of ["skills", "rules", "agents", "hooks", "profiles", "templates", "docs"]) {
|
|
968
|
-
const src =
|
|
969
|
-
if (
|
|
970
|
-
copyDirRecursive(src,
|
|
1134
|
+
const src = path8.join(contentDir, dir);
|
|
1135
|
+
if (fs7.existsSync(src)) {
|
|
1136
|
+
copyDirRecursive(src, path8.join(versionDir, dir));
|
|
971
1137
|
}
|
|
972
1138
|
}
|
|
973
1139
|
const meta = {
|
|
974
1140
|
version,
|
|
975
1141
|
cached_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
976
1142
|
};
|
|
977
|
-
|
|
978
|
-
|
|
1143
|
+
fs7.writeFileSync(
|
|
1144
|
+
path8.join(versionDir, ".cache-meta.json"),
|
|
979
1145
|
JSON.stringify(meta, null, 2) + "\n"
|
|
980
1146
|
);
|
|
981
1147
|
return versionDir;
|
|
982
1148
|
}
|
|
983
1149
|
function pruneCache(keepCount = 3) {
|
|
984
|
-
if (!
|
|
985
|
-
const entries =
|
|
986
|
-
const metaPath =
|
|
1150
|
+
if (!fs7.existsSync(CACHE_DIR)) return;
|
|
1151
|
+
const entries = fs7.readdirSync(CACHE_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
|
|
1152
|
+
const metaPath = path8.join(CACHE_DIR, e.name, ".cache-meta.json");
|
|
987
1153
|
let cachedAt = 0;
|
|
988
|
-
if (
|
|
1154
|
+
if (fs7.existsSync(metaPath)) {
|
|
989
1155
|
try {
|
|
990
|
-
const meta = JSON.parse(
|
|
1156
|
+
const meta = JSON.parse(fs7.readFileSync(metaPath, "utf-8"));
|
|
991
1157
|
cachedAt = new Date(meta.cached_at).getTime();
|
|
992
1158
|
} catch {
|
|
993
1159
|
cachedAt = 0;
|
|
@@ -996,18 +1162,18 @@ function pruneCache(keepCount = 3) {
|
|
|
996
1162
|
return { name: e.name, cachedAt };
|
|
997
1163
|
}).sort((a, b) => b.cachedAt - a.cachedAt);
|
|
998
1164
|
for (const entry of entries.slice(keepCount)) {
|
|
999
|
-
|
|
1165
|
+
fs7.rmSync(path8.join(CACHE_DIR, entry.name), { recursive: true, force: true });
|
|
1000
1166
|
}
|
|
1001
1167
|
}
|
|
1002
1168
|
function copyDirRecursive(src, dest) {
|
|
1003
|
-
|
|
1004
|
-
for (const entry of
|
|
1005
|
-
const srcPath =
|
|
1006
|
-
const destPath =
|
|
1169
|
+
fs7.mkdirSync(dest, { recursive: true });
|
|
1170
|
+
for (const entry of fs7.readdirSync(src, { withFileTypes: true })) {
|
|
1171
|
+
const srcPath = path8.join(src, entry.name);
|
|
1172
|
+
const destPath = path8.join(dest, entry.name);
|
|
1007
1173
|
if (entry.isDirectory()) {
|
|
1008
1174
|
copyDirRecursive(srcPath, destPath);
|
|
1009
1175
|
} else {
|
|
1010
|
-
|
|
1176
|
+
fs7.copyFileSync(srcPath, destPath);
|
|
1011
1177
|
}
|
|
1012
1178
|
}
|
|
1013
1179
|
}
|
|
@@ -1023,7 +1189,7 @@ var BundledContentSource = class {
|
|
|
1023
1189
|
}
|
|
1024
1190
|
async isAvailable() {
|
|
1025
1191
|
const root = getPackageRoot();
|
|
1026
|
-
return
|
|
1192
|
+
return fs8.existsSync(path9.join(root, "skills"));
|
|
1027
1193
|
}
|
|
1028
1194
|
async getVersion() {
|
|
1029
1195
|
return getPackageVersion();
|
|
@@ -1063,14 +1229,14 @@ var GitHubContentSource = class {
|
|
|
1063
1229
|
if (isCached(version)) {
|
|
1064
1230
|
return getCachedContentRoot(version);
|
|
1065
1231
|
}
|
|
1066
|
-
const tmpDir =
|
|
1232
|
+
const tmpDir = fs8.mkdtempSync(path9.join(os2.tmpdir(), "arcane-github-"));
|
|
1067
1233
|
try {
|
|
1068
1234
|
await this.downloadAndExtract(tmpDir);
|
|
1069
1235
|
const contentRoot = storeInCache(version, this.findExtractedRoot(tmpDir));
|
|
1070
1236
|
pruneCache(3);
|
|
1071
1237
|
return contentRoot;
|
|
1072
1238
|
} finally {
|
|
1073
|
-
|
|
1239
|
+
fs8.rmSync(tmpDir, { recursive: true, force: true });
|
|
1074
1240
|
}
|
|
1075
1241
|
}
|
|
1076
1242
|
async isAvailable() {
|
|
@@ -1126,8 +1292,8 @@ var GitHubContentSource = class {
|
|
|
1126
1292
|
throw new Error(`Failed to download tarball: ${response.status}`);
|
|
1127
1293
|
}
|
|
1128
1294
|
const tar = await import("tar");
|
|
1129
|
-
const tarballPath =
|
|
1130
|
-
const fileStream =
|
|
1295
|
+
const tarballPath = path9.join(destDir, "repo.tar.gz");
|
|
1296
|
+
const fileStream = fs8.createWriteStream(tarballPath);
|
|
1131
1297
|
const reader = response.body.getReader();
|
|
1132
1298
|
const writable = new WritableStream({
|
|
1133
1299
|
write(chunk) {
|
|
@@ -1152,12 +1318,12 @@ var GitHubContentSource = class {
|
|
|
1152
1318
|
});
|
|
1153
1319
|
}
|
|
1154
1320
|
findExtractedRoot(tmpDir) {
|
|
1155
|
-
const entries =
|
|
1321
|
+
const entries = fs8.readdirSync(tmpDir, { withFileTypes: true });
|
|
1156
1322
|
const dirs = entries.filter(
|
|
1157
1323
|
(e) => e.isDirectory() && e.name !== "." && e.name !== ".."
|
|
1158
1324
|
);
|
|
1159
1325
|
if (dirs.length === 1) {
|
|
1160
|
-
return
|
|
1326
|
+
return path9.join(tmpDir, dirs[0].name);
|
|
1161
1327
|
}
|
|
1162
1328
|
return tmpDir;
|
|
1163
1329
|
}
|
|
@@ -1212,14 +1378,14 @@ async function resolveContentSourceForVersion(version, opts = {}) {
|
|
|
1212
1378
|
return source;
|
|
1213
1379
|
}
|
|
1214
1380
|
function findLatestCache() {
|
|
1215
|
-
const cacheDir =
|
|
1216
|
-
if (!
|
|
1217
|
-
const entries =
|
|
1218
|
-
const metaPath =
|
|
1381
|
+
const cacheDir = path9.join(os2.homedir(), ".arcane", "cache");
|
|
1382
|
+
if (!fs8.existsSync(cacheDir)) return null;
|
|
1383
|
+
const entries = fs8.readdirSync(cacheDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
|
|
1384
|
+
const metaPath = path9.join(cacheDir, e.name, ".cache-meta.json");
|
|
1219
1385
|
let cachedAt = 0;
|
|
1220
|
-
if (
|
|
1386
|
+
if (fs8.existsSync(metaPath)) {
|
|
1221
1387
|
try {
|
|
1222
|
-
const meta = JSON.parse(
|
|
1388
|
+
const meta = JSON.parse(fs8.readFileSync(metaPath, "utf-8"));
|
|
1223
1389
|
cachedAt = new Date(meta.cached_at).getTime();
|
|
1224
1390
|
} catch {
|
|
1225
1391
|
cachedAt = 0;
|
|
@@ -1231,22 +1397,22 @@ function findLatestCache() {
|
|
|
1231
1397
|
}
|
|
1232
1398
|
|
|
1233
1399
|
// src/registry.ts
|
|
1234
|
-
import
|
|
1400
|
+
import path10 from "path";
|
|
1235
1401
|
import os3 from "os";
|
|
1236
1402
|
var REGISTRY_VERSION = 1;
|
|
1237
1403
|
function arcaneHome() {
|
|
1238
|
-
return process.env.ARCANE_HOME ??
|
|
1404
|
+
return process.env.ARCANE_HOME ?? path10.join(os3.homedir(), ".arcane");
|
|
1239
1405
|
}
|
|
1240
1406
|
function registryPath() {
|
|
1241
|
-
return
|
|
1407
|
+
return path10.join(arcaneHome(), "installations.json");
|
|
1242
1408
|
}
|
|
1243
1409
|
function readRegistryFile() {
|
|
1244
|
-
const
|
|
1245
|
-
if (!fileExists(
|
|
1410
|
+
const p2 = registryPath();
|
|
1411
|
+
if (!fileExists(p2)) {
|
|
1246
1412
|
return { version: REGISTRY_VERSION, installations: [] };
|
|
1247
1413
|
}
|
|
1248
1414
|
try {
|
|
1249
|
-
const data = readJsonSync(
|
|
1415
|
+
const data = readJsonSync(p2);
|
|
1250
1416
|
return {
|
|
1251
1417
|
version: REGISTRY_VERSION,
|
|
1252
1418
|
installations: Array.isArray(data.installations) ? data.installations : []
|
|
@@ -1263,7 +1429,7 @@ function writeRegistryFile(registry) {
|
|
|
1263
1429
|
}
|
|
1264
1430
|
}
|
|
1265
1431
|
function registerInstallation(target) {
|
|
1266
|
-
const abs =
|
|
1432
|
+
const abs = path10.resolve(target);
|
|
1267
1433
|
const registry = readRegistryFile();
|
|
1268
1434
|
if (registry.installations.some((e) => e.path === abs)) {
|
|
1269
1435
|
return;
|
|
@@ -1290,39 +1456,42 @@ async function installCommand(profileExpr, opts) {
|
|
|
1290
1456
|
quiet: !profileExpr
|
|
1291
1457
|
});
|
|
1292
1458
|
const root = await source.getContentRoot();
|
|
1293
|
-
const profilesDir =
|
|
1294
|
-
const target =
|
|
1459
|
+
const profilesDir = path11.join(root, "profiles");
|
|
1460
|
+
const target = path11.resolve(opts.target ?? process.cwd());
|
|
1295
1461
|
if (!profileExpr) {
|
|
1296
1462
|
const profiles = listProfiles(profilesDir);
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
console.log(
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1463
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
1464
|
+
const selected = await runInstallWizard(profiles, detectStack(target));
|
|
1465
|
+
if (!selected) return;
|
|
1466
|
+
profileExpr = selected.join("+");
|
|
1467
|
+
} else {
|
|
1468
|
+
console.log(chalk2.bold("\nAvailable profiles (combine freely with +):\n"));
|
|
1469
|
+
for (const group of groupByCategory(profiles)) {
|
|
1470
|
+
console.log(chalk2.cyan(` ${group.label}:`));
|
|
1471
|
+
for (const p2 of group.profiles) {
|
|
1472
|
+
console.log(
|
|
1473
|
+
` ${chalk2.green(p2.name.padEnd(20))} ${p2.description}`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
console.log();
|
|
1477
|
+
}
|
|
1306
1478
|
console.log(
|
|
1307
|
-
|
|
1479
|
+
chalk2.dim(" Usage: npx claude-code-arcane install backend-ts+agile+testing\n")
|
|
1308
1480
|
);
|
|
1481
|
+
return;
|
|
1309
1482
|
}
|
|
1310
|
-
console.log(
|
|
1311
|
-
chalk.dim("\n Usage: npx claude-code-arcane install backend-ts+agile+testing\n")
|
|
1312
|
-
);
|
|
1313
|
-
return;
|
|
1314
1483
|
}
|
|
1315
1484
|
const profileNames = profileExpr.split("+").filter(Boolean);
|
|
1316
1485
|
console.log(
|
|
1317
|
-
|
|
1318
|
-
Installing profile: ${
|
|
1486
|
+
chalk2.bold(`
|
|
1487
|
+
Installing profile: ${chalk2.cyan(profileExpr)}`)
|
|
1319
1488
|
);
|
|
1320
1489
|
console.log(` Target: ${target}`);
|
|
1321
1490
|
const wtInfo = getWorktreeInfo(target);
|
|
1322
1491
|
let shareFrom = opts.shareFrom;
|
|
1323
1492
|
let worktreeMeta;
|
|
1324
1493
|
if (wtInfo?.isWorktree) {
|
|
1325
|
-
console.log(
|
|
1494
|
+
console.log(chalk2.blue(` Worktree: yes (main: ${wtInfo.mainWorktreePath})`));
|
|
1326
1495
|
worktreeMeta = {
|
|
1327
1496
|
is_worktree: true,
|
|
1328
1497
|
main_worktree: wtInfo.mainWorktreePath
|
|
@@ -1330,13 +1499,13 @@ Installing profile: ${chalk.cyan(profileExpr)}`)
|
|
|
1330
1499
|
if (!shareFrom && findMainArcaneInstall(wtInfo.mainWorktreePath)) {
|
|
1331
1500
|
shareFrom = wtInfo.mainWorktreePath;
|
|
1332
1501
|
console.log(
|
|
1333
|
-
|
|
1502
|
+
chalk2.blue(" Sharing: hooks + docs from main worktree")
|
|
1334
1503
|
);
|
|
1335
1504
|
}
|
|
1336
1505
|
} else if (wtInfo) {
|
|
1337
1506
|
worktreeMeta = { is_worktree: false, main_worktree: target };
|
|
1338
1507
|
}
|
|
1339
|
-
if (opts.dryRun) console.log(
|
|
1508
|
+
if (opts.dryRun) console.log(chalk2.yellow(" Mode: dry-run\n"));
|
|
1340
1509
|
const merged = mergeProfiles(profilesDir, profileNames);
|
|
1341
1510
|
console.log(` Profiles loaded: ${merged.loaded.join(", ")}`);
|
|
1342
1511
|
console.log(` Skills: ${merged.skills.length}`);
|
|
@@ -1354,14 +1523,14 @@ Installing profile: ${chalk.cyan(profileExpr)}`)
|
|
|
1354
1523
|
installer.run(profileExpr, worktreeMeta);
|
|
1355
1524
|
if (!opts.dryRun) {
|
|
1356
1525
|
registerInstallation(target);
|
|
1357
|
-
console.log(
|
|
1526
|
+
console.log(chalk2.green("\n Installation complete."));
|
|
1358
1527
|
}
|
|
1359
1528
|
}
|
|
1360
1529
|
|
|
1361
1530
|
// src/commands/add.ts
|
|
1362
|
-
import
|
|
1363
|
-
import
|
|
1364
|
-
import
|
|
1531
|
+
import fs9 from "fs";
|
|
1532
|
+
import path12 from "path";
|
|
1533
|
+
import chalk3 from "chalk";
|
|
1365
1534
|
async function addCommand(items) {
|
|
1366
1535
|
const target = process.cwd();
|
|
1367
1536
|
const source = await resolveContentSource({ quiet: true });
|
|
@@ -1369,13 +1538,13 @@ async function addCommand(items) {
|
|
|
1369
1538
|
const manifest = readManifest(target);
|
|
1370
1539
|
if (!manifest) {
|
|
1371
1540
|
console.error(
|
|
1372
|
-
|
|
1541
|
+
chalk3.red(
|
|
1373
1542
|
"No arcane-manifest.json found. Run `npx claude-code-arcane install` first."
|
|
1374
1543
|
)
|
|
1375
1544
|
);
|
|
1376
1545
|
process.exit(1);
|
|
1377
1546
|
}
|
|
1378
|
-
const claudeDir =
|
|
1547
|
+
const claudeDir = path12.join(target, ".claude");
|
|
1379
1548
|
const added = [];
|
|
1380
1549
|
const skipped = [];
|
|
1381
1550
|
const notFound = [];
|
|
@@ -1385,9 +1554,9 @@ async function addCommand(items) {
|
|
|
1385
1554
|
for (const item of items) {
|
|
1386
1555
|
if (item.startsWith("+")) {
|
|
1387
1556
|
const profileName = item.slice(1);
|
|
1388
|
-
const profilePath =
|
|
1389
|
-
if (!
|
|
1390
|
-
console.error(
|
|
1557
|
+
const profilePath = path12.join(root, "profiles", `${profileName}.yaml`);
|
|
1558
|
+
if (!fs9.existsSync(profilePath)) {
|
|
1559
|
+
console.error(chalk3.red(` Profile '${profileName}' not found`));
|
|
1391
1560
|
continue;
|
|
1392
1561
|
}
|
|
1393
1562
|
const profile = parseProfile(profilePath);
|
|
@@ -1399,10 +1568,10 @@ async function addCommand(items) {
|
|
|
1399
1568
|
}
|
|
1400
1569
|
for (const rule of profile.rules.universal) {
|
|
1401
1570
|
if (!manifest.installed_rules.includes(rule)) {
|
|
1402
|
-
const src =
|
|
1403
|
-
if (
|
|
1404
|
-
ensureDir(
|
|
1405
|
-
|
|
1571
|
+
const src = path12.join(root, "rules", `${rule}.md`);
|
|
1572
|
+
if (fs9.existsSync(src)) {
|
|
1573
|
+
ensureDir(path12.join(claudeDir, "rules"));
|
|
1574
|
+
fs9.copyFileSync(src, path12.join(claudeDir, "rules", `${rule}.md`));
|
|
1406
1575
|
manifest.installed_rules.push(rule);
|
|
1407
1576
|
addedRules.push(rule);
|
|
1408
1577
|
}
|
|
@@ -1410,10 +1579,10 @@ async function addCommand(items) {
|
|
|
1410
1579
|
}
|
|
1411
1580
|
for (const rule of profile.rules.gamedev) {
|
|
1412
1581
|
if (!manifest.installed_rules.includes(rule)) {
|
|
1413
|
-
const src =
|
|
1414
|
-
if (
|
|
1415
|
-
ensureDir(
|
|
1416
|
-
|
|
1582
|
+
const src = path12.join(root, "rules", "gamedev", `${rule}.md`);
|
|
1583
|
+
if (fs9.existsSync(src)) {
|
|
1584
|
+
ensureDir(path12.join(claudeDir, "rules"));
|
|
1585
|
+
fs9.copyFileSync(src, path12.join(claudeDir, "rules", `${rule}.md`));
|
|
1417
1586
|
manifest.installed_rules.push(rule);
|
|
1418
1587
|
addedRules.push(rule);
|
|
1419
1588
|
}
|
|
@@ -1421,9 +1590,9 @@ async function addCommand(items) {
|
|
|
1421
1590
|
}
|
|
1422
1591
|
for (const agentDir of profile.agents) {
|
|
1423
1592
|
if (!manifest.installed_agents.includes(agentDir)) {
|
|
1424
|
-
const src =
|
|
1425
|
-
if (
|
|
1426
|
-
const dst =
|
|
1593
|
+
const src = path12.join(root, "agents", agentDir);
|
|
1594
|
+
if (fs9.existsSync(src)) {
|
|
1595
|
+
const dst = path12.join(claudeDir, "agents", agentDir);
|
|
1427
1596
|
copyDirSync(src, dst);
|
|
1428
1597
|
manifest.installed_agents.push(agentDir);
|
|
1429
1598
|
addedAgents.push(agentDir);
|
|
@@ -1431,9 +1600,9 @@ async function addCommand(items) {
|
|
|
1431
1600
|
}
|
|
1432
1601
|
}
|
|
1433
1602
|
if (profileName === "statusline") {
|
|
1434
|
-
const statuslineSrc =
|
|
1435
|
-
if (
|
|
1436
|
-
|
|
1603
|
+
const statuslineSrc = path12.join(root, "hooks", "statusline.sh");
|
|
1604
|
+
if (fs9.existsSync(statuslineSrc)) {
|
|
1605
|
+
fs9.copyFileSync(statuslineSrc, path12.join(claudeDir, "statusline.sh"));
|
|
1437
1606
|
statuslineAdded = true;
|
|
1438
1607
|
}
|
|
1439
1608
|
}
|
|
@@ -1442,7 +1611,7 @@ async function addCommand(items) {
|
|
|
1442
1611
|
}
|
|
1443
1612
|
if (!manifest.profiles.includes(profileName)) {
|
|
1444
1613
|
manifest.profiles.push(profileName);
|
|
1445
|
-
manifest.profile_command = manifest.profiles.filter((
|
|
1614
|
+
manifest.profile_command = manifest.profiles.filter((p2) => p2 !== "core").join("+");
|
|
1446
1615
|
}
|
|
1447
1616
|
if (statuslineAdded) {
|
|
1448
1617
|
addStatuslineToSettings(claudeDir);
|
|
@@ -1467,28 +1636,28 @@ async function addCommand(items) {
|
|
|
1467
1636
|
};
|
|
1468
1637
|
writeManifest(target, merged, manifest.profile_command, root);
|
|
1469
1638
|
const totalAdded = added.length + addedRules.length + addedAgents.length + (statuslineAdded ? 1 : 0);
|
|
1470
|
-
console.log(
|
|
1639
|
+
console.log(chalk3.bold(`
|
|
1471
1640
|
Added ${totalAdded} items:`));
|
|
1472
|
-
for (const s of added) console.log(
|
|
1473
|
-
for (const r of addedRules) console.log(
|
|
1474
|
-
for (const a of addedAgents) console.log(
|
|
1475
|
-
if (statuslineAdded) console.log(
|
|
1641
|
+
for (const s of added) console.log(chalk3.green(` [ok] skill: ${s}`));
|
|
1642
|
+
for (const r of addedRules) console.log(chalk3.green(` [ok] rule: ${r}`));
|
|
1643
|
+
for (const a of addedAgents) console.log(chalk3.green(` [ok] agents: ${a}/`));
|
|
1644
|
+
if (statuslineAdded) console.log(chalk3.green(" [ok] statusline.sh"));
|
|
1476
1645
|
for (const s of skipped)
|
|
1477
|
-
console.log(
|
|
1646
|
+
console.log(chalk3.dim(` [skip] ${s} (already installed)`));
|
|
1478
1647
|
for (const s of notFound)
|
|
1479
|
-
console.log(
|
|
1648
|
+
console.log(chalk3.red(` [miss] ${s} (not found in source)`));
|
|
1480
1649
|
}
|
|
1481
1650
|
function addSkill(root, target, skill, installed) {
|
|
1482
1651
|
if (installed.includes(skill)) return "skipped";
|
|
1483
|
-
const src =
|
|
1484
|
-
if (!
|
|
1485
|
-
const dst =
|
|
1652
|
+
const src = path12.join(root, "skills", skill);
|
|
1653
|
+
if (!fs9.existsSync(src)) return "not-found";
|
|
1654
|
+
const dst = path12.join(target, ".claude", "skills", skill);
|
|
1486
1655
|
copyDirSync(src, dst);
|
|
1487
1656
|
return "added";
|
|
1488
1657
|
}
|
|
1489
1658
|
function mergePermissions(claudeDir, newPerms) {
|
|
1490
|
-
const settingsPath =
|
|
1491
|
-
if (!
|
|
1659
|
+
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1660
|
+
if (!fs9.existsSync(settingsPath)) return;
|
|
1492
1661
|
const settings = readJsonSync(settingsPath);
|
|
1493
1662
|
const perms = settings.permissions ?? { allow: [], deny: [] };
|
|
1494
1663
|
const allowSet = new Set(perms.allow);
|
|
@@ -1501,8 +1670,8 @@ function mergePermissions(claudeDir, newPerms) {
|
|
|
1501
1670
|
writeJsonSync(settingsPath, settings);
|
|
1502
1671
|
}
|
|
1503
1672
|
function addStatuslineToSettings(claudeDir) {
|
|
1504
|
-
const settingsPath =
|
|
1505
|
-
if (!
|
|
1673
|
+
const settingsPath = path12.join(claudeDir, "settings.json");
|
|
1674
|
+
if (!fs9.existsSync(settingsPath)) return;
|
|
1506
1675
|
const settings = readJsonSync(settingsPath);
|
|
1507
1676
|
if (settings.statusLine) return;
|
|
1508
1677
|
settings.statusLine = {
|
|
@@ -1513,9 +1682,9 @@ function addStatuslineToSettings(claudeDir) {
|
|
|
1513
1682
|
}
|
|
1514
1683
|
|
|
1515
1684
|
// src/commands/remove.ts
|
|
1516
|
-
import
|
|
1517
|
-
import
|
|
1518
|
-
import
|
|
1685
|
+
import fs10 from "fs";
|
|
1686
|
+
import path13 from "path";
|
|
1687
|
+
import chalk4 from "chalk";
|
|
1519
1688
|
var CORE_SKILLS = [
|
|
1520
1689
|
"commit",
|
|
1521
1690
|
"create-pr",
|
|
@@ -1544,7 +1713,7 @@ async function removeCommand(items, opts = {}) {
|
|
|
1544
1713
|
const manifest = readManifest(target);
|
|
1545
1714
|
if (!manifest) {
|
|
1546
1715
|
console.error(
|
|
1547
|
-
|
|
1716
|
+
chalk4.red(
|
|
1548
1717
|
"No arcane-manifest.json found. Run `npx claude-code-arcane install` first."
|
|
1549
1718
|
)
|
|
1550
1719
|
);
|
|
@@ -1579,30 +1748,30 @@ async function removeCommand(items, opts = {}) {
|
|
|
1579
1748
|
}
|
|
1580
1749
|
manifest.total_skills = manifest.installed_skills.length;
|
|
1581
1750
|
manifest.total_rules = manifest.installed_rules.length;
|
|
1582
|
-
const mp =
|
|
1751
|
+
const mp = path13.join(target, ".claude", "arcane-manifest.json");
|
|
1583
1752
|
writeJsonSync(mp, manifest);
|
|
1584
1753
|
const totalRemoved = removedSkills.length + removedAgents.length + removedProfiles.length;
|
|
1585
|
-
console.log(
|
|
1754
|
+
console.log(chalk4.bold(`
|
|
1586
1755
|
Removed ${totalRemoved} items:`));
|
|
1587
|
-
for (const
|
|
1588
|
-
console.log(
|
|
1756
|
+
for (const p2 of removedProfiles)
|
|
1757
|
+
console.log(chalk4.magenta(` [ok] +${p2} (profile)`));
|
|
1589
1758
|
for (const s of removedSkills)
|
|
1590
|
-
console.log(
|
|
1759
|
+
console.log(chalk4.green(` [ok] ${s} (skill)`));
|
|
1591
1760
|
for (const a of removedAgents)
|
|
1592
|
-
console.log(
|
|
1761
|
+
console.log(chalk4.cyan(` [ok] ${a} (agent dir)`));
|
|
1593
1762
|
for (const s of skipped)
|
|
1594
|
-
console.log(
|
|
1763
|
+
console.log(chalk4.dim(` [skip] ${s}`));
|
|
1595
1764
|
}
|
|
1596
1765
|
function removeSkill(target, skill, manifest) {
|
|
1597
1766
|
if (CORE_SKILLS.includes(skill)) {
|
|
1598
1767
|
console.warn(
|
|
1599
|
-
|
|
1768
|
+
chalk4.yellow(` WARNING: '${skill}' is a core skill. Skipping.`)
|
|
1600
1769
|
);
|
|
1601
1770
|
return "skipped";
|
|
1602
1771
|
}
|
|
1603
|
-
const skillDir =
|
|
1604
|
-
if (!
|
|
1605
|
-
|
|
1772
|
+
const skillDir = path13.join(target, ".claude", "skills", skill);
|
|
1773
|
+
if (!fs10.existsSync(skillDir)) return "skipped";
|
|
1774
|
+
fs10.rmSync(skillDir, { recursive: true, force: true });
|
|
1606
1775
|
manifest.installed_skills = manifest.installed_skills.filter(
|
|
1607
1776
|
(s) => s !== skill
|
|
1608
1777
|
);
|
|
@@ -1611,33 +1780,33 @@ function removeSkill(target, skill, manifest) {
|
|
|
1611
1780
|
function removeProfile(root, target, profileName, manifest) {
|
|
1612
1781
|
if (profileName === "core") {
|
|
1613
1782
|
console.warn(
|
|
1614
|
-
|
|
1783
|
+
chalk4.yellow(" WARNING: Cannot remove core profile. Skipping.")
|
|
1615
1784
|
);
|
|
1616
1785
|
return { removed: false, skills: [], agents: [] };
|
|
1617
1786
|
}
|
|
1618
1787
|
if (!manifest.profiles.includes(profileName)) {
|
|
1619
1788
|
console.warn(
|
|
1620
|
-
|
|
1789
|
+
chalk4.yellow(` WARNING: Profile '${profileName}' is not installed.`)
|
|
1621
1790
|
);
|
|
1622
1791
|
return { removed: false, skills: [], agents: [] };
|
|
1623
1792
|
}
|
|
1624
|
-
const profilePath =
|
|
1625
|
-
if (!
|
|
1793
|
+
const profilePath = path13.join(root, "profiles", `${profileName}.yaml`);
|
|
1794
|
+
if (!fs10.existsSync(profilePath)) {
|
|
1626
1795
|
console.warn(
|
|
1627
|
-
|
|
1796
|
+
chalk4.yellow(` WARNING: Profile '${profileName}.yaml' not found.`)
|
|
1628
1797
|
);
|
|
1629
1798
|
return { removed: false, skills: [], agents: [] };
|
|
1630
1799
|
}
|
|
1631
1800
|
const profile = parseProfile(profilePath);
|
|
1632
1801
|
const remainingProfiles = manifest.profiles.filter(
|
|
1633
|
-
(
|
|
1802
|
+
(p2) => p2 !== profileName
|
|
1634
1803
|
);
|
|
1635
1804
|
const sharedSkills = /* @__PURE__ */ new Set();
|
|
1636
1805
|
const sharedAgents = /* @__PURE__ */ new Set();
|
|
1637
1806
|
const sharedRules = /* @__PURE__ */ new Set();
|
|
1638
1807
|
for (const rp of remainingProfiles) {
|
|
1639
|
-
const rpPath =
|
|
1640
|
-
if (!
|
|
1808
|
+
const rpPath = path13.join(root, "profiles", `${rp}.yaml`);
|
|
1809
|
+
if (!fs10.existsSync(rpPath)) continue;
|
|
1641
1810
|
const rpDef = parseProfile(rpPath);
|
|
1642
1811
|
rpDef.skills.forEach((s) => sharedSkills.add(s));
|
|
1643
1812
|
rpDef.agents.forEach((a) => sharedAgents.add(a));
|
|
@@ -1648,9 +1817,9 @@ function removeProfile(root, target, profileName, manifest) {
|
|
|
1648
1817
|
for (const skill of profile.skills) {
|
|
1649
1818
|
if (sharedSkills.has(skill)) continue;
|
|
1650
1819
|
if (CORE_SKILLS.includes(skill)) continue;
|
|
1651
|
-
const skillDir =
|
|
1652
|
-
if (
|
|
1653
|
-
|
|
1820
|
+
const skillDir = path13.join(target, ".claude", "skills", skill);
|
|
1821
|
+
if (fs10.existsSync(skillDir)) {
|
|
1822
|
+
fs10.rmSync(skillDir, { recursive: true, force: true });
|
|
1654
1823
|
removedSkills.push(skill);
|
|
1655
1824
|
}
|
|
1656
1825
|
manifest.installed_skills = manifest.installed_skills.filter(
|
|
@@ -1660,9 +1829,9 @@ function removeProfile(root, target, profileName, manifest) {
|
|
|
1660
1829
|
const removedAgents = [];
|
|
1661
1830
|
for (const agent of profile.agents) {
|
|
1662
1831
|
if (sharedAgents.has(agent)) continue;
|
|
1663
|
-
const agentDir =
|
|
1664
|
-
if (
|
|
1665
|
-
|
|
1832
|
+
const agentDir = path13.join(target, ".claude", "agents", agent);
|
|
1833
|
+
if (fs10.existsSync(agentDir)) {
|
|
1834
|
+
fs10.rmSync(agentDir, { recursive: true, force: true });
|
|
1666
1835
|
removedAgents.push(agent);
|
|
1667
1836
|
}
|
|
1668
1837
|
manifest.installed_agents = manifest.installed_agents.filter(
|
|
@@ -1674,138 +1843,133 @@ function removeProfile(root, target, profileName, manifest) {
|
|
|
1674
1843
|
...profile.rules.gamedev
|
|
1675
1844
|
]) {
|
|
1676
1845
|
if (sharedRules.has(rule)) continue;
|
|
1677
|
-
const ruleFile =
|
|
1678
|
-
if (
|
|
1679
|
-
|
|
1846
|
+
const ruleFile = path13.join(target, ".claude", "rules", `${rule}.md`);
|
|
1847
|
+
if (fs10.existsSync(ruleFile)) {
|
|
1848
|
+
fs10.rmSync(ruleFile);
|
|
1680
1849
|
}
|
|
1681
1850
|
manifest.installed_rules = manifest.installed_rules.filter(
|
|
1682
1851
|
(r) => r !== rule
|
|
1683
1852
|
);
|
|
1684
1853
|
}
|
|
1685
1854
|
if (profileName === "statusline") {
|
|
1686
|
-
const statuslineFile =
|
|
1687
|
-
if (
|
|
1688
|
-
|
|
1855
|
+
const statuslineFile = path13.join(target, ".claude", "statusline.sh");
|
|
1856
|
+
if (fs10.existsSync(statuslineFile)) {
|
|
1857
|
+
fs10.rmSync(statuslineFile);
|
|
1689
1858
|
}
|
|
1690
|
-
removeStatuslineFromSettings(
|
|
1859
|
+
removeStatuslineFromSettings(path13.join(target, ".claude"));
|
|
1691
1860
|
}
|
|
1692
1861
|
manifest.profiles = remainingProfiles;
|
|
1693
|
-
manifest.profile_command = remainingProfiles.filter((
|
|
1862
|
+
manifest.profile_command = remainingProfiles.filter((p2) => p2 !== "core").join("+");
|
|
1694
1863
|
return { removed: true, skills: removedSkills, agents: removedAgents };
|
|
1695
1864
|
}
|
|
1696
1865
|
function removeStatuslineFromSettings(claudeDir) {
|
|
1697
|
-
const settingsPath =
|
|
1698
|
-
if (!
|
|
1699
|
-
const settings = JSON.parse(
|
|
1866
|
+
const settingsPath = path13.join(claudeDir, "settings.json");
|
|
1867
|
+
if (!fs10.existsSync(settingsPath)) return;
|
|
1868
|
+
const settings = JSON.parse(fs10.readFileSync(settingsPath, "utf-8"));
|
|
1700
1869
|
if (settings.statusLine) {
|
|
1701
1870
|
delete settings.statusLine;
|
|
1702
|
-
|
|
1871
|
+
fs10.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
1703
1872
|
}
|
|
1704
1873
|
}
|
|
1705
1874
|
|
|
1706
1875
|
// src/commands/list.ts
|
|
1707
|
-
import
|
|
1708
|
-
import
|
|
1709
|
-
import
|
|
1876
|
+
import fs11 from "fs";
|
|
1877
|
+
import path14 from "path";
|
|
1878
|
+
import chalk5 from "chalk";
|
|
1710
1879
|
async function listCommand() {
|
|
1711
1880
|
const source = await resolveContentSource({ quiet: true });
|
|
1712
1881
|
const root = await source.getContentRoot();
|
|
1713
|
-
const profilesDir =
|
|
1714
|
-
const skillsDir =
|
|
1882
|
+
const profilesDir = path14.join(root, "profiles");
|
|
1883
|
+
const skillsDir = path14.join(root, "skills");
|
|
1715
1884
|
const target = process.cwd();
|
|
1716
1885
|
const manifest = readManifest(target);
|
|
1717
1886
|
const profiles = listProfiles(profilesDir);
|
|
1718
|
-
const bases = profiles.filter((p) => p.type === "base");
|
|
1719
|
-
const addons = profiles.filter((p) => p.type === "addon");
|
|
1720
1887
|
const installedSkills = new Set(manifest?.installed_skills ?? []);
|
|
1721
|
-
console.log(
|
|
1722
|
-
console.log(
|
|
1723
|
-
for (const
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
console.log(
|
|
1733
|
-
` ${chalk4.green(("+" + p.name).padEnd(20))} ${p.description}${tag}`
|
|
1734
|
-
);
|
|
1888
|
+
console.log(chalk5.bold("\n=== Available Profiles ===\n"));
|
|
1889
|
+
console.log(chalk5.dim("Profiles combine freely: install a+b+c\n"));
|
|
1890
|
+
for (const group of groupByCategory(profiles)) {
|
|
1891
|
+
console.log(chalk5.cyan(`${group.label}:`));
|
|
1892
|
+
for (const p2 of group.profiles) {
|
|
1893
|
+
const tag = manifest?.profiles.includes(p2.name) ? chalk5.green(" [installed]") : "";
|
|
1894
|
+
console.log(
|
|
1895
|
+
` ${chalk5.green(p2.name.padEnd(20))} ${p2.description}${tag}`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
console.log();
|
|
1735
1899
|
}
|
|
1736
|
-
if (
|
|
1737
|
-
const allSkills =
|
|
1738
|
-
(d) => d.isDirectory() && !d.name.startsWith("_") &&
|
|
1900
|
+
if (fs11.existsSync(skillsDir)) {
|
|
1901
|
+
const allSkills = fs11.readdirSync(skillsDir, { withFileTypes: true }).filter(
|
|
1902
|
+
(d) => d.isDirectory() && !d.name.startsWith("_") && fs11.existsSync(path14.join(skillsDir, d.name, "SKILL.md"))
|
|
1739
1903
|
).map((d) => d.name).sort();
|
|
1740
|
-
console.log(
|
|
1904
|
+
console.log(chalk5.bold(`
|
|
1741
1905
|
=== Skills (${allSkills.length}) ===
|
|
1742
1906
|
`));
|
|
1743
1907
|
const cols = 3;
|
|
1744
1908
|
for (let i = 0; i < allSkills.length; i += cols) {
|
|
1745
1909
|
const row = allSkills.slice(i, i + cols).map((s) => {
|
|
1746
|
-
const tag = installedSkills.has(s) ?
|
|
1910
|
+
const tag = installedSkills.has(s) ? chalk5.green("*") : " ";
|
|
1747
1911
|
return `${tag} ${s.padEnd(30)}`;
|
|
1748
1912
|
});
|
|
1749
1913
|
console.log(" " + row.join(""));
|
|
1750
1914
|
}
|
|
1751
1915
|
if (manifest) {
|
|
1752
|
-
console.log(
|
|
1916
|
+
console.log(chalk5.dim("\n * = installed in current project"));
|
|
1753
1917
|
}
|
|
1754
1918
|
}
|
|
1755
1919
|
}
|
|
1756
1920
|
|
|
1757
1921
|
// src/commands/status.ts
|
|
1758
|
-
import
|
|
1922
|
+
import chalk6 from "chalk";
|
|
1759
1923
|
async function statusCommand() {
|
|
1760
1924
|
const target = process.cwd();
|
|
1761
1925
|
const manifest = readManifest(target);
|
|
1762
1926
|
if (!manifest) {
|
|
1763
1927
|
console.log(
|
|
1764
|
-
|
|
1928
|
+
chalk6.yellow("No Arcane installation found in this project.")
|
|
1765
1929
|
);
|
|
1766
|
-
console.log(
|
|
1930
|
+
console.log(chalk6.dim("Run `npx claude-code-arcane install <profile>` to install."));
|
|
1767
1931
|
return;
|
|
1768
1932
|
}
|
|
1769
|
-
const profiles = manifest.profiles.filter((
|
|
1770
|
-
console.log(
|
|
1771
|
-
console.log(` Profiles: ${
|
|
1933
|
+
const profiles = manifest.profiles.filter((p2) => p2 !== "core");
|
|
1934
|
+
console.log(chalk6.bold("\n=== Arcane Status ===\n"));
|
|
1935
|
+
console.log(` Profiles: ${chalk6.cyan(profiles.join(" + "))}`);
|
|
1772
1936
|
console.log(` Installed: ${manifest.installed_at}`);
|
|
1773
1937
|
console.log(` Version: ${manifest.arcane_version}`);
|
|
1774
1938
|
console.log(` CLI: ${manifest.cli}`);
|
|
1775
|
-
console.log(` Source: ${
|
|
1939
|
+
console.log(` Source: ${chalk6.dim(manifest.source)}`);
|
|
1776
1940
|
if (manifest.worktree) {
|
|
1777
1941
|
const wt = manifest.worktree;
|
|
1778
1942
|
console.log(
|
|
1779
|
-
` Worktree: ${wt.is_worktree ?
|
|
1943
|
+
` Worktree: ${wt.is_worktree ? chalk6.blue("yes") : "no"} (main: ${chalk6.dim(wt.main_worktree)})`
|
|
1780
1944
|
);
|
|
1781
1945
|
if (wt.shared_dirs.length > 0) {
|
|
1782
1946
|
console.log(
|
|
1783
|
-
` Shared: ${
|
|
1947
|
+
` Shared: ${chalk6.blue(wt.shared_dirs.join(", "))} (symlinked)`
|
|
1784
1948
|
);
|
|
1785
1949
|
}
|
|
1786
1950
|
}
|
|
1787
|
-
console.log(
|
|
1951
|
+
console.log(chalk6.bold(`
|
|
1788
1952
|
Skills (${manifest.total_skills}):`));
|
|
1789
1953
|
const skills = manifest.installed_skills ?? [];
|
|
1790
1954
|
for (let i = 0; i < skills.length; i += 5) {
|
|
1791
1955
|
const row = skills.slice(i, i + 5).map((s) => s.padEnd(25)).join("");
|
|
1792
1956
|
console.log(` ${row}`);
|
|
1793
1957
|
}
|
|
1794
|
-
console.log(
|
|
1958
|
+
console.log(chalk6.bold(`
|
|
1795
1959
|
Rules (${manifest.total_rules}):`));
|
|
1796
1960
|
const rules = manifest.installed_rules ?? [];
|
|
1797
1961
|
console.log(` ${rules.join(", ")}`);
|
|
1798
|
-
console.log(
|
|
1962
|
+
console.log(chalk6.bold("\n Agents:"));
|
|
1799
1963
|
const agents = manifest.installed_agents ?? [];
|
|
1800
1964
|
console.log(` ${agents.join(", ")}`);
|
|
1801
1965
|
console.log();
|
|
1802
1966
|
}
|
|
1803
1967
|
|
|
1804
1968
|
// src/commands/update.ts
|
|
1805
|
-
import
|
|
1806
|
-
import
|
|
1969
|
+
import fs12 from "fs";
|
|
1970
|
+
import path15 from "path";
|
|
1807
1971
|
import os4 from "os";
|
|
1808
|
-
import
|
|
1972
|
+
import chalk7 from "chalk";
|
|
1809
1973
|
|
|
1810
1974
|
// src/self-update.ts
|
|
1811
1975
|
import { spawnSync } from "child_process";
|
|
@@ -1867,45 +2031,45 @@ async function updateCommand(opts) {
|
|
|
1867
2031
|
});
|
|
1868
2032
|
if (!opts.quiet) {
|
|
1869
2033
|
if (self.updated) {
|
|
1870
|
-
console.log(
|
|
2034
|
+
console.log(chalk7.green("npm package updated to latest."));
|
|
1871
2035
|
} else if (self.skipped && self.reason !== "dev/test environment") {
|
|
1872
|
-
console.log(
|
|
2036
|
+
console.log(chalk7.dim(`npm self-update skipped (${self.reason}).`));
|
|
1873
2037
|
} else if (!self.skipped) {
|
|
1874
|
-
console.log(
|
|
2038
|
+
console.log(chalk7.yellow(`npm self-update failed: ${self.reason}`));
|
|
1875
2039
|
}
|
|
1876
2040
|
}
|
|
1877
2041
|
const cwd = process.cwd();
|
|
1878
|
-
if (
|
|
2042
|
+
if (fs12.existsSync(manifestPath(cwd))) {
|
|
1879
2043
|
registerInstallation(cwd);
|
|
1880
2044
|
}
|
|
1881
2045
|
const targets = pruneRegistry().map((e) => e.path);
|
|
1882
2046
|
const globalTarget = os4.homedir();
|
|
1883
|
-
if (!process.env.VITEST &&
|
|
2047
|
+
if (!process.env.VITEST && fs12.existsSync(manifestPath(globalTarget)) && !targets.includes(globalTarget)) {
|
|
1884
2048
|
targets.push(globalTarget);
|
|
1885
2049
|
}
|
|
1886
2050
|
if (targets.length === 0) {
|
|
1887
2051
|
if (!opts.quiet) {
|
|
1888
|
-
console.log(
|
|
2052
|
+
console.log(chalk7.red("\nNo Arcane installations found. Run 'arcane install' first."));
|
|
1889
2053
|
}
|
|
1890
2054
|
return;
|
|
1891
2055
|
}
|
|
1892
2056
|
if (!opts.quiet) {
|
|
1893
2057
|
console.log(
|
|
1894
|
-
|
|
2058
|
+
chalk7.bold(`
|
|
1895
2059
|
Updating ${targets.length} installation${targets.length === 1 ? "" : "s"}...`)
|
|
1896
2060
|
);
|
|
1897
2061
|
}
|
|
1898
2062
|
const results = [];
|
|
1899
2063
|
for (const target of targets) {
|
|
1900
2064
|
if (!opts.quiet) {
|
|
1901
|
-
console.log(
|
|
2065
|
+
console.log(chalk7.bold.cyan(`
|
|
1902
2066
|
\u2022 ${target}`));
|
|
1903
2067
|
}
|
|
1904
2068
|
try {
|
|
1905
2069
|
results.push(await updateTarget(target, opts));
|
|
1906
2070
|
} catch (err) {
|
|
1907
2071
|
if (!opts.quiet) {
|
|
1908
|
-
console.log(
|
|
2072
|
+
console.log(chalk7.red(` Update failed: ${err.message}`));
|
|
1909
2073
|
}
|
|
1910
2074
|
results.push({
|
|
1911
2075
|
target,
|
|
@@ -1924,7 +2088,7 @@ async function updateTarget(target, opts) {
|
|
|
1924
2088
|
const manifest = readManifest(target);
|
|
1925
2089
|
if (!manifest) {
|
|
1926
2090
|
if (!opts.quiet) {
|
|
1927
|
-
console.log(
|
|
2091
|
+
console.log(chalk7.red("No Arcane installation found. Run 'arcane install' first."));
|
|
1928
2092
|
}
|
|
1929
2093
|
return { target, status: "no-manifest", updated: 0, skipped: 0, removed: 0 };
|
|
1930
2094
|
}
|
|
@@ -1937,7 +2101,7 @@ async function updateTarget(target, opts) {
|
|
|
1937
2101
|
const installedVersion = manifest.source_version ?? manifest.arcane_version;
|
|
1938
2102
|
if (installedVersion === currentVersion && !opts.force) {
|
|
1939
2103
|
if (!opts.quiet) {
|
|
1940
|
-
console.log(
|
|
2104
|
+
console.log(chalk7.green(`Already up to date (v${currentVersion}).`));
|
|
1941
2105
|
}
|
|
1942
2106
|
return {
|
|
1943
2107
|
target,
|
|
@@ -1951,16 +2115,16 @@ async function updateTarget(target, opts) {
|
|
|
1951
2115
|
}
|
|
1952
2116
|
if (!opts.quiet) {
|
|
1953
2117
|
console.log(
|
|
1954
|
-
|
|
1955
|
-
Update available: ${
|
|
2118
|
+
chalk7.bold(`
|
|
2119
|
+
Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersion)}`)
|
|
1956
2120
|
);
|
|
1957
2121
|
}
|
|
1958
2122
|
const profileNames = manifest.profile_command.split("+").filter(Boolean);
|
|
1959
|
-
const profilesDir =
|
|
2123
|
+
const profilesDir = path15.join(root, "profiles");
|
|
1960
2124
|
const merged = mergeProfiles(profilesDir, profileNames);
|
|
1961
2125
|
const allRules = [...merged.rules.universal, ...merged.rules.gamedev];
|
|
1962
2126
|
const sourceHashes = computeSourceHashes(root, merged.skills, allRules, merged.agents);
|
|
1963
|
-
const claudeDir =
|
|
2127
|
+
const claudeDir = path15.join(target, ".claude");
|
|
1964
2128
|
const installedHashes = computeContentHashes(claudeDir);
|
|
1965
2129
|
const items = computeUpdatePlan(
|
|
1966
2130
|
manifest.content_hashes ?? null,
|
|
@@ -1988,7 +2152,7 @@ Update available: ${chalk6.red(installedVersion)} -> ${chalk6.green(currentVersi
|
|
|
1988
2152
|
});
|
|
1989
2153
|
}
|
|
1990
2154
|
if (!opts.quiet) {
|
|
1991
|
-
console.log(
|
|
2155
|
+
console.log(chalk7.green("\nNo changes to apply."));
|
|
1992
2156
|
}
|
|
1993
2157
|
return {
|
|
1994
2158
|
target,
|
|
@@ -2025,7 +2189,7 @@ Update available: ${chalk6.red(installedVersion)} -> ${chalk6.green(currentVersi
|
|
|
2025
2189
|
total_rules: allRules.length
|
|
2026
2190
|
});
|
|
2027
2191
|
if (!opts.quiet) {
|
|
2028
|
-
console.log(
|
|
2192
|
+
console.log(chalk7.green(`
|
|
2029
2193
|
Updated to v${currentVersion}.`));
|
|
2030
2194
|
console.log(` ${updates.length} updated, ${skipped.length} skipped, ${removed.length} removed`);
|
|
2031
2195
|
}
|
|
@@ -2040,12 +2204,12 @@ Updated to v${currentVersion}.`));
|
|
|
2040
2204
|
};
|
|
2041
2205
|
}
|
|
2042
2206
|
function printGeneralSummary(results, isDryRun) {
|
|
2043
|
-
const prefix = isDryRun ?
|
|
2207
|
+
const prefix = isDryRun ? chalk7.yellow("[dry-run] ") : "";
|
|
2044
2208
|
const updated = results.filter((r) => r.status === "updated" || r.status === "dry-run").length;
|
|
2045
2209
|
const unchanged = results.filter((r) => r.status === "up-to-date" || r.status === "no-changes").length;
|
|
2046
|
-
console.log(
|
|
2210
|
+
console.log(chalk7.bold(`
|
|
2047
2211
|
${prefix}Done. ${results.length} installation${results.length === 1 ? "" : "s"} processed:`));
|
|
2048
|
-
console.log(` ${
|
|
2212
|
+
console.log(` ${chalk7.green(updated)} ${isDryRun ? "would change" : "updated"}, ${chalk7.dim(unchanged + " unchanged")}`);
|
|
2049
2213
|
}
|
|
2050
2214
|
function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
|
|
2051
2215
|
const items = [];
|
|
@@ -2086,7 +2250,7 @@ function resolveAction(manifestHash, installedHash, sourceHash) {
|
|
|
2086
2250
|
return "conflict";
|
|
2087
2251
|
}
|
|
2088
2252
|
function printUpdateSummary(items, isDryRun) {
|
|
2089
|
-
const prefix = isDryRun ?
|
|
2253
|
+
const prefix = isDryRun ? chalk7.yellow("[dry-run] ") : "";
|
|
2090
2254
|
const updates = items.filter((i) => i.action === "update");
|
|
2091
2255
|
const adds = items.filter((i) => i.action === "add");
|
|
2092
2256
|
const conflicts = items.filter((i) => i.action === "conflict");
|
|
@@ -2094,37 +2258,37 @@ function printUpdateSummary(items, isDryRun) {
|
|
|
2094
2258
|
const removed = items.filter((i) => i.action === "remove");
|
|
2095
2259
|
if (updates.length > 0) {
|
|
2096
2260
|
console.log(`
|
|
2097
|
-
${prefix}${
|
|
2261
|
+
${prefix}${chalk7.cyan("Will update:")}`);
|
|
2098
2262
|
for (const item of updates) {
|
|
2099
|
-
console.log(` ${
|
|
2263
|
+
console.log(` ${chalk7.green("~")} ${item.type}: ${item.name}`);
|
|
2100
2264
|
}
|
|
2101
2265
|
}
|
|
2102
2266
|
if (adds.length > 0) {
|
|
2103
2267
|
console.log(`
|
|
2104
|
-
${prefix}${
|
|
2268
|
+
${prefix}${chalk7.cyan("Will add:")}`);
|
|
2105
2269
|
for (const item of adds) {
|
|
2106
|
-
console.log(` ${
|
|
2270
|
+
console.log(` ${chalk7.green("+")} ${item.type}: ${item.name}`);
|
|
2107
2271
|
}
|
|
2108
2272
|
}
|
|
2109
2273
|
if (removed.length > 0) {
|
|
2110
2274
|
console.log(`
|
|
2111
|
-
${prefix}${
|
|
2275
|
+
${prefix}${chalk7.cyan("Will remove:")}`);
|
|
2112
2276
|
for (const item of removed) {
|
|
2113
|
-
console.log(` ${
|
|
2277
|
+
console.log(` ${chalk7.red("-")} ${item.type}: ${item.name}`);
|
|
2114
2278
|
}
|
|
2115
2279
|
}
|
|
2116
2280
|
if (conflicts.length > 0) {
|
|
2117
2281
|
console.log(`
|
|
2118
|
-
${prefix}${
|
|
2282
|
+
${prefix}${chalk7.yellow("Conflicts (will backup + update):")}`);
|
|
2119
2283
|
for (const item of conflicts) {
|
|
2120
|
-
console.log(` ${
|
|
2284
|
+
console.log(` ${chalk7.yellow("!")} ${item.type}: ${item.name}`);
|
|
2121
2285
|
}
|
|
2122
2286
|
}
|
|
2123
2287
|
if (customized.length > 0) {
|
|
2124
2288
|
console.log(`
|
|
2125
|
-
${prefix}${
|
|
2289
|
+
${prefix}${chalk7.dim("Skipped (locally customized):")}`);
|
|
2126
2290
|
for (const item of customized) {
|
|
2127
|
-
console.log(` ${
|
|
2291
|
+
console.log(` ${chalk7.dim(" " + item.type + ": " + item.name)}`);
|
|
2128
2292
|
}
|
|
2129
2293
|
}
|
|
2130
2294
|
}
|
|
@@ -2153,48 +2317,48 @@ function applyUpdates(items, root, claudeDir, merged, force) {
|
|
|
2153
2317
|
function applyItem(item, root, claudeDir) {
|
|
2154
2318
|
const src = getSourcePath(item, root);
|
|
2155
2319
|
const dst = getInstalledPath(item, claudeDir);
|
|
2156
|
-
if (!src || !
|
|
2320
|
+
if (!src || !fs12.existsSync(src)) return;
|
|
2157
2321
|
if (item.type === "skill" || item.type === "agent") {
|
|
2158
|
-
if (
|
|
2159
|
-
|
|
2322
|
+
if (fs12.existsSync(dst)) {
|
|
2323
|
+
fs12.rmSync(dst, { recursive: true, force: true });
|
|
2160
2324
|
}
|
|
2161
2325
|
copyDirSync(src, dst);
|
|
2162
2326
|
} else {
|
|
2163
|
-
const dir =
|
|
2164
|
-
if (!
|
|
2165
|
-
|
|
2327
|
+
const dir = path15.dirname(dst);
|
|
2328
|
+
if (!fs12.existsSync(dir)) {
|
|
2329
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
2166
2330
|
}
|
|
2167
|
-
|
|
2331
|
+
fs12.copyFileSync(src, dst);
|
|
2168
2332
|
}
|
|
2169
2333
|
}
|
|
2170
2334
|
function backupItem(item, claudeDir) {
|
|
2171
2335
|
const installed = getInstalledPath(item, claudeDir);
|
|
2172
|
-
if (!
|
|
2336
|
+
if (!fs12.existsSync(installed)) return;
|
|
2173
2337
|
const backupPath = installed + ".bak";
|
|
2174
|
-
if (
|
|
2175
|
-
|
|
2338
|
+
if (fs12.existsSync(backupPath)) {
|
|
2339
|
+
fs12.rmSync(backupPath, { recursive: true, force: true });
|
|
2176
2340
|
}
|
|
2177
|
-
|
|
2341
|
+
fs12.renameSync(installed, backupPath);
|
|
2178
2342
|
}
|
|
2179
2343
|
function removeItem(item, claudeDir) {
|
|
2180
2344
|
const installed = getInstalledPath(item, claudeDir);
|
|
2181
|
-
if (
|
|
2182
|
-
|
|
2345
|
+
if (fs12.existsSync(installed)) {
|
|
2346
|
+
fs12.rmSync(installed, { recursive: true, force: true });
|
|
2183
2347
|
}
|
|
2184
2348
|
}
|
|
2185
2349
|
function getSourcePath(item, root) {
|
|
2186
2350
|
switch (item.type) {
|
|
2187
2351
|
case "skill":
|
|
2188
|
-
return
|
|
2352
|
+
return path15.join(root, "skills", item.name);
|
|
2189
2353
|
case "rule": {
|
|
2190
|
-
const direct =
|
|
2191
|
-
if (
|
|
2192
|
-
return
|
|
2354
|
+
const direct = path15.join(root, "rules", item.name);
|
|
2355
|
+
if (fs12.existsSync(direct)) return direct;
|
|
2356
|
+
return path15.join(root, "rules", "gamedev", item.name);
|
|
2193
2357
|
}
|
|
2194
2358
|
case "agent":
|
|
2195
|
-
return
|
|
2359
|
+
return path15.join(root, "agents", item.name);
|
|
2196
2360
|
case "hook":
|
|
2197
|
-
return
|
|
2361
|
+
return path15.join(root, "hooks", item.name);
|
|
2198
2362
|
default:
|
|
2199
2363
|
return null;
|
|
2200
2364
|
}
|
|
@@ -2202,26 +2366,26 @@ function getSourcePath(item, root) {
|
|
|
2202
2366
|
function getInstalledPath(item, claudeDir) {
|
|
2203
2367
|
switch (item.type) {
|
|
2204
2368
|
case "skill":
|
|
2205
|
-
return
|
|
2369
|
+
return path15.join(claudeDir, "skills", item.name);
|
|
2206
2370
|
case "rule":
|
|
2207
|
-
return
|
|
2371
|
+
return path15.join(claudeDir, "rules", item.name);
|
|
2208
2372
|
case "agent":
|
|
2209
|
-
return
|
|
2373
|
+
return path15.join(claudeDir, "agents", item.name);
|
|
2210
2374
|
case "hook":
|
|
2211
|
-
return
|
|
2375
|
+
return path15.join(claudeDir, "hooks", item.name);
|
|
2212
2376
|
}
|
|
2213
2377
|
}
|
|
2214
2378
|
|
|
2215
2379
|
// src/commands/clean.ts
|
|
2216
|
-
import
|
|
2380
|
+
import chalk8 from "chalk";
|
|
2217
2381
|
async function cleanCommand(opts) {
|
|
2218
2382
|
const target = process.cwd();
|
|
2219
2383
|
const manifest = readManifest(target);
|
|
2220
2384
|
if (!manifest) {
|
|
2221
|
-
console.log(
|
|
2385
|
+
console.log(chalk8.yellow("No Arcane installation found."));
|
|
2222
2386
|
return;
|
|
2223
2387
|
}
|
|
2224
|
-
console.log(
|
|
2388
|
+
console.log(chalk8.bold("\nThis will remove:"));
|
|
2225
2389
|
console.log(` - ${manifest.total_skills} skills in .claude/skills/`);
|
|
2226
2390
|
console.log(` - ${manifest.total_rules} rules in .claude/rules/`);
|
|
2227
2391
|
console.log(
|
|
@@ -2232,33 +2396,33 @@ async function cleanCommand(opts) {
|
|
|
2232
2396
|
console.log(" - arcane-manifest.json");
|
|
2233
2397
|
if (!opts.force) {
|
|
2234
2398
|
console.log(
|
|
2235
|
-
|
|
2399
|
+
chalk8.yellow("\n Pass --force to confirm removal.\n")
|
|
2236
2400
|
);
|
|
2237
2401
|
return;
|
|
2238
2402
|
}
|
|
2239
2403
|
clean(target);
|
|
2240
2404
|
console.log(
|
|
2241
|
-
|
|
2405
|
+
chalk8.green("\n Run `npx claude-code-arcane install` to reinstall.\n")
|
|
2242
2406
|
);
|
|
2243
2407
|
}
|
|
2244
2408
|
|
|
2245
2409
|
// src/commands/worktree.ts
|
|
2246
|
-
import
|
|
2247
|
-
import
|
|
2410
|
+
import fs13 from "fs";
|
|
2411
|
+
import path16 from "path";
|
|
2248
2412
|
import { execSync as execSync2 } from "child_process";
|
|
2249
|
-
import
|
|
2413
|
+
import chalk9 from "chalk";
|
|
2250
2414
|
async function worktreeCommand(branch, opts) {
|
|
2251
2415
|
const cwd = process.cwd();
|
|
2252
2416
|
const wtInfo = getWorktreeInfo(cwd);
|
|
2253
2417
|
if (!wtInfo) {
|
|
2254
|
-
console.error(
|
|
2418
|
+
console.error(chalk9.red("Not a git repository."));
|
|
2255
2419
|
process.exit(1);
|
|
2256
2420
|
}
|
|
2257
2421
|
const repoRoot = wtInfo.isWorktree ? wtInfo.mainWorktreePath : cwd;
|
|
2258
|
-
const worktreePath = opts.path ?
|
|
2259
|
-
if (
|
|
2422
|
+
const worktreePath = opts.path ? path16.resolve(opts.path) : defaultWorktreePath(repoRoot, branch);
|
|
2423
|
+
if (fs13.existsSync(worktreePath)) {
|
|
2260
2424
|
console.error(
|
|
2261
|
-
|
|
2425
|
+
chalk9.red(`Path already exists: ${worktreePath}`)
|
|
2262
2426
|
);
|
|
2263
2427
|
process.exit(1);
|
|
2264
2428
|
}
|
|
@@ -2268,7 +2432,7 @@ async function worktreeCommand(branch, opts) {
|
|
|
2268
2432
|
if (manifest) {
|
|
2269
2433
|
profileExpr = manifest.profile_command;
|
|
2270
2434
|
console.log(
|
|
2271
|
-
|
|
2435
|
+
chalk9.dim(
|
|
2272
2436
|
` Inheriting profile from current installation: ${profileExpr}`
|
|
2273
2437
|
)
|
|
2274
2438
|
);
|
|
@@ -2276,34 +2440,34 @@ async function worktreeCommand(branch, opts) {
|
|
|
2276
2440
|
}
|
|
2277
2441
|
if (!profileExpr) {
|
|
2278
2442
|
console.error(
|
|
2279
|
-
|
|
2443
|
+
chalk9.red(
|
|
2280
2444
|
"No profile specified and no existing installation to inherit from.\nUse --profile to specify one: npx claude-code-arcane worktree feat/api --profile backend-ts+agile"
|
|
2281
2445
|
)
|
|
2282
2446
|
);
|
|
2283
2447
|
process.exit(1);
|
|
2284
2448
|
}
|
|
2285
|
-
console.log(
|
|
2286
|
-
console.log(` Branch: ${
|
|
2449
|
+
console.log(chalk9.bold("\nCreating worktree:"));
|
|
2450
|
+
console.log(` Branch: ${chalk9.cyan(branch)}`);
|
|
2287
2451
|
console.log(` Path: ${worktreePath}`);
|
|
2288
|
-
console.log(` Profile: ${
|
|
2452
|
+
console.log(` Profile: ${chalk9.green(profileExpr)}`);
|
|
2289
2453
|
if (opts.base) console.log(` Base: ${opts.base}`);
|
|
2290
2454
|
if (opts.dryRun) {
|
|
2291
|
-
console.log(
|
|
2455
|
+
console.log(chalk9.yellow("\n [dry-run] Would create worktree and install Arcane."));
|
|
2292
2456
|
return;
|
|
2293
2457
|
}
|
|
2294
2458
|
console.log("\n Creating git worktree...");
|
|
2295
2459
|
try {
|
|
2296
2460
|
createGitWorktree(repoRoot, worktreePath, branch, opts.base);
|
|
2297
|
-
console.log(
|
|
2461
|
+
console.log(chalk9.green(" [ok] Git worktree created"));
|
|
2298
2462
|
} catch (err) {
|
|
2299
2463
|
console.error(
|
|
2300
|
-
|
|
2464
|
+
chalk9.red(` Failed to create worktree: ${err.message}`)
|
|
2301
2465
|
);
|
|
2302
2466
|
process.exit(1);
|
|
2303
2467
|
}
|
|
2304
2468
|
console.log(" Installing Arcane...");
|
|
2305
2469
|
const root = getPackageRoot();
|
|
2306
|
-
const profilesDir =
|
|
2470
|
+
const profilesDir = path16.join(root, "profiles");
|
|
2307
2471
|
const profileNames = profileExpr.split("+").filter(Boolean);
|
|
2308
2472
|
const merged = mergeProfiles(profilesDir, profileNames);
|
|
2309
2473
|
const shareFrom = opts.share !== false ? cwd : void 0;
|
|
@@ -2318,7 +2482,7 @@ async function worktreeCommand(branch, opts) {
|
|
|
2318
2482
|
main_worktree: repoRoot
|
|
2319
2483
|
});
|
|
2320
2484
|
registerInstallation(worktreePath);
|
|
2321
|
-
console.log(
|
|
2485
|
+
console.log(chalk9.green(" [ok] Arcane installed"));
|
|
2322
2486
|
if (opts.installDeps) {
|
|
2323
2487
|
const pm = detectPackageManager(worktreePath);
|
|
2324
2488
|
if (pm) {
|
|
@@ -2330,22 +2494,22 @@ async function worktreeCommand(branch, opts) {
|
|
|
2330
2494
|
stdio: "pipe",
|
|
2331
2495
|
timeout: 12e4
|
|
2332
2496
|
});
|
|
2333
|
-
console.log(
|
|
2497
|
+
console.log(chalk9.green(` [ok] Dependencies installed`));
|
|
2334
2498
|
} catch {
|
|
2335
2499
|
console.warn(
|
|
2336
|
-
|
|
2500
|
+
chalk9.yellow(" WARN: Dependency install failed. Run manually.")
|
|
2337
2501
|
);
|
|
2338
2502
|
}
|
|
2339
2503
|
}
|
|
2340
2504
|
}
|
|
2341
2505
|
if (opts.isolate) {
|
|
2342
|
-
const isolateScript =
|
|
2506
|
+
const isolateScript = path16.join(
|
|
2343
2507
|
worktreePath,
|
|
2344
2508
|
".claude",
|
|
2345
2509
|
"skills",
|
|
2346
2510
|
"worktree-isolation"
|
|
2347
2511
|
);
|
|
2348
|
-
if (
|
|
2512
|
+
if (fs13.existsSync(isolateScript)) {
|
|
2349
2513
|
console.log(" Applying worktree isolation...");
|
|
2350
2514
|
try {
|
|
2351
2515
|
execSync2(
|
|
@@ -2357,36 +2521,36 @@ async function worktreeCommand(branch, opts) {
|
|
|
2357
2521
|
timeout: 3e4
|
|
2358
2522
|
}
|
|
2359
2523
|
);
|
|
2360
|
-
console.log(
|
|
2524
|
+
console.log(chalk9.green(" [ok] Worktree isolation applied"));
|
|
2361
2525
|
} catch {
|
|
2362
2526
|
console.warn(
|
|
2363
|
-
|
|
2527
|
+
chalk9.yellow(
|
|
2364
2528
|
" WARN: Worktree isolation failed. Run /worktree-isolation manually."
|
|
2365
2529
|
)
|
|
2366
2530
|
);
|
|
2367
2531
|
}
|
|
2368
2532
|
} else {
|
|
2369
2533
|
console.warn(
|
|
2370
|
-
|
|
2534
|
+
chalk9.yellow(
|
|
2371
2535
|
" WARN: worktree-isolation skill not installed. Add it with: npx claude-code-arcane add worktree-isolation"
|
|
2372
2536
|
)
|
|
2373
2537
|
);
|
|
2374
2538
|
}
|
|
2375
2539
|
}
|
|
2376
|
-
console.log(
|
|
2377
|
-
console.log(` ${
|
|
2378
|
-
console.log(` ${
|
|
2540
|
+
console.log(chalk9.bold.green("\nWorktree ready:"));
|
|
2541
|
+
console.log(` ${chalk9.cyan(`cd ${worktreePath}`)}`);
|
|
2542
|
+
console.log(` ${chalk9.dim("claude")}
|
|
2379
2543
|
`);
|
|
2380
2544
|
}
|
|
2381
2545
|
|
|
2382
2546
|
// src/commands/global.ts
|
|
2383
|
-
import
|
|
2384
|
-
import
|
|
2547
|
+
import fs14 from "fs";
|
|
2548
|
+
import path17 from "path";
|
|
2385
2549
|
import os5 from "os";
|
|
2386
|
-
import
|
|
2387
|
-
var CLAUDE_HOME =
|
|
2388
|
-
var SCRIPTS_DIR =
|
|
2389
|
-
var SETTINGS_PATH =
|
|
2550
|
+
import chalk10 from "chalk";
|
|
2551
|
+
var CLAUDE_HOME = path17.join(os5.homedir(), ".claude");
|
|
2552
|
+
var SCRIPTS_DIR = path17.join(CLAUDE_HOME, "scripts", "worktree-isolation");
|
|
2553
|
+
var SETTINGS_PATH = path17.join(CLAUDE_HOME, "settings.json");
|
|
2390
2554
|
async function globalCommand(opts) {
|
|
2391
2555
|
if (opts.status) {
|
|
2392
2556
|
showStatus();
|
|
@@ -2399,100 +2563,100 @@ async function globalCommand(opts) {
|
|
|
2399
2563
|
installGlobal();
|
|
2400
2564
|
}
|
|
2401
2565
|
function showStatus() {
|
|
2402
|
-
console.log(
|
|
2403
|
-
const hasScripts =
|
|
2404
|
-
|
|
2566
|
+
console.log(chalk10.bold("\n=== Arcane Global Status ===\n"));
|
|
2567
|
+
const hasScripts = fs14.existsSync(
|
|
2568
|
+
path17.join(SCRIPTS_DIR, "apply.py")
|
|
2405
2569
|
);
|
|
2406
2570
|
console.log(
|
|
2407
|
-
` Worktree scripts: ${hasScripts ?
|
|
2571
|
+
` Worktree scripts: ${hasScripts ? chalk10.green("installed") : chalk10.dim("not installed")}`
|
|
2408
2572
|
);
|
|
2409
2573
|
if (hasScripts) {
|
|
2410
|
-
console.log(` ${
|
|
2574
|
+
console.log(` ${chalk10.dim(SCRIPTS_DIR)}`);
|
|
2411
2575
|
}
|
|
2412
2576
|
const hookInstalled = isHookInstalled();
|
|
2413
2577
|
console.log(
|
|
2414
|
-
` SessionStart hook: ${hookInstalled ?
|
|
2578
|
+
` SessionStart hook: ${hookInstalled ? chalk10.green("active") : chalk10.dim("not configured")}`
|
|
2415
2579
|
);
|
|
2416
|
-
console.log(` ${
|
|
2580
|
+
console.log(` ${chalk10.dim(SETTINGS_PATH)}`);
|
|
2417
2581
|
console.log();
|
|
2418
2582
|
}
|
|
2419
2583
|
function installGlobal() {
|
|
2420
|
-
console.log(
|
|
2584
|
+
console.log(chalk10.bold("\nInstalling Arcane global hooks...\n"));
|
|
2421
2585
|
const root = getPackageRoot();
|
|
2422
|
-
const scriptsSrc =
|
|
2586
|
+
const scriptsSrc = path17.join(
|
|
2423
2587
|
root,
|
|
2424
2588
|
"skills",
|
|
2425
2589
|
"worktree-isolation",
|
|
2426
2590
|
"scripts"
|
|
2427
2591
|
);
|
|
2428
|
-
if (!
|
|
2592
|
+
if (!fs14.existsSync(scriptsSrc)) {
|
|
2429
2593
|
console.error(
|
|
2430
|
-
|
|
2594
|
+
chalk10.red(" worktree-isolation scripts not found in package.")
|
|
2431
2595
|
);
|
|
2432
2596
|
process.exit(1);
|
|
2433
2597
|
}
|
|
2434
|
-
|
|
2598
|
+
fs14.mkdirSync(SCRIPTS_DIR, { recursive: true });
|
|
2435
2599
|
for (const file of ["apply.py", "lib.py"]) {
|
|
2436
|
-
const src =
|
|
2437
|
-
const dst =
|
|
2438
|
-
if (!
|
|
2439
|
-
console.warn(
|
|
2600
|
+
const src = path17.join(scriptsSrc, file);
|
|
2601
|
+
const dst = path17.join(SCRIPTS_DIR, file);
|
|
2602
|
+
if (!fs14.existsSync(src)) {
|
|
2603
|
+
console.warn(chalk10.yellow(` WARN: ${file} not found, skipping`));
|
|
2440
2604
|
continue;
|
|
2441
2605
|
}
|
|
2442
|
-
if (
|
|
2443
|
-
console.log(
|
|
2606
|
+
if (fs14.existsSync(dst) && fs14.readFileSync(src).equals(fs14.readFileSync(dst))) {
|
|
2607
|
+
console.log(chalk10.dim(` [skip] ${file} (already up-to-date)`));
|
|
2444
2608
|
} else {
|
|
2445
|
-
|
|
2609
|
+
fs14.copyFileSync(src, dst);
|
|
2446
2610
|
if (process.platform !== "win32") {
|
|
2447
|
-
|
|
2611
|
+
fs14.chmodSync(dst, 493);
|
|
2448
2612
|
}
|
|
2449
|
-
console.log(
|
|
2613
|
+
console.log(chalk10.green(` [ok] ${file} -> ${SCRIPTS_DIR}`));
|
|
2450
2614
|
}
|
|
2451
2615
|
}
|
|
2452
2616
|
if (isHookInstalled()) {
|
|
2453
2617
|
console.log(
|
|
2454
|
-
|
|
2618
|
+
chalk10.dim(" [skip] SessionStart hook (already configured)")
|
|
2455
2619
|
);
|
|
2456
2620
|
} else {
|
|
2457
2621
|
addHookToSettings();
|
|
2458
2622
|
console.log(
|
|
2459
|
-
|
|
2623
|
+
chalk10.green(" [ok] SessionStart hook added to ~/.claude/settings.json")
|
|
2460
2624
|
);
|
|
2461
2625
|
}
|
|
2462
|
-
console.log(
|
|
2626
|
+
console.log(chalk10.bold.green("\n Global hooks installed."));
|
|
2463
2627
|
console.log(
|
|
2464
|
-
|
|
2628
|
+
chalk10.dim(
|
|
2465
2629
|
" Worktree isolation will auto-apply on every new Claude Code session.\n"
|
|
2466
2630
|
)
|
|
2467
2631
|
);
|
|
2468
2632
|
}
|
|
2469
2633
|
function removeGlobal() {
|
|
2470
|
-
console.log(
|
|
2471
|
-
if (
|
|
2472
|
-
|
|
2473
|
-
console.log(
|
|
2634
|
+
console.log(chalk10.bold("\nRemoving Arcane global hooks...\n"));
|
|
2635
|
+
if (fs14.existsSync(SCRIPTS_DIR)) {
|
|
2636
|
+
fs14.rmSync(SCRIPTS_DIR, { recursive: true, force: true });
|
|
2637
|
+
console.log(chalk10.green(" [ok] Removed worktree-isolation scripts"));
|
|
2474
2638
|
} else {
|
|
2475
|
-
console.log(
|
|
2639
|
+
console.log(chalk10.dim(" [skip] Scripts not found"));
|
|
2476
2640
|
}
|
|
2477
2641
|
if (isHookInstalled()) {
|
|
2478
2642
|
removeHookFromSettings();
|
|
2479
|
-
console.log(
|
|
2643
|
+
console.log(chalk10.green(" [ok] Removed SessionStart hook"));
|
|
2480
2644
|
} else {
|
|
2481
|
-
console.log(
|
|
2645
|
+
console.log(chalk10.dim(" [skip] Hook not configured"));
|
|
2482
2646
|
}
|
|
2483
|
-
console.log(
|
|
2647
|
+
console.log(chalk10.bold.green("\n Global hooks removed.\n"));
|
|
2484
2648
|
}
|
|
2485
2649
|
function readSettings() {
|
|
2486
|
-
if (!
|
|
2650
|
+
if (!fs14.existsSync(SETTINGS_PATH)) return {};
|
|
2487
2651
|
try {
|
|
2488
|
-
return JSON.parse(
|
|
2652
|
+
return JSON.parse(fs14.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
2489
2653
|
} catch {
|
|
2490
2654
|
return {};
|
|
2491
2655
|
}
|
|
2492
2656
|
}
|
|
2493
2657
|
function writeSettings(settings) {
|
|
2494
|
-
|
|
2495
|
-
|
|
2658
|
+
fs14.mkdirSync(path17.dirname(SETTINGS_PATH), { recursive: true });
|
|
2659
|
+
fs14.writeFileSync(
|
|
2496
2660
|
SETTINGS_PATH,
|
|
2497
2661
|
JSON.stringify(settings, null, 2) + "\n",
|
|
2498
2662
|
"utf-8"
|
|
@@ -2549,11 +2713,11 @@ function removeHookFromSettings() {
|
|
|
2549
2713
|
}
|
|
2550
2714
|
|
|
2551
2715
|
// src/update-check.ts
|
|
2552
|
-
import
|
|
2553
|
-
import
|
|
2716
|
+
import fs15 from "fs";
|
|
2717
|
+
import path18 from "path";
|
|
2554
2718
|
import os6 from "os";
|
|
2555
|
-
import
|
|
2556
|
-
var CHECK_FILE =
|
|
2719
|
+
import chalk11 from "chalk";
|
|
2720
|
+
var CHECK_FILE = path18.join(os6.homedir(), ".arcane", "last-check.json");
|
|
2557
2721
|
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
2558
2722
|
var GITHUB_OWNER2 = "SebastianLuser";
|
|
2559
2723
|
var GITHUB_REPO2 = "Claude-Code-Arcane";
|
|
@@ -2593,11 +2757,11 @@ async function checkForUpdates(opts = {}) {
|
|
|
2593
2757
|
}
|
|
2594
2758
|
function printUpdateNotice(localVersion, remoteSha) {
|
|
2595
2759
|
console.log(
|
|
2596
|
-
|
|
2760
|
+
chalk11.yellow(`
|
|
2597
2761
|
Arcane update available: ${localVersion} \u2192 ${remoteSha}`)
|
|
2598
2762
|
);
|
|
2599
2763
|
console.log(
|
|
2600
|
-
|
|
2764
|
+
chalk11.dim(" Run: arcane update (--dry-run to preview)\n")
|
|
2601
2765
|
);
|
|
2602
2766
|
}
|
|
2603
2767
|
async function getLatestCommitSha() {
|
|
@@ -2621,19 +2785,19 @@ async function getLatestCommitSha() {
|
|
|
2621
2785
|
}
|
|
2622
2786
|
function readCachedCheck() {
|
|
2623
2787
|
try {
|
|
2624
|
-
if (!
|
|
2625
|
-
return JSON.parse(
|
|
2788
|
+
if (!fs15.existsSync(CHECK_FILE)) return null;
|
|
2789
|
+
return JSON.parse(fs15.readFileSync(CHECK_FILE, "utf-8"));
|
|
2626
2790
|
} catch {
|
|
2627
2791
|
return null;
|
|
2628
2792
|
}
|
|
2629
2793
|
}
|
|
2630
2794
|
function writeCachedCheck(result) {
|
|
2631
2795
|
try {
|
|
2632
|
-
const dir =
|
|
2633
|
-
if (!
|
|
2634
|
-
|
|
2796
|
+
const dir = path18.dirname(CHECK_FILE);
|
|
2797
|
+
if (!fs15.existsSync(dir)) {
|
|
2798
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
2635
2799
|
}
|
|
2636
|
-
|
|
2800
|
+
fs15.writeFileSync(CHECK_FILE, JSON.stringify(result, null, 2) + "\n");
|
|
2637
2801
|
} catch {
|
|
2638
2802
|
}
|
|
2639
2803
|
}
|
|
@@ -2644,7 +2808,7 @@ program.name("arcane").description(
|
|
|
2644
2808
|
"Claude Code Arcane \u2014 Skills, agents, hooks and rules for Claude Code"
|
|
2645
2809
|
).version(getPackageVersion());
|
|
2646
2810
|
program.command("install [profile]").description(
|
|
2647
|
-
"Install
|
|
2811
|
+
"Install profiles (e.g., backend-ts+agile). Without args, opens an interactive picker."
|
|
2648
2812
|
).option("-t, --target <dir>", "Target project directory", process.cwd()).option("-n, --dry-run", "Show what would be installed without changing files").option("-f, --force", "Overwrite without backup prompt").option(
|
|
2649
2813
|
"-s, --share-from <dir>",
|
|
2650
2814
|
"Share hooks/docs from an existing Arcane installation (e.g., main worktree)"
|