arcane-cli 1.16.0 → 1.17.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 +32 -3
- package/dist/assets/.arcane/spells/_fragments/state-export.md +1 -0
- package/dist/assets/.arcane/spells/spell-open-session.md +3 -0
- package/dist/assets/schemas/state.schema.json +386 -0
- package/dist/index.js +1247 -217
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const require = __createRequire(import.meta.url);
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { createRequire as createRequire3 } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as dirname14, join as
|
|
9
|
+
import { dirname as dirname14, join as join29 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { select as select3, confirm as confirm2, input as input3 } from "@inquirer/prompts";
|
|
@@ -62,6 +62,10 @@ var WRITE_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
|
62
62
|
"notes"
|
|
63
63
|
]);
|
|
64
64
|
var AMBIGUOUS_AS_WRITE = /* @__PURE__ */ new Set(["branch", "config", "remote", "worktree"]);
|
|
65
|
+
var AMBIGUOUS_READ_FORMS = {
|
|
66
|
+
worktree: /* @__PURE__ */ new Set(["list"]),
|
|
67
|
+
remote: /* @__PURE__ */ new Set(["get-url"])
|
|
68
|
+
};
|
|
65
69
|
var READ_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
66
70
|
"status",
|
|
67
71
|
"rev-parse",
|
|
@@ -97,6 +101,7 @@ var DEFAULT_TIMEOUTS_MS = {
|
|
|
97
101
|
};
|
|
98
102
|
function classifyGitCommand(args) {
|
|
99
103
|
let subcommand;
|
|
104
|
+
let form;
|
|
100
105
|
for (let i = 0; i < args.length; i++) {
|
|
101
106
|
const arg = args[i];
|
|
102
107
|
if (GLOBAL_FLAGS_WITH_SEPARATE_VALUE.has(arg)) {
|
|
@@ -105,10 +110,12 @@ function classifyGitCommand(args) {
|
|
|
105
110
|
}
|
|
106
111
|
if (arg.startsWith("-")) continue;
|
|
107
112
|
subcommand = arg;
|
|
113
|
+
form = args[i + 1];
|
|
108
114
|
break;
|
|
109
115
|
}
|
|
110
116
|
if (!subcommand) return "read";
|
|
111
117
|
if (NETWORK_SUBCOMMANDS.has(subcommand)) return "network";
|
|
118
|
+
if (form !== void 0 && AMBIGUOUS_READ_FORMS[subcommand]?.has(form)) return "read";
|
|
112
119
|
if (WRITE_SUBCOMMANDS.has(subcommand) || AMBIGUOUS_AS_WRITE.has(subcommand)) return "write";
|
|
113
120
|
if (READ_SUBCOMMANDS.has(subcommand)) return "read";
|
|
114
121
|
return "write";
|
|
@@ -299,15 +306,15 @@ async function isUneditedTrackedFile(cwd, relativePath) {
|
|
|
299
306
|
// src/modules/real-path.ts
|
|
300
307
|
import { realpathSync } from "fs";
|
|
301
308
|
import { basename, dirname, join, resolve } from "path";
|
|
302
|
-
function resolveRealPath(
|
|
309
|
+
function resolveRealPath(path5) {
|
|
303
310
|
const missing = [];
|
|
304
|
-
let existing =
|
|
311
|
+
let existing = path5;
|
|
305
312
|
for (; ; ) {
|
|
306
313
|
try {
|
|
307
314
|
return join(realpathSync.native(existing), ...missing);
|
|
308
315
|
} catch {
|
|
309
316
|
const parent = dirname(existing);
|
|
310
|
-
if (parent === existing) return resolve(
|
|
317
|
+
if (parent === existing) return resolve(path5);
|
|
311
318
|
missing.unshift(basename(existing));
|
|
312
319
|
existing = parent;
|
|
313
320
|
}
|
|
@@ -1431,8 +1438,8 @@ var CLIENT_SHIM_PATH_PATTERNS = [
|
|
|
1431
1438
|
/^\.claude\/commands\/spell-[a-z0-9-]+\.md$/,
|
|
1432
1439
|
/^\.agents\/skills\/spell-[a-z0-9-]+\/SKILL\.md$/
|
|
1433
1440
|
];
|
|
1434
|
-
function isClientShimPath(
|
|
1435
|
-
const normalized =
|
|
1441
|
+
function isClientShimPath(path5) {
|
|
1442
|
+
const normalized = path5.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1436
1443
|
return CLIENT_SHIM_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
1437
1444
|
}
|
|
1438
1445
|
var FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
@@ -1515,11 +1522,11 @@ var STORE_SPELL_PATH_PATTERN = /^spells\/(spell-[a-z0-9-]+)\.md$/;
|
|
|
1515
1522
|
function storeSpellPath(id) {
|
|
1516
1523
|
return `${USER_TIER_SPELLS_DIR}/${id}.md`;
|
|
1517
1524
|
}
|
|
1518
|
-
function spellIdFromCanonicalPath(
|
|
1519
|
-
return
|
|
1525
|
+
function spellIdFromCanonicalPath(path5) {
|
|
1526
|
+
return path5.replace(/\\/g, "/").match(CANONICAL_SPELL_PATH_PATTERN)?.[1];
|
|
1520
1527
|
}
|
|
1521
|
-
function spellIdFromStorePath(
|
|
1522
|
-
return
|
|
1528
|
+
function spellIdFromStorePath(path5) {
|
|
1529
|
+
return path5.replace(/\\/g, "/").match(STORE_SPELL_PATH_PATTERN)?.[1];
|
|
1523
1530
|
}
|
|
1524
1531
|
function absoluteStoreSpellPath(storeRoot, id) {
|
|
1525
1532
|
return resolve2(storeRoot, storeSpellPath(id)).replace(/\\/g, "/");
|
|
@@ -1618,9 +1625,9 @@ async function renderUserFanout(storeRoot, spellIds, fallbackDir) {
|
|
|
1618
1625
|
}
|
|
1619
1626
|
return { files, missing };
|
|
1620
1627
|
}
|
|
1621
|
-
async function classifyTarget(
|
|
1628
|
+
async function classifyTarget(path5) {
|
|
1622
1629
|
try {
|
|
1623
|
-
const stats = await lstat2(
|
|
1630
|
+
const stats = await lstat2(path5);
|
|
1624
1631
|
return stats.isFile() ? "file" : "other";
|
|
1625
1632
|
} catch (err) {
|
|
1626
1633
|
if (err.code === "ENOENT") return "absent";
|
|
@@ -2231,29 +2238,29 @@ var AgentConfigValidationError = class extends Error {
|
|
|
2231
2238
|
function isRecord2(value) {
|
|
2232
2239
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2233
2240
|
}
|
|
2234
|
-
function requireRecord(value,
|
|
2241
|
+
function requireRecord(value, path5, issues) {
|
|
2235
2242
|
if (!isRecord2(value)) {
|
|
2236
|
-
issues.push(`${
|
|
2243
|
+
issues.push(`${path5} must be an object`);
|
|
2237
2244
|
return void 0;
|
|
2238
2245
|
}
|
|
2239
2246
|
return value;
|
|
2240
2247
|
}
|
|
2241
|
-
function requireString(value,
|
|
2248
|
+
function requireString(value, path5, issues) {
|
|
2242
2249
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2243
|
-
issues.push(`${
|
|
2250
|
+
issues.push(`${path5} must be a non-empty string`);
|
|
2244
2251
|
}
|
|
2245
2252
|
}
|
|
2246
|
-
function requireBoolean(value,
|
|
2247
|
-
if (typeof value !== "boolean") issues.push(`${
|
|
2253
|
+
function requireBoolean(value, path5, issues) {
|
|
2254
|
+
if (typeof value !== "boolean") issues.push(`${path5} must be a boolean`);
|
|
2248
2255
|
}
|
|
2249
|
-
function requireStringArray(value,
|
|
2256
|
+
function requireStringArray(value, path5, issues) {
|
|
2250
2257
|
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
2251
|
-
issues.push(`${
|
|
2258
|
+
issues.push(`${path5} must be an array of strings`);
|
|
2252
2259
|
}
|
|
2253
2260
|
}
|
|
2254
|
-
function requireEnum(value, allowed,
|
|
2261
|
+
function requireEnum(value, allowed, path5, issues) {
|
|
2255
2262
|
if (typeof value !== "string" || !allowed.has(value)) {
|
|
2256
|
-
issues.push(`${
|
|
2263
|
+
issues.push(`${path5} must be one of: ${[...allowed].join(", ")}`);
|
|
2257
2264
|
}
|
|
2258
2265
|
}
|
|
2259
2266
|
function validateOptionalClient(clients, key, property, expected, issues) {
|
|
@@ -5913,12 +5920,12 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
|
|
|
5913
5920
|
if (conversions.length > 0) {
|
|
5914
5921
|
console.log(`
|
|
5915
5922
|
Converted ${conversions.length} symlinked managed file(s) to regular files (#357):`);
|
|
5916
|
-
const
|
|
5923
|
+
const describe3 = {
|
|
5917
5924
|
"index-symlink": "the index tracked it as a symlink (mode 120000); rewritten and staged as a regular file",
|
|
5918
5925
|
symlink: "was a symlink in the working tree; now a regular file (stage the typechange with git add)",
|
|
5919
5926
|
"path-body": "its body was only a link target, a symlink written out as text; rewritten as a real file"
|
|
5920
5927
|
};
|
|
5921
|
-
for (const c of conversions) console.log(` ${c.file} \u2014 ${
|
|
5928
|
+
for (const c of conversions) console.log(` ${c.file} \u2014 ${describe3[c.shape]}`);
|
|
5922
5929
|
if (conversions.some((c) => c.shape === "index-symlink")) {
|
|
5923
5930
|
console.log(
|
|
5924
5931
|
" Commit the staged typechange, or every other checkout keeps reading a link whose target is the file's own text."
|
|
@@ -6103,6 +6110,56 @@ async function checkForUpdate(currentVersion, feedUrl, cachePath = DEFAULT_CACHE
|
|
|
6103
6110
|
}
|
|
6104
6111
|
}
|
|
6105
6112
|
|
|
6113
|
+
// src/modules/state/versions.ts
|
|
6114
|
+
function compareVersions(a, b) {
|
|
6115
|
+
const parse3 = (v) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v);
|
|
6116
|
+
const pa = parse3(a);
|
|
6117
|
+
const pb = parse3(b);
|
|
6118
|
+
if (!pa || !pb) return a < b ? -1 : a > b ? 1 : 0;
|
|
6119
|
+
for (let i = 1; i <= 3; i++) {
|
|
6120
|
+
const diff = Number(pa[i]) - Number(pb[i]);
|
|
6121
|
+
if (diff !== 0) return diff;
|
|
6122
|
+
}
|
|
6123
|
+
const sa = pa[4];
|
|
6124
|
+
const sb = pb[4];
|
|
6125
|
+
if (sa === sb) return 0;
|
|
6126
|
+
if (sa === void 0) return 1;
|
|
6127
|
+
if (sb === void 0) return -1;
|
|
6128
|
+
return sa < sb ? -1 : 1;
|
|
6129
|
+
}
|
|
6130
|
+
async function collectVersions(manifest, packageVersion, options) {
|
|
6131
|
+
const warnings = [];
|
|
6132
|
+
let lookup = null;
|
|
6133
|
+
if (options.online) {
|
|
6134
|
+
let feedUrl = options.feedUrl ?? "";
|
|
6135
|
+
if (feedUrl === "") {
|
|
6136
|
+
try {
|
|
6137
|
+
feedUrl = getFeedUrl();
|
|
6138
|
+
} catch {
|
|
6139
|
+
feedUrl = "";
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
lookup = feedUrl ? await checkForUpdate(packageVersion, feedUrl, options.cachePath) : { current: packageVersion, latest: null, updateAvailable: false, error: "Feed URL unavailable" };
|
|
6143
|
+
if (lookup.latest === null) {
|
|
6144
|
+
warnings.push(
|
|
6145
|
+
`versions.latest: the npm lookup gave no answer${lookup.error ? ` (${lookup.error})` : ""}; latest and drift.cli_behind_latest are null`
|
|
6146
|
+
);
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
const latest = lookup?.latest ?? null;
|
|
6150
|
+
const versions = {
|
|
6151
|
+
files: manifest.version,
|
|
6152
|
+
cli: packageVersion,
|
|
6153
|
+
latest,
|
|
6154
|
+
drift: {
|
|
6155
|
+
files_behind_cli: compareVersions(manifest.version, packageVersion) < 0,
|
|
6156
|
+
cli_behind_latest: latest === null ? null : compareVersions(packageVersion, latest) < 0
|
|
6157
|
+
},
|
|
6158
|
+
components: manifest.components.map((component) => ({ name: component.name, installedVersion: component.installedVersion })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
6159
|
+
};
|
|
6160
|
+
return { versions, warnings, lookup };
|
|
6161
|
+
}
|
|
6162
|
+
|
|
6106
6163
|
// src/modules/diagram-generator.ts
|
|
6107
6164
|
function generateVersionDriftDiagram(repoFilesVersion, installedCliVersion, npmLatestVersion) {
|
|
6108
6165
|
if (repoFilesVersion === installedCliVersion && installedCliVersion === npmLatestVersion) {
|
|
@@ -6156,13 +6213,8 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
6156
6213
|
console.log("No components installed.");
|
|
6157
6214
|
return;
|
|
6158
6215
|
}
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
feedUrl = getFeedUrl();
|
|
6162
|
-
} catch {
|
|
6163
|
-
feedUrl = "";
|
|
6164
|
-
}
|
|
6165
|
-
const versionResult = feedUrl ? await checkForUpdate(packageVersion, feedUrl) : { current: packageVersion, latest: null, updateAvailable: false, error: "Feed URL unavailable" };
|
|
6216
|
+
const { versions, lookup } = await collectVersions(manifest, packageVersion, { online: true });
|
|
6217
|
+
const versionResult = lookup ?? { current: packageVersion, latest: null, updateAvailable: false };
|
|
6166
6218
|
const nameWidth = Math.max(
|
|
6167
6219
|
"Component".length,
|
|
6168
6220
|
...manifest.components.map((c) => c.name.length)
|
|
@@ -6219,25 +6271,25 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
6219
6271
|
}
|
|
6220
6272
|
}
|
|
6221
6273
|
console.log("");
|
|
6222
|
-
const latestStr = versionResult.error ? "unable to check" :
|
|
6223
|
-
let footer = ` Installed: ${
|
|
6274
|
+
const latestStr = versionResult.error ? "unable to check" : versions.latest ?? versions.files;
|
|
6275
|
+
let footer = ` Installed: ${versions.cli} Latest: ${latestStr}`;
|
|
6224
6276
|
if (versionResult.updateAvailable) {
|
|
6225
6277
|
footer += " (spell update available)";
|
|
6226
6278
|
}
|
|
6227
6279
|
console.log(footer);
|
|
6228
|
-
if (
|
|
6280
|
+
if (versions.latest !== null) {
|
|
6229
6281
|
const diagram = generateVersionDriftDiagram(
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6282
|
+
versions.files,
|
|
6283
|
+
versions.cli,
|
|
6284
|
+
versions.latest
|
|
6233
6285
|
);
|
|
6234
6286
|
if (diagram !== null) {
|
|
6235
6287
|
console.log("");
|
|
6236
6288
|
if (process.stdout.isTTY) {
|
|
6237
6289
|
console.log(" Version drift:");
|
|
6238
|
-
console.log(` repo-files: ${
|
|
6239
|
-
console.log(` installed-cli: ${
|
|
6240
|
-
console.log(` latest (npm): ${
|
|
6290
|
+
console.log(` repo-files: ${versions.files}`);
|
|
6291
|
+
console.log(` installed-cli: ${versions.cli}`);
|
|
6292
|
+
console.log(` latest (npm): ${versions.latest}`);
|
|
6241
6293
|
} else {
|
|
6242
6294
|
console.log("```mermaid");
|
|
6243
6295
|
console.log(diagram);
|
|
@@ -6252,9 +6304,1059 @@ async function runStatus(targetDir, packageVersion, options = {}) {
|
|
|
6252
6304
|
}
|
|
6253
6305
|
}
|
|
6254
6306
|
|
|
6307
|
+
// src/modules/state/agents.ts
|
|
6308
|
+
import { createHash as createHash2 } from "crypto";
|
|
6309
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6310
|
+
import { join as join13 } from "path";
|
|
6311
|
+
async function fileRevision(path5) {
|
|
6312
|
+
try {
|
|
6313
|
+
return `sha256:${createHash2("sha256").update(await readFile11(path5)).digest("hex")}`;
|
|
6314
|
+
} catch {
|
|
6315
|
+
return null;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6318
|
+
async function collectAgents(targetDir, options = {}) {
|
|
6319
|
+
let resolved;
|
|
6320
|
+
try {
|
|
6321
|
+
resolved = await resolveRoster(targetDir, "repo", options.homeDir);
|
|
6322
|
+
} catch (error) {
|
|
6323
|
+
if (error instanceof AgentRosterNotFoundError) return { agents: null, warnings: [] };
|
|
6324
|
+
throw error;
|
|
6325
|
+
}
|
|
6326
|
+
const origin = resolved.fromUserTier ? "user" : "repo";
|
|
6327
|
+
const warnings = [];
|
|
6328
|
+
const entries = [];
|
|
6329
|
+
for (const entry of resolved.roster.roster) {
|
|
6330
|
+
const definitionPath = join13(resolved.definitionsDir, `${entry.definition}.yaml`);
|
|
6331
|
+
const definition_revision = await fileRevision(definitionPath);
|
|
6332
|
+
if (definition_revision === null) {
|
|
6333
|
+
warnings.push(`agents: the definition file for "${entry.definition}" (${entry.id}) is missing from its definitions directory; definition_revision is null`);
|
|
6334
|
+
}
|
|
6335
|
+
entries.push({
|
|
6336
|
+
definition: entry.definition,
|
|
6337
|
+
name: entry.name,
|
|
6338
|
+
id: entry.id,
|
|
6339
|
+
epithet: entry.epithet ?? null,
|
|
6340
|
+
origin,
|
|
6341
|
+
definition_revision
|
|
6342
|
+
});
|
|
6343
|
+
}
|
|
6344
|
+
return {
|
|
6345
|
+
agents: {
|
|
6346
|
+
schema: "roster-v2",
|
|
6347
|
+
naming_strategy: resolved.roster.naming_strategy,
|
|
6348
|
+
agent_profile: resolved.roster.agent_profile,
|
|
6349
|
+
entries
|
|
6350
|
+
},
|
|
6351
|
+
warnings
|
|
6352
|
+
};
|
|
6353
|
+
}
|
|
6354
|
+
|
|
6355
|
+
// src/modules/state/books.ts
|
|
6356
|
+
import { createHash as createHash3 } from "crypto";
|
|
6357
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6358
|
+
import { join as join15 } from "path";
|
|
6359
|
+
|
|
6360
|
+
// src/modules/state/decisions.ts
|
|
6361
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6362
|
+
import { join as join14 } from "path";
|
|
6363
|
+
var HEADING = /^## ([A-Z]{2,5})-(\d{3,})\b(.*)$/gm;
|
|
6364
|
+
function headingAnchor(heading) {
|
|
6365
|
+
return heading.trim().toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, "").replace(/ /g, "-");
|
|
6366
|
+
}
|
|
6367
|
+
function titleAfterId(rest) {
|
|
6368
|
+
return rest.replace(/^\s*[—–:-]+\s*/u, "").trim();
|
|
6369
|
+
}
|
|
6370
|
+
function parseDecisions(content) {
|
|
6371
|
+
const text = content.replace(/\r\n?/g, "\n");
|
|
6372
|
+
const positions = [];
|
|
6373
|
+
let match;
|
|
6374
|
+
HEADING.lastIndex = 0;
|
|
6375
|
+
while ((match = HEADING.exec(text)) !== null) {
|
|
6376
|
+
positions.push({ id: `${match[1]}-${match[2]}`, heading: `${match[1]}-${match[2]}${match[3]}`, index: match.index });
|
|
6377
|
+
}
|
|
6378
|
+
const entries = [];
|
|
6379
|
+
const warnings = [];
|
|
6380
|
+
positions.forEach(({ id, heading, index }, i) => {
|
|
6381
|
+
const end = i + 1 < positions.length ? positions[i + 1].index : text.length;
|
|
6382
|
+
const section = text.slice(index, end);
|
|
6383
|
+
const dateMatch = /^\*\*Date:\*\*\s*(\d{4}-\d{2}-\d{2})/m.exec(section);
|
|
6384
|
+
const statusMatch = /^\*\*Status:\*\*\s*(\S+)/m.exec(section);
|
|
6385
|
+
if (!dateMatch && !statusMatch) {
|
|
6386
|
+
warnings.push(`indexes.decisions: ${id} has no **Date:** or **Status:** line and was skipped`);
|
|
6387
|
+
return;
|
|
6388
|
+
}
|
|
6389
|
+
entries.push({
|
|
6390
|
+
id,
|
|
6391
|
+
title: titleAfterId(heading.slice(id.length)),
|
|
6392
|
+
status: statusMatch ? statusMatch[1] : null,
|
|
6393
|
+
date: dateMatch ? dateMatch[1] : null,
|
|
6394
|
+
anchor: headingAnchor(heading)
|
|
6395
|
+
});
|
|
6396
|
+
});
|
|
6397
|
+
return { entries, warnings };
|
|
6398
|
+
}
|
|
6399
|
+
function parseAdrs(decisionsContent) {
|
|
6400
|
+
return parseDecisions(decisionsContent).entries.map(({ id, date, status }) => ({ id, date, status }));
|
|
6401
|
+
}
|
|
6402
|
+
function countAcceptedAdrsInWindow(adrs, fromDate, toDate) {
|
|
6403
|
+
return adrs.filter(
|
|
6404
|
+
(adr) => adr.status === "Accepted" && adr.date !== null && adr.date >= fromDate && adr.date <= toDate
|
|
6405
|
+
).length;
|
|
6406
|
+
}
|
|
6407
|
+
async function collectDecisions(root) {
|
|
6408
|
+
let content;
|
|
6409
|
+
try {
|
|
6410
|
+
content = await readFile12(join14(root, "DECISIONS.md"), "utf8");
|
|
6411
|
+
} catch (error) {
|
|
6412
|
+
if (error.code === "ENOENT") return { decisions: null, warnings: [] };
|
|
6413
|
+
return {
|
|
6414
|
+
decisions: null,
|
|
6415
|
+
warnings: [`indexes.decisions: DECISIONS.md could not be read (${error instanceof Error ? error.message : String(error)})`]
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
const { entries, warnings } = parseDecisions(content);
|
|
6419
|
+
return {
|
|
6420
|
+
decisions: entries.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
|
|
6421
|
+
warnings
|
|
6422
|
+
};
|
|
6423
|
+
}
|
|
6424
|
+
|
|
6425
|
+
// src/modules/state/books.ts
|
|
6426
|
+
var ITEM = /^\s*- \[( |x|X)\] (.*)$/;
|
|
6427
|
+
var HEADING2 = /^#{1,6}\s+(.+?)\s*#*\s*$/;
|
|
6428
|
+
var TEXT_LIMIT = 200;
|
|
6429
|
+
function normalizeItemText(text) {
|
|
6430
|
+
return text.replace(/\s+/g, " ").trim();
|
|
6431
|
+
}
|
|
6432
|
+
function itemLocator(section, text) {
|
|
6433
|
+
const slug = section === null ? "" : headingAnchor(section);
|
|
6434
|
+
const hash = createHash3("sha256").update(normalizeItemText(text), "utf8").digest("hex").slice(0, 12);
|
|
6435
|
+
return `${slug === "" ? "root" : slug}/${hash}`;
|
|
6436
|
+
}
|
|
6437
|
+
function parseBook(content) {
|
|
6438
|
+
const items = [];
|
|
6439
|
+
let section = null;
|
|
6440
|
+
let open = 0;
|
|
6441
|
+
let done = 0;
|
|
6442
|
+
for (const line of content.split(/\r?\n/)) {
|
|
6443
|
+
const heading = HEADING2.exec(line);
|
|
6444
|
+
if (heading) {
|
|
6445
|
+
section = heading[1];
|
|
6446
|
+
continue;
|
|
6447
|
+
}
|
|
6448
|
+
const item = ITEM.exec(line);
|
|
6449
|
+
if (!item) continue;
|
|
6450
|
+
const checked = item[1] !== " ";
|
|
6451
|
+
const text = item[2].trim();
|
|
6452
|
+
if (checked) done++;
|
|
6453
|
+
else open++;
|
|
6454
|
+
items.push({
|
|
6455
|
+
text: text.length > TEXT_LIMIT ? text.slice(0, TEXT_LIMIT) : text,
|
|
6456
|
+
section,
|
|
6457
|
+
locator: itemLocator(section, text),
|
|
6458
|
+
checked
|
|
6459
|
+
});
|
|
6460
|
+
}
|
|
6461
|
+
return { open, done, items };
|
|
6462
|
+
}
|
|
6463
|
+
async function collectBook(root, fileName) {
|
|
6464
|
+
let content;
|
|
6465
|
+
try {
|
|
6466
|
+
content = await readFile13(join15(root, fileName), "utf8");
|
|
6467
|
+
} catch (error) {
|
|
6468
|
+
if (error.code === "ENOENT") return { book: null, warnings: [] };
|
|
6469
|
+
return {
|
|
6470
|
+
book: null,
|
|
6471
|
+
warnings: [`indexes: ${fileName} could not be read (${error instanceof Error ? error.message : String(error)})`]
|
|
6472
|
+
};
|
|
6473
|
+
}
|
|
6474
|
+
return { book: parseBook(content), warnings: [] };
|
|
6475
|
+
}
|
|
6476
|
+
|
|
6477
|
+
// src/modules/state/config.ts
|
|
6478
|
+
function collectConfig(manifest) {
|
|
6479
|
+
return {
|
|
6480
|
+
tracking_mode: manifest.tracking_mode ?? null,
|
|
6481
|
+
external_provider: manifest.external_provider ?? null,
|
|
6482
|
+
push_policy: manifest.push_policy ?? null,
|
|
6483
|
+
role: manifest.role ?? null,
|
|
6484
|
+
business_root: manifest.business_root ?? null,
|
|
6485
|
+
spell_scope: manifest.spell_scope ?? null,
|
|
6486
|
+
content_sensitivity: manifest.content_sensitivity ?? null
|
|
6487
|
+
};
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6490
|
+
// src/modules/state/features.ts
|
|
6491
|
+
import { readdir as readdir7, readFile as readFile14 } from "fs/promises";
|
|
6492
|
+
import { join as join16 } from "path";
|
|
6493
|
+
import { parse as parse2 } from "yaml";
|
|
6494
|
+
var FEATURES_DIR = "features";
|
|
6495
|
+
var PRD_FILE = "PRD.md";
|
|
6496
|
+
var FRONTMATTER = /^(?:# [^\n]*\n+)?---\n([\s\S]*?)\n---(?:\n|$)/;
|
|
6497
|
+
var H1 = /^# (.+?)\s*$/m;
|
|
6498
|
+
function isRecord3(value) {
|
|
6499
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6500
|
+
}
|
|
6501
|
+
function stringOrNull(value) {
|
|
6502
|
+
if (typeof value === "string") return value;
|
|
6503
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
6504
|
+
return null;
|
|
6505
|
+
}
|
|
6506
|
+
function idOrNull(value) {
|
|
6507
|
+
if (typeof value === "number" || typeof value === "string") return value;
|
|
6508
|
+
return null;
|
|
6509
|
+
}
|
|
6510
|
+
function featureTracking(frontmatter) {
|
|
6511
|
+
const nested = frontmatter["tracking"];
|
|
6512
|
+
const source = isRecord3(nested) ? nested : "tracking_mode" in frontmatter || "external_provider" in frontmatter ? frontmatter : null;
|
|
6513
|
+
if (source === null) return null;
|
|
6514
|
+
return {
|
|
6515
|
+
tracking_mode: stringOrNull(source["tracking_mode"]),
|
|
6516
|
+
external_provider: stringOrNull(source["external_provider"]),
|
|
6517
|
+
adoWorkItemId: idOrNull(source["adoWorkItemId"]),
|
|
6518
|
+
githubIssueId: idOrNull(source["githubIssueId"])
|
|
6519
|
+
};
|
|
6520
|
+
}
|
|
6521
|
+
function parsePrd(content, displayPath) {
|
|
6522
|
+
const text = content.replace(/\r\n?/g, "\n");
|
|
6523
|
+
const heading = H1.exec(text);
|
|
6524
|
+
const empty = { title: heading ? heading[1] : null, status: null, implementation_status: null, tracking: null };
|
|
6525
|
+
const match = FRONTMATTER.exec(text);
|
|
6526
|
+
if (!match) return { fields: empty, warning: `features: ${displayPath} has no frontmatter` };
|
|
6527
|
+
let parsed;
|
|
6528
|
+
try {
|
|
6529
|
+
parsed = parse2(match[1]);
|
|
6530
|
+
} catch (error) {
|
|
6531
|
+
const reason = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
6532
|
+
return { fields: empty, warning: `features: ${displayPath} has malformed frontmatter (${reason})` };
|
|
6533
|
+
}
|
|
6534
|
+
if (!isRecord3(parsed)) return { fields: empty, warning: `features: ${displayPath} has frontmatter that is not a map` };
|
|
6535
|
+
return {
|
|
6536
|
+
fields: {
|
|
6537
|
+
title: stringOrNull(parsed["title"]) ?? empty.title,
|
|
6538
|
+
status: stringOrNull(parsed["status"]),
|
|
6539
|
+
implementation_status: stringOrNull(parsed["implementation_status"]),
|
|
6540
|
+
tracking: featureTracking(parsed)
|
|
6541
|
+
},
|
|
6542
|
+
warning: null
|
|
6543
|
+
};
|
|
6544
|
+
}
|
|
6545
|
+
async function collectFeatures(root) {
|
|
6546
|
+
const dir = join16(root, FEATURES_DIR);
|
|
6547
|
+
let slugs;
|
|
6548
|
+
try {
|
|
6549
|
+
const entries = await readdir7(dir, { withFileTypes: true });
|
|
6550
|
+
slugs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
6551
|
+
} catch (error) {
|
|
6552
|
+
if (error.code === "ENOENT") return { features: null, warnings: [] };
|
|
6553
|
+
return {
|
|
6554
|
+
features: null,
|
|
6555
|
+
warnings: [`features: ${FEATURES_DIR}/ could not be read (${error instanceof Error ? error.message : String(error)})`]
|
|
6556
|
+
};
|
|
6557
|
+
}
|
|
6558
|
+
const warnings = [];
|
|
6559
|
+
const features = [];
|
|
6560
|
+
for (const slug of slugs) {
|
|
6561
|
+
const path5 = `${FEATURES_DIR}/${slug}/${PRD_FILE}`;
|
|
6562
|
+
let content;
|
|
6563
|
+
try {
|
|
6564
|
+
content = await readFile14(join16(dir, slug, PRD_FILE), "utf8");
|
|
6565
|
+
} catch (error) {
|
|
6566
|
+
if (error.code === "ENOENT") continue;
|
|
6567
|
+
warnings.push(`features: ${path5} could not be read (${error instanceof Error ? error.message : String(error)})`);
|
|
6568
|
+
continue;
|
|
6569
|
+
}
|
|
6570
|
+
const { fields, warning } = parsePrd(content, path5);
|
|
6571
|
+
if (warning !== null) warnings.push(warning);
|
|
6572
|
+
features.push({ slug, ...fields, path: path5 });
|
|
6573
|
+
}
|
|
6574
|
+
features.sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
6575
|
+
return { features, warnings };
|
|
6576
|
+
}
|
|
6577
|
+
|
|
6578
|
+
// src/modules/state/serialize.ts
|
|
6579
|
+
import path4 from "path";
|
|
6580
|
+
var object = (keys) => ({ kind: "object", keys });
|
|
6581
|
+
var array = (items, sortBy) => ({
|
|
6582
|
+
kind: "array",
|
|
6583
|
+
items,
|
|
6584
|
+
...sortBy === void 0 ? {} : { sortBy }
|
|
6585
|
+
});
|
|
6586
|
+
var book = object({
|
|
6587
|
+
open: void 0,
|
|
6588
|
+
done: void 0,
|
|
6589
|
+
items: array(object({ text: void 0, section: void 0, locator: void 0, checked: void 0 }))
|
|
6590
|
+
});
|
|
6591
|
+
var STATE_SHAPE = object({
|
|
6592
|
+
schema_version: void 0,
|
|
6593
|
+
generated_at: void 0,
|
|
6594
|
+
generator: object({ name: void 0, version: void 0 }),
|
|
6595
|
+
repository: object({ root: void 0, scope: void 0, role: void 0, profile: void 0 }),
|
|
6596
|
+
versions: object({
|
|
6597
|
+
files: void 0,
|
|
6598
|
+
cli: void 0,
|
|
6599
|
+
latest: void 0,
|
|
6600
|
+
drift: object({ files_behind_cli: void 0, cli_behind_latest: void 0 }),
|
|
6601
|
+
components: array(object({ name: void 0, installedVersion: void 0 }), ["name"])
|
|
6602
|
+
}),
|
|
6603
|
+
git: object({
|
|
6604
|
+
branch: void 0,
|
|
6605
|
+
detached: void 0,
|
|
6606
|
+
trunk: void 0,
|
|
6607
|
+
ahead: void 0,
|
|
6608
|
+
behind: void 0,
|
|
6609
|
+
uncommitted: void 0,
|
|
6610
|
+
remote: object({ provider: void 0, url: void 0 }),
|
|
6611
|
+
worktrees: array(object({ path: void 0, branch: void 0 }), ["path"]),
|
|
6612
|
+
pull_request: object({ id: void 0, url: void 0, state: void 0 })
|
|
6613
|
+
}),
|
|
6614
|
+
indexes: object({
|
|
6615
|
+
decisions: array(
|
|
6616
|
+
object({ id: void 0, title: void 0, status: void 0, date: void 0, anchor: void 0 }),
|
|
6617
|
+
["id"]
|
|
6618
|
+
),
|
|
6619
|
+
todos: book,
|
|
6620
|
+
ideas: book,
|
|
6621
|
+
done: book,
|
|
6622
|
+
journal: array(object({ date: void 0, slug: void 0, title: void 0, path: void 0 }), ["date", "slug"])
|
|
6623
|
+
}),
|
|
6624
|
+
config: object({
|
|
6625
|
+
tracking_mode: void 0,
|
|
6626
|
+
external_provider: void 0,
|
|
6627
|
+
push_policy: void 0,
|
|
6628
|
+
role: void 0,
|
|
6629
|
+
business_root: void 0,
|
|
6630
|
+
spell_scope: void 0,
|
|
6631
|
+
content_sensitivity: void 0
|
|
6632
|
+
}),
|
|
6633
|
+
registry: object({
|
|
6634
|
+
updated: void 0,
|
|
6635
|
+
ventures: array(
|
|
6636
|
+
object({
|
|
6637
|
+
slug: void 0,
|
|
6638
|
+
name: void 0,
|
|
6639
|
+
aliases: void 0,
|
|
6640
|
+
status: void 0,
|
|
6641
|
+
visibility: void 0,
|
|
6642
|
+
ownership: void 0,
|
|
6643
|
+
tracking: void 0,
|
|
6644
|
+
repos: array(object({ name: void 0, remote: void 0, default: void 0, local_path: void 0 }))
|
|
6645
|
+
}),
|
|
6646
|
+
["slug"]
|
|
6647
|
+
)
|
|
6648
|
+
}),
|
|
6649
|
+
features: array(
|
|
6650
|
+
object({
|
|
6651
|
+
slug: void 0,
|
|
6652
|
+
title: void 0,
|
|
6653
|
+
status: void 0,
|
|
6654
|
+
implementation_status: void 0,
|
|
6655
|
+
tracking: object({
|
|
6656
|
+
tracking_mode: void 0,
|
|
6657
|
+
external_provider: void 0,
|
|
6658
|
+
adoWorkItemId: void 0,
|
|
6659
|
+
githubIssueId: void 0
|
|
6660
|
+
}),
|
|
6661
|
+
path: void 0
|
|
6662
|
+
}),
|
|
6663
|
+
["slug"]
|
|
6664
|
+
),
|
|
6665
|
+
agents: object({
|
|
6666
|
+
schema: void 0,
|
|
6667
|
+
naming_strategy: void 0,
|
|
6668
|
+
agent_profile: void 0,
|
|
6669
|
+
entries: array(
|
|
6670
|
+
object({
|
|
6671
|
+
definition: void 0,
|
|
6672
|
+
name: void 0,
|
|
6673
|
+
id: void 0,
|
|
6674
|
+
epithet: void 0,
|
|
6675
|
+
origin: void 0,
|
|
6676
|
+
definition_revision: void 0
|
|
6677
|
+
})
|
|
6678
|
+
)
|
|
6679
|
+
}),
|
|
6680
|
+
warnings: array(void 0, [])
|
|
6681
|
+
});
|
|
6682
|
+
function isRecord4(value) {
|
|
6683
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6684
|
+
}
|
|
6685
|
+
function compareScalars(a, b) {
|
|
6686
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
6687
|
+
const left = String(a);
|
|
6688
|
+
const right = String(b);
|
|
6689
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
6690
|
+
}
|
|
6691
|
+
function sortedBy(values, keys) {
|
|
6692
|
+
const indexed = values.map((value, index) => ({ value, index }));
|
|
6693
|
+
indexed.sort((a, b) => {
|
|
6694
|
+
for (const key of keys) {
|
|
6695
|
+
const result = compareScalars(
|
|
6696
|
+
isRecord4(a.value) ? a.value[key] : void 0,
|
|
6697
|
+
isRecord4(b.value) ? b.value[key] : void 0
|
|
6698
|
+
);
|
|
6699
|
+
if (result !== 0) return result;
|
|
6700
|
+
}
|
|
6701
|
+
return a.index - b.index;
|
|
6702
|
+
});
|
|
6703
|
+
return indexed.map((entry) => entry.value);
|
|
6704
|
+
}
|
|
6705
|
+
function canonicalize(value, shape) {
|
|
6706
|
+
if (value === null || value === void 0 || shape === void 0) return value;
|
|
6707
|
+
if (shape.kind === "array") {
|
|
6708
|
+
if (!Array.isArray(value)) return value;
|
|
6709
|
+
const items = value.map((item) => canonicalize(item, shape.items));
|
|
6710
|
+
if (shape.sortBy === void 0) return items;
|
|
6711
|
+
if (shape.sortBy.length === 0) return [...items].sort(compareScalars);
|
|
6712
|
+
return sortedBy(items, shape.sortBy);
|
|
6713
|
+
}
|
|
6714
|
+
if (!isRecord4(value)) return value;
|
|
6715
|
+
const ordered = {};
|
|
6716
|
+
for (const [key, keyShape] of Object.entries(shape.keys)) {
|
|
6717
|
+
if (!(key in value) || value[key] === void 0) continue;
|
|
6718
|
+
ordered[key] = canonicalize(value[key], keyShape);
|
|
6719
|
+
}
|
|
6720
|
+
for (const [key, extra] of Object.entries(value)) {
|
|
6721
|
+
if (key in ordered || key in shape.keys || extra === void 0) continue;
|
|
6722
|
+
ordered[key] = extra;
|
|
6723
|
+
}
|
|
6724
|
+
return ordered;
|
|
6725
|
+
}
|
|
6726
|
+
function serializeState(doc) {
|
|
6727
|
+
return `${JSON.stringify(canonicalize(doc, STATE_SHAPE), null, 2)}
|
|
6728
|
+
`;
|
|
6729
|
+
}
|
|
6730
|
+
function toPosixRelative(root, target) {
|
|
6731
|
+
const relative4 = path4.relative(root, target);
|
|
6732
|
+
if (relative4 === "") return ".";
|
|
6733
|
+
if (path4.isAbsolute(relative4)) return relative4.replace(/\\/g, "/");
|
|
6734
|
+
return relative4.split(path4.sep).join("/");
|
|
6735
|
+
}
|
|
6736
|
+
|
|
6737
|
+
// src/modules/state/git.ts
|
|
6738
|
+
function remoteProvider(url) {
|
|
6739
|
+
const host = remoteHost(url).toLowerCase();
|
|
6740
|
+
if (host === "github.com" || host.endsWith(".github.com")) return "github";
|
|
6741
|
+
if (host === "dev.azure.com" || host.endsWith(".dev.azure.com") || host.endsWith(".visualstudio.com")) return "ado";
|
|
6742
|
+
return "other";
|
|
6743
|
+
}
|
|
6744
|
+
function remoteHost(url) {
|
|
6745
|
+
const scheme = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]*@)?([^/:]+)/i.exec(url);
|
|
6746
|
+
if (scheme) return scheme[1];
|
|
6747
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):/.exec(url);
|
|
6748
|
+
return scp ? scp[1] : "";
|
|
6749
|
+
}
|
|
6750
|
+
function stripUserinfo(url) {
|
|
6751
|
+
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^@/]*@/i, "$1");
|
|
6752
|
+
}
|
|
6753
|
+
function parseWorktreeList(porcelain, root) {
|
|
6754
|
+
const worktrees = [];
|
|
6755
|
+
for (const block of porcelain.replace(/\r\n/g, "\n").split(/\n\n+/)) {
|
|
6756
|
+
const lines = block.split("\n").filter((line) => line.length > 0);
|
|
6757
|
+
const pathLine = lines.find((line) => line.startsWith("worktree "));
|
|
6758
|
+
if (!pathLine) continue;
|
|
6759
|
+
const branchLine = lines.find((line) => line.startsWith("branch "));
|
|
6760
|
+
const branch = branchLine ? branchLine.slice("branch ".length).replace(/^refs\/heads\//, "") : null;
|
|
6761
|
+
const absolute = resolveRealPath(pathLine.slice("worktree ".length));
|
|
6762
|
+
worktrees.push({ path: toPosixRelative(root, absolute), branch });
|
|
6763
|
+
}
|
|
6764
|
+
return worktrees.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
6765
|
+
}
|
|
6766
|
+
async function probe(read) {
|
|
6767
|
+
try {
|
|
6768
|
+
return { ok: true, value: await read() };
|
|
6769
|
+
} catch (error) {
|
|
6770
|
+
return { ok: false, error };
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
function describe(error) {
|
|
6774
|
+
return error instanceof Error ? error.message : String(error);
|
|
6775
|
+
}
|
|
6776
|
+
async function collectGit(cwd, root, options = {}) {
|
|
6777
|
+
const git = options.runGit ?? runGit;
|
|
6778
|
+
const warnings = [];
|
|
6779
|
+
const inside = await probe(() => git(cwd, ["rev-parse", "--is-inside-work-tree"]));
|
|
6780
|
+
if (!inside.ok || inside.value.stdout.trim() !== "true") {
|
|
6781
|
+
warnings.push(
|
|
6782
|
+
inside.ok ? "git: not a git repository; the git section is null" : `git: could not determine whether this is a git repository (${describe(inside.error)}); the git section is null`
|
|
6783
|
+
);
|
|
6784
|
+
return { git: null, warnings };
|
|
6785
|
+
}
|
|
6786
|
+
let branch = null;
|
|
6787
|
+
let detached = false;
|
|
6788
|
+
const symbolic = await probe(() => git(cwd, ["symbolic-ref", "--short", "-q", "HEAD"]));
|
|
6789
|
+
if (symbolic.ok && symbolic.value.stdout.trim() !== "") {
|
|
6790
|
+
branch = symbolic.value.stdout.trim();
|
|
6791
|
+
} else {
|
|
6792
|
+
const head = await probe(() => git(cwd, ["rev-parse", "--short", "HEAD"]));
|
|
6793
|
+
if (head.ok) {
|
|
6794
|
+
branch = head.value.stdout.trim();
|
|
6795
|
+
detached = true;
|
|
6796
|
+
} else {
|
|
6797
|
+
warnings.push(`git.branch: HEAD could not be read (${describe(head.error)})`);
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
let trunk = null;
|
|
6801
|
+
const originHead = await probe(() => git(cwd, ["symbolic-ref", "--short", "-q", "refs/remotes/origin/HEAD"]));
|
|
6802
|
+
if (originHead.ok && originHead.value.stdout.trim() !== "") {
|
|
6803
|
+
trunk = originHead.value.stdout.trim().replace(/^origin\//, "");
|
|
6804
|
+
} else {
|
|
6805
|
+
for (const candidate of ["main", "master"]) {
|
|
6806
|
+
const exists = await probe(() => git(cwd, ["rev-parse", "--verify", "--quiet", `refs/heads/${candidate}`]));
|
|
6807
|
+
if (exists.ok) {
|
|
6808
|
+
trunk = candidate;
|
|
6809
|
+
break;
|
|
6810
|
+
}
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
let ahead = null;
|
|
6814
|
+
let behind = null;
|
|
6815
|
+
const upstream = await probe(() => git(cwd, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]));
|
|
6816
|
+
if (upstream.ok) {
|
|
6817
|
+
const counts = await probe(() => git(cwd, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"]));
|
|
6818
|
+
if (counts.ok) {
|
|
6819
|
+
const [left, right] = counts.value.stdout.trim().split(/\s+/);
|
|
6820
|
+
behind = Number.parseInt(left, 10);
|
|
6821
|
+
ahead = Number.parseInt(right, 10);
|
|
6822
|
+
if (Number.isNaN(ahead) || Number.isNaN(behind)) {
|
|
6823
|
+
ahead = behind = null;
|
|
6824
|
+
warnings.push(`git.ahead/behind: unexpected rev-list output "${counts.value.stdout.trim()}"`);
|
|
6825
|
+
}
|
|
6826
|
+
} else {
|
|
6827
|
+
warnings.push(`git.ahead/behind: rev-list failed (${describe(counts.error)})`);
|
|
6828
|
+
}
|
|
6829
|
+
} else if (isTimeout(upstream.error)) {
|
|
6830
|
+
warnings.push(`git.ahead/behind: ${describe(upstream.error)}`);
|
|
6831
|
+
}
|
|
6832
|
+
let uncommitted = null;
|
|
6833
|
+
const status = await probe(() => git(cwd, ["status", "--porcelain", "-uall"]));
|
|
6834
|
+
if (status.ok) {
|
|
6835
|
+
const lines = status.value.stdout.trim();
|
|
6836
|
+
uncommitted = lines.length === 0 ? 0 : lines.split("\n").length;
|
|
6837
|
+
} else {
|
|
6838
|
+
warnings.push(`git.uncommitted: status failed (${describe(status.error)})`);
|
|
6839
|
+
}
|
|
6840
|
+
let remote = null;
|
|
6841
|
+
const remotes = await probe(() => git(cwd, ["remote"], { commandClass: "read" }));
|
|
6842
|
+
if (!remotes.ok) {
|
|
6843
|
+
warnings.push(`git.remote: remote list failed (${describe(remotes.error)})`);
|
|
6844
|
+
} else if (remotes.value.stdout.split(/\r?\n/).includes("origin")) {
|
|
6845
|
+
const url = await probe(() => git(cwd, ["remote", "get-url", "origin"]));
|
|
6846
|
+
if (url.ok) {
|
|
6847
|
+
const raw = url.value.stdout.trim();
|
|
6848
|
+
remote = { provider: remoteProvider(raw), url: stripUserinfo(raw) };
|
|
6849
|
+
} else {
|
|
6850
|
+
warnings.push(`git.remote: remote get-url failed (${describe(url.error)})`);
|
|
6851
|
+
}
|
|
6852
|
+
}
|
|
6853
|
+
let worktrees = null;
|
|
6854
|
+
const list = await probe(() => git(cwd, ["worktree", "list", "--porcelain"]));
|
|
6855
|
+
if (list.ok) {
|
|
6856
|
+
worktrees = parseWorktreeList(list.value.stdout, root);
|
|
6857
|
+
} else {
|
|
6858
|
+
warnings.push(`git.worktrees: worktree list failed (${describe(list.error)})`);
|
|
6859
|
+
}
|
|
6860
|
+
return {
|
|
6861
|
+
git: { branch, detached, trunk, ahead, behind, uncommitted, remote, worktrees, pull_request: null },
|
|
6862
|
+
warnings
|
|
6863
|
+
};
|
|
6864
|
+
}
|
|
6865
|
+
function isTimeout(error) {
|
|
6866
|
+
return error instanceof Error && error.name === "GitTimeoutError";
|
|
6867
|
+
}
|
|
6868
|
+
|
|
6869
|
+
// src/modules/state/journal.ts
|
|
6870
|
+
import { readdir as readdir8, readFile as readFile15 } from "fs/promises";
|
|
6871
|
+
import { join as join17 } from "path";
|
|
6872
|
+
var JOURNAL_DIR = "journal";
|
|
6873
|
+
var ENTRY_NAME = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
6874
|
+
function journalTitle(content) {
|
|
6875
|
+
const text = content.replace(/\r\n?/g, "\n");
|
|
6876
|
+
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
6877
|
+
if (frontmatter) {
|
|
6878
|
+
const title = /^title:\s*(.+)$/m.exec(frontmatter[1]);
|
|
6879
|
+
if (title) return title[1].trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
6880
|
+
}
|
|
6881
|
+
const heading = /^# (.+?)\s*$/m.exec(text);
|
|
6882
|
+
return heading ? heading[1].trim() : null;
|
|
6883
|
+
}
|
|
6884
|
+
async function collectJournal(root) {
|
|
6885
|
+
const dir = join17(root, JOURNAL_DIR);
|
|
6886
|
+
let names;
|
|
6887
|
+
try {
|
|
6888
|
+
const entries = await readdir8(dir, { withFileTypes: true });
|
|
6889
|
+
names = entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".")).map((entry) => entry.name);
|
|
6890
|
+
} catch (error) {
|
|
6891
|
+
if (error.code === "ENOENT") return { journal: null, warnings: [] };
|
|
6892
|
+
return {
|
|
6893
|
+
journal: null,
|
|
6894
|
+
warnings: [`indexes.journal: ${JOURNAL_DIR}/ could not be read (${error instanceof Error ? error.message : String(error)})`]
|
|
6895
|
+
};
|
|
6896
|
+
}
|
|
6897
|
+
const warnings = [];
|
|
6898
|
+
const journal = [];
|
|
6899
|
+
for (const name of names) {
|
|
6900
|
+
const match = ENTRY_NAME.exec(name);
|
|
6901
|
+
if (!match) {
|
|
6902
|
+
warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} does not match YYYY-MM-DD-<slug>.md and was skipped`);
|
|
6903
|
+
continue;
|
|
6904
|
+
}
|
|
6905
|
+
let title = null;
|
|
6906
|
+
try {
|
|
6907
|
+
title = journalTitle(await readFile15(join17(dir, name), "utf8"));
|
|
6908
|
+
} catch (error) {
|
|
6909
|
+
warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} could not be read (${error instanceof Error ? error.message : String(error)})`);
|
|
6910
|
+
}
|
|
6911
|
+
if (title === null) warnings.push(`indexes.journal: ${JOURNAL_DIR}/${name} has no title: frontmatter or # heading`);
|
|
6912
|
+
journal.push({ date: match[1], slug: match[2], title, path: `${JOURNAL_DIR}/${name}` });
|
|
6913
|
+
}
|
|
6914
|
+
journal.sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
6915
|
+
return { journal, warnings };
|
|
6916
|
+
}
|
|
6917
|
+
|
|
6918
|
+
// src/modules/exec.ts
|
|
6919
|
+
import { execFile as execFile2 } from "child_process";
|
|
6920
|
+
import { existsSync, readFileSync } from "fs";
|
|
6921
|
+
import { win32 } from "path";
|
|
6922
|
+
var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
|
|
6923
|
+
function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
6924
|
+
return new Promise((resolve10, reject) => {
|
|
6925
|
+
const child = execFile2(
|
|
6926
|
+
file,
|
|
6927
|
+
args,
|
|
6928
|
+
{
|
|
6929
|
+
timeout: timeoutMs,
|
|
6930
|
+
killSignal: "SIGTERM",
|
|
6931
|
+
windowsHide: true,
|
|
6932
|
+
encoding: "utf8",
|
|
6933
|
+
...options.env ? { env: { ...process.env, ...options.env } } : {}
|
|
6934
|
+
},
|
|
6935
|
+
(error, stdout, stderr) => {
|
|
6936
|
+
if (error) {
|
|
6937
|
+
reject(error);
|
|
6938
|
+
return;
|
|
6939
|
+
}
|
|
6940
|
+
resolve10({ stdout, stderr });
|
|
6941
|
+
}
|
|
6942
|
+
);
|
|
6943
|
+
child.stdin?.end();
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6946
|
+
function defaultProbes() {
|
|
6947
|
+
return {
|
|
6948
|
+
platform: process.platform,
|
|
6949
|
+
pathEnv: process.env["PATH"] ?? process.env["Path"],
|
|
6950
|
+
readText: (path5) => {
|
|
6951
|
+
try {
|
|
6952
|
+
return readFileSync(path5, "utf8");
|
|
6953
|
+
} catch {
|
|
6954
|
+
return void 0;
|
|
6955
|
+
}
|
|
6956
|
+
},
|
|
6957
|
+
exists: (path5) => existsSync(path5)
|
|
6958
|
+
};
|
|
6959
|
+
}
|
|
6960
|
+
var MSI_LAUNCHER_LINE = /^\s*"%~dp0\\\.\.\\python\.exe"\s+-IBm\s+azure\.cli\s+%\*\s*$/im;
|
|
6961
|
+
function resolveAzureCli(probes = defaultProbes()) {
|
|
6962
|
+
const plain = { file: "az", prefixArgs: [] };
|
|
6963
|
+
if (probes.platform !== "win32") return plain;
|
|
6964
|
+
for (const raw of (probes.pathEnv ?? "").split(";")) {
|
|
6965
|
+
const dir = raw.trim().replace(/^"(.*)"$/, "$1");
|
|
6966
|
+
if (!dir || !win32.isAbsolute(dir)) continue;
|
|
6967
|
+
const launcher = probes.readText(win32.join(dir, "az.cmd"));
|
|
6968
|
+
if (launcher === void 0) continue;
|
|
6969
|
+
if (!MSI_LAUNCHER_LINE.test(launcher)) return plain;
|
|
6970
|
+
const interpreter = win32.resolve(dir, "..", "python.exe");
|
|
6971
|
+
if (!probes.exists(interpreter)) return plain;
|
|
6972
|
+
return { file: interpreter, prefixArgs: ["-IBm", "azure.cli"], env: { AZ_INSTALLER: "MSI" } };
|
|
6973
|
+
}
|
|
6974
|
+
return plain;
|
|
6975
|
+
}
|
|
6976
|
+
|
|
6977
|
+
// src/modules/state/pull-request.ts
|
|
6978
|
+
function parseAdoRemote(url) {
|
|
6979
|
+
const https = /^https?:\/\/(?:[^@/]*@)?dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/?#]+)/i.exec(url);
|
|
6980
|
+
if (https) return { org: https[1], project: decodeURIComponent(https[2]), repo: decodeURIComponent(https[3]) };
|
|
6981
|
+
const legacy = /^https?:\/\/(?:[^@/]*@)?([^./]+)\.visualstudio\.com\/(?:DefaultCollection\/)?([^/]+)\/_git\/([^/?#]+)/i.exec(url);
|
|
6982
|
+
if (legacy) return { org: legacy[1], project: decodeURIComponent(legacy[2]), repo: decodeURIComponent(legacy[3]) };
|
|
6983
|
+
const ssh = /^(?:ssh:\/\/)?git@(?:ssh\.|vs-ssh\.)?(?:dev\.azure\.com|visualstudio\.com)[:/]v3\/([^/]+)\/([^/]+)\/([^/?#]+)/i.exec(url);
|
|
6984
|
+
if (ssh) return { org: ssh[1], project: decodeURIComponent(ssh[2]), repo: decodeURIComponent(ssh[3]) };
|
|
6985
|
+
return null;
|
|
6986
|
+
}
|
|
6987
|
+
function describe2(error) {
|
|
6988
|
+
if (error instanceof Error) {
|
|
6989
|
+
const code = error.code;
|
|
6990
|
+
if (code === "ENOENT") return "the CLI is not installed";
|
|
6991
|
+
if (error.killed) return `timed out after ${EXTERNAL_CLI_TIMEOUT_MS}ms`;
|
|
6992
|
+
return error.message.split("\n")[0];
|
|
6993
|
+
}
|
|
6994
|
+
return String(error);
|
|
6995
|
+
}
|
|
6996
|
+
function isRecord5(value) {
|
|
6997
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6998
|
+
}
|
|
6999
|
+
async function lookupGitHub(branch, exec) {
|
|
7000
|
+
let stdout;
|
|
7001
|
+
try {
|
|
7002
|
+
({ stdout } = await exec("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,state"], EXTERNAL_CLI_TIMEOUT_MS));
|
|
7003
|
+
} catch (error) {
|
|
7004
|
+
return { pull_request: null, warnings: [`git.pull_request: gh pr list failed (${describe2(error)})`] };
|
|
7005
|
+
}
|
|
7006
|
+
let parsed;
|
|
7007
|
+
try {
|
|
7008
|
+
parsed = JSON.parse(stdout);
|
|
7009
|
+
} catch {
|
|
7010
|
+
return { pull_request: null, warnings: ["git.pull_request: gh pr list printed something other than JSON"] };
|
|
7011
|
+
}
|
|
7012
|
+
if (!Array.isArray(parsed)) return { pull_request: null, warnings: ["git.pull_request: gh pr list printed something other than a list"] };
|
|
7013
|
+
const first = parsed.find(isRecord5);
|
|
7014
|
+
if (!first) return { pull_request: null, warnings: [] };
|
|
7015
|
+
if (typeof first["number"] !== "number" || typeof first["url"] !== "string" || typeof first["state"] !== "string") {
|
|
7016
|
+
return { pull_request: null, warnings: ["git.pull_request: gh pr list printed an entry without number, url and state"] };
|
|
7017
|
+
}
|
|
7018
|
+
return { pull_request: { id: first["number"], url: first["url"], state: first["state"] }, warnings: [] };
|
|
7019
|
+
}
|
|
7020
|
+
async function lookupAdo(branch, remoteUrl, exec, launch) {
|
|
7021
|
+
const coordinates = parseAdoRemote(remoteUrl);
|
|
7022
|
+
if (!coordinates) {
|
|
7023
|
+
return { pull_request: null, warnings: ["git.pull_request: the Azure DevOps remote URL names no organization, project and repository"] };
|
|
7024
|
+
}
|
|
7025
|
+
const orgUrl = `https://dev.azure.com/${coordinates.org}`;
|
|
7026
|
+
let stdout;
|
|
7027
|
+
try {
|
|
7028
|
+
({ stdout } = await exec(
|
|
7029
|
+
launch.file,
|
|
7030
|
+
[
|
|
7031
|
+
...launch.prefixArgs,
|
|
7032
|
+
"repos",
|
|
7033
|
+
"pr",
|
|
7034
|
+
"list",
|
|
7035
|
+
"--source-branch",
|
|
7036
|
+
branch,
|
|
7037
|
+
"--status",
|
|
7038
|
+
"active",
|
|
7039
|
+
"--org",
|
|
7040
|
+
orgUrl,
|
|
7041
|
+
"--project",
|
|
7042
|
+
coordinates.project,
|
|
7043
|
+
"--repository",
|
|
7044
|
+
coordinates.repo,
|
|
7045
|
+
"--output",
|
|
7046
|
+
"json"
|
|
7047
|
+
],
|
|
7048
|
+
EXTERNAL_CLI_TIMEOUT_MS,
|
|
7049
|
+
launch.env ? { env: launch.env } : {}
|
|
7050
|
+
));
|
|
7051
|
+
} catch (error) {
|
|
7052
|
+
return { pull_request: null, warnings: [`git.pull_request: az repos pr list failed (${describe2(error)})`] };
|
|
7053
|
+
}
|
|
7054
|
+
let parsed;
|
|
7055
|
+
try {
|
|
7056
|
+
parsed = JSON.parse(stdout);
|
|
7057
|
+
} catch {
|
|
7058
|
+
return { pull_request: null, warnings: ["git.pull_request: az repos pr list printed something other than JSON"] };
|
|
7059
|
+
}
|
|
7060
|
+
if (!Array.isArray(parsed)) return { pull_request: null, warnings: ["git.pull_request: az repos pr list printed something other than a list"] };
|
|
7061
|
+
const first = parsed.find(isRecord5);
|
|
7062
|
+
if (!first) return { pull_request: null, warnings: [] };
|
|
7063
|
+
const id = first["pullRequestId"];
|
|
7064
|
+
const status = first["status"];
|
|
7065
|
+
if (typeof id !== "number" || typeof status !== "string") {
|
|
7066
|
+
return { pull_request: null, warnings: ["git.pull_request: az repos pr list printed an entry without pullRequestId and status"] };
|
|
7067
|
+
}
|
|
7068
|
+
return {
|
|
7069
|
+
pull_request: {
|
|
7070
|
+
id,
|
|
7071
|
+
url: `${orgUrl}/${encodeURIComponent(coordinates.project)}/_git/${encodeURIComponent(coordinates.repo)}/pullrequest/${id}`,
|
|
7072
|
+
state: status
|
|
7073
|
+
},
|
|
7074
|
+
warnings: []
|
|
7075
|
+
};
|
|
7076
|
+
}
|
|
7077
|
+
async function collectPullRequest(git, options = {}) {
|
|
7078
|
+
const exec = options.exec ?? execFileWithTimeout;
|
|
7079
|
+
if (git.remote === null) return { pull_request: null, warnings: [] };
|
|
7080
|
+
if (git.branch === null || git.detached) {
|
|
7081
|
+
return { pull_request: null, warnings: ["git.pull_request: no branch is checked out, so there is nothing to look up"] };
|
|
7082
|
+
}
|
|
7083
|
+
switch (git.remote.provider) {
|
|
7084
|
+
case "github":
|
|
7085
|
+
return lookupGitHub(git.branch, exec);
|
|
7086
|
+
case "ado":
|
|
7087
|
+
return lookupAdo(git.branch, git.remote.url, exec, options.azureLaunch ?? resolveAzureCli());
|
|
7088
|
+
default:
|
|
7089
|
+
return { pull_request: null, warnings: [`git.pull_request: no pull-request lookup for a ${git.remote.provider} remote`] };
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
// src/modules/state/registry.ts
|
|
7094
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
7095
|
+
import { hostname } from "os";
|
|
7096
|
+
import { join as join18 } from "path";
|
|
7097
|
+
var DEFAULT_BUSINESS_ROOT = "ventures";
|
|
7098
|
+
function isRecord6(value) {
|
|
7099
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7100
|
+
}
|
|
7101
|
+
function stringOrNull2(value) {
|
|
7102
|
+
return typeof value === "string" ? value : null;
|
|
7103
|
+
}
|
|
7104
|
+
function localPathFor(clones, machine) {
|
|
7105
|
+
if (!isRecord6(clones)) return null;
|
|
7106
|
+
const wanted = machine.toLowerCase();
|
|
7107
|
+
for (const [key, value] of Object.entries(clones)) {
|
|
7108
|
+
if (key.toLowerCase() === wanted) return typeof value === "string" ? value : null;
|
|
7109
|
+
}
|
|
7110
|
+
return null;
|
|
7111
|
+
}
|
|
7112
|
+
function repoEntry(raw, options, machine) {
|
|
7113
|
+
const repo = isRecord6(raw) ? raw : {};
|
|
7114
|
+
const entry = {
|
|
7115
|
+
name: stringOrNull2(repo["name"]),
|
|
7116
|
+
remote: stringOrNull2(repo["remote"]),
|
|
7117
|
+
default: repo["default"] === true
|
|
7118
|
+
};
|
|
7119
|
+
if (options.includeLocalPaths) entry.local_path = localPathFor(repo["clones"], machine);
|
|
7120
|
+
return entry;
|
|
7121
|
+
}
|
|
7122
|
+
function ventureEntry(slug, raw, options, machine) {
|
|
7123
|
+
const venture = isRecord6(raw) ? raw : {};
|
|
7124
|
+
const aliases = Array.isArray(venture["aliases"]) ? venture["aliases"].filter((a) => typeof a === "string") : [];
|
|
7125
|
+
const repos = Array.isArray(venture["repos"]) ? venture["repos"].map((repo) => repoEntry(repo, options, machine)) : [];
|
|
7126
|
+
return {
|
|
7127
|
+
slug,
|
|
7128
|
+
name: stringOrNull2(venture["name"]),
|
|
7129
|
+
aliases,
|
|
7130
|
+
status: stringOrNull2(venture["status"]),
|
|
7131
|
+
visibility: stringOrNull2(venture["visibility"]),
|
|
7132
|
+
ownership: stringOrNull2(venture["ownership"]),
|
|
7133
|
+
tracking: venture["tracking"] ?? null,
|
|
7134
|
+
repos
|
|
7135
|
+
};
|
|
7136
|
+
}
|
|
7137
|
+
function parseRegistry(raw, options) {
|
|
7138
|
+
const machine = options.hostname ?? hostname();
|
|
7139
|
+
const registry = isRecord6(raw) ? raw : {};
|
|
7140
|
+
const ventures = [];
|
|
7141
|
+
const source = registry["ventures"];
|
|
7142
|
+
if (Array.isArray(source)) {
|
|
7143
|
+
for (const entry of source) {
|
|
7144
|
+
if (isRecord6(entry) && typeof entry["slug"] === "string") ventures.push(ventureEntry(entry["slug"], entry, options, machine));
|
|
7145
|
+
}
|
|
7146
|
+
} else if (isRecord6(source)) {
|
|
7147
|
+
for (const [slug, entry] of Object.entries(source)) ventures.push(ventureEntry(slug, entry, options, machine));
|
|
7148
|
+
}
|
|
7149
|
+
ventures.sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
7150
|
+
return { updated: stringOrNull2(registry["updated"]), ventures };
|
|
7151
|
+
}
|
|
7152
|
+
async function collectRegistry(root, manifest, options) {
|
|
7153
|
+
if (manifest.role !== "hub") return { registry: null, warnings: [] };
|
|
7154
|
+
const businessRoot = manifest.business_root ?? DEFAULT_BUSINESS_ROOT;
|
|
7155
|
+
const relativePath = `${businessRoot}/registry.json`;
|
|
7156
|
+
let content;
|
|
7157
|
+
try {
|
|
7158
|
+
content = await readFile16(join18(root, businessRoot, "registry.json"), "utf8");
|
|
7159
|
+
} catch (error) {
|
|
7160
|
+
const reason = error.code === "ENOENT" ? "is missing" : `could not be read (${error instanceof Error ? error.message : String(error)})`;
|
|
7161
|
+
return { registry: null, warnings: [`registry: this repository is a hub but ${relativePath} ${reason}`] };
|
|
7162
|
+
}
|
|
7163
|
+
let raw;
|
|
7164
|
+
try {
|
|
7165
|
+
raw = JSON.parse(content);
|
|
7166
|
+
} catch (error) {
|
|
7167
|
+
return {
|
|
7168
|
+
registry: null,
|
|
7169
|
+
warnings: [`registry: ${relativePath} is not valid JSON (${error instanceof Error ? error.message : String(error)})`]
|
|
7170
|
+
};
|
|
7171
|
+
}
|
|
7172
|
+
return { registry: parseRegistry(raw, options), warnings: [] };
|
|
7173
|
+
}
|
|
7174
|
+
|
|
7175
|
+
// src/modules/state/types.ts
|
|
7176
|
+
var STATE_SCHEMA_VERSION = "1.0.0";
|
|
7177
|
+
|
|
7178
|
+
// src/modules/state/collect.ts
|
|
7179
|
+
async function guarded(section, warnings, run, fallback) {
|
|
7180
|
+
try {
|
|
7181
|
+
const result = await run();
|
|
7182
|
+
warnings.push(...result.warnings);
|
|
7183
|
+
return result.value;
|
|
7184
|
+
} catch (error) {
|
|
7185
|
+
if (error instanceof AgentConfigValidationError) throw error;
|
|
7186
|
+
warnings.push(`${section}: collector failed (${error instanceof Error ? error.message : String(error)}); the section is null`);
|
|
7187
|
+
return fallback;
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
async function collectState(cwd, packageVersion, options) {
|
|
7191
|
+
const manifest = await readManifest(cwd);
|
|
7192
|
+
const root = resolveRealPath(cwd);
|
|
7193
|
+
const warnings = [];
|
|
7194
|
+
const resolvedScope = resolveInstallScope(cwd, manifest.scope, false);
|
|
7195
|
+
if (resolvedScope.misplacedUserManifest) warnings.push(`repository.scope: ${misplacedUserManifestMessage(cwd)}`);
|
|
7196
|
+
const versions = await guarded(
|
|
7197
|
+
"versions",
|
|
7198
|
+
warnings,
|
|
7199
|
+
async () => {
|
|
7200
|
+
const result = await collectVersions(manifest, packageVersion, {
|
|
7201
|
+
online: options.online,
|
|
7202
|
+
...options.feedUrl === void 0 ? {} : { feedUrl: options.feedUrl },
|
|
7203
|
+
...options.cachePath === void 0 ? {} : { cachePath: options.cachePath }
|
|
7204
|
+
});
|
|
7205
|
+
return { value: result.versions, warnings: result.warnings };
|
|
7206
|
+
},
|
|
7207
|
+
{
|
|
7208
|
+
files: manifest.version,
|
|
7209
|
+
cli: packageVersion,
|
|
7210
|
+
latest: null,
|
|
7211
|
+
drift: { files_behind_cli: false, cli_behind_latest: null },
|
|
7212
|
+
components: []
|
|
7213
|
+
}
|
|
7214
|
+
);
|
|
7215
|
+
const git = await guarded(
|
|
7216
|
+
"git",
|
|
7217
|
+
warnings,
|
|
7218
|
+
async () => {
|
|
7219
|
+
const result = await collectGit(cwd, root, options.runGit === void 0 ? {} : { runGit: options.runGit });
|
|
7220
|
+
return { value: result.git, warnings: result.warnings };
|
|
7221
|
+
},
|
|
7222
|
+
null
|
|
7223
|
+
);
|
|
7224
|
+
if (options.online && git !== null) {
|
|
7225
|
+
git.pull_request = await guarded(
|
|
7226
|
+
"git.pull_request",
|
|
7227
|
+
warnings,
|
|
7228
|
+
async () => {
|
|
7229
|
+
const result = await collectPullRequest(git, options.exec === void 0 ? {} : { exec: options.exec });
|
|
7230
|
+
return { value: result.pull_request, warnings: result.warnings };
|
|
7231
|
+
},
|
|
7232
|
+
null
|
|
7233
|
+
);
|
|
7234
|
+
}
|
|
7235
|
+
const decisions = await guarded(
|
|
7236
|
+
"indexes.decisions",
|
|
7237
|
+
warnings,
|
|
7238
|
+
async () => {
|
|
7239
|
+
const result = await collectDecisions(root);
|
|
7240
|
+
return { value: result.decisions, warnings: result.warnings };
|
|
7241
|
+
},
|
|
7242
|
+
null
|
|
7243
|
+
);
|
|
7244
|
+
const book2 = (fileName) => guarded(
|
|
7245
|
+
`indexes.${fileName}`,
|
|
7246
|
+
warnings,
|
|
7247
|
+
async () => {
|
|
7248
|
+
const result = await collectBook(root, fileName);
|
|
7249
|
+
return { value: result.book, warnings: result.warnings };
|
|
7250
|
+
},
|
|
7251
|
+
null
|
|
7252
|
+
);
|
|
7253
|
+
const todos = await book2("TODO.md");
|
|
7254
|
+
const ideas = await book2("IDEAS.md");
|
|
7255
|
+
const done = await book2("DONE.md");
|
|
7256
|
+
const journal = await guarded(
|
|
7257
|
+
"indexes.journal",
|
|
7258
|
+
warnings,
|
|
7259
|
+
async () => {
|
|
7260
|
+
const result = await collectJournal(root);
|
|
7261
|
+
return { value: result.journal, warnings: result.warnings };
|
|
7262
|
+
},
|
|
7263
|
+
null
|
|
7264
|
+
);
|
|
7265
|
+
const registry = await guarded(
|
|
7266
|
+
"registry",
|
|
7267
|
+
warnings,
|
|
7268
|
+
async () => {
|
|
7269
|
+
const result = await collectRegistry(root, manifest, {
|
|
7270
|
+
includeLocalPaths: options.includeLocalPaths,
|
|
7271
|
+
...options.hostname === void 0 ? {} : { hostname: options.hostname }
|
|
7272
|
+
});
|
|
7273
|
+
return { value: result.registry, warnings: result.warnings };
|
|
7274
|
+
},
|
|
7275
|
+
null
|
|
7276
|
+
);
|
|
7277
|
+
const features = await guarded(
|
|
7278
|
+
"features",
|
|
7279
|
+
warnings,
|
|
7280
|
+
async () => {
|
|
7281
|
+
const result = await collectFeatures(root);
|
|
7282
|
+
return { value: result.features, warnings: result.warnings };
|
|
7283
|
+
},
|
|
7284
|
+
null
|
|
7285
|
+
);
|
|
7286
|
+
const agents = await guarded(
|
|
7287
|
+
"agents",
|
|
7288
|
+
warnings,
|
|
7289
|
+
async () => {
|
|
7290
|
+
const result = await collectAgents(cwd, options.homeDir === void 0 ? {} : { homeDir: options.homeDir });
|
|
7291
|
+
return { value: result.agents, warnings: result.warnings };
|
|
7292
|
+
},
|
|
7293
|
+
null
|
|
7294
|
+
);
|
|
7295
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
7296
|
+
return {
|
|
7297
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
7298
|
+
generated_at: now().toISOString(),
|
|
7299
|
+
generator: { name: "arcane-cli", version: packageVersion },
|
|
7300
|
+
repository: {
|
|
7301
|
+
root,
|
|
7302
|
+
scope: resolvedScope.scope,
|
|
7303
|
+
role: manifest.role ?? "consumer",
|
|
7304
|
+
profile: manifest.profile
|
|
7305
|
+
},
|
|
7306
|
+
versions,
|
|
7307
|
+
git,
|
|
7308
|
+
indexes: { decisions, todos, ideas, done, journal },
|
|
7309
|
+
config: collectConfig(manifest),
|
|
7310
|
+
registry,
|
|
7311
|
+
features,
|
|
7312
|
+
agents,
|
|
7313
|
+
warnings
|
|
7314
|
+
};
|
|
7315
|
+
}
|
|
7316
|
+
|
|
7317
|
+
// src/commands/state.ts
|
|
7318
|
+
var USAGE_POINTER = "spell state prints one JSON document to stdout: run `spell state --json` (spell status is the human view).";
|
|
7319
|
+
function writeStdout(text) {
|
|
7320
|
+
return new Promise((resolve10, reject) => {
|
|
7321
|
+
process.stdout.write(text, (error) => error ? reject(error) : resolve10());
|
|
7322
|
+
});
|
|
7323
|
+
}
|
|
7324
|
+
async function runState(targetDir, packageVersion, options = {}, deps = {}) {
|
|
7325
|
+
if (!options.json) {
|
|
7326
|
+
console.error(USAGE_POINTER);
|
|
7327
|
+
process.exitCode = 2;
|
|
7328
|
+
return;
|
|
7329
|
+
}
|
|
7330
|
+
const collect = deps.collect ?? collectState;
|
|
7331
|
+
let text;
|
|
7332
|
+
try {
|
|
7333
|
+
const doc = await collect(targetDir, packageVersion, {
|
|
7334
|
+
online: options.online === true,
|
|
7335
|
+
includeLocalPaths: options.includeLocalPaths === true
|
|
7336
|
+
});
|
|
7337
|
+
text = serializeState(doc);
|
|
7338
|
+
} catch (error) {
|
|
7339
|
+
if (error instanceof ManifestNotFoundError) {
|
|
7340
|
+
console.error('Not initialized. Run "spell init" first.');
|
|
7341
|
+
} else if (error instanceof AgentConfigValidationError) {
|
|
7342
|
+
console.error(error.message);
|
|
7343
|
+
} else {
|
|
7344
|
+
console.error(`spell state: ${error instanceof Error ? error.message : String(error)}`);
|
|
7345
|
+
}
|
|
7346
|
+
process.exitCode = 1;
|
|
7347
|
+
return;
|
|
7348
|
+
}
|
|
7349
|
+
try {
|
|
7350
|
+
await (deps.write ?? writeStdout)(text);
|
|
7351
|
+
} catch (error) {
|
|
7352
|
+
console.error(`spell state: stdout is not writable (${error instanceof Error ? error.message : String(error)})`);
|
|
7353
|
+
process.exitCode = 1;
|
|
7354
|
+
}
|
|
7355
|
+
}
|
|
7356
|
+
|
|
6255
7357
|
// src/commands/uninstall.ts
|
|
6256
7358
|
import { rm as rm5 } from "fs/promises";
|
|
6257
|
-
import { dirname as dirname10, join as
|
|
7359
|
+
import { dirname as dirname10, join as join19, resolve as resolve6 } from "path";
|
|
6258
7360
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
6259
7361
|
async function runUninstall(options, targetDir) {
|
|
6260
7362
|
let manifest;
|
|
@@ -6326,9 +7428,9 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
6326
7428
|
}
|
|
6327
7429
|
let removed = 0;
|
|
6328
7430
|
let leftBehindLinks = 0;
|
|
6329
|
-
const skip = (
|
|
7431
|
+
const skip = (path5, err) => {
|
|
6330
7432
|
if (err instanceof LinkedDirectoryError) leftBehindLinks++;
|
|
6331
|
-
console.log(` ! Skipped ${
|
|
7433
|
+
console.log(` ! Skipped ${path5}: ${skipReason(err, targetDir)}`);
|
|
6332
7434
|
};
|
|
6333
7435
|
for (const component of manifest.components) {
|
|
6334
7436
|
for (const file of component.files) {
|
|
@@ -6340,7 +7442,7 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
6340
7442
|
}
|
|
6341
7443
|
}
|
|
6342
7444
|
}
|
|
6343
|
-
await rm5(
|
|
7445
|
+
await rm5(join19(targetDir, ".arcane.json"), { force: true });
|
|
6344
7446
|
const agentOutputs = [
|
|
6345
7447
|
".github/agents",
|
|
6346
7448
|
".arcane/agents",
|
|
@@ -6351,10 +7453,10 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
6351
7453
|
try {
|
|
6352
7454
|
validateRealTargetPath(targetDir, output);
|
|
6353
7455
|
} catch (err) {
|
|
6354
|
-
if (await fileExists(
|
|
7456
|
+
if (await fileExists(join19(targetDir, output))) skip(output, err);
|
|
6355
7457
|
continue;
|
|
6356
7458
|
}
|
|
6357
|
-
await rm5(
|
|
7459
|
+
await rm5(join19(targetDir, output), { recursive: true, force: true });
|
|
6358
7460
|
}
|
|
6359
7461
|
const markerFiles = [
|
|
6360
7462
|
"CLAUDE.md",
|
|
@@ -6432,10 +7534,10 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
6432
7534
|
console.log(` ! Skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
6433
7535
|
}
|
|
6434
7536
|
}
|
|
6435
|
-
await rm5(
|
|
6436
|
-
await rm5(
|
|
6437
|
-
await rm5(
|
|
6438
|
-
await removeDirectoryIfEmpty(
|
|
7537
|
+
await rm5(join19(storeRoot, ".arcane.json"), { force: true });
|
|
7538
|
+
await rm5(join19(storeRoot, "agents"), { recursive: true, force: true });
|
|
7539
|
+
await rm5(join19(storeRoot, "agents.yaml"), { force: true });
|
|
7540
|
+
await removeDirectoryIfEmpty(join19(storeRoot, USER_TIER_SPELLS_DIR));
|
|
6439
7541
|
await removeDirectoryIfEmpty(storeRoot);
|
|
6440
7542
|
for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
|
|
6441
7543
|
const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
|
|
@@ -6447,8 +7549,8 @@ async function uninstallUserTier(options, storeRoot, manifest) {
|
|
|
6447
7549
|
}
|
|
6448
7550
|
|
|
6449
7551
|
// src/commands/doctor.ts
|
|
6450
|
-
import { access as access4, readdir as
|
|
6451
|
-
import { join as
|
|
7552
|
+
import { access as access4, readdir as readdir11, readFile as readFile19, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
|
|
7553
|
+
import { join as join22, dirname as dirname11, resolve as resolve7 } from "path";
|
|
6452
7554
|
|
|
6453
7555
|
// src/config/incidents.ts
|
|
6454
7556
|
var INCIDENT_QUEUE = [
|
|
@@ -6516,8 +7618,8 @@ function evaluateIncidentGate(records) {
|
|
|
6516
7618
|
}
|
|
6517
7619
|
|
|
6518
7620
|
// src/modules/denylist-scan.ts
|
|
6519
|
-
import { readFile as
|
|
6520
|
-
import { join as
|
|
7621
|
+
import { readFile as readFile17, readdir as readdir9 } from "fs/promises";
|
|
7622
|
+
import { join as join20, relative } from "path";
|
|
6521
7623
|
function escapeRegExp(value) {
|
|
6522
7624
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6523
7625
|
}
|
|
@@ -6534,7 +7636,7 @@ async function scanFile(absolutePath, displayPath, rules) {
|
|
|
6534
7636
|
const findings = [];
|
|
6535
7637
|
let content;
|
|
6536
7638
|
try {
|
|
6537
|
-
content = await
|
|
7639
|
+
content = await readFile17(absolutePath, "utf8");
|
|
6538
7640
|
} catch {
|
|
6539
7641
|
return findings;
|
|
6540
7642
|
}
|
|
@@ -6563,12 +7665,12 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
|
|
|
6563
7665
|
const collected = [];
|
|
6564
7666
|
let entries;
|
|
6565
7667
|
try {
|
|
6566
|
-
entries = await
|
|
7668
|
+
entries = await readdir9(root, { withFileTypes: true });
|
|
6567
7669
|
} catch {
|
|
6568
7670
|
return collected;
|
|
6569
7671
|
}
|
|
6570
7672
|
for (const entry of entries) {
|
|
6571
|
-
const absolutePath =
|
|
7673
|
+
const absolutePath = join20(root, entry.name);
|
|
6572
7674
|
if (entry.isDirectory()) {
|
|
6573
7675
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
6574
7676
|
collected.push(...await collectScannableFiles(absolutePath, repoRoot, extensions));
|
|
@@ -6589,65 +7691,6 @@ async function scanRepository(repoRoot, rules, extensions, excludePrefixes = [])
|
|
|
6589
7691
|
return findings;
|
|
6590
7692
|
}
|
|
6591
7693
|
|
|
6592
|
-
// src/modules/exec.ts
|
|
6593
|
-
import { execFile as execFile2 } from "child_process";
|
|
6594
|
-
import { existsSync, readFileSync } from "fs";
|
|
6595
|
-
import { win32 } from "path";
|
|
6596
|
-
var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
|
|
6597
|
-
function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
6598
|
-
return new Promise((resolve10, reject) => {
|
|
6599
|
-
const child = execFile2(
|
|
6600
|
-
file,
|
|
6601
|
-
args,
|
|
6602
|
-
{
|
|
6603
|
-
timeout: timeoutMs,
|
|
6604
|
-
killSignal: "SIGTERM",
|
|
6605
|
-
windowsHide: true,
|
|
6606
|
-
encoding: "utf8",
|
|
6607
|
-
...options.env ? { env: { ...process.env, ...options.env } } : {}
|
|
6608
|
-
},
|
|
6609
|
-
(error, stdout, stderr) => {
|
|
6610
|
-
if (error) {
|
|
6611
|
-
reject(error);
|
|
6612
|
-
return;
|
|
6613
|
-
}
|
|
6614
|
-
resolve10({ stdout, stderr });
|
|
6615
|
-
}
|
|
6616
|
-
);
|
|
6617
|
-
child.stdin?.end();
|
|
6618
|
-
});
|
|
6619
|
-
}
|
|
6620
|
-
function defaultProbes() {
|
|
6621
|
-
return {
|
|
6622
|
-
platform: process.platform,
|
|
6623
|
-
pathEnv: process.env["PATH"] ?? process.env["Path"],
|
|
6624
|
-
readText: (path4) => {
|
|
6625
|
-
try {
|
|
6626
|
-
return readFileSync(path4, "utf8");
|
|
6627
|
-
} catch {
|
|
6628
|
-
return void 0;
|
|
6629
|
-
}
|
|
6630
|
-
},
|
|
6631
|
-
exists: (path4) => existsSync(path4)
|
|
6632
|
-
};
|
|
6633
|
-
}
|
|
6634
|
-
var MSI_LAUNCHER_LINE = /^\s*"%~dp0\\\.\.\\python\.exe"\s+-IBm\s+azure\.cli\s+%\*\s*$/im;
|
|
6635
|
-
function resolveAzureCli(probes = defaultProbes()) {
|
|
6636
|
-
const plain = { file: "az", prefixArgs: [] };
|
|
6637
|
-
if (probes.platform !== "win32") return plain;
|
|
6638
|
-
for (const raw of (probes.pathEnv ?? "").split(";")) {
|
|
6639
|
-
const dir = raw.trim().replace(/^"(.*)"$/, "$1");
|
|
6640
|
-
if (!dir || !win32.isAbsolute(dir)) continue;
|
|
6641
|
-
const launcher = probes.readText(win32.join(dir, "az.cmd"));
|
|
6642
|
-
if (launcher === void 0) continue;
|
|
6643
|
-
if (!MSI_LAUNCHER_LINE.test(launcher)) return plain;
|
|
6644
|
-
const interpreter = win32.resolve(dir, "..", "python.exe");
|
|
6645
|
-
if (!probes.exists(interpreter)) return plain;
|
|
6646
|
-
return { file: interpreter, prefixArgs: ["-IBm", "azure.cli"], env: { AZ_INSTALLER: "MSI" } };
|
|
6647
|
-
}
|
|
6648
|
-
return plain;
|
|
6649
|
-
}
|
|
6650
|
-
|
|
6651
7694
|
// src/modules/platform-policy.ts
|
|
6652
7695
|
var SANCTIONED_MERGE_METHODS = ["merge", "rebase"];
|
|
6653
7696
|
function compareToLadder(effective, declaredButBlocked, interactionNote) {
|
|
@@ -6815,7 +7858,7 @@ function decodeAdoSegment(raw) {
|
|
|
6815
7858
|
if (!decoded || decoded.startsWith("-")) return null;
|
|
6816
7859
|
return decoded;
|
|
6817
7860
|
}
|
|
6818
|
-
function
|
|
7861
|
+
function parseAdoRemote2(url) {
|
|
6819
7862
|
const match = /dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/?]+)/.exec(url) ?? /([^./]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/?]+)/.exec(url);
|
|
6820
7863
|
if (!match) return null;
|
|
6821
7864
|
const [, rawOrg, rawProject, rawRepo] = match;
|
|
@@ -6828,8 +7871,8 @@ function parseAdoRemote(url) {
|
|
|
6828
7871
|
}
|
|
6829
7872
|
|
|
6830
7873
|
// src/modules/placeholders.ts
|
|
6831
|
-
import { readdir as
|
|
6832
|
-
import { join as
|
|
7874
|
+
import { readdir as readdir10, readFile as readFile18 } from "fs/promises";
|
|
7875
|
+
import { join as join21 } from "path";
|
|
6833
7876
|
var PLACEHOLDER_STANDARD_FILE = "spell-authoring-standards.md";
|
|
6834
7877
|
var RUNTIME_PLACEHOLDERS_START = "<!-- runtime-placeholders:start -->";
|
|
6835
7878
|
var RUNTIME_PLACEHOLDERS_END = "<!-- runtime-placeholders:end -->";
|
|
@@ -6924,7 +7967,7 @@ function parseRuntimePlaceholders(standardContent) {
|
|
|
6924
7967
|
async function scanGovernancePlaceholders(governanceDir) {
|
|
6925
7968
|
let standard;
|
|
6926
7969
|
try {
|
|
6927
|
-
standard = await
|
|
7970
|
+
standard = await readFile18(join21(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
|
|
6928
7971
|
} catch (err) {
|
|
6929
7972
|
if (err.code === "ENOENT") {
|
|
6930
7973
|
return { status: "skipped", reason: `${PLACEHOLDER_STANDARD_FILE} is not installed` };
|
|
@@ -6938,12 +7981,12 @@ async function scanGovernancePlaceholders(governanceDir) {
|
|
|
6938
7981
|
reason: `${PLACEHOLDER_STANDARD_FILE} has no runtime-placeholders list (an older version?)`
|
|
6939
7982
|
};
|
|
6940
7983
|
}
|
|
6941
|
-
const files = (await
|
|
7984
|
+
const files = (await readdir10(governanceDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
6942
7985
|
const activeDocs = [];
|
|
6943
7986
|
const templateDocs = [];
|
|
6944
7987
|
const findings = [];
|
|
6945
7988
|
for (const file of files) {
|
|
6946
|
-
const content = await
|
|
7989
|
+
const content = await readFile18(join21(governanceDir, file), "utf8");
|
|
6947
7990
|
const status = parseFrontmatterStatus(content);
|
|
6948
7991
|
if (status === "template") templateDocs.push(file);
|
|
6949
7992
|
if (status !== "active") continue;
|
|
@@ -6955,7 +7998,7 @@ async function scanGovernancePlaceholders(governanceDir) {
|
|
|
6955
7998
|
}
|
|
6956
7999
|
|
|
6957
8000
|
// src/commands/doctor.ts
|
|
6958
|
-
function
|
|
8001
|
+
function isRecord7(value) {
|
|
6959
8002
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6960
8003
|
}
|
|
6961
8004
|
async function checkNodeVersion() {
|
|
@@ -6974,14 +8017,14 @@ async function checkNodeVersion() {
|
|
|
6974
8017
|
async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
|
|
6975
8018
|
const name = `VS Code extension: ${label2}`;
|
|
6976
8019
|
const flavours = [
|
|
6977
|
-
{ dir:
|
|
6978
|
-
{ dir:
|
|
8020
|
+
{ dir: join22(homeDir, ".vscode", "extensions"), bin: "code" },
|
|
8021
|
+
{ dir: join22(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
|
|
6979
8022
|
];
|
|
6980
8023
|
let installBin = null;
|
|
6981
8024
|
for (const flavour of flavours) {
|
|
6982
8025
|
let entries;
|
|
6983
8026
|
try {
|
|
6984
|
-
entries = await
|
|
8027
|
+
entries = await readdir11(flavour.dir);
|
|
6985
8028
|
} catch {
|
|
6986
8029
|
continue;
|
|
6987
8030
|
}
|
|
@@ -7003,15 +8046,15 @@ async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
|
|
|
7003
8046
|
}
|
|
7004
8047
|
async function checkArcaneManifest(targetDir) {
|
|
7005
8048
|
const name = "Arcane manifest (.arcane.json)";
|
|
7006
|
-
const manifestPath2 =
|
|
8049
|
+
const manifestPath2 = join22(targetDir, ".arcane.json");
|
|
7007
8050
|
try {
|
|
7008
8051
|
await access4(manifestPath2);
|
|
7009
8052
|
} catch {
|
|
7010
|
-
const selfHostedManifestPath =
|
|
8053
|
+
const selfHostedManifestPath = join22(targetDir, "src", "assets", ".arcane.json");
|
|
7011
8054
|
try {
|
|
7012
|
-
const raw = await
|
|
8055
|
+
const raw = await readFile19(selfHostedManifestPath, "utf8");
|
|
7013
8056
|
const manifest = JSON.parse(raw);
|
|
7014
|
-
if (
|
|
8057
|
+
if (isRecord7(manifest) && manifest["selfHosted"] === true && manifest["tracking_mode"] === "internal") {
|
|
7015
8058
|
return {
|
|
7016
8059
|
name,
|
|
7017
8060
|
passed: true,
|
|
@@ -7028,9 +8071,9 @@ async function checkArcaneManifest(targetDir) {
|
|
|
7028
8071
|
};
|
|
7029
8072
|
}
|
|
7030
8073
|
try {
|
|
7031
|
-
const raw = await
|
|
8074
|
+
const raw = await readFile19(manifestPath2, "utf8");
|
|
7032
8075
|
const manifest = JSON.parse(raw);
|
|
7033
|
-
if (!
|
|
8076
|
+
if (!isRecord7(manifest) || typeof manifest["version"] !== "string" || !manifest["version"] || !Array.isArray(manifest["components"])) {
|
|
7034
8077
|
return {
|
|
7035
8078
|
name,
|
|
7036
8079
|
passed: false,
|
|
@@ -7113,7 +8156,7 @@ async function checkSessionContinuity(targetDir) {
|
|
|
7113
8156
|
const results = [];
|
|
7114
8157
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
7115
8158
|
const name = `Session file: ${file}`;
|
|
7116
|
-
const filePath =
|
|
8159
|
+
const filePath = join22(targetDir, file);
|
|
7117
8160
|
try {
|
|
7118
8161
|
await access4(filePath);
|
|
7119
8162
|
results.push({ name, passed: true, message: "present" });
|
|
@@ -7131,7 +8174,7 @@ async function checkSessionContinuity(targetDir) {
|
|
|
7131
8174
|
async function fixSessionContinuity(targetDir, assetsDir) {
|
|
7132
8175
|
const created = [];
|
|
7133
8176
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
7134
|
-
const destPath =
|
|
8177
|
+
const destPath = join22(targetDir, file);
|
|
7135
8178
|
try {
|
|
7136
8179
|
await access4(destPath);
|
|
7137
8180
|
} catch {
|
|
@@ -7142,7 +8185,7 @@ async function fixSessionContinuity(targetDir, assetsDir) {
|
|
|
7142
8185
|
);
|
|
7143
8186
|
continue;
|
|
7144
8187
|
}
|
|
7145
|
-
const srcPath =
|
|
8188
|
+
const srcPath = join22(assetsDir, file);
|
|
7146
8189
|
await mkdir7(dirname11(destPath), { recursive: true });
|
|
7147
8190
|
await fsCopyFile(srcPath, destPath);
|
|
7148
8191
|
created.push(file);
|
|
@@ -7270,7 +8313,7 @@ async function checkGovernancePlaceholders(targetDir) {
|
|
|
7270
8313
|
const name = "Governance placeholders (ARC-051)";
|
|
7271
8314
|
let scan;
|
|
7272
8315
|
try {
|
|
7273
|
-
scan = await scanGovernancePlaceholders(
|
|
8316
|
+
scan = await scanGovernancePlaceholders(join22(targetDir, ".arcane", "governance"));
|
|
7274
8317
|
} catch (err) {
|
|
7275
8318
|
return {
|
|
7276
8319
|
name,
|
|
@@ -7357,10 +8400,10 @@ async function checkNewlyAvailableComponents(targetDir) {
|
|
|
7357
8400
|
}
|
|
7358
8401
|
async function checkMcpConfig(targetDir) {
|
|
7359
8402
|
const name = "MCP config (per-server timeout, I12)";
|
|
7360
|
-
const filePath =
|
|
8403
|
+
const filePath = join22(targetDir, ".mcp.json");
|
|
7361
8404
|
let raw;
|
|
7362
8405
|
try {
|
|
7363
|
-
raw = await
|
|
8406
|
+
raw = await readFile19(filePath, "utf8");
|
|
7364
8407
|
} catch {
|
|
7365
8408
|
return { name, passed: true, blocking: false, message: "no .mcp.json \u2014 nothing to check" };
|
|
7366
8409
|
}
|
|
@@ -7375,7 +8418,7 @@ async function checkMcpConfig(targetDir) {
|
|
|
7375
8418
|
message: ".mcp.json exists but contains invalid JSON"
|
|
7376
8419
|
};
|
|
7377
8420
|
}
|
|
7378
|
-
if (!
|
|
8421
|
+
if (!isRecord7(rawParsed)) {
|
|
7379
8422
|
return {
|
|
7380
8423
|
name,
|
|
7381
8424
|
passed: false,
|
|
@@ -7433,17 +8476,17 @@ async function checkMcpConfig(targetDir) {
|
|
|
7433
8476
|
};
|
|
7434
8477
|
}
|
|
7435
8478
|
function isDelegation(value) {
|
|
7436
|
-
return
|
|
8479
|
+
return isRecord7(value) && typeof value["id"] === "string" && typeof value["status"] === "string" && typeof value["scope"] === "string" && Array.isArray(value["excludedActions"]);
|
|
7437
8480
|
}
|
|
7438
8481
|
function isDelegationsFile(value) {
|
|
7439
|
-
return
|
|
8482
|
+
return isRecord7(value) && Array.isArray(value["delegations"]) && value["delegations"].every(isDelegation);
|
|
7440
8483
|
}
|
|
7441
8484
|
async function checkDelegations(targetDir) {
|
|
7442
8485
|
const name = "Standing delegations (T13)";
|
|
7443
|
-
const filePath =
|
|
8486
|
+
const filePath = join22(targetDir, ".arcane", "delegations.json");
|
|
7444
8487
|
let raw;
|
|
7445
8488
|
try {
|
|
7446
|
-
raw = await
|
|
8489
|
+
raw = await readFile19(filePath, "utf8");
|
|
7447
8490
|
} catch {
|
|
7448
8491
|
return { name, passed: true, blocking: false, message: "no delegations recorded" };
|
|
7449
8492
|
}
|
|
@@ -7501,7 +8544,7 @@ async function checkPlatformBranchPolicy(targetDir) {
|
|
|
7501
8544
|
const result = evaluateGitHubMergePolicy(rulesets);
|
|
7502
8545
|
return { name, passed: result.passed, blocking: false, message: result.message };
|
|
7503
8546
|
}
|
|
7504
|
-
const ado =
|
|
8547
|
+
const ado = parseAdoRemote2(url);
|
|
7505
8548
|
if (ado) {
|
|
7506
8549
|
const policies = await fetchAdoMergeTypePolicies(ado.org, ado.project, ado.repo, "main");
|
|
7507
8550
|
if (policies === null) {
|
|
@@ -7543,7 +8586,7 @@ async function checkManagedFileModes(targetDir) {
|
|
|
7543
8586
|
if (!isClientShimPath(file) || asLinks.includes(file)) continue;
|
|
7544
8587
|
let body;
|
|
7545
8588
|
try {
|
|
7546
|
-
body = await
|
|
8589
|
+
body = await readFile19(join22(targetDir, file), "utf-8");
|
|
7547
8590
|
} catch {
|
|
7548
8591
|
continue;
|
|
7549
8592
|
}
|
|
@@ -7594,19 +8637,19 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
|
|
|
7594
8637
|
}
|
|
7595
8638
|
let spellCount = 0;
|
|
7596
8639
|
let probed = false;
|
|
7597
|
-
const
|
|
8640
|
+
const probe2 = "spell-status.md";
|
|
7598
8641
|
try {
|
|
7599
|
-
const entries = await
|
|
8642
|
+
const entries = await readdir11(join22(storeRoot, USER_TIER_SPELLS_DIR));
|
|
7600
8643
|
const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
|
|
7601
8644
|
spellCount = spells.length;
|
|
7602
|
-
probed = spells.includes(
|
|
8645
|
+
probed = spells.includes(probe2);
|
|
7603
8646
|
} catch {
|
|
7604
8647
|
}
|
|
7605
8648
|
if (spellCount === 0 || !probed) {
|
|
7606
8649
|
return {
|
|
7607
8650
|
name,
|
|
7608
8651
|
passed: false,
|
|
7609
|
-
message: `the user tier at ${storeRoot} holds ${spellCount} spell(s)${spellCount > 0 && !probed ? ` and not ${
|
|
8652
|
+
message: `the user tier at ${storeRoot} holds ${spellCount} spell(s)${spellCount > 0 && !probed ? ` and not ${probe2}` : ""} \u2014 this repository expects it to supply every spell. Run \`spell update --user\`.`
|
|
7610
8653
|
};
|
|
7611
8654
|
}
|
|
7612
8655
|
const majorMinor = (v) => v.split(".").slice(0, 2).join(".");
|
|
@@ -7630,7 +8673,7 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
|
|
|
7630
8673
|
name,
|
|
7631
8674
|
passed: true,
|
|
7632
8675
|
blocking: false,
|
|
7633
|
-
message: `user \u2014 ${spellCount} spell(s) from ${storeRoot} (v${store.version}), ${
|
|
8676
|
+
message: `user \u2014 ${spellCount} spell(s) from ${storeRoot} (v${store.version}), ${probe2} present${repoExpectsAgents ? ", agent roster present in the tier" : ""}`
|
|
7634
8677
|
};
|
|
7635
8678
|
}
|
|
7636
8679
|
async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
@@ -7714,16 +8757,16 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
7714
8757
|
}
|
|
7715
8758
|
|
|
7716
8759
|
// src/commands/ward.ts
|
|
7717
|
-
import { readFile as
|
|
8760
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
7718
8761
|
|
|
7719
8762
|
// src/modules/ward.ts
|
|
7720
|
-
import { readdir as
|
|
7721
|
-
import { join as
|
|
8763
|
+
import { readdir as readdir12, readFile as readFile20 } from "fs/promises";
|
|
8764
|
+
import { join as join23, relative as relative2 } from "path";
|
|
7722
8765
|
async function scanFileAllRules(absolutePath, displayPath, rules) {
|
|
7723
8766
|
const findings = [];
|
|
7724
8767
|
let content;
|
|
7725
8768
|
try {
|
|
7726
|
-
content = await
|
|
8769
|
+
content = await readFile20(absolutePath, "utf8");
|
|
7727
8770
|
} catch {
|
|
7728
8771
|
return findings;
|
|
7729
8772
|
}
|
|
@@ -7776,12 +8819,12 @@ async function scanFilenames(root, rules, repoRoot = root) {
|
|
|
7776
8819
|
const findings = [];
|
|
7777
8820
|
let entries;
|
|
7778
8821
|
try {
|
|
7779
|
-
entries = await
|
|
8822
|
+
entries = await readdir12(root, { withFileTypes: true });
|
|
7780
8823
|
} catch {
|
|
7781
8824
|
return findings;
|
|
7782
8825
|
}
|
|
7783
8826
|
for (const entry of entries) {
|
|
7784
|
-
const absolutePath =
|
|
8827
|
+
const absolutePath = join23(root, entry.name);
|
|
7785
8828
|
if (entry.isDirectory()) {
|
|
7786
8829
|
if (SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
7787
8830
|
findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
|
|
@@ -7818,7 +8861,7 @@ async function runWard(repoRoot, options = {}) {
|
|
|
7818
8861
|
const findings2 = await scanFileAllRules(absolutePath, displayPath, allRules);
|
|
7819
8862
|
if (findings2.length > 0) {
|
|
7820
8863
|
try {
|
|
7821
|
-
fileLines.set(displayPath, (await
|
|
8864
|
+
fileLines.set(displayPath, (await readFile20(absolutePath, "utf8")).split("\n"));
|
|
7822
8865
|
} catch {
|
|
7823
8866
|
}
|
|
7824
8867
|
}
|
|
@@ -7846,7 +8889,7 @@ function parseTermsFileContent(raw) {
|
|
|
7846
8889
|
}
|
|
7847
8890
|
async function readTermsFile(filePath) {
|
|
7848
8891
|
try {
|
|
7849
|
-
return parseTermsFileContent(await
|
|
8892
|
+
return parseTermsFileContent(await readFile21(filePath, "utf8"));
|
|
7850
8893
|
} catch (error) {
|
|
7851
8894
|
console.warn(
|
|
7852
8895
|
`\u26A0 arcane: could not read --terms-file "${filePath}": ${error.message}`
|
|
@@ -8057,7 +9100,7 @@ async function runBlockPush(targetDir) {
|
|
|
8057
9100
|
}
|
|
8058
9101
|
|
|
8059
9102
|
// src/modules/credential-store.ts
|
|
8060
|
-
import { createHash as
|
|
9103
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
8061
9104
|
var CHUNK_SIZE = 1e3;
|
|
8062
9105
|
var MAX_CHUNKS = 64;
|
|
8063
9106
|
var INDEX_NAME = "session/index";
|
|
@@ -8071,7 +9114,7 @@ var KeychainUnavailableError = class extends Error {
|
|
|
8071
9114
|
}
|
|
8072
9115
|
};
|
|
8073
9116
|
function sha256Hex(value) {
|
|
8074
|
-
return
|
|
9117
|
+
return createHash4("sha256").update(value, "utf8").digest("hex");
|
|
8075
9118
|
}
|
|
8076
9119
|
function chunkName(gen, n) {
|
|
8077
9120
|
return `${CHUNK_PREFIX}${gen}/${n}`;
|
|
@@ -8215,17 +9258,17 @@ import { execFile as execFile3 } from "child_process";
|
|
|
8215
9258
|
import * as client from "openid-client";
|
|
8216
9259
|
|
|
8217
9260
|
// src/modules/result-pages.ts
|
|
8218
|
-
import { createHash as
|
|
8219
|
-
import { readFile as
|
|
8220
|
-
import { join as
|
|
9261
|
+
import { createHash as createHash5 } from "crypto";
|
|
9262
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
9263
|
+
import { join as join24 } from "path";
|
|
8221
9264
|
var RESULT_KINDS = ["success", "denied", "failure"];
|
|
8222
9265
|
function resultPagePath(kind) {
|
|
8223
|
-
return
|
|
9266
|
+
return join24("sign-in", `sign-in-callback-${kind}.html`);
|
|
8224
9267
|
}
|
|
8225
9268
|
function styleHashes(html) {
|
|
8226
9269
|
return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((match) => {
|
|
8227
9270
|
const text = match[1].replace(/\r\n?/g, "\n");
|
|
8228
|
-
return `'sha256-${
|
|
9271
|
+
return `'sha256-${createHash5("sha256").update(text, "utf8").digest("base64")}'`;
|
|
8229
9272
|
});
|
|
8230
9273
|
}
|
|
8231
9274
|
function resultPageHeaders(html) {
|
|
@@ -8268,7 +9311,7 @@ var PLAIN_RESULT_PAGES = {
|
|
|
8268
9311
|
async function loadResultPages(assetsDir) {
|
|
8269
9312
|
try {
|
|
8270
9313
|
const [success, denied, failure] = await Promise.all(
|
|
8271
|
-
RESULT_KINDS.map(async (kind) => resultPage(await
|
|
9314
|
+
RESULT_KINDS.map(async (kind) => resultPage(await readFile22(join24(assetsDir, resultPagePath(kind)), "utf8")))
|
|
8272
9315
|
);
|
|
8273
9316
|
return { success, denied, failure };
|
|
8274
9317
|
} catch {
|
|
@@ -8480,7 +9523,7 @@ function openInDefaultBrowser(url) {
|
|
|
8480
9523
|
import { execFileSync } from "child_process";
|
|
8481
9524
|
import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
8482
9525
|
import { userInfo } from "os";
|
|
8483
|
-
import { dirname as dirname12, join as
|
|
9526
|
+
import { dirname as dirname12, join as join25 } from "path";
|
|
8484
9527
|
|
|
8485
9528
|
// src/modules/keychain-backend.ts
|
|
8486
9529
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -8614,7 +9657,7 @@ function restrictToCurrentUser(filePath) {
|
|
|
8614
9657
|
});
|
|
8615
9658
|
}
|
|
8616
9659
|
function insecureSessionFilePath(homeDir) {
|
|
8617
|
-
return
|
|
9660
|
+
return join25(userTierRoot(homeDir), INSECURE_SESSION_FILE_NAME);
|
|
8618
9661
|
}
|
|
8619
9662
|
function createSessionStorage(options = {}) {
|
|
8620
9663
|
const keychain = new ChunkedSecretStore(options.keychainBackend ?? createKeychainBackend());
|
|
@@ -8882,15 +9925,15 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
|
|
|
8882
9925
|
|
|
8883
9926
|
// src/commands/report.ts
|
|
8884
9927
|
import { access as access5 } from "fs/promises";
|
|
8885
|
-
import { join as
|
|
9928
|
+
import { join as join28 } from "path";
|
|
8886
9929
|
|
|
8887
9930
|
// src/modules/show-report/generate.ts
|
|
8888
|
-
import { mkdir as mkdir8, readdir as
|
|
8889
|
-
import { basename as basename4, dirname as dirname13, isAbsolute, join as
|
|
9931
|
+
import { mkdir as mkdir8, readdir as readdir13, readFile as readFile24, writeFile as writeFile11 } from "fs/promises";
|
|
9932
|
+
import { basename as basename4, dirname as dirname13, isAbsolute, join as join27, relative as relative3, resolve as resolve9 } from "path";
|
|
8890
9933
|
|
|
8891
9934
|
// src/modules/show-report/model.ts
|
|
8892
|
-
import { readFile as
|
|
8893
|
-
import { join as
|
|
9935
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
9936
|
+
import { join as join26 } from "path";
|
|
8894
9937
|
|
|
8895
9938
|
// src/modules/show-report/plan-parser.ts
|
|
8896
9939
|
function parseFrontmatter(content) {
|
|
@@ -9076,28 +10119,6 @@ function stripMarkdown2(text) {
|
|
|
9076
10119
|
return text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
9077
10120
|
}
|
|
9078
10121
|
|
|
9079
|
-
// src/modules/show-report/decisions-parser.ts
|
|
9080
|
-
function parseAdrs(decisionsContent) {
|
|
9081
|
-
const headingPattern = /^## ARC-(\d{3})\b/gm;
|
|
9082
|
-
const positions = [];
|
|
9083
|
-
let match;
|
|
9084
|
-
while ((match = headingPattern.exec(decisionsContent)) !== null) {
|
|
9085
|
-
positions.push({ id: `ARC-${match[1]}`, index: match.index });
|
|
9086
|
-
}
|
|
9087
|
-
return positions.map(({ id, index }, i) => {
|
|
9088
|
-
const end = i + 1 < positions.length ? positions[i + 1].index : decisionsContent.length;
|
|
9089
|
-
const section = decisionsContent.slice(index, end);
|
|
9090
|
-
const dateMatch = /^\*\*Date:\*\*\s*(\d{4}-\d{2}-\d{2})/m.exec(section);
|
|
9091
|
-
const statusMatch = /^\*\*Status:\*\*\s*(\S+)/m.exec(section);
|
|
9092
|
-
return { id, date: dateMatch ? dateMatch[1] : null, status: statusMatch ? statusMatch[1] : null };
|
|
9093
|
-
});
|
|
9094
|
-
}
|
|
9095
|
-
function countAcceptedAdrsInWindow(adrs, fromDate, toDate) {
|
|
9096
|
-
return adrs.filter(
|
|
9097
|
-
(adr) => adr.status === "Accepted" && adr.date !== null && adr.date >= fromDate && adr.date <= toDate
|
|
9098
|
-
).length;
|
|
9099
|
-
}
|
|
9100
|
-
|
|
9101
10122
|
// src/modules/show-report/sources.ts
|
|
9102
10123
|
async function runGitTextOrNull(cwd, args) {
|
|
9103
10124
|
try {
|
|
@@ -9161,9 +10182,9 @@ async function getCast(cwd, fromRef, toRef, generatedOutputs = []) {
|
|
|
9161
10182
|
}
|
|
9162
10183
|
|
|
9163
10184
|
// src/modules/show-report/model.ts
|
|
9164
|
-
async function readOptional(
|
|
10185
|
+
async function readOptional(path5) {
|
|
9165
10186
|
try {
|
|
9166
|
-
return await
|
|
10187
|
+
return await readFile23(path5, "utf8");
|
|
9167
10188
|
} catch {
|
|
9168
10189
|
return null;
|
|
9169
10190
|
}
|
|
@@ -9259,13 +10280,13 @@ function buildCorrections(ledgerRows, programName) {
|
|
|
9259
10280
|
};
|
|
9260
10281
|
}
|
|
9261
10282
|
async function buildShowReportModel(options) {
|
|
9262
|
-
const planAbsPath =
|
|
9263
|
-
const queueAbsPath =
|
|
10283
|
+
const planAbsPath = join26(options.rootDir, options.planRelPath);
|
|
10284
|
+
const queueAbsPath = join26(options.rootDir, options.queueRelPath);
|
|
9264
10285
|
const ledgerRelPath = "docs/verification-ledger.md";
|
|
9265
|
-
const ledgerAbsPath =
|
|
9266
|
-
const decisionsAbsPath =
|
|
10286
|
+
const ledgerAbsPath = join26(options.rootDir, ledgerRelPath);
|
|
10287
|
+
const decisionsAbsPath = join26(options.rootDir, "DECISIONS.md");
|
|
9267
10288
|
const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
|
|
9268
|
-
|
|
10289
|
+
readFile23(planAbsPath, "utf8"),
|
|
9269
10290
|
readOptional(queueAbsPath),
|
|
9270
10291
|
readOptional(ledgerAbsPath),
|
|
9271
10292
|
readOptional(decisionsAbsPath)
|
|
@@ -9455,13 +10476,13 @@ function programNameFromTitle(title) {
|
|
|
9455
10476
|
function deterministicCompiledAt(frontmatter) {
|
|
9456
10477
|
return frontmatter.completed ?? frontmatter.activated ?? frontmatter.created ?? "unknown";
|
|
9457
10478
|
}
|
|
9458
|
-
function toPosix(
|
|
9459
|
-
return
|
|
10479
|
+
function toPosix(path5) {
|
|
10480
|
+
return path5.replace(/\\/g, "/");
|
|
9460
10481
|
}
|
|
9461
10482
|
async function programFromPlan(rootDir, slug, planRelPath) {
|
|
9462
10483
|
let planContent;
|
|
9463
10484
|
try {
|
|
9464
|
-
planContent = await
|
|
10485
|
+
planContent = await readFile24(join27(rootDir, planRelPath), "utf8");
|
|
9465
10486
|
} catch {
|
|
9466
10487
|
return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
|
|
9467
10488
|
}
|
|
@@ -9473,16 +10494,16 @@ async function programFromPlan(rootDir, slug, planRelPath) {
|
|
|
9473
10494
|
program: {
|
|
9474
10495
|
slug,
|
|
9475
10496
|
planRelPath,
|
|
9476
|
-
queueRelPath: toPosix(
|
|
10497
|
+
queueRelPath: toPosix(join27(dirname13(planRelPath), "OPERATOR-QUEUE.md")),
|
|
9477
10498
|
ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
|
|
9478
10499
|
}
|
|
9479
10500
|
};
|
|
9480
10501
|
}
|
|
9481
10502
|
async function discoverPrograms(rootDir) {
|
|
9482
|
-
const plansDir =
|
|
10503
|
+
const plansDir = join27(rootDir, "docs", "plans");
|
|
9483
10504
|
let entries;
|
|
9484
10505
|
try {
|
|
9485
|
-
entries = (await
|
|
10506
|
+
entries = (await readdir13(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
9486
10507
|
} catch (error) {
|
|
9487
10508
|
if (error.code === "ENOENT") return { programs: [], skipped: [] };
|
|
9488
10509
|
throw error;
|
|
@@ -9504,16 +10525,16 @@ async function programFromPlanPath(rootDir, planPath) {
|
|
|
9504
10525
|
function normalizeLineEndings2(content) {
|
|
9505
10526
|
return content.replace(/\r\n?/g, "\n");
|
|
9506
10527
|
}
|
|
9507
|
-
async function readOptionalFile(
|
|
10528
|
+
async function readOptionalFile(path5) {
|
|
9508
10529
|
try {
|
|
9509
|
-
return await
|
|
10530
|
+
return await readFile24(path5, "utf8");
|
|
9510
10531
|
} catch (error) {
|
|
9511
10532
|
if (error.code === "ENOENT") return null;
|
|
9512
10533
|
throw error;
|
|
9513
10534
|
}
|
|
9514
10535
|
}
|
|
9515
10536
|
async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
9516
|
-
const planContent = await
|
|
10537
|
+
const planContent = await readFile24(join27(rootDir, program2.planRelPath), "utf8");
|
|
9517
10538
|
const frontmatter = parseFrontmatter(planContent);
|
|
9518
10539
|
const model = await buildShowReportModel({
|
|
9519
10540
|
rootDir,
|
|
@@ -9533,7 +10554,7 @@ async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
|
9533
10554
|
};
|
|
9534
10555
|
}
|
|
9535
10556
|
async function runReportGeneration(options) {
|
|
9536
|
-
const templateHtml = await
|
|
10557
|
+
const templateHtml = await readFile24(options.templatePath, "utf8");
|
|
9537
10558
|
const theme = options.theme ?? "auto";
|
|
9538
10559
|
const drifted = [];
|
|
9539
10560
|
const repaired = [];
|
|
@@ -9551,12 +10572,12 @@ async function runReportGeneration(options) {
|
|
|
9551
10572
|
[`${outRel}/show-report.html`, generated.html]
|
|
9552
10573
|
];
|
|
9553
10574
|
for (const [relPath, expected] of outputs) {
|
|
9554
|
-
const actual = await readOptionalFile(
|
|
10575
|
+
const actual = await readOptionalFile(join27(options.rootDir, relPath));
|
|
9555
10576
|
if (actual === null || normalizeLineEndings2(actual) !== normalizeLineEndings2(expected)) {
|
|
9556
10577
|
drifted.push(`${relPath} does not match a regeneration from its sources.`);
|
|
9557
10578
|
if (options.mode === "write") {
|
|
9558
10579
|
await mkdir8(outAbs, { recursive: true });
|
|
9559
|
-
await writeFile11(
|
|
10580
|
+
await writeFile11(join27(options.rootDir, relPath), expected, "utf8");
|
|
9560
10581
|
repaired.push(relPath);
|
|
9561
10582
|
}
|
|
9562
10583
|
}
|
|
@@ -9567,7 +10588,7 @@ async function runReportGeneration(options) {
|
|
|
9567
10588
|
|
|
9568
10589
|
// src/commands/report.ts
|
|
9569
10590
|
async function runReport(targetDir, options = {}, assetsDir) {
|
|
9570
|
-
const templatePath =
|
|
10591
|
+
const templatePath = join28(assetsDir, "report", "show-report.template.html");
|
|
9571
10592
|
try {
|
|
9572
10593
|
await access5(templatePath);
|
|
9573
10594
|
} catch {
|
|
@@ -9637,14 +10658,14 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
9637
10658
|
return;
|
|
9638
10659
|
}
|
|
9639
10660
|
console.log(`Show report: wrote ${result.repaired.length} file(s).`);
|
|
9640
|
-
for (const
|
|
10661
|
+
for (const path5 of result.repaired) console.log(` ${path5}`);
|
|
9641
10662
|
}
|
|
9642
10663
|
|
|
9643
10664
|
// src/index.ts
|
|
9644
10665
|
import { realpathSync as realpathSync2 } from "fs";
|
|
9645
10666
|
var require3 = createRequire3(import.meta.url);
|
|
9646
10667
|
var pkg = require3("../package.json");
|
|
9647
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ??
|
|
10668
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join29(dirname14(fileURLToPath(import.meta.url)), "assets");
|
|
9648
10669
|
var program = new Command();
|
|
9649
10670
|
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)";
|
|
9650
10671
|
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)";
|
|
@@ -9758,6 +10779,15 @@ withManifestFlags(
|
|
|
9758
10779
|
program.command("status").description("Show installed components and check for updates").option("--user", USER_FLAG_DESCRIPTION).action(async (opts) => {
|
|
9759
10780
|
await runStatus(targetDirFor(opts), pkg.version, { user: opts.user });
|
|
9760
10781
|
});
|
|
10782
|
+
program.command("state").description("Export this repository's Arcane state as one JSON document (schema https://arcaneai.net/schemas/state/v1)").option("--json", "Print the document to stdout (required; `spell status` is the human view)").option(
|
|
10783
|
+
"--online",
|
|
10784
|
+
"Also look up the latest published arcane-cli version and this branch's open pull request (gh / az). Off by default: a plain run makes no network request"
|
|
10785
|
+
).option(
|
|
10786
|
+
"--include-local-paths",
|
|
10787
|
+
"Hub only: add each registered repository's clone path on this machine. Other machines' paths are never exported"
|
|
10788
|
+
).action(async (opts) => {
|
|
10789
|
+
await runState(process.cwd(), pkg.version, opts);
|
|
10790
|
+
});
|
|
9761
10791
|
program.command("uninstall").description("Remove all installed Arcane framework files").option("--yes", "Skip confirmation prompt").option("--dry-run", "Preview what would be removed without deleting files").option("--user", USER_FLAG_DESCRIPTION).action(async (opts) => {
|
|
9762
10792
|
await runUninstall({ yes: opts.yes, dryRun: opts.dryRun, user: opts.user }, targetDirFor(opts));
|
|
9763
10793
|
});
|