arcane-cli 1.12.1 → 1.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -7
- package/dist/assets/.arcane/governance/mobile-release-standards.md +42 -1
- package/dist/assets/.arcane/spells/spell-eas-store-deploy.md +190 -7
- package/dist/assets/.arcane/spells/spell-test.md +56 -1
- package/dist/index.js +598 -403
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -17,14 +17,285 @@ import chalk2 from "chalk";
|
|
|
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,19 +343,64 @@ async function fileExists(filePath) {
|
|
|
72
343
|
return false;
|
|
73
344
|
}
|
|
74
345
|
}
|
|
75
|
-
async function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`Destination already exists: "${relativePath}". Use --force to overwrite.`
|
|
82
|
-
);
|
|
346
|
+
async function replaceSymlinkedDestination(destPath, relativePath, opts) {
|
|
347
|
+
let isLink;
|
|
348
|
+
try {
|
|
349
|
+
isLink = (await lstat(destPath)).isSymbolicLink();
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
83
352
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 = {}) {
|
|
392
|
+
validateTargetPath(targetDir, relativePath);
|
|
393
|
+
const destPath = path.resolve(targetDir, relativePath);
|
|
394
|
+
await ensureDir(path.dirname(destPath));
|
|
395
|
+
if (!opts.force && await fileExists(destPath)) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Destination already exists: "${relativePath}". Use --force to overwrite.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const content = await readFile(srcPath);
|
|
401
|
+
const hash = hashContent(content);
|
|
402
|
+
await writeManagedFile(targetDir, relativePath, content, opts);
|
|
403
|
+
return hash;
|
|
88
404
|
}
|
|
89
405
|
async function removeWithin(targetDir, relativePath) {
|
|
90
406
|
validateTargetPath(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";
|
|
@@ -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,
|
|
@@ -2589,389 +2908,171 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
2589
2908
|
schema_version: 2,
|
|
2590
2909
|
naming_strategy: namingStrategy,
|
|
2591
2910
|
agent_profile: profileId,
|
|
2592
|
-
openclaw: {
|
|
2593
|
-
enabled: true,
|
|
2594
|
-
workspace_root: "~/.openclaw"
|
|
2595
|
-
},
|
|
2596
|
-
roster: rosterEntries
|
|
2597
|
-
};
|
|
2598
|
-
const arcaneDir = join4(baseDir, "agents");
|
|
2599
|
-
if (!options.dryRun) {
|
|
2600
|
-
await mkdir4(arcaneDir, { recursive: true });
|
|
2601
|
-
await writeFile6(join4(baseDir, "agents.yaml"), stringify(roster), "utf8");
|
|
2602
|
-
} else {
|
|
2603
|
-
console.log(` [dry-run] Would write: ${label2}/agents.yaml`);
|
|
2604
|
-
}
|
|
2605
|
-
for (const roleId of selectedRoleIds) {
|
|
2606
|
-
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((resolve6, 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
|
-
resolve6({ stdout, stderr });
|
|
2911
|
+
openclaw: {
|
|
2912
|
+
enabled: true,
|
|
2913
|
+
workspace_root: "~/.openclaw"
|
|
2914
|
+
},
|
|
2915
|
+
roster: rosterEntries
|
|
2916
|
+
};
|
|
2917
|
+
const arcaneDir = join4(baseDir, "agents");
|
|
2918
|
+
if (!options.dryRun) {
|
|
2919
|
+
await mkdir4(arcaneDir, { recursive: true });
|
|
2920
|
+
await writeFile6(join4(baseDir, "agents.yaml"), stringify(roster), "utf8");
|
|
2921
|
+
} else {
|
|
2922
|
+
console.log(` [dry-run] Would write: ${label2}/agents.yaml`);
|
|
2923
|
+
}
|
|
2924
|
+
for (const roleId of selectedRoleIds) {
|
|
2925
|
+
const src = join4(assetsDir, "agents", `${roleId}.yaml`);
|
|
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();
|
|
@@ -4491,14 +4592,14 @@ async function planComponentInstall(component, targetDir, assetsDir, opts = {})
|
|
|
4491
4592
|
}
|
|
4492
4593
|
return { component, files, toCopy: of("missing", "identical"), kept: [], conflicts: of("differs", "directory") };
|
|
4493
4594
|
}
|
|
4494
|
-
async function executeInstallPlan(plan, targetDir) {
|
|
4595
|
+
async function executeInstallPlan(plan, targetDir, opts = {}) {
|
|
4495
4596
|
const files = [];
|
|
4496
4597
|
const fileHashes = {};
|
|
4497
4598
|
const written = [];
|
|
4498
4599
|
try {
|
|
4499
4600
|
for (const { file, src, state } of plan.toCopy) {
|
|
4500
4601
|
const previous = state === "missing" ? null : await readFile8(join9(targetDir, file));
|
|
4501
|
-
fileHashes[file] = await copyFile(src, targetDir, file, { force: true });
|
|
4602
|
+
fileHashes[file] = await copyFile(src, targetDir, file, { force: true, ...opts });
|
|
4502
4603
|
written.push({ file, previous });
|
|
4503
4604
|
files.push(file);
|
|
4504
4605
|
}
|
|
@@ -4652,7 +4753,7 @@ async function runAdd(names, options, targetDir, assetsDir, packageVersion) {
|
|
|
4652
4753
|
|
|
4653
4754
|
// src/commands/update.ts
|
|
4654
4755
|
import { dirname as dirname6, join as join11 } from "path";
|
|
4655
|
-
import { readFile as readFile9
|
|
4756
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4656
4757
|
|
|
4657
4758
|
// src/modules/merge3.ts
|
|
4658
4759
|
var CONFLICT_START = "<<<<<<< yours (edited)";
|
|
@@ -4945,11 +5046,11 @@ function describeInstallPlan(plan) {
|
|
|
4945
5046
|
}
|
|
4946
5047
|
return lines;
|
|
4947
5048
|
}
|
|
4948
|
-
async function executeUpdateInstalls(plan, targetDir, packageVersion) {
|
|
5049
|
+
async function executeUpdateInstalls(plan, targetDir, packageVersion, copyOpts = {}) {
|
|
4949
5050
|
const installed = [];
|
|
4950
5051
|
const lines = [];
|
|
4951
5052
|
for (const item of plan.items) {
|
|
4952
|
-
const { files, fileHashes } = await executeInstallPlan(item.plan, targetDir);
|
|
5053
|
+
const { files, fileHashes } = await executeInstallPlan(item.plan, targetDir, copyOpts);
|
|
4953
5054
|
installed.push({
|
|
4954
5055
|
name: item.plan.component.name,
|
|
4955
5056
|
files,
|
|
@@ -4986,7 +5087,7 @@ async function missingTierWarning(scope, spellScope, homeDir) {
|
|
|
4986
5087
|
} catch {
|
|
4987
5088
|
return [
|
|
4988
5089
|
"",
|
|
4989
|
-
` ! 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).`,
|
|
4990
5091
|
" No client can reach a spell here: the routing block in CLAUDE.md and AGENTS.md still",
|
|
4991
5092
|
" names spells that resolve to nothing, so an agent improvises the workflow instead of",
|
|
4992
5093
|
" failing. Fix it with one of:",
|
|
@@ -5092,8 +5193,17 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
5092
5193
|
if (!await fileExists(filePath)) return "not-found";
|
|
5093
5194
|
if (!prune) return "reported";
|
|
5094
5195
|
if (recordedHash === void 0) {
|
|
5095
|
-
|
|
5096
|
-
|
|
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";
|
|
5097
5207
|
}
|
|
5098
5208
|
if (!await fileMatchesHash(filePath, recordedHash)) {
|
|
5099
5209
|
console.log(` ! Orphaned but edited since install \u2014 not pruning: ${file}`);
|
|
@@ -5237,7 +5347,15 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5237
5347
|
const spellScope = effectiveSpellScope(manifest);
|
|
5238
5348
|
const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
|
|
5239
5349
|
const homeDir = dirname6(targetDir);
|
|
5240
|
-
|
|
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
|
+
}
|
|
5241
5359
|
if (scope === "repo") {
|
|
5242
5360
|
console.warn(
|
|
5243
5361
|
`WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
|
|
@@ -5268,6 +5386,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5268
5386
|
return;
|
|
5269
5387
|
}
|
|
5270
5388
|
}
|
|
5389
|
+
const linkTracked = scope === "repo" ? await listSymlinkTrackedPaths(targetDir) : /* @__PURE__ */ new Set();
|
|
5390
|
+
const conversions = [];
|
|
5271
5391
|
const registryChanges = {
|
|
5272
5392
|
retired: findTrackedRetirements(manifest.components),
|
|
5273
5393
|
...scope === "repo" ? {
|
|
@@ -5307,7 +5427,10 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5307
5427
|
for (const line of describeInstallPlan(installPlan)) console.log(line);
|
|
5308
5428
|
return [];
|
|
5309
5429
|
}
|
|
5310
|
-
const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion
|
|
5430
|
+
const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion, {
|
|
5431
|
+
linkTracked,
|
|
5432
|
+
conversions
|
|
5433
|
+
});
|
|
5311
5434
|
for (const line of lines) console.log(line);
|
|
5312
5435
|
return installed;
|
|
5313
5436
|
};
|
|
@@ -5423,7 +5546,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5423
5546
|
if (options.prune && !options.dryRun && installed.files.every((f) => orphanReport.find((o) => o.file === f)?.status !== "reported")) {
|
|
5424
5547
|
continue;
|
|
5425
5548
|
}
|
|
5426
|
-
|
|
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
|
+
});
|
|
5427
5559
|
continue;
|
|
5428
5560
|
}
|
|
5429
5561
|
throw err;
|
|
@@ -5452,7 +5584,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5452
5584
|
continue;
|
|
5453
5585
|
}
|
|
5454
5586
|
}
|
|
5455
|
-
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);
|
|
5456
5589
|
if (preserveExisting) {
|
|
5457
5590
|
console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
|
|
5458
5591
|
} else if (editedByOperator && isClientShimPath(file)) {
|
|
@@ -5462,7 +5595,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5462
5595
|
` ${options.dryRun ? "[dry-run] Would keep" : "Kept"} customized (not merged): ${file}`
|
|
5463
5596
|
);
|
|
5464
5597
|
} else if (options.dryRun) {
|
|
5465
|
-
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
|
+
);
|
|
5466
5601
|
fileCount++;
|
|
5467
5602
|
} else {
|
|
5468
5603
|
let handled = false;
|
|
@@ -5483,7 +5618,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5483
5618
|
readFile9(srcPath, "utf-8")
|
|
5484
5619
|
]);
|
|
5485
5620
|
const result = merge3(oldVendorContent, currentContent, newContent);
|
|
5486
|
-
await
|
|
5621
|
+
await writeManagedFile(targetDir, file, result.content, { linkTracked, conversions });
|
|
5487
5622
|
fileHashes[file] = await hashFile(srcPath);
|
|
5488
5623
|
if (result.hasConflict) {
|
|
5489
5624
|
conflictedFiles.push(file);
|
|
@@ -5495,7 +5630,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5495
5630
|
}
|
|
5496
5631
|
}
|
|
5497
5632
|
if (!handled) {
|
|
5498
|
-
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" });
|
|
5499
5635
|
if (sameVersionRestore) console.log(` Restored missing: ${file}`);
|
|
5500
5636
|
}
|
|
5501
5637
|
fileCount++;
|
|
@@ -5509,7 +5645,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
5509
5645
|
if (options.dryRun) {
|
|
5510
5646
|
console.log(` [dry-run] Would update directory: ${dir}/`);
|
|
5511
5647
|
} else {
|
|
5512
|
-
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true });
|
|
5648
|
+
const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true, linkTracked, conversions });
|
|
5513
5649
|
for (const { path: copiedPath, hash } of copied) {
|
|
5514
5650
|
updatedFiles.push(copiedPath);
|
|
5515
5651
|
fileHashes[copiedPath] = hash;
|
|
@@ -5573,6 +5709,21 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5573
5709
|
console.log(" Run `spell update --prune` to remove the ones that are safe to delete.");
|
|
5574
5710
|
}
|
|
5575
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
|
+
}
|
|
5576
5727
|
printRegistryChanges();
|
|
5577
5728
|
if (customizedShims.length > 0) {
|
|
5578
5729
|
const verb = options.dryRun ? "[dry-run] Would keep" : "Kept";
|
|
@@ -5668,7 +5819,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5668
5819
|
import { dirname as dirname8 } from "path";
|
|
5669
5820
|
|
|
5670
5821
|
// src/modules/version-check.ts
|
|
5671
|
-
import { readFile as readFile10, writeFile as
|
|
5822
|
+
import { readFile as readFile10, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
|
|
5672
5823
|
import { dirname as dirname7, join as join12 } from "path";
|
|
5673
5824
|
import { homedir as homedir3 } from "os";
|
|
5674
5825
|
import { createRequire } from "module";
|
|
@@ -5700,7 +5851,7 @@ async function readCache(cachePath) {
|
|
|
5700
5851
|
async function writeCache(cachePath, latest) {
|
|
5701
5852
|
await mkdir6(dirname7(cachePath), { recursive: true });
|
|
5702
5853
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5703
|
-
await
|
|
5854
|
+
await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
5704
5855
|
}
|
|
5705
5856
|
async function getLatestVersion(feedUrl) {
|
|
5706
5857
|
const normalizedBase = feedUrl.endsWith("/") ? feedUrl : feedUrl + "/";
|
|
@@ -7139,6 +7290,49 @@ async function checkPlatformBranchPolicy(targetDir) {
|
|
|
7139
7290
|
message: "remote is neither GitHub nor Azure DevOps \u2014 platform policy verification not available for this provider"
|
|
7140
7291
|
};
|
|
7141
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
|
+
}
|
|
7142
7336
|
async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
|
|
7143
7337
|
const name = "Spell scope (ARC-045)";
|
|
7144
7338
|
let manifest;
|
|
@@ -7234,6 +7428,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
7234
7428
|
checkMcpConfig(targetDir),
|
|
7235
7429
|
checkUserTier(resolveHomeDir(), options.packageVersion),
|
|
7236
7430
|
checkSpellScope(targetDir),
|
|
7431
|
+
checkManagedFileModes(targetDir),
|
|
7237
7432
|
checkGovernancePlaceholders(targetDir),
|
|
7238
7433
|
checkComponentRequires(targetDir),
|
|
7239
7434
|
checkNewlyAvailableComponents(targetDir)
|
|
@@ -8451,7 +8646,7 @@ import { access as access5 } from "fs/promises";
|
|
|
8451
8646
|
import { join as join22 } from "path";
|
|
8452
8647
|
|
|
8453
8648
|
// src/modules/show-report/generate.ts
|
|
8454
|
-
import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as
|
|
8649
|
+
import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
|
|
8455
8650
|
import { basename as basename4, dirname as dirname12, isAbsolute, join as join21, relative as relative3, resolve as resolve5 } from "path";
|
|
8456
8651
|
|
|
8457
8652
|
// src/modules/show-report/model.ts
|
|
@@ -9122,7 +9317,7 @@ async function runReportGeneration(options) {
|
|
|
9122
9317
|
drifted.push(`${relPath} does not match a regeneration from its sources.`);
|
|
9123
9318
|
if (options.mode === "write") {
|
|
9124
9319
|
await mkdir8(outAbs, { recursive: true });
|
|
9125
|
-
await
|
|
9320
|
+
await writeFile11(join21(options.rootDir, relPath), expected, "utf8");
|
|
9126
9321
|
repaired.push(relPath);
|
|
9127
9322
|
}
|
|
9128
9323
|
}
|