@archme/cli 0.10.0 → 0.11.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/dist/cli.js +232 -154
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -9838,6 +9838,7 @@ var install_exports = {};
|
|
|
9838
9838
|
__export(install_exports, {
|
|
9839
9839
|
NotSignedInError: () => NotSignedInError,
|
|
9840
9840
|
PUBLISHED_BIN_RELATIVE: () => PUBLISHED_BIN_RELATIVE,
|
|
9841
|
+
archmeSelfCommand: () => archmeSelfCommand,
|
|
9841
9842
|
ensureStartPrerequisites: () => ensureStartPrerequisites,
|
|
9842
9843
|
findPackageRoot: () => findPackageRoot,
|
|
9843
9844
|
pickCliEntry: () => pickCliEntry,
|
|
@@ -9881,6 +9882,9 @@ function resolveCliEntry() {
|
|
|
9881
9882
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
9882
9883
|
return pickCliEntry(findPackageRoot(here, existsSync2), existsSync2);
|
|
9883
9884
|
}
|
|
9885
|
+
function archmeSelfCommand() {
|
|
9886
|
+
return [process.execPath, resolveCliEntry()];
|
|
9887
|
+
}
|
|
9884
9888
|
async function ensureStartPrerequisites(opts = {}) {
|
|
9885
9889
|
const out = opts.out ?? process.stdout;
|
|
9886
9890
|
const platform3 = opts.platform ?? process.platform;
|
|
@@ -10399,6 +10403,11 @@ function buildClaudeArgs(opts) {
|
|
|
10399
10403
|
"--allowedTools",
|
|
10400
10404
|
tools
|
|
10401
10405
|
];
|
|
10406
|
+
if (opts.resumeSessionId) {
|
|
10407
|
+
args.push("--resume", opts.resumeSessionId);
|
|
10408
|
+
} else if (opts.sessionId) {
|
|
10409
|
+
args.push("--session-id", opts.sessionId);
|
|
10410
|
+
}
|
|
10402
10411
|
if (opts.builtinTools) {
|
|
10403
10412
|
args.push("--tools", opts.builtinTools.join(","));
|
|
10404
10413
|
}
|
|
@@ -10410,6 +10419,9 @@ function buildClaudeArgs(opts) {
|
|
|
10410
10419
|
}
|
|
10411
10420
|
return args.map(stripNulBytes);
|
|
10412
10421
|
}
|
|
10422
|
+
function claudeTimeoutMs(timeoutMs) {
|
|
10423
|
+
return timeoutMs === null ? void 0 : timeoutMs ?? DEFAULT_CLAUDE_TIMEOUT_MS;
|
|
10424
|
+
}
|
|
10413
10425
|
function spawnClaude(opts) {
|
|
10414
10426
|
requireBinary("claude");
|
|
10415
10427
|
const env = scrubPrivilegedEnv(process.env);
|
|
@@ -10433,7 +10445,7 @@ function spawnClaude(opts) {
|
|
|
10433
10445
|
args,
|
|
10434
10446
|
{
|
|
10435
10447
|
reject: false,
|
|
10436
|
-
timeout: opts.timeoutMs
|
|
10448
|
+
timeout: claudeTimeoutMs(opts.timeoutMs),
|
|
10437
10449
|
cwd: opts.cwd,
|
|
10438
10450
|
env,
|
|
10439
10451
|
// Claude reads stdin opportunistically and prints a warning after 3s
|
|
@@ -10530,7 +10542,12 @@ var init_spawn_claude = __esm({
|
|
|
10530
10542
|
// "Agent" is inherited from DEFAULT_ALLOWED_TOOLS (sub-agent dispatch).
|
|
10531
10543
|
"Write",
|
|
10532
10544
|
"Edit",
|
|
10533
|
-
"Bash"
|
|
10545
|
+
"Bash",
|
|
10546
|
+
// The scan prompts research the product's public site and docs. A scan could
|
|
10547
|
+
// already fetch pages through Bash (curl), so this adds search, not reach.
|
|
10548
|
+
// Interactive sessions still ask the person before every fetch.
|
|
10549
|
+
"WebSearch",
|
|
10550
|
+
"WebFetch"
|
|
10534
10551
|
];
|
|
10535
10552
|
DEFAULT_CLAUDE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
10536
10553
|
}
|
|
@@ -13904,7 +13921,7 @@ function buildRepoContainerDiffGuidance(containerPath) {
|
|
|
13904
13921
|
return `## Multi-repo project
|
|
13905
13922
|
This project holds several repos, each a root-level \`kind: "repo"\` container; this repo's code lives under \`${containerPath}\`. Before modelling anything that crosses repos, read the project's modelling conventions with ${READ_CONVENTIONS}. Never create a root-level external for a system that has a repo container \u2014 connect to the components inside that container instead.`;
|
|
13906
13923
|
}
|
|
13907
|
-
var READ_CONVENTIONS, PLAN_CHANGES_ID, STITCH_PLAN_GUIDANCE;
|
|
13924
|
+
var READ_CONVENTIONS, PLAN_CHANGES_ID, STORY_PLAN_SHAPE, STORY_PLAN_GUIDANCE, PLAN_CHANGES_EXPLANATION, STITCH_PLAN_GUIDANCE, READ_FIRST;
|
|
13908
13925
|
var init_multiRepoScan = __esm({
|
|
13909
13926
|
"../shared/multiRepoScan.ts"() {
|
|
13910
13927
|
init_scanPrompts();
|
|
@@ -13913,7 +13930,11 @@ var init_multiRepoScan = __esm({
|
|
|
13913
13930
|
init_repoContainer();
|
|
13914
13931
|
READ_CONVENTIONS = `read_context({ path: "root", type: "decision" })`;
|
|
13915
13932
|
PLAN_CHANGES_ID = "multi-repo-plan-changes";
|
|
13916
|
-
|
|
13933
|
+
STORY_PLAN_SHAPE = `Write it in one \`update_architecture\` call at root: feature groups holding leaf stories, every leaf \`draft: true\` with a \`title\` and a \`description\` and no \`sequence\`. Each leaf's description names the repos it touches, ending \`Repos: owner/a, owner/b\`. A flow that crosses repos is ONE story naming all of them, never one per repo. If the board already has stories, they are the plan's starting point: keep, edit or remove them rather than starting over.`;
|
|
13934
|
+
STORY_PLAN_GUIDANCE = `**Draft the story plan** every repo's scan will build against. Read each clone only as far as it takes to say what the system does for the people and systems that use it (READMEs, entry points, API and resource definitions) \u2014 don't trace flows. ${STORY_PLAN_SHAPE}`;
|
|
13935
|
+
PLAN_CHANGES_EXPLANATION = `write ONE root-context decision \u2014 \`update_node_context({ path: "root", add: [{ id: "${PLAN_CHANGES_ID}", type: "decision", title: "What changed from the approved plan, and why", content: "\u2026" }] })\` \u2014 listing every story added, dropped, renamed or re-scoped against the plan, the repos' departures and your own, each with its reason in a plain sentence ("No changes from the approved plan." if there were none). Present it to the user and answer their questions about it before you finish.`;
|
|
13936
|
+
STITCH_PLAN_GUIDANCE = `**Finish the approved story plan, then explain what changed from it.** The user approved a draft story plan before the repos were scanned. Each repo's scan left the stories that cross repos to you, and recorded its own departures from the plan in root context (\`${PLAN_CHANGES_ID}-<container>\` decisions). Write each remaining draft story's sequence against the code on every side it touches, and set \`draft: false\`. Then ${PLAN_CHANGES_EXPLANATION}`;
|
|
13937
|
+
READ_FIRST = `**Read first.** \`read_architecture\` at root and ${READ_CONVENTIONS}. A rerun reuses what is already there under the ids below and rewrites it in place.`;
|
|
13917
13938
|
}
|
|
13918
13939
|
});
|
|
13919
13940
|
|
|
@@ -19886,6 +19907,8 @@ var init_identity = __esm({
|
|
|
19886
19907
|
// src/scan/authedClient.ts
|
|
19887
19908
|
var authedClient_exports = {};
|
|
19888
19909
|
__export(authedClient_exports, {
|
|
19910
|
+
ORCHESTRATED_SCAN_TIMEOUT_MS: () => ORCHESTRATED_SCAN_TIMEOUT_MS,
|
|
19911
|
+
ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS: () => ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS,
|
|
19889
19912
|
SCAN_CLAUDE_TIMEOUT_MS: () => SCAN_CLAUDE_TIMEOUT_MS,
|
|
19890
19913
|
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS: () => SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS,
|
|
19891
19914
|
createAuthedClient: () => createAuthedClient
|
|
@@ -19924,7 +19947,7 @@ async function createAuthedClient(opts) {
|
|
|
19924
19947
|
accountKind: loaded.kind ?? "oauth"
|
|
19925
19948
|
};
|
|
19926
19949
|
}
|
|
19927
|
-
var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS;
|
|
19950
|
+
var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, ORCHESTRATED_SCAN_TIMEOUT_MS, ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS;
|
|
19928
19951
|
var init_authedClient = __esm({
|
|
19929
19952
|
"src/scan/authedClient.ts"() {
|
|
19930
19953
|
"use strict";
|
|
@@ -19932,6 +19955,8 @@ var init_authedClient = __esm({
|
|
|
19932
19955
|
init_identity();
|
|
19933
19956
|
SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
19934
19957
|
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
19958
|
+
ORCHESTRATED_SCAN_TIMEOUT_MS = 6 * 60 * 60 * 1e3;
|
|
19959
|
+
ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS = ORCHESTRATED_SCAN_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
19935
19960
|
}
|
|
19936
19961
|
});
|
|
19937
19962
|
|
|
@@ -20507,24 +20532,32 @@ var init_claimDefaultProject = __esm({
|
|
|
20507
20532
|
});
|
|
20508
20533
|
|
|
20509
20534
|
// src/scan/run-scan.ts
|
|
20535
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
20510
20536
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
20511
|
-
import { mkdir as mkdir13, readFile as readFile14 } from "node:fs/promises";
|
|
20512
|
-
import { basename as basename2, join as join21 } from "node:path";
|
|
20537
|
+
import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile12 } from "node:fs/promises";
|
|
20538
|
+
import { basename as basename2, dirname as dirname15, join as join21 } from "node:path";
|
|
20513
20539
|
function modeInstruction(opts) {
|
|
20514
20540
|
const line = modeLine(opts);
|
|
20541
|
+
if (opts.mode === "orchestrator" && opts.plan) {
|
|
20542
|
+
throw new Error("the orchestrator always plans; start_scan refuses `plan` with it");
|
|
20543
|
+
}
|
|
20515
20544
|
return line && opts.plan ? `${line} Also pass \`plan: true\`: this bootstrap builds against a draft story plan.` : line;
|
|
20516
20545
|
}
|
|
20546
|
+
function reposArg(opts, mode) {
|
|
20547
|
+
const repos = opts.repos ?? [];
|
|
20548
|
+
if (repos.length === 0 || !repos.every((r) => REPO_SLUG_RE.test(r))) {
|
|
20549
|
+
throw new Error(`${mode} mode needs at least one repo, each \`owner/name\``);
|
|
20550
|
+
}
|
|
20551
|
+
return repos.map((r) => `"${r}"`).join(", ");
|
|
20552
|
+
}
|
|
20517
20553
|
function modeLine(opts) {
|
|
20518
20554
|
switch (opts.mode) {
|
|
20519
20555
|
case void 0:
|
|
20520
20556
|
return null;
|
|
20521
|
-
case "roster":
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
|
|
20525
|
-
}
|
|
20526
|
-
return `This is the multi-repo roster session \u2014 pass \`mode: "roster"\` and \`repos: [${repos.map((r) => `"${r}"`).join(", ")}]\` to \`start_scan\`.`;
|
|
20527
|
-
}
|
|
20557
|
+
case "roster":
|
|
20558
|
+
return `This is the multi-repo roster session \u2014 pass \`mode: "roster"\` and \`repos: [${reposArg(opts, "roster")}]\` to \`start_scan\`.`;
|
|
20559
|
+
case "orchestrator":
|
|
20560
|
+
return `This is the multi-repo orchestrator session \u2014 pass \`mode: "orchestrator"\` and \`repos: [${reposArg(opts, "orchestrator")}]\` to \`start_scan\`.`;
|
|
20528
20561
|
case "repo":
|
|
20529
20562
|
if (!opts.container || !CONTAINER_ID_RE.test(opts.container)) {
|
|
20530
20563
|
throw new Error(`invalid container id \`${opts.container ?? ""}\``);
|
|
@@ -20543,6 +20576,9 @@ ${briefing}` : BOOTSTRAP_INTERACTIVE_NO_BRIEFING : BOOTSTRAP_HEADLESS;
|
|
|
20543
20576
|
if (modeLine2) base += `
|
|
20544
20577
|
|
|
20545
20578
|
${modeLine2}`;
|
|
20579
|
+
if (opts.runSheet) base += `
|
|
20580
|
+
|
|
20581
|
+
${opts.runSheet}`;
|
|
20546
20582
|
if (!promptVariant) return base;
|
|
20547
20583
|
if (!PROMPT_VARIANT_RE.test(promptVariant)) {
|
|
20548
20584
|
throw new Error(
|
|
@@ -20554,6 +20590,26 @@ ${modeLine2}`;
|
|
|
20554
20590
|
Use scan-prompt variant \`${promptVariant}\` \u2014 pass \`variant: "${promptVariant}"\` to \`start_scan\`.`;
|
|
20555
20591
|
return base;
|
|
20556
20592
|
}
|
|
20593
|
+
async function writeScanSession(repoPath, session) {
|
|
20594
|
+
const path7 = join21(repoPath, SCAN_SESSION_FILE);
|
|
20595
|
+
await mkdir13(dirname15(path7), { recursive: true });
|
|
20596
|
+
await writeFile12(path7, JSON.stringify(session, null, 2) + "\n");
|
|
20597
|
+
}
|
|
20598
|
+
async function readScanSession(repoPath) {
|
|
20599
|
+
const path7 = join21(repoPath, SCAN_SESSION_FILE);
|
|
20600
|
+
const raw = await readFile14(path7, "utf8").catch(() => null);
|
|
20601
|
+
if (raw === null) throw new Error(`no scan session to resume at ${path7} \u2014 launch it instead`);
|
|
20602
|
+
let parsed;
|
|
20603
|
+
try {
|
|
20604
|
+
parsed = JSON.parse(raw);
|
|
20605
|
+
} catch {
|
|
20606
|
+
throw new Error(`${path7} is not valid JSON; its session id cannot be read`);
|
|
20607
|
+
}
|
|
20608
|
+
if (typeof parsed.sessionId !== "string" || !SESSION_ID_RE.test(parsed.sessionId)) {
|
|
20609
|
+
throw new Error(`${path7} holds no valid session id`);
|
|
20610
|
+
}
|
|
20611
|
+
return { sessionId: parsed.sessionId, pid: typeof parsed.pid === "number" ? parsed.pid : 0 };
|
|
20612
|
+
}
|
|
20557
20613
|
function todayDate() {
|
|
20558
20614
|
const d = /* @__PURE__ */ new Date();
|
|
20559
20615
|
const yyyy = d.getUTCFullYear();
|
|
@@ -20837,7 +20893,7 @@ async function runScan(opts, deps = {}) {
|
|
|
20837
20893
|
});
|
|
20838
20894
|
log(` signed in as ${authed2.identity}`);
|
|
20839
20895
|
const { supabase, userId } = authed2;
|
|
20840
|
-
const workspace = opts.mode === "roster" || opts.mode === "stitch";
|
|
20896
|
+
const workspace = opts.mode === "roster" || opts.mode === "stitch" || opts.mode === "orchestrator";
|
|
20841
20897
|
let headSha = null;
|
|
20842
20898
|
if (!workspace) {
|
|
20843
20899
|
try {
|
|
@@ -20898,25 +20954,31 @@ async function runScan(opts, deps = {}) {
|
|
|
20898
20954
|
try {
|
|
20899
20955
|
const model = await resolveRunnableModel(supabase, { warn: log });
|
|
20900
20956
|
const interactive = opts.interactive !== false;
|
|
20957
|
+
if (interactive && opts.resume) throw new Error("only a headless scan can be resumed");
|
|
20958
|
+
const session = interactive ? null : { sessionId: opts.resume ? (await readScanSession(opts.repoPath)).sessionId : randomUUID2(), pid: process.pid };
|
|
20959
|
+
if (session) await writeScanSession(opts.repoPath, session);
|
|
20960
|
+
const timeoutMs = opts.timeoutMs === void 0 ? SCAN_CLAUDE_TIMEOUT_MS : opts.timeoutMs;
|
|
20901
20961
|
log(
|
|
20902
|
-
interactive ? `starting claude (mcp config at ${configPath}, model ${model}) \u2014 this session is interactive; answer its questions as they come` :
|
|
20962
|
+
interactive ? `starting claude (mcp config at ${configPath}, model ${model}) \u2014 this session is interactive; answer its questions as they come` : `${opts.resume ? "resuming" : "spawning"} claude (session ${session.sessionId}, mcp config at ${configPath}, model ${model}, ${timeoutMs === null ? "no timeout" : `timeout ${Math.round(timeoutMs / 6e4)} min`})`
|
|
20903
20963
|
);
|
|
20904
20964
|
const spawn2 = deps.spawn ?? spawnClaude;
|
|
20905
20965
|
const proc = spawn2({
|
|
20906
|
-
prompt: buildBootstrapPrompt({
|
|
20966
|
+
prompt: opts.resume ? opts.resume.message : buildBootstrapPrompt({
|
|
20907
20967
|
promptVariant: opts.promptVariant,
|
|
20908
20968
|
interactive,
|
|
20909
20969
|
briefing: opts.briefing,
|
|
20910
20970
|
mode: opts.mode,
|
|
20911
20971
|
container: opts.container,
|
|
20912
20972
|
repos: opts.repos,
|
|
20913
|
-
plan: opts.plan
|
|
20973
|
+
plan: opts.plan,
|
|
20974
|
+
runSheet: opts.runSheet
|
|
20914
20975
|
}),
|
|
20915
20976
|
mcpConfigPath: configPath,
|
|
20916
20977
|
cwd: opts.repoPath,
|
|
20917
20978
|
// No timeout interactively — a human is present and can Ctrl-C. Killing a
|
|
20918
20979
|
// session mid-answer would be worse than any runaway.
|
|
20919
|
-
timeoutMs: interactive ? void 0 :
|
|
20980
|
+
timeoutMs: interactive ? void 0 : timeoutMs,
|
|
20981
|
+
...session ? opts.resume ? { resumeSessionId: session.sessionId } : { sessionId: session.sessionId } : {},
|
|
20920
20982
|
allowedTools: SCAN_ALLOWED_TOOLS,
|
|
20921
20983
|
model,
|
|
20922
20984
|
interactive,
|
|
@@ -20929,7 +20991,7 @@ async function runScan(opts, deps = {}) {
|
|
|
20929
20991
|
const transcriptDir = join21(opts.repoPath, ".archme", "scan");
|
|
20930
20992
|
const transcriptPath = join21(transcriptDir, "transcript.log");
|
|
20931
20993
|
await mkdir13(transcriptDir, { recursive: true });
|
|
20932
|
-
const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
|
|
20994
|
+
const transcriptStream = createWriteStream2(transcriptPath, { flags: opts.resume ? "a" : "w" });
|
|
20933
20995
|
let finalResultText = "";
|
|
20934
20996
|
const echo = opts.echo ?? ((line) => process.stdout.write(line + "\n"));
|
|
20935
20997
|
if (proc.stdout) {
|
|
@@ -21037,7 +21099,7 @@ async function runScan(opts, deps = {}) {
|
|
|
21037
21099
|
});
|
|
21038
21100
|
}
|
|
21039
21101
|
}
|
|
21040
|
-
var BOOTSTRAP_HEADLESS, BOOTSTRAP_INTERACTIVE, BOOTSTRAP_INTERACTIVE_NO_BRIEFING, PROMPT_VARIANT_RE, CONTAINER_ID_RE, REPO_SLUG_RE;
|
|
21102
|
+
var BOOTSTRAP_HEADLESS, BOOTSTRAP_INTERACTIVE, BOOTSTRAP_INTERACTIVE_NO_BRIEFING, PROMPT_VARIANT_RE, CONTAINER_ID_RE, REPO_SLUG_RE, SCAN_SESSION_FILE, SESSION_ID_RE;
|
|
21041
21103
|
var init_run_scan = __esm({
|
|
21042
21104
|
"src/scan/run-scan.ts"() {
|
|
21043
21105
|
"use strict";
|
|
@@ -21064,6 +21126,8 @@ Before you call \`start_scan\`, show the user this briefing verbatim and ask the
|
|
|
21064
21126
|
PROMPT_VARIANT_RE = /^\d{2}-\d{2}-\d{2}-\d{4}(-[A-Za-z0-9_-]+)?$/;
|
|
21065
21127
|
CONTAINER_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
21066
21128
|
REPO_SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
21129
|
+
SCAN_SESSION_FILE = ".archme/scan/session.json";
|
|
21130
|
+
SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21067
21131
|
}
|
|
21068
21132
|
});
|
|
21069
21133
|
|
|
@@ -22125,7 +22189,7 @@ var init_plan = __esm({
|
|
|
22125
22189
|
});
|
|
22126
22190
|
|
|
22127
22191
|
// src/init/run-init.ts
|
|
22128
|
-
import { mkdtemp as mkdtemp4, rm as rm9, writeFile as
|
|
22192
|
+
import { mkdtemp as mkdtemp4, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
|
|
22129
22193
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
22130
22194
|
import { join as join24 } from "node:path";
|
|
22131
22195
|
async function defaultStartDaemon() {
|
|
@@ -22175,7 +22239,7 @@ async function defaultRunInstallScript(script, cwd) {
|
|
|
22175
22239
|
const dir = await mkdtemp4(join24(tmpdir4(), "archme-init-"));
|
|
22176
22240
|
const scriptPath = join24(dir, "install-agent.sh");
|
|
22177
22241
|
try {
|
|
22178
|
-
await
|
|
22242
|
+
await writeFile13(scriptPath, script, { mode: 448 });
|
|
22179
22243
|
await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
|
|
22180
22244
|
} finally {
|
|
22181
22245
|
await rm9(dir, { recursive: true, force: true });
|
|
@@ -23405,7 +23469,8 @@ async function addRepoToBoard(board, add, deps = {}) {
|
|
|
23405
23469
|
mode: "repo",
|
|
23406
23470
|
container: containerId,
|
|
23407
23471
|
interactive: add.interactive,
|
|
23408
|
-
...add.plan ? { plan: true } : {},
|
|
23472
|
+
...add.plan ? { plan: true, timeoutMs: ORCHESTRATED_SCAN_TIMEOUT_MS } : {},
|
|
23473
|
+
...add.resume ? { resume: add.resume } : {},
|
|
23409
23474
|
...add.echo ? { echo: add.echo } : {},
|
|
23410
23475
|
log
|
|
23411
23476
|
},
|
|
@@ -23458,7 +23523,7 @@ async function runAddRepo(opts, deps = {}) {
|
|
|
23458
23523
|
supabaseUrl: cfg.supabaseUrl,
|
|
23459
23524
|
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
23460
23525
|
clientId: cfg.archmeDaemonClientId,
|
|
23461
|
-
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
23526
|
+
preSpawnMinLifetimeMs: opts.plan ? ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS : SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
23462
23527
|
});
|
|
23463
23528
|
if (!authed2.orgId) return fail(3, "this login is not paired with an org \u2014 run `archme login`");
|
|
23464
23529
|
const project = await (deps.resolveProject ?? resolveOrCreateProject)(authed2.supabase, authed2.userId, opts.project);
|
|
@@ -23471,7 +23536,9 @@ async function runAddRepo(opts, deps = {}) {
|
|
|
23471
23536
|
target: { ...early.target, archmeBranch: scanBranch },
|
|
23472
23537
|
container: flagContainer,
|
|
23473
23538
|
local: opts.local,
|
|
23474
|
-
interactive: !opts.yes
|
|
23539
|
+
interactive: !opts.yes,
|
|
23540
|
+
...opts.plan ? { plan: true } : {},
|
|
23541
|
+
...opts.resume !== void 0 ? { resume: { message: opts.resume } } : {}
|
|
23475
23542
|
},
|
|
23476
23543
|
deps
|
|
23477
23544
|
);
|
|
@@ -23479,6 +23546,7 @@ async function runAddRepo(opts, deps = {}) {
|
|
|
23479
23546
|
const containerPath = added.containerPath;
|
|
23480
23547
|
log(`
|
|
23481
23548
|
\u2713 ${repo} is in ${containerPath}.`);
|
|
23549
|
+
if (opts.plan) return 0;
|
|
23482
23550
|
log(" Next: run `archme add-repo` inside each remaining repo's clone; once all are in,");
|
|
23483
23551
|
log(` run \`archme scan --stitch --project ${opts.project}\` from the directory holding the clones.`);
|
|
23484
23552
|
log(" Watching queues work; the daemon runs it: archme daemon start");
|
|
@@ -24634,6 +24702,9 @@ function parseAddRepoArgs(argv) {
|
|
|
24634
24702
|
let container;
|
|
24635
24703
|
let local = false;
|
|
24636
24704
|
let yes = false;
|
|
24705
|
+
let plan = false;
|
|
24706
|
+
let resume = false;
|
|
24707
|
+
let message;
|
|
24637
24708
|
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
24638
24709
|
const arg = argv[i2];
|
|
24639
24710
|
const next = argv[i2 + 1];
|
|
@@ -24649,6 +24720,14 @@ function parseAddRepoArgs(argv) {
|
|
|
24649
24720
|
local = true;
|
|
24650
24721
|
} else if (arg === "--yes" || arg === "--no-interactive") {
|
|
24651
24722
|
yes = true;
|
|
24723
|
+
} else if (arg === "--plan") {
|
|
24724
|
+
plan = true;
|
|
24725
|
+
} else if (arg === "--resume") {
|
|
24726
|
+
resume = true;
|
|
24727
|
+
} else if (arg === "--message") {
|
|
24728
|
+
if (!next) return err2("--message needs the text to resume with");
|
|
24729
|
+
message = next;
|
|
24730
|
+
i2++;
|
|
24652
24731
|
} else if (arg.startsWith("-")) {
|
|
24653
24732
|
return err2(`unknown flag '${arg}'`);
|
|
24654
24733
|
} else if (repoPath === void 0) {
|
|
@@ -24658,7 +24737,11 @@ function parseAddRepoArgs(argv) {
|
|
|
24658
24737
|
}
|
|
24659
24738
|
}
|
|
24660
24739
|
if (!project) return err2("--project <org/slug> is required \u2014 the multi-repo project to add this repo to");
|
|
24661
|
-
|
|
24740
|
+
if (resume !== (message !== void 0)) return err2("--resume and --message go together: the message is what the scan resumes with");
|
|
24741
|
+
if (resume) {
|
|
24742
|
+
return { kind: "run", opts: { repoPath, project, container, local, yes: true, plan, resume: message } };
|
|
24743
|
+
}
|
|
24744
|
+
return { kind: "run", opts: { repoPath, project, container, local, yes, plan } };
|
|
24662
24745
|
}
|
|
24663
24746
|
function printAddRepoHelp() {
|
|
24664
24747
|
console.log(`archme add-repo \u2014 add one repo to a multi-repo project
|
|
@@ -24678,7 +24761,15 @@ Options:
|
|
|
24678
24761
|
--local Poll from this machine instead of the GitHub App
|
|
24679
24762
|
--yes Run the scan headless (no questions)
|
|
24680
24763
|
|
|
24681
|
-
|
|
24764
|
+
Used by the \`archme add-repos\` orchestrator:
|
|
24765
|
+
--plan Build against the approved story plan, with a
|
|
24766
|
+
${ORCHESTRATED_SCAN_HOURS}-hour timeout
|
|
24767
|
+
--resume --message <text>
|
|
24768
|
+
Continue this repo's last headless scan session,
|
|
24769
|
+
with its full history, from this message
|
|
24770
|
+
|
|
24771
|
+
Several repos at once (one agent plans with you, scans each, stitches):
|
|
24772
|
+
\`archme add-repos\`.`);
|
|
24682
24773
|
}
|
|
24683
24774
|
async function dispatchAddRepo(argv) {
|
|
24684
24775
|
const parsed = parseAddRepoArgs(argv);
|
|
@@ -24699,10 +24790,12 @@ async function dispatchAddRepo(argv) {
|
|
|
24699
24790
|
return 3;
|
|
24700
24791
|
}
|
|
24701
24792
|
}
|
|
24702
|
-
var PROJECT_SLUG_RE4;
|
|
24793
|
+
var ORCHESTRATED_SCAN_HOURS, PROJECT_SLUG_RE4;
|
|
24703
24794
|
var init_dispatch6 = __esm({
|
|
24704
24795
|
"src/addRepo/dispatch.ts"() {
|
|
24705
24796
|
"use strict";
|
|
24797
|
+
init_authedClient();
|
|
24798
|
+
ORCHESTRATED_SCAN_HOURS = ORCHESTRATED_SCAN_TIMEOUT_MS / 36e5;
|
|
24706
24799
|
PROJECT_SLUG_RE4 = /^[^/\s]+\/[^/\s]+$/;
|
|
24707
24800
|
}
|
|
24708
24801
|
});
|
|
@@ -24710,6 +24803,7 @@ var init_dispatch6 = __esm({
|
|
|
24710
24803
|
// src/addRepos/run.ts
|
|
24711
24804
|
var run_exports3 = {};
|
|
24712
24805
|
__export(run_exports3, {
|
|
24806
|
+
buildRunSheet: () => buildRunSheet,
|
|
24713
24807
|
cloneFromGitHub: () => cloneFromGitHub,
|
|
24714
24808
|
listClones: () => listClones,
|
|
24715
24809
|
runAddRepos: () => runAddRepos,
|
|
@@ -24717,7 +24811,7 @@ __export(run_exports3, {
|
|
|
24717
24811
|
watchedElsewhere: () => watchedElsewhere
|
|
24718
24812
|
});
|
|
24719
24813
|
import { readdir as readdir2, stat as stat11 } from "node:fs/promises";
|
|
24720
|
-
import { dirname as
|
|
24814
|
+
import { dirname as dirname16, join as join26, resolve as resolve6, sep as sep2 } from "node:path";
|
|
24721
24815
|
function isThisRunsWrite(metadata) {
|
|
24722
24816
|
return metadata?.kind === "scan" || !!metadata?.repo_fold;
|
|
24723
24817
|
}
|
|
@@ -24752,7 +24846,7 @@ async function listClones(dir) {
|
|
|
24752
24846
|
return out.sort();
|
|
24753
24847
|
}
|
|
24754
24848
|
function commonDir(paths) {
|
|
24755
|
-
const split = paths.map((p) =>
|
|
24849
|
+
const split = paths.map((p) => dirname16(p).split(sep2));
|
|
24756
24850
|
const common2 = [];
|
|
24757
24851
|
for (let i2 = 0; split.every((s) => s[i2] !== void 0 && s[i2] === split[0][i2]); i2++) common2.push(split[0][i2]);
|
|
24758
24852
|
return common2.join(sep2) || sep2;
|
|
@@ -24764,9 +24858,6 @@ function duration(ms) {
|
|
|
24764
24858
|
const s = Math.round(ms / 1e3);
|
|
24765
24859
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
24766
24860
|
}
|
|
24767
|
-
function plural(n2, word) {
|
|
24768
|
-
return `${n2} ${word}${n2 === 1 ? "" : "s"}`;
|
|
24769
|
-
}
|
|
24770
24861
|
function serialized(fn) {
|
|
24771
24862
|
let tail = Promise.resolve();
|
|
24772
24863
|
return (...args) => {
|
|
@@ -24776,18 +24867,28 @@ function serialized(fn) {
|
|
|
24776
24867
|
return run;
|
|
24777
24868
|
};
|
|
24778
24869
|
}
|
|
24779
|
-
|
|
24780
|
-
const
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
|
|
24784
|
-
|
|
24785
|
-
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24870
|
+
function buildRunSheet(opts) {
|
|
24871
|
+
const launch = (r) => `${opts.archme} add-repo ${shellQuote(r.root)} --project ${shellQuote(opts.project)} --container ${r.containerId} --plan --yes${opts.local ? " --local" : ""}`;
|
|
24872
|
+
const lines = [
|
|
24873
|
+
"## Session brief",
|
|
24874
|
+
"",
|
|
24875
|
+
`Project \`${opts.project}\`, branch \`${opts.scanBranch}\`; the clones are under \`${opts.workspace}\`.`,
|
|
24876
|
+
"",
|
|
24877
|
+
"Scan these repos. Launch each once; resume a scan rather than relaunch it.",
|
|
24878
|
+
...opts.scan.flatMap((r) => [
|
|
24879
|
+
"",
|
|
24880
|
+
`### ${r.repo} \u2192 \`root.${r.containerId}\``,
|
|
24881
|
+
`- Clone: \`${r.root}\``,
|
|
24882
|
+
`- Launch: \`${launch(r)}\``,
|
|
24883
|
+
`- Resume: \`${launch(r)} --resume --message "<answer or correction>"\``,
|
|
24884
|
+
`- Session file: \`${join26(r.root, SCAN_SESSION_FILE)}\``
|
|
24885
|
+
])
|
|
24886
|
+
];
|
|
24887
|
+
if (opts.context.length > 0) {
|
|
24888
|
+
lines.push("", "Already on the board \u2014 context only, never launch a scan for these:");
|
|
24889
|
+
for (const r of opts.context) lines.push(`- ${r.repo} (\`root.${r.containerId}\`): ${r.why}`);
|
|
24890
|
+
}
|
|
24891
|
+
return lines.join("\n");
|
|
24791
24892
|
}
|
|
24792
24893
|
async function runAddRepos(opts, deps = {}) {
|
|
24793
24894
|
const log = deps.log ?? ((m) => console.log(m));
|
|
@@ -24802,7 +24903,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24802
24903
|
const session = deps.session ?? runScan;
|
|
24803
24904
|
const confirm = deps.confirm ?? ((q) => promptYesNo(q, { defaultYes: false }));
|
|
24804
24905
|
if (!opts.yes && !(deps.isInteractive ?? stdinIsInteractive)()) {
|
|
24805
|
-
return fail(78, "the
|
|
24906
|
+
return fail(78, "the orchestrator session is interactive and needs a terminal \u2014 pass --yes to run everything headless");
|
|
24806
24907
|
}
|
|
24807
24908
|
const explicit = opts.targets.length > 0;
|
|
24808
24909
|
const toClone = (inspected) => {
|
|
@@ -24855,7 +24956,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24855
24956
|
supabaseUrl: cfg.supabaseUrl,
|
|
24856
24957
|
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
24857
24958
|
clientId: cfg.archmeDaemonClientId,
|
|
24858
|
-
preSpawnMinLifetimeMs:
|
|
24959
|
+
preSpawnMinLifetimeMs: ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS
|
|
24859
24960
|
});
|
|
24860
24961
|
if (!authed2.orgId) return fail(3, "this login is not paired with an org \u2014 run `archme login`");
|
|
24861
24962
|
const { supabase } = authed2;
|
|
@@ -24885,7 +24986,7 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24885
24986
|
const toWatch = done.filter((c3) => !live(c3.target.repo));
|
|
24886
24987
|
const skipped = done.filter((c3) => live(c3.target.repo));
|
|
24887
24988
|
const noClone = roster.added.filter((r) => !clones.some((c3) => sameRepo(c3.target.repo, r)));
|
|
24888
|
-
log(`archme add-repos \u2192 ${opts.project} (branch ${scanBranch}),
|
|
24989
|
+
log(`archme add-repos \u2192 ${opts.project} (branch ${scanBranch}), the orchestrator runs in ${workspace}`);
|
|
24889
24990
|
for (const c3 of todo) log(` add: ${c3.target.repo} \u2192 root.${containerFor(c3.target.repo)}`);
|
|
24890
24991
|
for (const c3 of toWatch) log(` watch: ${c3.target.repo} \u2192 root.${containerFor(c3.target.repo)} (already scanned, so not rescanned)`);
|
|
24891
24992
|
if (skipped.length) {
|
|
@@ -24921,24 +25022,24 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24921
25022
|
Nothing was changed. Unwatch it there first, or add it to that project instead.`);
|
|
24922
25023
|
}
|
|
24923
25024
|
if (!opts.local) {
|
|
24924
|
-
const
|
|
25025
|
+
const missing2 = [];
|
|
24925
25026
|
for (const c3 of watching) {
|
|
24926
25027
|
const found = await (deps.findInstallation ?? findInstallation)(supabase, authed2.orgId, c3.target.repoOwner);
|
|
24927
|
-
if (!found.ok)
|
|
25028
|
+
if (!found.ok) missing2.push(`${c3.target.repo}: ${found.reason}`);
|
|
24928
25029
|
}
|
|
24929
|
-
if (
|
|
24930
|
-
return fail(3, `${
|
|
25030
|
+
if (missing2.length) {
|
|
25031
|
+
return fail(3, `${missing2.join("\n ")}
|
|
24931
25032
|
Nothing was changed. Install the GitHub App there, or re-run with --local to poll from this machine.`);
|
|
24932
25033
|
}
|
|
24933
25034
|
}
|
|
24934
25035
|
const project = existing ?? await (deps.resolveProject ?? resolveOrCreateProject)(supabase, authed2.userId, opts.project);
|
|
24935
25036
|
const board = { authed: authed2, orgId: authed2.orgId, projectId: project.projectId, project: opts.project, scanBranch };
|
|
24936
25037
|
if (roster.fold) {
|
|
24937
|
-
const
|
|
25038
|
+
const failed = await foldForRepos({ supabase, projectId: project.projectId, scanBranch }, roster.fold, "multi-repo add-repos", {
|
|
24938
25039
|
foldBoard: deps.foldBoard,
|
|
24939
25040
|
log
|
|
24940
25041
|
});
|
|
24941
|
-
if (
|
|
25042
|
+
if (failed) return fail(3, `${failed}
|
|
24942
25043
|
Nothing was scanned.`);
|
|
24943
25044
|
}
|
|
24944
25045
|
const register = serialized(registerLocalWatch);
|
|
@@ -24967,23 +25068,23 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24967
25068
|
${MULTI_REPO_DAEMON_NOTE}`);
|
|
24968
25069
|
return 0;
|
|
24969
25070
|
}
|
|
24970
|
-
const offerUndo = async () => {
|
|
25071
|
+
const offerUndo = async (client) => {
|
|
24971
25072
|
if (!await confirm("Remove what this run wrote to the board? It goes back to how it was before this run, as one new commit.")) {
|
|
24972
25073
|
log(" Left the board as it is.");
|
|
24973
25074
|
return;
|
|
24974
25075
|
}
|
|
24975
25076
|
try {
|
|
24976
|
-
const architecture = before.headCommitId ? before.architecture : (await load2(
|
|
24977
|
-
const restored = await (deps.restoreBoard ?? ((s, o2) => restoreBoard(s, o2)))(
|
|
25077
|
+
const architecture = before.headCommitId ? before.architecture : (await load2(client, project.projectId, project.defaultBranch)).architecture;
|
|
25078
|
+
const restored = await (deps.restoreBoard ?? ((s, o2) => restoreBoard(s, o2)))(client, {
|
|
24978
25079
|
projectId: project.projectId,
|
|
24979
25080
|
branch: scanBranch,
|
|
24980
25081
|
snapshot: { architecture, headCommitId: before.headCommitId },
|
|
24981
|
-
message: "Undo archme add-repos (
|
|
25082
|
+
message: "Undo archme add-repos (no repo was scanned)",
|
|
24982
25083
|
isOwn: isThisRunsWrite
|
|
24983
25084
|
});
|
|
24984
25085
|
if (roster.fold && !hasRepoContainers(before.architecture)) {
|
|
24985
25086
|
await (deps.setRepoContainer ?? ((s, p, r, c3) => setRepoContainer(s, p, r, c3)))(
|
|
24986
|
-
|
|
25087
|
+
client,
|
|
24987
25088
|
project.projectId,
|
|
24988
25089
|
roster.fold.repo,
|
|
24989
25090
|
null
|
|
@@ -24995,105 +25096,82 @@ async function runAddRepos(opts, deps = {}) {
|
|
|
24995
25096
|
}
|
|
24996
25097
|
};
|
|
24997
25098
|
const interactive = !opts.yes;
|
|
25099
|
+
const [nodeBin, cliEntry] = deps.archmeCommand === void 0 ? archmeSelfCommand() : [null, null];
|
|
25100
|
+
const runSheet = buildRunSheet({
|
|
25101
|
+
project: opts.project,
|
|
25102
|
+
scanBranch,
|
|
25103
|
+
workspace,
|
|
25104
|
+
archme: deps.archmeCommand ?? `${shellQuote(nodeBin)} ${shellQuote(cliEntry)}`,
|
|
25105
|
+
local: opts.local,
|
|
25106
|
+
scan: todo.map((c3) => ({ repo: c3.target.repo, root: c3.root, containerId: containerFor(c3.target.repo) })),
|
|
25107
|
+
context: [
|
|
25108
|
+
...done.map((c3) => ({ repo: c3.target.repo, containerId: containerFor(c3.target.repo), why: "already scanned" })),
|
|
25109
|
+
...noClone.map((repo) => ({ repo, containerId: containerFor(repo), why: "no clone here; read it through the graph" }))
|
|
25110
|
+
]
|
|
25111
|
+
});
|
|
24998
25112
|
const t0 = Date.now();
|
|
24999
25113
|
log(`
|
|
25000
|
-
\u2500\u2500
|
|
25001
|
-
const
|
|
25002
|
-
{
|
|
25114
|
+
\u2500\u2500 Orchestrator session (${interactive ? "interactive \u2014 keep it open until it finishes; closing it stops the scans it runs" : "headless"}) \u2500\u2500`);
|
|
25115
|
+
const orchestrated = await session(
|
|
25116
|
+
{
|
|
25117
|
+
repoPath: workspace,
|
|
25118
|
+
project: opts.project,
|
|
25119
|
+
branch: scanBranch,
|
|
25120
|
+
mode: "orchestrator",
|
|
25121
|
+
repos: roster.repos,
|
|
25122
|
+
runSheet,
|
|
25123
|
+
interactive,
|
|
25124
|
+
timeoutMs: null,
|
|
25125
|
+
log
|
|
25126
|
+
},
|
|
25003
25127
|
{ authed: authed2 }
|
|
25004
25128
|
);
|
|
25005
|
-
const
|
|
25006
|
-
log(`
|
|
25007
|
-
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
|
|
25011
|
-
|
|
25012
|
-
if (interactive) {
|
|
25013
|
-
if (!await confirm(`Scan ${plural(todo.length, "repo")} against this plan?`)) {
|
|
25014
|
-
await offerUndo();
|
|
25015
|
-
return 1;
|
|
25016
|
-
}
|
|
25017
|
-
} else {
|
|
25018
|
-
log(" plan approved (--yes)");
|
|
25019
|
-
}
|
|
25020
|
-
log(`
|
|
25021
|
-
\u2500\u2500 Scanning ${plural(todo.length, "repo")}, ${Math.min(opts.parallel, todo.length)} at a time \u2500\u2500`);
|
|
25022
|
-
const results = await mapLimited(todo, opts.parallel, async (clone) => {
|
|
25023
|
-
const tag = `[${clone.target.repoName}]`;
|
|
25024
|
-
const started = Date.now();
|
|
25025
|
-
const containerPath = `root.${containerFor(clone.target.repo)}`;
|
|
25026
|
-
try {
|
|
25027
|
-
const out = await addRepo(
|
|
25028
|
-
board,
|
|
25029
|
-
{
|
|
25030
|
-
root: clone.root,
|
|
25031
|
-
originUrl: clone.originUrl,
|
|
25032
|
-
target: { ...clone.target, archmeBranch: scanBranch },
|
|
25033
|
-
container: containerFor(clone.target.repo),
|
|
25034
|
-
local: opts.local,
|
|
25035
|
-
interactive: false,
|
|
25036
|
-
plan: true,
|
|
25037
|
-
echo: (line) => log(`${tag} ${line}`)
|
|
25038
|
-
},
|
|
25039
|
-
{ log: tagged(clone), registerLocalWatch: register }
|
|
25040
|
-
);
|
|
25041
|
-
const ms = Date.now() - started;
|
|
25042
|
-
if (!out.ok) return { clone, containerPath, marker: null, ms, error: { exitCode: out.exitCode, reason: out.reason ?? "the scan failed" } };
|
|
25043
|
-
const marker = await readMarker(supabase, project.projectId, scanBranch, clone.target.repo);
|
|
25044
|
-
const error = marker ? null : { exitCode: 1, reason: "the scan ended without stamping its resume marker (complete_scan)" };
|
|
25045
|
-
return { clone, containerPath: out.containerPath, marker, ms, error };
|
|
25046
|
-
} catch (e) {
|
|
25047
|
-
return { clone, containerPath, marker: null, ms: Date.now() - started, error: { exitCode: 3, reason: e instanceof Error ? e.message : String(e) } };
|
|
25048
|
-
}
|
|
25129
|
+
const sessionExit = dispatchExitCode(orchestrated);
|
|
25130
|
+
log(sessionExit === 0 ? ` orchestrator session: ${duration(Date.now() - t0)}` : ` orchestrator session exited ${sessionExit} after ${duration(Date.now() - t0)}`);
|
|
25131
|
+
const after = await (deps.authenticate ?? createAuthedClient)({
|
|
25132
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
25133
|
+
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
25134
|
+
clientId: cfg.archmeDaemonClientId,
|
|
25135
|
+
preSpawnMinLifetimeMs: ORCHESTRATED_SCAN_TOKEN_MIN_LIFETIME_MS
|
|
25049
25136
|
});
|
|
25050
|
-
const
|
|
25051
|
-
const
|
|
25052
|
-
|
|
25053
|
-
if (added.length > 0) {
|
|
25054
|
-
const t1 = Date.now();
|
|
25055
|
-
log(`
|
|
25056
|
-
\u2500\u2500 Stitch session (${interactive ? "interactive" : "headless"}) \u2500\u2500`);
|
|
25057
|
-
stitchExit = dispatchExitCode(await session(
|
|
25058
|
-
{ repoPath: workspace, project: opts.project, branch: scanBranch, mode: "stitch", plan: true, interactive, log },
|
|
25059
|
-
{ authed: authed2 }
|
|
25060
|
-
));
|
|
25061
|
-
log(` stitch session: ${duration(Date.now() - t1)}`);
|
|
25137
|
+
const results = [];
|
|
25138
|
+
for (const c3 of todo) {
|
|
25139
|
+
results.push({ clone: c3, marker: await readMarker(after.supabase, project.projectId, scanBranch, c3.target.repo) });
|
|
25062
25140
|
}
|
|
25141
|
+
const added = results.filter((r) => r.marker);
|
|
25142
|
+
const missing = results.filter((r) => !r.marker);
|
|
25143
|
+
if (added.length === 0 && interactive) await offerUndo(after.supabase);
|
|
25063
25144
|
const rerun = [
|
|
25064
25145
|
explicit ? "" : `cd ${shellQuote(workspace)} && `,
|
|
25065
25146
|
"archme add-repos",
|
|
25066
25147
|
...explicit ? clones.map((c3) => ` ${shellQuote(c3.root)}`) : [],
|
|
25067
25148
|
` --project ${opts.project}`,
|
|
25068
25149
|
opts.local ? " --local" : "",
|
|
25069
|
-
opts.parallel !== 2 ? ` --parallel ${opts.parallel}` : "",
|
|
25070
25150
|
opts.yes ? " --yes" : ""
|
|
25071
25151
|
].join("");
|
|
25072
25152
|
const width = Math.max(...results.map((r) => r.clone.target.repo.length));
|
|
25073
25153
|
log("\n\u2500\u2500 Summary \u2500\u2500");
|
|
25074
25154
|
for (const r of added) {
|
|
25075
|
-
log(` \u2713 ${r.clone.target.repo.padEnd(width)} \u2192
|
|
25155
|
+
log(` \u2713 ${r.clone.target.repo.padEnd(width)} \u2192 root.${containerFor(r.clone.target.repo)} marker ${r.marker.slice(0, 7)}`);
|
|
25076
25156
|
}
|
|
25077
|
-
for (const r of
|
|
25078
|
-
log(` \u2717 ${r.clone.target.repo.padEnd(width)}
|
|
25157
|
+
for (const r of missing) {
|
|
25158
|
+
log(` \u2717 ${r.clone.target.repo.padEnd(width)} has no resume marker: its scan did not finish`);
|
|
25079
25159
|
}
|
|
25080
25160
|
if (toWatch.length) log(` watched, already scanned: ${toWatch.map((c3) => c3.target.repo).join(", ")}`);
|
|
25081
25161
|
if (skipped.length) log(` skipped, already on the board: ${skipped.map((c3) => c3.target.repo).join(", ")}`);
|
|
25082
|
-
if (
|
|
25162
|
+
if (missing.length) log(` Re-run (skips what is already on the board):
|
|
25083
25163
|
${rerun}`);
|
|
25084
|
-
if (stitchExit === null) log(" Stitch: not run \u2014 no repo was added.");
|
|
25085
|
-
else if (stitchExit !== 0) log(` Stitch: exited ${stitchExit}. Re-run it alone: cd ${shellQuote(workspace)} && archme scan --stitch --project ${opts.project}`);
|
|
25086
25164
|
log(` Board: ${deriveWebOrigin(cfg.archmeMcpBaseUrl, "fallback")}/a/${encodeURIComponent(project.orgSlug)}/${encodeURIComponent(project.projectSlug)}/${encodeURIComponent(scanBranch)}`);
|
|
25087
|
-
|
|
25088
|
-
|
|
25089
|
-
|
|
25165
|
+
const end = await load2(after.supabase, project.projectId, scanBranch).catch(() => null);
|
|
25166
|
+
const changes = end?.architecture.context?.entries.find((e) => e.id === PLAN_CHANGES_ID);
|
|
25167
|
+
if (changes) {
|
|
25090
25168
|
log("\nWhat changed from the approved plan, and why:");
|
|
25091
|
-
log(changes
|
|
25169
|
+
log(changes.content.split("\n").map((l) => ` ${l}`).join("\n"));
|
|
25092
25170
|
}
|
|
25093
25171
|
log(`
|
|
25094
25172
|
Watching queues work; the daemon runs it: archme daemon start
|
|
25095
25173
|
${MULTI_REPO_DAEMON_NOTE}`);
|
|
25096
|
-
return
|
|
25174
|
+
return sessionExit !== 0 ? sessionExit : missing.length > 0 ? 1 : 0;
|
|
25097
25175
|
}
|
|
25098
25176
|
var EMPTY, REPO_NAME_RE;
|
|
25099
25177
|
var init_run3 = __esm({
|
|
@@ -25103,6 +25181,7 @@ var init_run3 = __esm({
|
|
|
25103
25181
|
init_defaults();
|
|
25104
25182
|
init_authedClient();
|
|
25105
25183
|
init_run_scan();
|
|
25184
|
+
init_install();
|
|
25106
25185
|
init_dispatch3();
|
|
25107
25186
|
init_web_url();
|
|
25108
25187
|
init_run_init();
|
|
@@ -25129,7 +25208,6 @@ var init_run3 = __esm({
|
|
|
25129
25208
|
// src/addRepos/dispatch.ts
|
|
25130
25209
|
var dispatch_exports7 = {};
|
|
25131
25210
|
__export(dispatch_exports7, {
|
|
25132
|
-
MAX_PARALLEL: () => MAX_PARALLEL,
|
|
25133
25211
|
dispatchAddRepos: () => dispatchAddRepos,
|
|
25134
25212
|
parseAddReposArgs: () => parseAddReposArgs,
|
|
25135
25213
|
printAddReposHelp: () => printAddReposHelp
|
|
@@ -25143,7 +25221,6 @@ function parseAddReposArgs(argv) {
|
|
|
25143
25221
|
let project;
|
|
25144
25222
|
let local = false;
|
|
25145
25223
|
let yes = false;
|
|
25146
|
-
let parallel = 2;
|
|
25147
25224
|
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
25148
25225
|
const arg = argv[i2];
|
|
25149
25226
|
const next = argv[i2 + 1];
|
|
@@ -25151,11 +25228,6 @@ function parseAddReposArgs(argv) {
|
|
|
25151
25228
|
if (!next || !PROJECT_SLUG_RE5.test(next)) return err3("--project needs `org/slug`");
|
|
25152
25229
|
project = next;
|
|
25153
25230
|
i2++;
|
|
25154
|
-
} else if (arg === "--parallel") {
|
|
25155
|
-
const n2 = Number(next);
|
|
25156
|
-
if (!next || !Number.isInteger(n2) || n2 < 1 || n2 > MAX_PARALLEL) return err3(`--parallel needs a number from 1 to ${MAX_PARALLEL}`);
|
|
25157
|
-
parallel = n2;
|
|
25158
|
-
i2++;
|
|
25159
25231
|
} else if (arg === "--local") {
|
|
25160
25232
|
local = true;
|
|
25161
25233
|
} else if (arg === "--yes" || arg === "--no-interactive") {
|
|
@@ -25167,33 +25239,39 @@ function parseAddReposArgs(argv) {
|
|
|
25167
25239
|
}
|
|
25168
25240
|
}
|
|
25169
25241
|
if (!project) return err3("--project <org/slug> is required \u2014 the multi-repo project to add the repos to");
|
|
25170
|
-
return { kind: "run", opts: { targets, project, local,
|
|
25242
|
+
return { kind: "run", opts: { targets, project, local, yes } };
|
|
25171
25243
|
}
|
|
25172
25244
|
function printAddReposHelp() {
|
|
25173
25245
|
console.log(`archme add-repos \u2014 add several repos to a multi-repo project in one run
|
|
25174
25246
|
|
|
25175
25247
|
Usage:
|
|
25176
|
-
archme add-repos [clone-path | owner/name \u2026] --project <org/slug> [--local] [--
|
|
25248
|
+
archme add-repos [clone-path | owner/name \u2026] --project <org/slug> [--local] [--yes]
|
|
25177
25249
|
|
|
25178
25250
|
Each repo is a clone path, or owner/name: a clone of it already in the current
|
|
25179
25251
|
folder is used, otherwise it is cloned there with your own git access. With
|
|
25180
25252
|
none given, every git clone directly under the current folder is added.
|
|
25181
25253
|
Clones of repos the board already holds are read as context, not re-added.
|
|
25182
25254
|
|
|
25183
|
-
|
|
25184
|
-
|
|
25185
|
-
|
|
25186
|
-
|
|
25187
|
-
|
|
25188
|
-
|
|
25189
|
-
|
|
25190
|
-
|
|
25255
|
+
Checks everything first (project, GitHub App, containers) and writes nothing
|
|
25256
|
+
until all pass; a single-repo board is folded into its repo's container. Then
|
|
25257
|
+
ONE orchestrator agent runs the rest, in an interactive session:
|
|
25258
|
+
|
|
25259
|
+
1. The gist: names every repo's container and the shared externals, studies
|
|
25260
|
+
each repo and the product (web included), and drafts the story plan with
|
|
25261
|
+
you until you approve it.
|
|
25262
|
+
2. Depth: studies each repo's code and git history, revises the plan, and
|
|
25263
|
+
shows you what changed until you approve it.
|
|
25264
|
+
3. Build: launches one scan per repo (\`archme add-repo --plan\`, hours each),
|
|
25265
|
+
supervises them \u2014 answering their questions, pausing and resuming any that
|
|
25266
|
+
go off course \u2014 and stitches what crosses repos as each side lands, then
|
|
25267
|
+
explains what changed from the plan.
|
|
25268
|
+
|
|
25269
|
+
Keep the session open until it finishes: closing it stops the scans it runs.
|
|
25191
25270
|
Re-running skips repos already on the board, so a failed run resumes.
|
|
25192
25271
|
|
|
25193
25272
|
Options:
|
|
25194
25273
|
--project <org/slug> The multi-repo project (required)
|
|
25195
25274
|
--local Poll from this machine instead of the GitHub App
|
|
25196
|
-
--parallel <n> Repos scanned at once, 1-${MAX_PARALLEL} (default 2)
|
|
25197
25275
|
--yes Everything headless, no questions; the plan is approved`);
|
|
25198
25276
|
}
|
|
25199
25277
|
async function dispatchAddRepos(argv) {
|
|
@@ -25215,12 +25293,11 @@ async function dispatchAddRepos(argv) {
|
|
|
25215
25293
|
return 3;
|
|
25216
25294
|
}
|
|
25217
25295
|
}
|
|
25218
|
-
var PROJECT_SLUG_RE5
|
|
25296
|
+
var PROJECT_SLUG_RE5;
|
|
25219
25297
|
var init_dispatch7 = __esm({
|
|
25220
25298
|
"src/addRepos/dispatch.ts"() {
|
|
25221
25299
|
"use strict";
|
|
25222
25300
|
PROJECT_SLUG_RE5 = /^[^/\s]+\/[^/\s]+$/;
|
|
25223
|
-
MAX_PARALLEL = 3;
|
|
25224
25301
|
}
|
|
25225
25302
|
});
|
|
25226
25303
|
|
|
@@ -25616,7 +25693,7 @@ __export(dispatch_exports8, {
|
|
|
25616
25693
|
parseDoctorArgs: () => parseDoctorArgs,
|
|
25617
25694
|
printDoctorHelp: () => printDoctorHelp
|
|
25618
25695
|
});
|
|
25619
|
-
import { mkdtemp as mkdtemp5, rm as rm10, writeFile as
|
|
25696
|
+
import { mkdtemp as mkdtemp5, rm as rm10, writeFile as writeFile14 } from "node:fs/promises";
|
|
25620
25697
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
25621
25698
|
import { join as join28 } from "node:path";
|
|
25622
25699
|
function parseDoctorArgs(argv) {
|
|
@@ -25681,7 +25758,7 @@ async function dispatchDoctor(argv) {
|
|
|
25681
25758
|
}
|
|
25682
25759
|
const dir = await mkdtemp5(join28(tmpdir5(), "archme-doctor-"));
|
|
25683
25760
|
const mcpConfigPath = join28(dir, "mcp.json");
|
|
25684
|
-
await
|
|
25761
|
+
await writeFile14(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
|
|
25685
25762
|
try {
|
|
25686
25763
|
const result = await spawnClaude({
|
|
25687
25764
|
prompt: buildDoctorPrompt(facts, issues),
|
|
@@ -26090,7 +26167,8 @@ Other commands:
|
|
|
26090
26167
|
(--list|--stop|--resume)
|
|
26091
26168
|
archme add-repos [clone-path | owner/name \u2026] --project <org/slug>
|
|
26092
26169
|
Add several repos to a multi-repo project:
|
|
26093
|
-
|
|
26170
|
+
one agent plans with you, scans each,
|
|
26171
|
+
stitches as they land (resumable)
|
|
26094
26172
|
archme add-repo [repo-path] --project <org/slug>
|
|
26095
26173
|
Add one repo to a multi-repo project
|
|
26096
26174
|
archme uninstall [repo-path] Stop ArchitectMe for a repo: config,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archme/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|