arcane-cli 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/dist/assets/.arcane/governance/portable-bootstrap.md +1 -1
- package/dist/index.js +426 -116
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,11 +6,11 @@ const require = __createRequire(import.meta.url);
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { createRequire as createRequire2 } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as
|
|
9
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { select as select2, confirm, input as input2 } from "@inquirer/prompts";
|
|
13
|
-
import { dirname as
|
|
13
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
14
14
|
import chalk2 from "chalk";
|
|
15
15
|
|
|
16
16
|
// src/modules/copier.ts
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
readdir,
|
|
21
21
|
readFile,
|
|
22
22
|
rm,
|
|
23
|
+
rmdir,
|
|
23
24
|
writeFile
|
|
24
25
|
} from "fs/promises";
|
|
25
26
|
import { createHash } from "crypto";
|
|
@@ -89,6 +90,20 @@ async function removeWithin(targetDir, relativePath) {
|
|
|
89
90
|
validateTargetPath(targetDir, relativePath);
|
|
90
91
|
await rm(path.resolve(targetDir, relativePath), { force: true });
|
|
91
92
|
}
|
|
93
|
+
async function removeEmptyAncestors(targetDir, relativePath) {
|
|
94
|
+
validateTargetPath(targetDir, relativePath);
|
|
95
|
+
const base = path.resolve(targetDir);
|
|
96
|
+
let dir = path.dirname(path.resolve(targetDir, relativePath));
|
|
97
|
+
while (dir !== base && dir.startsWith(base + path.sep)) {
|
|
98
|
+
try {
|
|
99
|
+
if ((await readdir(dir)).length > 0) return;
|
|
100
|
+
await rmdir(dir);
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
dir = path.dirname(dir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
92
107
|
async function copyDirectory(srcDir, targetDir, relativeDir, opts = {}) {
|
|
93
108
|
validateTargetPath(targetDir, relativeDir);
|
|
94
109
|
const copiedFiles = [];
|
|
@@ -201,6 +216,9 @@ function validateTrackingFields(manifest, filePath) {
|
|
|
201
216
|
if (manifest.scope !== void 0 && !VALID_INSTALL_SCOPES.includes(manifest.scope)) {
|
|
202
217
|
throw new ManifestInvalidFieldError(filePath, "scope", manifest.scope);
|
|
203
218
|
}
|
|
219
|
+
if (manifest.spell_scope !== void 0 && !VALID_INSTALL_SCOPES.includes(manifest.spell_scope)) {
|
|
220
|
+
throw new ManifestInvalidFieldError(filePath, "spell_scope", manifest.spell_scope);
|
|
221
|
+
}
|
|
204
222
|
if (manifest.fanout !== void 0 && !isValidFanoutRecord(manifest.fanout)) {
|
|
205
223
|
throw new ManifestInvalidFieldError(filePath, "fanout", manifest.fanout);
|
|
206
224
|
}
|
|
@@ -866,7 +884,7 @@ function listProfiles() {
|
|
|
866
884
|
}
|
|
867
885
|
|
|
868
886
|
// src/modules/user-tier.ts
|
|
869
|
-
import { lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, rmdir, writeFile as writeFile3 } from "fs/promises";
|
|
887
|
+
import { lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, rmdir as rmdir2, writeFile as writeFile3 } from "fs/promises";
|
|
870
888
|
import { homedir } from "os";
|
|
871
889
|
import { dirname, join, resolve } from "path";
|
|
872
890
|
|
|
@@ -999,6 +1017,14 @@ function componentForScope(component, scope) {
|
|
|
999
1017
|
}
|
|
1000
1018
|
return { name: component.name, description: component.description, files, sourceOverrides };
|
|
1001
1019
|
}
|
|
1020
|
+
function componentForSpellScope(component, spellScope) {
|
|
1021
|
+
if (spellScope === "repo") return component;
|
|
1022
|
+
if (!SPELL_COMPONENT_NAMES.includes(component.name)) return component;
|
|
1023
|
+
return { ...component, files: [] };
|
|
1024
|
+
}
|
|
1025
|
+
function effectiveSpellScope(manifest) {
|
|
1026
|
+
return manifest.spell_scope ?? "repo";
|
|
1027
|
+
}
|
|
1002
1028
|
function spellIdsInStore(components) {
|
|
1003
1029
|
const ids = /* @__PURE__ */ new Set();
|
|
1004
1030
|
for (const component of components) {
|
|
@@ -1009,18 +1035,25 @@ function spellIdsInStore(components) {
|
|
|
1009
1035
|
}
|
|
1010
1036
|
return [...ids].sort();
|
|
1011
1037
|
}
|
|
1038
|
+
var USER_AGENT_FANOUT_DIR = ".copilot/agents";
|
|
1039
|
+
var SPELL_FANOUT_CLIENTS = ["codex", "claude"];
|
|
1012
1040
|
var USER_FANOUT_PATHS = {
|
|
1013
1041
|
codex: (id) => `.agents/skills/${id}/SKILL.md`,
|
|
1014
1042
|
claude: (id) => `.claude/commands/${id}.md`
|
|
1015
1043
|
};
|
|
1044
|
+
function userAgentFanoutPath(slug) {
|
|
1045
|
+
return `${USER_AGENT_FANOUT_DIR}/${slug}.agent.md`;
|
|
1046
|
+
}
|
|
1016
1047
|
var FANOUT_CLIENT_LABELS = {
|
|
1017
1048
|
codex: "Codex/Copilot skills",
|
|
1018
|
-
claude: "Claude Code commands"
|
|
1049
|
+
claude: "Claude Code commands",
|
|
1050
|
+
"copilot-agents": "Copilot agent modes"
|
|
1019
1051
|
};
|
|
1020
1052
|
function clientOfFanoutPath(relativePath) {
|
|
1021
1053
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
1022
1054
|
if (normalized.startsWith(".agents/skills/")) return "codex";
|
|
1023
1055
|
if (normalized.startsWith(".claude/commands/")) return "claude";
|
|
1056
|
+
if (normalized.startsWith(`${USER_AGENT_FANOUT_DIR}/`)) return "copilot-agents";
|
|
1024
1057
|
return void 0;
|
|
1025
1058
|
}
|
|
1026
1059
|
async function readSpellSource(storeRoot, id, fallbackDir) {
|
|
@@ -1076,20 +1109,22 @@ function perSpellDirectory(homeDir, relativePath) {
|
|
|
1076
1109
|
async function removeDirectoryIfEmpty(dir) {
|
|
1077
1110
|
try {
|
|
1078
1111
|
const entries = await readdir2(dir);
|
|
1079
|
-
if (entries.length === 0) await
|
|
1112
|
+
if (entries.length === 0) await rmdir2(dir);
|
|
1080
1113
|
} catch {
|
|
1081
1114
|
}
|
|
1082
1115
|
}
|
|
1083
1116
|
async function syncUserTierFanout(options) {
|
|
1084
1117
|
const rendered = await renderUserFanout(options.storeRoot, options.spellIds, options.fallbackDir);
|
|
1085
|
-
const desired = rendered.files;
|
|
1118
|
+
const desired = [...rendered.files, ...options.extraFiles ?? []];
|
|
1119
|
+
const owned = options.ownedClients;
|
|
1120
|
+
const ownsClient = (client) => owned === void 0 || client !== void 0 && owned.includes(client);
|
|
1086
1121
|
const previous = options.previous ?? {};
|
|
1087
1122
|
const dryRun = Boolean(options.dryRun);
|
|
1088
1123
|
const record = {};
|
|
1089
1124
|
const outcomes = [];
|
|
1090
1125
|
const desiredPaths = new Set(desired.map((f) => f.relativePath));
|
|
1091
1126
|
for (const id of rendered.missing) {
|
|
1092
|
-
for (const client of
|
|
1127
|
+
for (const client of SPELL_FANOUT_CLIENTS) {
|
|
1093
1128
|
const relativePath = USER_FANOUT_PATHS[client](id);
|
|
1094
1129
|
desiredPaths.add(relativePath);
|
|
1095
1130
|
if (previous[relativePath] !== void 0) record[relativePath] = previous[relativePath];
|
|
@@ -1132,6 +1167,10 @@ async function syncUserTierFanout(options) {
|
|
|
1132
1167
|
}
|
|
1133
1168
|
for (const [relativePath, recorded] of Object.entries(previous)) {
|
|
1134
1169
|
if (desiredPaths.has(relativePath)) continue;
|
|
1170
|
+
if (!ownsClient(clientOfFanoutPath(relativePath))) {
|
|
1171
|
+
record[relativePath] = recorded;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1135
1174
|
validateTargetPath(options.homeDir, relativePath);
|
|
1136
1175
|
const target = join(options.homeDir, relativePath);
|
|
1137
1176
|
const state = await classifyTarget(target);
|
|
@@ -1172,7 +1211,7 @@ function describeFanoutOutcomes(outcomes, dryRun = false) {
|
|
|
1172
1211
|
const lines = [];
|
|
1173
1212
|
const prefix = dryRun ? "[dry-run] Would " : "";
|
|
1174
1213
|
const byClient = (status) => {
|
|
1175
|
-
const n = { codex: 0, claude: 0 };
|
|
1214
|
+
const n = { codex: 0, claude: 0, "copilot-agents": 0 };
|
|
1176
1215
|
for (const o of outcomes) {
|
|
1177
1216
|
if (o.status !== status) continue;
|
|
1178
1217
|
const client = clientOfFanoutPath(o.relativePath);
|
|
@@ -1183,8 +1222,9 @@ function describeFanoutOutcomes(outcomes, dryRun = false) {
|
|
|
1183
1222
|
const paths = (status) => outcomes.filter((o) => o.status === status).map((o) => ` ~/${o.relativePath}`).join("\n");
|
|
1184
1223
|
if (counts.written > 0) {
|
|
1185
1224
|
const n = byClient("written");
|
|
1225
|
+
const breakdown = Object.keys(n).filter((client) => n[client] > 0).map((client) => `${n[client]} ${FANOUT_CLIENT_LABELS[client]}`).join(", ");
|
|
1186
1226
|
lines.push(
|
|
1187
|
-
`${prefix}${dryRun ? "write" : "Wrote"} ${counts.written} client file(s): ${
|
|
1227
|
+
`${prefix}${dryRun ? "write" : "Wrote"} ${counts.written} client file(s): ${breakdown}.`
|
|
1188
1228
|
);
|
|
1189
1229
|
}
|
|
1190
1230
|
if (counts.pruned > 0) {
|
|
@@ -1222,7 +1262,7 @@ ${paths("kept-edited")}`
|
|
|
1222
1262
|
async function inspectUserTierFanout(homeDir, record) {
|
|
1223
1263
|
const health = {
|
|
1224
1264
|
total: 0,
|
|
1225
|
-
byClient: { codex: 0, claude: 0 },
|
|
1265
|
+
byClient: { codex: 0, claude: 0, "copilot-agents": 0 },
|
|
1226
1266
|
missing: [],
|
|
1227
1267
|
customized: []
|
|
1228
1268
|
};
|
|
@@ -1399,7 +1439,7 @@ function printWarning(message) {
|
|
|
1399
1439
|
// src/modules/agents.ts
|
|
1400
1440
|
import { select, checkbox, Separator } from "@inquirer/prompts";
|
|
1401
1441
|
import { mkdir as mkdir4, writeFile as writeFile6, copyFile as copyFile2 } from "fs/promises";
|
|
1402
|
-
import { join as join4 } from "path";
|
|
1442
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1403
1443
|
import { stringify } from "yaml";
|
|
1404
1444
|
|
|
1405
1445
|
// src/config/agent-roles.ts
|
|
@@ -1808,9 +1848,15 @@ var AgentDefinitionNotFoundError = class extends Error {
|
|
|
1808
1848
|
}
|
|
1809
1849
|
};
|
|
1810
1850
|
var AgentRosterNotFoundError = class extends Error {
|
|
1811
|
-
|
|
1851
|
+
/**
|
|
1852
|
+
* Names the tier the caller actually asked for. A `--user` run that reports
|
|
1853
|
+
* the repository's path sends the operator to the wrong file and the wrong
|
|
1854
|
+
* command, which is worse than no message.
|
|
1855
|
+
*/
|
|
1856
|
+
constructor(scope = "repo") {
|
|
1857
|
+
const user = scope === "user";
|
|
1812
1858
|
super(
|
|
1813
|
-
|
|
1859
|
+
`No agent roster found at ${user ? "~/.arcane/agents.yaml" : ".arcane/agents.yaml"}. Run "spell agents init${user ? " --user" : ""}" to create one.`
|
|
1814
1860
|
);
|
|
1815
1861
|
this.name = "AgentRosterNotFoundError";
|
|
1816
1862
|
}
|
|
@@ -1833,30 +1879,33 @@ async function loadAgentDefinition(agentsDir, id) {
|
|
|
1833
1879
|
}
|
|
1834
1880
|
return validateAgentDefinition(parseYaml2(content, filePath), filePath);
|
|
1835
1881
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1882
|
+
function agentsBaseDir(targetDir, scope = "repo") {
|
|
1883
|
+
return scope === "user" ? targetDir : join2(targetDir, ".arcane");
|
|
1884
|
+
}
|
|
1885
|
+
async function loadRoster(targetDir, scope = "repo") {
|
|
1886
|
+
const rosterPath = join2(agentsBaseDir(targetDir, scope), "agents.yaml");
|
|
1838
1887
|
let content;
|
|
1839
1888
|
try {
|
|
1840
1889
|
content = await readFile4(rosterPath, "utf8");
|
|
1841
1890
|
} catch {
|
|
1842
|
-
throw new AgentRosterNotFoundError();
|
|
1891
|
+
throw new AgentRosterNotFoundError(scope);
|
|
1843
1892
|
}
|
|
1844
1893
|
return validateAgentRoster(parseYaml2(content, rosterPath), rosterPath);
|
|
1845
1894
|
}
|
|
1846
|
-
async function rosterExists(targetDir) {
|
|
1895
|
+
async function rosterExists(targetDir, scope = "repo") {
|
|
1847
1896
|
try {
|
|
1848
|
-
await access2(join2(targetDir,
|
|
1897
|
+
await access2(join2(agentsBaseDir(targetDir, scope), "agents.yaml"));
|
|
1849
1898
|
return true;
|
|
1850
1899
|
} catch {
|
|
1851
1900
|
return false;
|
|
1852
1901
|
}
|
|
1853
1902
|
}
|
|
1854
|
-
function projectAgentsDir(targetDir) {
|
|
1855
|
-
return join2(targetDir,
|
|
1903
|
+
function projectAgentsDir(targetDir, scope = "repo") {
|
|
1904
|
+
return join2(agentsBaseDir(targetDir, scope), "agents");
|
|
1856
1905
|
}
|
|
1857
1906
|
|
|
1858
1907
|
// src/modules/agent-generator.ts
|
|
1859
|
-
import { writeFile as writeFile5, mkdir as mkdir3 } from "fs/promises";
|
|
1908
|
+
import { writeFile as writeFile5, mkdir as mkdir3, readdir as readdir4 } from "fs/promises";
|
|
1860
1909
|
import { join as join3 } from "path";
|
|
1861
1910
|
import { homedir as homedir2 } from "os";
|
|
1862
1911
|
|
|
@@ -2065,11 +2114,23 @@ function renderAgentRosterSection(entries) {
|
|
|
2065
2114
|
${rows}
|
|
2066
2115
|
`;
|
|
2067
2116
|
}
|
|
2117
|
+
async function existingAgentFiles(agentsDir) {
|
|
2118
|
+
try {
|
|
2119
|
+
const entries = await readdir4(agentsDir);
|
|
2120
|
+
return entries.filter((name) => name.endsWith(".agent.md")).sort().map((name) => `.github/agents/${name}`);
|
|
2121
|
+
} catch {
|
|
2122
|
+
return [];
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2068
2125
|
async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
2069
2126
|
const synced = [];
|
|
2070
2127
|
const skipped = [];
|
|
2071
2128
|
let hasUnresolvedRoles = false;
|
|
2072
|
-
const
|
|
2129
|
+
const userTier = options.scope === "user";
|
|
2130
|
+
const repoOptedOut = !userTier && options.spellScope === "user";
|
|
2131
|
+
const userAgentFiles = [];
|
|
2132
|
+
const unmanagedAgentFiles = [];
|
|
2133
|
+
const projectDefs = projectAgentsDir(targetDir, userTier ? "user" : "repo");
|
|
2073
2134
|
const bundledDefs = join3(assetsDir, "agents");
|
|
2074
2135
|
const resolved = [];
|
|
2075
2136
|
for (const entry of roster.roster) {
|
|
@@ -2091,7 +2152,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2091
2152
|
displayName: entry.name ?? entry.definition
|
|
2092
2153
|
});
|
|
2093
2154
|
}
|
|
2094
|
-
if (roster.openclaw.enabled && options.openclaw !== false) {
|
|
2155
|
+
if (!userTier && roster.openclaw.enabled && options.openclaw !== false) {
|
|
2095
2156
|
const openclawRoot = roster.openclaw.workspace_root.replace(
|
|
2096
2157
|
/^~/,
|
|
2097
2158
|
homedir2()
|
|
@@ -2156,34 +2217,46 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
|
|
|
2156
2217
|
}
|
|
2157
2218
|
if (options.copilot !== false) {
|
|
2158
2219
|
const agentsDir = join3(targetDir, ".github", "agents");
|
|
2159
|
-
if (!options.dryRun) {
|
|
2160
|
-
await mkdir3(agentsDir, { recursive: true });
|
|
2220
|
+
if (!options.dryRun && !userTier) {
|
|
2221
|
+
await mkdir3(repoOptedOut ? join3(targetDir, ".github") : agentsDir, { recursive: true });
|
|
2161
2222
|
}
|
|
2162
2223
|
for (const { def, displayName } of resolved) {
|
|
2163
2224
|
if (!def.clients.copilot?.agent_file) continue;
|
|
2164
2225
|
const slug = displayName.toLowerCase().replace(/\s+/g, "-");
|
|
2165
2226
|
const fileName = `${slug}.agent.md`;
|
|
2227
|
+
const content = renderCopilotAgent(def, displayName);
|
|
2228
|
+
if (userTier) {
|
|
2229
|
+
const relativePath = userAgentFanoutPath(slug);
|
|
2230
|
+
userAgentFiles.push({ relativePath, client: "copilot-agents", content });
|
|
2231
|
+
synced.push(`~/${relativePath}`);
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
if (repoOptedOut) continue;
|
|
2166
2235
|
if (!options.dryRun) {
|
|
2167
|
-
await writeFile5(
|
|
2168
|
-
join3(agentsDir, fileName),
|
|
2169
|
-
renderCopilotAgent(def, displayName),
|
|
2170
|
-
"utf8"
|
|
2171
|
-
);
|
|
2236
|
+
await writeFile5(join3(agentsDir, fileName), content, "utf8");
|
|
2172
2237
|
} else {
|
|
2173
2238
|
console.log(` [dry-run] Would write: .github/agents/${fileName}`);
|
|
2174
2239
|
}
|
|
2175
2240
|
synced.push(`.github/agents/${fileName}`);
|
|
2176
2241
|
}
|
|
2177
|
-
|
|
2242
|
+
if (repoOptedOut) {
|
|
2243
|
+
unmanagedAgentFiles.push(...await existingAgentFiles(agentsDir));
|
|
2244
|
+
}
|
|
2245
|
+
if (!userTier) {
|
|
2246
|
+
const copilotSection = `${renderSpellRoutingSection()}
|
|
2178
2247
|
${renderAgentRosterSection(resolved)}`;
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2248
|
+
const copilotMerged = await mergeIntoFile(
|
|
2249
|
+
targetDir,
|
|
2250
|
+
".github/copilot-instructions.md",
|
|
2251
|
+
copilotSection,
|
|
2252
|
+
{ force: true, dryRun: options.dryRun }
|
|
2253
|
+
);
|
|
2254
|
+
if (copilotMerged)
|
|
2255
|
+
synced.push(".github/copilot-instructions.md (agents section)");
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (userTier) {
|
|
2259
|
+
return { synced, skipped, hasUnresolvedRoles, userAgentFiles, unmanagedAgentFiles };
|
|
2187
2260
|
}
|
|
2188
2261
|
if (options.claude !== false) {
|
|
2189
2262
|
const claudeSection = `${renderSpellRoutingSection()}
|
|
@@ -2207,12 +2280,83 @@ ${renderAgentRosterSection(resolved)}`;
|
|
|
2207
2280
|
);
|
|
2208
2281
|
if (codexMerged) synced.push("AGENTS.md (agents section)");
|
|
2209
2282
|
}
|
|
2210
|
-
return { synced, skipped, hasUnresolvedRoles };
|
|
2283
|
+
return { synced, skipped, hasUnresolvedRoles, userAgentFiles, unmanagedAgentFiles };
|
|
2211
2284
|
}
|
|
2212
2285
|
|
|
2213
2286
|
// src/modules/agents.ts
|
|
2287
|
+
function agentsTargetDir(scope, cwd) {
|
|
2288
|
+
return scope === "user" ? userTierRoot() : cwd;
|
|
2289
|
+
}
|
|
2290
|
+
async function requireUserStore(storeRoot) {
|
|
2291
|
+
try {
|
|
2292
|
+
return await readManifest(storeRoot);
|
|
2293
|
+
} catch {
|
|
2294
|
+
console.error(
|
|
2295
|
+
`
|
|
2296
|
+
\u2717 No user tier at ${storeRoot}.
|
|
2297
|
+
Run \`spell init --user\` once to create it, then re-run this command.
|
|
2298
|
+
`
|
|
2299
|
+
);
|
|
2300
|
+
process.exit(1);
|
|
2301
|
+
throw new Error("unreachable");
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
async function repoSpellScope(targetDir) {
|
|
2305
|
+
try {
|
|
2306
|
+
return effectiveSpellScope(await readManifest(targetDir));
|
|
2307
|
+
} catch {
|
|
2308
|
+
return "repo";
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
async function performAgentSync(targetDir, assetsDir, roster, options) {
|
|
2312
|
+
const scope = options.scope ?? "repo";
|
|
2313
|
+
const spellScope = scope === "user" ? void 0 : await repoSpellScope(targetDir);
|
|
2314
|
+
const result = await syncAgents(targetDir, assetsDir, roster, {
|
|
2315
|
+
...options,
|
|
2316
|
+
scope,
|
|
2317
|
+
...spellScope === void 0 ? {} : { spellScope }
|
|
2318
|
+
});
|
|
2319
|
+
if (scope === "user") {
|
|
2320
|
+
const manifest = await requireUserStore(targetDir);
|
|
2321
|
+
const fanout = await syncUserTierFanout({
|
|
2322
|
+
homeDir: dirname2(targetDir),
|
|
2323
|
+
storeRoot: targetDir,
|
|
2324
|
+
spellIds: [],
|
|
2325
|
+
extraFiles: result.userAgentFiles,
|
|
2326
|
+
ownedClients: ["copilot-agents"],
|
|
2327
|
+
previous: manifest.fanout,
|
|
2328
|
+
dryRun: options.dryRun,
|
|
2329
|
+
fallbackDir: assetsDir
|
|
2330
|
+
});
|
|
2331
|
+
for (const line of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) {
|
|
2332
|
+
console.log(` ${line}`);
|
|
2333
|
+
}
|
|
2334
|
+
if (!options.dryRun) {
|
|
2335
|
+
await writeManifest(targetDir, { ...manifest, fanout: fanout.record });
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (result.unmanagedAgentFiles.length > 0) {
|
|
2339
|
+
console.log(
|
|
2340
|
+
`
|
|
2341
|
+
! This repository takes its spells from the user tier (spell_scope: "user"), so it no
|
|
2342
|
+
longer receives agent files of its own. ${result.unmanagedAgentFiles.length} are still on disk
|
|
2343
|
+
from before the opt-out. Nothing here deletes them -- no component ever tracked
|
|
2344
|
+
them, so there is no recorded hash to prove one untouched. Remove them yourself:
|
|
2345
|
+
`
|
|
2346
|
+
);
|
|
2347
|
+
for (const file of result.unmanagedAgentFiles) console.log(` ${file}`);
|
|
2348
|
+
console.log(`
|
|
2349
|
+
git rm ${result.unmanagedAgentFiles.join(" ")}
|
|
2350
|
+
`);
|
|
2351
|
+
}
|
|
2352
|
+
return result;
|
|
2353
|
+
}
|
|
2214
2354
|
async function runAgentsInit(options, targetDir, assetsDir) {
|
|
2215
|
-
|
|
2355
|
+
const scope = options.scope ?? "repo";
|
|
2356
|
+
if (scope === "user") await requireUserStore(targetDir);
|
|
2357
|
+
const baseDir = agentsBaseDir(targetDir, scope);
|
|
2358
|
+
const label = scope === "user" ? "~/.arcane" : ".arcane";
|
|
2359
|
+
if (!options.force && await rosterExists(targetDir, scope)) {
|
|
2216
2360
|
console.log(
|
|
2217
2361
|
'Agent roster already exists at .arcane/agents.yaml. Run "spell agents sync" to regenerate outputs, or use --force to reinitialize.'
|
|
2218
2362
|
);
|
|
@@ -2306,16 +2450,12 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
2306
2450
|
},
|
|
2307
2451
|
roster: rosterEntries
|
|
2308
2452
|
};
|
|
2309
|
-
const arcaneDir = join4(
|
|
2453
|
+
const arcaneDir = join4(baseDir, "agents");
|
|
2310
2454
|
if (!options.dryRun) {
|
|
2311
2455
|
await mkdir4(arcaneDir, { recursive: true });
|
|
2312
|
-
await writeFile6(
|
|
2313
|
-
join4(targetDir, ".arcane", "agents.yaml"),
|
|
2314
|
-
stringify(roster),
|
|
2315
|
-
"utf8"
|
|
2316
|
-
);
|
|
2456
|
+
await writeFile6(join4(baseDir, "agents.yaml"), stringify(roster), "utf8");
|
|
2317
2457
|
} else {
|
|
2318
|
-
console.log(
|
|
2458
|
+
console.log(` [dry-run] Would write: ${label}/agents.yaml`);
|
|
2319
2459
|
}
|
|
2320
2460
|
for (const roleId of selectedRoleIds) {
|
|
2321
2461
|
const src = join4(assetsDir, "agents", `${roleId}.yaml`);
|
|
@@ -2327,7 +2467,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
2327
2467
|
console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
|
|
2328
2468
|
}
|
|
2329
2469
|
} else {
|
|
2330
|
-
console.log(` [dry-run] Would copy:
|
|
2470
|
+
console.log(` [dry-run] Would copy: ${label}/agents/${roleId}.yaml`);
|
|
2331
2471
|
}
|
|
2332
2472
|
}
|
|
2333
2473
|
if (options.dryRun) {
|
|
@@ -2338,20 +2478,24 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
2338
2478
|
return;
|
|
2339
2479
|
}
|
|
2340
2480
|
printStep("Syncing agent definitions to all clients...");
|
|
2341
|
-
const { synced, skipped, hasUnresolvedRoles } = await
|
|
2481
|
+
const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
|
|
2342
2482
|
targetDir,
|
|
2343
2483
|
assetsDir,
|
|
2344
2484
|
roster,
|
|
2345
|
-
{ dryRun: false }
|
|
2485
|
+
{ dryRun: false, scope }
|
|
2346
2486
|
);
|
|
2347
2487
|
printSuccess("Agent roster initialized");
|
|
2348
2488
|
console.log(` \u{1F4CB} Profile: ${profileId} \xB7 Naming: ${namingStrategy} \xB7 Agents: ${rosterEntries.length}`);
|
|
2349
|
-
console.log(
|
|
2489
|
+
console.log(
|
|
2490
|
+
` \u{1F504} Synced: ${synced.length} outputs (${scope === "user" ? "Copilot agent modes in ~/.copilot/agents" : "Copilot, Claude, Codex, OpenClaw"})`
|
|
2491
|
+
);
|
|
2350
2492
|
if (skipped.length > 0) {
|
|
2351
2493
|
console.log(` \u26A0\uFE0F Skipped: ${skipped.length} (${skipped.join(", ")})`);
|
|
2352
2494
|
}
|
|
2353
2495
|
console.log();
|
|
2354
|
-
console.log(
|
|
2496
|
+
console.log(
|
|
2497
|
+
` \u{1F4A1} Customize: edit ${label}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
|
|
2498
|
+
);
|
|
2355
2499
|
console.log();
|
|
2356
2500
|
if (hasUnresolvedRoles) {
|
|
2357
2501
|
console.error(
|
|
@@ -2361,9 +2505,10 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
|
|
|
2361
2505
|
}
|
|
2362
2506
|
}
|
|
2363
2507
|
async function runAgentsSync(options, targetDir, assetsDir) {
|
|
2508
|
+
const scope = options.scope ?? "repo";
|
|
2364
2509
|
let roster;
|
|
2365
2510
|
try {
|
|
2366
|
-
roster = await loadRoster(targetDir);
|
|
2511
|
+
roster = await loadRoster(targetDir, scope);
|
|
2367
2512
|
} catch (err) {
|
|
2368
2513
|
if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
|
|
2369
2514
|
console.error(err.message);
|
|
@@ -2377,11 +2522,11 @@ async function runAgentsSync(options, targetDir, assetsDir) {
|
|
|
2377
2522
|
} else {
|
|
2378
2523
|
console.log("\n Syncing agents...\n");
|
|
2379
2524
|
}
|
|
2380
|
-
const { synced, skipped, hasUnresolvedRoles } = await
|
|
2525
|
+
const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
|
|
2381
2526
|
targetDir,
|
|
2382
2527
|
assetsDir,
|
|
2383
2528
|
roster,
|
|
2384
|
-
options
|
|
2529
|
+
{ ...options, scope }
|
|
2385
2530
|
);
|
|
2386
2531
|
if (options.dryRun) {
|
|
2387
2532
|
console.log(`
|
|
@@ -2408,10 +2553,10 @@ async function runAgentsSync(options, targetDir, assetsDir) {
|
|
|
2408
2553
|
process.exitCode = 1;
|
|
2409
2554
|
}
|
|
2410
2555
|
}
|
|
2411
|
-
async function runAgentsList(targetDir) {
|
|
2556
|
+
async function runAgentsList(targetDir, scope = "repo") {
|
|
2412
2557
|
let roster;
|
|
2413
2558
|
try {
|
|
2414
|
-
roster = await loadRoster(targetDir);
|
|
2559
|
+
roster = await loadRoster(targetDir, scope);
|
|
2415
2560
|
} catch (err) {
|
|
2416
2561
|
if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
|
|
2417
2562
|
console.error(err.message);
|
|
@@ -2453,9 +2598,9 @@ async function runAgentsList(targetDir) {
|
|
|
2453
2598
|
}
|
|
2454
2599
|
|
|
2455
2600
|
// src/modules/push-safety.ts
|
|
2456
|
-
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7, chmod, access as access3, rm as rm4, readdir as
|
|
2601
|
+
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7, chmod, access as access3, rm as rm4, readdir as readdir5 } from "fs/promises";
|
|
2457
2602
|
import { constants } from "fs";
|
|
2458
|
-
import { join as join5, resolve as resolve2, dirname as
|
|
2603
|
+
import { join as join5, resolve as resolve2, dirname as dirname3, basename } from "path";
|
|
2459
2604
|
|
|
2460
2605
|
// src/modules/git.ts
|
|
2461
2606
|
import { execFile } from "child_process";
|
|
@@ -2752,7 +2897,7 @@ async function repositoryRoot(cwd) {
|
|
|
2752
2897
|
]);
|
|
2753
2898
|
const commonDir = stdout.trim();
|
|
2754
2899
|
if (commonDir !== "") {
|
|
2755
|
-
const parent =
|
|
2900
|
+
const parent = dirname3(commonDir);
|
|
2756
2901
|
if (basename(commonDir) === ".git" && parent !== "") return parent;
|
|
2757
2902
|
}
|
|
2758
2903
|
} catch {
|
|
@@ -2842,7 +2987,7 @@ async function defaultHooks(cwd) {
|
|
|
2842
2987
|
return [];
|
|
2843
2988
|
}
|
|
2844
2989
|
try {
|
|
2845
|
-
return (await
|
|
2990
|
+
return (await readdir5(hooksDir)).filter((name) => !name.endsWith(".sample")).sort();
|
|
2846
2991
|
} catch {
|
|
2847
2992
|
return [];
|
|
2848
2993
|
}
|
|
@@ -2876,7 +3021,7 @@ async function removeHook(cwd, hookName) {
|
|
|
2876
3021
|
const hooksDir = join5(root, ARCANE_HOOKS_DIR);
|
|
2877
3022
|
let remaining = [];
|
|
2878
3023
|
try {
|
|
2879
|
-
remaining = await
|
|
3024
|
+
remaining = await readdir5(hooksDir);
|
|
2880
3025
|
} catch {
|
|
2881
3026
|
remaining = [];
|
|
2882
3027
|
}
|
|
@@ -3274,16 +3419,35 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
3274
3419
|
profile = await select2({
|
|
3275
3420
|
message: "Select an installation profile:\n",
|
|
3276
3421
|
choices: profiles.map((p) => {
|
|
3277
|
-
const
|
|
3422
|
+
const profileComponents2 = getProfile(p.id);
|
|
3278
3423
|
return {
|
|
3279
3424
|
value: p.id,
|
|
3280
3425
|
name: `${PROFILE_ICONS[p.id] ?? "\u{1F4CB}"} ${p.displayName} \u2014 ${p.description}`,
|
|
3281
|
-
description: buildProfileDescription(
|
|
3426
|
+
description: buildProfileDescription(profileComponents2, p.id)
|
|
3282
3427
|
};
|
|
3283
3428
|
})
|
|
3284
3429
|
});
|
|
3285
3430
|
}
|
|
3286
|
-
const
|
|
3431
|
+
const profileComponents = getProfile(profile);
|
|
3432
|
+
let spell_scope;
|
|
3433
|
+
if (!options.profile && !options.dryRun) {
|
|
3434
|
+
let storeVersion;
|
|
3435
|
+
try {
|
|
3436
|
+
storeVersion = (await readManifest(userTierRoot())).version;
|
|
3437
|
+
} catch {
|
|
3438
|
+
}
|
|
3439
|
+
if (storeVersion !== void 0) {
|
|
3440
|
+
console.log();
|
|
3441
|
+
const useTier = await confirm({
|
|
3442
|
+
message: `This machine has an Arcane user tier (v${storeVersion}). Use it for this repository's spells instead of installing copies here?`,
|
|
3443
|
+
default: false
|
|
3444
|
+
});
|
|
3445
|
+
spell_scope = useTier ? "user" : "repo";
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
const components = profileComponents.map(
|
|
3449
|
+
(component) => componentForSpellScope(component, spell_scope ?? "repo")
|
|
3450
|
+
);
|
|
3287
3451
|
const groups = categorizeComponents(components);
|
|
3288
3452
|
console.log();
|
|
3289
3453
|
printSection(`\u{1F4E6} Profile: ${profile}`);
|
|
@@ -3470,6 +3634,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
3470
3634
|
profile,
|
|
3471
3635
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3472
3636
|
components: installedComponents,
|
|
3637
|
+
...spell_scope ? { spell_scope } : {},
|
|
3473
3638
|
...role ? { role } : {},
|
|
3474
3639
|
...tracking_mode ? { tracking_mode, external_provider } : {},
|
|
3475
3640
|
...subject_root !== void 0 ? { subject_root } : {},
|
|
@@ -3621,7 +3786,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
3621
3786
|
console.log(chalk2.hex("#a855f7")(`
|
|
3622
3787
|
\u2726 Arcane v${packageVersion}
|
|
3623
3788
|
`));
|
|
3624
|
-
const homeDir =
|
|
3789
|
+
const homeDir = dirname4(storeRoot);
|
|
3625
3790
|
const components = USER_TIER_COMPONENTS.map(
|
|
3626
3791
|
(name) => componentForScope(getComponent(name), "user")
|
|
3627
3792
|
);
|
|
@@ -3669,7 +3834,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
|
|
|
3669
3834
|
fileHashes
|
|
3670
3835
|
});
|
|
3671
3836
|
}
|
|
3672
|
-
const fanout = await syncUserTierFanout({ homeDir, storeRoot, spellIds, fallbackDir: assetsDir });
|
|
3837
|
+
const fanout = await syncUserTierFanout({ homeDir, storeRoot, spellIds, fallbackDir: assetsDir, ownedClients: SPELL_FANOUT_CLIENTS });
|
|
3673
3838
|
const manifest = {
|
|
3674
3839
|
version: packageVersion,
|
|
3675
3840
|
// The tier is un-profiled; "full" records the profile its component set
|
|
@@ -3783,12 +3948,12 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
|
|
|
3783
3948
|
}
|
|
3784
3949
|
|
|
3785
3950
|
// src/commands/update.ts
|
|
3786
|
-
import { dirname as
|
|
3951
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
3787
3952
|
import { readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
|
|
3788
3953
|
|
|
3789
3954
|
// src/modules/hub.ts
|
|
3790
3955
|
import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
|
|
3791
|
-
import { readdir as
|
|
3956
|
+
import { readdir as readdir6, writeFile as writeFile8 } from "fs/promises";
|
|
3792
3957
|
import { join as join8 } from "path";
|
|
3793
3958
|
var MANIFEST_RETROFITS = [
|
|
3794
3959
|
{
|
|
@@ -3933,7 +4098,7 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
|
3933
4098
|
const businessRootPath = join8(targetDir, businessRoot);
|
|
3934
4099
|
let ventureDirs;
|
|
3935
4100
|
try {
|
|
3936
|
-
const entries = await
|
|
4101
|
+
const entries = await readdir6(businessRootPath, { withFileTypes: true });
|
|
3937
4102
|
ventureDirs = entries.filter((e) => e.isDirectory() && e.name !== "_template").map((e) => e.name);
|
|
3938
4103
|
} catch {
|
|
3939
4104
|
return;
|
|
@@ -4155,26 +4320,57 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
|
|
|
4155
4320
|
return "reported";
|
|
4156
4321
|
}
|
|
4157
4322
|
await removeWithin(targetDir, file);
|
|
4323
|
+
await removeEmptyAncestors(targetDir, file);
|
|
4158
4324
|
console.log(` Pruned orphaned file: ${file}`);
|
|
4159
4325
|
return "pruned";
|
|
4160
4326
|
}
|
|
4161
|
-
|
|
4327
|
+
function findReadoptedSpellComponents(components, scope = "repo", spellScope = "repo") {
|
|
4328
|
+
if (scope !== "repo" || spellScope !== "repo") return /* @__PURE__ */ new Set();
|
|
4329
|
+
return new Set(
|
|
4330
|
+
components.filter((c) => SPELL_COMPONENT_NAMES.includes(c.name) && c.files.length === 0).map((c) => c.name)
|
|
4331
|
+
);
|
|
4332
|
+
}
|
|
4333
|
+
async function findMissingTrackedFiles(targetDir, components, scope = "repo", spellScope = "repo", readopted = /* @__PURE__ */ new Set()) {
|
|
4162
4334
|
const missing = [];
|
|
4163
4335
|
for (const installed of components) {
|
|
4164
4336
|
let component;
|
|
4165
4337
|
try {
|
|
4166
|
-
component =
|
|
4338
|
+
component = componentForSpellScope(
|
|
4339
|
+
componentForScope(getComponent(installed.name), scope),
|
|
4340
|
+
scope === "repo" ? spellScope : "repo"
|
|
4341
|
+
);
|
|
4167
4342
|
} catch {
|
|
4168
4343
|
continue;
|
|
4169
4344
|
}
|
|
4170
4345
|
if (component.initOnly || component.skipExisting) continue;
|
|
4171
|
-
|
|
4346
|
+
const candidates = readopted.has(installed.name) ? component.files : installed.files;
|
|
4347
|
+
for (const file of candidates) {
|
|
4172
4348
|
if (!component.files.includes(file)) continue;
|
|
4173
4349
|
if (!await fileExists(join9(targetDir, file))) missing.push(file);
|
|
4174
4350
|
}
|
|
4175
4351
|
}
|
|
4176
4352
|
return missing;
|
|
4177
4353
|
}
|
|
4354
|
+
function findUnmanagedTrackedFiles(components, scope = "repo", spellScope = "repo") {
|
|
4355
|
+
const unmanaged = [];
|
|
4356
|
+
for (const installed of components) {
|
|
4357
|
+
let component;
|
|
4358
|
+
try {
|
|
4359
|
+
component = componentForSpellScope(
|
|
4360
|
+
componentForScope(getComponent(installed.name), scope),
|
|
4361
|
+
scope === "repo" ? spellScope : "repo"
|
|
4362
|
+
);
|
|
4363
|
+
} catch {
|
|
4364
|
+
unmanaged.push(...installed.files);
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
const shipped = new Set(component.files);
|
|
4368
|
+
for (const file of installed.files) {
|
|
4369
|
+
if (!shipped.has(file)) unmanaged.push(file);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
return unmanaged;
|
|
4373
|
+
}
|
|
4178
4374
|
async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
4179
4375
|
let manifest;
|
|
4180
4376
|
try {
|
|
@@ -4196,7 +4392,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4196
4392
|
return;
|
|
4197
4393
|
}
|
|
4198
4394
|
const scope = resolvedScope.scope;
|
|
4199
|
-
const
|
|
4395
|
+
const spellScope = effectiveSpellScope(manifest);
|
|
4396
|
+
const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
|
|
4397
|
+
const homeDir = dirname5(targetDir);
|
|
4200
4398
|
if (scope === "repo") {
|
|
4201
4399
|
console.warn(
|
|
4202
4400
|
`WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
|
|
@@ -4229,11 +4427,13 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4229
4427
|
}
|
|
4230
4428
|
let sameVersionRestore = false;
|
|
4231
4429
|
if (manifest.version === packageVersion && manifest.components.length > 0) {
|
|
4232
|
-
const missing = await findMissingTrackedFiles(targetDir, manifest.components, scope);
|
|
4233
|
-
|
|
4430
|
+
const missing = await findMissingTrackedFiles(targetDir, manifest.components, scope, spellScope, readopted);
|
|
4431
|
+
const unmanaged = findUnmanagedTrackedFiles(manifest.components, scope, spellScope);
|
|
4432
|
+
if (missing.length === 0 && unmanaged.length === 0) {
|
|
4234
4433
|
console.log("Already up to date.");
|
|
4235
4434
|
if (scope === "user") {
|
|
4236
4435
|
const fanout = await syncUserTierFanout({
|
|
4436
|
+
ownedClients: SPELL_FANOUT_CLIENTS,
|
|
4237
4437
|
homeDir,
|
|
4238
4438
|
storeRoot: targetDir,
|
|
4239
4439
|
spellIds: spellIdsInStore(manifest.components),
|
|
@@ -4249,9 +4449,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4249
4449
|
return;
|
|
4250
4450
|
}
|
|
4251
4451
|
sameVersionRestore = true;
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4452
|
+
const prefix = options.dryRun ? "[dry-run] " : "";
|
|
4453
|
+
if (missing.length > 0) {
|
|
4454
|
+
console.log(
|
|
4455
|
+
`${prefix}Already at v${packageVersion}, but ${missing.length} tracked file${missing.length === 1 ? " is" : "s are"} missing \u2014 restoring.`
|
|
4456
|
+
);
|
|
4457
|
+
} else {
|
|
4458
|
+
console.log(
|
|
4459
|
+
`${prefix}Already at v${packageVersion}, but ${unmanaged.length} tracked file${unmanaged.length === 1 ? " is" : "s are"} no longer managed here \u2014 reviewing.`
|
|
4460
|
+
);
|
|
4461
|
+
}
|
|
4255
4462
|
}
|
|
4256
4463
|
if (manifest.components.length === 0) {
|
|
4257
4464
|
console.log("No components installed. Nothing to update.");
|
|
@@ -4266,7 +4473,10 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4266
4473
|
for (const installed of componentsToUpdate) {
|
|
4267
4474
|
let component;
|
|
4268
4475
|
try {
|
|
4269
|
-
component =
|
|
4476
|
+
component = componentForSpellScope(
|
|
4477
|
+
componentForScope(getComponent(installed.name), scope),
|
|
4478
|
+
scope === "repo" ? spellScope : "repo"
|
|
4479
|
+
);
|
|
4270
4480
|
} catch (err) {
|
|
4271
4481
|
if (err instanceof ComponentNotFoundError) {
|
|
4272
4482
|
console.log(` ! ${installed.name} not in registry \u2014 skipping.`);
|
|
@@ -4301,7 +4511,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4301
4511
|
continue;
|
|
4302
4512
|
}
|
|
4303
4513
|
if (sameVersionRestore) {
|
|
4304
|
-
const tracked = installed.files.includes(file);
|
|
4514
|
+
const tracked = installed.files.includes(file) || readopted.has(installed.name);
|
|
4305
4515
|
const restorable = tracked && !targetExists && !component.skipExisting;
|
|
4306
4516
|
if (!restorable) {
|
|
4307
4517
|
if (tracked) {
|
|
@@ -4378,13 +4588,24 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
4378
4588
|
}
|
|
4379
4589
|
for (const file of installed.files) {
|
|
4380
4590
|
if (updatedFiles.includes(file)) continue;
|
|
4591
|
+
const recordedHash = installed.fileHashes?.[file];
|
|
4381
4592
|
const status = await resolveOrphan(
|
|
4382
4593
|
targetDir,
|
|
4383
4594
|
file,
|
|
4384
|
-
|
|
4595
|
+
recordedHash,
|
|
4385
4596
|
Boolean(options.prune) && !options.dryRun
|
|
4386
4597
|
);
|
|
4387
4598
|
orphanReport.push({ file, status });
|
|
4599
|
+
if (status === "reported") {
|
|
4600
|
+
try {
|
|
4601
|
+
validateTargetPath(targetDir, file);
|
|
4602
|
+
if (await fileExists(join9(targetDir, file))) {
|
|
4603
|
+
updatedFiles.push(file);
|
|
4604
|
+
if (recordedHash !== void 0) fileHashes[file] = recordedHash;
|
|
4605
|
+
}
|
|
4606
|
+
} catch {
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4388
4609
|
}
|
|
4389
4610
|
updatedComponents.push({
|
|
4390
4611
|
...installed,
|
|
@@ -4404,6 +4625,11 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
4404
4625
|
const marker = status === "pruned" ? "pruned" : options.prune ? "kept (see reason above)" : "not removed";
|
|
4405
4626
|
console.log(` ${file} \u2014 ${marker}`);
|
|
4406
4627
|
}
|
|
4628
|
+
if (scope === "repo" && spellScope === "user") {
|
|
4629
|
+
console.log(
|
|
4630
|
+
' This repository takes its spells from the user tier (spell_scope: "user"), so the files above are no longer managed here \u2014 every client reads ~/.arcane instead.'
|
|
4631
|
+
);
|
|
4632
|
+
}
|
|
4407
4633
|
if (!options.prune) {
|
|
4408
4634
|
console.log(" Run `spell update --prune` to remove the ones that are safe to delete.");
|
|
4409
4635
|
}
|
|
@@ -4420,6 +4646,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
4420
4646
|
let fanoutRecord;
|
|
4421
4647
|
if (scope === "user") {
|
|
4422
4648
|
const fanout = await syncUserTierFanout({
|
|
4649
|
+
ownedClients: SPELL_FANOUT_CLIENTS,
|
|
4423
4650
|
homeDir,
|
|
4424
4651
|
storeRoot: targetDir,
|
|
4425
4652
|
spellIds: spellIdsInStore(updatedComponents),
|
|
@@ -4483,11 +4710,11 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
4483
4710
|
}
|
|
4484
4711
|
|
|
4485
4712
|
// src/commands/status.ts
|
|
4486
|
-
import { dirname as
|
|
4713
|
+
import { dirname as dirname7 } from "path";
|
|
4487
4714
|
|
|
4488
4715
|
// src/modules/version-check.ts
|
|
4489
4716
|
import { readFile as readFile8, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
|
|
4490
|
-
import { dirname as
|
|
4717
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
4491
4718
|
import { homedir as homedir3 } from "os";
|
|
4492
4719
|
import { createRequire } from "module";
|
|
4493
4720
|
var require2 = createRequire(import.meta.url);
|
|
@@ -4516,7 +4743,7 @@ async function readCache(cachePath) {
|
|
|
4516
4743
|
}
|
|
4517
4744
|
}
|
|
4518
4745
|
async function writeCache(cachePath, latest) {
|
|
4519
|
-
await mkdir6(
|
|
4746
|
+
await mkdir6(dirname6(cachePath), { recursive: true });
|
|
4520
4747
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4521
4748
|
await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
4522
4749
|
}
|
|
@@ -4657,7 +4884,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
4657
4884
|
console.log("");
|
|
4658
4885
|
if (scope === "user") {
|
|
4659
4886
|
console.log(` Scope: user \u2014 ${targetDir}`);
|
|
4660
|
-
const health = await inspectUserTierFanout(
|
|
4887
|
+
const health = await inspectUserTierFanout(dirname7(targetDir), manifest.fanout);
|
|
4661
4888
|
console.log(
|
|
4662
4889
|
` Client files: ${health.total} (${health.byClient.codex} ${FANOUT_CLIENT_LABELS.codex}, ${health.byClient.claude} ${FANOUT_CLIENT_LABELS.claude})`
|
|
4663
4890
|
);
|
|
@@ -4670,12 +4897,18 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
4670
4897
|
);
|
|
4671
4898
|
}
|
|
4672
4899
|
} else {
|
|
4673
|
-
|
|
4900
|
+
const spellScope = effectiveSpellScope(manifest);
|
|
4901
|
+
console.log(spellScope === "user" ? " Scope: repo (spells: user tier)" : " Scope: repo");
|
|
4902
|
+
const store = userTierRoot();
|
|
4674
4903
|
try {
|
|
4675
|
-
const store = userTierRoot();
|
|
4676
4904
|
const userManifest = await readManifest(store);
|
|
4677
4905
|
console.log(` User tier: v${userManifest.version} at ${store} (spell status --user)`);
|
|
4678
4906
|
} catch {
|
|
4907
|
+
if (spellScope === "user") {
|
|
4908
|
+
console.log(
|
|
4909
|
+
` ! User tier: not installed at ${store} \u2014 this repository expects it for every spell. Run \`spell init --user\` (\`spell doctor\` explains).`
|
|
4910
|
+
);
|
|
4911
|
+
}
|
|
4679
4912
|
}
|
|
4680
4913
|
}
|
|
4681
4914
|
console.log("");
|
|
@@ -4714,7 +4947,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
4714
4947
|
|
|
4715
4948
|
// src/commands/uninstall.ts
|
|
4716
4949
|
import { rm as rm5 } from "fs/promises";
|
|
4717
|
-
import { dirname as
|
|
4950
|
+
import { dirname as dirname8, join as join11 } from "path";
|
|
4718
4951
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
4719
4952
|
async function runUninstall(options, targetDir) {
|
|
4720
4953
|
let manifest;
|
|
@@ -4823,7 +5056,7 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
4823
5056
|
\u2713 Uninstalled \u2014 ${removed} files removed.`);
|
|
4824
5057
|
}
|
|
4825
5058
|
async function uninstallUserTier(options, storeRoot, manifest) {
|
|
4826
|
-
const homeDir =
|
|
5059
|
+
const homeDir = dirname8(storeRoot);
|
|
4827
5060
|
const storeFiles = manifest.components.flatMap((c) => c.files);
|
|
4828
5061
|
const recordedCount = Object.keys(manifest.fanout ?? {}).length;
|
|
4829
5062
|
if (options.dryRun) {
|
|
@@ -4869,6 +5102,8 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
4869
5102
|
}
|
|
4870
5103
|
}
|
|
4871
5104
|
await rm5(join11(storeRoot, ".arcane.json"), { force: true });
|
|
5105
|
+
await rm5(join11(storeRoot, "agents"), { recursive: true, force: true });
|
|
5106
|
+
await rm5(join11(storeRoot, "agents.yaml"), { force: true });
|
|
4872
5107
|
await removeDirectoryIfEmpty(join11(storeRoot, USER_TIER_SPELLS_DIR));
|
|
4873
5108
|
await removeDirectoryIfEmpty(storeRoot);
|
|
4874
5109
|
for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
|
|
@@ -4881,8 +5116,8 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
4881
5116
|
}
|
|
4882
5117
|
|
|
4883
5118
|
// src/commands/doctor.ts
|
|
4884
|
-
import { access as access4, readFile as readFile10, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
4885
|
-
import { join as join13, dirname as
|
|
5119
|
+
import { access as access4, readdir as readdir8, readFile as readFile10, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
5120
|
+
import { join as join13, dirname as dirname9 } from "path";
|
|
4886
5121
|
import { execFile as execFile3 } from "child_process";
|
|
4887
5122
|
import { promisify as promisify2 } from "util";
|
|
4888
5123
|
|
|
@@ -4952,7 +5187,7 @@ function evaluateIncidentGate(records) {
|
|
|
4952
5187
|
}
|
|
4953
5188
|
|
|
4954
5189
|
// src/modules/denylist-scan.ts
|
|
4955
|
-
import { readFile as readFile9, readdir as
|
|
5190
|
+
import { readFile as readFile9, readdir as readdir7 } from "fs/promises";
|
|
4956
5191
|
import { join as join12, relative } from "path";
|
|
4957
5192
|
function escapeRegExp(value) {
|
|
4958
5193
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4999,7 +5234,7 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
|
|
|
4999
5234
|
const collected = [];
|
|
5000
5235
|
let entries;
|
|
5001
5236
|
try {
|
|
5002
|
-
entries = await
|
|
5237
|
+
entries = await readdir7(root, { withFileTypes: true });
|
|
5003
5238
|
} catch {
|
|
5004
5239
|
return collected;
|
|
5005
5240
|
}
|
|
@@ -5221,8 +5456,8 @@ async function checkVSCodeExtension(id, label) {
|
|
|
5221
5456
|
];
|
|
5222
5457
|
for (const dir of extDirs) {
|
|
5223
5458
|
try {
|
|
5224
|
-
const { readdir:
|
|
5225
|
-
const entries = await
|
|
5459
|
+
const { readdir: readdir11 } = await import("fs/promises");
|
|
5460
|
+
const entries = await readdir11(dir);
|
|
5226
5461
|
const prefix = id.toLowerCase() + "-";
|
|
5227
5462
|
const found = entries.some(
|
|
5228
5463
|
(e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
|
|
@@ -5384,7 +5619,7 @@ async function fixSessionContinuity(targetDir, assetsDir) {
|
|
|
5384
5619
|
await access4(destPath);
|
|
5385
5620
|
} catch {
|
|
5386
5621
|
const srcPath = join13(assetsDir, file);
|
|
5387
|
-
await mkdir7(
|
|
5622
|
+
await mkdir7(dirname9(destPath), { recursive: true });
|
|
5388
5623
|
await fsCopyFile(srcPath, destPath);
|
|
5389
5624
|
created.push(file);
|
|
5390
5625
|
}
|
|
@@ -5646,6 +5881,74 @@ async function checkPlatformBranchPolicy(targetDir) {
|
|
|
5646
5881
|
message: "remote is neither GitHub nor Azure DevOps \u2014 platform policy verification not available for this provider"
|
|
5647
5882
|
};
|
|
5648
5883
|
}
|
|
5884
|
+
async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
|
|
5885
|
+
const name = "Spell scope (ARC-045)";
|
|
5886
|
+
let manifest;
|
|
5887
|
+
try {
|
|
5888
|
+
manifest = await readManifest(targetDir);
|
|
5889
|
+
} catch {
|
|
5890
|
+
return { name, passed: true, blocking: false, message: "no .arcane.json \u2014 nothing to check" };
|
|
5891
|
+
}
|
|
5892
|
+
if (effectiveSpellScope(manifest) === "repo") {
|
|
5893
|
+
return {
|
|
5894
|
+
name,
|
|
5895
|
+
passed: true,
|
|
5896
|
+
blocking: false,
|
|
5897
|
+
message: "repo \u2014 this repository carries its own spells (the default)"
|
|
5898
|
+
};
|
|
5899
|
+
}
|
|
5900
|
+
const storeRoot = userTierRoot(homeDir);
|
|
5901
|
+
let store;
|
|
5902
|
+
try {
|
|
5903
|
+
store = await readManifest(storeRoot);
|
|
5904
|
+
} catch (err) {
|
|
5905
|
+
return {
|
|
5906
|
+
name,
|
|
5907
|
+
passed: false,
|
|
5908
|
+
message: `this repository takes its spells from the user tier (spell_scope: "user"), but ${storeRoot} ${err instanceof ManifestNotFoundError ? "is not installed" : `could not be read (${err instanceof Error ? err.message : String(err)})`} \u2014 no client can reach a spell here. Run \`spell init --user\`, or set spell_scope back to "repo" and run \`spell update\`.`
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5911
|
+
let spellCount = 0;
|
|
5912
|
+
let probed = false;
|
|
5913
|
+
const probe = "spell-status.md";
|
|
5914
|
+
try {
|
|
5915
|
+
const entries = await readdir8(join13(storeRoot, USER_TIER_SPELLS_DIR));
|
|
5916
|
+
const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
|
|
5917
|
+
spellCount = spells.length;
|
|
5918
|
+
probed = spells.includes(probe);
|
|
5919
|
+
} catch {
|
|
5920
|
+
}
|
|
5921
|
+
if (spellCount === 0 || !probed) {
|
|
5922
|
+
return {
|
|
5923
|
+
name,
|
|
5924
|
+
passed: false,
|
|
5925
|
+
message: `the user tier at ${storeRoot} holds ${spellCount} spell(s)${spellCount > 0 && !probed ? ` and not ${probe}` : ""} \u2014 this repository expects it to supply every spell. Run \`spell update --user\`.`
|
|
5926
|
+
};
|
|
5927
|
+
}
|
|
5928
|
+
const majorMinor = (v) => v.split(".").slice(0, 2).join(".");
|
|
5929
|
+
if (typeof manifest.version === "string" && majorMinor(store.version) !== majorMinor(manifest.version)) {
|
|
5930
|
+
return {
|
|
5931
|
+
name,
|
|
5932
|
+
passed: false,
|
|
5933
|
+
blocking: false,
|
|
5934
|
+
message: `this repository is at v${manifest.version} but the user tier it uses is at v${store.version} \u2014 run \`spell update --user\` (and \`spell update\` here) to bring them together.`
|
|
5935
|
+
};
|
|
5936
|
+
}
|
|
5937
|
+
const repoExpectsAgents = await rosterExists(targetDir);
|
|
5938
|
+
if (repoExpectsAgents && !await rosterExists(storeRoot, "user")) {
|
|
5939
|
+
return {
|
|
5940
|
+
name,
|
|
5941
|
+
passed: false,
|
|
5942
|
+
message: `this repository has an agent roster and takes its client files from the user tier, but ${storeRoot} has no roster of its own \u2014 no agent mode can reach a client here. Run \`spell agents init --user\`, or set spell_scope back to "repo" and run \`spell agents sync\`.`
|
|
5943
|
+
};
|
|
5944
|
+
}
|
|
5945
|
+
return {
|
|
5946
|
+
name,
|
|
5947
|
+
passed: true,
|
|
5948
|
+
blocking: false,
|
|
5949
|
+
message: `user \u2014 ${spellCount} spell(s) from ${storeRoot} (v${store.version}), ${probe} present${repoExpectsAgents ? ", agent roster present in the tier" : ""}`
|
|
5950
|
+
};
|
|
5951
|
+
}
|
|
5649
5952
|
async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
5650
5953
|
console.log("\nspell doctor \u2014 checking your Arcane environment\n");
|
|
5651
5954
|
if (options.leaks) {
|
|
@@ -5671,7 +5974,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
5671
5974
|
checkPlatformBranchPolicy(targetDir),
|
|
5672
5975
|
checkDelegations(targetDir),
|
|
5673
5976
|
checkMcpConfig(targetDir),
|
|
5674
|
-
checkUserTier(resolveHomeDir(), options.packageVersion)
|
|
5977
|
+
checkUserTier(resolveHomeDir(), options.packageVersion),
|
|
5978
|
+
checkSpellScope(targetDir)
|
|
5675
5979
|
]);
|
|
5676
5980
|
const sessionResults = await checkSessionContinuity(targetDir);
|
|
5677
5981
|
results.push(...sessionResults);
|
|
@@ -5719,7 +6023,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
5719
6023
|
import { readFile as readFile12 } from "fs/promises";
|
|
5720
6024
|
|
|
5721
6025
|
// src/modules/ward.ts
|
|
5722
|
-
import { readdir as
|
|
6026
|
+
import { readdir as readdir9, readFile as readFile11 } from "fs/promises";
|
|
5723
6027
|
import { join as join14, relative as relative2 } from "path";
|
|
5724
6028
|
async function scanFileAllRules(absolutePath, displayPath, rules) {
|
|
5725
6029
|
const findings = [];
|
|
@@ -5778,7 +6082,7 @@ async function scanFilenames(root, rules, repoRoot = root) {
|
|
|
5778
6082
|
const findings = [];
|
|
5779
6083
|
let entries;
|
|
5780
6084
|
try {
|
|
5781
|
-
entries = await
|
|
6085
|
+
entries = await readdir9(root, { withFileTypes: true });
|
|
5782
6086
|
} catch {
|
|
5783
6087
|
return findings;
|
|
5784
6088
|
}
|
|
@@ -6000,8 +6304,8 @@ import { access as access5 } from "fs/promises";
|
|
|
6000
6304
|
import { join as join17 } from "path";
|
|
6001
6305
|
|
|
6002
6306
|
// src/modules/show-report/generate.ts
|
|
6003
|
-
import { mkdir as mkdir8, readdir as
|
|
6004
|
-
import { basename as basename3, dirname as
|
|
6307
|
+
import { mkdir as mkdir8, readdir as readdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
6308
|
+
import { basename as basename3, dirname as dirname10, isAbsolute, join as join16, relative as relative3, resolve as resolve4 } from "path";
|
|
6005
6309
|
|
|
6006
6310
|
// src/modules/show-report/model.ts
|
|
6007
6311
|
import { readFile as readFile13 } from "fs/promises";
|
|
@@ -6558,7 +6862,7 @@ async function programFromPlan(rootDir, slug, planRelPath) {
|
|
|
6558
6862
|
program: {
|
|
6559
6863
|
slug,
|
|
6560
6864
|
planRelPath,
|
|
6561
|
-
queueRelPath: toPosix(join16(
|
|
6865
|
+
queueRelPath: toPosix(join16(dirname10(planRelPath), "OPERATOR-QUEUE.md")),
|
|
6562
6866
|
ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
|
|
6563
6867
|
}
|
|
6564
6868
|
};
|
|
@@ -6567,7 +6871,7 @@ async function discoverPrograms(rootDir) {
|
|
|
6567
6871
|
const plansDir = join16(rootDir, "docs", "plans");
|
|
6568
6872
|
let entries;
|
|
6569
6873
|
try {
|
|
6570
|
-
entries = (await
|
|
6874
|
+
entries = (await readdir10(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
6571
6875
|
} catch (error) {
|
|
6572
6876
|
if (error.code === "ENOENT") return { programs: [], skipped: [] };
|
|
6573
6877
|
throw error;
|
|
@@ -6584,7 +6888,7 @@ async function discoverPrograms(rootDir) {
|
|
|
6584
6888
|
async function programFromPlanPath(rootDir, planPath) {
|
|
6585
6889
|
const planAbs = isAbsolute(planPath) ? planPath : resolve4(rootDir, planPath);
|
|
6586
6890
|
const planRelPath = toPosix(relative3(rootDir, planAbs));
|
|
6587
|
-
return programFromPlan(rootDir, basename3(
|
|
6891
|
+
return programFromPlan(rootDir, basename3(dirname10(planAbs)), planRelPath);
|
|
6588
6892
|
}
|
|
6589
6893
|
function normalizeLineEndings2(content) {
|
|
6590
6894
|
return content.replace(/\r\n?/g, "\n");
|
|
@@ -6624,7 +6928,7 @@ async function runReportGeneration(options) {
|
|
|
6624
6928
|
const generated = await generateReport(options.rootDir, program2, templateHtml);
|
|
6625
6929
|
const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
|
|
6626
6930
|
if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
|
|
6627
|
-
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir,
|
|
6931
|
+
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir, dirname10(program2.planRelPath));
|
|
6628
6932
|
const outRel = toPosix(relative3(options.rootDir, outAbs));
|
|
6629
6933
|
const outputs = [
|
|
6630
6934
|
[`${outRel}/show-report.json`, generated.json],
|
|
@@ -6710,9 +7014,10 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
6710
7014
|
import { realpathSync } from "fs";
|
|
6711
7015
|
var require3 = createRequire2(import.meta.url);
|
|
6712
7016
|
var pkg = require3("../package.json");
|
|
6713
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join18(
|
|
7017
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join18(dirname11(fileURLToPath(import.meta.url)), "assets");
|
|
6714
7018
|
var program = new Command();
|
|
6715
7019
|
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)";
|
|
7020
|
+
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)";
|
|
6716
7021
|
function targetDirFor(opts) {
|
|
6717
7022
|
return opts.user ? userTierRoot() : process.cwd();
|
|
6718
7023
|
}
|
|
@@ -6814,39 +7119,44 @@ agentsCommand.command("init").description(
|
|
|
6814
7119
|
).option(
|
|
6815
7120
|
"--naming <strategy>",
|
|
6816
7121
|
"Naming strategy: arcanos | generic | random | custom"
|
|
6817
|
-
).option("--force", "Reinitialize even if roster already exists").option("--dry-run", "Preview what would be created without writing files").action(
|
|
7122
|
+
).option("--force", "Reinitialize even if roster already exists").option("--dry-run", "Preview what would be created without writing files").option("--user", USER_TIER_AGENTS_DESCRIPTION).action(
|
|
6818
7123
|
async (opts) => {
|
|
7124
|
+
const scope = opts.user ? "user" : "repo";
|
|
6819
7125
|
await runAgentsInit(
|
|
6820
7126
|
{
|
|
6821
7127
|
profile: opts.profile,
|
|
6822
7128
|
naming: opts.naming,
|
|
6823
7129
|
force: opts.force,
|
|
6824
|
-
dryRun: opts.dryRun
|
|
7130
|
+
dryRun: opts.dryRun,
|
|
7131
|
+
scope
|
|
6825
7132
|
},
|
|
6826
|
-
process.cwd(),
|
|
7133
|
+
agentsTargetDir(scope, process.cwd()),
|
|
6827
7134
|
ASSETS_DIR
|
|
6828
7135
|
);
|
|
6829
7136
|
}
|
|
6830
7137
|
);
|
|
6831
7138
|
agentsCommand.command("sync").description(
|
|
6832
7139
|
"Regenerate all client instruction files from canonical agent definitions"
|
|
6833
|
-
).option("--dry-run", "Preview what would be generated without writing files").option("--no-openclaw", "Skip OpenClaw workspace output").option("--no-copilot", "Skip Copilot output").option("--no-claude", "Skip Claude output").option("--no-codex", "Skip Codex output").action(
|
|
7140
|
+
).option("--dry-run", "Preview what would be generated without writing files").option("--no-openclaw", "Skip OpenClaw workspace output").option("--no-copilot", "Skip Copilot output").option("--no-claude", "Skip Claude output").option("--no-codex", "Skip Codex output").option("--user", USER_TIER_AGENTS_DESCRIPTION).action(
|
|
6834
7141
|
async (opts) => {
|
|
7142
|
+
const scope = opts.user ? "user" : "repo";
|
|
6835
7143
|
await runAgentsSync(
|
|
6836
7144
|
{
|
|
6837
7145
|
dryRun: opts.dryRun,
|
|
6838
7146
|
openclaw: opts.openclaw,
|
|
6839
7147
|
copilot: opts.copilot,
|
|
6840
7148
|
claude: opts.claude,
|
|
6841
|
-
codex: opts.codex
|
|
7149
|
+
codex: opts.codex,
|
|
7150
|
+
scope
|
|
6842
7151
|
},
|
|
6843
|
-
process.cwd(),
|
|
7152
|
+
agentsTargetDir(scope, process.cwd()),
|
|
6844
7153
|
ASSETS_DIR
|
|
6845
7154
|
);
|
|
6846
7155
|
}
|
|
6847
7156
|
);
|
|
6848
|
-
agentsCommand.command("list").description("Show the current agent roster with assigned names and roles").action(async () => {
|
|
6849
|
-
|
|
7157
|
+
agentsCommand.command("list").description("Show the current agent roster with assigned names and roles").option("--user", USER_TIER_AGENTS_DESCRIPTION).action(async (opts) => {
|
|
7158
|
+
const scope = opts.user ? "user" : "repo";
|
|
7159
|
+
await runAgentsList(agentsTargetDir(scope, process.cwd()), scope);
|
|
6850
7160
|
});
|
|
6851
7161
|
if (realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
6852
7162
|
await program.parseAsync(process.argv);
|