arcane-cli 1.8.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/.mcp.json +2 -2
- package/dist/index.js +119 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/assets/.mcp.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Example MCP server config. Replace or remove the example server below with your own. Set a per-server \"timeout\" (milliseconds) so a hung server aborts on a predictable schedule instead of running to the client's own default idle limit (as long as 30 minutes) -- see .arcane/governance/git-conventions.md's MCP fail-fast / fallback rule.",
|
|
2
|
+
"$comment": "Example MCP server config. Replace or remove the example server below with your own: the package argument is a placeholder that npm rejects, so the example does not run until you name a real server package. Set a per-server \"timeout\" (milliseconds) so a hung server aborts on a predictable schedule instead of running to the client's own default idle limit (as long as 30 minutes) -- see .arcane/governance/git-conventions.md's MCP fail-fast / fallback rule.",
|
|
3
3
|
"mcpServers": {
|
|
4
4
|
"example-server": {
|
|
5
5
|
"command": "npx",
|
|
6
|
-
"args": ["-y", "
|
|
6
|
+
"args": ["-y", "<your-mcp-server-package>"],
|
|
7
7
|
"timeout": 30000
|
|
8
8
|
}
|
|
9
9
|
}
|
package/dist/index.js
CHANGED
|
@@ -5896,8 +5896,10 @@ async function scanRepository(repoRoot, rules, extensions, excludePrefixes = [])
|
|
|
5896
5896
|
|
|
5897
5897
|
// src/modules/exec.ts
|
|
5898
5898
|
import { execFile as execFile2 } from "child_process";
|
|
5899
|
+
import { existsSync, readFileSync } from "fs";
|
|
5900
|
+
import { win32 } from "path";
|
|
5899
5901
|
var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
|
|
5900
|
-
function execFileWithTimeout(file, args, timeoutMs) {
|
|
5902
|
+
function execFileWithTimeout(file, args, timeoutMs, options = {}) {
|
|
5901
5903
|
return new Promise((resolve5, reject) => {
|
|
5902
5904
|
const child = execFile2(
|
|
5903
5905
|
file,
|
|
@@ -5906,7 +5908,8 @@ function execFileWithTimeout(file, args, timeoutMs) {
|
|
|
5906
5908
|
timeout: timeoutMs,
|
|
5907
5909
|
killSignal: "SIGTERM",
|
|
5908
5910
|
windowsHide: true,
|
|
5909
|
-
encoding: "utf8"
|
|
5911
|
+
encoding: "utf8",
|
|
5912
|
+
...options.env ? { env: { ...process.env, ...options.env } } : {}
|
|
5910
5913
|
},
|
|
5911
5914
|
(error, stdout, stderr) => {
|
|
5912
5915
|
if (error) {
|
|
@@ -5919,6 +5922,36 @@ function execFileWithTimeout(file, args, timeoutMs) {
|
|
|
5919
5922
|
child.stdin?.end();
|
|
5920
5923
|
});
|
|
5921
5924
|
}
|
|
5925
|
+
function defaultProbes() {
|
|
5926
|
+
return {
|
|
5927
|
+
platform: process.platform,
|
|
5928
|
+
pathEnv: process.env["PATH"] ?? process.env["Path"],
|
|
5929
|
+
readText: (path4) => {
|
|
5930
|
+
try {
|
|
5931
|
+
return readFileSync(path4, "utf8");
|
|
5932
|
+
} catch {
|
|
5933
|
+
return void 0;
|
|
5934
|
+
}
|
|
5935
|
+
},
|
|
5936
|
+
exists: (path4) => existsSync(path4)
|
|
5937
|
+
};
|
|
5938
|
+
}
|
|
5939
|
+
var MSI_LAUNCHER_LINE = /^\s*"%~dp0\\\.\.\\python\.exe"\s+-IBm\s+azure\.cli\s+%\*\s*$/im;
|
|
5940
|
+
function resolveAzureCli(probes = defaultProbes()) {
|
|
5941
|
+
const plain = { file: "az", prefixArgs: [] };
|
|
5942
|
+
if (probes.platform !== "win32") return plain;
|
|
5943
|
+
for (const raw of (probes.pathEnv ?? "").split(";")) {
|
|
5944
|
+
const dir = raw.trim().replace(/^"(.*)"$/, "$1");
|
|
5945
|
+
if (!dir || !win32.isAbsolute(dir)) continue;
|
|
5946
|
+
const launcher = probes.readText(win32.join(dir, "az.cmd"));
|
|
5947
|
+
if (launcher === void 0) continue;
|
|
5948
|
+
if (!MSI_LAUNCHER_LINE.test(launcher)) return plain;
|
|
5949
|
+
const interpreter = win32.resolve(dir, "..", "python.exe");
|
|
5950
|
+
if (!probes.exists(interpreter)) return plain;
|
|
5951
|
+
return { file: interpreter, prefixArgs: ["-IBm", "azure.cli"], env: { AZ_INSTALLER: "MSI" } };
|
|
5952
|
+
}
|
|
5953
|
+
return plain;
|
|
5954
|
+
}
|
|
5922
5955
|
|
|
5923
5956
|
// src/modules/platform-policy.ts
|
|
5924
5957
|
var SANCTIONED_MERGE_METHODS = ["merge", "rebase"];
|
|
@@ -6028,10 +6061,13 @@ function evaluateAdoMergePolicy(policies) {
|
|
|
6028
6061
|
}
|
|
6029
6062
|
return compareToLadder([...effective], []);
|
|
6030
6063
|
}
|
|
6031
|
-
async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
|
|
6064
|
+
async function fetchAdoMergeTypePolicies(org, project, repoName, branch, launch = resolveAzureCli()) {
|
|
6065
|
+
const az = (args) => execFileWithTimeout(launch.file, [...launch.prefixArgs, ...args], EXTERNAL_CLI_TIMEOUT_MS, {
|
|
6066
|
+
...launch.env ? { env: launch.env } : {}
|
|
6067
|
+
});
|
|
6032
6068
|
try {
|
|
6033
6069
|
const orgUrl = `https://dev.azure.com/${org}`;
|
|
6034
|
-
const { stdout: repoOut } = await
|
|
6070
|
+
const { stdout: repoOut } = await az([
|
|
6035
6071
|
"repos",
|
|
6036
6072
|
"show",
|
|
6037
6073
|
"--repository",
|
|
@@ -6044,10 +6080,10 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
|
|
|
6044
6080
|
"id",
|
|
6045
6081
|
"--output",
|
|
6046
6082
|
"tsv"
|
|
6047
|
-
]
|
|
6083
|
+
]);
|
|
6048
6084
|
const repositoryId = repoOut.trim();
|
|
6049
6085
|
if (!repositoryId) return null;
|
|
6050
|
-
const { stdout } = await
|
|
6086
|
+
const { stdout } = await az([
|
|
6051
6087
|
"repos",
|
|
6052
6088
|
"policy",
|
|
6053
6089
|
"list",
|
|
@@ -6061,7 +6097,7 @@ async function fetchAdoMergeTypePolicies(org, project, repoName, branch) {
|
|
|
6061
6097
|
project,
|
|
6062
6098
|
"--output",
|
|
6063
6099
|
"json"
|
|
6064
|
-
]
|
|
6100
|
+
]);
|
|
6065
6101
|
return JSON.parse(stdout);
|
|
6066
6102
|
} catch {
|
|
6067
6103
|
return null;
|
|
@@ -6552,6 +6588,7 @@ async function checkGovernancePlaceholders(targetDir) {
|
|
|
6552
6588
|
message: `unknown placeholder(s) in status: active doc(s) \u2014 ${listed.join("; ")}. Fill each one in; or, if an agent resolves it at use time, add it to the runtime-placeholders list in .arcane/governance/${PLACEHOLDER_STANDARD_FILE}; or, if the document is meant to be filled in by hand, mark it \`status: template\`.`
|
|
6553
6589
|
};
|
|
6554
6590
|
}
|
|
6591
|
+
var UNOWNED_EXAMPLE_SCOPE = "@example/";
|
|
6555
6592
|
async function checkComponentRequires(targetDir) {
|
|
6556
6593
|
const name = "Component prerequisites";
|
|
6557
6594
|
let manifest;
|
|
@@ -6611,6 +6648,22 @@ async function checkMcpConfig(targetDir) {
|
|
|
6611
6648
|
if (serverNames.length === 0) {
|
|
6612
6649
|
return { name, passed: true, blocking: false, message: "no MCP servers configured" };
|
|
6613
6650
|
}
|
|
6651
|
+
const exampleHits = [];
|
|
6652
|
+
for (const n of serverNames) {
|
|
6653
|
+
const args = servers[n]?.args;
|
|
6654
|
+
if (!Array.isArray(args)) continue;
|
|
6655
|
+
for (const a of args) {
|
|
6656
|
+
if (typeof a === "string" && a.startsWith(UNOWNED_EXAMPLE_SCOPE)) exampleHits.push(`${n} (${a})`);
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
if (exampleHits.length > 0) {
|
|
6660
|
+
return {
|
|
6661
|
+
name,
|
|
6662
|
+
passed: false,
|
|
6663
|
+
blocking: false,
|
|
6664
|
+
message: `.mcp.json runs a package under the ${UNOWNED_EXAMPLE_SCOPE} scope nobody here owns: ${exampleHits.join(", ")}. The old \`spell init\` scaffold shipped it as a placeholder, and \`npx -y\` would run whoever publishes it. Replace the entry with your own MCP server package, or remove it; the file is yours, so doctor does not edit it`
|
|
6665
|
+
};
|
|
6666
|
+
}
|
|
6614
6667
|
const missingTimeout = serverNames.filter((n) => typeof servers[n]?.timeout !== "number");
|
|
6615
6668
|
if (missingTimeout.length > 0) {
|
|
6616
6669
|
return {
|
|
@@ -7441,13 +7494,16 @@ async function getVersionAtRef(cwd, ref) {
|
|
|
7441
7494
|
return null;
|
|
7442
7495
|
}
|
|
7443
7496
|
}
|
|
7497
|
+
function excludeOutputs(generatedOutputs) {
|
|
7498
|
+
return generatedOutputs.length === 0 ? [] : ["--", ".", ...generatedOutputs.map((p) => `:(exclude)${p}`)];
|
|
7499
|
+
}
|
|
7444
7500
|
async function getCloseCommit(cwd, completedDate, generatedOutputs = []) {
|
|
7445
7501
|
if (!completedDate) {
|
|
7446
|
-
const args = generatedOutputs.length === 0 ? ["rev-parse", "HEAD"] : ["log", "-1", "--format=%H", "HEAD",
|
|
7502
|
+
const args = generatedOutputs.length === 0 ? ["rev-parse", "HEAD"] : ["log", "-1", "--format=%H", "HEAD", ...excludeOutputs(generatedOutputs)];
|
|
7447
7503
|
const head = await runGitTextOrNull(cwd, args);
|
|
7448
7504
|
return head === null || head === "" ? null : head;
|
|
7449
7505
|
}
|
|
7450
|
-
const log = await runGitTextOrNull(cwd, ["log", "--format=%H%x09%cs", "HEAD"]);
|
|
7506
|
+
const log = await runGitTextOrNull(cwd, ["log", "--format=%H%x09%cs", "HEAD", ...excludeOutputs(generatedOutputs)]);
|
|
7451
7507
|
if (log === null || log === "") return null;
|
|
7452
7508
|
for (const line of log.split("\n")) {
|
|
7453
7509
|
const [sha, landed] = line.split(" ");
|
|
@@ -7735,6 +7791,33 @@ function renderShowReport(model, templateHtml) {
|
|
|
7735
7791
|
});
|
|
7736
7792
|
}
|
|
7737
7793
|
|
|
7794
|
+
// src/modules/show-report/theme.ts
|
|
7795
|
+
var REPORT_THEMES = ["auto", "light", "dark"];
|
|
7796
|
+
function radioTagPattern(id) {
|
|
7797
|
+
return new RegExp(`<input\\b[^>]*\\bid="arcane-report-theme-${id}"[^>]*?/?>`);
|
|
7798
|
+
}
|
|
7799
|
+
function withoutChecked(tag) {
|
|
7800
|
+
return tag.replace(/\s+checked(?:="[^"]*")?/, "");
|
|
7801
|
+
}
|
|
7802
|
+
function withChecked(tag) {
|
|
7803
|
+
return /\bchecked\b/.test(tag) ? tag : tag.replace(/(\/?>)$/, " checked$1");
|
|
7804
|
+
}
|
|
7805
|
+
function applyReportTheme(html, theme) {
|
|
7806
|
+
if (theme === "auto") return { html, applied: true };
|
|
7807
|
+
if (!radioTagPattern(theme).test(html)) return { html, applied: false };
|
|
7808
|
+
let result = html;
|
|
7809
|
+
for (const id of REPORT_THEMES) {
|
|
7810
|
+
const match = radioTagPattern(id).exec(result);
|
|
7811
|
+
if (!match) continue;
|
|
7812
|
+
const original = match[0];
|
|
7813
|
+
const replaced = id === theme ? withChecked(original) : withoutChecked(original);
|
|
7814
|
+
if (replaced !== original) {
|
|
7815
|
+
result = result.slice(0, match.index) + replaced + result.slice(match.index + original.length);
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
return { html: result, applied: true };
|
|
7819
|
+
}
|
|
7820
|
+
|
|
7738
7821
|
// src/modules/show-report/generate.ts
|
|
7739
7822
|
var TEMPLATE_VERSION = "v0-interim";
|
|
7740
7823
|
var NO_EPICS_REASON = "no parseable `- [x] **ID \u2014 Title.**` epic bullets";
|
|
@@ -7801,7 +7884,7 @@ async function readOptionalFile(path4) {
|
|
|
7801
7884
|
throw error;
|
|
7802
7885
|
}
|
|
7803
7886
|
}
|
|
7804
|
-
async function generateReport(rootDir, program2, templateHtml) {
|
|
7887
|
+
async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
7805
7888
|
const planContent = await readFile15(join18(rootDir, program2.planRelPath), "utf8");
|
|
7806
7889
|
const frontmatter = parseFrontmatter(planContent);
|
|
7807
7890
|
const model = await buildShowReportModel({
|
|
@@ -7813,19 +7896,24 @@ async function generateReport(rootDir, program2, templateHtml) {
|
|
|
7813
7896
|
compiledAt: deterministicCompiledAt(frontmatter),
|
|
7814
7897
|
templateVersion: TEMPLATE_VERSION
|
|
7815
7898
|
});
|
|
7899
|
+
const themed = applyReportTheme(renderShowReport(model, templateHtml), theme);
|
|
7816
7900
|
return {
|
|
7817
7901
|
json: `${JSON.stringify(model, null, 2)}
|
|
7818
7902
|
`,
|
|
7819
|
-
html:
|
|
7903
|
+
html: themed.html,
|
|
7904
|
+
themeApplied: themed.applied
|
|
7820
7905
|
};
|
|
7821
7906
|
}
|
|
7822
7907
|
async function runReportGeneration(options) {
|
|
7823
7908
|
const templateHtml = await readFile15(options.templatePath, "utf8");
|
|
7909
|
+
const theme = options.theme ?? "auto";
|
|
7824
7910
|
const drifted = [];
|
|
7825
7911
|
const repaired = [];
|
|
7826
7912
|
const unwritten = [];
|
|
7913
|
+
const themeSkipped = [];
|
|
7827
7914
|
for (const program2 of options.programs) {
|
|
7828
|
-
const generated = await generateReport(options.rootDir, program2, templateHtml);
|
|
7915
|
+
const generated = await generateReport(options.rootDir, program2, templateHtml, theme);
|
|
7916
|
+
if (!generated.themeApplied) themeSkipped.push(program2.slug);
|
|
7829
7917
|
const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
|
|
7830
7918
|
if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
|
|
7831
7919
|
const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir, dirname10(program2.planRelPath));
|
|
@@ -7846,7 +7934,7 @@ async function runReportGeneration(options) {
|
|
|
7846
7934
|
}
|
|
7847
7935
|
}
|
|
7848
7936
|
}
|
|
7849
|
-
return { drifted, repaired, unwritten };
|
|
7937
|
+
return { drifted, repaired, unwritten, themeSkipped };
|
|
7850
7938
|
}
|
|
7851
7939
|
|
|
7852
7940
|
// src/commands/report.ts
|
|
@@ -7866,6 +7954,14 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
7866
7954
|
"--refresh: external-data snapshotting (gh PR titles/merge dates) is not implemented yet; regenerating from local sources."
|
|
7867
7955
|
);
|
|
7868
7956
|
}
|
|
7957
|
+
const theme = options.theme ?? "auto";
|
|
7958
|
+
if (!REPORT_THEMES.includes(theme)) {
|
|
7959
|
+
console.error(
|
|
7960
|
+
`\u2716 arcane: --theme must be one of ${REPORT_THEMES.join(" | ")}, got "${options.theme}".`
|
|
7961
|
+
);
|
|
7962
|
+
process.exitCode = 1;
|
|
7963
|
+
return;
|
|
7964
|
+
}
|
|
7869
7965
|
if (await isShallowRepository(targetDir)) {
|
|
7870
7966
|
console.warn(
|
|
7871
7967
|
"\u26A0 arcane: shallow clone -- the report's version span and cast are derived from git history and will be omitted. Run `git fetch --unshallow` for a complete report."
|
|
@@ -7895,13 +7991,19 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
7895
7991
|
templatePath,
|
|
7896
7992
|
mode: "write",
|
|
7897
7993
|
programs,
|
|
7898
|
-
outDir: options.out
|
|
7994
|
+
outDir: options.out,
|
|
7995
|
+
theme
|
|
7899
7996
|
});
|
|
7900
7997
|
for (const u of result.unwritten) {
|
|
7901
7998
|
console.warn(
|
|
7902
7999
|
`\u26A0 arcane: ${u.slug} has ${u.ids.length} epic(s) with no \`**Report:**\` line in PLAN.md -- they render as "unwritten": ${u.ids.join(", ")}.`
|
|
7903
8000
|
);
|
|
7904
8001
|
}
|
|
8002
|
+
for (const slug of result.themeSkipped) {
|
|
8003
|
+
console.warn(
|
|
8004
|
+
`\u26A0 arcane: ${slug} -- --theme ${theme} requested, but this template has no theme toggle to set (older vendored template?); left unchanged.`
|
|
8005
|
+
);
|
|
8006
|
+
}
|
|
7905
8007
|
if (result.repaired.length === 0) {
|
|
7906
8008
|
console.log(`Show report: ${programs.length} program(s) already up to date.`);
|
|
7907
8009
|
return;
|
|
@@ -8054,6 +8156,9 @@ program.command("report").description(
|
|
|
8054
8156
|
).option("--plan <path>", "Report on one PLAN.md instead of auto-discovering docs/plans/*/PLAN.md").option("--out <dir>", "Write show-report.{json,html} into this directory instead of next to the plan").option(
|
|
8055
8157
|
"--refresh",
|
|
8056
8158
|
"Accepted for forward compatibility; external-data snapshotting is not implemented yet, so this regenerates from local sources"
|
|
8159
|
+
).option(
|
|
8160
|
+
"--theme <mode>",
|
|
8161
|
+
"Force the in-page toggle's initial state: auto | light | dark (default auto, byte-for-byte unchanged). Pair with --out for a one-off themed copy so it doesn't overwrite the tracked show-report.html"
|
|
8057
8162
|
).action(async (opts) => {
|
|
8058
8163
|
await runReport(process.cwd(), opts, ASSETS_DIR);
|
|
8059
8164
|
});
|