arcane-cli 1.12.0 → 1.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -6,25 +6,296 @@ const require = __createRequire(import.meta.url);
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { createRequire as createRequire3 } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as
|
|
9
|
+
import { dirname as dirname13, join as join23 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { select as select3, confirm as confirm2, input as input3 } from "@inquirer/prompts";
|
|
13
|
-
import { dirname as
|
|
13
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
14
14
|
import chalk2 from "chalk";
|
|
15
15
|
|
|
16
16
|
// src/modules/copier.ts
|
|
17
17
|
import {
|
|
18
18
|
mkdir,
|
|
19
19
|
access,
|
|
20
|
+
lstat,
|
|
20
21
|
readdir,
|
|
21
22
|
readFile,
|
|
22
23
|
rm,
|
|
23
24
|
rmdir,
|
|
25
|
+
unlink,
|
|
24
26
|
writeFile
|
|
25
27
|
} from "fs/promises";
|
|
26
28
|
import { createHash } from "crypto";
|
|
27
29
|
import path from "path";
|
|
30
|
+
|
|
31
|
+
// src/modules/git.ts
|
|
32
|
+
import { execFile } from "child_process";
|
|
33
|
+
var GitTimeoutError = class extends Error {
|
|
34
|
+
constructor(message, options) {
|
|
35
|
+
super(message, options);
|
|
36
|
+
this.name = "GitTimeoutError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set(["fetch", "pull", "push", "clone", "ls-remote", "send-email"]);
|
|
40
|
+
var WRITE_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
41
|
+
"init",
|
|
42
|
+
"commit",
|
|
43
|
+
"add",
|
|
44
|
+
"checkout",
|
|
45
|
+
"switch",
|
|
46
|
+
"merge",
|
|
47
|
+
"rebase",
|
|
48
|
+
"reset",
|
|
49
|
+
"tag",
|
|
50
|
+
"mv",
|
|
51
|
+
"rm",
|
|
52
|
+
"cherry-pick",
|
|
53
|
+
"revert",
|
|
54
|
+
"stash",
|
|
55
|
+
"apply",
|
|
56
|
+
"am",
|
|
57
|
+
"restore",
|
|
58
|
+
"submodule",
|
|
59
|
+
"sparse-checkout",
|
|
60
|
+
"maintenance",
|
|
61
|
+
"notes"
|
|
62
|
+
]);
|
|
63
|
+
var AMBIGUOUS_AS_WRITE = /* @__PURE__ */ new Set(["branch", "config", "remote", "worktree"]);
|
|
64
|
+
var READ_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
65
|
+
"status",
|
|
66
|
+
"rev-parse",
|
|
67
|
+
"log",
|
|
68
|
+
"diff",
|
|
69
|
+
"show",
|
|
70
|
+
"describe",
|
|
71
|
+
"cat-file",
|
|
72
|
+
"ls-files",
|
|
73
|
+
"ls-tree",
|
|
74
|
+
"symbolic-ref",
|
|
75
|
+
"show-ref",
|
|
76
|
+
"for-each-ref",
|
|
77
|
+
"merge-base",
|
|
78
|
+
"blame",
|
|
79
|
+
"shortlog",
|
|
80
|
+
"grep",
|
|
81
|
+
"reflog",
|
|
82
|
+
"diff-tree",
|
|
83
|
+
"rev-list",
|
|
84
|
+
"name-rev",
|
|
85
|
+
"count-objects",
|
|
86
|
+
"verify-commit",
|
|
87
|
+
"verify-tag",
|
|
88
|
+
"help",
|
|
89
|
+
"version"
|
|
90
|
+
]);
|
|
91
|
+
var GLOBAL_FLAGS_WITH_SEPARATE_VALUE = /* @__PURE__ */ new Set(["-c", "-C"]);
|
|
92
|
+
var DEFAULT_TIMEOUTS_MS = {
|
|
93
|
+
read: 15e3,
|
|
94
|
+
write: 3e4,
|
|
95
|
+
network: 12e4
|
|
96
|
+
};
|
|
97
|
+
function classifyGitCommand(args) {
|
|
98
|
+
let subcommand;
|
|
99
|
+
for (let i = 0; i < args.length; i++) {
|
|
100
|
+
const arg = args[i];
|
|
101
|
+
if (GLOBAL_FLAGS_WITH_SEPARATE_VALUE.has(arg)) {
|
|
102
|
+
i++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg.startsWith("-")) continue;
|
|
106
|
+
subcommand = arg;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (!subcommand) return "read";
|
|
110
|
+
if (NETWORK_SUBCOMMANDS.has(subcommand)) return "network";
|
|
111
|
+
if (WRITE_SUBCOMMANDS.has(subcommand) || AMBIGUOUS_AS_WRITE.has(subcommand)) return "write";
|
|
112
|
+
if (READ_SUBCOMMANDS.has(subcommand)) return "read";
|
|
113
|
+
return "write";
|
|
114
|
+
}
|
|
115
|
+
function buildGitEnv() {
|
|
116
|
+
return {
|
|
117
|
+
...process.env,
|
|
118
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
119
|
+
GCM_INTERACTIVE: "Never",
|
|
120
|
+
GIT_OPTIONAL_LOCKS: "0"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function runGit(cwd, args, options = {}) {
|
|
124
|
+
const commandClass = options.commandClass ?? classifyGitCommand(args);
|
|
125
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
|
|
126
|
+
return new Promise((resolve6, reject) => {
|
|
127
|
+
const child = execFile(
|
|
128
|
+
"git",
|
|
129
|
+
args,
|
|
130
|
+
{
|
|
131
|
+
cwd,
|
|
132
|
+
env: buildGitEnv(),
|
|
133
|
+
timeout: timeoutMs,
|
|
134
|
+
// SIGTERM (Node's default) rather than SIGKILL: gives git a chance
|
|
135
|
+
// to run its own cleanup on POSIX, relevant to EF-20's own concern
|
|
136
|
+
// about a killed operation leaving refs/locks in an uncertain state.
|
|
137
|
+
// Moot on Windows -- Node docs: both map to an abrupt TerminateProcess
|
|
138
|
+
// there -- but this module isn't Windows-gated.
|
|
139
|
+
killSignal: "SIGTERM",
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
encoding: "utf8"
|
|
142
|
+
},
|
|
143
|
+
(error, stdout, stderr) => {
|
|
144
|
+
if (error) {
|
|
145
|
+
if (error.killed) {
|
|
146
|
+
reject(
|
|
147
|
+
new GitTimeoutError(
|
|
148
|
+
// NOTE: embeds the raw args, including any secrets a future
|
|
149
|
+
// network-class caller might pass (e.g. a credentialed
|
|
150
|
+
// clone URL). No current caller does; redact before one does.
|
|
151
|
+
`git ${args.join(" ")} timed out after ${timeoutMs}ms (class: ${commandClass})`,
|
|
152
|
+
{ cause: error }
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
reject(error);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
resolve6({ stdout, stderr });
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
child.stdin?.end();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function inspectGitRepository(cwd) {
|
|
167
|
+
try {
|
|
168
|
+
const { stdout: stdout2 } = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
169
|
+
if (stdout2.trim() !== "true") {
|
|
170
|
+
return { status: "not-repository" };
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
return { status: "not-repository" };
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
|
|
177
|
+
} catch {
|
|
178
|
+
return { status: "no-commits" };
|
|
179
|
+
}
|
|
180
|
+
const { stdout } = await runGit(cwd, ["status", "--porcelain", "-uall"]);
|
|
181
|
+
const lines = stdout.trim();
|
|
182
|
+
return {
|
|
183
|
+
status: "ready",
|
|
184
|
+
uncommittedChanges: lines.length === 0 ? 0 : lines.split("\n").length
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async function correctUnbornMasterDefault(cwd) {
|
|
188
|
+
let current;
|
|
189
|
+
try {
|
|
190
|
+
const { stdout } = await runGit(cwd, ["symbolic-ref", "--short", "HEAD"]);
|
|
191
|
+
current = stdout.trim();
|
|
192
|
+
} catch {
|
|
193
|
+
return { corrected: false, to: "main" };
|
|
194
|
+
}
|
|
195
|
+
if (current !== "master") {
|
|
196
|
+
return { corrected: false, to: "main" };
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
|
|
200
|
+
return { corrected: false, to: "main" };
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
let mainRef;
|
|
204
|
+
try {
|
|
205
|
+
mainRef = await runGit(cwd, ["for-each-ref", "--format=%(objectname)", "refs/heads/main"]);
|
|
206
|
+
} catch {
|
|
207
|
+
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
208
|
+
}
|
|
209
|
+
if (mainRef.stdout.trim() !== "") {
|
|
210
|
+
return { corrected: false, to: "main" };
|
|
211
|
+
}
|
|
212
|
+
if (mainRef.stderr.trim() !== "") {
|
|
213
|
+
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await runGit(cwd, ["symbolic-ref", "-q", "refs/heads/main"]);
|
|
217
|
+
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error.code !== 1) {
|
|
220
|
+
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
await runGit(cwd, ["symbolic-ref", "HEAD", "refs/heads/main"]);
|
|
224
|
+
return { corrected: true, from: current, to: "main" };
|
|
225
|
+
}
|
|
226
|
+
async function ensureLocalPullRebase(cwd) {
|
|
227
|
+
let rawLocalValue;
|
|
228
|
+
try {
|
|
229
|
+
const { stdout } = await runGit(cwd, ["config", "--local", "--get", "pull.rebase"]);
|
|
230
|
+
rawLocalValue = stdout.trim();
|
|
231
|
+
} catch {
|
|
232
|
+
rawLocalValue = void 0;
|
|
233
|
+
}
|
|
234
|
+
if (rawLocalValue === void 0) {
|
|
235
|
+
await runGit(cwd, ["config", "--local", "pull.rebase", "true"]);
|
|
236
|
+
return { action: "set" };
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const { stdout } = await runGit(cwd, ["config", "--local", "--type=bool", "--get", "pull.rebase"]);
|
|
240
|
+
if (stdout.trim() === "false") {
|
|
241
|
+
return { action: "explicit-false-preserved" };
|
|
242
|
+
}
|
|
243
|
+
return { action: "already-set" };
|
|
244
|
+
} catch {
|
|
245
|
+
return { action: "already-set" };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function listSymlinkTrackedPaths(cwd) {
|
|
249
|
+
const paths = /* @__PURE__ */ new Set();
|
|
250
|
+
let stdout;
|
|
251
|
+
try {
|
|
252
|
+
({ stdout } = await runGit(cwd, ["ls-files", "-s", "-z"]));
|
|
253
|
+
} catch {
|
|
254
|
+
return paths;
|
|
255
|
+
}
|
|
256
|
+
for (const entry of stdout.split("\0")) {
|
|
257
|
+
const tab = entry.indexOf(" ");
|
|
258
|
+
if (tab < 0) continue;
|
|
259
|
+
const [mode] = entry.slice(0, tab).split(" ");
|
|
260
|
+
if (mode === "120000") paths.add(entry.slice(tab + 1));
|
|
261
|
+
}
|
|
262
|
+
return paths;
|
|
263
|
+
}
|
|
264
|
+
async function recordRegularFileInIndex(cwd, relativePath) {
|
|
265
|
+
const { stdout } = await runGit(cwd, ["hash-object", "-w", "--", relativePath]);
|
|
266
|
+
const { stdout: prefix } = await runGit(cwd, ["rev-parse", "--show-prefix"]);
|
|
267
|
+
const rootRelative = `${prefix.trim()}${relativePath.replace(/\\/g, "/")}`;
|
|
268
|
+
await runGit(cwd, ["update-index", "--add", "--cacheinfo", `100644,${stdout.trim()},${rootRelative}`]);
|
|
269
|
+
}
|
|
270
|
+
async function isUneditedTrackedFile(cwd, relativePath) {
|
|
271
|
+
let status;
|
|
272
|
+
try {
|
|
273
|
+
({ stdout: status } = await runGit(cwd, ["status", "--porcelain", "-z", "--", relativePath]));
|
|
274
|
+
} catch {
|
|
275
|
+
return { verdict: "unknown", detail: "not a git repository, or git is unavailable" };
|
|
276
|
+
}
|
|
277
|
+
if (status.startsWith("??")) return { verdict: "untracked", detail: "git does not track it" };
|
|
278
|
+
if (status.length > 0) return { verdict: "edited", detail: "it has uncommitted changes" };
|
|
279
|
+
try {
|
|
280
|
+
const { stdout: shallow } = await runGit(cwd, ["rev-parse", "--is-shallow-repository"]);
|
|
281
|
+
if (shallow.trim() === "true") {
|
|
282
|
+
return { verdict: "unknown", detail: "this is a shallow clone, so the history is truncated" };
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
let log;
|
|
287
|
+
try {
|
|
288
|
+
({ stdout: log } = await runGit(cwd, ["log", "--format=%H", "--", relativePath]));
|
|
289
|
+
} catch {
|
|
290
|
+
return { verdict: "unknown", detail: "git log could not read its history" };
|
|
291
|
+
}
|
|
292
|
+
const commits = log.split("\n").filter(Boolean).length;
|
|
293
|
+
if (commits === 0) return { verdict: "untracked", detail: "no commit contains it" };
|
|
294
|
+
if (commits > 1) return { verdict: "edited", detail: `${commits} commits touched it` };
|
|
295
|
+
return { verdict: "unedited", detail: "tracked, clean, and added by its only commit" };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/modules/copier.ts
|
|
28
299
|
function normalizeLineEndings(content) {
|
|
29
300
|
if (!content.includes(13)) return content;
|
|
30
301
|
const out = Buffer.allocUnsafe(content.length);
|
|
@@ -72,7 +343,52 @@ async function fileExists(filePath) {
|
|
|
72
343
|
return false;
|
|
73
344
|
}
|
|
74
345
|
}
|
|
75
|
-
async function
|
|
346
|
+
async function replaceSymlinkedDestination(destPath, relativePath, opts) {
|
|
347
|
+
let isLink;
|
|
348
|
+
try {
|
|
349
|
+
isLink = (await lstat(destPath)).isSymbolicLink();
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
if (isLink) {
|
|
354
|
+
await unlink(destPath);
|
|
355
|
+
return "symlink";
|
|
356
|
+
}
|
|
357
|
+
if (opts.linkTracked?.has(relativePath.replace(/\\/g, "/"))) {
|
|
358
|
+
await unlink(destPath);
|
|
359
|
+
return "index-symlink";
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function looksLikeBarePath(body) {
|
|
364
|
+
const trimmed = body.trim();
|
|
365
|
+
return trimmed.length <= 512 && /^\.{1,2}\/\S+$/.test(trimmed) && !trimmed.includes("://");
|
|
366
|
+
}
|
|
367
|
+
async function managedPathLinkShape(targetDir, relativePath, linkTracked, bodyHeuristic) {
|
|
368
|
+
const destPath = path.resolve(targetDir, relativePath);
|
|
369
|
+
try {
|
|
370
|
+
if ((await lstat(destPath)).isSymbolicLink()) return "symlink";
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
if (linkTracked?.has(relativePath.replace(/\\/g, "/"))) return "index-symlink";
|
|
375
|
+
if (!bodyHeuristic) return null;
|
|
376
|
+
try {
|
|
377
|
+
return looksLikeBarePath(await readFile(destPath, "utf-8")) ? "path-body" : null;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async function writeManagedFile(targetDir, relativePath, content, opts = {}) {
|
|
383
|
+
validateTargetPath(targetDir, relativePath);
|
|
384
|
+
const destPath = path.resolve(targetDir, relativePath);
|
|
385
|
+
await ensureDir(path.dirname(destPath));
|
|
386
|
+
const shape = await replaceSymlinkedDestination(destPath, relativePath, opts);
|
|
387
|
+
await writeFile(destPath, content);
|
|
388
|
+
if (shape === "index-symlink") await recordRegularFileInIndex(targetDir, relativePath);
|
|
389
|
+
if (shape !== null) opts.conversions?.push({ file: relativePath, shape });
|
|
390
|
+
}
|
|
391
|
+
async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
|
|
76
392
|
validateTargetPath(targetDir, relativePath);
|
|
77
393
|
const destPath = path.resolve(targetDir, relativePath);
|
|
78
394
|
await ensureDir(path.dirname(destPath));
|
|
@@ -83,7 +399,7 @@ async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
|
|
|
83
399
|
}
|
|
84
400
|
const content = await readFile(srcPath);
|
|
85
401
|
const hash = hashContent(content);
|
|
86
|
-
await
|
|
402
|
+
await writeManagedFile(targetDir, relativePath, content, opts);
|
|
87
403
|
return hash;
|
|
88
404
|
}
|
|
89
405
|
async function removeWithin(targetDir, relativePath) {
|
|
@@ -976,7 +1292,7 @@ function listProfiles() {
|
|
|
976
1292
|
}
|
|
977
1293
|
|
|
978
1294
|
// src/modules/user-tier.ts
|
|
979
|
-
import { lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, rmdir as rmdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1295
|
+
import { lstat as lstat2, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, rmdir as rmdir2, writeFile as writeFile3 } from "fs/promises";
|
|
980
1296
|
import { homedir } from "os";
|
|
981
1297
|
import { dirname, join, resolve } from "path";
|
|
982
1298
|
|
|
@@ -1192,7 +1508,7 @@ async function renderUserFanout(storeRoot, spellIds, fallbackDir) {
|
|
|
1192
1508
|
}
|
|
1193
1509
|
async function classifyTarget(path4) {
|
|
1194
1510
|
try {
|
|
1195
|
-
const stats = await
|
|
1511
|
+
const stats = await lstat2(path4);
|
|
1196
1512
|
return stats.isFile() ? "file" : "other";
|
|
1197
1513
|
} catch (err) {
|
|
1198
1514
|
if (err.code === "ENOENT") return "absent";
|
|
@@ -1417,7 +1733,7 @@ var WORDMARK = [
|
|
|
1417
1733
|
var INDENT = " ";
|
|
1418
1734
|
var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
|
|
1419
1735
|
function sleep(ms) {
|
|
1420
|
-
return new Promise((
|
|
1736
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1421
1737
|
}
|
|
1422
1738
|
function isAnimatable() {
|
|
1423
1739
|
return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
|
|
@@ -2194,7 +2510,10 @@ ${rules}
|
|
|
2194
2510
|
**Denied:** ${def.tools.denied.join(", ")}
|
|
2195
2511
|
`;
|
|
2196
2512
|
}
|
|
2197
|
-
function renderSpellRoutingSection() {
|
|
2513
|
+
function renderSpellRoutingSection(spellScope) {
|
|
2514
|
+
const userTierRule = spellScope === "user" ? `
|
|
2515
|
+
This repository takes its spells from the user tier (\`~/.arcane\`), not from files it carries. If a spell named here does not resolve for your client, stop and say that \`spell init --user\` is needed on this machine; do not improvise the workflow from general knowledge.
|
|
2516
|
+
` : "";
|
|
2198
2517
|
return `## Spell Routing
|
|
2199
2518
|
|
|
2200
2519
|
| When you're about to... | Invoke |
|
|
@@ -2208,7 +2527,7 @@ function renderSpellRoutingSection() {
|
|
|
2208
2527
|
| Review code or a PR | \`spell-review\` |
|
|
2209
2528
|
|
|
2210
2529
|
If a spell exists for the workflow you are about to perform, invoke it \u2014 do not improvise the workflow from general knowledge, even when the user doesn't name the spell.
|
|
2211
|
-
`;
|
|
2530
|
+
${userTierRule}`;
|
|
2212
2531
|
}
|
|
2213
2532
|
async function loadBranchNamingFragment(assetsDir) {
|
|
2214
2533
|
try {
|
|
@@ -2370,7 +2689,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2370
2689
|
unmanagedAgentFiles.push(...await existingAgentFiles(agentsDir));
|
|
2371
2690
|
}
|
|
2372
2691
|
if (!userTier) {
|
|
2373
|
-
const copilotSection = `${renderSpellRoutingSection()}
|
|
2692
|
+
const copilotSection = `${renderSpellRoutingSection(options.spellScope)}
|
|
2374
2693
|
${branchNaming}${renderAgentRosterSection(resolved)}`;
|
|
2375
2694
|
const copilotMerged = await mergeIntoFile(
|
|
2376
2695
|
targetDir,
|
|
@@ -2386,7 +2705,7 @@ ${branchNaming}${renderAgentRosterSection(resolved)}`;
|
|
|
2386
2705
|
return { synced, skipped, hasUnresolvedRoles, userAgentFiles, unmanagedAgentFiles };
|
|
2387
2706
|
}
|
|
2388
2707
|
if (options.claude !== false) {
|
|
2389
|
-
const claudeSection = `${renderSpellRoutingSection()}
|
|
2708
|
+
const claudeSection = `${renderSpellRoutingSection(options.spellScope)}
|
|
2390
2709
|
${branchNaming}${renderAgentRosterSection(resolved)}`;
|
|
2391
2710
|
const claudeMerged = await mergeIntoFile(
|
|
2392
2711
|
targetDir,
|
|
@@ -2397,7 +2716,7 @@ ${branchNaming}${renderAgentRosterSection(resolved)}`;
|
|
|
2397
2716
|
if (claudeMerged) synced.push("CLAUDE.md (agents section)");
|
|
2398
2717
|
}
|
|
2399
2718
|
if (options.codex !== false) {
|
|
2400
|
-
const codexSection = `${renderSpellRoutingSection()}
|
|
2719
|
+
const codexSection = `${renderSpellRoutingSection(options.spellScope)}
|
|
2401
2720
|
${branchNaming}${renderAgentRosterSection(resolved)}`;
|
|
2402
2721
|
const codexMerged = await mergeIntoFile(
|
|
2403
2722
|
targetDir,
|
|
@@ -2604,374 +2923,156 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
2604
2923
|
}
|
|
2605
2924
|
for (const roleId of selectedRoleIds) {
|
|
2606
2925
|
const src = join4(assetsDir, "agents", `${roleId}.yaml`);
|
|
2607
|
-
const dest = join4(arcaneDir, `${roleId}.yaml`);
|
|
2608
|
-
if (!options.dryRun) {
|
|
2609
|
-
try {
|
|
2610
|
-
await copyFile2(src, dest);
|
|
2611
|
-
} catch {
|
|
2612
|
-
console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
|
|
2613
|
-
}
|
|
2614
|
-
} else {
|
|
2615
|
-
console.log(` [dry-run] Would copy: ${label2}/agents/${roleId}.yaml`);
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
if (options.dryRun) {
|
|
2619
|
-
console.log(
|
|
2620
|
-
`
|
|
2621
|
-
[dry-run] Would initialize roster with ${selectedRoleIds.length} agents`
|
|
2622
|
-
);
|
|
2623
|
-
return;
|
|
2624
|
-
}
|
|
2625
|
-
printStep("Syncing agent definitions to all clients...");
|
|
2626
|
-
const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
|
|
2627
|
-
targetDir,
|
|
2628
|
-
assetsDir,
|
|
2629
|
-
roster,
|
|
2630
|
-
{ dryRun: false, scope }
|
|
2631
|
-
);
|
|
2632
|
-
printSuccess("Agent roster initialized");
|
|
2633
|
-
console.log(` \u{1F4CB} Profile: ${profileId} \xB7 Naming: ${namingStrategy} \xB7 Agents: ${rosterEntries.length}`);
|
|
2634
|
-
console.log(
|
|
2635
|
-
` \u{1F504} Synced: ${synced.length} outputs (${scope === "user" ? "agent personas in ~/.claude/agents" : "Copilot, Claude, Codex, OpenClaw"})`
|
|
2636
|
-
);
|
|
2637
|
-
if (skipped.length > 0) {
|
|
2638
|
-
console.log(` \u26A0\uFE0F Skipped: ${skipped.length} (${skipped.join(", ")})`);
|
|
2639
|
-
}
|
|
2640
|
-
console.log();
|
|
2641
|
-
console.log(
|
|
2642
|
-
` \u{1F4A1} Customize: edit ${label2}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
|
|
2643
|
-
);
|
|
2644
|
-
console.log();
|
|
2645
|
-
if (hasUnresolvedRoles) {
|
|
2646
|
-
console.error(
|
|
2647
|
-
' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
|
|
2648
|
-
);
|
|
2649
|
-
process.exitCode = 1;
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
async function runAgentsSync(options, targetDir, assetsDir) {
|
|
2653
|
-
const scope = options.scope ?? "repo";
|
|
2654
|
-
let roster;
|
|
2655
|
-
let definitionsDir;
|
|
2656
|
-
let fromUserTier = false;
|
|
2657
|
-
try {
|
|
2658
|
-
({ roster, definitionsDir, fromUserTier } = await resolveRoster(targetDir, scope));
|
|
2659
|
-
} catch (err) {
|
|
2660
|
-
if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
|
|
2661
|
-
console.error(err.message);
|
|
2662
|
-
process.exit(1);
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
throw err;
|
|
2666
|
-
}
|
|
2667
|
-
if (options.dryRun) {
|
|
2668
|
-
console.log("\n [dry-run] Would sync agents from roster:\n");
|
|
2669
|
-
} else {
|
|
2670
|
-
console.log("\n Syncing agents...\n");
|
|
2671
|
-
}
|
|
2672
|
-
if (fromUserTier && scope === "repo") {
|
|
2673
|
-
console.log(
|
|
2674
|
-
" This repository has no roster of its own and takes its spells from the user tier,\n so its roster tables are built from ~/.arcane/agents.yaml.\n"
|
|
2675
|
-
);
|
|
2676
|
-
}
|
|
2677
|
-
const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
|
|
2678
|
-
targetDir,
|
|
2679
|
-
assetsDir,
|
|
2680
|
-
roster,
|
|
2681
|
-
{ ...options, scope, ...definitionsDir === void 0 ? {} : { definitionsDir } }
|
|
2682
|
-
);
|
|
2683
|
-
if (options.dryRun) {
|
|
2684
|
-
console.log(`
|
|
2685
|
-
[dry-run] ${synced.length} outputs would be generated`);
|
|
2686
|
-
return;
|
|
2687
|
-
}
|
|
2688
|
-
console.log(`
|
|
2689
|
-
\u2713 Synced ${synced.length} outputs`);
|
|
2690
|
-
for (const item of synced) {
|
|
2691
|
-
console.log(` + ${item}`);
|
|
2692
|
-
}
|
|
2693
|
-
if (skipped.length > 0) {
|
|
2694
|
-
console.log(`
|
|
2695
|
-
! Skipped ${skipped.length}:`);
|
|
2696
|
-
for (const item of skipped) {
|
|
2697
|
-
console.log(` - ${item}`);
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
console.log();
|
|
2701
|
-
if (hasUnresolvedRoles) {
|
|
2702
|
-
console.error(
|
|
2703
|
-
' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
|
|
2704
|
-
);
|
|
2705
|
-
process.exitCode = 1;
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
async function runAgentsList(targetDir, scope = "repo") {
|
|
2709
|
-
let roster;
|
|
2710
|
-
try {
|
|
2711
|
-
roster = await loadRoster(targetDir, scope);
|
|
2712
|
-
} catch (err) {
|
|
2713
|
-
if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
|
|
2714
|
-
console.error(err.message);
|
|
2715
|
-
process.exit(1);
|
|
2716
|
-
return;
|
|
2717
|
-
}
|
|
2718
|
-
throw err;
|
|
2719
|
-
}
|
|
2720
|
-
const nameWidth = Math.max(
|
|
2721
|
-
"Name".length,
|
|
2722
|
-
...roster.roster.map((e) => (e.name ?? e.definition).length)
|
|
2723
|
-
);
|
|
2724
|
-
const defWidth = Math.max(
|
|
2725
|
-
"Role ID".length,
|
|
2726
|
-
...roster.roster.map((e) => e.definition.length)
|
|
2727
|
-
);
|
|
2728
|
-
const idWidth = Math.max(
|
|
2729
|
-
"OpenClaw ID".length,
|
|
2730
|
-
...roster.roster.map((e) => e.id.length)
|
|
2731
|
-
);
|
|
2732
|
-
const pad = (s, n) => s.padEnd(n);
|
|
2733
|
-
console.log(
|
|
2734
|
-
`
|
|
2735
|
-
Profile: ${roster.agent_profile} | Naming: ${roster.naming_strategy} | Agents: ${roster.roster.length}
|
|
2736
|
-
`
|
|
2737
|
-
);
|
|
2738
|
-
console.log(
|
|
2739
|
-
` ${pad("Name", nameWidth)} ${pad("Role ID", defWidth)} ${pad("OpenClaw ID", idWidth)}`
|
|
2740
|
-
);
|
|
2741
|
-
console.log(
|
|
2742
|
-
` ${"-".repeat(nameWidth)} ${"-".repeat(defWidth)} ${"-".repeat(idWidth)}`
|
|
2743
|
-
);
|
|
2744
|
-
for (const entry of roster.roster) {
|
|
2745
|
-
console.log(
|
|
2746
|
-
` ${pad(entry.name ?? "(unnamed)", nameWidth)} ${pad(entry.definition, defWidth)} ${pad(entry.id, idWidth)}`
|
|
2747
|
-
);
|
|
2748
|
-
}
|
|
2749
|
-
console.log();
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2752
|
-
// src/modules/hub.ts
|
|
2753
|
-
import { confirm, select as select2, input as input2 } from "@inquirer/prompts";
|
|
2754
|
-
import { readdir as readdir5, writeFile as writeFile7 } from "fs/promises";
|
|
2755
|
-
import { join as join5 } from "path";
|
|
2756
|
-
|
|
2757
|
-
// src/modules/git.ts
|
|
2758
|
-
import { execFile } from "child_process";
|
|
2759
|
-
var GitTimeoutError = class extends Error {
|
|
2760
|
-
constructor(message, options) {
|
|
2761
|
-
super(message, options);
|
|
2762
|
-
this.name = "GitTimeoutError";
|
|
2763
|
-
}
|
|
2764
|
-
};
|
|
2765
|
-
var NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set(["fetch", "pull", "push", "clone", "ls-remote", "send-email"]);
|
|
2766
|
-
var WRITE_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
2767
|
-
"init",
|
|
2768
|
-
"commit",
|
|
2769
|
-
"add",
|
|
2770
|
-
"checkout",
|
|
2771
|
-
"switch",
|
|
2772
|
-
"merge",
|
|
2773
|
-
"rebase",
|
|
2774
|
-
"reset",
|
|
2775
|
-
"tag",
|
|
2776
|
-
"mv",
|
|
2777
|
-
"rm",
|
|
2778
|
-
"cherry-pick",
|
|
2779
|
-
"revert",
|
|
2780
|
-
"stash",
|
|
2781
|
-
"apply",
|
|
2782
|
-
"am",
|
|
2783
|
-
"restore",
|
|
2784
|
-
"submodule",
|
|
2785
|
-
"sparse-checkout",
|
|
2786
|
-
"maintenance",
|
|
2787
|
-
"notes"
|
|
2788
|
-
]);
|
|
2789
|
-
var AMBIGUOUS_AS_WRITE = /* @__PURE__ */ new Set(["branch", "config", "remote", "worktree"]);
|
|
2790
|
-
var READ_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
2791
|
-
"status",
|
|
2792
|
-
"rev-parse",
|
|
2793
|
-
"log",
|
|
2794
|
-
"diff",
|
|
2795
|
-
"show",
|
|
2796
|
-
"describe",
|
|
2797
|
-
"cat-file",
|
|
2798
|
-
"ls-files",
|
|
2799
|
-
"ls-tree",
|
|
2800
|
-
"symbolic-ref",
|
|
2801
|
-
"show-ref",
|
|
2802
|
-
"for-each-ref",
|
|
2803
|
-
"merge-base",
|
|
2804
|
-
"blame",
|
|
2805
|
-
"shortlog",
|
|
2806
|
-
"grep",
|
|
2807
|
-
"reflog",
|
|
2808
|
-
"diff-tree",
|
|
2809
|
-
"rev-list",
|
|
2810
|
-
"name-rev",
|
|
2811
|
-
"count-objects",
|
|
2812
|
-
"verify-commit",
|
|
2813
|
-
"verify-tag",
|
|
2814
|
-
"help",
|
|
2815
|
-
"version"
|
|
2816
|
-
]);
|
|
2817
|
-
var GLOBAL_FLAGS_WITH_SEPARATE_VALUE = /* @__PURE__ */ new Set(["-c", "-C"]);
|
|
2818
|
-
var DEFAULT_TIMEOUTS_MS = {
|
|
2819
|
-
read: 15e3,
|
|
2820
|
-
write: 3e4,
|
|
2821
|
-
network: 12e4
|
|
2822
|
-
};
|
|
2823
|
-
function classifyGitCommand(args) {
|
|
2824
|
-
let subcommand;
|
|
2825
|
-
for (let i = 0; i < args.length; i++) {
|
|
2826
|
-
const arg = args[i];
|
|
2827
|
-
if (GLOBAL_FLAGS_WITH_SEPARATE_VALUE.has(arg)) {
|
|
2828
|
-
i++;
|
|
2829
|
-
continue;
|
|
2830
|
-
}
|
|
2831
|
-
if (arg.startsWith("-")) continue;
|
|
2832
|
-
subcommand = arg;
|
|
2833
|
-
break;
|
|
2834
|
-
}
|
|
2835
|
-
if (!subcommand) return "read";
|
|
2836
|
-
if (NETWORK_SUBCOMMANDS.has(subcommand)) return "network";
|
|
2837
|
-
if (WRITE_SUBCOMMANDS.has(subcommand) || AMBIGUOUS_AS_WRITE.has(subcommand)) return "write";
|
|
2838
|
-
if (READ_SUBCOMMANDS.has(subcommand)) return "read";
|
|
2839
|
-
return "write";
|
|
2840
|
-
}
|
|
2841
|
-
function buildGitEnv() {
|
|
2842
|
-
return {
|
|
2843
|
-
...process.env,
|
|
2844
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
2845
|
-
GCM_INTERACTIVE: "Never",
|
|
2846
|
-
GIT_OPTIONAL_LOCKS: "0"
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
function runGit(cwd, args, options = {}) {
|
|
2850
|
-
const commandClass = options.commandClass ?? classifyGitCommand(args);
|
|
2851
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
|
|
2852
|
-
return new Promise((resolve5, reject) => {
|
|
2853
|
-
const child = execFile(
|
|
2854
|
-
"git",
|
|
2855
|
-
args,
|
|
2856
|
-
{
|
|
2857
|
-
cwd,
|
|
2858
|
-
env: buildGitEnv(),
|
|
2859
|
-
timeout: timeoutMs,
|
|
2860
|
-
// SIGTERM (Node's default) rather than SIGKILL: gives git a chance
|
|
2861
|
-
// to run its own cleanup on POSIX, relevant to EF-20's own concern
|
|
2862
|
-
// about a killed operation leaving refs/locks in an uncertain state.
|
|
2863
|
-
// Moot on Windows -- Node docs: both map to an abrupt TerminateProcess
|
|
2864
|
-
// there -- but this module isn't Windows-gated.
|
|
2865
|
-
killSignal: "SIGTERM",
|
|
2866
|
-
windowsHide: true,
|
|
2867
|
-
encoding: "utf8"
|
|
2868
|
-
},
|
|
2869
|
-
(error, stdout, stderr) => {
|
|
2870
|
-
if (error) {
|
|
2871
|
-
if (error.killed) {
|
|
2872
|
-
reject(
|
|
2873
|
-
new GitTimeoutError(
|
|
2874
|
-
// NOTE: embeds the raw args, including any secrets a future
|
|
2875
|
-
// network-class caller might pass (e.g. a credentialed
|
|
2876
|
-
// clone URL). No current caller does; redact before one does.
|
|
2877
|
-
`git ${args.join(" ")} timed out after ${timeoutMs}ms (class: ${commandClass})`,
|
|
2878
|
-
{ cause: error }
|
|
2879
|
-
)
|
|
2880
|
-
);
|
|
2881
|
-
return;
|
|
2882
|
-
}
|
|
2883
|
-
reject(error);
|
|
2884
|
-
return;
|
|
2885
|
-
}
|
|
2886
|
-
resolve5({ stdout, stderr });
|
|
2926
|
+
const dest = join4(arcaneDir, `${roleId}.yaml`);
|
|
2927
|
+
if (!options.dryRun) {
|
|
2928
|
+
try {
|
|
2929
|
+
await copyFile2(src, dest);
|
|
2930
|
+
} catch {
|
|
2931
|
+
console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
|
|
2887
2932
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
});
|
|
2891
|
-
}
|
|
2892
|
-
async function inspectGitRepository(cwd) {
|
|
2893
|
-
try {
|
|
2894
|
-
const { stdout: stdout2 } = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
2895
|
-
if (stdout2.trim() !== "true") {
|
|
2896
|
-
return { status: "not-repository" };
|
|
2933
|
+
} else {
|
|
2934
|
+
console.log(` [dry-run] Would copy: ${label2}/agents/${roleId}.yaml`);
|
|
2897
2935
|
}
|
|
2898
|
-
} catch {
|
|
2899
|
-
return { status: "not-repository" };
|
|
2900
2936
|
}
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2937
|
+
if (options.dryRun) {
|
|
2938
|
+
console.log(
|
|
2939
|
+
`
|
|
2940
|
+
[dry-run] Would initialize roster with ${selectedRoleIds.length} agents`
|
|
2941
|
+
);
|
|
2942
|
+
return;
|
|
2905
2943
|
}
|
|
2906
|
-
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2944
|
+
printStep("Syncing agent definitions to all clients...");
|
|
2945
|
+
const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
|
|
2946
|
+
targetDir,
|
|
2947
|
+
assetsDir,
|
|
2948
|
+
roster,
|
|
2949
|
+
{ dryRun: false, scope }
|
|
2950
|
+
);
|
|
2951
|
+
printSuccess("Agent roster initialized");
|
|
2952
|
+
console.log(` \u{1F4CB} Profile: ${profileId} \xB7 Naming: ${namingStrategy} \xB7 Agents: ${rosterEntries.length}`);
|
|
2953
|
+
console.log(
|
|
2954
|
+
` \u{1F504} Synced: ${synced.length} outputs (${scope === "user" ? "agent personas in ~/.claude/agents" : "Copilot, Claude, Codex, OpenClaw"})`
|
|
2955
|
+
);
|
|
2956
|
+
if (skipped.length > 0) {
|
|
2957
|
+
console.log(` \u26A0\uFE0F Skipped: ${skipped.length} (${skipped.join(", ")})`);
|
|
2920
2958
|
}
|
|
2921
|
-
|
|
2922
|
-
|
|
2959
|
+
console.log();
|
|
2960
|
+
console.log(
|
|
2961
|
+
` \u{1F4A1} Customize: edit ${label2}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
|
|
2962
|
+
);
|
|
2963
|
+
console.log();
|
|
2964
|
+
if (hasUnresolvedRoles) {
|
|
2965
|
+
console.error(
|
|
2966
|
+
' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
|
|
2967
|
+
);
|
|
2968
|
+
process.exitCode = 1;
|
|
2923
2969
|
}
|
|
2970
|
+
}
|
|
2971
|
+
async function runAgentsSync(options, targetDir, assetsDir) {
|
|
2972
|
+
const scope = options.scope ?? "repo";
|
|
2973
|
+
let roster;
|
|
2974
|
+
let definitionsDir;
|
|
2975
|
+
let fromUserTier = false;
|
|
2924
2976
|
try {
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2977
|
+
({ roster, definitionsDir, fromUserTier } = await resolveRoster(targetDir, scope));
|
|
2978
|
+
} catch (err) {
|
|
2979
|
+
if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
|
|
2980
|
+
console.error(err.message);
|
|
2981
|
+
process.exit(1);
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
throw err;
|
|
2928
2985
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
2986
|
+
if (options.dryRun) {
|
|
2987
|
+
console.log("\n [dry-run] Would sync agents from roster:\n");
|
|
2988
|
+
} else {
|
|
2989
|
+
console.log("\n Syncing agents...\n");
|
|
2934
2990
|
}
|
|
2935
|
-
if (
|
|
2936
|
-
|
|
2991
|
+
if (fromUserTier && scope === "repo") {
|
|
2992
|
+
console.log(
|
|
2993
|
+
" This repository has no roster of its own and takes its spells from the user tier,\n so its roster tables are built from ~/.arcane/agents.yaml.\n"
|
|
2994
|
+
);
|
|
2937
2995
|
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2996
|
+
const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
|
|
2997
|
+
targetDir,
|
|
2998
|
+
assetsDir,
|
|
2999
|
+
roster,
|
|
3000
|
+
{ ...options, scope, ...definitionsDir === void 0 ? {} : { definitionsDir } }
|
|
3001
|
+
);
|
|
3002
|
+
if (options.dryRun) {
|
|
3003
|
+
console.log(`
|
|
3004
|
+
[dry-run] ${synced.length} outputs would be generated`);
|
|
3005
|
+
return;
|
|
2940
3006
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
if (error.code !== 1) {
|
|
2946
|
-
return { corrected: false, to: "main", blockedReason: "target-unreadable" };
|
|
2947
|
-
}
|
|
3007
|
+
console.log(`
|
|
3008
|
+
\u2713 Synced ${synced.length} outputs`);
|
|
3009
|
+
for (const item of synced) {
|
|
3010
|
+
console.log(` + ${item}`);
|
|
2948
3011
|
}
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
}
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
const { stdout } = await runGit(cwd, ["config", "--local", "--get", "pull.rebase"]);
|
|
2956
|
-
rawLocalValue = stdout.trim();
|
|
2957
|
-
} catch {
|
|
2958
|
-
rawLocalValue = void 0;
|
|
3012
|
+
if (skipped.length > 0) {
|
|
3013
|
+
console.log(`
|
|
3014
|
+
! Skipped ${skipped.length}:`);
|
|
3015
|
+
for (const item of skipped) {
|
|
3016
|
+
console.log(` - ${item}`);
|
|
3017
|
+
}
|
|
2959
3018
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3019
|
+
console.log();
|
|
3020
|
+
if (hasUnresolvedRoles) {
|
|
3021
|
+
console.error(
|
|
3022
|
+
' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
|
|
3023
|
+
);
|
|
3024
|
+
process.exitCode = 1;
|
|
2963
3025
|
}
|
|
3026
|
+
}
|
|
3027
|
+
async function runAgentsList(targetDir, scope = "repo") {
|
|
3028
|
+
let roster;
|
|
2964
3029
|
try {
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3030
|
+
roster = await loadRoster(targetDir, scope);
|
|
3031
|
+
} catch (err) {
|
|
3032
|
+
if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
|
|
3033
|
+
console.error(err.message);
|
|
3034
|
+
process.exit(1);
|
|
3035
|
+
return;
|
|
2968
3036
|
}
|
|
2969
|
-
|
|
2970
|
-
}
|
|
2971
|
-
|
|
3037
|
+
throw err;
|
|
3038
|
+
}
|
|
3039
|
+
const nameWidth = Math.max(
|
|
3040
|
+
"Name".length,
|
|
3041
|
+
...roster.roster.map((e) => (e.name ?? e.definition).length)
|
|
3042
|
+
);
|
|
3043
|
+
const defWidth = Math.max(
|
|
3044
|
+
"Role ID".length,
|
|
3045
|
+
...roster.roster.map((e) => e.definition.length)
|
|
3046
|
+
);
|
|
3047
|
+
const idWidth = Math.max(
|
|
3048
|
+
"OpenClaw ID".length,
|
|
3049
|
+
...roster.roster.map((e) => e.id.length)
|
|
3050
|
+
);
|
|
3051
|
+
const pad = (s, n) => s.padEnd(n);
|
|
3052
|
+
console.log(
|
|
3053
|
+
`
|
|
3054
|
+
Profile: ${roster.agent_profile} | Naming: ${roster.naming_strategy} | Agents: ${roster.roster.length}
|
|
3055
|
+
`
|
|
3056
|
+
);
|
|
3057
|
+
console.log(
|
|
3058
|
+
` ${pad("Name", nameWidth)} ${pad("Role ID", defWidth)} ${pad("OpenClaw ID", idWidth)}`
|
|
3059
|
+
);
|
|
3060
|
+
console.log(
|
|
3061
|
+
` ${"-".repeat(nameWidth)} ${"-".repeat(defWidth)} ${"-".repeat(idWidth)}`
|
|
3062
|
+
);
|
|
3063
|
+
for (const entry of roster.roster) {
|
|
3064
|
+
console.log(
|
|
3065
|
+
` ${pad(entry.name ?? "(unnamed)", nameWidth)} ${pad(entry.definition, defWidth)} ${pad(entry.id, idWidth)}`
|
|
3066
|
+
);
|
|
2972
3067
|
}
|
|
3068
|
+
console.log();
|
|
2973
3069
|
}
|
|
2974
3070
|
|
|
3071
|
+
// src/modules/hub.ts
|
|
3072
|
+
import { confirm, select as select2, input as input2 } from "@inquirer/prompts";
|
|
3073
|
+
import { readdir as readdir5, writeFile as writeFile7 } from "fs/promises";
|
|
3074
|
+
import { join as join5 } from "path";
|
|
3075
|
+
|
|
2975
3076
|
// src/modules/ado-context.ts
|
|
2976
3077
|
function remoteUrlHost(url) {
|
|
2977
3078
|
const trimmed = url.trim();
|
|
@@ -3336,7 +3437,27 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
|
3336
3437
|
// src/modules/push-safety.ts
|
|
3337
3438
|
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8, chmod, access as access3, rm as rm4, readdir as readdir6 } from "fs/promises";
|
|
3338
3439
|
import { constants } from "fs";
|
|
3339
|
-
import { join as
|
|
3440
|
+
import { join as join7, resolve as resolve3, dirname as dirname4, basename as basename2 } from "path";
|
|
3441
|
+
|
|
3442
|
+
// src/modules/real-path.ts
|
|
3443
|
+
import { realpathSync } from "fs";
|
|
3444
|
+
import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "path";
|
|
3445
|
+
function resolveRealPath(path4) {
|
|
3446
|
+
const missing = [];
|
|
3447
|
+
let existing = path4;
|
|
3448
|
+
for (; ; ) {
|
|
3449
|
+
try {
|
|
3450
|
+
return join6(realpathSync.native(existing), ...missing);
|
|
3451
|
+
} catch {
|
|
3452
|
+
const parent = dirname3(existing);
|
|
3453
|
+
if (parent === existing) return resolve2(path4);
|
|
3454
|
+
missing.unshift(basename(existing));
|
|
3455
|
+
existing = parent;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
// src/modules/push-safety.ts
|
|
3340
3461
|
var DISABLED_PUSH_URL = "arcane-push-blocked-run-spell-unblock-push://this-repository";
|
|
3341
3462
|
var NO_PUSH_URL_MARKER = (remote) => `remote.${remote}.arcaneHadNoPushUrl`;
|
|
3342
3463
|
var ARCANE_HOOKS_DIR = ".arcane/hooks";
|
|
@@ -3365,10 +3486,12 @@ function isArcaneHooksPath(root, value) {
|
|
|
3365
3486
|
const windows = process.platform === "win32";
|
|
3366
3487
|
const normalize = (p) => {
|
|
3367
3488
|
const separated = windows ? p.replace(/\\/g, "/") : p;
|
|
3368
|
-
const
|
|
3489
|
+
const resolved = resolve3(root, separated);
|
|
3490
|
+
const absolute = (windows ? resolved.replace(/\\/g, "/") : resolved).replace(/\/+$/, "");
|
|
3369
3491
|
return windows ? absolute.toLowerCase() : absolute;
|
|
3370
3492
|
};
|
|
3371
|
-
|
|
3493
|
+
if (normalize(value) === normalize(ARCANE_HOOKS_DIR)) return true;
|
|
3494
|
+
return normalize(resolveRealPath(resolve3(root, value))) === normalize(join7(resolveRealPath(root), ARCANE_HOOKS_DIR));
|
|
3372
3495
|
}
|
|
3373
3496
|
var PRE_PUSH_HOOK_BODY = `#!/bin/sh
|
|
3374
3497
|
# Installed by Arcane because this repository's .arcane.json sets
|
|
@@ -3413,8 +3536,8 @@ async function repositoryRoot(cwd) {
|
|
|
3413
3536
|
]);
|
|
3414
3537
|
const commonDir = stdout.trim();
|
|
3415
3538
|
if (commonDir !== "") {
|
|
3416
|
-
const parent =
|
|
3417
|
-
if (
|
|
3539
|
+
const parent = dirname4(commonDir);
|
|
3540
|
+
if (basename2(commonDir) === ".git" && parent !== "") return parent;
|
|
3418
3541
|
}
|
|
3419
3542
|
} catch {
|
|
3420
3543
|
}
|
|
@@ -3427,10 +3550,11 @@ async function repositoryRoot(cwd) {
|
|
|
3427
3550
|
return cwd;
|
|
3428
3551
|
}
|
|
3429
3552
|
async function hookFilePath(cwd, hookName = "pre-push") {
|
|
3430
|
-
return
|
|
3553
|
+
return join7(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, hookName);
|
|
3431
3554
|
}
|
|
3432
3555
|
async function hooksPathValue(cwd) {
|
|
3433
|
-
|
|
3556
|
+
const value = join7(await repositoryRoot(cwd), ARCANE_HOOKS_DIR);
|
|
3557
|
+
return process.platform === "win32" ? value.replace(/\\/g, "/") : value;
|
|
3434
3558
|
}
|
|
3435
3559
|
async function installHook(cwd, hookName, body) {
|
|
3436
3560
|
const root = await repositoryRoot(cwd);
|
|
@@ -3449,11 +3573,11 @@ async function installHook(cwd, hookName, body) {
|
|
|
3449
3573
|
return { status: "refused-default-hooks", hooks: displaced };
|
|
3450
3574
|
}
|
|
3451
3575
|
}
|
|
3452
|
-
const hookPath =
|
|
3576
|
+
const hookPath = join7(root, ARCANE_HOOKS_DIR, hookName);
|
|
3453
3577
|
if (existing !== void 0 && await hookFileMatches(cwd, hookName, body)) {
|
|
3454
3578
|
return { status: "already-ours" };
|
|
3455
3579
|
}
|
|
3456
|
-
await mkdir5(
|
|
3580
|
+
await mkdir5(join7(root, ARCANE_HOOKS_DIR), { recursive: true });
|
|
3457
3581
|
await writeFile8(hookPath, body, "utf-8");
|
|
3458
3582
|
await chmod(hookPath, 493);
|
|
3459
3583
|
await runGit(cwd, ["config", "--local", "core.hooksPath", await hooksPathValue(cwd)]);
|
|
@@ -3498,7 +3622,7 @@ async function defaultHooks(cwd) {
|
|
|
3498
3622
|
let hooksDir;
|
|
3499
3623
|
try {
|
|
3500
3624
|
const { stdout } = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
|
|
3501
|
-
hooksDir =
|
|
3625
|
+
hooksDir = join7(resolve3(cwd, stdout.trim()), "hooks");
|
|
3502
3626
|
} catch {
|
|
3503
3627
|
return [];
|
|
3504
3628
|
}
|
|
@@ -3534,7 +3658,7 @@ async function removeHook(cwd, hookName) {
|
|
|
3534
3658
|
await rm4(await hookFilePath(cwd, hookName), { force: true });
|
|
3535
3659
|
} catch {
|
|
3536
3660
|
}
|
|
3537
|
-
const hooksDir =
|
|
3661
|
+
const hooksDir = join7(root, ARCANE_HOOKS_DIR);
|
|
3538
3662
|
let remaining = [];
|
|
3539
3663
|
try {
|
|
3540
3664
|
remaining = await readdir6(hooksDir);
|
|
@@ -3937,7 +4061,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
3937
4061
|
}
|
|
3938
4062
|
let flags = {};
|
|
3939
4063
|
try {
|
|
3940
|
-
flags = resolveManifestFlags(options.manifestFlags, {},
|
|
4064
|
+
flags = resolveManifestFlags(options.manifestFlags, {}, join8(targetDir, ".arcane.json"));
|
|
3941
4065
|
} catch (err) {
|
|
3942
4066
|
if (!(err instanceof ManifestFlagError)) throw err;
|
|
3943
4067
|
console.error(`
|
|
@@ -4072,7 +4196,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4072
4196
|
const installedFiles = [];
|
|
4073
4197
|
const fileHashes = {};
|
|
4074
4198
|
for (const file of component.files) {
|
|
4075
|
-
const srcPath =
|
|
4199
|
+
const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
4076
4200
|
if (options.dryRun) {
|
|
4077
4201
|
printDryRun(`Would copy: ${file}`);
|
|
4078
4202
|
} else {
|
|
@@ -4089,7 +4213,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4089
4213
|
fileCount++;
|
|
4090
4214
|
}
|
|
4091
4215
|
for (const dir of component.directories ?? []) {
|
|
4092
|
-
const srcDirPath =
|
|
4216
|
+
const srcDirPath = join8(assetsDir, dir);
|
|
4093
4217
|
if (options.dryRun) {
|
|
4094
4218
|
printDryRun(`Would copy directory: ${dir}/`);
|
|
4095
4219
|
} else {
|
|
@@ -4118,7 +4242,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
4118
4242
|
if (options.dryRun) {
|
|
4119
4243
|
if (hasAgents) {
|
|
4120
4244
|
const agentFiles = await import("fs").then(
|
|
4121
|
-
(fsModule) => fsModule.promises.readdir(
|
|
4245
|
+
(fsModule) => fsModule.promises.readdir(join8(assetsDir, "agents"))
|
|
4122
4246
|
);
|
|
4123
4247
|
const yamlFiles = agentFiles.filter((f) => f.endsWith(".yaml")).sort();
|
|
4124
4248
|
printDryRun("Would run agent setup (interactive):");
|
|
@@ -4357,7 +4481,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4357
4481
|
console.log(chalk2.hex("#a855f7")(`
|
|
4358
4482
|
\u2726 Arcane v${packageVersion}
|
|
4359
4483
|
`));
|
|
4360
|
-
const homeDir =
|
|
4484
|
+
const homeDir = dirname5(storeRoot);
|
|
4361
4485
|
const components = USER_TIER_COMPONENTS.map(
|
|
4362
4486
|
(name) => componentForScope(getComponent(name), "user")
|
|
4363
4487
|
);
|
|
@@ -4375,7 +4499,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4375
4499
|
for (const id of spellIds) {
|
|
4376
4500
|
for (const client2 of ["codex", "claude"]) {
|
|
4377
4501
|
const relativePath = USER_FANOUT_PATHS[client2](id);
|
|
4378
|
-
if (await fileExists(
|
|
4502
|
+
if (await fileExists(join8(homeDir, relativePath))) {
|
|
4379
4503
|
printDryRun(`Would leave alone (exists, not written by Arcane): ~/${relativePath}`);
|
|
4380
4504
|
} else {
|
|
4381
4505
|
printDryRun(`Would write: ~/${relativePath}`);
|
|
@@ -4393,7 +4517,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4393
4517
|
const installedFiles = [];
|
|
4394
4518
|
const fileHashes = {};
|
|
4395
4519
|
for (const file of component.files) {
|
|
4396
|
-
const srcPath =
|
|
4520
|
+
const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
4397
4521
|
fileHashes[file] = await copyFile(srcPath, storeRoot, file, { force: options.force });
|
|
4398
4522
|
installedFiles.push(file);
|
|
4399
4523
|
fileCount++;
|
|
@@ -4440,23 +4564,23 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
4440
4564
|
|
|
4441
4565
|
// src/modules/component-install.ts
|
|
4442
4566
|
import { readFile as readFile8, stat, writeFile as writeFile9 } from "fs/promises";
|
|
4443
|
-
import { join as
|
|
4567
|
+
import { join as join9 } from "path";
|
|
4444
4568
|
async function planComponentInstall(component, targetDir, assetsDir, opts = {}) {
|
|
4445
4569
|
const files = [];
|
|
4446
4570
|
const stateOf = async (file, src) => {
|
|
4447
|
-
const dest =
|
|
4571
|
+
const dest = join9(targetDir, file);
|
|
4448
4572
|
if (!await fileExists(dest)) return { file, src, state: "missing" };
|
|
4449
4573
|
if ((await stat(dest)).isDirectory()) return { file, src, state: "directory" };
|
|
4450
4574
|
const same = await hashFile(dest) === await hashFile(src);
|
|
4451
4575
|
return { file, src, state: same ? "identical" : "differs" };
|
|
4452
4576
|
};
|
|
4453
4577
|
for (const file of component.files) {
|
|
4454
|
-
files.push(await stateOf(file,
|
|
4578
|
+
files.push(await stateOf(file, join9(assetsDir, component.sourceOverrides?.[file] ?? file)));
|
|
4455
4579
|
}
|
|
4456
4580
|
for (const dir of component.directories ?? []) {
|
|
4457
|
-
const srcDir =
|
|
4581
|
+
const srcDir = join9(assetsDir, dir);
|
|
4458
4582
|
for (const file of await listDirectoryFiles(srcDir, dir)) {
|
|
4459
|
-
files.push(await stateOf(file,
|
|
4583
|
+
files.push(await stateOf(file, join9(assetsDir, file)));
|
|
4460
4584
|
}
|
|
4461
4585
|
}
|
|
4462
4586
|
const of = (...states) => files.filter((f) => states.includes(f.state));
|
|
@@ -4468,14 +4592,14 @@ async function planComponentInstall(component, targetDir, assetsDir, opts = {})
|
|
|
4468
4592
|
}
|
|
4469
4593
|
return { component, files, toCopy: of("missing", "identical"), kept: [], conflicts: of("differs", "directory") };
|
|
4470
4594
|
}
|
|
4471
|
-
async function executeInstallPlan(plan, targetDir) {
|
|
4595
|
+
async function executeInstallPlan(plan, targetDir, opts = {}) {
|
|
4472
4596
|
const files = [];
|
|
4473
4597
|
const fileHashes = {};
|
|
4474
4598
|
const written = [];
|
|
4475
4599
|
try {
|
|
4476
4600
|
for (const { file, src, state } of plan.toCopy) {
|
|
4477
|
-
const previous = state === "missing" ? null : await readFile8(
|
|
4478
|
-
fileHashes[file] = await copyFile(src, targetDir, file, { force: true });
|
|
4601
|
+
const previous = state === "missing" ? null : await readFile8(join9(targetDir, file));
|
|
4602
|
+
fileHashes[file] = await copyFile(src, targetDir, file, { force: true, ...opts });
|
|
4479
4603
|
written.push({ file, previous });
|
|
4480
4604
|
files.push(file);
|
|
4481
4605
|
}
|
|
@@ -4492,7 +4616,7 @@ async function rollback(written, targetDir) {
|
|
|
4492
4616
|
await removeWithin(targetDir, file);
|
|
4493
4617
|
await removeEmptyAncestors(targetDir, file);
|
|
4494
4618
|
} else {
|
|
4495
|
-
await writeFile9(
|
|
4619
|
+
await writeFile9(join9(targetDir, file), previous);
|
|
4496
4620
|
}
|
|
4497
4621
|
} catch {
|
|
4498
4622
|
}
|
|
@@ -4628,8 +4752,8 @@ async function runAdd(names, options, targetDir, assetsDir, packageVersion) {
|
|
|
4628
4752
|
}
|
|
4629
4753
|
|
|
4630
4754
|
// src/commands/update.ts
|
|
4631
|
-
import { dirname as
|
|
4632
|
-
import { readFile as readFile9
|
|
4755
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
4756
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4633
4757
|
|
|
4634
4758
|
// src/modules/merge3.ts
|
|
4635
4759
|
var CONFLICT_START = "<<<<<<< yours (edited)";
|
|
@@ -4760,7 +4884,7 @@ function merge3(base, ours, theirs) {
|
|
|
4760
4884
|
}
|
|
4761
4885
|
|
|
4762
4886
|
// src/modules/registry-changes.ts
|
|
4763
|
-
import { join as
|
|
4887
|
+
import { join as join10 } from "path";
|
|
4764
4888
|
function findMissingRequires(installed, profile) {
|
|
4765
4889
|
const names = new Set(installed.flatMap((c) => LEGACY_COMPONENT_MIGRATIONS[c.name] ?? [c.name]));
|
|
4766
4890
|
const promised = listProfiles().find((p) => p.id === profile)?.components;
|
|
@@ -4806,7 +4930,7 @@ async function findNewlyAvailable(targetDir, profile, installed, spellScope) {
|
|
|
4806
4930
|
const alreadyPresent = [];
|
|
4807
4931
|
if (component.skipExisting) {
|
|
4808
4932
|
for (const file of component.files) {
|
|
4809
|
-
if (await fileExists(
|
|
4933
|
+
if (await fileExists(join10(targetDir, file))) alreadyPresent.push(file);
|
|
4810
4934
|
}
|
|
4811
4935
|
if (component.files.length > 0 && alreadyPresent.length === component.files.length) continue;
|
|
4812
4936
|
}
|
|
@@ -4922,11 +5046,11 @@ function describeInstallPlan(plan) {
|
|
|
4922
5046
|
}
|
|
4923
5047
|
return lines;
|
|
4924
5048
|
}
|
|
4925
|
-
async function executeUpdateInstalls(plan, targetDir, packageVersion) {
|
|
5049
|
+
async function executeUpdateInstalls(plan, targetDir, packageVersion, copyOpts = {}) {
|
|
4926
5050
|
const installed = [];
|
|
4927
5051
|
const lines = [];
|
|
4928
5052
|
for (const item of plan.items) {
|
|
4929
|
-
const { files, fileHashes } = await executeInstallPlan(item.plan, targetDir);
|
|
5053
|
+
const { files, fileHashes } = await executeInstallPlan(item.plan, targetDir, copyOpts);
|
|
4930
5054
|
installed.push({
|
|
4931
5055
|
name: item.plan.component.name,
|
|
4932
5056
|
files,
|
|
@@ -4963,7 +5087,7 @@ async function missingTierWarning(scope, spellScope, homeDir) {
|
|
|
4963
5087
|
} catch {
|
|
4964
5088
|
return [
|
|
4965
5089
|
"",
|
|
4966
|
-
` ! This repository takes its spells from the user tier, and ${storeRoot} is not installed.`,
|
|
5090
|
+
` ! This repository takes its spells from the user tier, and ${storeRoot} is not installed (or its manifest cannot be read).`,
|
|
4967
5091
|
" No client can reach a spell here: the routing block in CLAUDE.md and AGENTS.md still",
|
|
4968
5092
|
" names spells that resolve to nothing, so an agent improvises the workflow instead of",
|
|
4969
5093
|
" failing. Fix it with one of:",
|
|
@@ -5065,12 +5189,21 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
5065
5189
|
console.log(` ! Refused (path escapes the target directory): ${file}`);
|
|
5066
5190
|
return "reported";
|
|
5067
5191
|
}
|
|
5068
|
-
const filePath =
|
|
5192
|
+
const filePath = join11(targetDir, file);
|
|
5069
5193
|
if (!await fileExists(filePath)) return "not-found";
|
|
5070
5194
|
if (!prune) return "reported";
|
|
5071
5195
|
if (recordedHash === void 0) {
|
|
5072
|
-
|
|
5073
|
-
|
|
5196
|
+
const evidence = await isUneditedTrackedFile(targetDir, file);
|
|
5197
|
+
if (evidence.verdict !== "unedited") {
|
|
5198
|
+
console.log(
|
|
5199
|
+
` ! Orphaned, not pruned: ${file} \u2014 no recorded hash, and git cannot confirm it is untouched (${evidence.detail}). Delete it yourself (git rm -- <path> if git tracks it), then run spell update --prune again to drop it from .arcane.json.`
|
|
5200
|
+
);
|
|
5201
|
+
return "reported";
|
|
5202
|
+
}
|
|
5203
|
+
await removeWithin(targetDir, file);
|
|
5204
|
+
await removeEmptyAncestors(targetDir, file);
|
|
5205
|
+
console.log(` Pruned orphaned file (no recorded hash; git shows it ${evidence.detail}): ${file}`);
|
|
5206
|
+
return "pruned";
|
|
5074
5207
|
}
|
|
5075
5208
|
if (!await fileMatchesHash(filePath, recordedHash)) {
|
|
5076
5209
|
console.log(` ! Orphaned but edited since install \u2014 not pruning: ${file}`);
|
|
@@ -5087,7 +5220,7 @@ async function isReleasableUserOwnedFile(targetDir, file, recordedHash) {
|
|
|
5087
5220
|
} catch {
|
|
5088
5221
|
return false;
|
|
5089
5222
|
}
|
|
5090
|
-
const filePath =
|
|
5223
|
+
const filePath = join11(targetDir, file);
|
|
5091
5224
|
if (!await fileExists(filePath)) return false;
|
|
5092
5225
|
if (recordedHash === void 0) return true;
|
|
5093
5226
|
return !await fileMatchesHash(filePath, recordedHash);
|
|
@@ -5098,7 +5231,7 @@ async function retainReportedOrphan(targetDir, file, recordedHash, into) {
|
|
|
5098
5231
|
} catch {
|
|
5099
5232
|
return;
|
|
5100
5233
|
}
|
|
5101
|
-
if (!await fileExists(
|
|
5234
|
+
if (!await fileExists(join11(targetDir, file))) return;
|
|
5102
5235
|
if (!into.files.includes(file)) into.files.push(file);
|
|
5103
5236
|
if (recordedHash !== void 0) {
|
|
5104
5237
|
into.fileHashes ??= {};
|
|
@@ -5127,7 +5260,7 @@ async function findMissingTrackedFiles(targetDir, components, scope = "repo", sp
|
|
|
5127
5260
|
const candidates = readopted.has(installed.name) ? component.files : installed.files;
|
|
5128
5261
|
for (const file of candidates) {
|
|
5129
5262
|
if (!component.files.includes(file)) continue;
|
|
5130
|
-
if (!await fileExists(
|
|
5263
|
+
if (!await fileExists(join11(targetDir, file))) missing.push(file);
|
|
5131
5264
|
}
|
|
5132
5265
|
}
|
|
5133
5266
|
return missing;
|
|
@@ -5169,7 +5302,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5169
5302
|
const movedFilesPresent = /* @__PURE__ */ new Set();
|
|
5170
5303
|
for (const move of COMPONENT_FILE_MOVES) {
|
|
5171
5304
|
for (const file of move.files) {
|
|
5172
|
-
if (await fileExists(
|
|
5305
|
+
if (await fileExists(join11(targetDir, file))) movedFilesPresent.add(file);
|
|
5173
5306
|
}
|
|
5174
5307
|
}
|
|
5175
5308
|
manifest = {
|
|
@@ -5200,7 +5333,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5200
5333
|
return;
|
|
5201
5334
|
}
|
|
5202
5335
|
try {
|
|
5203
|
-
flagPatch = resolveManifestFlags(options.manifestFlags, manifest,
|
|
5336
|
+
flagPatch = resolveManifestFlags(options.manifestFlags, manifest, join11(targetDir, ".arcane.json"));
|
|
5204
5337
|
} catch (err) {
|
|
5205
5338
|
if (!(err instanceof ManifestFlagError)) throw err;
|
|
5206
5339
|
console.error(`
|
|
@@ -5213,8 +5346,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5213
5346
|
const answered = { ...manifest, ...flagPatch };
|
|
5214
5347
|
const spellScope = effectiveSpellScope(manifest);
|
|
5215
5348
|
const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
|
|
5216
|
-
const homeDir =
|
|
5217
|
-
|
|
5349
|
+
const homeDir = dirname6(targetDir);
|
|
5350
|
+
const tierGate = await missingTierWarning(scope, spellScope);
|
|
5351
|
+
if (tierGate.length > 0) {
|
|
5352
|
+
for (const line of tierGate) console.error(line);
|
|
5353
|
+
console.error(
|
|
5354
|
+
" \u2717 Update refused: nothing was written. Install the tier or change spell_scope, then run `spell update` again."
|
|
5355
|
+
);
|
|
5356
|
+
process.exit(1);
|
|
5357
|
+
return;
|
|
5358
|
+
}
|
|
5218
5359
|
if (scope === "repo") {
|
|
5219
5360
|
console.warn(
|
|
5220
5361
|
`WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
|
|
@@ -5245,6 +5386,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5245
5386
|
return;
|
|
5246
5387
|
}
|
|
5247
5388
|
}
|
|
5389
|
+
const linkTracked = scope === "repo" ? await listSymlinkTrackedPaths(targetDir) : /* @__PURE__ */ new Set();
|
|
5390
|
+
const conversions = [];
|
|
5248
5391
|
const registryChanges = {
|
|
5249
5392
|
retired: findTrackedRetirements(manifest.components),
|
|
5250
5393
|
...scope === "repo" ? {
|
|
@@ -5284,7 +5427,10 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5284
5427
|
for (const line of describeInstallPlan(installPlan)) console.log(line);
|
|
5285
5428
|
return [];
|
|
5286
5429
|
}
|
|
5287
|
-
const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion
|
|
5430
|
+
const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion, {
|
|
5431
|
+
linkTracked,
|
|
5432
|
+
conversions
|
|
5433
|
+
});
|
|
5288
5434
|
for (const line of lines) console.log(line);
|
|
5289
5435
|
return installed;
|
|
5290
5436
|
};
|
|
@@ -5400,7 +5546,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5400
5546
|
if (options.prune && !options.dryRun && installed.files.every((f) => orphanReport.find((o) => o.file === f)?.status !== "reported")) {
|
|
5401
5547
|
continue;
|
|
5402
5548
|
}
|
|
5403
|
-
|
|
5549
|
+
const keptHashes = {};
|
|
5550
|
+
for (const file of stillTracked) {
|
|
5551
|
+
const hash = installed.fileHashes?.[file];
|
|
5552
|
+
if (hash !== void 0) keptHashes[file] = hash;
|
|
5553
|
+
}
|
|
5554
|
+
updatedComponents.push({
|
|
5555
|
+
...installed,
|
|
5556
|
+
files: stillTracked,
|
|
5557
|
+
...installed.fileHashes ? { fileHashes: keptHashes } : {}
|
|
5558
|
+
});
|
|
5404
5559
|
continue;
|
|
5405
5560
|
}
|
|
5406
5561
|
throw err;
|
|
@@ -5408,8 +5563,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5408
5563
|
const updatedFiles = [];
|
|
5409
5564
|
const fileHashes = {};
|
|
5410
5565
|
for (const file of component.files) {
|
|
5411
|
-
const srcPath =
|
|
5412
|
-
const targetExists = await fileExists(
|
|
5566
|
+
const srcPath = join11(assetsDir, component.sourceOverrides?.[file] ?? file);
|
|
5567
|
+
const targetExists = await fileExists(join11(targetDir, file));
|
|
5413
5568
|
const preserveExisting = Boolean(component.skipExisting) && targetExists;
|
|
5414
5569
|
const recordedHash = installed.fileHashes?.[file];
|
|
5415
5570
|
if (component.initOnly && !targetExists) {
|
|
@@ -5429,7 +5584,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5429
5584
|
continue;
|
|
5430
5585
|
}
|
|
5431
5586
|
}
|
|
5432
|
-
const
|
|
5587
|
+
const linkShape = targetExists ? await managedPathLinkShape(targetDir, file, linkTracked, isClientShimPath(file)) : null;
|
|
5588
|
+
const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && linkShape === null && !await fileMatchesHash(join11(targetDir, file), recordedHash);
|
|
5433
5589
|
if (preserveExisting) {
|
|
5434
5590
|
console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
|
|
5435
5591
|
} else if (editedByOperator && isClientShimPath(file)) {
|
|
@@ -5439,7 +5595,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5439
5595
|
` ${options.dryRun ? "[dry-run] Would keep" : "Kept"} customized (not merged): ${file}`
|
|
5440
5596
|
);
|
|
5441
5597
|
} else if (options.dryRun) {
|
|
5442
|
-
console.log(
|
|
5598
|
+
console.log(
|
|
5599
|
+
` [dry-run] Would ${linkShape !== null ? "convert to a regular file (it is a symlink, or a link written out as text)" : sameVersionRestore ? "restore missing" : "update"}: ${file}`
|
|
5600
|
+
);
|
|
5443
5601
|
fileCount++;
|
|
5444
5602
|
} else {
|
|
5445
5603
|
let handled = false;
|
|
@@ -5456,11 +5614,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5456
5614
|
handled = true;
|
|
5457
5615
|
} else {
|
|
5458
5616
|
const [currentContent, newContent] = await Promise.all([
|
|
5459
|
-
readFile9(
|
|
5617
|
+
readFile9(join11(targetDir, file), "utf-8"),
|
|
5460
5618
|
readFile9(srcPath, "utf-8")
|
|
5461
5619
|
]);
|
|
5462
5620
|
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
5463
|
-
await
|
|
5621
|
+
await writeManagedFile(targetDir, file, result.content, { linkTracked, conversions });
|
|
5464
5622
|
fileHashes[file] = await hashFile(srcPath);
|
|
5465
5623
|
if (result.hasConflict) {
|
|
5466
5624
|
conflictedFiles.push(file);
|
|
@@ -5472,7 +5630,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5472
5630
|
}
|
|
5473
5631
|
}
|
|
5474
5632
|
if (!handled) {
|
|
5475
|
-
fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: true });
|
|
5633
|
+
fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: true, linkTracked, conversions });
|
|
5634
|
+
if (linkShape === "path-body") conversions.push({ file, shape: "path-body" });
|
|
5476
5635
|
if (sameVersionRestore) console.log(` Restored missing: ${file}`);
|
|
5477
5636
|
}
|
|
5478
5637
|
fileCount++;
|
|
@@ -5482,11 +5641,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5482
5641
|
}
|
|
5483
5642
|
}
|
|
5484
5643
|
for (const dir of component.directories ?? []) {
|
|
5485
|
-
const srcDirPath =
|
|
5644
|
+
const srcDirPath = join11(assetsDir, dir);
|
|
5486
5645
|
if (options.dryRun) {
|
|
5487
5646
|
console.log(` [dry-run] Would update directory: ${dir}/`);
|
|
5488
5647
|
} else {
|
|
5489
|
-
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true });
|
|
5648
|
+
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true, linkTracked, conversions });
|
|
5490
5649
|
for (const { path: copiedPath, hash } of copied) {
|
|
5491
5650
|
updatedFiles.push(copiedPath);
|
|
5492
5651
|
fileHashes[copiedPath] = hash;
|
|
@@ -5550,6 +5709,21 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5550
5709
|
console.log(" Run `spell update --prune` to remove the ones that are safe to delete.");
|
|
5551
5710
|
}
|
|
5552
5711
|
}
|
|
5712
|
+
if (conversions.length > 0) {
|
|
5713
|
+
console.log(`
|
|
5714
|
+
Converted ${conversions.length} symlinked managed file(s) to regular files (#357):`);
|
|
5715
|
+
const describe = {
|
|
5716
|
+
"index-symlink": "the index tracked it as a symlink (mode 120000); rewritten and staged as a regular file",
|
|
5717
|
+
symlink: "was a symlink in the working tree; now a regular file (stage the typechange with git add)",
|
|
5718
|
+
"path-body": "its body was only a link target, a symlink written out as text; rewritten as a real file"
|
|
5719
|
+
};
|
|
5720
|
+
for (const c of conversions) console.log(` ${c.file} \u2014 ${describe[c.shape]}`);
|
|
5721
|
+
if (conversions.some((c) => c.shape === "index-symlink")) {
|
|
5722
|
+
console.log(
|
|
5723
|
+
" Commit the staged typechange, or every other checkout keeps reading a link whose target is the file's own text."
|
|
5724
|
+
);
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5553
5727
|
printRegistryChanges();
|
|
5554
5728
|
if (customizedShims.length > 0) {
|
|
5555
5729
|
const verb = options.dryRun ? "[dry-run] Would keep" : "Kept";
|
|
@@ -5642,11 +5816,11 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5642
5816
|
}
|
|
5643
5817
|
|
|
5644
5818
|
// src/commands/status.ts
|
|
5645
|
-
import { dirname as
|
|
5819
|
+
import { dirname as dirname8 } from "path";
|
|
5646
5820
|
|
|
5647
5821
|
// src/modules/version-check.ts
|
|
5648
|
-
import { readFile as readFile10, writeFile as
|
|
5649
|
-
import { dirname as
|
|
5822
|
+
import { readFile as readFile10, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
|
|
5823
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
5650
5824
|
import { homedir as homedir3 } from "os";
|
|
5651
5825
|
import { createRequire } from "module";
|
|
5652
5826
|
var require2 = createRequire(import.meta.url);
|
|
@@ -5660,7 +5834,7 @@ function getFeedUrl() {
|
|
|
5660
5834
|
}
|
|
5661
5835
|
return url;
|
|
5662
5836
|
}
|
|
5663
|
-
var DEFAULT_CACHE_PATH =
|
|
5837
|
+
var DEFAULT_CACHE_PATH = join12(homedir3(), ".arcane", "version-cache.json");
|
|
5664
5838
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5665
5839
|
async function readCache(cachePath) {
|
|
5666
5840
|
try {
|
|
@@ -5675,9 +5849,9 @@ async function readCache(cachePath) {
|
|
|
5675
5849
|
}
|
|
5676
5850
|
}
|
|
5677
5851
|
async function writeCache(cachePath, latest) {
|
|
5678
|
-
await mkdir6(
|
|
5852
|
+
await mkdir6(dirname7(cachePath), { recursive: true });
|
|
5679
5853
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5680
|
-
await
|
|
5854
|
+
await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
5681
5855
|
}
|
|
5682
5856
|
async function getLatestVersion(feedUrl) {
|
|
5683
5857
|
const normalizedBase = feedUrl.endsWith("/") ? feedUrl : feedUrl + "/";
|
|
@@ -5816,7 +5990,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
5816
5990
|
console.log("");
|
|
5817
5991
|
if (scope === "user") {
|
|
5818
5992
|
console.log(` Scope: user \u2014 ${targetDir}`);
|
|
5819
|
-
const health = await inspectUserTierFanout(
|
|
5993
|
+
const health = await inspectUserTierFanout(dirname8(targetDir), manifest.fanout);
|
|
5820
5994
|
console.log(
|
|
5821
5995
|
` Client files: ${health.total} (${health.byClient.codex} ${FANOUT_CLIENT_LABELS.codex}, ${health.byClient.claude} ${FANOUT_CLIENT_LABELS.claude})`
|
|
5822
5996
|
);
|
|
@@ -5879,7 +6053,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
5879
6053
|
|
|
5880
6054
|
// src/commands/uninstall.ts
|
|
5881
6055
|
import { rm as rm5 } from "fs/promises";
|
|
5882
|
-
import { dirname as
|
|
6056
|
+
import { dirname as dirname9, join as join13 } from "path";
|
|
5883
6057
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
5884
6058
|
async function runUninstall(options, targetDir) {
|
|
5885
6059
|
let manifest;
|
|
@@ -5962,16 +6136,16 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
5962
6136
|
}
|
|
5963
6137
|
}
|
|
5964
6138
|
}
|
|
5965
|
-
await rm5(
|
|
6139
|
+
await rm5(join13(targetDir, ".arcane.json"), { force: true });
|
|
5966
6140
|
const agentDirs = [
|
|
5967
6141
|
".github/agents",
|
|
5968
6142
|
".arcane/agents",
|
|
5969
6143
|
".arcane/generated"
|
|
5970
6144
|
];
|
|
5971
6145
|
for (const dir of agentDirs) {
|
|
5972
|
-
await rm5(
|
|
6146
|
+
await rm5(join13(targetDir, dir), { recursive: true, force: true });
|
|
5973
6147
|
}
|
|
5974
|
-
await rm5(
|
|
6148
|
+
await rm5(join13(targetDir, ".arcane/agents.yaml"), { force: true });
|
|
5975
6149
|
const markerFiles = [
|
|
5976
6150
|
"CLAUDE.md",
|
|
5977
6151
|
"AGENTS.md",
|
|
@@ -5988,7 +6162,7 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
5988
6162
|
\u2713 Uninstalled \u2014 ${removed} files removed.`);
|
|
5989
6163
|
}
|
|
5990
6164
|
async function uninstallUserTier(options, storeRoot, manifest) {
|
|
5991
|
-
const homeDir =
|
|
6165
|
+
const homeDir = dirname9(storeRoot);
|
|
5992
6166
|
const storeFiles = manifest.components.flatMap((c) => c.files);
|
|
5993
6167
|
const recordedCount = Object.keys(manifest.fanout ?? {}).length;
|
|
5994
6168
|
if (options.dryRun) {
|
|
@@ -6033,10 +6207,10 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
6033
6207
|
console.log(` ! Skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6034
6208
|
}
|
|
6035
6209
|
}
|
|
6036
|
-
await rm5(
|
|
6037
|
-
await rm5(
|
|
6038
|
-
await rm5(
|
|
6039
|
-
await removeDirectoryIfEmpty(
|
|
6210
|
+
await rm5(join13(storeRoot, ".arcane.json"), { force: true });
|
|
6211
|
+
await rm5(join13(storeRoot, "agents"), { recursive: true, force: true });
|
|
6212
|
+
await rm5(join13(storeRoot, "agents.yaml"), { force: true });
|
|
6213
|
+
await removeDirectoryIfEmpty(join13(storeRoot, USER_TIER_SPELLS_DIR));
|
|
6040
6214
|
await removeDirectoryIfEmpty(storeRoot);
|
|
6041
6215
|
for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
|
|
6042
6216
|
const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
|
|
@@ -6049,7 +6223,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
6049
6223
|
|
|
6050
6224
|
// src/commands/doctor.ts
|
|
6051
6225
|
import { access as access4, readdir as readdir9, readFile as readFile13, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
6052
|
-
import { join as
|
|
6226
|
+
import { join as join16, dirname as dirname10 } from "path";
|
|
6053
6227
|
|
|
6054
6228
|
// src/config/incidents.ts
|
|
6055
6229
|
var INCIDENT_QUEUE = [
|
|
@@ -6118,7 +6292,7 @@ function evaluateIncidentGate(records) {
|
|
|
6118
6292
|
|
|
6119
6293
|
// src/modules/denylist-scan.ts
|
|
6120
6294
|
import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
|
|
6121
|
-
import { join as
|
|
6295
|
+
import { join as join14, relative } from "path";
|
|
6122
6296
|
function escapeRegExp(value) {
|
|
6123
6297
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6124
6298
|
}
|
|
@@ -6169,7 +6343,7 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
|
|
|
6169
6343
|
return collected;
|
|
6170
6344
|
}
|
|
6171
6345
|
for (const entry of entries) {
|
|
6172
|
-
const absolutePath =
|
|
6346
|
+
const absolutePath = join14(root, entry.name);
|
|
6173
6347
|
if (entry.isDirectory()) {
|
|
6174
6348
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
6175
6349
|
collected.push(...await collectScannableFiles(absolutePath, repoRoot, extensions));
|
|
@@ -6196,7 +6370,7 @@ import { existsSync, readFileSync } from "fs";
|
|
|
6196
6370
|
import { win32 } from "path";
|
|
6197
6371
|
var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
|
|
6198
6372
|
function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
6199
|
-
return new Promise((
|
|
6373
|
+
return new Promise((resolve6, reject) => {
|
|
6200
6374
|
const child = execFile2(
|
|
6201
6375
|
file,
|
|
6202
6376
|
args,
|
|
@@ -6212,7 +6386,7 @@ function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
|
6212
6386
|
reject(error);
|
|
6213
6387
|
return;
|
|
6214
6388
|
}
|
|
6215
|
-
|
|
6389
|
+
resolve6({ stdout, stderr });
|
|
6216
6390
|
}
|
|
6217
6391
|
);
|
|
6218
6392
|
child.stdin?.end();
|
|
@@ -6430,7 +6604,7 @@ function parseAdoRemote(url) {
|
|
|
6430
6604
|
|
|
6431
6605
|
// src/modules/placeholders.ts
|
|
6432
6606
|
import { readdir as readdir8, readFile as readFile12 } from "fs/promises";
|
|
6433
|
-
import { join as
|
|
6607
|
+
import { join as join15 } from "path";
|
|
6434
6608
|
var PLACEHOLDER_STANDARD_FILE = "spell-authoring-standards.md";
|
|
6435
6609
|
var RUNTIME_PLACEHOLDERS_START = "<!-- runtime-placeholders:start -->";
|
|
6436
6610
|
var RUNTIME_PLACEHOLDERS_END = "<!-- runtime-placeholders:end -->";
|
|
@@ -6525,7 +6699,7 @@ function parseRuntimePlaceholders(standardContent) {
|
|
|
6525
6699
|
async function scanGovernancePlaceholders(governanceDir) {
|
|
6526
6700
|
let standard;
|
|
6527
6701
|
try {
|
|
6528
|
-
standard = await readFile12(
|
|
6702
|
+
standard = await readFile12(join15(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
|
|
6529
6703
|
} catch (err) {
|
|
6530
6704
|
if (err.code === "ENOENT") {
|
|
6531
6705
|
return { status: "skipped", reason: `${PLACEHOLDER_STANDARD_FILE} is not installed` };
|
|
@@ -6544,7 +6718,7 @@ async function scanGovernancePlaceholders(governanceDir) {
|
|
|
6544
6718
|
const templateDocs = [];
|
|
6545
6719
|
const findings = [];
|
|
6546
6720
|
for (const file of files) {
|
|
6547
|
-
const content = await readFile12(
|
|
6721
|
+
const content = await readFile12(join15(governanceDir, file), "utf8");
|
|
6548
6722
|
const status = parseFrontmatterStatus(content);
|
|
6549
6723
|
if (status === "template") templateDocs.push(file);
|
|
6550
6724
|
if (status !== "active") continue;
|
|
@@ -6575,8 +6749,8 @@ async function checkNodeVersion() {
|
|
|
6575
6749
|
async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
|
|
6576
6750
|
const name = `VS Code extension: ${label2}`;
|
|
6577
6751
|
const flavours = [
|
|
6578
|
-
{ dir:
|
|
6579
|
-
{ dir:
|
|
6752
|
+
{ dir: join16(homeDir, ".vscode", "extensions"), bin: "code" },
|
|
6753
|
+
{ dir: join16(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
|
|
6580
6754
|
];
|
|
6581
6755
|
let installBin = null;
|
|
6582
6756
|
for (const flavour of flavours) {
|
|
@@ -6604,11 +6778,11 @@ async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
|
|
|
6604
6778
|
}
|
|
6605
6779
|
async function checkArcaneManifest(targetDir) {
|
|
6606
6780
|
const name = "Arcane manifest (.arcane.json)";
|
|
6607
|
-
const manifestPath2 =
|
|
6781
|
+
const manifestPath2 = join16(targetDir, ".arcane.json");
|
|
6608
6782
|
try {
|
|
6609
6783
|
await access4(manifestPath2);
|
|
6610
6784
|
} catch {
|
|
6611
|
-
const selfHostedManifestPath =
|
|
6785
|
+
const selfHostedManifestPath = join16(targetDir, "src", "assets", ".arcane.json");
|
|
6612
6786
|
try {
|
|
6613
6787
|
const raw = await readFile13(selfHostedManifestPath, "utf8");
|
|
6614
6788
|
const manifest = JSON.parse(raw);
|
|
@@ -6714,7 +6888,7 @@ async function checkSessionContinuity(targetDir) {
|
|
|
6714
6888
|
const results = [];
|
|
6715
6889
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
6716
6890
|
const name = `Session file: ${file}`;
|
|
6717
|
-
const filePath =
|
|
6891
|
+
const filePath = join16(targetDir, file);
|
|
6718
6892
|
try {
|
|
6719
6893
|
await access4(filePath);
|
|
6720
6894
|
results.push({ name, passed: true, message: "present" });
|
|
@@ -6732,12 +6906,12 @@ async function checkSessionContinuity(targetDir) {
|
|
|
6732
6906
|
async function fixSessionContinuity(targetDir, assetsDir) {
|
|
6733
6907
|
const created = [];
|
|
6734
6908
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
6735
|
-
const destPath =
|
|
6909
|
+
const destPath = join16(targetDir, file);
|
|
6736
6910
|
try {
|
|
6737
6911
|
await access4(destPath);
|
|
6738
6912
|
} catch {
|
|
6739
|
-
const srcPath =
|
|
6740
|
-
await mkdir7(
|
|
6913
|
+
const srcPath = join16(assetsDir, file);
|
|
6914
|
+
await mkdir7(dirname10(destPath), { recursive: true });
|
|
6741
6915
|
await fsCopyFile(srcPath, destPath);
|
|
6742
6916
|
created.push(file);
|
|
6743
6917
|
}
|
|
@@ -6864,7 +7038,7 @@ async function checkGovernancePlaceholders(targetDir) {
|
|
|
6864
7038
|
const name = "Governance placeholders (ARC-051)";
|
|
6865
7039
|
let scan;
|
|
6866
7040
|
try {
|
|
6867
|
-
scan = await scanGovernancePlaceholders(
|
|
7041
|
+
scan = await scanGovernancePlaceholders(join16(targetDir, ".arcane", "governance"));
|
|
6868
7042
|
} catch (err) {
|
|
6869
7043
|
return {
|
|
6870
7044
|
name,
|
|
@@ -6951,7 +7125,7 @@ async function checkNewlyAvailableComponents(targetDir) {
|
|
|
6951
7125
|
}
|
|
6952
7126
|
async function checkMcpConfig(targetDir) {
|
|
6953
7127
|
const name = "MCP config (per-server timeout, I12)";
|
|
6954
|
-
const filePath =
|
|
7128
|
+
const filePath = join16(targetDir, ".mcp.json");
|
|
6955
7129
|
let raw;
|
|
6956
7130
|
try {
|
|
6957
7131
|
raw = await readFile13(filePath, "utf8");
|
|
@@ -7034,7 +7208,7 @@ function isDelegationsFile(value) {
|
|
|
7034
7208
|
}
|
|
7035
7209
|
async function checkDelegations(targetDir) {
|
|
7036
7210
|
const name = "Standing delegations (T13)";
|
|
7037
|
-
const filePath =
|
|
7211
|
+
const filePath = join16(targetDir, ".arcane", "delegations.json");
|
|
7038
7212
|
let raw;
|
|
7039
7213
|
try {
|
|
7040
7214
|
raw = await readFile13(filePath, "utf8");
|
|
@@ -7116,6 +7290,49 @@ async function checkPlatformBranchPolicy(targetDir) {
|
|
|
7116
7290
|
message: "remote is neither GitHub nor Azure DevOps \u2014 platform policy verification not available for this provider"
|
|
7117
7291
|
};
|
|
7118
7292
|
}
|
|
7293
|
+
async function checkManagedFileModes(targetDir) {
|
|
7294
|
+
const name = "Managed files are regular files (#357)";
|
|
7295
|
+
let manifest;
|
|
7296
|
+
try {
|
|
7297
|
+
manifest = await readManifest(targetDir);
|
|
7298
|
+
} catch {
|
|
7299
|
+
return { name, passed: true, skipped: true, message: "no .arcane.json \u2014 nothing to check" };
|
|
7300
|
+
}
|
|
7301
|
+
try {
|
|
7302
|
+
await runGit(targetDir, ["rev-parse", "--git-dir"]);
|
|
7303
|
+
} catch {
|
|
7304
|
+
return { name, passed: true, skipped: true, message: "not a git repository \u2014 the index cannot be read" };
|
|
7305
|
+
}
|
|
7306
|
+
const managed = [...new Set(manifest.components.flatMap((c) => c.files))];
|
|
7307
|
+
const linkTracked = await listSymlinkTrackedPaths(targetDir);
|
|
7308
|
+
const asLinks = managed.filter((file) => linkTracked.has(file));
|
|
7309
|
+
const pathBodies = [];
|
|
7310
|
+
for (const file of managed) {
|
|
7311
|
+
if (!isClientShimPath(file) || asLinks.includes(file)) continue;
|
|
7312
|
+
let body;
|
|
7313
|
+
try {
|
|
7314
|
+
body = await readFile13(join16(targetDir, file), "utf-8");
|
|
7315
|
+
} catch {
|
|
7316
|
+
continue;
|
|
7317
|
+
}
|
|
7318
|
+
if (looksLikeBarePath(body)) pathBodies.push(file);
|
|
7319
|
+
}
|
|
7320
|
+
if (asLinks.length === 0 && pathBodies.length === 0) {
|
|
7321
|
+
return { name, passed: true, message: `${managed.length} managed path(s), all regular files` };
|
|
7322
|
+
}
|
|
7323
|
+
const parts = [];
|
|
7324
|
+
if (asLinks.length > 0) {
|
|
7325
|
+
parts.push(`${asLinks.length} tracked as a symlink (mode 120000): ${asLinks.join(", ")}`);
|
|
7326
|
+
}
|
|
7327
|
+
if (pathBodies.length > 0) {
|
|
7328
|
+
parts.push(`${pathBodies.length} client shim(s) whose body is only a path: ${pathBodies.join(", ")}`);
|
|
7329
|
+
}
|
|
7330
|
+
return {
|
|
7331
|
+
name,
|
|
7332
|
+
passed: false,
|
|
7333
|
+
message: parts.join("; ") + " \u2014 clients silently skip or empty these spells. Run `spell update` (it rewrites them as regular files and stages the typechange), then commit."
|
|
7334
|
+
};
|
|
7335
|
+
}
|
|
7119
7336
|
async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
|
|
7120
7337
|
const name = "Spell scope (ARC-045)";
|
|
7121
7338
|
let manifest;
|
|
@@ -7147,7 +7364,7 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
|
|
|
7147
7364
|
let probed = false;
|
|
7148
7365
|
const probe = "spell-status.md";
|
|
7149
7366
|
try {
|
|
7150
|
-
const entries = await readdir9(
|
|
7367
|
+
const entries = await readdir9(join16(storeRoot, USER_TIER_SPELLS_DIR));
|
|
7151
7368
|
const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
|
|
7152
7369
|
spellCount = spells.length;
|
|
7153
7370
|
probed = spells.includes(probe);
|
|
@@ -7211,6 +7428,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
7211
7428
|
checkMcpConfig(targetDir),
|
|
7212
7429
|
checkUserTier(resolveHomeDir(), options.packageVersion),
|
|
7213
7430
|
checkSpellScope(targetDir),
|
|
7431
|
+
checkManagedFileModes(targetDir),
|
|
7214
7432
|
checkGovernancePlaceholders(targetDir),
|
|
7215
7433
|
checkComponentRequires(targetDir),
|
|
7216
7434
|
checkNewlyAvailableComponents(targetDir)
|
|
@@ -7261,7 +7479,7 @@ import { readFile as readFile15 } from "fs/promises";
|
|
|
7261
7479
|
|
|
7262
7480
|
// src/modules/ward.ts
|
|
7263
7481
|
import { readdir as readdir10, readFile as readFile14 } from "fs/promises";
|
|
7264
|
-
import { join as
|
|
7482
|
+
import { join as join17, relative as relative2 } from "path";
|
|
7265
7483
|
async function scanFileAllRules(absolutePath, displayPath, rules) {
|
|
7266
7484
|
const findings = [];
|
|
7267
7485
|
let content;
|
|
@@ -7324,7 +7542,7 @@ async function scanFilenames(root, rules, repoRoot = root) {
|
|
|
7324
7542
|
return findings;
|
|
7325
7543
|
}
|
|
7326
7544
|
for (const entry of entries) {
|
|
7327
|
-
const absolutePath =
|
|
7545
|
+
const absolutePath = join17(root, entry.name);
|
|
7328
7546
|
if (entry.isDirectory()) {
|
|
7329
7547
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
7330
7548
|
findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
|
|
@@ -7439,7 +7657,7 @@ async function runWardCli(targetDir, options = {}, exclusions = []) {
|
|
|
7439
7657
|
}
|
|
7440
7658
|
|
|
7441
7659
|
// src/commands/unblock-push.ts
|
|
7442
|
-
import { basename as
|
|
7660
|
+
import { basename as basename3, resolve as resolve4 } from "path";
|
|
7443
7661
|
import { input as input4 } from "@inquirer/prompts";
|
|
7444
7662
|
async function runUnblockPush(targetDir) {
|
|
7445
7663
|
let manifest;
|
|
@@ -7471,7 +7689,7 @@ async function runUnblockPush(targetDir) {
|
|
|
7471
7689
|
process.exit(1);
|
|
7472
7690
|
return;
|
|
7473
7691
|
}
|
|
7474
|
-
const repoName =
|
|
7692
|
+
const repoName = basename3(resolve4(targetDir));
|
|
7475
7693
|
if (repoName === "") {
|
|
7476
7694
|
console.error(
|
|
7477
7695
|
"Could not determine a repository name to confirm against (is this repository at a filesystem root?). Refusing to unblock."
|
|
@@ -7760,10 +7978,10 @@ import * as client from "openid-client";
|
|
|
7760
7978
|
// src/modules/result-pages.ts
|
|
7761
7979
|
import { createHash as createHash3 } from "crypto";
|
|
7762
7980
|
import { readFile as readFile16 } from "fs/promises";
|
|
7763
|
-
import { join as
|
|
7981
|
+
import { join as join18 } from "path";
|
|
7764
7982
|
var RESULT_KINDS = ["success", "denied", "failure"];
|
|
7765
7983
|
function resultPagePath(kind) {
|
|
7766
|
-
return
|
|
7984
|
+
return join18("sign-in", `sign-in-callback-${kind}.html`);
|
|
7767
7985
|
}
|
|
7768
7986
|
function styleHashes(html) {
|
|
7769
7987
|
return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((match) => {
|
|
@@ -7811,7 +8029,7 @@ var PLAIN_RESULT_PAGES = {
|
|
|
7811
8029
|
async function loadResultPages(assetsDir) {
|
|
7812
8030
|
try {
|
|
7813
8031
|
const [success, denied, failure] = await Promise.all(
|
|
7814
|
-
RESULT_KINDS.map(async (kind) => resultPage(await readFile16(
|
|
8032
|
+
RESULT_KINDS.map(async (kind) => resultPage(await readFile16(join18(assetsDir, resultPagePath(kind)), "utf8")))
|
|
7815
8033
|
);
|
|
7816
8034
|
return { success, denied, failure };
|
|
7817
8035
|
} catch {
|
|
@@ -7889,9 +8107,9 @@ async function browserLogin(config, options) {
|
|
|
7889
8107
|
const state = client.randomState();
|
|
7890
8108
|
const nonce = client.randomNonce();
|
|
7891
8109
|
const server = createServer();
|
|
7892
|
-
await new Promise((
|
|
8110
|
+
await new Promise((resolve6, reject) => {
|
|
7893
8111
|
server.once("error", reject);
|
|
7894
|
-
server.listen(0, "127.0.0.1", () =>
|
|
8112
|
+
server.listen(0, "127.0.0.1", () => resolve6());
|
|
7895
8113
|
});
|
|
7896
8114
|
const address = server.address();
|
|
7897
8115
|
if (address === null || typeof address === "string") {
|
|
@@ -7917,7 +8135,7 @@ async function browserLogin(config, options) {
|
|
|
7917
8135
|
server.closeAllConnections?.();
|
|
7918
8136
|
server.close();
|
|
7919
8137
|
};
|
|
7920
|
-
const tokens = new Promise((
|
|
8138
|
+
const tokens = new Promise((resolve6, reject) => {
|
|
7921
8139
|
server.on("request", (req, res) => {
|
|
7922
8140
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
7923
8141
|
if (RESULT_PATHS.has(url.pathname)) {
|
|
@@ -7928,7 +8146,7 @@ async function browserLogin(config, options) {
|
|
|
7928
8146
|
const served = browserResult;
|
|
7929
8147
|
const page = pages[served.kind];
|
|
7930
8148
|
const settle = () => {
|
|
7931
|
-
if (served.kind === "success")
|
|
8149
|
+
if (served.kind === "success") resolve6(served.result);
|
|
7932
8150
|
else reject(served.error);
|
|
7933
8151
|
};
|
|
7934
8152
|
res.once("finish", settle).once("close", settle);
|
|
@@ -7957,7 +8175,7 @@ async function browserLogin(config, options) {
|
|
|
7957
8175
|
}).end();
|
|
7958
8176
|
if (result.kind === "success") {
|
|
7959
8177
|
if (timer) clearTimeout(timer);
|
|
7960
|
-
timer = setTimeout(() =>
|
|
8178
|
+
timer = setTimeout(() => resolve6(result.result), Math.min(timeoutMs, 5e3));
|
|
7961
8179
|
}
|
|
7962
8180
|
};
|
|
7963
8181
|
const errorCode = url.searchParams.get("error");
|
|
@@ -8009,12 +8227,12 @@ async function refreshSession(config, refreshToken, scopes) {
|
|
|
8009
8227
|
}
|
|
8010
8228
|
function openInDefaultBrowser(url) {
|
|
8011
8229
|
const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
8012
|
-
return new Promise((
|
|
8230
|
+
return new Promise((resolve6) => {
|
|
8013
8231
|
try {
|
|
8014
|
-
const child = execFile3(command, args, { windowsHide: true }, (error) =>
|
|
8015
|
-
child.on("error", () =>
|
|
8232
|
+
const child = execFile3(command, args, { windowsHide: true }, (error) => resolve6(!error));
|
|
8233
|
+
child.on("error", () => resolve6(false));
|
|
8016
8234
|
} catch {
|
|
8017
|
-
|
|
8235
|
+
resolve6(false);
|
|
8018
8236
|
}
|
|
8019
8237
|
});
|
|
8020
8238
|
}
|
|
@@ -8023,7 +8241,7 @@ function openInDefaultBrowser(url) {
|
|
|
8023
8241
|
import { execFileSync } from "child_process";
|
|
8024
8242
|
import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
8025
8243
|
import { userInfo } from "os";
|
|
8026
|
-
import { dirname as
|
|
8244
|
+
import { dirname as dirname11, join as join19 } from "path";
|
|
8027
8245
|
|
|
8028
8246
|
// src/modules/keychain-backend.ts
|
|
8029
8247
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -8112,7 +8330,7 @@ var FileSecretBackend = class {
|
|
|
8112
8330
|
rmSync(this.filePath, { force: true });
|
|
8113
8331
|
return;
|
|
8114
8332
|
}
|
|
8115
|
-
mkdirSync(
|
|
8333
|
+
mkdirSync(dirname11(this.filePath), { recursive: true });
|
|
8116
8334
|
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
8117
8335
|
writeFileSync(tmp, JSON.stringify(entries), { encoding: "utf8", mode: 384 });
|
|
8118
8336
|
try {
|
|
@@ -8157,7 +8375,7 @@ function restrictToCurrentUser(filePath) {
|
|
|
8157
8375
|
});
|
|
8158
8376
|
}
|
|
8159
8377
|
function insecureSessionFilePath(homeDir) {
|
|
8160
|
-
return
|
|
8378
|
+
return join19(userTierRoot(homeDir), INSECURE_SESSION_FILE_NAME);
|
|
8161
8379
|
}
|
|
8162
8380
|
function createSessionStorage(options = {}) {
|
|
8163
8381
|
const keychain = new ChunkedSecretStore(options.keychainBackend ?? createKeychainBackend());
|
|
@@ -8425,15 +8643,15 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
|
|
|
8425
8643
|
|
|
8426
8644
|
// src/commands/report.ts
|
|
8427
8645
|
import { access as access5 } from "fs/promises";
|
|
8428
|
-
import { join as
|
|
8646
|
+
import { join as join22 } from "path";
|
|
8429
8647
|
|
|
8430
8648
|
// src/modules/show-report/generate.ts
|
|
8431
|
-
import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as
|
|
8432
|
-
import { basename as
|
|
8649
|
+
import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
|
|
8650
|
+
import { basename as basename4, dirname as dirname12, isAbsolute, join as join21, relative as relative3, resolve as resolve5 } from "path";
|
|
8433
8651
|
|
|
8434
8652
|
// src/modules/show-report/model.ts
|
|
8435
8653
|
import { readFile as readFile17 } from "fs/promises";
|
|
8436
|
-
import { join as
|
|
8654
|
+
import { join as join20 } from "path";
|
|
8437
8655
|
|
|
8438
8656
|
// src/modules/show-report/plan-parser.ts
|
|
8439
8657
|
function parseFrontmatter(content) {
|
|
@@ -8802,11 +9020,11 @@ function buildCorrections(ledgerRows, programName) {
|
|
|
8802
9020
|
};
|
|
8803
9021
|
}
|
|
8804
9022
|
async function buildShowReportModel(options) {
|
|
8805
|
-
const planAbsPath =
|
|
8806
|
-
const queueAbsPath =
|
|
9023
|
+
const planAbsPath = join20(options.rootDir, options.planRelPath);
|
|
9024
|
+
const queueAbsPath = join20(options.rootDir, options.queueRelPath);
|
|
8807
9025
|
const ledgerRelPath = "docs/verification-ledger.md";
|
|
8808
|
-
const ledgerAbsPath =
|
|
8809
|
-
const decisionsAbsPath =
|
|
9026
|
+
const ledgerAbsPath = join20(options.rootDir, ledgerRelPath);
|
|
9027
|
+
const decisionsAbsPath = join20(options.rootDir, "DECISIONS.md");
|
|
8810
9028
|
const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
|
|
8811
9029
|
readFile17(planAbsPath, "utf8"),
|
|
8812
9030
|
readOptional(queueAbsPath),
|
|
@@ -9004,7 +9222,7 @@ function toPosix(path4) {
|
|
|
9004
9222
|
async function programFromPlan(rootDir, slug, planRelPath) {
|
|
9005
9223
|
let planContent;
|
|
9006
9224
|
try {
|
|
9007
|
-
planContent = await readFile18(
|
|
9225
|
+
planContent = await readFile18(join21(rootDir, planRelPath), "utf8");
|
|
9008
9226
|
} catch {
|
|
9009
9227
|
return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
|
|
9010
9228
|
}
|
|
@@ -9016,13 +9234,13 @@ async function programFromPlan(rootDir, slug, planRelPath) {
|
|
|
9016
9234
|
program: {
|
|
9017
9235
|
slug,
|
|
9018
9236
|
planRelPath,
|
|
9019
|
-
queueRelPath: toPosix(
|
|
9237
|
+
queueRelPath: toPosix(join21(dirname12(planRelPath), "OPERATOR-QUEUE.md")),
|
|
9020
9238
|
ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
|
|
9021
9239
|
}
|
|
9022
9240
|
};
|
|
9023
9241
|
}
|
|
9024
9242
|
async function discoverPrograms(rootDir) {
|
|
9025
|
-
const plansDir =
|
|
9243
|
+
const plansDir = join21(rootDir, "docs", "plans");
|
|
9026
9244
|
let entries;
|
|
9027
9245
|
try {
|
|
9028
9246
|
entries = (await readdir11(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
@@ -9040,9 +9258,9 @@ async function discoverPrograms(rootDir) {
|
|
|
9040
9258
|
return { programs, skipped };
|
|
9041
9259
|
}
|
|
9042
9260
|
async function programFromPlanPath(rootDir, planPath) {
|
|
9043
|
-
const planAbs = isAbsolute(planPath) ? planPath :
|
|
9261
|
+
const planAbs = isAbsolute(planPath) ? planPath : resolve5(rootDir, planPath);
|
|
9044
9262
|
const planRelPath = toPosix(relative3(rootDir, planAbs));
|
|
9045
|
-
return programFromPlan(rootDir,
|
|
9263
|
+
return programFromPlan(rootDir, basename4(dirname12(planAbs)), planRelPath);
|
|
9046
9264
|
}
|
|
9047
9265
|
function normalizeLineEndings2(content) {
|
|
9048
9266
|
return content.replace(/\r\n?/g, "\n");
|
|
@@ -9056,7 +9274,7 @@ async function readOptionalFile(path4) {
|
|
|
9056
9274
|
}
|
|
9057
9275
|
}
|
|
9058
9276
|
async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
9059
|
-
const planContent = await readFile18(
|
|
9277
|
+
const planContent = await readFile18(join21(rootDir, program2.planRelPath), "utf8");
|
|
9060
9278
|
const frontmatter = parseFrontmatter(planContent);
|
|
9061
9279
|
const model = await buildShowReportModel({
|
|
9062
9280
|
rootDir,
|
|
@@ -9087,19 +9305,19 @@ async function runReportGeneration(options) {
|
|
|
9087
9305
|
if (!generated.themeApplied) themeSkipped.push(program2.slug);
|
|
9088
9306
|
const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
|
|
9089
9307
|
if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
|
|
9090
|
-
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir :
|
|
9308
|
+
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve5(options.rootDir, options.outDir) : resolve5(options.rootDir, dirname12(program2.planRelPath));
|
|
9091
9309
|
const outRel = toPosix(relative3(options.rootDir, outAbs));
|
|
9092
9310
|
const outputs = [
|
|
9093
9311
|
[`${outRel}/show-report.json`, generated.json],
|
|
9094
9312
|
[`${outRel}/show-report.html`, generated.html]
|
|
9095
9313
|
];
|
|
9096
9314
|
for (const [relPath, expected] of outputs) {
|
|
9097
|
-
const actual = await readOptionalFile(
|
|
9315
|
+
const actual = await readOptionalFile(join21(options.rootDir, relPath));
|
|
9098
9316
|
if (actual === null || normalizeLineEndings2(actual) !== normalizeLineEndings2(expected)) {
|
|
9099
9317
|
drifted.push(`${relPath} does not match a regeneration from its sources.`);
|
|
9100
9318
|
if (options.mode === "write") {
|
|
9101
9319
|
await mkdir8(outAbs, { recursive: true });
|
|
9102
|
-
await
|
|
9320
|
+
await writeFile11(join21(options.rootDir, relPath), expected, "utf8");
|
|
9103
9321
|
repaired.push(relPath);
|
|
9104
9322
|
}
|
|
9105
9323
|
}
|
|
@@ -9110,7 +9328,7 @@ async function runReportGeneration(options) {
|
|
|
9110
9328
|
|
|
9111
9329
|
// src/commands/report.ts
|
|
9112
9330
|
async function runReport(targetDir, options = {}, assetsDir) {
|
|
9113
|
-
const templatePath =
|
|
9331
|
+
const templatePath = join22(assetsDir, "report", "show-report.template.html");
|
|
9114
9332
|
try {
|
|
9115
9333
|
await access5(templatePath);
|
|
9116
9334
|
} catch {
|
|
@@ -9184,10 +9402,10 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
9184
9402
|
}
|
|
9185
9403
|
|
|
9186
9404
|
// src/index.ts
|
|
9187
|
-
import { realpathSync } from "fs";
|
|
9405
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
9188
9406
|
var require3 = createRequire3(import.meta.url);
|
|
9189
9407
|
var pkg = require3("../package.json");
|
|
9190
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ??
|
|
9408
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join23(dirname13(fileURLToPath(import.meta.url)), "assets");
|
|
9191
9409
|
var program = new Command();
|
|
9192
9410
|
var USER_FLAG_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 spells shared by every repository on this machine, fanned out to ~/.agents/skills (Codex, Copilot) and ~/.claude/commands (Claude Code)";
|
|
9193
9411
|
var USER_TIER_AGENTS_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 one agent roster shared by every repository on this machine, rendered to ~/.copilot/agents (VS Code agent modes)";
|
|
@@ -9397,7 +9615,7 @@ agentsCommand.command("list").description("Show the current agent roster with as
|
|
|
9397
9615
|
const scope = opts.user ? "user" : "repo";
|
|
9398
9616
|
await runAgentsList(agentsTargetDir(scope, process.cwd()), scope);
|
|
9399
9617
|
});
|
|
9400
|
-
if (
|
|
9618
|
+
if (realpathSync2(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
9401
9619
|
await program.parseAsync(process.argv);
|
|
9402
9620
|
}
|
|
9403
9621
|
export {
|