@archme/cli 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2601 -319
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1020,15 +1020,15 @@ var require_windows = __commonJS({
|
|
|
1020
1020
|
}
|
|
1021
1021
|
return false;
|
|
1022
1022
|
}
|
|
1023
|
-
function checkStat(
|
|
1024
|
-
if (!
|
|
1023
|
+
function checkStat(stat11, path7, options) {
|
|
1024
|
+
if (!stat11.isSymbolicLink() && !stat11.isFile()) {
|
|
1025
1025
|
return false;
|
|
1026
1026
|
}
|
|
1027
1027
|
return checkPathExt(path7, options);
|
|
1028
1028
|
}
|
|
1029
1029
|
function isexe(path7, options, cb) {
|
|
1030
|
-
fs.stat(path7, function(er,
|
|
1031
|
-
cb(er, er ? false : checkStat(
|
|
1030
|
+
fs.stat(path7, function(er, stat11) {
|
|
1031
|
+
cb(er, er ? false : checkStat(stat11, path7, options));
|
|
1032
1032
|
});
|
|
1033
1033
|
}
|
|
1034
1034
|
function sync(path7, options) {
|
|
@@ -1044,20 +1044,20 @@ var require_mode = __commonJS({
|
|
|
1044
1044
|
isexe.sync = sync;
|
|
1045
1045
|
var fs = __require("fs");
|
|
1046
1046
|
function isexe(path7, options, cb) {
|
|
1047
|
-
fs.stat(path7, function(er,
|
|
1048
|
-
cb(er, er ? false : checkStat(
|
|
1047
|
+
fs.stat(path7, function(er, stat11) {
|
|
1048
|
+
cb(er, er ? false : checkStat(stat11, options));
|
|
1049
1049
|
});
|
|
1050
1050
|
}
|
|
1051
1051
|
function sync(path7, options) {
|
|
1052
1052
|
return checkStat(fs.statSync(path7), options);
|
|
1053
1053
|
}
|
|
1054
|
-
function checkStat(
|
|
1055
|
-
return
|
|
1054
|
+
function checkStat(stat11, options) {
|
|
1055
|
+
return stat11.isFile() && checkMode(stat11, options);
|
|
1056
1056
|
}
|
|
1057
|
-
function checkMode(
|
|
1058
|
-
var mod =
|
|
1059
|
-
var uid =
|
|
1060
|
-
var gid =
|
|
1057
|
+
function checkMode(stat11, options) {
|
|
1058
|
+
var mod = stat11.mode;
|
|
1059
|
+
var uid = stat11.uid;
|
|
1060
|
+
var gid = stat11.gid;
|
|
1061
1061
|
var myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid();
|
|
1062
1062
|
var myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid();
|
|
1063
1063
|
var u2 = parseInt("100", 8);
|
|
@@ -1091,12 +1091,12 @@ var require_isexe = __commonJS({
|
|
|
1091
1091
|
if (typeof Promise !== "function") {
|
|
1092
1092
|
throw new TypeError("callback not provided");
|
|
1093
1093
|
}
|
|
1094
|
-
return new Promise(function(
|
|
1094
|
+
return new Promise(function(resolve5, reject) {
|
|
1095
1095
|
isexe(path7, options || {}, function(er, is) {
|
|
1096
1096
|
if (er) {
|
|
1097
1097
|
reject(er);
|
|
1098
1098
|
} else {
|
|
1099
|
-
|
|
1099
|
+
resolve5(is);
|
|
1100
1100
|
}
|
|
1101
1101
|
});
|
|
1102
1102
|
});
|
|
@@ -1162,27 +1162,27 @@ var require_which = __commonJS({
|
|
|
1162
1162
|
opt = {};
|
|
1163
1163
|
const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
|
|
1164
1164
|
const found = [];
|
|
1165
|
-
const step = (i2) => new Promise((
|
|
1165
|
+
const step = (i2) => new Promise((resolve5, reject) => {
|
|
1166
1166
|
if (i2 === pathEnv.length)
|
|
1167
|
-
return opt.all && found.length ?
|
|
1167
|
+
return opt.all && found.length ? resolve5(found) : reject(getNotFoundError(cmd));
|
|
1168
1168
|
const ppRaw = pathEnv[i2];
|
|
1169
1169
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
1170
1170
|
const pCmd = path7.join(pathPart, cmd);
|
|
1171
1171
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
1172
|
-
|
|
1172
|
+
resolve5(subStep(p, i2, 0));
|
|
1173
1173
|
});
|
|
1174
|
-
const subStep = (p, i2, ii) => new Promise((
|
|
1174
|
+
const subStep = (p, i2, ii) => new Promise((resolve5, reject) => {
|
|
1175
1175
|
if (ii === pathExt.length)
|
|
1176
|
-
return
|
|
1176
|
+
return resolve5(step(i2 + 1));
|
|
1177
1177
|
const ext2 = pathExt[ii];
|
|
1178
1178
|
isexe(p + ext2, { pathExt: pathExtExe }, (er, is) => {
|
|
1179
1179
|
if (!er && is) {
|
|
1180
1180
|
if (opt.all)
|
|
1181
1181
|
found.push(p + ext2);
|
|
1182
1182
|
else
|
|
1183
|
-
return
|
|
1183
|
+
return resolve5(p + ext2);
|
|
1184
1184
|
}
|
|
1185
|
-
return
|
|
1185
|
+
return resolve5(subStep(p, i2, ii + 1));
|
|
1186
1186
|
});
|
|
1187
1187
|
});
|
|
1188
1188
|
return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
|
|
@@ -1253,12 +1253,12 @@ var require_resolveCommand = __commonJS({
|
|
|
1253
1253
|
if (shouldSwitchCwd) {
|
|
1254
1254
|
try {
|
|
1255
1255
|
process.chdir(parsed.options.cwd);
|
|
1256
|
-
} catch (
|
|
1256
|
+
} catch (err2) {
|
|
1257
1257
|
}
|
|
1258
1258
|
}
|
|
1259
|
-
let
|
|
1259
|
+
let resolved2;
|
|
1260
1260
|
try {
|
|
1261
|
-
|
|
1261
|
+
resolved2 = which.sync(parsed.command, {
|
|
1262
1262
|
path: env[getPathKey({ env })],
|
|
1263
1263
|
pathExt: withoutPathExt ? path7.delimiter : void 0
|
|
1264
1264
|
});
|
|
@@ -1268,10 +1268,10 @@ var require_resolveCommand = __commonJS({
|
|
|
1268
1268
|
process.chdir(cwd);
|
|
1269
1269
|
}
|
|
1270
1270
|
}
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1271
|
+
if (resolved2) {
|
|
1272
|
+
resolved2 = path7.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved2);
|
|
1273
1273
|
}
|
|
1274
|
-
return
|
|
1274
|
+
return resolved2;
|
|
1275
1275
|
}
|
|
1276
1276
|
function resolveCommand(parsed) {
|
|
1277
1277
|
return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, true);
|
|
@@ -1394,7 +1394,7 @@ var require_parse = __commonJS({
|
|
|
1394
1394
|
}
|
|
1395
1395
|
return parsed;
|
|
1396
1396
|
}
|
|
1397
|
-
function
|
|
1397
|
+
function parse2(command, args, options) {
|
|
1398
1398
|
if (args && !Array.isArray(args)) {
|
|
1399
1399
|
options = args;
|
|
1400
1400
|
args = null;
|
|
@@ -1413,7 +1413,7 @@ var require_parse = __commonJS({
|
|
|
1413
1413
|
};
|
|
1414
1414
|
return options.shell ? parsed : parseNonShell(parsed);
|
|
1415
1415
|
}
|
|
1416
|
-
module.exports =
|
|
1416
|
+
module.exports = parse2;
|
|
1417
1417
|
}
|
|
1418
1418
|
});
|
|
1419
1419
|
|
|
@@ -1438,9 +1438,9 @@ var require_enoent = __commonJS({
|
|
|
1438
1438
|
const originalEmit = cp.emit;
|
|
1439
1439
|
cp.emit = function(name, arg1) {
|
|
1440
1440
|
if (name === "exit") {
|
|
1441
|
-
const
|
|
1442
|
-
if (
|
|
1443
|
-
return originalEmit.call(cp, "error",
|
|
1441
|
+
const err2 = verifyENOENT(arg1, parsed);
|
|
1442
|
+
if (err2) {
|
|
1443
|
+
return originalEmit.call(cp, "error", err2);
|
|
1444
1444
|
}
|
|
1445
1445
|
}
|
|
1446
1446
|
return originalEmit.apply(cp, arguments);
|
|
@@ -1472,16 +1472,16 @@ var require_cross_spawn = __commonJS({
|
|
|
1472
1472
|
"node_modules/cross-spawn/index.js"(exports, module) {
|
|
1473
1473
|
"use strict";
|
|
1474
1474
|
var cp = __require("child_process");
|
|
1475
|
-
var
|
|
1475
|
+
var parse2 = require_parse();
|
|
1476
1476
|
var enoent = require_enoent();
|
|
1477
1477
|
function spawn2(command, args, options) {
|
|
1478
|
-
const parsed =
|
|
1478
|
+
const parsed = parse2(command, args, options);
|
|
1479
1479
|
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
1480
1480
|
enoent.hookChildProcess(spawned, parsed);
|
|
1481
1481
|
return spawned;
|
|
1482
1482
|
}
|
|
1483
1483
|
function spawnSync2(command, args, options) {
|
|
1484
|
-
const parsed =
|
|
1484
|
+
const parsed = parse2(command, args, options);
|
|
1485
1485
|
const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
|
|
1486
1486
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
1487
1487
|
return result;
|
|
@@ -1489,7 +1489,7 @@ var require_cross_spawn = __commonJS({
|
|
|
1489
1489
|
module.exports = spawn2;
|
|
1490
1490
|
module.exports.spawn = spawn2;
|
|
1491
1491
|
module.exports.sync = spawnSync2;
|
|
1492
|
-
module.exports._parse =
|
|
1492
|
+
module.exports._parse = parse2;
|
|
1493
1493
|
module.exports._enoent = enoent;
|
|
1494
1494
|
}
|
|
1495
1495
|
});
|
|
@@ -2262,8 +2262,8 @@ var init_deferred = __esm({
|
|
|
2262
2262
|
"node_modules/execa/lib/utils/deferred.js"() {
|
|
2263
2263
|
createDeferred = () => {
|
|
2264
2264
|
const methods = {};
|
|
2265
|
-
const promise = new Promise((
|
|
2266
|
-
Object.assign(methods, { resolve:
|
|
2265
|
+
const promise = new Promise((resolve5, reject) => {
|
|
2266
|
+
Object.assign(methods, { resolve: resolve5, reject });
|
|
2267
2267
|
});
|
|
2268
2268
|
return Object.assign(promise, methods);
|
|
2269
2269
|
};
|
|
@@ -4057,8 +4057,8 @@ function prettyMilliseconds(milliseconds, options) {
|
|
|
4057
4057
|
const flooredValue = Math.round(flooredInterimValue) / 10 ** decimalDigits;
|
|
4058
4058
|
return flooredValue.toFixed(decimalDigits);
|
|
4059
4059
|
};
|
|
4060
|
-
const add = (value, long,
|
|
4061
|
-
if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation &&
|
|
4060
|
+
const add = (value, long, short4, valueString) => {
|
|
4061
|
+
if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation && short4 === "m")) {
|
|
4062
4062
|
return;
|
|
4063
4063
|
}
|
|
4064
4064
|
valueString ??= String(value);
|
|
@@ -4067,7 +4067,7 @@ function prettyMilliseconds(milliseconds, options) {
|
|
|
4067
4067
|
const minLength = result.length > 0 ? 2 : 1;
|
|
4068
4068
|
valueString = "0".repeat(Math.max(0, minLength - wholeDigits)) + valueString;
|
|
4069
4069
|
} else {
|
|
4070
|
-
valueString += options.verbose ? " " + pluralize(long, value) :
|
|
4070
|
+
valueString += options.verbose ? " " + pluralize(long, value) : short4;
|
|
4071
4071
|
}
|
|
4072
4072
|
result.push(valueString);
|
|
4073
4073
|
};
|
|
@@ -7560,11 +7560,11 @@ var init_concurrent = __esm({
|
|
|
7560
7560
|
const promises = weakMap.get(stream);
|
|
7561
7561
|
const promise = createDeferred();
|
|
7562
7562
|
promises.push(promise);
|
|
7563
|
-
const
|
|
7564
|
-
return { resolve:
|
|
7563
|
+
const resolve5 = promise.resolve.bind(promise);
|
|
7564
|
+
return { resolve: resolve5, promises };
|
|
7565
7565
|
};
|
|
7566
|
-
waitForConcurrentStreams = async ({ resolve:
|
|
7567
|
-
|
|
7566
|
+
waitForConcurrentStreams = async ({ resolve: resolve5, promises }, subprocess) => {
|
|
7567
|
+
resolve5();
|
|
7568
7568
|
const [isSubprocessExit] = await Promise.race([
|
|
7569
7569
|
Promise.allSettled([true, subprocess]),
|
|
7570
7570
|
Promise.all([false, ...promises])
|
|
@@ -8376,9 +8376,9 @@ async function saveCredentials(credentials) {
|
|
|
8376
8376
|
next2.push(merged2);
|
|
8377
8377
|
try {
|
|
8378
8378
|
await keytar.setPassword(SERVICE_NAME, credentials.supabase_url, serializeStore(next2));
|
|
8379
|
-
} catch (
|
|
8380
|
-
warnCredentialPersistFailure("macOS Keychain",
|
|
8381
|
-
throw
|
|
8379
|
+
} catch (err2) {
|
|
8380
|
+
warnCredentialPersistFailure("macOS Keychain", err2);
|
|
8381
|
+
throw err2;
|
|
8382
8382
|
}
|
|
8383
8383
|
return { credentials: merged2, source: "keychain" };
|
|
8384
8384
|
}
|
|
@@ -8391,8 +8391,8 @@ async function saveCredentials(credentials) {
|
|
|
8391
8391
|
return { credentials: merged, source: "file", path: fallbackPath() };
|
|
8392
8392
|
});
|
|
8393
8393
|
}
|
|
8394
|
-
function warnCredentialPersistFailure(target,
|
|
8395
|
-
const detail =
|
|
8394
|
+
function warnCredentialPersistFailure(target, err2) {
|
|
8395
|
+
const detail = err2 instanceof Error ? err2.message : String(err2);
|
|
8396
8396
|
console.error(
|
|
8397
8397
|
`auth: CRITICAL \u2014 failed to persist credentials to ${target}: ${detail}. The rotated refresh token was NOT saved; the daemon keeps working on its in-memory token until it restarts, then may fail with "Refresh Token Not Found" because it reloads the previous, already-used refresh token. If a keychain password prompt was dismissed, grant access (or re-run \`archme login\`) before restarting.`
|
|
8398
8398
|
);
|
|
@@ -8452,9 +8452,9 @@ async function writeFileStore(all) {
|
|
|
8452
8452
|
await writeFile(fallbackPath(), serializeStore(all), { mode: 384 });
|
|
8453
8453
|
await chmod(fallbackPath(), 384).catch(() => {
|
|
8454
8454
|
});
|
|
8455
|
-
} catch (
|
|
8456
|
-
warnCredentialPersistFailure(`file fallback (${fallbackPath()})`,
|
|
8457
|
-
throw
|
|
8455
|
+
} catch (err2) {
|
|
8456
|
+
warnCredentialPersistFailure(`file fallback (${fallbackPath()})`, err2);
|
|
8457
|
+
throw err2;
|
|
8458
8458
|
}
|
|
8459
8459
|
}
|
|
8460
8460
|
function describeCredentialSource(result) {
|
|
@@ -8518,8 +8518,8 @@ function resolveConfig() {
|
|
|
8518
8518
|
if (!archmeDaemonClientId) missing.push("ARCHME_DAEMON_CLIENT_ID");
|
|
8519
8519
|
if (!archmeMcpBaseUrl) missing.push("ARCHME_MCP_BASE_URL");
|
|
8520
8520
|
if (missing.length > 0) {
|
|
8521
|
-
const
|
|
8522
|
-
throw
|
|
8521
|
+
const err2 = new MissingConfigError({ archmeEnv, missing });
|
|
8522
|
+
throw err2;
|
|
8523
8523
|
}
|
|
8524
8524
|
return {
|
|
8525
8525
|
archmeEnv,
|
|
@@ -8596,12 +8596,12 @@ async function runLogin() {
|
|
|
8596
8596
|
let SUPABASE_URL2;
|
|
8597
8597
|
try {
|
|
8598
8598
|
SUPABASE_URL2 = resolveConfig().supabaseUrl;
|
|
8599
|
-
} catch (
|
|
8600
|
-
if (
|
|
8601
|
-
console.error(
|
|
8599
|
+
} catch (err2) {
|
|
8600
|
+
if (err2 instanceof MissingConfigError) {
|
|
8601
|
+
console.error(err2.message);
|
|
8602
8602
|
process.exit(1);
|
|
8603
8603
|
}
|
|
8604
|
-
throw
|
|
8604
|
+
throw err2;
|
|
8605
8605
|
}
|
|
8606
8606
|
function functionsBaseUrl() {
|
|
8607
8607
|
return `${SUPABASE_URL2.replace(/\/$/, "")}/functions/v1/agents-device`;
|
|
@@ -8636,7 +8636,7 @@ async function runLogin() {
|
|
|
8636
8636
|
const expiresAt = Date.now() + data.expires_in * 1e3;
|
|
8637
8637
|
let intervalMs = Math.max(data.interval, 1) * 1e3;
|
|
8638
8638
|
while (Date.now() < expiresAt) {
|
|
8639
|
-
await new Promise((
|
|
8639
|
+
await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
|
|
8640
8640
|
const poll = await postJson("/poll", {
|
|
8641
8641
|
device_code: data.device_code
|
|
8642
8642
|
});
|
|
@@ -8667,8 +8667,8 @@ var init_enroll = __esm({
|
|
|
8667
8667
|
init_credentials();
|
|
8668
8668
|
init_defaults();
|
|
8669
8669
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
8670
|
-
runLogin().catch((
|
|
8671
|
-
console.error(
|
|
8670
|
+
runLogin().catch((err2) => {
|
|
8671
|
+
console.error(err2 instanceof Error ? err2.message : err2);
|
|
8672
8672
|
process.exit(1);
|
|
8673
8673
|
});
|
|
8674
8674
|
}
|
|
@@ -8852,7 +8852,7 @@ var init_launchctl = __esm({
|
|
|
8852
8852
|
if (r.exitCode === 0 || r.exitCode === 17) return;
|
|
8853
8853
|
if (r.exitCode === 5) {
|
|
8854
8854
|
await this.opts.spawner.run("launchctl", ["bootout", SERVICE_TARGET(this.opts.uid)]);
|
|
8855
|
-
await new Promise((
|
|
8855
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
8856
8856
|
r = await bootstrap();
|
|
8857
8857
|
if (r.exitCode === 0 || r.exitCode === 17) return;
|
|
8858
8858
|
}
|
|
@@ -9222,16 +9222,16 @@ async function tailLog(opts = {}) {
|
|
|
9222
9222
|
await handle.close();
|
|
9223
9223
|
}
|
|
9224
9224
|
offset = st.size;
|
|
9225
|
-
} catch (
|
|
9226
|
-
if (
|
|
9225
|
+
} catch (err2) {
|
|
9226
|
+
if (err2.code === "ENOENT") {
|
|
9227
9227
|
out.write(`(no log file yet at ${path7})
|
|
9228
9228
|
`);
|
|
9229
9229
|
} else {
|
|
9230
|
-
throw
|
|
9230
|
+
throw err2;
|
|
9231
9231
|
}
|
|
9232
9232
|
}
|
|
9233
9233
|
if (!opts.follow) return;
|
|
9234
|
-
await new Promise((
|
|
9234
|
+
await new Promise((resolve5, reject) => {
|
|
9235
9235
|
let fileWatcher = null;
|
|
9236
9236
|
let dirWatcher = null;
|
|
9237
9237
|
const dir = dirname6(path7);
|
|
@@ -9249,9 +9249,9 @@ async function tailLog(opts = {}) {
|
|
|
9249
9249
|
});
|
|
9250
9250
|
offset = st.size;
|
|
9251
9251
|
}
|
|
9252
|
-
} catch (
|
|
9253
|
-
if (
|
|
9254
|
-
reject(
|
|
9252
|
+
} catch (err2) {
|
|
9253
|
+
if (err2.code === "ENOENT") return;
|
|
9254
|
+
reject(err2);
|
|
9255
9255
|
}
|
|
9256
9256
|
};
|
|
9257
9257
|
const attachFileWatcher = () => {
|
|
@@ -9259,9 +9259,9 @@ async function tailLog(opts = {}) {
|
|
|
9259
9259
|
fileWatcher = watch(path7, (event) => {
|
|
9260
9260
|
if (event === "change") void onChange();
|
|
9261
9261
|
});
|
|
9262
|
-
} catch (
|
|
9263
|
-
if (
|
|
9264
|
-
reject(
|
|
9262
|
+
} catch (err2) {
|
|
9263
|
+
if (err2.code !== "ENOENT") {
|
|
9264
|
+
reject(err2);
|
|
9265
9265
|
return;
|
|
9266
9266
|
}
|
|
9267
9267
|
attachDirWatcher();
|
|
@@ -9278,24 +9278,24 @@ async function tailLog(opts = {}) {
|
|
|
9278
9278
|
});
|
|
9279
9279
|
}
|
|
9280
9280
|
});
|
|
9281
|
-
} catch (
|
|
9282
|
-
reject(
|
|
9281
|
+
} catch (err2) {
|
|
9282
|
+
reject(err2);
|
|
9283
9283
|
}
|
|
9284
9284
|
};
|
|
9285
9285
|
stat(path7).then(
|
|
9286
9286
|
() => attachFileWatcher(),
|
|
9287
|
-
(
|
|
9288
|
-
if (
|
|
9287
|
+
(err2) => {
|
|
9288
|
+
if (err2.code === "ENOENT") {
|
|
9289
9289
|
attachDirWatcher();
|
|
9290
9290
|
} else {
|
|
9291
|
-
reject(
|
|
9291
|
+
reject(err2);
|
|
9292
9292
|
}
|
|
9293
9293
|
}
|
|
9294
9294
|
);
|
|
9295
9295
|
const handleSigint = () => {
|
|
9296
9296
|
fileWatcher?.close();
|
|
9297
9297
|
dirWatcher?.close();
|
|
9298
|
-
|
|
9298
|
+
resolve5();
|
|
9299
9299
|
};
|
|
9300
9300
|
process.once("SIGINT", handleSigint);
|
|
9301
9301
|
});
|
|
@@ -9495,6 +9495,83 @@ var init_env_scrub = __esm({
|
|
|
9495
9495
|
}
|
|
9496
9496
|
});
|
|
9497
9497
|
|
|
9498
|
+
// src/preflight.ts
|
|
9499
|
+
import { accessSync, constants as constants4 } from "node:fs";
|
|
9500
|
+
import { delimiter as pathDelimiter, join as join6 } from "node:path";
|
|
9501
|
+
function defaultIsExecutable(path7) {
|
|
9502
|
+
try {
|
|
9503
|
+
accessSync(path7, constants4.X_OK);
|
|
9504
|
+
return true;
|
|
9505
|
+
} catch {
|
|
9506
|
+
return false;
|
|
9507
|
+
}
|
|
9508
|
+
}
|
|
9509
|
+
function findOnPath(name, opts = {}) {
|
|
9510
|
+
const env = opts.env ?? process.env;
|
|
9511
|
+
const isExecutable = opts.isExecutable ?? defaultIsExecutable;
|
|
9512
|
+
const delim = opts.delimiter ?? pathDelimiter;
|
|
9513
|
+
const dirs = (env.PATH ?? "").split(delim).filter((d) => d.length > 0);
|
|
9514
|
+
if (dirs.length === 0) return null;
|
|
9515
|
+
const exts = env.PATHEXT ? ["", ...env.PATHEXT.split(";").filter(Boolean)] : [""];
|
|
9516
|
+
for (const dir of dirs) {
|
|
9517
|
+
for (const ext2 of exts) {
|
|
9518
|
+
const candidate = join6(dir, `${name}${ext2}`);
|
|
9519
|
+
if (isExecutable(candidate)) return candidate;
|
|
9520
|
+
}
|
|
9521
|
+
}
|
|
9522
|
+
return null;
|
|
9523
|
+
}
|
|
9524
|
+
function missingBinaryMessage(binary) {
|
|
9525
|
+
const spec = REQUIRED_BINARIES[binary];
|
|
9526
|
+
return [
|
|
9527
|
+
`\`${spec.name}\` was not found on your PATH.`,
|
|
9528
|
+
"",
|
|
9529
|
+
spec.why,
|
|
9530
|
+
"",
|
|
9531
|
+
"Install it, then re-run:",
|
|
9532
|
+
` ${spec.install}`,
|
|
9533
|
+
"",
|
|
9534
|
+
`If \`${spec.name}\` is installed somewhere unusual, add its directory to PATH.`,
|
|
9535
|
+
"Note: the daemon service captures PATH at `archme daemon start`, so after",
|
|
9536
|
+
"changing PATH you must run `archme daemon restart` for it to take effect."
|
|
9537
|
+
].join("\n");
|
|
9538
|
+
}
|
|
9539
|
+
function requireBinary(binary, opts = {}) {
|
|
9540
|
+
const cached = resolved.get(binary);
|
|
9541
|
+
if (cached) return cached;
|
|
9542
|
+
const found = findOnPath(binary, opts);
|
|
9543
|
+
if (!found) throw new MissingBinaryError(binary, missingBinaryMessage(binary));
|
|
9544
|
+
resolved.set(binary, found);
|
|
9545
|
+
return found;
|
|
9546
|
+
}
|
|
9547
|
+
var REQUIRED_BINARIES, MissingBinaryError, resolved;
|
|
9548
|
+
var init_preflight = __esm({
|
|
9549
|
+
"src/preflight.ts"() {
|
|
9550
|
+
"use strict";
|
|
9551
|
+
REQUIRED_BINARIES = {
|
|
9552
|
+
claude: {
|
|
9553
|
+
name: "claude",
|
|
9554
|
+
why: "archme runs Claude Code locally to read your diff and update the architecture.",
|
|
9555
|
+
install: "npm i -g @anthropic-ai/claude-code"
|
|
9556
|
+
},
|
|
9557
|
+
git: {
|
|
9558
|
+
name: "git",
|
|
9559
|
+
why: "archme clones and diffs repositories with git.",
|
|
9560
|
+
install: "macOS: xcode-select --install \xB7 Debian/Ubuntu: sudo apt-get install git"
|
|
9561
|
+
}
|
|
9562
|
+
};
|
|
9563
|
+
MissingBinaryError = class extends Error {
|
|
9564
|
+
binary;
|
|
9565
|
+
constructor(binary, message) {
|
|
9566
|
+
super(message);
|
|
9567
|
+
this.name = "MissingBinaryError";
|
|
9568
|
+
this.binary = binary;
|
|
9569
|
+
}
|
|
9570
|
+
};
|
|
9571
|
+
resolved = /* @__PURE__ */ new Map();
|
|
9572
|
+
}
|
|
9573
|
+
});
|
|
9574
|
+
|
|
9498
9575
|
// ../shared/daemonModel.ts
|
|
9499
9576
|
var DEFAULT_CLAUDE_MODEL;
|
|
9500
9577
|
var init_daemonModel = __esm({
|
|
@@ -9533,6 +9610,7 @@ function buildClaudeArgs(opts) {
|
|
|
9533
9610
|
return args;
|
|
9534
9611
|
}
|
|
9535
9612
|
function spawnClaude(opts) {
|
|
9613
|
+
requireBinary("claude");
|
|
9536
9614
|
const env = scrubPrivilegedEnv(process.env);
|
|
9537
9615
|
const args = buildClaudeArgs({ ...opts, model: opts.model || DEFAULT_CLAUDE_MODEL });
|
|
9538
9616
|
if (opts.streamJson && !opts.interactive) {
|
|
@@ -9571,6 +9649,7 @@ var init_spawn_claude = __esm({
|
|
|
9571
9649
|
init_execa();
|
|
9572
9650
|
init_env_scrub();
|
|
9573
9651
|
init_mcp_config();
|
|
9652
|
+
init_preflight();
|
|
9574
9653
|
init_daemonModel();
|
|
9575
9654
|
init_daemonModel();
|
|
9576
9655
|
mcpTool = (name) => `mcp__${MCP_SERVER_NAME}__${name}`;
|
|
@@ -9743,7 +9822,7 @@ var init_digestPrompt = __esm({
|
|
|
9743
9822
|
// src/daemon/digest.ts
|
|
9744
9823
|
import { mkdtemp as mkdtemp2, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
|
|
9745
9824
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
9746
|
-
import { join as
|
|
9825
|
+
import { join as join7 } from "node:path";
|
|
9747
9826
|
function isCleanDigestExit(res) {
|
|
9748
9827
|
return !res.timedOut && res.signal == null && res.exitCode === 0;
|
|
9749
9828
|
}
|
|
@@ -9777,12 +9856,12 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
|
|
|
9777
9856
|
}
|
|
9778
9857
|
let dir;
|
|
9779
9858
|
try {
|
|
9780
|
-
dir = await mkdtemp2(
|
|
9859
|
+
dir = await mkdtemp2(join7(tmpdir2(), "archme-digest-"));
|
|
9781
9860
|
let mcpConfigPath;
|
|
9782
9861
|
if (mcp) {
|
|
9783
9862
|
mcpConfigPath = mcp.configPath;
|
|
9784
9863
|
} else {
|
|
9785
|
-
mcpConfigPath =
|
|
9864
|
+
mcpConfigPath = join7(dir, "mcp.json");
|
|
9786
9865
|
await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: {} }));
|
|
9787
9866
|
}
|
|
9788
9867
|
const model = await resolveModelPin(ctx.supabase);
|
|
@@ -9813,8 +9892,8 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
|
|
|
9813
9892
|
}
|
|
9814
9893
|
await completeDigest(ctx, job.id, deviceId, narrative);
|
|
9815
9894
|
console.log(`[${job.id}] digest synthesized (${narrative.length} chars) for ${payload.projectName}`);
|
|
9816
|
-
} catch (
|
|
9817
|
-
await failDigest(ctx, job.id, deviceId,
|
|
9895
|
+
} catch (err2) {
|
|
9896
|
+
await failDigest(ctx, job.id, deviceId, err2 instanceof Error ? err2.message : String(err2));
|
|
9818
9897
|
} finally {
|
|
9819
9898
|
inFlight2?.delete(job.id);
|
|
9820
9899
|
if (dir) await rm5(dir, { recursive: true, force: true }).catch(() => {
|
|
@@ -11774,8 +11853,8 @@ function extractParticipantsFromSequence(sequence) {
|
|
|
11774
11853
|
return [...participants];
|
|
11775
11854
|
}
|
|
11776
11855
|
function componentScopeSegments(componentPath) {
|
|
11777
|
-
const
|
|
11778
|
-
return
|
|
11856
|
+
const resolved2 = resolveWaypoint(componentPath, []);
|
|
11857
|
+
return resolved2.leafId ? [...resolved2.scopePath, resolved2.leafId] : resolved2.scopePath;
|
|
11779
11858
|
}
|
|
11780
11859
|
function isPrefix(prefix, full) {
|
|
11781
11860
|
return full.length >= prefix.length && prefix.every((s, i2) => full[i2] === s);
|
|
@@ -12270,10 +12349,10 @@ async function ensureBugsFolder(client, projectId) {
|
|
|
12270
12349
|
const createdId = created?.id ?? null;
|
|
12271
12350
|
if (createdId) cache.set(projectId, createdId);
|
|
12272
12351
|
return createdId;
|
|
12273
|
-
} catch (
|
|
12352
|
+
} catch (err2) {
|
|
12274
12353
|
console.warn(
|
|
12275
12354
|
`[bugs-folder] could not resolve/create the "Bugs" folder for project ${projectId}; bug filing falls back to agent find-or-create:`,
|
|
12276
|
-
|
|
12355
|
+
err2 instanceof Error ? err2.message : err2
|
|
12277
12356
|
);
|
|
12278
12357
|
return null;
|
|
12279
12358
|
}
|
|
@@ -12334,17 +12413,17 @@ var init_lang_detect = __esm({
|
|
|
12334
12413
|
|
|
12335
12414
|
// src/analyze/tree-sitter/cache.ts
|
|
12336
12415
|
import { mkdir as mkdir5, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
12337
|
-
import { dirname as dirname7, join as
|
|
12416
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
12338
12417
|
import { homedir as homedir3 } from "node:os";
|
|
12339
12418
|
import { createHash } from "node:crypto";
|
|
12340
12419
|
function defaultCacheRoot() {
|
|
12341
|
-
return process.env.ARCHME_TOOLS_DIR ??
|
|
12420
|
+
return process.env.ARCHME_TOOLS_DIR ?? join8(homedir3(), ".archme", "tools");
|
|
12342
12421
|
}
|
|
12343
12422
|
function grammarCacheDir(cacheRoot) {
|
|
12344
|
-
return
|
|
12423
|
+
return join8(cacheRoot, "grammars");
|
|
12345
12424
|
}
|
|
12346
12425
|
function cachedGrammarPath(cacheRoot, lang, version) {
|
|
12347
|
-
return
|
|
12426
|
+
return join8(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
|
|
12348
12427
|
}
|
|
12349
12428
|
async function fileExists(path7) {
|
|
12350
12429
|
try {
|
|
@@ -12354,6 +12433,12 @@ async function fileExists(path7) {
|
|
|
12354
12433
|
return false;
|
|
12355
12434
|
}
|
|
12356
12435
|
}
|
|
12436
|
+
function describeFetchFailure(err2) {
|
|
12437
|
+
const top = err2 instanceof Error ? err2.message : String(err2);
|
|
12438
|
+
const cause = err2?.cause;
|
|
12439
|
+
const inner = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
|
|
12440
|
+
return inner && inner !== top ? `${top} (${inner})` : top;
|
|
12441
|
+
}
|
|
12357
12442
|
async function ensureGrammar(opts) {
|
|
12358
12443
|
const target = cachedGrammarPath(opts.cacheDir, opts.lang, opts.version);
|
|
12359
12444
|
if (await fileExists(target)) {
|
|
@@ -12363,11 +12448,25 @@ async function ensureGrammar(opts) {
|
|
|
12363
12448
|
const url = `${opts.urlBase}/${opts.filename}`;
|
|
12364
12449
|
process.stderr.write(`fetching grammar: ${opts.lang}@${opts.version} (${url})
|
|
12365
12450
|
`);
|
|
12366
|
-
|
|
12451
|
+
let res;
|
|
12452
|
+
try {
|
|
12453
|
+
res = await fetch(url);
|
|
12454
|
+
} catch (err2) {
|
|
12455
|
+
throw new GrammarFetchError({
|
|
12456
|
+
lang: opts.lang,
|
|
12457
|
+
version: opts.version,
|
|
12458
|
+
url,
|
|
12459
|
+
reason: describeFetchFailure(err2),
|
|
12460
|
+
cause: err2
|
|
12461
|
+
});
|
|
12462
|
+
}
|
|
12367
12463
|
if (!res.ok) {
|
|
12368
|
-
throw new
|
|
12369
|
-
|
|
12370
|
-
|
|
12464
|
+
throw new GrammarFetchError({
|
|
12465
|
+
lang: opts.lang,
|
|
12466
|
+
version: opts.version,
|
|
12467
|
+
url,
|
|
12468
|
+
reason: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`
|
|
12469
|
+
});
|
|
12371
12470
|
}
|
|
12372
12471
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
12373
12472
|
const actualHash = createHash("sha256").update(buf).digest("hex");
|
|
@@ -12379,9 +12478,41 @@ async function ensureGrammar(opts) {
|
|
|
12379
12478
|
await writeFile6(target, buf);
|
|
12380
12479
|
return { path: target, downloaded: true };
|
|
12381
12480
|
}
|
|
12481
|
+
var GrammarFetchError;
|
|
12382
12482
|
var init_cache = __esm({
|
|
12383
12483
|
"src/analyze/tree-sitter/cache.ts"() {
|
|
12384
12484
|
"use strict";
|
|
12485
|
+
GrammarFetchError = class extends Error {
|
|
12486
|
+
lang;
|
|
12487
|
+
url;
|
|
12488
|
+
constructor(opts) {
|
|
12489
|
+
super(
|
|
12490
|
+
[
|
|
12491
|
+
`Failed to download the ${opts.lang} tree-sitter grammar (${opts.lang}@${opts.version}).`,
|
|
12492
|
+
` URL: ${opts.url}`,
|
|
12493
|
+
` Reason: ${opts.reason}`,
|
|
12494
|
+
"",
|
|
12495
|
+
"archme fetches language grammars on first use and caches them under",
|
|
12496
|
+
"~/.archme/tools/grammars (override with ARCHME_TOOLS_DIR). This host could",
|
|
12497
|
+
"not reach the CDN.",
|
|
12498
|
+
"",
|
|
12499
|
+
"If you're behind a proxy, export it before re-running:",
|
|
12500
|
+
" export HTTPS_PROXY=http://proxy.example.com:8080",
|
|
12501
|
+
" export HTTP_PROXY=$HTTPS_PROXY",
|
|
12502
|
+
"",
|
|
12503
|
+
"Then warm the cache once (it is reused by every later run):",
|
|
12504
|
+
" archme analyze prefetch --all",
|
|
12505
|
+
"",
|
|
12506
|
+
"Offline/air-gapped: copy a populated ~/.archme/tools/grammars directory",
|
|
12507
|
+
"from a machine that has network access, or point ARCHME_TOOLS_DIR at one."
|
|
12508
|
+
].join("\n"),
|
|
12509
|
+
opts.cause === void 0 ? void 0 : { cause: opts.cause }
|
|
12510
|
+
);
|
|
12511
|
+
this.name = "GrammarFetchError";
|
|
12512
|
+
this.lang = opts.lang;
|
|
12513
|
+
this.url = opts.url;
|
|
12514
|
+
}
|
|
12515
|
+
};
|
|
12385
12516
|
}
|
|
12386
12517
|
});
|
|
12387
12518
|
|
|
@@ -12864,13 +12995,13 @@ async function outlineSource(source, file) {
|
|
|
12864
12995
|
confidence: 0.8,
|
|
12865
12996
|
data: { file, symbols }
|
|
12866
12997
|
});
|
|
12867
|
-
} catch (
|
|
12998
|
+
} catch (err2) {
|
|
12868
12999
|
return envelope({
|
|
12869
13000
|
tool: "file",
|
|
12870
13001
|
tier: 0,
|
|
12871
13002
|
confidence: 0.2,
|
|
12872
13003
|
data: { file, symbols: [] },
|
|
12873
|
-
warnings: [`parse failed, falling back to file tier: ${
|
|
13004
|
+
warnings: [`parse failed, falling back to file tier: ${err2.message}`]
|
|
12874
13005
|
});
|
|
12875
13006
|
}
|
|
12876
13007
|
}
|
|
@@ -12887,13 +13018,13 @@ async function runOutline(file) {
|
|
|
12887
13018
|
let source;
|
|
12888
13019
|
try {
|
|
12889
13020
|
source = await readFile6(file, "utf8");
|
|
12890
|
-
} catch (
|
|
13021
|
+
} catch (err2) {
|
|
12891
13022
|
return envelope({
|
|
12892
13023
|
tool: "file",
|
|
12893
13024
|
tier: 0,
|
|
12894
13025
|
confidence: 0,
|
|
12895
13026
|
data: { file, symbols: [] },
|
|
12896
|
-
errors: [`cannot read ${file}: ${
|
|
13027
|
+
errors: [`cannot read ${file}: ${err2.message}`]
|
|
12897
13028
|
});
|
|
12898
13029
|
}
|
|
12899
13030
|
return outlineSource(source, file);
|
|
@@ -13496,11 +13627,11 @@ var require_ignore = __commonJS({
|
|
|
13496
13627
|
|
|
13497
13628
|
// src/analyze/repo-walk.ts
|
|
13498
13629
|
import { readdir, readFile as readFile7, stat as stat4 } from "node:fs/promises";
|
|
13499
|
-
import { join as
|
|
13630
|
+
import { join as join9, relative } from "node:path";
|
|
13500
13631
|
async function loadGitignore(root) {
|
|
13501
13632
|
const ig = (0, import_ignore.default)();
|
|
13502
13633
|
try {
|
|
13503
|
-
const text = await readFile7(
|
|
13634
|
+
const text = await readFile7(join9(root, ".gitignore"), "utf8");
|
|
13504
13635
|
ig.add(text);
|
|
13505
13636
|
} catch {
|
|
13506
13637
|
ig.add(["node_modules", "dist", "build", ".next", "coverage", ".turbo"]);
|
|
@@ -13514,7 +13645,7 @@ async function* walkRepoFiles(root, filter2 = {}) {
|
|
|
13514
13645
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
13515
13646
|
for (const e of entries) {
|
|
13516
13647
|
if (ALWAYS_SKIP.has(e.name)) continue;
|
|
13517
|
-
const full =
|
|
13648
|
+
const full = join9(dir, e.name);
|
|
13518
13649
|
const rel = relative(root, full);
|
|
13519
13650
|
if (e.isDirectory()) {
|
|
13520
13651
|
if (ig.ignores(rel + "/")) continue;
|
|
@@ -13538,7 +13669,7 @@ async function collectRepoFiles(root, filter2 = {}) {
|
|
|
13538
13669
|
}
|
|
13539
13670
|
async function gitListFiles(root) {
|
|
13540
13671
|
try {
|
|
13541
|
-
await stat4(
|
|
13672
|
+
await stat4(join9(root, ".git"));
|
|
13542
13673
|
} catch {
|
|
13543
13674
|
return null;
|
|
13544
13675
|
}
|
|
@@ -13579,11 +13710,11 @@ var init_repo_walk = __esm({
|
|
|
13579
13710
|
});
|
|
13580
13711
|
|
|
13581
13712
|
// src/analyze/commands/repo.ts
|
|
13582
|
-
import { join as
|
|
13713
|
+
import { join as join10 } from "node:path";
|
|
13583
13714
|
async function* walkRepo(root, opts = {}) {
|
|
13584
13715
|
const files = await collectRepoFilesWithGit(root, opts);
|
|
13585
13716
|
for (const rel of files) {
|
|
13586
|
-
const env = await runOutline(
|
|
13717
|
+
const env = await runOutline(join10(root, rel));
|
|
13587
13718
|
yield env;
|
|
13588
13719
|
}
|
|
13589
13720
|
}
|
|
@@ -13597,9 +13728,9 @@ var init_repo = __esm({
|
|
|
13597
13728
|
|
|
13598
13729
|
// src/analyze/matchers/file.ts
|
|
13599
13730
|
import { stat as stat5 } from "node:fs/promises";
|
|
13600
|
-
import { join as
|
|
13731
|
+
import { join as join11 } from "node:path";
|
|
13601
13732
|
async function matchFileAnchor(anchor, repoRoot) {
|
|
13602
|
-
const full =
|
|
13733
|
+
const full = join11(repoRoot, anchor.path);
|
|
13603
13734
|
try {
|
|
13604
13735
|
const s = await stat5(full);
|
|
13605
13736
|
if (!s.isFile()) {
|
|
@@ -13630,9 +13761,9 @@ var init_file = __esm({
|
|
|
13630
13761
|
|
|
13631
13762
|
// src/analyze/matchers/directory.ts
|
|
13632
13763
|
import { stat as stat6 } from "node:fs/promises";
|
|
13633
|
-
import { join as
|
|
13764
|
+
import { join as join12 } from "node:path";
|
|
13634
13765
|
async function matchDirectoryAnchor(anchor, repoRoot) {
|
|
13635
|
-
const full =
|
|
13766
|
+
const full = join12(repoRoot, anchor.path);
|
|
13636
13767
|
try {
|
|
13637
13768
|
const s = await stat6(full);
|
|
13638
13769
|
if (!s.isDirectory()) {
|
|
@@ -13701,9 +13832,9 @@ var init_findSymbol = __esm({
|
|
|
13701
13832
|
|
|
13702
13833
|
// src/analyze/matchers/symbol.ts
|
|
13703
13834
|
import { stat as stat7 } from "node:fs/promises";
|
|
13704
|
-
import { join as
|
|
13835
|
+
import { join as join13 } from "node:path";
|
|
13705
13836
|
async function matchSymbolAnchor(anchor, repoRoot) {
|
|
13706
|
-
const full =
|
|
13837
|
+
const full = join13(repoRoot, anchor.path);
|
|
13707
13838
|
try {
|
|
13708
13839
|
await stat7(full);
|
|
13709
13840
|
} catch {
|
|
@@ -13805,8 +13936,8 @@ async function runPrefetch(langs) {
|
|
|
13805
13936
|
} else {
|
|
13806
13937
|
result.skipped.push(lang);
|
|
13807
13938
|
}
|
|
13808
|
-
} catch (
|
|
13809
|
-
result.errors.push({ lang, message:
|
|
13939
|
+
} catch (err2) {
|
|
13940
|
+
result.errors.push({ lang, message: err2.message });
|
|
13810
13941
|
}
|
|
13811
13942
|
}
|
|
13812
13943
|
return result;
|
|
@@ -14027,8 +14158,8 @@ async function acquireSingleInstanceLock(pidfile) {
|
|
|
14027
14158
|
await mkdir6(dirname8(pidfile), { recursive: true });
|
|
14028
14159
|
try {
|
|
14029
14160
|
await writeFile7(pidfile, String(process.pid), { flag: "wx" });
|
|
14030
|
-
} catch (
|
|
14031
|
-
if (
|
|
14161
|
+
} catch (err2) {
|
|
14162
|
+
if (err2.code !== "EEXIST") throw err2;
|
|
14032
14163
|
}
|
|
14033
14164
|
let release;
|
|
14034
14165
|
try {
|
|
@@ -14097,7 +14228,7 @@ var init_semaphore = __esm({
|
|
|
14097
14228
|
acquire() {
|
|
14098
14229
|
const slot = this.tryAcquire();
|
|
14099
14230
|
if (slot) return Promise.resolve(slot);
|
|
14100
|
-
return new Promise((
|
|
14231
|
+
return new Promise((resolve5) => this.waiters.push(resolve5));
|
|
14101
14232
|
}
|
|
14102
14233
|
makeRelease() {
|
|
14103
14234
|
let released = false;
|
|
@@ -14144,11 +14275,11 @@ async function refreshOAuthCredentialsWith(refreshToken, cfg) {
|
|
|
14144
14275
|
client_id: cfg.clientId
|
|
14145
14276
|
})
|
|
14146
14277
|
});
|
|
14147
|
-
} catch (
|
|
14278
|
+
} catch (err2) {
|
|
14148
14279
|
return {
|
|
14149
14280
|
ok: false,
|
|
14150
14281
|
permanent: false,
|
|
14151
|
-
error: `network error: ${
|
|
14282
|
+
error: `network error: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
14152
14283
|
};
|
|
14153
14284
|
}
|
|
14154
14285
|
const body = await parseJsonResponse(response);
|
|
@@ -14182,11 +14313,11 @@ async function getOAuthUserInfoWith(accessToken, cfg) {
|
|
|
14182
14313
|
function resolveUserId(userInfo2, storedUserId) {
|
|
14183
14314
|
return userInfo2.sub ?? storedUserId ?? null;
|
|
14184
14315
|
}
|
|
14185
|
-
function isAuthError(
|
|
14186
|
-
if (!
|
|
14187
|
-
const msg = ("message" in
|
|
14188
|
-
const code = ("code" in
|
|
14189
|
-
const status = "status" in
|
|
14316
|
+
function isAuthError(err2) {
|
|
14317
|
+
if (!err2 || typeof err2 !== "object") return false;
|
|
14318
|
+
const msg = ("message" in err2 ? String(err2.message) : "").toLowerCase();
|
|
14319
|
+
const code = ("code" in err2 ? String(err2.code) : "").toLowerCase();
|
|
14320
|
+
const status = "status" in err2 ? Number(err2.status) : 0;
|
|
14190
14321
|
return status === 401 || code === "pgrst301" || msg.includes("jwt expired") || msg.includes("invalid jwt") || msg.includes("not authenticated");
|
|
14191
14322
|
}
|
|
14192
14323
|
async function withAuthRetry(label, fn, reLogin) {
|
|
@@ -14195,8 +14326,8 @@ async function withAuthRetry(label, fn, reLogin) {
|
|
|
14195
14326
|
console.warn(`${label}: auth error on first attempt, re-logging in and retrying once`);
|
|
14196
14327
|
try {
|
|
14197
14328
|
await reLogin();
|
|
14198
|
-
} catch (
|
|
14199
|
-
console.error(`${label}: re-login failed:`,
|
|
14329
|
+
} catch (err2) {
|
|
14330
|
+
console.error(`${label}: re-login failed:`, err2);
|
|
14200
14331
|
return first;
|
|
14201
14332
|
}
|
|
14202
14333
|
return await fn();
|
|
@@ -14207,8 +14338,8 @@ async function fetchWithAuthRetry(label, doFetch, reLogin) {
|
|
|
14207
14338
|
console.warn(`${label}: 401 on first attempt, re-logging in and retrying once`);
|
|
14208
14339
|
try {
|
|
14209
14340
|
await reLogin();
|
|
14210
|
-
} catch (
|
|
14211
|
-
console.error(`${label}: re-login failed:`,
|
|
14341
|
+
} catch (err2) {
|
|
14342
|
+
console.error(`${label}: re-login failed:`, err2);
|
|
14212
14343
|
return first;
|
|
14213
14344
|
}
|
|
14214
14345
|
return await doFetch();
|
|
@@ -14315,8 +14446,8 @@ var init_sessionAuth = __esm({
|
|
|
14315
14446
|
for (const l of this.listeners) {
|
|
14316
14447
|
try {
|
|
14317
14448
|
l(token);
|
|
14318
|
-
} catch (
|
|
14319
|
-
console.error("sessionAuth: listener error:",
|
|
14449
|
+
} catch (err2) {
|
|
14450
|
+
console.error("sessionAuth: listener error:", err2);
|
|
14320
14451
|
}
|
|
14321
14452
|
}
|
|
14322
14453
|
}
|
|
@@ -14362,7 +14493,7 @@ var init_sessionAuth = __esm({
|
|
|
14362
14493
|
armRefreshTimer(delayMs) {
|
|
14363
14494
|
if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
14364
14495
|
this.refreshTimer = setTimeout(() => {
|
|
14365
|
-
this.runScheduledRefresh().catch((
|
|
14496
|
+
this.runScheduledRefresh().catch((err2) => console.error("auth: token refresh failed:", err2));
|
|
14366
14497
|
}, delayMs);
|
|
14367
14498
|
}
|
|
14368
14499
|
// Translate a token lifetime (seconds) into the timer delay: fire 5 min before
|
|
@@ -14404,9 +14535,9 @@ var init_sessionAuth = __esm({
|
|
|
14404
14535
|
try {
|
|
14405
14536
|
await this.coalescedRefresh("token refresh");
|
|
14406
14537
|
this.rearmFromLiveExpiry();
|
|
14407
|
-
} catch (
|
|
14408
|
-
const detail =
|
|
14409
|
-
const permanent =
|
|
14538
|
+
} catch (err2) {
|
|
14539
|
+
const detail = err2 instanceof Error ? err2.message : String(err2);
|
|
14540
|
+
const permanent = err2 instanceof LoadStoredAndRefreshError && err2.permanent;
|
|
14410
14541
|
if (permanent) {
|
|
14411
14542
|
console.error(`auth: token refresh failed permanently; auto-refresh stopped: ${detail}`);
|
|
14412
14543
|
return;
|
|
@@ -14781,15 +14912,50 @@ var init_rateLimit = __esm({
|
|
|
14781
14912
|
}
|
|
14782
14913
|
});
|
|
14783
14914
|
|
|
14915
|
+
// src/packageRoot.ts
|
|
14916
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
14917
|
+
import { dirname as dirname9, join as join14, parse } from "node:path";
|
|
14918
|
+
function defaultReadFile(path7) {
|
|
14919
|
+
return readFileSync4(path7, "utf8");
|
|
14920
|
+
}
|
|
14921
|
+
function findPackageRoot2(startDir, opts = {}) {
|
|
14922
|
+
const readFile14 = opts.readFile ?? defaultReadFile;
|
|
14923
|
+
const stopAt = parse(startDir).root;
|
|
14924
|
+
let dir = startDir;
|
|
14925
|
+
for (; ; ) {
|
|
14926
|
+
try {
|
|
14927
|
+
const parsed = JSON.parse(readFile14(join14(dir, "package.json")));
|
|
14928
|
+
if (parsed.name === PACKAGE_NAME) return dir;
|
|
14929
|
+
} catch {
|
|
14930
|
+
}
|
|
14931
|
+
if (dir === stopAt) return null;
|
|
14932
|
+
const parent = dirname9(dir);
|
|
14933
|
+
if (parent === dir) return null;
|
|
14934
|
+
dir = parent;
|
|
14935
|
+
}
|
|
14936
|
+
}
|
|
14937
|
+
var PACKAGE_NAME;
|
|
14938
|
+
var init_packageRoot = __esm({
|
|
14939
|
+
"src/packageRoot.ts"() {
|
|
14940
|
+
"use strict";
|
|
14941
|
+
PACKAGE_NAME = "@archme/cli";
|
|
14942
|
+
}
|
|
14943
|
+
});
|
|
14944
|
+
|
|
14784
14945
|
// src/daemon/version.ts
|
|
14946
|
+
var version_exports = {};
|
|
14947
|
+
__export(version_exports, {
|
|
14948
|
+
readDaemonVersion: () => readDaemonVersion
|
|
14949
|
+
});
|
|
14785
14950
|
import { readFile as readFile10 } from "node:fs/promises";
|
|
14786
|
-
import { dirname as
|
|
14951
|
+
import { dirname as dirname10, join as join15 } from "node:path";
|
|
14787
14952
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
14788
14953
|
async function readDaemonVersion() {
|
|
14789
14954
|
try {
|
|
14790
|
-
const here =
|
|
14791
|
-
const
|
|
14792
|
-
|
|
14955
|
+
const here = dirname10(fileURLToPath4(import.meta.url));
|
|
14956
|
+
const root = findPackageRoot2(here);
|
|
14957
|
+
if (!root) return null;
|
|
14958
|
+
const raw = await readFile10(join15(root, "package.json"), "utf8");
|
|
14793
14959
|
const parsed = JSON.parse(raw);
|
|
14794
14960
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
14795
14961
|
} catch {
|
|
@@ -14799,12 +14965,13 @@ async function readDaemonVersion() {
|
|
|
14799
14965
|
var init_version = __esm({
|
|
14800
14966
|
"src/daemon/version.ts"() {
|
|
14801
14967
|
"use strict";
|
|
14968
|
+
init_packageRoot();
|
|
14802
14969
|
}
|
|
14803
14970
|
});
|
|
14804
14971
|
|
|
14805
14972
|
// src/daemon/materializeDiff.ts
|
|
14806
14973
|
import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
|
|
14807
|
-
import { join as
|
|
14974
|
+
import { join as join16 } from "node:path";
|
|
14808
14975
|
import { createHash as createHash2 } from "node:crypto";
|
|
14809
14976
|
function splitPatchByFile(patch) {
|
|
14810
14977
|
if (!patch.trim()) return [];
|
|
@@ -14889,18 +15056,18 @@ function buildManifest(files, rangeLabel) {
|
|
|
14889
15056
|
}
|
|
14890
15057
|
async function materializeDiff(worktreeDir, patch, rangeLabel) {
|
|
14891
15058
|
const files = splitPatchByFile(patch);
|
|
14892
|
-
const dir =
|
|
14893
|
-
const byFileDir =
|
|
15059
|
+
const dir = join16(worktreeDir, CHANGES_DIR);
|
|
15060
|
+
const byFileDir = join16(dir, "by-file");
|
|
14894
15061
|
await mkdir7(byFileDir, { recursive: true });
|
|
14895
|
-
await writeFile8(
|
|
15062
|
+
await writeFile8(join16(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
|
|
14896
15063
|
`, "utf8");
|
|
14897
15064
|
for (const f of files) {
|
|
14898
15065
|
const rel = `by-file/${hashPath(f.path)}.diff`;
|
|
14899
15066
|
f.diffPath = rel;
|
|
14900
|
-
await writeFile8(
|
|
15067
|
+
await writeFile8(join16(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
|
|
14901
15068
|
`, "utf8");
|
|
14902
15069
|
}
|
|
14903
|
-
await writeFile8(
|
|
15070
|
+
await writeFile8(join16(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
|
|
14904
15071
|
return { dir, fileCount: files.length };
|
|
14905
15072
|
}
|
|
14906
15073
|
var CHANGES_DIR, STATUS_LABEL;
|
|
@@ -14912,6 +15079,583 @@ var init_materializeDiff = __esm({
|
|
|
14912
15079
|
}
|
|
14913
15080
|
});
|
|
14914
15081
|
|
|
15082
|
+
// src/daemon/branchMarker.ts
|
|
15083
|
+
async function readBranchMarker(client, projectId, archmeBranch) {
|
|
15084
|
+
const { data, error } = await client.from("branches").select("last_source_sha, last_source_repo").eq("project_id", projectId).eq("name", archmeBranch).is("deleted_at", null).maybeSingle();
|
|
15085
|
+
if (error || !data) return { sha: null, repo: null };
|
|
15086
|
+
return { sha: data.last_source_sha ?? null, repo: data.last_source_repo ?? null };
|
|
15087
|
+
}
|
|
15088
|
+
var init_branchMarker = __esm({
|
|
15089
|
+
"src/daemon/branchMarker.ts"() {
|
|
15090
|
+
"use strict";
|
|
15091
|
+
}
|
|
15092
|
+
});
|
|
15093
|
+
|
|
15094
|
+
// src/daemon/localRepos.ts
|
|
15095
|
+
import { chmod as chmod2, mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
|
|
15096
|
+
import { homedir as homedir4 } from "node:os";
|
|
15097
|
+
import { dirname as dirname11, join as join17 } from "node:path";
|
|
15098
|
+
function localReposPath(env = process.env) {
|
|
15099
|
+
return env.ARCHME_LOCAL_REPOS_PATH ?? join17(homedir4(), ".archme", "repos.json");
|
|
15100
|
+
}
|
|
15101
|
+
function isEntry(v) {
|
|
15102
|
+
if (!v || typeof v !== "object") return false;
|
|
15103
|
+
const o2 = v;
|
|
15104
|
+
return REQUIRED_KEYS.every((k) => typeof o2[k] === "string" && o2[k].length > 0);
|
|
15105
|
+
}
|
|
15106
|
+
function parseLocalRepos(raw) {
|
|
15107
|
+
let parsed;
|
|
15108
|
+
try {
|
|
15109
|
+
parsed = JSON.parse(raw);
|
|
15110
|
+
} catch {
|
|
15111
|
+
return [];
|
|
15112
|
+
}
|
|
15113
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
|
|
15114
|
+
const env = parsed;
|
|
15115
|
+
if (env.v !== LOCAL_REPOS_VERSION) return [];
|
|
15116
|
+
if (!Array.isArray(env.repos)) return [];
|
|
15117
|
+
return env.repos.filter(isEntry).map((e) => {
|
|
15118
|
+
const entry = {
|
|
15119
|
+
watchedRepoId: e.watchedRepoId,
|
|
15120
|
+
mirrorPath: e.mirrorPath,
|
|
15121
|
+
sourcePath: e.sourcePath,
|
|
15122
|
+
repoOwner: e.repoOwner,
|
|
15123
|
+
repoName: e.repoName
|
|
15124
|
+
};
|
|
15125
|
+
const raw2 = e;
|
|
15126
|
+
if (typeof raw2.haltedAt === "string" && raw2.haltedAt) entry.haltedAt = raw2.haltedAt;
|
|
15127
|
+
if (typeof raw2.haltReason === "string" && raw2.haltReason) entry.haltReason = raw2.haltReason;
|
|
15128
|
+
if (typeof raw2.haltCode === "string" && raw2.haltCode) entry.haltCode = raw2.haltCode;
|
|
15129
|
+
return entry;
|
|
15130
|
+
});
|
|
15131
|
+
}
|
|
15132
|
+
function serializeLocalRepos(repos) {
|
|
15133
|
+
return JSON.stringify({ v: LOCAL_REPOS_VERSION, repos }, null, 2);
|
|
15134
|
+
}
|
|
15135
|
+
function keyOf(e) {
|
|
15136
|
+
return e.watchedRepoId;
|
|
15137
|
+
}
|
|
15138
|
+
function upsertLocalRepo(repos, entry) {
|
|
15139
|
+
const k = keyOf(entry);
|
|
15140
|
+
const next = repos.filter((r) => keyOf(r) !== k);
|
|
15141
|
+
next.push(entry);
|
|
15142
|
+
return next;
|
|
15143
|
+
}
|
|
15144
|
+
function slugOf(e) {
|
|
15145
|
+
return `${e.repoOwner}/${e.repoName}`;
|
|
15146
|
+
}
|
|
15147
|
+
function findLocalReposBySlug(repos, slug) {
|
|
15148
|
+
const want = slug.toLowerCase();
|
|
15149
|
+
return repos.filter((r) => slugOf(r).toLowerCase() === want);
|
|
15150
|
+
}
|
|
15151
|
+
function removeLocalReposBySlug(repos, slug) {
|
|
15152
|
+
const want = slug.toLowerCase();
|
|
15153
|
+
return repos.filter((r) => slugOf(r).toLowerCase() !== want);
|
|
15154
|
+
}
|
|
15155
|
+
function markHalted(repos, watchedRepoId, reason, at, code) {
|
|
15156
|
+
return repos.map(
|
|
15157
|
+
(r) => r.watchedRepoId === watchedRepoId ? { ...r, haltedAt: at, haltReason: reason, haltCode: code } : r
|
|
15158
|
+
);
|
|
15159
|
+
}
|
|
15160
|
+
function clearHalt(repos, watchedRepoId) {
|
|
15161
|
+
return repos.map((r) => {
|
|
15162
|
+
if (r.watchedRepoId !== watchedRepoId) return r;
|
|
15163
|
+
const { haltedAt: _at, haltReason: _reason, haltCode: _code, ...rest } = r;
|
|
15164
|
+
return rest;
|
|
15165
|
+
});
|
|
15166
|
+
}
|
|
15167
|
+
function findLocalRepoForJob(repos, job) {
|
|
15168
|
+
if (!job.watched_repo_id) return null;
|
|
15169
|
+
const match2 = repos.find((r) => r.watchedRepoId === job.watched_repo_id);
|
|
15170
|
+
if (!match2) return null;
|
|
15171
|
+
const sameRepo = match2.repoOwner.toLowerCase() === job.repo_owner.toLowerCase() && match2.repoName.toLowerCase() === job.repo_name.toLowerCase();
|
|
15172
|
+
return sameRepo ? match2 : null;
|
|
15173
|
+
}
|
|
15174
|
+
async function loadLocalRepos(env = process.env) {
|
|
15175
|
+
try {
|
|
15176
|
+
return parseLocalRepos(await readFile11(localReposPath(env), "utf8"));
|
|
15177
|
+
} catch {
|
|
15178
|
+
return [];
|
|
15179
|
+
}
|
|
15180
|
+
}
|
|
15181
|
+
async function saveLocalRepos(repos, env = process.env) {
|
|
15182
|
+
const path7 = localReposPath(env);
|
|
15183
|
+
await mkdir8(dirname11(path7), { recursive: true, mode: 448 });
|
|
15184
|
+
await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
|
|
15185
|
+
await chmod2(path7, 384).catch(() => {
|
|
15186
|
+
});
|
|
15187
|
+
}
|
|
15188
|
+
async function registerLocalRepo(entry, env = process.env) {
|
|
15189
|
+
await saveLocalRepos(upsertLocalRepo(await loadLocalRepos(env), entry), env);
|
|
15190
|
+
}
|
|
15191
|
+
var LOCAL_REPOS_VERSION, REQUIRED_KEYS;
|
|
15192
|
+
var init_localRepos = __esm({
|
|
15193
|
+
"src/daemon/localRepos.ts"() {
|
|
15194
|
+
"use strict";
|
|
15195
|
+
LOCAL_REPOS_VERSION = 1;
|
|
15196
|
+
REQUIRED_KEYS = [
|
|
15197
|
+
"watchedRepoId",
|
|
15198
|
+
"mirrorPath",
|
|
15199
|
+
"sourcePath",
|
|
15200
|
+
"repoOwner",
|
|
15201
|
+
"repoName"
|
|
15202
|
+
];
|
|
15203
|
+
}
|
|
15204
|
+
});
|
|
15205
|
+
|
|
15206
|
+
// ../shared/commitAdvance.ts
|
|
15207
|
+
function planAdvance(input) {
|
|
15208
|
+
const { marker, upstreamTip, markerIsAncestorOfTip, tipIsAncestorOfMarker, firstParentShas } = input;
|
|
15209
|
+
if (!marker) return { kind: "bootstrap-needed" };
|
|
15210
|
+
if (marker === upstreamTip) return { kind: "up-to-date" };
|
|
15211
|
+
if (markerIsAncestorOfTip) {
|
|
15212
|
+
return firstParentShas.length === 0 ? { kind: "force-push", tip: upstreamTip } : { kind: "fast-forward", shas: firstParentShas };
|
|
15213
|
+
}
|
|
15214
|
+
if (tipIsAncestorOfMarker) return { kind: "upstream-behind", tip: upstreamTip };
|
|
15215
|
+
return { kind: "force-push", tip: upstreamTip };
|
|
15216
|
+
}
|
|
15217
|
+
var init_commitAdvance = __esm({
|
|
15218
|
+
"../shared/commitAdvance.ts"() {
|
|
15219
|
+
}
|
|
15220
|
+
});
|
|
15221
|
+
|
|
15222
|
+
// ../shared/commitGate.ts
|
|
15223
|
+
function decideGate(input) {
|
|
15224
|
+
const { targetSha, markerSha, job, elapsedMs, timeoutMs } = input;
|
|
15225
|
+
const pushedNewRef = input.pushedNewRef ?? true;
|
|
15226
|
+
const noopGraceMs = input.noopGraceMs ?? 5 * 60 * 1e3;
|
|
15227
|
+
if (markerSha && markerSha === targetSha) {
|
|
15228
|
+
return {
|
|
15229
|
+
action: "advance",
|
|
15230
|
+
code: "marker-advanced",
|
|
15231
|
+
reason: "marker advanced to target (clean completion)"
|
|
15232
|
+
};
|
|
15233
|
+
}
|
|
15234
|
+
if (job && (job.status === "failed" || job.status === "cancelled")) {
|
|
15235
|
+
return {
|
|
15236
|
+
action: "stop",
|
|
15237
|
+
code: "job-terminal",
|
|
15238
|
+
reason: `job ${job.status} after ${job.attemptCount} attempt(s); marker did not advance \u2014 re-push commit ${short(targetSha)} (fresh delivery) or admin-requeue, do not fold forward`
|
|
15239
|
+
};
|
|
15240
|
+
}
|
|
15241
|
+
if (!pushedNewRef && job === null && elapsedMs >= noopGraceMs) {
|
|
15242
|
+
return {
|
|
15243
|
+
action: "stop",
|
|
15244
|
+
code: "noop-push-no-job",
|
|
15245
|
+
reason: `no webhook fired for ${short(targetSha)} (fork ref already there) and no job appeared in ${Math.round(noopGraceMs / 1e3)}s \u2014 likely a lost webhook or rolled-back job; re-push with a fresh delivery (ref-bounce) or admin-requeue`
|
|
15246
|
+
};
|
|
15247
|
+
}
|
|
15248
|
+
if (elapsedMs >= timeoutMs) {
|
|
15249
|
+
return {
|
|
15250
|
+
action: "stop",
|
|
15251
|
+
code: "timeout",
|
|
15252
|
+
reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${short(targetSha)} (job: ${job ? job.status : "not enqueued"})`
|
|
15253
|
+
};
|
|
15254
|
+
}
|
|
15255
|
+
return {
|
|
15256
|
+
action: "wait",
|
|
15257
|
+
code: "waiting",
|
|
15258
|
+
reason: job ? `job ${job.status}` : "job not enqueued yet"
|
|
15259
|
+
};
|
|
15260
|
+
}
|
|
15261
|
+
function short(sha) {
|
|
15262
|
+
return sha.slice(0, 7);
|
|
15263
|
+
}
|
|
15264
|
+
var init_commitGate = __esm({
|
|
15265
|
+
"../shared/commitGate.ts"() {
|
|
15266
|
+
}
|
|
15267
|
+
});
|
|
15268
|
+
|
|
15269
|
+
// src/daemon/localPoll.ts
|
|
15270
|
+
function planPollTick(input) {
|
|
15271
|
+
const { marker, repo } = input;
|
|
15272
|
+
if (!marker.sha) {
|
|
15273
|
+
return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
|
|
15274
|
+
}
|
|
15275
|
+
if (marker.repo !== repo) {
|
|
15276
|
+
return {
|
|
15277
|
+
kind: "halt",
|
|
15278
|
+
code: "job-terminal",
|
|
15279
|
+
reason: `marker repo ${marker.repo ?? "n/a"} does not match ${repo} \u2014 local mode requires a github.com origin (re-run \`archme init\` if the remote changed)`
|
|
15280
|
+
};
|
|
15281
|
+
}
|
|
15282
|
+
const plan = planAdvance({
|
|
15283
|
+
marker: marker.sha,
|
|
15284
|
+
upstreamTip: input.upstreamTip,
|
|
15285
|
+
markerIsAncestorOfTip: input.markerIsAncestorOfTip,
|
|
15286
|
+
tipIsAncestorOfMarker: input.tipIsAncestorOfMarker,
|
|
15287
|
+
firstParentShas: input.firstParentShas
|
|
15288
|
+
});
|
|
15289
|
+
let target;
|
|
15290
|
+
switch (plan.kind) {
|
|
15291
|
+
case "bootstrap-needed":
|
|
15292
|
+
return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
|
|
15293
|
+
case "up-to-date":
|
|
15294
|
+
return { kind: "idle", reason: "up to date" };
|
|
15295
|
+
case "upstream-behind":
|
|
15296
|
+
return {
|
|
15297
|
+
kind: "idle",
|
|
15298
|
+
reason: `upstream is behind the marker (reset to ${short2(plan.tip)}); nothing to do`
|
|
15299
|
+
};
|
|
15300
|
+
case "force-push":
|
|
15301
|
+
target = plan.tip;
|
|
15302
|
+
break;
|
|
15303
|
+
case "fast-forward":
|
|
15304
|
+
target = plan.shas[0];
|
|
15305
|
+
break;
|
|
15306
|
+
}
|
|
15307
|
+
if (!input.jobForTarget) {
|
|
15308
|
+
return { kind: "enqueue", beforeSha: marker.sha, afterSha: target };
|
|
15309
|
+
}
|
|
15310
|
+
const decision = decideGate({
|
|
15311
|
+
targetSha: target,
|
|
15312
|
+
markerSha: marker.sha,
|
|
15313
|
+
job: { status: input.jobForTarget.status, attemptCount: input.jobForTarget.attemptCount },
|
|
15314
|
+
elapsedMs: input.nowMs - input.jobForTarget.createdAtMs,
|
|
15315
|
+
timeoutMs: input.gateTimeoutMs,
|
|
15316
|
+
// The poller enqueues directly, so the "no-op push fired no webhook" case
|
|
15317
|
+
// decideGate guards for cannot arise here.
|
|
15318
|
+
pushedNewRef: true
|
|
15319
|
+
});
|
|
15320
|
+
switch (decision.action) {
|
|
15321
|
+
case "stop":
|
|
15322
|
+
return {
|
|
15323
|
+
kind: "halt",
|
|
15324
|
+
code: decision.code,
|
|
15325
|
+
reason: localHaltReason(decision, input.jobForTarget, target)
|
|
15326
|
+
};
|
|
15327
|
+
case "advance":
|
|
15328
|
+
return { kind: "idle", reason: decision.reason };
|
|
15329
|
+
case "wait":
|
|
15330
|
+
return { kind: "idle", reason: `${short2(target)}: ${decision.reason}` };
|
|
15331
|
+
}
|
|
15332
|
+
}
|
|
15333
|
+
function localHaltReason(decision, job, targetSha) {
|
|
15334
|
+
if (decision.code !== "job-terminal" || !job) return decision.reason;
|
|
15335
|
+
return `commit ${short2(targetSha)} could not be processed (job ${job.status} after ${job.attemptCount} attempt(s)). Polling has stopped here rather than skipping it, because skipping would fold its changes into the next update.
|
|
15336
|
+
This one needs us \u2014 the failure is recorded against your account and visible to the ArchMe team. Get in touch if it is blocking you.`;
|
|
15337
|
+
}
|
|
15338
|
+
function short2(sha) {
|
|
15339
|
+
return sha.slice(0, 7);
|
|
15340
|
+
}
|
|
15341
|
+
var init_localPoll = __esm({
|
|
15342
|
+
"src/daemon/localPoll.ts"() {
|
|
15343
|
+
"use strict";
|
|
15344
|
+
init_commitAdvance();
|
|
15345
|
+
init_commitGate();
|
|
15346
|
+
}
|
|
15347
|
+
});
|
|
15348
|
+
|
|
15349
|
+
// src/daemon/defaultBranch.ts
|
|
15350
|
+
function pickDefaultBranch(input) {
|
|
15351
|
+
const onOrigin = new Set(input.remoteBranches);
|
|
15352
|
+
if (input.symbolicRef) {
|
|
15353
|
+
const slash = input.symbolicRef.indexOf("/");
|
|
15354
|
+
const named = slash >= 0 ? input.symbolicRef.slice(slash + 1) : null;
|
|
15355
|
+
if (named && onOrigin.has(named)) return named;
|
|
15356
|
+
}
|
|
15357
|
+
for (const candidate of CONVENTIONAL) {
|
|
15358
|
+
if (onOrigin.has(candidate)) return candidate;
|
|
15359
|
+
}
|
|
15360
|
+
if (input.currentBranch && onOrigin.has(input.currentBranch)) return input.currentBranch;
|
|
15361
|
+
return null;
|
|
15362
|
+
}
|
|
15363
|
+
var CONVENTIONAL;
|
|
15364
|
+
var init_defaultBranch = __esm({
|
|
15365
|
+
"src/daemon/defaultBranch.ts"() {
|
|
15366
|
+
"use strict";
|
|
15367
|
+
CONVENTIONAL = ["main", "master"];
|
|
15368
|
+
}
|
|
15369
|
+
});
|
|
15370
|
+
|
|
15371
|
+
// src/daemon/localGit.ts
|
|
15372
|
+
var localGit_exports = {};
|
|
15373
|
+
__export(localGit_exports, {
|
|
15374
|
+
createMirrorClone: () => createMirrorClone,
|
|
15375
|
+
detectDefaultBranch: () => detectDefaultBranch,
|
|
15376
|
+
fetchOrigin: () => fetchOrigin,
|
|
15377
|
+
firstParentRange: () => firstParentRange,
|
|
15378
|
+
isAncestor: () => isAncestor,
|
|
15379
|
+
readCommitMeta: () => readCommitMeta,
|
|
15380
|
+
readOriginUrl: () => readOriginUrl,
|
|
15381
|
+
revParse: () => revParse,
|
|
15382
|
+
setOriginUrl: () => setOriginUrl
|
|
15383
|
+
});
|
|
15384
|
+
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
15385
|
+
import { dirname as dirname12 } from "node:path";
|
|
15386
|
+
async function git(dir, args, opts = {}) {
|
|
15387
|
+
requireBinary("git");
|
|
15388
|
+
const res = await execa("git", args, {
|
|
15389
|
+
cwd: dir ?? void 0,
|
|
15390
|
+
reject: false,
|
|
15391
|
+
timeout: GIT_TIMEOUT_MS,
|
|
15392
|
+
// Kill the whole process group: a hung `git fetch` has spawned helper
|
|
15393
|
+
// processes (ssh, credential helpers) that keep the pipe open, so killing
|
|
15394
|
+
// git alone can leave the await pending on a dead parent.
|
|
15395
|
+
killSignal: "SIGKILL",
|
|
15396
|
+
// A large `rev-list` on a busy repo can be megabytes.
|
|
15397
|
+
maxBuffer: 64 * 1024 * 1024
|
|
15398
|
+
});
|
|
15399
|
+
const exitCode = res.exitCode ?? 1;
|
|
15400
|
+
if (!opts.allowFail && exitCode !== 0) {
|
|
15401
|
+
throw new Error(`git ${args.join(" ")} failed (${exitCode}): ${(res.stderr || "").trim()}`);
|
|
15402
|
+
}
|
|
15403
|
+
return { exitCode, stdout: res.stdout || "", stderr: res.stderr || "" };
|
|
15404
|
+
}
|
|
15405
|
+
async function createMirrorClone(sourcePath, mirrorPath, originUrl) {
|
|
15406
|
+
await mkdir9(dirname12(mirrorPath), { recursive: true });
|
|
15407
|
+
await git(null, ["clone", "--local", "--no-hardlinks", sourcePath, mirrorPath]);
|
|
15408
|
+
await git(mirrorPath, ["remote", "set-url", "origin", originUrl]);
|
|
15409
|
+
}
|
|
15410
|
+
async function setOriginUrl(dir, originUrl) {
|
|
15411
|
+
await git(dir, ["remote", "set-url", "origin", originUrl]);
|
|
15412
|
+
}
|
|
15413
|
+
async function readOriginUrl(dir) {
|
|
15414
|
+
const { exitCode, stdout } = await git(dir, ["remote", "get-url", "origin"], { allowFail: true });
|
|
15415
|
+
if (exitCode !== 0) return null;
|
|
15416
|
+
return stdout.trim() || null;
|
|
15417
|
+
}
|
|
15418
|
+
async function fetchOrigin(dir, branch) {
|
|
15419
|
+
await git(dir, ["fetch", "--quiet", "--no-tags", "origin", branch]);
|
|
15420
|
+
}
|
|
15421
|
+
async function revParse(dir, ref) {
|
|
15422
|
+
return (await git(dir, ["rev-parse", ref])).stdout.trim();
|
|
15423
|
+
}
|
|
15424
|
+
async function isAncestor(dir, a2, b) {
|
|
15425
|
+
return (await git(dir, ["merge-base", "--is-ancestor", a2, b], { allowFail: true })).exitCode === 0;
|
|
15426
|
+
}
|
|
15427
|
+
async function firstParentRange(dir, marker, tip) {
|
|
15428
|
+
const { stdout } = await git(dir, [
|
|
15429
|
+
"rev-list",
|
|
15430
|
+
"--reverse",
|
|
15431
|
+
"--first-parent",
|
|
15432
|
+
`${marker}..${tip}`
|
|
15433
|
+
]);
|
|
15434
|
+
return stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
15435
|
+
}
|
|
15436
|
+
async function readCommitMeta(dir, sha) {
|
|
15437
|
+
const { exitCode, stdout } = await git(
|
|
15438
|
+
dir,
|
|
15439
|
+
["show", "-s", "--format=%s%n%an%n%cI", sha],
|
|
15440
|
+
{ allowFail: true }
|
|
15441
|
+
);
|
|
15442
|
+
if (exitCode !== 0) return { message: "", authorName: null, committedAt: null };
|
|
15443
|
+
const [message = "", authorName = "", committedAt = ""] = stdout.split("\n");
|
|
15444
|
+
return {
|
|
15445
|
+
message: message.trim(),
|
|
15446
|
+
authorName: authorName.trim() || null,
|
|
15447
|
+
committedAt: committedAt.trim() || null
|
|
15448
|
+
};
|
|
15449
|
+
}
|
|
15450
|
+
async function detectDefaultBranch(dir) {
|
|
15451
|
+
const symbolic = await git(dir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
|
|
15452
|
+
allowFail: true
|
|
15453
|
+
});
|
|
15454
|
+
const remotes = await git(
|
|
15455
|
+
dir,
|
|
15456
|
+
["for-each-ref", "--format=%(refname:strip=3)", "refs/remotes/origin"],
|
|
15457
|
+
{ allowFail: true }
|
|
15458
|
+
);
|
|
15459
|
+
const current = await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"], { allowFail: true });
|
|
15460
|
+
return pickDefaultBranch({
|
|
15461
|
+
symbolicRef: symbolic.exitCode === 0 ? symbolic.stdout.trim() : null,
|
|
15462
|
+
remoteBranches: remotes.exitCode === 0 ? remotes.stdout.split("\n").map((l) => l.trim()).filter((l) => l && l !== "HEAD") : [],
|
|
15463
|
+
// Detached HEAD reports the literal "HEAD", which is not a branch name.
|
|
15464
|
+
currentBranch: current.exitCode === 0 && current.stdout.trim() !== "HEAD" ? current.stdout.trim() : null
|
|
15465
|
+
});
|
|
15466
|
+
}
|
|
15467
|
+
var GIT_TIMEOUT_MS;
|
|
15468
|
+
var init_localGit = __esm({
|
|
15469
|
+
"src/daemon/localGit.ts"() {
|
|
15470
|
+
"use strict";
|
|
15471
|
+
init_execa();
|
|
15472
|
+
init_preflight();
|
|
15473
|
+
init_defaultBranch();
|
|
15474
|
+
GIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
15475
|
+
}
|
|
15476
|
+
});
|
|
15477
|
+
|
|
15478
|
+
// src/daemon/localPoller.ts
|
|
15479
|
+
async function pollRepoOnce(entry, deps) {
|
|
15480
|
+
const repo = `${entry.repoOwner}/${entry.repoName}`;
|
|
15481
|
+
const gateTimeoutMs = deps.gateTimeoutMs ?? DEFAULT_GATE_TIMEOUT_MS;
|
|
15482
|
+
const row = await deps.readWatchedRepo(entry.watchedRepoId);
|
|
15483
|
+
if (!row) {
|
|
15484
|
+
return {
|
|
15485
|
+
kind: "halt",
|
|
15486
|
+
// Not a gate outcome — the repo is gone from under us. Reuses the
|
|
15487
|
+
// terminal code because the consequence is the same: this will not fix
|
|
15488
|
+
// itself, and `--resume` cannot clear it.
|
|
15489
|
+
code: "job-terminal",
|
|
15490
|
+
reason: `${repo} is no longer registered on the server (the watched repo was removed). Stop watching it here with \`archme local-watch --stop ${repo}\`.`
|
|
15491
|
+
};
|
|
15492
|
+
}
|
|
15493
|
+
if (row.sourceKind !== "local") {
|
|
15494
|
+
return {
|
|
15495
|
+
kind: "halt",
|
|
15496
|
+
code: "job-terminal",
|
|
15497
|
+
reason: `${repo} is now connected via the GitHub App, which produces its own updates. Local polling is no longer needed \u2014 \`archme local-watch --stop ${repo}\`.`
|
|
15498
|
+
};
|
|
15499
|
+
}
|
|
15500
|
+
if (!row.enabled) {
|
|
15501
|
+
return { kind: "idle", reason: `${repo}: watching is disabled on the server` };
|
|
15502
|
+
}
|
|
15503
|
+
await deps.git.fetchOrigin(entry.mirrorPath, row.repoBranch);
|
|
15504
|
+
const marker = await deps.readMarker(row.projectId, row.archmeBranch);
|
|
15505
|
+
const upstreamTip = await deps.git.revParse(entry.mirrorPath, `origin/${row.repoBranch}`);
|
|
15506
|
+
const base = {
|
|
15507
|
+
repo,
|
|
15508
|
+
marker,
|
|
15509
|
+
upstreamTip,
|
|
15510
|
+
nowMs: deps.now(),
|
|
15511
|
+
gateTimeoutMs
|
|
15512
|
+
};
|
|
15513
|
+
if (!marker.sha) {
|
|
15514
|
+
return planPollTick({
|
|
15515
|
+
...base,
|
|
15516
|
+
markerIsAncestorOfTip: false,
|
|
15517
|
+
tipIsAncestorOfMarker: false,
|
|
15518
|
+
firstParentShas: [],
|
|
15519
|
+
jobForTarget: null
|
|
15520
|
+
});
|
|
15521
|
+
}
|
|
15522
|
+
const markerIsAncestorOfTip = await deps.git.isAncestor(entry.mirrorPath, marker.sha, upstreamTip);
|
|
15523
|
+
const tipIsAncestorOfMarker = await deps.git.isAncestor(entry.mirrorPath, upstreamTip, marker.sha);
|
|
15524
|
+
const firstParentShas = markerIsAncestorOfTip ? await deps.git.firstParentRange(entry.mirrorPath, marker.sha, upstreamTip) : [];
|
|
15525
|
+
const ancestry = {
|
|
15526
|
+
...base,
|
|
15527
|
+
markerIsAncestorOfTip,
|
|
15528
|
+
tipIsAncestorOfMarker,
|
|
15529
|
+
firstParentShas
|
|
15530
|
+
};
|
|
15531
|
+
const first = planPollTick({ ...ancestry, jobForTarget: null });
|
|
15532
|
+
if (first.kind !== "enqueue") return first;
|
|
15533
|
+
const jobForTarget = await deps.readJobForSha(row.projectId, row.archmeBranch, first.afterSha);
|
|
15534
|
+
const observedAtMs = deps.firstObservedAt(
|
|
15535
|
+
`${entry.watchedRepoId}:${first.afterSha}`,
|
|
15536
|
+
deps.now()
|
|
15537
|
+
);
|
|
15538
|
+
const decision = planPollTick({
|
|
15539
|
+
...ancestry,
|
|
15540
|
+
jobForTarget: jobForTarget ? { ...jobForTarget, createdAtMs: observedAtMs } : null
|
|
15541
|
+
});
|
|
15542
|
+
if (decision.kind !== "enqueue") return decision;
|
|
15543
|
+
const result = await deps.enqueue(entry, row, decision.beforeSha, decision.afterSha);
|
|
15544
|
+
if (result.dedup) {
|
|
15545
|
+
deps.log(` ${repo}: ${short3(decision.afterSha)} already enqueued elsewhere`);
|
|
15546
|
+
} else {
|
|
15547
|
+
deps.log(` ${repo}: enqueued ${short3(decision.afterSha)} (job ${result.jobId ?? "?"})`);
|
|
15548
|
+
}
|
|
15549
|
+
return decision;
|
|
15550
|
+
}
|
|
15551
|
+
async function pollAllRepos(repos, deps) {
|
|
15552
|
+
for (const entry of repos) {
|
|
15553
|
+
const key = `${entry.repoOwner}/${entry.repoName} (${entry.watchedRepoId.slice(0, 8)})`;
|
|
15554
|
+
if (entry.haltedAt) continue;
|
|
15555
|
+
try {
|
|
15556
|
+
const action = await pollRepoOnce(entry, deps);
|
|
15557
|
+
if (action.kind === "halt") {
|
|
15558
|
+
await deps.persistHalt(entry, action.reason, action.code);
|
|
15559
|
+
deps.log(
|
|
15560
|
+
`
|
|
15561
|
+
\u26A0 local poll HALTED for ${key}: ${action.reason}
|
|
15562
|
+
No further commits will be processed for this repo.
|
|
15563
|
+
Advancing past a failed commit would fold it into the next diff and break the
|
|
15564
|
+
one-ArchMe-commit-per-git-commit guarantee, so this stops instead.
|
|
15565
|
+
When the cause is fixed: archme local-watch --resume ${entry.repoOwner}/${entry.repoName}
|
|
15566
|
+
`
|
|
15567
|
+
);
|
|
15568
|
+
}
|
|
15569
|
+
} catch (err2) {
|
|
15570
|
+
deps.log(` local poll ${key} failed (will retry): ${err2.message}`);
|
|
15571
|
+
}
|
|
15572
|
+
}
|
|
15573
|
+
}
|
|
15574
|
+
function makePollerDeps(client, log, gateTimeoutMs) {
|
|
15575
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
15576
|
+
return {
|
|
15577
|
+
git: localGit_exports,
|
|
15578
|
+
log,
|
|
15579
|
+
now: () => Date.now(),
|
|
15580
|
+
gateTimeoutMs,
|
|
15581
|
+
firstObservedAt(key, nowMs) {
|
|
15582
|
+
const existing = firstSeen.get(key);
|
|
15583
|
+
if (existing !== void 0) return existing;
|
|
15584
|
+
firstSeen.set(key, nowMs);
|
|
15585
|
+
return nowMs;
|
|
15586
|
+
},
|
|
15587
|
+
async persistHalt(entry, reason, code) {
|
|
15588
|
+
const repos = await loadLocalRepos();
|
|
15589
|
+
await saveLocalRepos(
|
|
15590
|
+
markHalted(repos, entry.watchedRepoId, reason, (/* @__PURE__ */ new Date()).toISOString(), code)
|
|
15591
|
+
);
|
|
15592
|
+
},
|
|
15593
|
+
async readWatchedRepo(watchedRepoId) {
|
|
15594
|
+
const { data, error } = await client.from("watched_repos").select("project_id, branch, commit_update_archme_branch, source_kind, enabled").eq("id", watchedRepoId).is("deleted_at", null).maybeSingle();
|
|
15595
|
+
if (error || !data) return null;
|
|
15596
|
+
const r = data;
|
|
15597
|
+
return {
|
|
15598
|
+
projectId: r.project_id,
|
|
15599
|
+
repoBranch: r.branch,
|
|
15600
|
+
// Mirrors the webhook's own fallback (`commit_update_archme_branch ??
|
|
15601
|
+
// branch`), so both producers resolve the same lane for the same row.
|
|
15602
|
+
archmeBranch: r.commit_update_archme_branch ?? r.branch,
|
|
15603
|
+
sourceKind: r.source_kind,
|
|
15604
|
+
enabled: r.enabled
|
|
15605
|
+
};
|
|
15606
|
+
},
|
|
15607
|
+
// Shared with the job runner and local-watch's readiness check: three
|
|
15608
|
+
// readers of the same marker drifting apart is how one of them ends up
|
|
15609
|
+
// deciding against a value the others don't see.
|
|
15610
|
+
readMarker: (projectId, archmeBranch) => readBranchMarker(client, projectId, archmeBranch),
|
|
15611
|
+
async readJobForSha(projectId, archmeBranch, sha) {
|
|
15612
|
+
const { data, error } = await client.from("archme_jobs").select("status, attempt_count, created_at").eq("project_id", projectId).eq("archme_branch", archmeBranch).eq("after_sha", sha).order("created_at", { ascending: false }).limit(1);
|
|
15613
|
+
if (error || !data || data.length === 0) return null;
|
|
15614
|
+
const row = data[0];
|
|
15615
|
+
return {
|
|
15616
|
+
status: row.status,
|
|
15617
|
+
attemptCount: row.attempt_count ?? 0,
|
|
15618
|
+
createdAtMs: new Date(row.created_at).getTime()
|
|
15619
|
+
};
|
|
15620
|
+
},
|
|
15621
|
+
async enqueue(entry, row, beforeSha, afterSha) {
|
|
15622
|
+
const meta = await readCommitMeta(entry.mirrorPath, afterSha);
|
|
15623
|
+
const { data, error } = await client.rpc("enqueue_local_archme_job", {
|
|
15624
|
+
p_watched_repo_id: entry.watchedRepoId,
|
|
15625
|
+
// From the ROW, not the registry: these decide which lane the job lands
|
|
15626
|
+
// in, and a stale copy would enqueue against the wrong branch.
|
|
15627
|
+
p_repo_branch: row.repoBranch,
|
|
15628
|
+
p_archme_branch: row.archmeBranch,
|
|
15629
|
+
p_before_sha: beforeSha,
|
|
15630
|
+
p_after_sha: afterSha,
|
|
15631
|
+
p_commit_message: meta.message || null,
|
|
15632
|
+
p_head_commit_timestamp: meta.committedAt,
|
|
15633
|
+
p_author_name: meta.authorName,
|
|
15634
|
+
// `author_login` is a GitHub concept with no git equivalent. The webhook
|
|
15635
|
+
// path fills it from the push payload; local mode leaves it null.
|
|
15636
|
+
p_author_login: null
|
|
15637
|
+
});
|
|
15638
|
+
if (error) throw new Error(`enqueue_local_archme_job: ${error.message}`);
|
|
15639
|
+
return data ?? {};
|
|
15640
|
+
}
|
|
15641
|
+
};
|
|
15642
|
+
}
|
|
15643
|
+
function short3(sha) {
|
|
15644
|
+
return sha.slice(0, 7);
|
|
15645
|
+
}
|
|
15646
|
+
var DEFAULT_POLL_INTERVAL_MS, DEFAULT_GATE_TIMEOUT_MS;
|
|
15647
|
+
var init_localPoller = __esm({
|
|
15648
|
+
"src/daemon/localPoller.ts"() {
|
|
15649
|
+
"use strict";
|
|
15650
|
+
init_localPoll();
|
|
15651
|
+
init_localRepos();
|
|
15652
|
+
init_branchMarker();
|
|
15653
|
+
init_localGit();
|
|
15654
|
+
DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
15655
|
+
DEFAULT_GATE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
15656
|
+
}
|
|
15657
|
+
});
|
|
15658
|
+
|
|
14915
15659
|
// src/daemon/run.ts
|
|
14916
15660
|
var run_exports = {};
|
|
14917
15661
|
__export(run_exports, {
|
|
@@ -14933,11 +15677,11 @@ __export(run_exports, {
|
|
|
14933
15677
|
});
|
|
14934
15678
|
import { createClient } from "@supabase/supabase-js";
|
|
14935
15679
|
import { randomUUID } from "node:crypto";
|
|
14936
|
-
import { mkdtemp as mkdtemp3, mkdir as
|
|
15680
|
+
import { mkdtemp as mkdtemp3, mkdir as mkdir10, writeFile as writeFile10, rm as rm6, stat as stat8, chmod as chmod3 } from "node:fs/promises";
|
|
14937
15681
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
14938
|
-
import { join as
|
|
15682
|
+
import { join as join18 } from "node:path";
|
|
14939
15683
|
import { pathToFileURL } from "node:url";
|
|
14940
|
-
import { homedir as
|
|
15684
|
+
import { homedir as homedir5 } from "node:os";
|
|
14941
15685
|
function attributeKillCause(result, recorded) {
|
|
14942
15686
|
if (recorded) return recorded;
|
|
14943
15687
|
if (result.signal === "SIGTERM") return "timeout";
|
|
@@ -14968,7 +15712,7 @@ async function lookupProjectSlug(client, projectId) {
|
|
|
14968
15712
|
return `${orgSlug}/${data.slug}`;
|
|
14969
15713
|
}
|
|
14970
15714
|
function cloneDirFor(baseDir, owner, name) {
|
|
14971
|
-
return
|
|
15715
|
+
return join18(baseDir, owner, name);
|
|
14972
15716
|
}
|
|
14973
15717
|
async function exists(p) {
|
|
14974
15718
|
try {
|
|
@@ -15013,9 +15757,9 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
|
|
|
15013
15757
|
};
|
|
15014
15758
|
}
|
|
15015
15759
|
async function buildGitEnv(credentials) {
|
|
15016
|
-
const dir = await mkdtemp3(
|
|
15017
|
-
const askpassPath =
|
|
15018
|
-
await
|
|
15760
|
+
const dir = await mkdtemp3(join18(tmpdir3(), "archme-git-askpass-"));
|
|
15761
|
+
const askpassPath = join18(dir, "askpass.sh");
|
|
15762
|
+
await writeFile10(
|
|
15019
15763
|
askpassPath,
|
|
15020
15764
|
[
|
|
15021
15765
|
"#!/bin/sh",
|
|
@@ -15028,7 +15772,7 @@ async function buildGitEnv(credentials) {
|
|
|
15028
15772
|
].join("\n"),
|
|
15029
15773
|
{ mode: 448 }
|
|
15030
15774
|
);
|
|
15031
|
-
await
|
|
15775
|
+
await chmod3(askpassPath, 448).catch(() => {
|
|
15032
15776
|
});
|
|
15033
15777
|
const gitConfigEntries = [
|
|
15034
15778
|
["credential.helper", ""],
|
|
@@ -15053,16 +15797,38 @@ async function buildGitEnv(credentials) {
|
|
|
15053
15797
|
return { env, cleanup: () => rm6(dir, { recursive: true, force: true }).catch(() => {
|
|
15054
15798
|
}) };
|
|
15055
15799
|
}
|
|
15800
|
+
async function ensureLocalCloneAndFetch(job, entry) {
|
|
15801
|
+
const dir = entry.mirrorPath;
|
|
15802
|
+
await cloneMutex.runExclusive(dir, async () => {
|
|
15803
|
+
if (!await exists(join18(dir, ".git"))) {
|
|
15804
|
+
throw new Error(
|
|
15805
|
+
`local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
|
|
15806
|
+
);
|
|
15807
|
+
}
|
|
15808
|
+
await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, reject: false });
|
|
15809
|
+
const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, reject: false });
|
|
15810
|
+
if (afterCheck.exitCode !== 0) {
|
|
15811
|
+
await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
|
|
15812
|
+
}
|
|
15813
|
+
}).catch((err2) => {
|
|
15814
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
15815
|
+
throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
|
|
15816
|
+
});
|
|
15817
|
+
return { dir, gitEnv: process.env, cleanupGitEnv: async () => {
|
|
15818
|
+
} };
|
|
15819
|
+
}
|
|
15056
15820
|
async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
15821
|
+
const localEntry = findLocalRepoForJob(await loadLocalRepos(), job);
|
|
15822
|
+
if (localEntry) return await ensureLocalCloneAndFetch(job, localEntry);
|
|
15057
15823
|
const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
|
|
15058
15824
|
if (!deviceId) throw new Error("daemon is not signed in with a registered device");
|
|
15059
15825
|
const credentials = await getRepoCredentials(ctx, job.id, deviceId);
|
|
15060
|
-
await
|
|
15826
|
+
await mkdir10(ctx.cloneBaseDir, { recursive: true });
|
|
15061
15827
|
const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
|
|
15062
15828
|
const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
|
|
15063
15829
|
await cloneMutex.runExclusive(dir, async () => {
|
|
15064
|
-
if (!await exists(
|
|
15065
|
-
await
|
|
15830
|
+
if (!await exists(join18(dir, ".git"))) {
|
|
15831
|
+
await mkdir10(join18(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
|
|
15066
15832
|
console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
|
|
15067
15833
|
await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
|
|
15068
15834
|
} else {
|
|
@@ -15083,22 +15849,22 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
|
15083
15849
|
await execa("git", ["fetch", "origin", job.before_sha], { cwd: dir, env, reject: false });
|
|
15084
15850
|
}
|
|
15085
15851
|
}
|
|
15086
|
-
}).catch(async (
|
|
15852
|
+
}).catch(async (err2) => {
|
|
15087
15853
|
await cleanupGitEnv();
|
|
15088
|
-
const msg =
|
|
15854
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
15089
15855
|
throw new Error(`GitHub App cannot access ${job.repo_owner}/${job.repo_name}: ${msg}`);
|
|
15090
15856
|
});
|
|
15091
15857
|
return { dir, gitEnv: env, cleanupGitEnv };
|
|
15092
15858
|
}
|
|
15093
15859
|
async function createJobWorktree(repoDir, job) {
|
|
15094
|
-
const parentDir = await mkdtemp3(
|
|
15095
|
-
const worktreeDir =
|
|
15860
|
+
const parentDir = await mkdtemp3(join18(tmpdir3(), "archme-job-worktree-"));
|
|
15861
|
+
const worktreeDir = join18(parentDir, "repo");
|
|
15096
15862
|
try {
|
|
15097
15863
|
await cloneMutex.runExclusive(repoDir, () => execa("git", ["worktree", "add", "--detach", worktreeDir, job.after_sha], { cwd: repoDir, reject: true }));
|
|
15098
|
-
} catch (
|
|
15864
|
+
} catch (err2) {
|
|
15099
15865
|
await rm6(parentDir, { recursive: true, force: true }).catch(() => {
|
|
15100
15866
|
});
|
|
15101
|
-
throw
|
|
15867
|
+
throw err2;
|
|
15102
15868
|
}
|
|
15103
15869
|
return {
|
|
15104
15870
|
worktreeDir,
|
|
@@ -15117,7 +15883,7 @@ function noUsableBaseError(branch, markerSha, detail) {
|
|
|
15117
15883
|
);
|
|
15118
15884
|
}
|
|
15119
15885
|
async function isShaReachable(repoDir, sha) {
|
|
15120
|
-
const { exitCode } = await execa("git", ["cat-file", "-e", sha], { cwd: repoDir, reject: false });
|
|
15886
|
+
const { exitCode } = await execa("git", ["cat-file", "-e", `${sha}^{commit}`], { cwd: repoDir, reject: false });
|
|
15121
15887
|
return exitCode === 0;
|
|
15122
15888
|
}
|
|
15123
15889
|
async function buildDiffPayload(repoDir, before, after) {
|
|
@@ -15140,7 +15906,16 @@ async function forcePushProvenance(repoDir, base, after) {
|
|
|
15140
15906
|
const forkNote = fork.trim() ? ` (history diverged at \`${fork.trim().slice(0, 8)}\`)` : "";
|
|
15141
15907
|
return `Force push: the branch tip was rewritten${forkNote}. The diff below is the exact net delta from the last reflected commit \`${base.slice(0, 8)}\` to the new tip \`${after.slice(0, 8)}\`, computed tree-to-tree \u2014 the rewrite does not affect it.`;
|
|
15142
15908
|
}
|
|
15909
|
+
function isGitSha(v) {
|
|
15910
|
+
return typeof v === "string" && GIT_SHA_RE.test(v);
|
|
15911
|
+
}
|
|
15912
|
+
function assertGitSha(v, field) {
|
|
15913
|
+
if (!isGitSha(v)) {
|
|
15914
|
+
throw new Error(`${field} ${v ?? "(null)"} is not a 40-character git SHA`);
|
|
15915
|
+
}
|
|
15916
|
+
}
|
|
15143
15917
|
async function computeDiff(job, repoDir, marker, gitEnv) {
|
|
15918
|
+
assertGitSha(job.after_sha, "after_sha");
|
|
15144
15919
|
if (!await isShaReachable(repoDir, job.after_sha)) {
|
|
15145
15920
|
throw new Error(`after_sha ${job.after_sha} is not reachable after fetch`);
|
|
15146
15921
|
}
|
|
@@ -15163,6 +15938,9 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
|
|
|
15163
15938
|
if (marker.repo !== repo) {
|
|
15164
15939
|
throw noUsableBaseError(job.archme_branch, marker.sha, `marker repo ${marker.repo ?? "n/a"} does not match job repo ${repo}`);
|
|
15165
15940
|
}
|
|
15941
|
+
if (!isGitSha(marker.sha)) {
|
|
15942
|
+
throw noUsableBaseError(job.archme_branch, marker.sha, `marker SHA ${marker.sha} is not a 40-character git SHA`);
|
|
15943
|
+
}
|
|
15166
15944
|
const base = marker.sha;
|
|
15167
15945
|
if (base === after) return "skip";
|
|
15168
15946
|
if (!await isShaReachable(repoDir, base)) {
|
|
@@ -15177,11 +15955,6 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
|
|
|
15177
15955
|
if (job.forced) payload.provenance = await forcePushProvenance(repoDir, base, after);
|
|
15178
15956
|
return payload;
|
|
15179
15957
|
}
|
|
15180
|
-
async function readBranchMarker(client, projectId, archmeBranch) {
|
|
15181
|
-
const { data, error } = await client.from("branches").select("last_source_sha, last_source_repo").eq("project_id", projectId).eq("name", archmeBranch).maybeSingle();
|
|
15182
|
-
if (error || !data) return { sha: null, repo: null };
|
|
15183
|
-
return { sha: data.last_source_sha ?? null, repo: data.last_source_repo ?? null };
|
|
15184
|
-
}
|
|
15185
15958
|
function buildDiffPrompt(job, projectSlug, diff, checkoutDir, affectedSeed, bugsFolderId) {
|
|
15186
15959
|
const sentinel = `UNTRUSTED-DIFF-${randomUUID()}`;
|
|
15187
15960
|
const seedBlock = affectedSeed ? `
|
|
@@ -15515,9 +16288,9 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
|
|
|
15515
16288
|
};
|
|
15516
16289
|
}
|
|
15517
16290
|
lastDetail = "MCP returned 401 \u2014 token rejected by JWKS verification";
|
|
15518
|
-
} catch (
|
|
16291
|
+
} catch (err2) {
|
|
15519
16292
|
lastStatus = null;
|
|
15520
|
-
lastDetail = `network error reaching MCP: ${
|
|
16293
|
+
lastDetail = `network error reaching MCP: ${err2 instanceof Error ? err2.message : String(err2)}`;
|
|
15521
16294
|
}
|
|
15522
16295
|
}
|
|
15523
16296
|
if (lastStatus === 401) {
|
|
@@ -15765,7 +16538,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15765
16538
|
const affected = resolveAffected(seedArch, diff.changedFiles, changedSymbolsByFile);
|
|
15766
16539
|
affectedForComplete = affected;
|
|
15767
16540
|
const symbolsInFile = async (file) => {
|
|
15768
|
-
const outline = await runOutline(
|
|
16541
|
+
const outline = await runOutline(join18(worktreeDir, file));
|
|
15769
16542
|
if (outline.tier !== 2) return null;
|
|
15770
16543
|
return outline.data.symbols.map((s) => s.name);
|
|
15771
16544
|
};
|
|
@@ -15893,9 +16666,9 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15893
16666
|
true,
|
|
15894
16667
|
affectedFieldForComplete(job.source_mode, affectedForComplete)
|
|
15895
16668
|
);
|
|
15896
|
-
} catch (
|
|
16669
|
+
} catch (err2) {
|
|
15897
16670
|
if (cancelled.has(job.id)) {
|
|
15898
|
-
console.log(`[${job.id}] cancelled, swallowing error: ${
|
|
16671
|
+
console.log(`[${job.id}] cancelled, swallowing error: ${err2 instanceof Error ? err2.message : err2}`);
|
|
15899
16672
|
await rpcRollback(ctx, job.id, deviceId);
|
|
15900
16673
|
return;
|
|
15901
16674
|
}
|
|
@@ -15903,7 +16676,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15903
16676
|
console.log(`[${job.id}] shutdown \u2014 routed to retry by shutdown handler`);
|
|
15904
16677
|
return;
|
|
15905
16678
|
}
|
|
15906
|
-
const msg =
|
|
16679
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
15907
16680
|
const haystack = `${msg}
|
|
15908
16681
|
${logs.stderr}`;
|
|
15909
16682
|
const plan = rateLimitDeferPlanForExit({ message: msg, stderr: logs.stderr, stdout: logs.stdout });
|
|
@@ -15975,7 +16748,7 @@ async function dispatch(sched) {
|
|
|
15975
16748
|
}
|
|
15976
16749
|
ctxByJob.set(claimed.job.id, claimed.ctx);
|
|
15977
16750
|
deviceByJob.set(claimed.job.id, claimed.deviceId);
|
|
15978
|
-
const p = sched.run(claimed.ctx, claimed.job, claimed.deviceId).catch((
|
|
16751
|
+
const p = sched.run(claimed.ctx, claimed.job, claimed.deviceId).catch((err2) => console.error(`[${claimed.job.id}] run error:`, err2 instanceof Error ? err2.message : err2)).finally(() => {
|
|
15979
16752
|
ctxByJob.delete(claimed.job.id);
|
|
15980
16753
|
deviceByJob.delete(claimed.job.id);
|
|
15981
16754
|
slot();
|
|
@@ -16011,7 +16784,7 @@ function subscribe(client, sched) {
|
|
|
16011
16784
|
"postgres_changes",
|
|
16012
16785
|
{ event: "INSERT", schema: "public", table: "archme_jobs" },
|
|
16013
16786
|
(payload) => {
|
|
16014
|
-
if (payload.new.status === "pending") dispatch(sched).catch((
|
|
16787
|
+
if (payload.new.status === "pending") dispatch(sched).catch((err2) => console.error("dispatch:", err2));
|
|
16015
16788
|
}
|
|
16016
16789
|
).on(
|
|
16017
16790
|
"postgres_changes",
|
|
@@ -16028,12 +16801,12 @@ function subscribe(client, sched) {
|
|
|
16028
16801
|
console.log(`[${payload.new.id}] received cancellation (pre-spawn), will abort`);
|
|
16029
16802
|
}
|
|
16030
16803
|
} else if (payload.new.status === "pending") {
|
|
16031
|
-
dispatch(sched).catch((
|
|
16804
|
+
dispatch(sched).catch((err2) => console.error("dispatch:", err2));
|
|
16032
16805
|
}
|
|
16033
16806
|
}
|
|
16034
16807
|
).subscribe((status) => {
|
|
16035
16808
|
console.log(`realtime channel: ${status}`);
|
|
16036
|
-
if (status === "SUBSCRIBED") dispatch(sched).catch((
|
|
16809
|
+
if (status === "SUBSCRIBED") dispatch(sched).catch((err2) => console.error("startup dispatch:", err2));
|
|
16037
16810
|
});
|
|
16038
16811
|
return () => {
|
|
16039
16812
|
client.removeChannel(channel);
|
|
@@ -16074,9 +16847,9 @@ async function signInAccount(creds, makeAuth) {
|
|
|
16074
16847
|
active = { auth, cred, result };
|
|
16075
16848
|
usedFallback = attempts > 1;
|
|
16076
16849
|
break;
|
|
16077
|
-
} catch (
|
|
16850
|
+
} catch (err2) {
|
|
16078
16851
|
auth.clearRefreshTimer();
|
|
16079
|
-
lastErr =
|
|
16852
|
+
lastErr = err2;
|
|
16080
16853
|
}
|
|
16081
16854
|
}
|
|
16082
16855
|
if (active) {
|
|
@@ -16093,19 +16866,19 @@ async function runDaemon() {
|
|
|
16093
16866
|
let lockHandle = null;
|
|
16094
16867
|
try {
|
|
16095
16868
|
lockHandle = await acquireSingleInstanceLock(resolvePidfilePath(process.env));
|
|
16096
|
-
} catch (
|
|
16097
|
-
if (
|
|
16098
|
-
console.error(String(
|
|
16869
|
+
} catch (err2) {
|
|
16870
|
+
if (err2 instanceof SingleInstanceLockedError) {
|
|
16871
|
+
console.error(String(err2.message));
|
|
16099
16872
|
process.exit(64);
|
|
16100
16873
|
}
|
|
16101
|
-
throw
|
|
16874
|
+
throw err2;
|
|
16102
16875
|
}
|
|
16103
16876
|
let cfg;
|
|
16104
16877
|
try {
|
|
16105
16878
|
cfg = resolveConfig();
|
|
16106
|
-
} catch (
|
|
16107
|
-
if (
|
|
16108
|
-
console.error(
|
|
16879
|
+
} catch (err2) {
|
|
16880
|
+
if (err2 instanceof MissingConfigError) {
|
|
16881
|
+
console.error(err2.message);
|
|
16109
16882
|
if (lockHandle) {
|
|
16110
16883
|
try {
|
|
16111
16884
|
await lockHandle.release();
|
|
@@ -16114,7 +16887,7 @@ async function runDaemon() {
|
|
|
16114
16887
|
}
|
|
16115
16888
|
process.exit(1);
|
|
16116
16889
|
}
|
|
16117
|
-
throw
|
|
16890
|
+
throw err2;
|
|
16118
16891
|
}
|
|
16119
16892
|
SUPABASE_URL = cfg.supabaseUrl;
|
|
16120
16893
|
SUPABASE_ANON_KEY = cfg.supabaseAnonKey;
|
|
@@ -16148,9 +16921,9 @@ async function runDaemon() {
|
|
|
16148
16921
|
let credentials;
|
|
16149
16922
|
try {
|
|
16150
16923
|
credentials = assertSingleAccount(loaded);
|
|
16151
|
-
} catch (
|
|
16152
|
-
if (
|
|
16153
|
-
console.error(
|
|
16924
|
+
} catch (err2) {
|
|
16925
|
+
if (err2 instanceof MultipleAccountsError) {
|
|
16926
|
+
console.error(err2.message);
|
|
16154
16927
|
if (lockHandle) {
|
|
16155
16928
|
try {
|
|
16156
16929
|
await lockHandle.release();
|
|
@@ -16159,7 +16932,7 @@ async function runDaemon() {
|
|
|
16159
16932
|
}
|
|
16160
16933
|
process.exit(78);
|
|
16161
16934
|
}
|
|
16162
|
-
throw
|
|
16935
|
+
throw err2;
|
|
16163
16936
|
}
|
|
16164
16937
|
const devices = buildDevices(credentials);
|
|
16165
16938
|
console.log(` booting 1 session across ${devices.length} org(s)`);
|
|
@@ -16251,17 +17024,50 @@ async function runDaemon() {
|
|
|
16251
17024
|
console.warn(`daemon_pauses lookup failed (proceeding unpaused): ${e.message}`);
|
|
16252
17025
|
}
|
|
16253
17026
|
const sweepTimer = setInterval(
|
|
16254
|
-
() => dispatch(sched).catch((
|
|
17027
|
+
() => dispatch(sched).catch((err2) => console.error("safety sweep:", err2)),
|
|
16255
17028
|
SAFETY_SWEEP_MS
|
|
16256
17029
|
);
|
|
16257
|
-
const
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
17030
|
+
const localPollDeps = makePollerDeps(client, (m) => console.log(m));
|
|
17031
|
+
const localPollIntervalMs = Number.parseInt(process.env.ARCHME_LOCAL_POLL_INTERVAL_MS ?? "", 10);
|
|
17032
|
+
const pollEveryMs = Number.isFinite(localPollIntervalMs) && localPollIntervalMs > 0 ? localPollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
|
|
17033
|
+
let localPollInFlight = false;
|
|
17034
|
+
const runLocalPoll = async () => {
|
|
17035
|
+
if (localPollInFlight) return;
|
|
17036
|
+
localPollInFlight = true;
|
|
17037
|
+
try {
|
|
17038
|
+
const repos = await loadLocalRepos();
|
|
17039
|
+
if (repos.length === 0) return;
|
|
17040
|
+
await pollAllRepos(repos, localPollDeps);
|
|
17041
|
+
} finally {
|
|
17042
|
+
localPollInFlight = false;
|
|
17043
|
+
}
|
|
17044
|
+
};
|
|
17045
|
+
const initialLocalRepos = await loadLocalRepos();
|
|
17046
|
+
if (initialLocalRepos.length > 0) {
|
|
17047
|
+
const halted = initialLocalRepos.filter((r) => r.haltedAt);
|
|
17048
|
+
console.log(
|
|
17049
|
+
` local-poll: ${initialLocalRepos.length} repo(s) every ${Math.round(pollEveryMs / 1e3)}s`
|
|
17050
|
+
);
|
|
17051
|
+
for (const r of halted) {
|
|
17052
|
+
console.log(
|
|
17053
|
+
` \u26A0 ${r.repoOwner}/${r.repoName} is HALTED (${r.haltReason ?? "unknown reason"}) \u2014 resume with \`archme local-watch --resume ${r.repoOwner}/${r.repoName}\``
|
|
17054
|
+
);
|
|
17055
|
+
}
|
|
17056
|
+
}
|
|
17057
|
+
const localPollTimer = setInterval(
|
|
17058
|
+
() => void runLocalPoll().catch((err2) => console.error("local poll:", err2)),
|
|
17059
|
+
pollEveryMs
|
|
17060
|
+
);
|
|
17061
|
+
void runLocalPoll().catch((err2) => console.error("local poll:", err2));
|
|
17062
|
+
const shutdown = async (signal) => {
|
|
17063
|
+
if (shuttingDown) return;
|
|
17064
|
+
shuttingDown = true;
|
|
17065
|
+
console.log(`
|
|
17066
|
+
${signal} received, shutting down`);
|
|
17067
|
+
for (const ctx of sessions) ctx.auth.clearRefreshTimer();
|
|
17068
|
+
clearInterval(sweepTimer);
|
|
17069
|
+
clearInterval(localPollTimer);
|
|
17070
|
+
for (const u2 of unsubscribers) u2();
|
|
16265
17071
|
const SHUTDOWN_DEADLINE_MS = 3e4;
|
|
16266
17072
|
const deadline = setTimeout(() => {
|
|
16267
17073
|
console.error("shutdown deadline exceeded, forcing exit");
|
|
@@ -16300,7 +17106,7 @@ ${signal} received, shutting down`);
|
|
|
16300
17106
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
16301
17107
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
16302
17108
|
}
|
|
16303
|
-
var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, INLINE_PATCH_BYTE_CAP, CURATOR_PREAMBLE, CORE_PRINCIPLES, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
|
|
17109
|
+
var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, SUPABASE_URL, SUPABASE_ANON_KEY, ARCHME_DAEMON_CLIENT_ID, ARCHME_MCP_BASE_URL, CLONE_BASE_DIR, DAEMON_ID, LEASE_SECONDS, inFlight, cancelled, killReasons, activeCleanups, cloneMutex, shuttingDown, daemonVersion, pauseUntilMs, resumeTimer, currentSched, MAX_CONCURRENT_JOBS, ctxByJob, deviceByJob, DEFAULT_CLONE_BASE_DIR, INLINE_PATCH_BYTE_CAP, GIT_SHA_RE, CURATOR_PREAMBLE, CORE_PRINCIPLES, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
|
|
16304
17110
|
var init_run = __esm({
|
|
16305
17111
|
"src/daemon/run.ts"() {
|
|
16306
17112
|
"use strict";
|
|
@@ -16331,6 +17137,10 @@ var init_run = __esm({
|
|
|
16331
17137
|
init_rateLimit();
|
|
16332
17138
|
init_version();
|
|
16333
17139
|
init_materializeDiff();
|
|
17140
|
+
init_branchMarker();
|
|
17141
|
+
init_branchMarker();
|
|
17142
|
+
init_localRepos();
|
|
17143
|
+
init_localPoller();
|
|
16334
17144
|
HEARTBEAT_INTERVAL_MS = 6e4;
|
|
16335
17145
|
CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
16336
17146
|
CLAUDE_LARGE_DIFF_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
@@ -16351,8 +17161,9 @@ var init_run = __esm({
|
|
|
16351
17161
|
MAX_CONCURRENT_JOBS = Math.max(1, Number(process.env.MAX_CONCURRENT_JOBS) || 3);
|
|
16352
17162
|
ctxByJob = /* @__PURE__ */ new Map();
|
|
16353
17163
|
deviceByJob = /* @__PURE__ */ new Map();
|
|
16354
|
-
DEFAULT_CLONE_BASE_DIR = `${
|
|
17164
|
+
DEFAULT_CLONE_BASE_DIR = `${homedir5()}/.archme-job-runner/clones`;
|
|
16355
17165
|
INLINE_PATCH_BYTE_CAP = 2e5;
|
|
17166
|
+
GIT_SHA_RE = /^[0-9a-f]{40}$/;
|
|
16356
17167
|
CURATOR_PREAMBLE = `## Your role
|
|
16357
17168
|
|
|
16358
17169
|
You are curating the architecture diagram, not just transcribing the change. The diagram is an abstraction read by engineers and technical leaders to understand the codebase at a higher level than the code itself. Two things have to stay true: (A) it reflects the reality of the code, and (B) it is a useful abstraction of it. Use judgement on the altitude \u2014 pick the level of detail that helps a reader build a mental model, not one that mirrors every function and file.`;
|
|
@@ -16517,8 +17328,8 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
16517
17328
|
}
|
|
16518
17329
|
};
|
|
16519
17330
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
16520
|
-
runDaemon().catch((
|
|
16521
|
-
console.error("fatal:",
|
|
17331
|
+
runDaemon().catch((err2) => {
|
|
17332
|
+
console.error("fatal:", err2);
|
|
16522
17333
|
process.exit(1);
|
|
16523
17334
|
});
|
|
16524
17335
|
}
|
|
@@ -16557,12 +17368,12 @@ async function dispatchDaemon(route2) {
|
|
|
16557
17368
|
console.log(`Logs: ${out.logPath}`);
|
|
16558
17369
|
console.log(`State: ${out.result.actions.join(" \u2192 ")}`);
|
|
16559
17370
|
return 0;
|
|
16560
|
-
} catch (
|
|
16561
|
-
if (
|
|
16562
|
-
console.error(String(
|
|
17371
|
+
} catch (err2) {
|
|
17372
|
+
if (err2 instanceof NotSignedInError) {
|
|
17373
|
+
console.error(String(err2.message));
|
|
16563
17374
|
return 75;
|
|
16564
17375
|
}
|
|
16565
|
-
console.error(String(
|
|
17376
|
+
console.error(String(err2));
|
|
16566
17377
|
return 1;
|
|
16567
17378
|
}
|
|
16568
17379
|
}
|
|
@@ -16575,13 +17386,13 @@ async function dispatchDaemon(route2) {
|
|
|
16575
17386
|
case "restart": {
|
|
16576
17387
|
try {
|
|
16577
17388
|
await ensureStartPrerequisites({ noLogin: route2.noLogin });
|
|
16578
|
-
} catch (
|
|
16579
|
-
if (
|
|
16580
|
-
console.error(String(
|
|
17389
|
+
} catch (err2) {
|
|
17390
|
+
if (err2 instanceof NotSignedInError) {
|
|
17391
|
+
console.error(String(err2.message));
|
|
16581
17392
|
console.error("(daemon left running unchanged)");
|
|
16582
17393
|
return 75;
|
|
16583
17394
|
}
|
|
16584
|
-
console.error(String(
|
|
17395
|
+
console.error(String(err2));
|
|
16585
17396
|
console.error("(daemon left running unchanged)");
|
|
16586
17397
|
return 1;
|
|
16587
17398
|
}
|
|
@@ -16669,9 +17480,9 @@ async function defaultLoadSupabaseState(_deviceId) {
|
|
|
16669
17480
|
async function defaultLoadUptime(pid) {
|
|
16670
17481
|
if (process.platform === "linux") {
|
|
16671
17482
|
try {
|
|
16672
|
-
const { readFile:
|
|
16673
|
-
const
|
|
16674
|
-
const parts =
|
|
17483
|
+
const { readFile: readFile14 } = await import("node:fs/promises");
|
|
17484
|
+
const stat11 = await readFile14(`/proc/${pid}/stat`, "utf8");
|
|
17485
|
+
const parts = stat11.split(") ").pop()?.split(" ") ?? [];
|
|
16675
17486
|
const ticks = Number(parts[19]);
|
|
16676
17487
|
if (!Number.isFinite(ticks)) return null;
|
|
16677
17488
|
const { readFile: rf } = await import("node:fs/promises");
|
|
@@ -16738,8 +17549,8 @@ async function createAuthedClient(opts) {
|
|
|
16738
17549
|
let loaded;
|
|
16739
17550
|
try {
|
|
16740
17551
|
loaded = await loadStoredAndRefresh();
|
|
16741
|
-
} catch (
|
|
16742
|
-
const msg =
|
|
17552
|
+
} catch (err2) {
|
|
17553
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
16743
17554
|
throw new Error(`${msg}
|
|
16744
17555
|
\u2192 run \`archme login\` first`);
|
|
16745
17556
|
}
|
|
@@ -16770,9 +17581,9 @@ function deriveWebOrigin(apiUrl, onMalformed = "throw") {
|
|
|
16770
17581
|
let parsed;
|
|
16771
17582
|
try {
|
|
16772
17583
|
parsed = new URL(apiUrl);
|
|
16773
|
-
} catch (
|
|
17584
|
+
} catch (err2) {
|
|
16774
17585
|
if (onMalformed === "throw") {
|
|
16775
|
-
throw new Error(`could not parse API URL \`${apiUrl}\`: ${
|
|
17586
|
+
throw new Error(`could not parse API URL \`${apiUrl}\`: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
16776
17587
|
}
|
|
16777
17588
|
return "http://localhost:5173";
|
|
16778
17589
|
}
|
|
@@ -16981,8 +17792,8 @@ var init_transcript_signals = __esm({
|
|
|
16981
17792
|
});
|
|
16982
17793
|
|
|
16983
17794
|
// src/scan/report.ts
|
|
16984
|
-
import { mkdir as
|
|
16985
|
-
import { dirname as
|
|
17795
|
+
import { mkdir as mkdir11, writeFile as writeFile11 } from "node:fs/promises";
|
|
17796
|
+
import { dirname as dirname13 } from "node:path";
|
|
16986
17797
|
async function buildReport(input) {
|
|
16987
17798
|
return {
|
|
16988
17799
|
schema_version: REPORT_SCHEMA_VERSION,
|
|
@@ -17016,10 +17827,10 @@ async function buildReport(input) {
|
|
|
17016
17827
|
};
|
|
17017
17828
|
}
|
|
17018
17829
|
async function writeReport(reportPath, orphansTxtPath, report, orphansFull) {
|
|
17019
|
-
await
|
|
17020
|
-
await
|
|
17021
|
-
await
|
|
17022
|
-
await
|
|
17830
|
+
await mkdir11(dirname13(reportPath), { recursive: true });
|
|
17831
|
+
await writeFile11(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
17832
|
+
await mkdir11(dirname13(orphansTxtPath), { recursive: true });
|
|
17833
|
+
await writeFile11(
|
|
17023
17834
|
orphansTxtPath,
|
|
17024
17835
|
orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
|
|
17025
17836
|
"utf8"
|
|
@@ -17044,10 +17855,26 @@ var init_github_remote = __esm({
|
|
|
17044
17855
|
}
|
|
17045
17856
|
});
|
|
17046
17857
|
|
|
17858
|
+
// src/scan/claimDefaultProject.ts
|
|
17859
|
+
function isClaimableDefaultProject(c3) {
|
|
17860
|
+
if (c3.slug !== SIGNUP_DEFAULT_SLUG) return false;
|
|
17861
|
+
if (c3.name.trim().toLowerCase() !== SIGNUP_DEFAULT_NAME) return false;
|
|
17862
|
+
if (c3.fileCount === null || c3.commitCount === null) return false;
|
|
17863
|
+
return c3.fileCount === 0 && c3.commitCount === 0;
|
|
17864
|
+
}
|
|
17865
|
+
var SIGNUP_DEFAULT_SLUG, SIGNUP_DEFAULT_NAME;
|
|
17866
|
+
var init_claimDefaultProject = __esm({
|
|
17867
|
+
"src/scan/claimDefaultProject.ts"() {
|
|
17868
|
+
"use strict";
|
|
17869
|
+
SIGNUP_DEFAULT_SLUG = "default";
|
|
17870
|
+
SIGNUP_DEFAULT_NAME = "default";
|
|
17871
|
+
}
|
|
17872
|
+
});
|
|
17873
|
+
|
|
17047
17874
|
// src/scan/run-scan.ts
|
|
17048
17875
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
17049
|
-
import { mkdir as
|
|
17050
|
-
import { basename as basename2, join as
|
|
17876
|
+
import { mkdir as mkdir12, readFile as readFile12 } from "node:fs/promises";
|
|
17877
|
+
import { basename as basename2, join as join19 } from "node:path";
|
|
17051
17878
|
function buildBootstrapPrompt(opts = {}) {
|
|
17052
17879
|
const { promptVariant, interactive, briefing } = opts;
|
|
17053
17880
|
let base = interactive ? briefing ? `${BOOTSTRAP_INTERACTIVE}
|
|
@@ -17132,6 +17959,8 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
|
|
|
17132
17959
|
if (!membership) {
|
|
17133
17960
|
throw new Error(`you are not a member of org \`${orgSlug}\`; cannot create project there`);
|
|
17134
17961
|
}
|
|
17962
|
+
const claimed = await claimSignupDefaultProject(supabase, org, projectSlug);
|
|
17963
|
+
if (claimed) return claimed;
|
|
17135
17964
|
const { data: created, error: cErr } = await supabase.from("projects").insert({ name: projectSlug, org_id: org.id, slug: projectSlug }).select("id").single();
|
|
17136
17965
|
if (cErr) {
|
|
17137
17966
|
if (cErr.code === "23505" || /duplicate key/i.test(cErr.message ?? "")) {
|
|
@@ -17164,6 +17993,43 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
|
|
|
17164
17993
|
created: true
|
|
17165
17994
|
};
|
|
17166
17995
|
}
|
|
17996
|
+
async function claimSignupDefaultProject(supabase, org, projectSlug) {
|
|
17997
|
+
if (projectSlug === SIGNUP_DEFAULT_SLUG) return null;
|
|
17998
|
+
try {
|
|
17999
|
+
const { data: candidate } = await supabase.from("projects").select("id, slug, name").eq("org_id", org.id).eq("slug", SIGNUP_DEFAULT_SLUG).is("deleted_at", null).maybeSingle();
|
|
18000
|
+
if (!candidate) return null;
|
|
18001
|
+
const row = candidate;
|
|
18002
|
+
const [{ count: fileCount }, { count: commitCount }] = await Promise.all([
|
|
18003
|
+
supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", row.id),
|
|
18004
|
+
supabase.from("commits").select("id", { count: "exact", head: true }).eq("project_id", row.id)
|
|
18005
|
+
]);
|
|
18006
|
+
if (!isClaimableDefaultProject({
|
|
18007
|
+
slug: row.slug,
|
|
18008
|
+
name: row.name,
|
|
18009
|
+
fileCount: fileCount ?? null,
|
|
18010
|
+
commitCount: commitCount ?? null
|
|
18011
|
+
})) {
|
|
18012
|
+
return null;
|
|
18013
|
+
}
|
|
18014
|
+
const { data: branches } = await supabase.from("branches").select("name, is_default").eq("project_id", row.id).is("deleted_at", null);
|
|
18015
|
+
const existing = branches ?? [];
|
|
18016
|
+
if (existing.length === 0) {
|
|
18017
|
+
const { error: bErr } = await supabase.from("branches").insert({ project_id: row.id, name: "main", is_default: true });
|
|
18018
|
+
if (bErr) return null;
|
|
18019
|
+
}
|
|
18020
|
+
const { error: renameErr } = await supabase.from("projects").update({ name: projectSlug, slug: projectSlug }).eq("id", row.id);
|
|
18021
|
+
if (renameErr) return null;
|
|
18022
|
+
return {
|
|
18023
|
+
orgSlug: org.slug,
|
|
18024
|
+
projectSlug,
|
|
18025
|
+
projectId: row.id,
|
|
18026
|
+
defaultBranch: existing.find((b) => b.is_default)?.name ?? "main",
|
|
18027
|
+
created: true
|
|
18028
|
+
};
|
|
18029
|
+
} catch {
|
|
18030
|
+
return null;
|
|
18031
|
+
}
|
|
18032
|
+
}
|
|
17167
18033
|
async function fetchExistingProject(supabase, org, projectSlug) {
|
|
17168
18034
|
const project = await mustQuery(
|
|
17169
18035
|
supabase.from("projects").select("id, slug").eq("org_id", org.id).eq("slug", projectSlug).is("deleted_at", null).maybeSingle(),
|
|
@@ -17269,8 +18135,8 @@ async function runScan(opts, deps = {}) {
|
|
|
17269
18135
|
let headSha;
|
|
17270
18136
|
try {
|
|
17271
18137
|
headSha = await repoHeadSha(opts.repoPath);
|
|
17272
|
-
} catch (
|
|
17273
|
-
const msg =
|
|
18138
|
+
} catch (err2) {
|
|
18139
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
17274
18140
|
throw new Error(`could not read git HEAD at \`${opts.repoPath}\` (is this a git repo?): ${msg}`);
|
|
17275
18141
|
}
|
|
17276
18142
|
let fullSlug;
|
|
@@ -17287,19 +18153,19 @@ async function runScan(opts, deps = {}) {
|
|
|
17287
18153
|
}
|
|
17288
18154
|
fullSlug = defaultProjectSlug(orgSlug, opts.repoPath);
|
|
17289
18155
|
}
|
|
17290
|
-
const
|
|
17291
|
-
if (
|
|
17292
|
-
log(` project: ${
|
|
18156
|
+
const resolved2 = await resolveOrCreateProject(supabase, userId, fullSlug);
|
|
18157
|
+
if (resolved2.created) {
|
|
18158
|
+
log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (created)`);
|
|
17293
18159
|
} else {
|
|
17294
|
-
log(` project: ${
|
|
18160
|
+
log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (using existing \u2014 pass --project <unique-slug> to start fresh)`);
|
|
17295
18161
|
}
|
|
17296
|
-
log(` default branch: ${
|
|
18162
|
+
log(` default branch: ${resolved2.defaultBranch}`);
|
|
17297
18163
|
const branchName = opts.branch ? normalizeScanBranchName(opts.branch) : defaultBranchName(headSha);
|
|
17298
18164
|
log(` scan branch: ${branchName}`);
|
|
17299
18165
|
const bearer = await getMcpAccessToken();
|
|
17300
18166
|
const { configPath, cleanup: cleanupMcp } = await writeMcpConfig({
|
|
17301
18167
|
baseUrl: mcpUrl,
|
|
17302
|
-
projectSlug: `${
|
|
18168
|
+
projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
|
|
17303
18169
|
branch: branchName,
|
|
17304
18170
|
bearer,
|
|
17305
18171
|
// No source-SHA block (no job id for a local run), but DO tag these as scan
|
|
@@ -17334,9 +18200,9 @@ async function runScan(opts, deps = {}) {
|
|
|
17334
18200
|
// Incompatible with an interactive TTY session.
|
|
17335
18201
|
streamJson: !interactive
|
|
17336
18202
|
});
|
|
17337
|
-
const transcriptDir =
|
|
17338
|
-
const transcriptPath =
|
|
17339
|
-
await
|
|
18203
|
+
const transcriptDir = join19(opts.repoPath, ".archme", "scan");
|
|
18204
|
+
const transcriptPath = join19(transcriptDir, "transcript.log");
|
|
18205
|
+
await mkdir12(transcriptDir, { recursive: true });
|
|
17340
18206
|
const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
|
|
17341
18207
|
let finalResultText = "";
|
|
17342
18208
|
if (proc.stdout) {
|
|
@@ -17370,19 +18236,19 @@ async function runScan(opts, deps = {}) {
|
|
|
17370
18236
|
} else {
|
|
17371
18237
|
log(`claude exited ${exitCode}`);
|
|
17372
18238
|
}
|
|
17373
|
-
const counts = await countArchitecture(supabase,
|
|
18239
|
+
const counts = await countArchitecture(supabase, resolved2.projectId, branchName);
|
|
17374
18240
|
log(` components: ${counts.components}`);
|
|
17375
18241
|
log(` stories: ${counts.stories}`);
|
|
17376
18242
|
let signals2 = null;
|
|
17377
18243
|
try {
|
|
17378
|
-
await new Promise((
|
|
17379
|
-
transcriptStream.end((
|
|
18244
|
+
await new Promise((resolve5, reject) => {
|
|
18245
|
+
transcriptStream.end((err2) => err2 ? reject(err2) : resolve5());
|
|
17380
18246
|
});
|
|
17381
|
-
const reportPath =
|
|
17382
|
-
const orphansTxtPath =
|
|
17383
|
-
const transcript = await
|
|
18247
|
+
const reportPath = join19(transcriptDir, "report.json");
|
|
18248
|
+
const orphansTxtPath = join19(transcriptDir, "orphans.txt");
|
|
18249
|
+
const transcript = await readFile12(transcriptPath, "utf8").catch(() => "");
|
|
17384
18250
|
signals2 = extractTranscriptSignals(transcript);
|
|
17385
|
-
const architecture = await fetchArchitectureDetail(supabase,
|
|
18251
|
+
const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName);
|
|
17386
18252
|
const orphans = await classifyOrphans({
|
|
17387
18253
|
repoRoot: opts.repoPath,
|
|
17388
18254
|
fileAnchors: architecture.fileAnchors,
|
|
@@ -17391,7 +18257,7 @@ async function runScan(opts, deps = {}) {
|
|
|
17391
18257
|
excludeGlobs: DEFAULT_EXCLUDE_GLOBS
|
|
17392
18258
|
});
|
|
17393
18259
|
const report = await buildReport({
|
|
17394
|
-
projectSlug: `${
|
|
18260
|
+
projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
|
|
17395
18261
|
branch: branchName,
|
|
17396
18262
|
transcriptPath,
|
|
17397
18263
|
reportPath,
|
|
@@ -17407,8 +18273,8 @@ async function runScan(opts, deps = {}) {
|
|
|
17407
18273
|
log(` coverage: ${report.repo.anchor_coverage_ratio} orphans: ${orphans.orphan_files_full.length}`);
|
|
17408
18274
|
log(` report: ${reportPath}`);
|
|
17409
18275
|
log(` transcript: ${transcriptPath}`);
|
|
17410
|
-
} catch (
|
|
17411
|
-
const msg =
|
|
18276
|
+
} catch (err2) {
|
|
18277
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
17412
18278
|
log(`warning: failed to build scan report: ${msg}`);
|
|
17413
18279
|
}
|
|
17414
18280
|
if (signals2 !== null && shouldStampMarker(signals2, exitCode, signal)) {
|
|
@@ -17418,15 +18284,15 @@ async function runScan(opts, deps = {}) {
|
|
|
17418
18284
|
repo = stdout ? parseGitHubRepo(stdout) : null;
|
|
17419
18285
|
} catch {
|
|
17420
18286
|
}
|
|
17421
|
-
const { error: markerErr } = await supabase.from("branches").update({ last_source_sha: headSha, last_source_repo: repo }).eq("project_id",
|
|
18287
|
+
const { error: markerErr } = await supabase.from("branches").update({ last_source_sha: headSha, last_source_repo: repo }).eq("project_id", resolved2.projectId).eq("name", branchName);
|
|
17422
18288
|
if (markerErr) log(` marker advance failed (next daemon job has no usable base): ${markerErr.message}`);
|
|
17423
18289
|
else log(` marker advanced: ${branchName} \u2192 ${headSha.slice(0, 8)}${repo ? ` (${repo})` : ""}`);
|
|
17424
18290
|
}
|
|
17425
|
-
const diffBase =
|
|
18291
|
+
const diffBase = resolved2.defaultBranch;
|
|
17426
18292
|
const webUrl = buildWebUrl({
|
|
17427
18293
|
apiUrl: mcpUrl,
|
|
17428
|
-
orgSlug:
|
|
17429
|
-
projectSlug:
|
|
18294
|
+
orgSlug: resolved2.orgSlug,
|
|
18295
|
+
projectSlug: resolved2.projectSlug,
|
|
17430
18296
|
branchName,
|
|
17431
18297
|
defaultBranch: diffBase,
|
|
17432
18298
|
// CLI: throw on malformed mcpUrl so misconfiguration is loud.
|
|
@@ -17439,8 +18305,8 @@ async function runScan(opts, deps = {}) {
|
|
|
17439
18305
|
components: counts.components,
|
|
17440
18306
|
stories: counts.stories,
|
|
17441
18307
|
branchName,
|
|
17442
|
-
orgSlug:
|
|
17443
|
-
projectSlug:
|
|
18308
|
+
orgSlug: resolved2.orgSlug,
|
|
18309
|
+
projectSlug: resolved2.projectSlug,
|
|
17444
18310
|
webUrl,
|
|
17445
18311
|
summary
|
|
17446
18312
|
};
|
|
@@ -17466,6 +18332,7 @@ var init_run_scan = __esm({
|
|
|
17466
18332
|
init_transcript_signals();
|
|
17467
18333
|
init_report();
|
|
17468
18334
|
init_github_remote();
|
|
18335
|
+
init_claimDefaultProject();
|
|
17469
18336
|
BOOTSTRAP_HEADLESS = `Run the \`scan\` MCP prompt now and follow it to completion. This is a non-interactive automated run \u2014 skip any steps that pause for user confirmation (e.g. "does this hierarchy look right?"). Proceed with your best-judgement decisions rather than waiting for human input.`;
|
|
17470
18337
|
BOOTSTRAP_INTERACTIVE = `Run the \`scan\` MCP prompt now and follow it to completion. The user is at an interactive terminal \u2014 ask them when the prompt says to, and wait for their answer.
|
|
17471
18338
|
|
|
@@ -17475,6 +18342,62 @@ Before you call \`start_scan\`, show the user this briefing verbatim and ask the
|
|
|
17475
18342
|
}
|
|
17476
18343
|
});
|
|
17477
18344
|
|
|
18345
|
+
// src/scan/scanOutcome.ts
|
|
18346
|
+
function scanSucceeded(r) {
|
|
18347
|
+
return r.exitCode === 0 && !r.signal && r.components > 0;
|
|
18348
|
+
}
|
|
18349
|
+
function nextSteps() {
|
|
18350
|
+
return [
|
|
18351
|
+
"",
|
|
18352
|
+
"This is a snapshot of your architecture as of now. It will not follow your",
|
|
18353
|
+
"code unless something keeps it up to date:",
|
|
18354
|
+
"",
|
|
18355
|
+
" archme init walks you through the options",
|
|
18356
|
+
"",
|
|
18357
|
+
" \xB7 GitHub App \u2014 a couple of minutes, PR-aware, works for your whole team.",
|
|
18358
|
+
" Needs someone who can grant the app read access to the repo.",
|
|
18359
|
+
" \xB7 archme local-watch \u2014 no GitHub App and no approval, but updates only",
|
|
18360
|
+
" happen while your daemon is running on a machine that stays on."
|
|
18361
|
+
];
|
|
18362
|
+
}
|
|
18363
|
+
function formatScanOutcome(r) {
|
|
18364
|
+
const lines = [""];
|
|
18365
|
+
if (!scanSucceeded(r)) {
|
|
18366
|
+
if (r.signal || r.exitCode !== 0) {
|
|
18367
|
+
lines.push("scan did not complete");
|
|
18368
|
+
lines.push(r.signal ? ` killed by ${r.signal}` : ` exit ${r.exitCode}`);
|
|
18369
|
+
} else {
|
|
18370
|
+
lines.push("scan finished but wrote no components");
|
|
18371
|
+
}
|
|
18372
|
+
lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
|
|
18373
|
+
lines.push(` components: ${r.components}`);
|
|
18374
|
+
lines.push(` stories: ${r.stories}`);
|
|
18375
|
+
lines.push("");
|
|
18376
|
+
lines.push("If Claude Code is installed but not signed in, run `claude` once and");
|
|
18377
|
+
lines.push("authenticate, then re-run this command.");
|
|
18378
|
+
} else {
|
|
18379
|
+
lines.push("scan complete");
|
|
18380
|
+
lines.push(` components: ${r.components}`);
|
|
18381
|
+
lines.push(` stories: ${r.stories}`);
|
|
18382
|
+
lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
|
|
18383
|
+
lines.push(` web UI: ${r.webUrl}`);
|
|
18384
|
+
}
|
|
18385
|
+
if (r.summary) {
|
|
18386
|
+
const tail = r.summary.length > SUMMARY_TAIL_CHARS ? `\u2026${r.summary.slice(-SUMMARY_TAIL_CHARS)}` : r.summary;
|
|
18387
|
+
lines.push("", `Claude summary:
|
|
18388
|
+
${tail}`);
|
|
18389
|
+
}
|
|
18390
|
+
if (scanSucceeded(r)) lines.push(...nextSteps());
|
|
18391
|
+
return lines.join("\n");
|
|
18392
|
+
}
|
|
18393
|
+
var SUMMARY_TAIL_CHARS;
|
|
18394
|
+
var init_scanOutcome = __esm({
|
|
18395
|
+
"src/scan/scanOutcome.ts"() {
|
|
18396
|
+
"use strict";
|
|
18397
|
+
SUMMARY_TAIL_CHARS = 2e3;
|
|
18398
|
+
}
|
|
18399
|
+
});
|
|
18400
|
+
|
|
17478
18401
|
// src/scan/dispatch.ts
|
|
17479
18402
|
var dispatch_exports3 = {};
|
|
17480
18403
|
__export(dispatch_exports3, {
|
|
@@ -17546,22 +18469,10 @@ async function dispatchScan(args) {
|
|
|
17546
18469
|
opts.repoPath = resolve2(repoPath);
|
|
17547
18470
|
try {
|
|
17548
18471
|
const result = await runScan(opts);
|
|
17549
|
-
console.log(
|
|
17550
|
-
console.log(`scan complete`);
|
|
17551
|
-
console.log(` exit: ${result.exitCode}${result.signal ? ` (signal: ${result.signal})` : ""}`);
|
|
17552
|
-
console.log(` components: ${result.components}`);
|
|
17553
|
-
console.log(` stories: ${result.stories}`);
|
|
17554
|
-
console.log(` branch: ${result.orgSlug}/${result.projectSlug}@${result.branchName}`);
|
|
17555
|
-
console.log(` web UI: ${result.webUrl}`);
|
|
17556
|
-
if (result.summary) {
|
|
17557
|
-
const tail = result.summary.length > SUMMARY_TAIL_CHARS ? `\u2026${result.summary.slice(-SUMMARY_TAIL_CHARS)}` : result.summary;
|
|
17558
|
-
console.log(`
|
|
17559
|
-
Claude summary:
|
|
17560
|
-
${tail}`);
|
|
17561
|
-
}
|
|
18472
|
+
console.log(formatScanOutcome(result));
|
|
17562
18473
|
return dispatchExitCode(result);
|
|
17563
|
-
} catch (
|
|
17564
|
-
const msg =
|
|
18474
|
+
} catch (err2) {
|
|
18475
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
17565
18476
|
console.error(`archme scan: ${msg}`);
|
|
17566
18477
|
return EX_INFRA;
|
|
17567
18478
|
}
|
|
@@ -17573,7 +18484,7 @@ Usage:
|
|
|
17573
18484
|
archme scan <repo-path> [flags]
|
|
17574
18485
|
|
|
17575
18486
|
Flags:
|
|
17576
|
-
--project <org/slug> Target project. Defaults to
|
|
18487
|
+
--project <org/slug> Target project. Defaults to <your-org>/<basename>.
|
|
17577
18488
|
--branch <name> Target scan branch. Defaults to <date>-<sha7>-scan.
|
|
17578
18489
|
--no-interactive Run headless (no questions). For CI/automation.
|
|
17579
18490
|
--prompt-variant <id> Scan prompt variant. Defaults to latest.
|
|
@@ -17582,23 +18493,23 @@ Flags:
|
|
|
17582
18493
|
Reads OAuth credentials from the local keychain (run \`archme login\` first).
|
|
17583
18494
|
Outputs component / story counts and the web-UI URL on completion.`);
|
|
17584
18495
|
}
|
|
17585
|
-
var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE,
|
|
18496
|
+
var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE, PROJECT_SLUG_RE;
|
|
17586
18497
|
var init_dispatch3 = __esm({
|
|
17587
18498
|
"src/scan/dispatch.ts"() {
|
|
17588
18499
|
"use strict";
|
|
17589
18500
|
init_run_scan();
|
|
18501
|
+
init_scanOutcome();
|
|
17590
18502
|
EX_OK = 0;
|
|
17591
18503
|
EX_SIGNAL = 2;
|
|
17592
18504
|
EX_INFRA = 3;
|
|
17593
18505
|
EX_USAGE = 78;
|
|
17594
|
-
SUMMARY_TAIL_CHARS = 2e3;
|
|
17595
18506
|
PROJECT_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
17596
18507
|
}
|
|
17597
18508
|
});
|
|
17598
18509
|
|
|
17599
18510
|
// src/init/repoSize.ts
|
|
17600
18511
|
import { stat as stat9 } from "node:fs/promises";
|
|
17601
|
-
import { extname, join as
|
|
18512
|
+
import { extname, join as join20 } from "node:path";
|
|
17602
18513
|
async function measureRepo(repoPath) {
|
|
17603
18514
|
const files = await gitListFiles(repoPath);
|
|
17604
18515
|
if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
|
|
@@ -17607,7 +18518,7 @@ async function measureRepo(repoPath) {
|
|
|
17607
18518
|
for (const rel of files) {
|
|
17608
18519
|
if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
|
|
17609
18520
|
try {
|
|
17610
|
-
const s = await stat9(
|
|
18521
|
+
const s = await stat9(join20(repoPath, rel));
|
|
17611
18522
|
if (!s.isFile()) continue;
|
|
17612
18523
|
sourceFiles++;
|
|
17613
18524
|
sourceBytes += s.size;
|
|
@@ -17726,7 +18637,7 @@ async function watchRepoForProject(input) {
|
|
|
17726
18637
|
reason: "the ArchMe GitHub App is not installed on this org yet \u2014 connect it in the app to get automatic updates on every push."
|
|
17727
18638
|
};
|
|
17728
18639
|
}
|
|
17729
|
-
const { error } = await supabase.rpc("watch_repo", {
|
|
18640
|
+
const { data: watchedRepoId, error } = await supabase.rpc("watch_repo", {
|
|
17730
18641
|
p_org_id: orgId,
|
|
17731
18642
|
p_project_id: projectId,
|
|
17732
18643
|
p_repo_owner: owner,
|
|
@@ -17737,6 +18648,17 @@ async function watchRepoForProject(input) {
|
|
|
17737
18648
|
if (error) {
|
|
17738
18649
|
return { watched: false, reason: `could not watch the repo (${error.message}).` };
|
|
17739
18650
|
}
|
|
18651
|
+
const { error: enableErr } = await supabase.from("watched_repos").update({
|
|
18652
|
+
commit_update_enabled: true,
|
|
18653
|
+
commit_update_archme_branch: input.archmeBranch,
|
|
18654
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18655
|
+
}).eq("id", watchedRepoId);
|
|
18656
|
+
if (enableErr) {
|
|
18657
|
+
return {
|
|
18658
|
+
watched: false,
|
|
18659
|
+
reason: `the repo is linked but commit-driven updates could not be switched on (${enableErr.message}). Pushes will not reach your daemon until they are.`
|
|
18660
|
+
};
|
|
18661
|
+
}
|
|
17740
18662
|
return { watched: true };
|
|
17741
18663
|
}
|
|
17742
18664
|
var init_watchRepo = __esm({
|
|
@@ -17746,21 +18668,21 @@ var init_watchRepo = __esm({
|
|
|
17746
18668
|
});
|
|
17747
18669
|
|
|
17748
18670
|
// src/init/gitContext.ts
|
|
17749
|
-
async function
|
|
18671
|
+
async function git2(cwd, args) {
|
|
17750
18672
|
const { stdout } = await execa("git", args, { cwd });
|
|
17751
18673
|
return stdout.trim();
|
|
17752
18674
|
}
|
|
17753
18675
|
async function resolveGitContext(repoPath) {
|
|
17754
18676
|
let root;
|
|
17755
18677
|
try {
|
|
17756
|
-
root = await
|
|
18678
|
+
root = await git2(repoPath, ["rev-parse", "--show-toplevel"]);
|
|
17757
18679
|
} catch {
|
|
17758
18680
|
throw new Error(
|
|
17759
18681
|
`\`${repoPath}\` is not inside a git repository \u2014 run \`archme init\` from a repo, or pass its path`
|
|
17760
18682
|
);
|
|
17761
18683
|
}
|
|
17762
18684
|
try {
|
|
17763
|
-
await
|
|
18685
|
+
await git2(root, ["rev-parse", "HEAD"]);
|
|
17764
18686
|
} catch {
|
|
17765
18687
|
throw new Error(
|
|
17766
18688
|
`\`${root}\` is a git repository with no commits yet \u2014 make a commit first, then run \`archme init\``
|
|
@@ -17768,26 +18690,29 @@ async function resolveGitContext(repoPath) {
|
|
|
17768
18690
|
}
|
|
17769
18691
|
let branch = "";
|
|
17770
18692
|
try {
|
|
17771
|
-
branch = await
|
|
18693
|
+
branch = await git2(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
17772
18694
|
} catch {
|
|
17773
18695
|
branch = "";
|
|
17774
18696
|
}
|
|
17775
18697
|
if (!branch || branch === "HEAD") {
|
|
17776
18698
|
try {
|
|
17777
|
-
const ref = await
|
|
18699
|
+
const ref = await git2(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
17778
18700
|
branch = ref.replace(/^refs\/remotes\/origin\//, "");
|
|
17779
18701
|
} catch {
|
|
17780
18702
|
branch = "main";
|
|
17781
18703
|
}
|
|
17782
18704
|
}
|
|
17783
18705
|
let repoSlug = null;
|
|
18706
|
+
let originUrl = null;
|
|
17784
18707
|
try {
|
|
17785
|
-
const url = await
|
|
18708
|
+
const url = await git2(root, ["remote", "get-url", "origin"]);
|
|
18709
|
+
originUrl = url || null;
|
|
17786
18710
|
repoSlug = url ? parseGitHubRepo(url) : null;
|
|
17787
18711
|
} catch {
|
|
17788
18712
|
repoSlug = null;
|
|
18713
|
+
originUrl = null;
|
|
17789
18714
|
}
|
|
17790
|
-
return { root, branch, repoSlug };
|
|
18715
|
+
return { root, branch, repoSlug, originUrl };
|
|
17791
18716
|
}
|
|
17792
18717
|
var init_gitContext = __esm({
|
|
17793
18718
|
"src/init/gitContext.ts"() {
|
|
@@ -17797,6 +18722,654 @@ var init_gitContext = __esm({
|
|
|
17797
18722
|
}
|
|
17798
18723
|
});
|
|
17799
18724
|
|
|
18725
|
+
// src/init/keepCurrent.ts
|
|
18726
|
+
async function chooseKeepCurrent(deps) {
|
|
18727
|
+
if (deps.yes) {
|
|
18728
|
+
return { detection: deps.alreadyWatched ? "github-app" : "local-watch", runner: "self" };
|
|
18729
|
+
}
|
|
18730
|
+
const detection = deps.alreadyWatched ? "github-app" : await deps.choose(
|
|
18731
|
+
"How should ArchMe detect new commits?",
|
|
18732
|
+
DETECTION_OPTIONS
|
|
18733
|
+
);
|
|
18734
|
+
const runner = await deps.choose(
|
|
18735
|
+
"Something has to process each update. Who runs it?",
|
|
18736
|
+
RUNNER_OPTIONS
|
|
18737
|
+
);
|
|
18738
|
+
return { detection, runner };
|
|
18739
|
+
}
|
|
18740
|
+
function formatKeepCurrentPlan(plan, ctx) {
|
|
18741
|
+
const steps = [];
|
|
18742
|
+
if (plan.detection === "local-watch" && !ctx.localWatchRegistered) {
|
|
18743
|
+
steps.push({
|
|
18744
|
+
title: "Register this repo so your machine watches it:",
|
|
18745
|
+
command: "archme local-watch"
|
|
18746
|
+
});
|
|
18747
|
+
}
|
|
18748
|
+
if (plan.runner === "self") {
|
|
18749
|
+
steps.push({
|
|
18750
|
+
title: "Start the daemon \u2014 nothing processes the queue until you do:",
|
|
18751
|
+
command: "archme daemon start",
|
|
18752
|
+
notes: [
|
|
18753
|
+
"Keep it running on a machine that stays on. Each update spends tokens",
|
|
18754
|
+
"from your Claude subscription, the same way this scan did. Your code is",
|
|
18755
|
+
"not sent to our servers \u2014 only the resulting architecture."
|
|
18756
|
+
]
|
|
18757
|
+
});
|
|
18758
|
+
} else {
|
|
18759
|
+
steps.push({
|
|
18760
|
+
title: `Email ${CONTACT_EMAIL} and we'll set up the daemon for you.`,
|
|
18761
|
+
notes: [
|
|
18762
|
+
ctx.repoSlug ? `Mention ${ctx.repoSlug} so we know which repo you mean.` : "",
|
|
18763
|
+
// Deliberately not "we'll be in touch": running this command notifies
|
|
18764
|
+
// nobody. Promising contact that depends on the user sending the mail
|
|
18765
|
+
// is how someone ends up waiting on a team that never heard.
|
|
18766
|
+
"Until then this architecture stays as it is \u2014 it won't track new commits."
|
|
18767
|
+
].filter(Boolean)
|
|
18768
|
+
});
|
|
18769
|
+
}
|
|
18770
|
+
const lines = ["", "Keeping it current:"];
|
|
18771
|
+
if (ctx.localWatchRegistered) {
|
|
18772
|
+
lines.push("", ` \u2713 ${ctx.repoSlug ?? "This repo"} is registered \u2014 your machine is watching it.`);
|
|
18773
|
+
}
|
|
18774
|
+
const numbered = steps.length > 1;
|
|
18775
|
+
steps.forEach((step, i2) => {
|
|
18776
|
+
const bullet = numbered ? ` ${i2 + 1}. ` : " ";
|
|
18777
|
+
const indent = " ".repeat(bullet.length);
|
|
18778
|
+
lines.push("", `${bullet}${step.title}`);
|
|
18779
|
+
if (step.command) lines.push("", `${indent} ${step.command}`);
|
|
18780
|
+
if (step.notes?.length) {
|
|
18781
|
+
lines.push("");
|
|
18782
|
+
for (const note of step.notes) lines.push(`${indent}${note}`);
|
|
18783
|
+
}
|
|
18784
|
+
});
|
|
18785
|
+
if (ctx.boardUrl) lines.push("", ` Your board: ${ctx.boardUrl}`);
|
|
18786
|
+
return lines.join("\n");
|
|
18787
|
+
}
|
|
18788
|
+
var CONTACT_EMAIL, DETECTION_OPTIONS, RUNNER_OPTIONS;
|
|
18789
|
+
var init_keepCurrent = __esm({
|
|
18790
|
+
"src/init/keepCurrent.ts"() {
|
|
18791
|
+
"use strict";
|
|
18792
|
+
CONTACT_EMAIL = "signup@archme.io";
|
|
18793
|
+
DETECTION_OPTIONS = [
|
|
18794
|
+
{
|
|
18795
|
+
// First, and named as recommended: it is less work per-commit, survives
|
|
18796
|
+
// this laptop being closed, and is the only path that sees pull requests.
|
|
18797
|
+
label: "GitHub App (recommended) \u2014 a couple of minutes to set up. Sees pushes and pull requests, works for your whole team, nothing to keep running on your side. Someone has to grant it read access to the repo.",
|
|
18798
|
+
value: "github-app"
|
|
18799
|
+
},
|
|
18800
|
+
{
|
|
18801
|
+
label: "Local polling \u2014 no GitHub App and nobody's approval needed. This machine watches the repo instead. No pull-request awareness.",
|
|
18802
|
+
value: "local-watch"
|
|
18803
|
+
}
|
|
18804
|
+
];
|
|
18805
|
+
RUNNER_OPTIONS = [
|
|
18806
|
+
{
|
|
18807
|
+
label: "I'll run it (recommended) \u2014 `archme daemon start` on a machine that stays on. Updates use your own Claude subscription, the same way this scan did, and your code never leaves your machine.",
|
|
18808
|
+
value: "self"
|
|
18809
|
+
},
|
|
18810
|
+
{
|
|
18811
|
+
label: "ArchMe runs it \u2014 if you don't have a machine that's always on, or a Claude subscription to spend on it.",
|
|
18812
|
+
value: "archme"
|
|
18813
|
+
}
|
|
18814
|
+
];
|
|
18815
|
+
}
|
|
18816
|
+
});
|
|
18817
|
+
|
|
18818
|
+
// src/init/keepCurrentFlow.ts
|
|
18819
|
+
async function applyKeepCurrentPlan(plan, deps, alreadyWatched) {
|
|
18820
|
+
const base = { detection: plan.detection, runner: plan.runner };
|
|
18821
|
+
if (plan.detection === "github-app") {
|
|
18822
|
+
if (alreadyWatched) {
|
|
18823
|
+
return { ...base, wired: true, localWatchRegistered: false };
|
|
18824
|
+
}
|
|
18825
|
+
const url = await deps.startGitHubAppInstall();
|
|
18826
|
+
deps.log("");
|
|
18827
|
+
deps.log(" Install the ArchMe GitHub App here, then come back \u2014 I'll wait:");
|
|
18828
|
+
deps.log("");
|
|
18829
|
+
deps.log(` ${url}`);
|
|
18830
|
+
deps.log("");
|
|
18831
|
+
const installed = await deps.waitForInstallation();
|
|
18832
|
+
if (!installed) {
|
|
18833
|
+
return {
|
|
18834
|
+
...base,
|
|
18835
|
+
wired: false,
|
|
18836
|
+
localWatchRegistered: false,
|
|
18837
|
+
reason: "the GitHub App install hasn't come through yet. If your org needs an owner to approve it, that can take a while \u2014 re-run `archme init` once it's approved."
|
|
18838
|
+
};
|
|
18839
|
+
}
|
|
18840
|
+
const watched = await deps.watchRepo();
|
|
18841
|
+
if (!watched.watched) {
|
|
18842
|
+
return {
|
|
18843
|
+
...base,
|
|
18844
|
+
wired: false,
|
|
18845
|
+
localWatchRegistered: false,
|
|
18846
|
+
reason: watched.reason ?? "the repo could not be linked to this project."
|
|
18847
|
+
};
|
|
18848
|
+
}
|
|
18849
|
+
deps.log(" \u2713 GitHub App installed and the repo is linked.");
|
|
18850
|
+
return { ...base, wired: true, localWatchRegistered: false };
|
|
18851
|
+
}
|
|
18852
|
+
const registered = await deps.registerLocalWatch();
|
|
18853
|
+
if (!registered.ok) {
|
|
18854
|
+
return {
|
|
18855
|
+
...base,
|
|
18856
|
+
wired: false,
|
|
18857
|
+
localWatchRegistered: false,
|
|
18858
|
+
reason: registered.reason ?? "the repo could not be registered for local polling."
|
|
18859
|
+
};
|
|
18860
|
+
}
|
|
18861
|
+
return { ...base, wired: true, localWatchRegistered: true };
|
|
18862
|
+
}
|
|
18863
|
+
var init_keepCurrentFlow = __esm({
|
|
18864
|
+
"src/init/keepCurrentFlow.ts"() {
|
|
18865
|
+
"use strict";
|
|
18866
|
+
}
|
|
18867
|
+
});
|
|
18868
|
+
|
|
18869
|
+
// src/init/githubAppSetup.ts
|
|
18870
|
+
async function waitForInstallation(deps) {
|
|
18871
|
+
const deadline = deps.now() + INSTALL_WAIT_TIMEOUT_MS;
|
|
18872
|
+
let announced = false;
|
|
18873
|
+
for (; ; ) {
|
|
18874
|
+
try {
|
|
18875
|
+
if (await deps.hasInstallation()) return true;
|
|
18876
|
+
} catch {
|
|
18877
|
+
}
|
|
18878
|
+
if (deps.now() >= deadline) return false;
|
|
18879
|
+
if (!announced) {
|
|
18880
|
+
deps.log(" waiting for the installation to come through\u2026");
|
|
18881
|
+
announced = true;
|
|
18882
|
+
}
|
|
18883
|
+
await deps.sleep(INSTALL_POLL_INTERVAL_MS);
|
|
18884
|
+
}
|
|
18885
|
+
}
|
|
18886
|
+
async function buildInstallUrl(opts) {
|
|
18887
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
18888
|
+
const res = await doFetch(`${opts.supabaseUrl}/functions/v1/github/install-state`, {
|
|
18889
|
+
method: "POST",
|
|
18890
|
+
headers: {
|
|
18891
|
+
"Content-Type": "application/json",
|
|
18892
|
+
Authorization: `Bearer ${opts.accessToken}`
|
|
18893
|
+
},
|
|
18894
|
+
body: JSON.stringify({ orgId: opts.orgId, env: opts.env ?? "prod" })
|
|
18895
|
+
});
|
|
18896
|
+
if (!res.ok) {
|
|
18897
|
+
const body = await res.json().catch(() => ({}));
|
|
18898
|
+
throw new Error(body.error ?? `could not start the GitHub App install (${res.status})`);
|
|
18899
|
+
}
|
|
18900
|
+
const { token, url } = await res.json();
|
|
18901
|
+
if (url) return url;
|
|
18902
|
+
if (!token) throw new Error("install-state returned neither a url nor a token");
|
|
18903
|
+
const slug = process.env.ARCHME_GITHUB_APP_SLUG || "architectme";
|
|
18904
|
+
return `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(token)}`;
|
|
18905
|
+
}
|
|
18906
|
+
async function orgHasInstallation(supabase, orgId) {
|
|
18907
|
+
const { data, error } = await supabase.from("org_github_installations").select("installation_id").eq("org_id", orgId).limit(1);
|
|
18908
|
+
if (error) throw new Error(error.message);
|
|
18909
|
+
return (data?.length ?? 0) > 0;
|
|
18910
|
+
}
|
|
18911
|
+
var INSTALL_WAIT_TIMEOUT_MS, INSTALL_POLL_INTERVAL_MS;
|
|
18912
|
+
var init_githubAppSetup = __esm({
|
|
18913
|
+
"src/init/githubAppSetup.ts"() {
|
|
18914
|
+
"use strict";
|
|
18915
|
+
INSTALL_WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
18916
|
+
INSTALL_POLL_INTERVAL_MS = 3e3;
|
|
18917
|
+
}
|
|
18918
|
+
});
|
|
18919
|
+
|
|
18920
|
+
// src/daemon/prompt.ts
|
|
18921
|
+
import { createInterface } from "node:readline/promises";
|
|
18922
|
+
async function withReadline(fn) {
|
|
18923
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
18924
|
+
try {
|
|
18925
|
+
return await fn(rl);
|
|
18926
|
+
} finally {
|
|
18927
|
+
rl.close();
|
|
18928
|
+
}
|
|
18929
|
+
}
|
|
18930
|
+
async function promptLine(question) {
|
|
18931
|
+
return await withReadline(async (rl) => {
|
|
18932
|
+
try {
|
|
18933
|
+
return await rl.question(`${question}: `);
|
|
18934
|
+
} catch {
|
|
18935
|
+
return "";
|
|
18936
|
+
}
|
|
18937
|
+
});
|
|
18938
|
+
}
|
|
18939
|
+
async function promptChoice(question, options) {
|
|
18940
|
+
return await withReadline(async (rl) => {
|
|
18941
|
+
console.log(`
|
|
18942
|
+
${question}
|
|
18943
|
+
`);
|
|
18944
|
+
options.forEach((o2, i2) => console.log(` ${i2 + 1}. ${o2.label}
|
|
18945
|
+
`));
|
|
18946
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
18947
|
+
let answer;
|
|
18948
|
+
try {
|
|
18949
|
+
answer = (await rl.question(`Choose 1-${options.length} [1]: `)).trim();
|
|
18950
|
+
} catch {
|
|
18951
|
+
return options[0].value;
|
|
18952
|
+
}
|
|
18953
|
+
if (answer === "") return options[0].value;
|
|
18954
|
+
const n2 = Number.parseInt(answer, 10);
|
|
18955
|
+
if (Number.isInteger(n2) && n2 >= 1 && n2 <= options.length) return options[n2 - 1].value;
|
|
18956
|
+
console.log(` "${answer}" isn't one of the options.`);
|
|
18957
|
+
}
|
|
18958
|
+
return options[0].value;
|
|
18959
|
+
});
|
|
18960
|
+
}
|
|
18961
|
+
async function promptYesNo(question) {
|
|
18962
|
+
return await withReadline(async (rl) => {
|
|
18963
|
+
try {
|
|
18964
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
18965
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
18966
|
+
} catch {
|
|
18967
|
+
return false;
|
|
18968
|
+
}
|
|
18969
|
+
});
|
|
18970
|
+
}
|
|
18971
|
+
var init_prompt = __esm({
|
|
18972
|
+
"src/daemon/prompt.ts"() {
|
|
18973
|
+
"use strict";
|
|
18974
|
+
}
|
|
18975
|
+
});
|
|
18976
|
+
|
|
18977
|
+
// src/daemon/watchReadiness.ts
|
|
18978
|
+
function evaluateWatchReadiness(input) {
|
|
18979
|
+
const { marker, repo, archmeBranch } = input;
|
|
18980
|
+
if (!marker.sha) {
|
|
18981
|
+
return {
|
|
18982
|
+
ok: false,
|
|
18983
|
+
reason: `${repo} has no architecture on \`${archmeBranch}\` yet, so there is nothing to keep up to date and no commit to diff against.
|
|
18984
|
+
Run \`archme init\` in this repo first \u2014 it creates the architecture, then comes back here.`
|
|
18985
|
+
};
|
|
18986
|
+
}
|
|
18987
|
+
if (!GIT_SHA_RE2.test(marker.sha)) {
|
|
18988
|
+
return {
|
|
18989
|
+
ok: false,
|
|
18990
|
+
reason: `the recorded commit for \`${archmeBranch}\` is "${marker.sha}", which is not a git commit id. Re-run \`archme init --force\` to re-establish it.`
|
|
18991
|
+
};
|
|
18992
|
+
}
|
|
18993
|
+
if (!marker.repo) {
|
|
18994
|
+
return {
|
|
18995
|
+
ok: false,
|
|
18996
|
+
reason: `the architecture on \`${archmeBranch}\` has no source repo recorded, so updates cannot be matched to ${repo}.
|
|
18997
|
+
That usually means the scan ran against a non-github.com remote. Re-run \`archme init --force\` from a clone whose \`origin\` is on github.com.`
|
|
18998
|
+
};
|
|
18999
|
+
}
|
|
19000
|
+
if (marker.repo !== repo) {
|
|
19001
|
+
return {
|
|
19002
|
+
ok: false,
|
|
19003
|
+
reason: `\`${archmeBranch}\` is already tracking ${marker.repo}, not ${repo}. Watching it from here would make every update fail on the mismatch.
|
|
19004
|
+
Use \`--project <org>/<slug>\` to pick a different project, or a different branch.`
|
|
19005
|
+
};
|
|
19006
|
+
}
|
|
19007
|
+
return { ok: true };
|
|
19008
|
+
}
|
|
19009
|
+
var GIT_SHA_RE2;
|
|
19010
|
+
var init_watchReadiness = __esm({
|
|
19011
|
+
"src/daemon/watchReadiness.ts"() {
|
|
19012
|
+
"use strict";
|
|
19013
|
+
GIT_SHA_RE2 = /^[0-9a-f]{40}$/;
|
|
19014
|
+
}
|
|
19015
|
+
});
|
|
19016
|
+
|
|
19017
|
+
// src/daemon/localWatchList.ts
|
|
19018
|
+
function formatWatchList(repos) {
|
|
19019
|
+
if (repos.length === 0) {
|
|
19020
|
+
return [
|
|
19021
|
+
"Not watching any repos on this machine.",
|
|
19022
|
+
"",
|
|
19023
|
+
" archme local-watch register the repo you're in"
|
|
19024
|
+
].join("\n");
|
|
19025
|
+
}
|
|
19026
|
+
const lines = [
|
|
19027
|
+
`Watching ${repos.length} repo${repos.length === 1 ? "" : "s"} on this machine:`,
|
|
19028
|
+
""
|
|
19029
|
+
];
|
|
19030
|
+
for (const r of repos) {
|
|
19031
|
+
const slug = slugOf(r);
|
|
19032
|
+
lines.push(` ${slug}`);
|
|
19033
|
+
lines.push(` mirror: ${r.mirrorPath}`);
|
|
19034
|
+
if (r.haltedAt) {
|
|
19035
|
+
lines.push(` HALTED at ${r.haltedAt}: ${r.haltReason ?? "unknown reason"}`);
|
|
19036
|
+
lines.push(` no new commits are being picked up. Resume with:`);
|
|
19037
|
+
lines.push(` archme local-watch --resume ${slug}`);
|
|
19038
|
+
}
|
|
19039
|
+
lines.push("");
|
|
19040
|
+
}
|
|
19041
|
+
if (repos.every((r) => !r.haltedAt)) {
|
|
19042
|
+
lines.push("Updates only happen while the daemon is running: archme daemon status");
|
|
19043
|
+
}
|
|
19044
|
+
return lines.join("\n").trimEnd();
|
|
19045
|
+
}
|
|
19046
|
+
var init_localWatchList = __esm({
|
|
19047
|
+
"src/daemon/localWatchList.ts"() {
|
|
19048
|
+
"use strict";
|
|
19049
|
+
init_localRepos();
|
|
19050
|
+
}
|
|
19051
|
+
});
|
|
19052
|
+
|
|
19053
|
+
// src/daemon/localWatchResolve.ts
|
|
19054
|
+
function confirmationFor(target, repoPath) {
|
|
19055
|
+
return `Watch ${target.repo} (branch ${target.repoBranch}) for architecture updates?
|
|
19056
|
+
checkout: ${repoPath}`;
|
|
19057
|
+
}
|
|
19058
|
+
async function evaluate(path7, deps) {
|
|
19059
|
+
const inspected = await deps.inspect(path7);
|
|
19060
|
+
if (!inspected.ok) return { ok: false, reason: inspected.error };
|
|
19061
|
+
const resolved2 = resolveInitTarget({
|
|
19062
|
+
originUrl: inspected.originUrl,
|
|
19063
|
+
branchOverride: deps.branch,
|
|
19064
|
+
detectedDefaultBranch: inspected.detectedDefaultBranch
|
|
19065
|
+
});
|
|
19066
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.error };
|
|
19067
|
+
return {
|
|
19068
|
+
ok: true,
|
|
19069
|
+
// The ROOT, never the cwd — registering a subdirectory would mirror a
|
|
19070
|
+
// partial tree and record a source path that isn't the repo.
|
|
19071
|
+
repoPath: inspected.root,
|
|
19072
|
+
originUrl: inspected.originUrl,
|
|
19073
|
+
target: resolved2.target
|
|
19074
|
+
};
|
|
19075
|
+
}
|
|
19076
|
+
async function resolveRepoInteractive(deps) {
|
|
19077
|
+
const first = deps.explicitPath ?? deps.cwd;
|
|
19078
|
+
let evaluated = await evaluate(first, deps);
|
|
19079
|
+
if (deps.yes) {
|
|
19080
|
+
if (!evaluated.ok) return { ok: false, message: evaluated.reason };
|
|
19081
|
+
return evaluated;
|
|
19082
|
+
}
|
|
19083
|
+
if (evaluated.ok) {
|
|
19084
|
+
if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
|
|
19085
|
+
}
|
|
19086
|
+
let reason = evaluated.ok ? "Which repo should ArchMe watch? Enter its path" : `${evaluated.reason}
|
|
19087
|
+
Which repo should ArchMe watch? Enter its path`;
|
|
19088
|
+
for (let attempt = 0; attempt < MAX_REPO_PROMPTS; attempt++) {
|
|
19089
|
+
const answer = (await deps.ask(reason)).trim();
|
|
19090
|
+
if (!answer) return { ok: false, message: "cancelled \u2014 nothing was registered" };
|
|
19091
|
+
evaluated = await evaluate(answer, deps);
|
|
19092
|
+
if (evaluated.ok) {
|
|
19093
|
+
if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
|
|
19094
|
+
reason = "Which repo should ArchMe watch? Enter its path";
|
|
19095
|
+
continue;
|
|
19096
|
+
}
|
|
19097
|
+
reason = `${evaluated.reason}
|
|
19098
|
+
Which repo should ArchMe watch? Enter its path`;
|
|
19099
|
+
}
|
|
19100
|
+
return {
|
|
19101
|
+
ok: false,
|
|
19102
|
+
message: "could not find a repo to watch. `archme local-watch` needs a git clone with a github.com origin \u2014 run it from inside one, or pass the path directly."
|
|
19103
|
+
};
|
|
19104
|
+
}
|
|
19105
|
+
var MAX_REPO_PROMPTS;
|
|
19106
|
+
var init_localWatchResolve = __esm({
|
|
19107
|
+
"src/daemon/localWatchResolve.ts"() {
|
|
19108
|
+
"use strict";
|
|
19109
|
+
init_localWatch();
|
|
19110
|
+
MAX_REPO_PROMPTS = 3;
|
|
19111
|
+
}
|
|
19112
|
+
});
|
|
19113
|
+
|
|
19114
|
+
// src/daemon/localWatch.ts
|
|
19115
|
+
var localWatch_exports = {};
|
|
19116
|
+
__export(localWatch_exports, {
|
|
19117
|
+
DEFAULT_CLONE_BASE_DIR: () => DEFAULT_CLONE_BASE_DIR2,
|
|
19118
|
+
inspectRepoPath: () => inspectRepoPath,
|
|
19119
|
+
mirrorPathFor: () => mirrorPathFor,
|
|
19120
|
+
registerLocalWatch: () => registerLocalWatch,
|
|
19121
|
+
resolveInitTarget: () => resolveInitTarget,
|
|
19122
|
+
runLocalWatch: () => runLocalWatch,
|
|
19123
|
+
runLocalWatchList: () => runLocalWatchList,
|
|
19124
|
+
runLocalWatchResume: () => runLocalWatchResume,
|
|
19125
|
+
runLocalWatchStop: () => runLocalWatchStop
|
|
19126
|
+
});
|
|
19127
|
+
import { homedir as homedir6 } from "node:os";
|
|
19128
|
+
import { join as join21, resolve as resolve3 } from "node:path";
|
|
19129
|
+
import { stat as stat10 } from "node:fs/promises";
|
|
19130
|
+
function mirrorPathFor(cloneBaseDir, owner, name) {
|
|
19131
|
+
return join21(cloneBaseDir, owner, name);
|
|
19132
|
+
}
|
|
19133
|
+
function resolveInitTarget(input) {
|
|
19134
|
+
if (!input.originUrl) {
|
|
19135
|
+
return {
|
|
19136
|
+
ok: false,
|
|
19137
|
+
error: "no `origin` remote found \u2014 archme local-watch needs a clone with a GitHub origin"
|
|
19138
|
+
};
|
|
19139
|
+
}
|
|
19140
|
+
const repo = parseGitHubRepo(input.originUrl);
|
|
19141
|
+
if (!repo) {
|
|
19142
|
+
return {
|
|
19143
|
+
ok: false,
|
|
19144
|
+
error: `origin ${input.originUrl} is not a github.com remote.
|
|
19145
|
+
Local mode identifies repos the same way the GitHub App does, so that a team can later install the App without re-scanning or losing history. That identity only exists for github.com remotes.`
|
|
19146
|
+
};
|
|
19147
|
+
}
|
|
19148
|
+
const repoBranch = input.branchOverride ?? input.detectedDefaultBranch;
|
|
19149
|
+
if (!repoBranch) {
|
|
19150
|
+
return {
|
|
19151
|
+
ok: false,
|
|
19152
|
+
error: "could not determine the default branch \u2014 pass --branch <name>"
|
|
19153
|
+
};
|
|
19154
|
+
}
|
|
19155
|
+
const [repoOwner, repoName] = repo.split("/");
|
|
19156
|
+
return {
|
|
19157
|
+
ok: true,
|
|
19158
|
+
target: {
|
|
19159
|
+
repo,
|
|
19160
|
+
repoOwner,
|
|
19161
|
+
repoName,
|
|
19162
|
+
repoBranch,
|
|
19163
|
+
// Scanned projects live on a `<default>-scan` companion branch; the poller
|
|
19164
|
+
// must enqueue against the SAME branch the scan stamped the marker on.
|
|
19165
|
+
archmeBranch: input.archmeBranchOverride ?? normalizeScanBranchName(repoBranch)
|
|
19166
|
+
}
|
|
19167
|
+
};
|
|
19168
|
+
}
|
|
19169
|
+
async function inspectRepoPath(path7) {
|
|
19170
|
+
const abs = resolve3(path7);
|
|
19171
|
+
const st = await stat10(abs).catch(() => null);
|
|
19172
|
+
if (!st?.isDirectory()) return { ok: false, error: `${abs} is not a directory` };
|
|
19173
|
+
const toplevel = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: abs, reject: false });
|
|
19174
|
+
if (toplevel.exitCode !== 0) return { ok: false, error: `${abs} is not a git repository` };
|
|
19175
|
+
const root = toplevel.stdout.trim() || abs;
|
|
19176
|
+
return {
|
|
19177
|
+
ok: true,
|
|
19178
|
+
root,
|
|
19179
|
+
originUrl: await readOriginUrl(root),
|
|
19180
|
+
detectedDefaultBranch: await detectDefaultBranch(root)
|
|
19181
|
+
};
|
|
19182
|
+
}
|
|
19183
|
+
async function runLocalWatch(opts) {
|
|
19184
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
19185
|
+
const cloneBaseDir = opts.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
|
|
19186
|
+
const resolved2 = await resolveRepoInteractive({
|
|
19187
|
+
cwd: process.cwd(),
|
|
19188
|
+
explicitPath: opts.repoPath,
|
|
19189
|
+
branch: opts.branch,
|
|
19190
|
+
yes: opts.yes ?? false,
|
|
19191
|
+
inspect: inspectRepoPath,
|
|
19192
|
+
ask: promptLine,
|
|
19193
|
+
confirm: promptYesNo,
|
|
19194
|
+
log
|
|
19195
|
+
});
|
|
19196
|
+
if (!resolved2.ok) {
|
|
19197
|
+
console.error(`archme local-watch: ${resolved2.message}`);
|
|
19198
|
+
return 78;
|
|
19199
|
+
}
|
|
19200
|
+
const { repoPath, originUrl, target } = resolved2;
|
|
19201
|
+
log(` repo: ${target.repo}`);
|
|
19202
|
+
log(` branch: ${target.repoBranch} \u2192 archme branch ${target.archmeBranch}`);
|
|
19203
|
+
const cfg = resolveConfig();
|
|
19204
|
+
let authed;
|
|
19205
|
+
try {
|
|
19206
|
+
authed = await createAuthedClient({
|
|
19207
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
19208
|
+
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
19209
|
+
clientId: cfg.archmeDaemonClientId,
|
|
19210
|
+
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
19211
|
+
});
|
|
19212
|
+
} catch (err2) {
|
|
19213
|
+
console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
19214
|
+
return 3;
|
|
19215
|
+
}
|
|
19216
|
+
const { supabase, userId } = authed;
|
|
19217
|
+
const orgId = authed.orgId;
|
|
19218
|
+
if (!orgId) {
|
|
19219
|
+
console.error("archme local-watch: this login is not paired with an org \u2014 run `archme login`");
|
|
19220
|
+
return 3;
|
|
19221
|
+
}
|
|
19222
|
+
const orgSlug = await resolveCurrentOrgSlug(supabase, orgId);
|
|
19223
|
+
if (!opts.project && !orgSlug) {
|
|
19224
|
+
console.error(
|
|
19225
|
+
"archme local-watch: could not resolve your organization slug, so the project name can't be derived \u2014 pass --project <org/slug>"
|
|
19226
|
+
);
|
|
19227
|
+
return 3;
|
|
19228
|
+
}
|
|
19229
|
+
const fullSlug = opts.project ?? defaultProjectSlug(orgSlug, repoPath);
|
|
19230
|
+
const project = await resolveOrCreateProject(supabase, userId, fullSlug);
|
|
19231
|
+
log(` project: ${project.orgSlug}/${project.projectSlug}${project.created ? " (created)" : ""}`);
|
|
19232
|
+
const registered = await registerLocalWatch({
|
|
19233
|
+
supabase,
|
|
19234
|
+
orgId,
|
|
19235
|
+
projectId: project.projectId,
|
|
19236
|
+
repoPath,
|
|
19237
|
+
originUrl,
|
|
19238
|
+
target,
|
|
19239
|
+
cloneBaseDir,
|
|
19240
|
+
log
|
|
19241
|
+
});
|
|
19242
|
+
if (!registered.ok) {
|
|
19243
|
+
console.error(`archme local-watch: ${registered.reason}`);
|
|
19244
|
+
return registered.exitCode;
|
|
19245
|
+
}
|
|
19246
|
+
log(`
|
|
19247
|
+
\u2713 ${target.repo} is now watched locally.`);
|
|
19248
|
+
log(`
|
|
19249
|
+
Start the daemon if it isn't running: archme daemon start`);
|
|
19250
|
+
log(` New commits on ${target.repoBranch} will appear as ArchMe commits within a minute or two.`);
|
|
19251
|
+
return 0;
|
|
19252
|
+
}
|
|
19253
|
+
async function registerLocalWatch(args) {
|
|
19254
|
+
const log = args.log ?? (() => {
|
|
19255
|
+
});
|
|
19256
|
+
const { target, supabase, orgId, projectId, repoPath, originUrl } = args;
|
|
19257
|
+
const cloneBaseDir = args.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
|
|
19258
|
+
const marker = await readBranchMarker(supabase, projectId, target.archmeBranch);
|
|
19259
|
+
const readiness = evaluateWatchReadiness({
|
|
19260
|
+
repo: target.repo,
|
|
19261
|
+
archmeBranch: target.archmeBranch,
|
|
19262
|
+
marker
|
|
19263
|
+
});
|
|
19264
|
+
if (!readiness.ok) return { ok: false, reason: readiness.reason, exitCode: 78 };
|
|
19265
|
+
const { data: watchedRepoId, error: watchErr } = await supabase.rpc("watch_local_repo", {
|
|
19266
|
+
p_org_id: orgId,
|
|
19267
|
+
p_project_id: projectId,
|
|
19268
|
+
p_repo_owner: target.repoOwner,
|
|
19269
|
+
p_repo_name: target.repoName,
|
|
19270
|
+
p_branch: target.repoBranch
|
|
19271
|
+
});
|
|
19272
|
+
if (watchErr || typeof watchedRepoId !== "string") {
|
|
19273
|
+
return {
|
|
19274
|
+
ok: false,
|
|
19275
|
+
reason: `could not register the repo: ${watchErr?.message ?? "no id returned"}`,
|
|
19276
|
+
exitCode: 3
|
|
19277
|
+
};
|
|
19278
|
+
}
|
|
19279
|
+
const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
|
|
19280
|
+
const mirrorExists = await stat10(join21(mirrorPath, ".git")).then(() => true).catch(() => false);
|
|
19281
|
+
if (mirrorExists) {
|
|
19282
|
+
log(` mirror: ${mirrorPath} (reusing)`);
|
|
19283
|
+
await setOriginUrl(mirrorPath, originUrl);
|
|
19284
|
+
} else {
|
|
19285
|
+
log(` mirror: ${mirrorPath}`);
|
|
19286
|
+
await createMirrorClone(repoPath, mirrorPath, originUrl);
|
|
19287
|
+
}
|
|
19288
|
+
await execa("git", ["fetch", "--no-tags", "origin", target.repoBranch], {
|
|
19289
|
+
cwd: mirrorPath,
|
|
19290
|
+
reject: true
|
|
19291
|
+
});
|
|
19292
|
+
const entry = {
|
|
19293
|
+
watchedRepoId,
|
|
19294
|
+
mirrorPath,
|
|
19295
|
+
sourcePath: repoPath,
|
|
19296
|
+
repoOwner: target.repoOwner,
|
|
19297
|
+
repoName: target.repoName
|
|
19298
|
+
};
|
|
19299
|
+
await registerLocalRepo(entry);
|
|
19300
|
+
return { ok: true, entry };
|
|
19301
|
+
}
|
|
19302
|
+
async function runLocalWatchList() {
|
|
19303
|
+
console.log(formatWatchList(await loadLocalRepos()));
|
|
19304
|
+
return 0;
|
|
19305
|
+
}
|
|
19306
|
+
async function runLocalWatchStop(slug) {
|
|
19307
|
+
const repos = await loadLocalRepos();
|
|
19308
|
+
const matches = findLocalReposBySlug(repos, slug);
|
|
19309
|
+
if (matches.length === 0) {
|
|
19310
|
+
console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
|
|
19311
|
+
console.error("Run `archme local-watch --list` to see what is.");
|
|
19312
|
+
return 78;
|
|
19313
|
+
}
|
|
19314
|
+
await saveLocalRepos(removeLocalReposBySlug(repos, slug));
|
|
19315
|
+
for (const m of matches) {
|
|
19316
|
+
console.log(`Stopped watching ${slugOf(m)}.`);
|
|
19317
|
+
}
|
|
19318
|
+
console.log(
|
|
19319
|
+
`
|
|
19320
|
+
The architecture already on the board is untouched \u2014 this only stops new commits
|
|
19321
|
+
from being picked up. Re-watch with: archme local-watch ${matches[0].sourcePath}`
|
|
19322
|
+
);
|
|
19323
|
+
return 0;
|
|
19324
|
+
}
|
|
19325
|
+
async function runLocalWatchResume(slug) {
|
|
19326
|
+
const repos = await loadLocalRepos();
|
|
19327
|
+
const matches = findLocalReposBySlug(repos, slug);
|
|
19328
|
+
if (matches.length === 0) {
|
|
19329
|
+
console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
|
|
19330
|
+
return 78;
|
|
19331
|
+
}
|
|
19332
|
+
const halted = matches.filter((m) => m.haltedAt);
|
|
19333
|
+
if (halted.length === 0) {
|
|
19334
|
+
console.log(`${slug} is not halted \u2014 nothing to resume.`);
|
|
19335
|
+
return 0;
|
|
19336
|
+
}
|
|
19337
|
+
let next = repos;
|
|
19338
|
+
for (const m of halted) next = clearHalt(next, m.watchedRepoId);
|
|
19339
|
+
await saveLocalRepos(next);
|
|
19340
|
+
for (const m of halted) {
|
|
19341
|
+
console.log(`Resumed ${slugOf(m)} \u2014 was: ${m.haltReason ?? "unknown reason"}`);
|
|
19342
|
+
}
|
|
19343
|
+
const stillTerminal = halted.some((m) => m.haltCode === "job-terminal");
|
|
19344
|
+
if (stillTerminal) {
|
|
19345
|
+
console.log(
|
|
19346
|
+
"\nNote: at least one of these stopped because a job failed outright. Resuming\ndoes not re-run a failed job \u2014 polling will stop again on the next tick.\nThat failure is visible to the ArchMe team; get in touch if it is blocking you."
|
|
19347
|
+
);
|
|
19348
|
+
} else {
|
|
19349
|
+
console.log("\nPolling resumes from the recorded commit on the next tick.");
|
|
19350
|
+
}
|
|
19351
|
+
return 0;
|
|
19352
|
+
}
|
|
19353
|
+
var DEFAULT_CLONE_BASE_DIR2;
|
|
19354
|
+
var init_localWatch = __esm({
|
|
19355
|
+
"src/daemon/localWatch.ts"() {
|
|
19356
|
+
"use strict";
|
|
19357
|
+
init_execa();
|
|
19358
|
+
init_defaults();
|
|
19359
|
+
init_github_remote();
|
|
19360
|
+
init_run_scan();
|
|
19361
|
+
init_authedClient();
|
|
19362
|
+
init_watchReadiness();
|
|
19363
|
+
init_localGit();
|
|
19364
|
+
init_localRepos();
|
|
19365
|
+
init_branchMarker();
|
|
19366
|
+
init_localWatchList();
|
|
19367
|
+
init_localWatchResolve();
|
|
19368
|
+
init_prompt();
|
|
19369
|
+
DEFAULT_CLONE_BASE_DIR2 = join21(homedir6(), ".archme-job-runner", "clones");
|
|
19370
|
+
}
|
|
19371
|
+
});
|
|
19372
|
+
|
|
17800
19373
|
// src/init/plan.ts
|
|
17801
19374
|
function shouldRunScan(d) {
|
|
17802
19375
|
if (d.skipScan) return false;
|
|
@@ -17826,9 +19399,9 @@ var init_plan = __esm({
|
|
|
17826
19399
|
});
|
|
17827
19400
|
|
|
17828
19401
|
// src/init/run-init.ts
|
|
17829
|
-
import { mkdtemp as mkdtemp4, rm as rm8, writeFile as
|
|
19402
|
+
import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile12 } from "node:fs/promises";
|
|
17830
19403
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
17831
|
-
import { join as
|
|
19404
|
+
import { join as join22 } from "node:path";
|
|
17832
19405
|
function initScanBranch(defaultBranch) {
|
|
17833
19406
|
return normalizeScanBranchName(defaultBranch);
|
|
17834
19407
|
}
|
|
@@ -17840,10 +19413,10 @@ async function defaultFetchInstallScript(url) {
|
|
|
17840
19413
|
return await resp.text();
|
|
17841
19414
|
}
|
|
17842
19415
|
async function defaultRunInstallScript(script, cwd) {
|
|
17843
|
-
const dir = await mkdtemp4(
|
|
17844
|
-
const scriptPath =
|
|
19416
|
+
const dir = await mkdtemp4(join22(tmpdir4(), "archme-init-"));
|
|
19417
|
+
const scriptPath = join22(dir, "install-agent.sh");
|
|
17845
19418
|
try {
|
|
17846
|
-
await
|
|
19419
|
+
await writeFile12(scriptPath, script, { mode: 448 });
|
|
17847
19420
|
await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
|
|
17848
19421
|
} finally {
|
|
17849
19422
|
await rm8(dir, { recursive: true, force: true });
|
|
@@ -17864,8 +19437,8 @@ async function runInit(opts, deps = {}) {
|
|
|
17864
19437
|
const measure = deps.measure ?? measureRepo;
|
|
17865
19438
|
const watchRepo = deps.watchRepo ?? watchRepoForProject;
|
|
17866
19439
|
const cfg = resolveConfig();
|
|
17867
|
-
const
|
|
17868
|
-
const repoPath =
|
|
19440
|
+
const git4 = await gitContext(opts.repoPath);
|
|
19441
|
+
const repoPath = git4.root;
|
|
17869
19442
|
const { credentials } = await loadCreds(cfg.supabaseUrl);
|
|
17870
19443
|
const signedIn = credentials.length > 0;
|
|
17871
19444
|
if (!signedIn) {
|
|
@@ -17924,8 +19497,9 @@ async function runInit(opts, deps = {}) {
|
|
|
17924
19497
|
supabase: authed.supabase,
|
|
17925
19498
|
orgId: authed.orgId,
|
|
17926
19499
|
projectId: project.projectId,
|
|
17927
|
-
repoSlug:
|
|
17928
|
-
branch:
|
|
19500
|
+
repoSlug: git4.repoSlug,
|
|
19501
|
+
branch: git4.branch,
|
|
19502
|
+
archmeBranch: scanBranch,
|
|
17929
19503
|
log
|
|
17930
19504
|
});
|
|
17931
19505
|
if (!watched.watched && watched.reason) {
|
|
@@ -17975,28 +19549,79 @@ Your board: ${webUrl}`);
|
|
|
17975
19549
|
log(`
|
|
17976
19550
|
\u2713 Done. Your architecture: ${finalUrl}`);
|
|
17977
19551
|
log(" Restart Claude Code in this repo to pick up the archme MCP server.");
|
|
17978
|
-
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
log
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
17987
|
-
|
|
17988
|
-
|
|
17989
|
-
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
19552
|
+
const plan = await chooseKeepCurrent({
|
|
19553
|
+
alreadyWatched: watched.watched,
|
|
19554
|
+
yes: Boolean(opts.yes),
|
|
19555
|
+
repoSlug: git4.repoSlug,
|
|
19556
|
+
choose: deps.choose ?? promptChoice,
|
|
19557
|
+
log
|
|
19558
|
+
});
|
|
19559
|
+
const outcome = await applyKeepCurrentPlan(
|
|
19560
|
+
plan,
|
|
19561
|
+
{
|
|
19562
|
+
startGitHubAppInstall: () => (deps.buildInstallUrl ?? buildInstallUrl)({
|
|
19563
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
19564
|
+
accessToken: getCurrentAccessToken() ?? "",
|
|
19565
|
+
orgId: authed.orgId
|
|
19566
|
+
}),
|
|
19567
|
+
waitForInstallation: () => (deps.waitForInstallation ?? waitForInstallation)({
|
|
19568
|
+
hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed.supabase, authed.orgId),
|
|
19569
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
19570
|
+
now: () => Date.now(),
|
|
19571
|
+
log
|
|
19572
|
+
}),
|
|
19573
|
+
watchRepo: async () => {
|
|
19574
|
+
const w = await watchRepo({
|
|
19575
|
+
supabase: authed.supabase,
|
|
19576
|
+
orgId: authed.orgId,
|
|
19577
|
+
projectId: project.projectId,
|
|
19578
|
+
repoSlug: git4.repoSlug,
|
|
19579
|
+
branch: git4.branch,
|
|
19580
|
+
archmeBranch: scanBranch,
|
|
19581
|
+
log
|
|
19582
|
+
});
|
|
19583
|
+
return { watched: w.watched, reason: w.reason };
|
|
19584
|
+
},
|
|
19585
|
+
registerLocalWatch: async () => {
|
|
19586
|
+
const target = resolveInitTarget({
|
|
19587
|
+
originUrl: git4.originUrl,
|
|
19588
|
+
detectedDefaultBranch: git4.branch,
|
|
19589
|
+
// The branch this run actually scanned. Deriving it from the git
|
|
19590
|
+
// branch instead would point the poller at `<checkout>-scan` while
|
|
19591
|
+
// the marker sits on `<default>-scan` — so registering would fail
|
|
19592
|
+
// on a feature-branch checkout, immediately after a scan that
|
|
19593
|
+
// succeeded.
|
|
19594
|
+
archmeBranchOverride: scanBranch
|
|
19595
|
+
});
|
|
19596
|
+
if (!target.ok) return { ok: false, reason: target.error };
|
|
19597
|
+
const res = await (deps.registerLocalWatch ?? registerLocalWatch)({
|
|
19598
|
+
supabase: authed.supabase,
|
|
19599
|
+
orgId: authed.orgId,
|
|
19600
|
+
projectId: project.projectId,
|
|
19601
|
+
repoPath: git4.root,
|
|
19602
|
+
originUrl: git4.originUrl,
|
|
19603
|
+
target: target.target,
|
|
19604
|
+
log
|
|
19605
|
+
});
|
|
19606
|
+
return res.ok ? { ok: true } : { ok: false, reason: res.reason };
|
|
19607
|
+
},
|
|
19608
|
+
log
|
|
19609
|
+
},
|
|
19610
|
+
watched.watched
|
|
19611
|
+
);
|
|
19612
|
+
if (!outcome.wired && outcome.reason) {
|
|
17994
19613
|
log("");
|
|
17995
19614
|
log("Not yet set up for automatic updates:");
|
|
17996
|
-
log(` ${
|
|
17997
|
-
log(" Until that is resolved, this architecture will not track new");
|
|
17998
|
-
log(" commits \u2014 re-run `archme init` once it is.");
|
|
19615
|
+
log(` ${outcome.reason}`);
|
|
19616
|
+
log(" Until that is resolved, this architecture will not track new commits.");
|
|
17999
19617
|
}
|
|
19618
|
+
log(
|
|
19619
|
+
formatKeepCurrentPlan(plan, {
|
|
19620
|
+
repoSlug: git4.repoSlug,
|
|
19621
|
+
boardUrl: finalUrl,
|
|
19622
|
+
localWatchRegistered: outcome.localWatchRegistered
|
|
19623
|
+
})
|
|
19624
|
+
);
|
|
18000
19625
|
}
|
|
18001
19626
|
return {
|
|
18002
19627
|
exitCode: result.exitCode,
|
|
@@ -18020,6 +19645,12 @@ var init_run_init = __esm({
|
|
|
18020
19645
|
init_watchRepo();
|
|
18021
19646
|
init_gitContext();
|
|
18022
19647
|
init_web_url();
|
|
19648
|
+
init_bearer();
|
|
19649
|
+
init_keepCurrent();
|
|
19650
|
+
init_keepCurrentFlow();
|
|
19651
|
+
init_githubAppSetup();
|
|
19652
|
+
init_prompt();
|
|
19653
|
+
init_localWatch();
|
|
18023
19654
|
init_plan();
|
|
18024
19655
|
}
|
|
18025
19656
|
});
|
|
@@ -18111,7 +19742,7 @@ __export(dispatch_exports4, {
|
|
|
18111
19742
|
dispatchInit: () => dispatchInit,
|
|
18112
19743
|
printInitHelp: () => printInitHelp
|
|
18113
19744
|
});
|
|
18114
|
-
import { resolve as
|
|
19745
|
+
import { resolve as resolve4 } from "node:path";
|
|
18115
19746
|
function printInitHelp() {
|
|
18116
19747
|
console.log(`archme init \u2014 guided first-time setup for this repo
|
|
18117
19748
|
|
|
@@ -18149,7 +19780,7 @@ async function dispatchInit(argv) {
|
|
|
18149
19780
|
}
|
|
18150
19781
|
try {
|
|
18151
19782
|
const result = await runInit({
|
|
18152
|
-
repoPath:
|
|
19783
|
+
repoPath: resolve4(parsed.args.repoPath ?? process.cwd()),
|
|
18153
19784
|
project: parsed.args.project,
|
|
18154
19785
|
appOrigin: parsed.args.appOrigin,
|
|
18155
19786
|
force: parsed.args.force,
|
|
@@ -18157,8 +19788,8 @@ async function dispatchInit(argv) {
|
|
|
18157
19788
|
yes: parsed.args.yes || parsed.args.noInteractive
|
|
18158
19789
|
});
|
|
18159
19790
|
return result.exitCode;
|
|
18160
|
-
} catch (
|
|
18161
|
-
console.error(`archme init: ${
|
|
19791
|
+
} catch (err2) {
|
|
19792
|
+
console.error(`archme init: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
18162
19793
|
return EX_INFRA2;
|
|
18163
19794
|
}
|
|
18164
19795
|
}
|
|
@@ -18174,6 +19805,620 @@ var init_dispatch4 = __esm({
|
|
|
18174
19805
|
}
|
|
18175
19806
|
});
|
|
18176
19807
|
|
|
19808
|
+
// src/daemon/localWatchDispatch.ts
|
|
19809
|
+
var localWatchDispatch_exports = {};
|
|
19810
|
+
__export(localWatchDispatch_exports, {
|
|
19811
|
+
EX_INFRA: () => EX_INFRA3,
|
|
19812
|
+
EX_OK: () => EX_OK3,
|
|
19813
|
+
EX_USAGE: () => EX_USAGE3,
|
|
19814
|
+
dispatchLocalWatch: () => dispatchLocalWatch,
|
|
19815
|
+
parseLocalWatchArgs: () => parseLocalWatchArgs,
|
|
19816
|
+
printLocalWatchHelp: () => printLocalWatchHelp
|
|
19817
|
+
});
|
|
19818
|
+
function err(message) {
|
|
19819
|
+
return { kind: "error", message: `archme local-watch: ${message}` };
|
|
19820
|
+
}
|
|
19821
|
+
function parseLocalWatchArgs(argv) {
|
|
19822
|
+
if (argv.some((a2) => a2 === "--help" || a2 === "-h" || a2 === "help")) return { kind: "help" };
|
|
19823
|
+
let repoPath;
|
|
19824
|
+
let project;
|
|
19825
|
+
let branch;
|
|
19826
|
+
let yes = false;
|
|
19827
|
+
let list = false;
|
|
19828
|
+
let stop;
|
|
19829
|
+
let resume;
|
|
19830
|
+
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
19831
|
+
const arg = argv[i2];
|
|
19832
|
+
const next = argv[i2 + 1];
|
|
19833
|
+
if (arg === "--project") {
|
|
19834
|
+
if (!next) return err("--project needs an org/slug");
|
|
19835
|
+
if (!PROJECT_SLUG_RE3.test(next)) {
|
|
19836
|
+
return err(
|
|
19837
|
+
`--project '${next}' must be \`org/slug\` (exactly two non-empty segments separated by /)`
|
|
19838
|
+
);
|
|
19839
|
+
}
|
|
19840
|
+
project = next;
|
|
19841
|
+
i2++;
|
|
19842
|
+
} else if (arg === "--branch") {
|
|
19843
|
+
if (!next) return err("--branch needs a branch name");
|
|
19844
|
+
branch = next;
|
|
19845
|
+
i2++;
|
|
19846
|
+
} else if (arg === "--yes" || arg === "--no-interactive") {
|
|
19847
|
+
yes = true;
|
|
19848
|
+
} else if (arg === "--list") {
|
|
19849
|
+
list = true;
|
|
19850
|
+
} else if (arg === "--stop" || arg === "--resume") {
|
|
19851
|
+
if (!next || next.startsWith("-")) return err(`${arg} needs a repo (owner/name)`);
|
|
19852
|
+
if (!REPO_SLUG_RE.test(next)) {
|
|
19853
|
+
return err(`${arg} '${next}' must be \`owner/name\` \u2014 that is how repos are registered`);
|
|
19854
|
+
}
|
|
19855
|
+
if (arg === "--stop") stop = next;
|
|
19856
|
+
else resume = next;
|
|
19857
|
+
i2++;
|
|
19858
|
+
} else if (arg && arg.startsWith("-")) {
|
|
19859
|
+
return err(`unknown flag '${arg}'`);
|
|
19860
|
+
} else if (arg && repoPath === void 0) {
|
|
19861
|
+
repoPath = arg;
|
|
19862
|
+
} else if (arg) {
|
|
19863
|
+
return err(`unexpected argument '${arg}'`);
|
|
19864
|
+
}
|
|
19865
|
+
}
|
|
19866
|
+
const modes = [list && "--list", stop && "--stop", resume && "--resume"].filter(Boolean);
|
|
19867
|
+
if (modes.length > 1) {
|
|
19868
|
+
return err(`${modes.join(" and ")} do different things \u2014 pass one of them at once, not both`);
|
|
19869
|
+
}
|
|
19870
|
+
if (list) return { kind: "list" };
|
|
19871
|
+
if (stop) return { kind: "stop", repo: stop };
|
|
19872
|
+
if (resume) return { kind: "resume", repo: resume };
|
|
19873
|
+
return { kind: "register", repoPath, project, branch, yes };
|
|
19874
|
+
}
|
|
19875
|
+
function printLocalWatchHelp() {
|
|
19876
|
+
console.log(`archme local-watch \u2014 keep ArchMe current without a GitHub App
|
|
19877
|
+
|
|
19878
|
+
Registers a repo so this machine polls \`origin\` and feeds new commits into
|
|
19879
|
+
ArchMe, one commit at a time. No GitHub App, no admin approval \u2014 but the
|
|
19880
|
+
updates only happen while your daemon is running. Requires a github.com origin.
|
|
19881
|
+
|
|
19882
|
+
Your working checkout is read exactly once (to find its origin URL and seed a
|
|
19883
|
+
mirror); everything afterwards runs against a daemon-owned copy, so ArchMe
|
|
19884
|
+
never touches your working tree.
|
|
19885
|
+
|
|
19886
|
+
Usage:
|
|
19887
|
+
archme local-watch [repo-path] Register a repo (defaults to the one you're in)
|
|
19888
|
+
archme local-watch --list Show what this machine is watching
|
|
19889
|
+
archme local-watch --stop <owner/name>
|
|
19890
|
+
archme local-watch --resume <owner/name>
|
|
19891
|
+
|
|
19892
|
+
Options:
|
|
19893
|
+
--project <org/slug> Project to write to (default: derived from the repo name)
|
|
19894
|
+
--branch <name> Branch to track (default: the repo's default branch)
|
|
19895
|
+
--yes Don't ask for confirmation (needs an explicit repo-path)
|
|
19896
|
+
|
|
19897
|
+
Watching queues work; the daemon does it. After registering: archme daemon start`);
|
|
19898
|
+
}
|
|
19899
|
+
async function dispatchLocalWatch(argv) {
|
|
19900
|
+
const parsed = parseLocalWatchArgs(argv);
|
|
19901
|
+
if (parsed.kind === "help") {
|
|
19902
|
+
printLocalWatchHelp();
|
|
19903
|
+
return EX_OK3;
|
|
19904
|
+
}
|
|
19905
|
+
if (parsed.kind === "error") {
|
|
19906
|
+
console.error(parsed.message);
|
|
19907
|
+
printLocalWatchHelp();
|
|
19908
|
+
return EX_USAGE3;
|
|
19909
|
+
}
|
|
19910
|
+
const mod = await Promise.resolve().then(() => (init_localWatch(), localWatch_exports));
|
|
19911
|
+
switch (parsed.kind) {
|
|
19912
|
+
case "list":
|
|
19913
|
+
return await mod.runLocalWatchList();
|
|
19914
|
+
case "stop":
|
|
19915
|
+
return await mod.runLocalWatchStop(parsed.repo);
|
|
19916
|
+
case "resume":
|
|
19917
|
+
return await mod.runLocalWatchResume(parsed.repo);
|
|
19918
|
+
case "register":
|
|
19919
|
+
return await mod.runLocalWatch(parsed);
|
|
19920
|
+
}
|
|
19921
|
+
}
|
|
19922
|
+
var EX_OK3, EX_INFRA3, EX_USAGE3, PROJECT_SLUG_RE3, REPO_SLUG_RE;
|
|
19923
|
+
var init_localWatchDispatch = __esm({
|
|
19924
|
+
"src/daemon/localWatchDispatch.ts"() {
|
|
19925
|
+
"use strict";
|
|
19926
|
+
EX_OK3 = 0;
|
|
19927
|
+
EX_INFRA3 = 3;
|
|
19928
|
+
EX_USAGE3 = 78;
|
|
19929
|
+
PROJECT_SLUG_RE3 = /^[^/\s]+\/[^/\s]+$/;
|
|
19930
|
+
REPO_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
19931
|
+
}
|
|
19932
|
+
});
|
|
19933
|
+
|
|
19934
|
+
// src/doctor/collect.ts
|
|
19935
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
19936
|
+
import { join as join23 } from "node:path";
|
|
19937
|
+
function binaryPresent(name) {
|
|
19938
|
+
try {
|
|
19939
|
+
requireBinary(name);
|
|
19940
|
+
return true;
|
|
19941
|
+
} catch {
|
|
19942
|
+
return false;
|
|
19943
|
+
}
|
|
19944
|
+
}
|
|
19945
|
+
async function git3(cwd, args) {
|
|
19946
|
+
const r = await execa("git", args, { cwd, reject: false });
|
|
19947
|
+
return r.exitCode === 0 ? r.stdout.trim() || null : null;
|
|
19948
|
+
}
|
|
19949
|
+
async function readMcpPin(repoRoot) {
|
|
19950
|
+
const empty = { installed: false, pin: { project: null, branch: null } };
|
|
19951
|
+
try {
|
|
19952
|
+
const raw = await readFile13(join23(repoRoot, ".mcp.json"), "utf8");
|
|
19953
|
+
const parsed = JSON.parse(raw);
|
|
19954
|
+
const servers = parsed.mcpServers ?? {};
|
|
19955
|
+
const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
|
|
19956
|
+
if (!entry) return empty;
|
|
19957
|
+
const url = entry[1]?.url;
|
|
19958
|
+
if (!url) return { installed: true, pin: { project: null, branch: null } };
|
|
19959
|
+
const params = new URL(url).searchParams;
|
|
19960
|
+
return {
|
|
19961
|
+
installed: true,
|
|
19962
|
+
pin: { project: params.get("project"), branch: params.get("branch") }
|
|
19963
|
+
};
|
|
19964
|
+
} catch {
|
|
19965
|
+
return empty;
|
|
19966
|
+
}
|
|
19967
|
+
}
|
|
19968
|
+
async function isDaemonRunning() {
|
|
19969
|
+
const r = await execa("pgrep", ["-f", "archme.*daemon"], { reject: false });
|
|
19970
|
+
return r.exitCode === 0 && Boolean(r.stdout.trim());
|
|
19971
|
+
}
|
|
19972
|
+
async function collectFacts(cwd) {
|
|
19973
|
+
const facts = {
|
|
19974
|
+
cliVersion: await readDaemonVersion(),
|
|
19975
|
+
// `requireBinary` throws when absent; doctor's whole job is to report that
|
|
19976
|
+
// calmly rather than die on it.
|
|
19977
|
+
claudeInstalled: binaryPresent("claude"),
|
|
19978
|
+
gitInstalled: binaryPresent("git"),
|
|
19979
|
+
signedIn: false,
|
|
19980
|
+
identity: null,
|
|
19981
|
+
orgId: null,
|
|
19982
|
+
inRepo: false,
|
|
19983
|
+
repoRoot: null,
|
|
19984
|
+
repoSlug: null,
|
|
19985
|
+
gitBranch: null,
|
|
19986
|
+
agentConfigInstalled: false,
|
|
19987
|
+
pinnedProject: null,
|
|
19988
|
+
pinnedBranch: null,
|
|
19989
|
+
pinnedOrgVisible: null,
|
|
19990
|
+
projectExists: null,
|
|
19991
|
+
componentCount: null,
|
|
19992
|
+
markerSha: null,
|
|
19993
|
+
watchedBy: "unknown",
|
|
19994
|
+
localRepos: [],
|
|
19995
|
+
daemonRunning: await isDaemonRunning()
|
|
19996
|
+
};
|
|
19997
|
+
const root = facts.gitInstalled ? await git3(cwd, ["rev-parse", "--show-toplevel"]) : null;
|
|
19998
|
+
if (root) {
|
|
19999
|
+
facts.inRepo = true;
|
|
20000
|
+
facts.repoRoot = root;
|
|
20001
|
+
facts.gitBranch = await git3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
20002
|
+
const originUrl = await git3(root, ["remote", "get-url", "origin"]);
|
|
20003
|
+
facts.repoSlug = originUrl ? parseGitHubRepo(originUrl) : null;
|
|
20004
|
+
const mcp = await readMcpPin(root);
|
|
20005
|
+
facts.agentConfigInstalled = mcp.installed;
|
|
20006
|
+
facts.pinnedProject = mcp.pin.project;
|
|
20007
|
+
facts.pinnedBranch = mcp.pin.branch;
|
|
20008
|
+
}
|
|
20009
|
+
const registry = await loadLocalRepos();
|
|
20010
|
+
const forThisRepo = facts.repoSlug ? registry.filter((r) => slugOf(r).toLowerCase() === facts.repoSlug.toLowerCase()) : [];
|
|
20011
|
+
facts.localRepos = forThisRepo.map(
|
|
20012
|
+
(r) => ({
|
|
20013
|
+
slug: slugOf(r),
|
|
20014
|
+
halted: Boolean(r.haltedAt),
|
|
20015
|
+
haltReason: r.haltReason
|
|
20016
|
+
})
|
|
20017
|
+
);
|
|
20018
|
+
try {
|
|
20019
|
+
const cfg = resolveConfig();
|
|
20020
|
+
const authed = await createAuthedClient({
|
|
20021
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
20022
|
+
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
20023
|
+
clientId: cfg.archmeDaemonClientId,
|
|
20024
|
+
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
20025
|
+
});
|
|
20026
|
+
if (!authed.orgId) return facts;
|
|
20027
|
+
facts.signedIn = true;
|
|
20028
|
+
facts.orgId = authed.orgId;
|
|
20029
|
+
facts.identity = authed.identity;
|
|
20030
|
+
const supabase = authed.supabase;
|
|
20031
|
+
const slug = facts.pinnedProject;
|
|
20032
|
+
if (slug) {
|
|
20033
|
+
const [orgSlug, projectSlug] = slug.split("/");
|
|
20034
|
+
const { data: org } = await supabase.from("organizations").select("id").eq("slug", orgSlug ?? "").maybeSingle();
|
|
20035
|
+
facts.pinnedOrgVisible = Boolean(org);
|
|
20036
|
+
const { data: project } = org ? await supabase.from("projects").select("id").eq("org_id", org.id).eq("slug", projectSlug ?? "").is("deleted_at", null).maybeSingle() : { data: null };
|
|
20037
|
+
if (!org) {
|
|
20038
|
+
facts.projectExists = null;
|
|
20039
|
+
} else if (!project) {
|
|
20040
|
+
facts.projectExists = false;
|
|
20041
|
+
} else {
|
|
20042
|
+
facts.projectExists = true;
|
|
20043
|
+
const branch = facts.pinnedBranch ?? "main";
|
|
20044
|
+
const { count: count2 } = await supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("branch", branch);
|
|
20045
|
+
facts.componentCount = count2 ?? null;
|
|
20046
|
+
const marker = await readBranchMarker(supabase, project.id, branch);
|
|
20047
|
+
facts.markerSha = marker.sha;
|
|
20048
|
+
}
|
|
20049
|
+
}
|
|
20050
|
+
if (facts.localRepos.length > 0) {
|
|
20051
|
+
facts.watchedBy = "local";
|
|
20052
|
+
} else if (facts.repoSlug) {
|
|
20053
|
+
const [owner, name] = facts.repoSlug.split("/");
|
|
20054
|
+
const { data: watched } = await supabase.from("watched_repos").select("source_kind, enabled, commit_update_enabled").eq("repo_owner", owner ?? "").eq("repo_name", name ?? "").is("deleted_at", null);
|
|
20055
|
+
const rows = watched ?? [];
|
|
20056
|
+
const live = rows.find((r) => r.commit_update_enabled);
|
|
20057
|
+
facts.watchedBy = live ? live.source_kind === "local" ? "local" : "github" : "none";
|
|
20058
|
+
}
|
|
20059
|
+
} catch {
|
|
20060
|
+
}
|
|
20061
|
+
return facts;
|
|
20062
|
+
}
|
|
20063
|
+
var init_collect = __esm({
|
|
20064
|
+
"src/doctor/collect.ts"() {
|
|
20065
|
+
"use strict";
|
|
20066
|
+
init_execa();
|
|
20067
|
+
init_defaults();
|
|
20068
|
+
init_version();
|
|
20069
|
+
init_preflight();
|
|
20070
|
+
init_branchMarker();
|
|
20071
|
+
init_localRepos();
|
|
20072
|
+
init_github_remote();
|
|
20073
|
+
init_authedClient();
|
|
20074
|
+
}
|
|
20075
|
+
});
|
|
20076
|
+
|
|
20077
|
+
// src/doctor/facts.ts
|
|
20078
|
+
function diagnose(f) {
|
|
20079
|
+
const issues = [];
|
|
20080
|
+
if (!f.claudeInstalled) {
|
|
20081
|
+
issues.push({
|
|
20082
|
+
severity: "error",
|
|
20083
|
+
title: "Claude Code is not installed",
|
|
20084
|
+
detail: "archme runs Claude Code locally to read your diff and update the architecture. Without it, scans and every daemon job fail at the spawn.",
|
|
20085
|
+
fix: "npm i -g @anthropic-ai/claude-code"
|
|
20086
|
+
});
|
|
20087
|
+
}
|
|
20088
|
+
if (!f.gitInstalled) {
|
|
20089
|
+
issues.push({
|
|
20090
|
+
severity: "error",
|
|
20091
|
+
title: "git is not installed",
|
|
20092
|
+
detail: "archme clones and diffs repositories with git.",
|
|
20093
|
+
fix: "macOS: xcode-select --install \xB7 Debian/Ubuntu: sudo apt-get install git"
|
|
20094
|
+
});
|
|
20095
|
+
}
|
|
20096
|
+
if (f.cliVersion === null) {
|
|
20097
|
+
issues.push({
|
|
20098
|
+
severity: "error",
|
|
20099
|
+
title: "this install cannot read its own version",
|
|
20100
|
+
detail: "The daemon reports its version on every job claim, and the server treats an unknown version as below any minimum. If a floor is ever set, this install stops claiming jobs silently \u2014 no error, just an idle daemon.",
|
|
20101
|
+
fix: "npm i -g @archme/cli"
|
|
20102
|
+
});
|
|
20103
|
+
}
|
|
20104
|
+
if (!f.signedIn) {
|
|
20105
|
+
issues.push({
|
|
20106
|
+
severity: "error",
|
|
20107
|
+
title: "not signed in",
|
|
20108
|
+
detail: "Your project, architecture and watch status can't be checked until this machine is paired with an ArchMe account.",
|
|
20109
|
+
fix: "archme login"
|
|
20110
|
+
});
|
|
20111
|
+
return issues;
|
|
20112
|
+
}
|
|
20113
|
+
if (!f.inRepo) {
|
|
20114
|
+
issues.push({
|
|
20115
|
+
severity: "warn",
|
|
20116
|
+
title: "not inside a git repository",
|
|
20117
|
+
detail: "Run this from the repo you want ArchMe to track, so it can check that repo's setup.",
|
|
20118
|
+
fix: "cd <your repo> && archme doctor"
|
|
20119
|
+
});
|
|
20120
|
+
return issues;
|
|
20121
|
+
}
|
|
20122
|
+
if (f.pinnedOrgVisible === false) {
|
|
20123
|
+
issues.push({
|
|
20124
|
+
severity: "error",
|
|
20125
|
+
title: "this repo is pinned to a project your account cannot see",
|
|
20126
|
+
detail: `.mcp.json pins \`${f.pinnedProject}\`, but this machine is signed in to an account that is not a member of that organisation. Nothing here can read or update that project. The architecture may well exist \u2014 you just cannot reach it from this login.`,
|
|
20127
|
+
fix: "archme login (choose the org that owns the project), or re-run `archme init` to pin one you can reach"
|
|
20128
|
+
});
|
|
20129
|
+
return issues;
|
|
20130
|
+
}
|
|
20131
|
+
if (f.projectExists === false || f.componentCount === 0) {
|
|
20132
|
+
issues.push({
|
|
20133
|
+
severity: "error",
|
|
20134
|
+
title: "this repo has no architecture yet",
|
|
20135
|
+
detail: "Nothing has been scanned, so there is nothing for updates to keep current.",
|
|
20136
|
+
fix: "archme init"
|
|
20137
|
+
});
|
|
20138
|
+
return issues;
|
|
20139
|
+
}
|
|
20140
|
+
if (!f.agentConfigInstalled) {
|
|
20141
|
+
issues.push({
|
|
20142
|
+
severity: "warn",
|
|
20143
|
+
title: "agent config is missing from this repo",
|
|
20144
|
+
detail: "Without `.mcp.json`, Claude Code in this repo can't read or write the architecture while you work. Updates from commits still happen.",
|
|
20145
|
+
fix: "archme init"
|
|
20146
|
+
});
|
|
20147
|
+
}
|
|
20148
|
+
if (f.projectExists === true && f.markerSha === null) {
|
|
20149
|
+
issues.push({
|
|
20150
|
+
severity: "error",
|
|
20151
|
+
title: "the last scan left no resume point",
|
|
20152
|
+
detail: 'Updates diff from the commit the architecture reflects, and that commit was never recorded. Every job will fail with "no usable base" until a scan completes cleanly.',
|
|
20153
|
+
fix: "archme init --force"
|
|
20154
|
+
});
|
|
20155
|
+
}
|
|
20156
|
+
if (f.watchedBy === "none") {
|
|
20157
|
+
issues.push({
|
|
20158
|
+
severity: "error",
|
|
20159
|
+
title: "nothing is watching this repo",
|
|
20160
|
+
detail: "The architecture exists but will not track new commits \u2014 the board stays as it is while the code moves on, which looks current and isn't.",
|
|
20161
|
+
fix: "archme init"
|
|
20162
|
+
});
|
|
20163
|
+
}
|
|
20164
|
+
const halted = f.localRepos.filter((r) => r.halted);
|
|
20165
|
+
for (const r of halted) {
|
|
20166
|
+
issues.push({
|
|
20167
|
+
severity: "error",
|
|
20168
|
+
title: `local polling is halted for ${r.slug}`,
|
|
20169
|
+
detail: `Stopped after: ${r.haltReason ?? "an unrecorded failure"}. No new commits are being picked up. It stops rather than skipping the failed commit, which would fold it into the next diff.`,
|
|
20170
|
+
fix: `archme local-watch --resume ${r.slug}`
|
|
20171
|
+
});
|
|
20172
|
+
}
|
|
20173
|
+
if (!f.daemonRunning && f.watchedBy !== "none" && f.watchedBy !== "unknown") {
|
|
20174
|
+
issues.push({
|
|
20175
|
+
severity: "error",
|
|
20176
|
+
title: "the daemon is not running",
|
|
20177
|
+
detail: "Commits are queueing up, but nothing is processing them. Updates only happen while the daemon runs, on a machine that stays on.",
|
|
20178
|
+
fix: "archme daemon start"
|
|
20179
|
+
});
|
|
20180
|
+
}
|
|
20181
|
+
return issues;
|
|
20182
|
+
}
|
|
20183
|
+
function yesNo(v) {
|
|
20184
|
+
return v ? "yes" : "no";
|
|
20185
|
+
}
|
|
20186
|
+
function formatFacts(f) {
|
|
20187
|
+
const lines = ["ArchMe setup"];
|
|
20188
|
+
lines.push("", " This machine");
|
|
20189
|
+
lines.push(` archme version: ${f.cliVersion ?? "UNKNOWN (cannot read its own version)"}`);
|
|
20190
|
+
lines.push(` claude installed: ${yesNo(f.claudeInstalled)}`);
|
|
20191
|
+
lines.push(` git installed: ${yesNo(f.gitInstalled)}`);
|
|
20192
|
+
lines.push(` signed in: ${f.signedIn ? f.identity ?? "yes" : "no"}`);
|
|
20193
|
+
lines.push(` daemon running: ${yesNo(f.daemonRunning)}`);
|
|
20194
|
+
lines.push("", " This repo");
|
|
20195
|
+
if (!f.inRepo) {
|
|
20196
|
+
lines.push(" not inside a git repository");
|
|
20197
|
+
} else {
|
|
20198
|
+
lines.push(` path: ${f.repoRoot}`);
|
|
20199
|
+
lines.push(` repo: ${f.repoSlug ?? "no github.com remote"}`);
|
|
20200
|
+
lines.push(` branch: ${f.gitBranch ?? "unknown"}`);
|
|
20201
|
+
lines.push(` .mcp.json: ${f.agentConfigInstalled ? "installed" : "missing"}`);
|
|
20202
|
+
if (f.pinnedProject) lines.push(` pinned to: ${f.pinnedProject} @ ${f.pinnedBranch ?? "?"}`);
|
|
20203
|
+
}
|
|
20204
|
+
lines.push("", " Architecture");
|
|
20205
|
+
if (!f.signedIn) {
|
|
20206
|
+
lines.push(" unknown \u2014 not signed in");
|
|
20207
|
+
} else {
|
|
20208
|
+
if (f.pinnedOrgVisible === false) {
|
|
20209
|
+
lines.push(` NOT VISIBLE \u2014 ${f.pinnedProject} belongs to another organisation`);
|
|
20210
|
+
}
|
|
20211
|
+
lines.push(` project exists: ${f.projectExists === null ? "unknown" : yesNo(f.projectExists)}`);
|
|
20212
|
+
lines.push(` components: ${f.componentCount === null ? "unknown" : f.componentCount}`);
|
|
20213
|
+
lines.push(
|
|
20214
|
+
` last scanned at: ${f.markerSha ? f.markerSha.slice(0, 7) : "never recorded"}`
|
|
20215
|
+
);
|
|
20216
|
+
lines.push(` updates from: ${describeWatch(f)}`);
|
|
20217
|
+
}
|
|
20218
|
+
for (const r of f.localRepos) {
|
|
20219
|
+
lines.push(
|
|
20220
|
+
` \xB7 ${r.slug}${r.halted ? ` HALTED: ${r.haltReason ?? "unknown"}` : ""}`
|
|
20221
|
+
);
|
|
20222
|
+
}
|
|
20223
|
+
return lines.join("\n");
|
|
20224
|
+
}
|
|
20225
|
+
function describeWatch(f) {
|
|
20226
|
+
switch (f.watchedBy) {
|
|
20227
|
+
case "github":
|
|
20228
|
+
return "GitHub App (pushes and pull requests)";
|
|
20229
|
+
case "local":
|
|
20230
|
+
return "local polling on this machine";
|
|
20231
|
+
case "none":
|
|
20232
|
+
return "NOTHING \u2014 this architecture will not track new commits";
|
|
20233
|
+
default:
|
|
20234
|
+
return "unknown";
|
|
20235
|
+
}
|
|
20236
|
+
}
|
|
20237
|
+
var init_facts = __esm({
|
|
20238
|
+
"src/doctor/facts.ts"() {
|
|
20239
|
+
"use strict";
|
|
20240
|
+
}
|
|
20241
|
+
});
|
|
20242
|
+
|
|
20243
|
+
// src/doctor/prompt.ts
|
|
20244
|
+
function buildDoctorPrompt(facts, issues) {
|
|
20245
|
+
const problems = issues.length === 0 ? "No problems were found." : issues.map((i2) => `- [${i2.severity}] ${i2.title}
|
|
20246
|
+
${i2.detail}
|
|
20247
|
+
Fix: ${i2.fix}`).join("\n");
|
|
20248
|
+
return `You are helping someone understand their ArchMe setup. This is the
|
|
20249
|
+
\`archme doctor\` command.
|
|
20250
|
+
|
|
20251
|
+
${HOW_IT_WORKS}
|
|
20252
|
+
|
|
20253
|
+
## What was measured on this machine
|
|
20254
|
+
|
|
20255
|
+
These facts were gathered by the CLI before you started. They are accurate.
|
|
20256
|
+
Do NOT re-derive them, and do not go looking for them in the filesystem \u2014 you
|
|
20257
|
+
have everything already.
|
|
20258
|
+
|
|
20259
|
+
\`\`\`
|
|
20260
|
+
${formatFacts(facts)}
|
|
20261
|
+
\`\`\`
|
|
20262
|
+
|
|
20263
|
+
## Problems found
|
|
20264
|
+
|
|
20265
|
+
${problems}
|
|
20266
|
+
|
|
20267
|
+
## Your job
|
|
20268
|
+
|
|
20269
|
+
The report above has ALREADY been printed to the user's terminal. Do not repeat
|
|
20270
|
+
it back to them.
|
|
20271
|
+
|
|
20272
|
+
Open with two or three sentences: where they are in the flow above, and the
|
|
20273
|
+
single most important thing to do next (or that nothing needs doing). Then stop
|
|
20274
|
+
and let them ask.
|
|
20275
|
+
|
|
20276
|
+
Rules:
|
|
20277
|
+
- You are READ-ONLY. You cannot fix anything, and you must not offer to. Give
|
|
20278
|
+
them the command to run.
|
|
20279
|
+
- Do not invent state. If they ask something the facts above do not answer, say
|
|
20280
|
+
so and tell them which command would show it.
|
|
20281
|
+
- Never tell them their setup is fine if a problem was listed.
|
|
20282
|
+
- Be brief. They ran a diagnostic, not a tutorial. Depth on request.`;
|
|
20283
|
+
}
|
|
20284
|
+
var HOW_IT_WORKS;
|
|
20285
|
+
var init_prompt2 = __esm({
|
|
20286
|
+
"src/doctor/prompt.ts"() {
|
|
20287
|
+
"use strict";
|
|
20288
|
+
init_facts();
|
|
20289
|
+
HOW_IT_WORKS = `## How ArchMe is meant to work
|
|
20290
|
+
|
|
20291
|
+
1. **Scan** \u2014 \`archme init\` maps the repo into an architecture on the board.
|
|
20292
|
+
This is a one-time snapshot. It runs Claude Code locally, on the user's own
|
|
20293
|
+
Claude subscription. Their code is never sent to ArchMe's servers; only the
|
|
20294
|
+
resulting architecture is.
|
|
20295
|
+
|
|
20296
|
+
2. **Detection** \u2014 something has to notice new commits. Two options:
|
|
20297
|
+
- **GitHub App**: webhooks. Sees pushes AND pull requests, works for the
|
|
20298
|
+
whole team, nothing to keep running. Needs someone to grant repo read
|
|
20299
|
+
access.
|
|
20300
|
+
- **Local polling** (\`archme local-watch\`): this machine fetches the repo on
|
|
20301
|
+
a timer. No GitHub App, no approvals, but no pull-request awareness.
|
|
20302
|
+
|
|
20303
|
+
3. **Execution** \u2014 something has to PROCESS each detected commit. This is the
|
|
20304
|
+
step people miss. Detection only queues work; the queue does nothing on its
|
|
20305
|
+
own. Either the user runs \`archme daemon start\` on a machine that stays on,
|
|
20306
|
+
or they email signup@archme.io and ArchMe runs it for them.
|
|
20307
|
+
|
|
20308
|
+
The failure mode that matters most: an architecture that exists but is not
|
|
20309
|
+
being updated. The board still looks current, so nobody notices it is stale.
|
|
20310
|
+
Detection without execution produces exactly that, which is why step 3 is not
|
|
20311
|
+
optional.`;
|
|
20312
|
+
}
|
|
20313
|
+
});
|
|
20314
|
+
|
|
20315
|
+
// src/doctor/dispatch.ts
|
|
20316
|
+
var dispatch_exports5 = {};
|
|
20317
|
+
__export(dispatch_exports5, {
|
|
20318
|
+
EX_INFRA: () => EX_INFRA4,
|
|
20319
|
+
EX_OK: () => EX_OK4,
|
|
20320
|
+
EX_USAGE: () => EX_USAGE4,
|
|
20321
|
+
dispatchDoctor: () => dispatchDoctor,
|
|
20322
|
+
parseDoctorArgs: () => parseDoctorArgs,
|
|
20323
|
+
printDoctorHelp: () => printDoctorHelp
|
|
20324
|
+
});
|
|
20325
|
+
import { mkdtemp as mkdtemp5, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
|
|
20326
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
20327
|
+
import { join as join24 } from "node:path";
|
|
20328
|
+
function parseDoctorArgs(argv) {
|
|
20329
|
+
const args = { plain: false, help: false };
|
|
20330
|
+
for (const a2 of argv) {
|
|
20331
|
+
if (a2 === "--help" || a2 === "-h" || a2 === "help") args.help = true;
|
|
20332
|
+
else if (a2 === "--plain" || a2 === "--no-agent") args.plain = true;
|
|
20333
|
+
else return { ok: false, message: `archme doctor: unknown flag '${a2}'` };
|
|
20334
|
+
}
|
|
20335
|
+
return { ok: true, args };
|
|
20336
|
+
}
|
|
20337
|
+
function printDoctorHelp() {
|
|
20338
|
+
console.log(`archme doctor \u2014 check this machine's ArchMe setup and explain it
|
|
20339
|
+
|
|
20340
|
+
Reports what is installed, signed in, scanned, watched and running, then hands
|
|
20341
|
+
the results to Claude Code so you can ask about them.
|
|
20342
|
+
|
|
20343
|
+
Read-only: it never changes anything. Anything that needs doing is printed as a
|
|
20344
|
+
command for you to run.
|
|
20345
|
+
|
|
20346
|
+
Usage:
|
|
20347
|
+
archme doctor [--plain]
|
|
20348
|
+
|
|
20349
|
+
Options:
|
|
20350
|
+
--plain Just print the report \u2014 don't start Claude. Use this when Claude
|
|
20351
|
+
Code itself is what's broken.
|
|
20352
|
+
-h, --help`);
|
|
20353
|
+
}
|
|
20354
|
+
async function dispatchDoctor(argv) {
|
|
20355
|
+
const parsed = parseDoctorArgs(argv);
|
|
20356
|
+
if (!parsed.ok) {
|
|
20357
|
+
console.error(parsed.message);
|
|
20358
|
+
printDoctorHelp();
|
|
20359
|
+
return EX_USAGE4;
|
|
20360
|
+
}
|
|
20361
|
+
if (parsed.args.help) {
|
|
20362
|
+
printDoctorHelp();
|
|
20363
|
+
return EX_OK4;
|
|
20364
|
+
}
|
|
20365
|
+
const facts = await collectFacts(process.cwd());
|
|
20366
|
+
const issues = diagnose(facts);
|
|
20367
|
+
console.log(formatFacts(facts));
|
|
20368
|
+
if (issues.length === 0) {
|
|
20369
|
+
console.log("\n\u2713 Everything checks out.");
|
|
20370
|
+
} else {
|
|
20371
|
+
console.log("");
|
|
20372
|
+
for (const issue of issues) {
|
|
20373
|
+
console.log(`${issue.severity === "error" ? "\u2717" : "!"} ${issue.title}`);
|
|
20374
|
+
console.log(` ${issue.detail}`);
|
|
20375
|
+
console.log(` \u2192 ${issue.fix}`);
|
|
20376
|
+
console.log("");
|
|
20377
|
+
}
|
|
20378
|
+
}
|
|
20379
|
+
if (parsed.args.plain) return EX_OK4;
|
|
20380
|
+
try {
|
|
20381
|
+
requireBinary("claude");
|
|
20382
|
+
} catch {
|
|
20383
|
+
console.log(
|
|
20384
|
+
"Install Claude Code to ask about any of this: npm i -g @anthropic-ai/claude-code"
|
|
20385
|
+
);
|
|
20386
|
+
return EX_OK4;
|
|
20387
|
+
}
|
|
20388
|
+
const dir = await mkdtemp5(join24(tmpdir5(), "archme-doctor-"));
|
|
20389
|
+
const mcpConfigPath = join24(dir, "mcp.json");
|
|
20390
|
+
await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
|
|
20391
|
+
try {
|
|
20392
|
+
const result = await spawnClaude({
|
|
20393
|
+
prompt: buildDoctorPrompt(facts, issues),
|
|
20394
|
+
cwd: facts.repoRoot ?? process.cwd(),
|
|
20395
|
+
mcpConfigPath,
|
|
20396
|
+
interactive: true,
|
|
20397
|
+
// Read-only, and no MCP: doctor diagnoses, it does not repair. An LLM
|
|
20398
|
+
// with write or shell access on a machine whose setup is already broken
|
|
20399
|
+
// is a much larger surface than the problem justifies.
|
|
20400
|
+
allowedTools: ["Read", "Glob", "Grep"]
|
|
20401
|
+
});
|
|
20402
|
+
return result.exitCode === 0 ? EX_OK4 : EX_INFRA4;
|
|
20403
|
+
} finally {
|
|
20404
|
+
await rm9(dir, { recursive: true, force: true });
|
|
20405
|
+
}
|
|
20406
|
+
}
|
|
20407
|
+
var EX_OK4, EX_INFRA4, EX_USAGE4;
|
|
20408
|
+
var init_dispatch5 = __esm({
|
|
20409
|
+
"src/doctor/dispatch.ts"() {
|
|
20410
|
+
"use strict";
|
|
20411
|
+
init_collect();
|
|
20412
|
+
init_facts();
|
|
20413
|
+
init_prompt2();
|
|
20414
|
+
init_spawn_claude();
|
|
20415
|
+
init_preflight();
|
|
20416
|
+
EX_OK4 = 0;
|
|
20417
|
+
EX_INFRA4 = 3;
|
|
20418
|
+
EX_USAGE4 = 78;
|
|
20419
|
+
}
|
|
20420
|
+
});
|
|
20421
|
+
|
|
18177
20422
|
// src/auth/logout.ts
|
|
18178
20423
|
var logout_exports = {};
|
|
18179
20424
|
__export(logout_exports, {
|
|
@@ -18187,12 +20432,12 @@ async function runLogout(args) {
|
|
|
18187
20432
|
let url;
|
|
18188
20433
|
try {
|
|
18189
20434
|
url = resolveConfig().supabaseUrl;
|
|
18190
|
-
} catch (
|
|
18191
|
-
if (
|
|
18192
|
-
console.error(
|
|
20435
|
+
} catch (err2) {
|
|
20436
|
+
if (err2 instanceof MissingConfigError) {
|
|
20437
|
+
console.error(err2.message);
|
|
18193
20438
|
return 1;
|
|
18194
20439
|
}
|
|
18195
|
-
throw
|
|
20440
|
+
throw err2;
|
|
18196
20441
|
}
|
|
18197
20442
|
const target = args.find((a2) => !a2.startsWith("-"));
|
|
18198
20443
|
if (!target || args.includes("--all")) {
|
|
@@ -18220,10 +20465,22 @@ var init_logout = __esm({
|
|
|
18220
20465
|
|
|
18221
20466
|
// src/cli.ts
|
|
18222
20467
|
init_enroll();
|
|
18223
|
-
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
20468
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
20469
|
+
"daemon",
|
|
20470
|
+
"analyze",
|
|
20471
|
+
"login",
|
|
20472
|
+
"logout",
|
|
20473
|
+
"keys",
|
|
20474
|
+
"scan",
|
|
20475
|
+
"init",
|
|
20476
|
+
"local-watch",
|
|
20477
|
+
"doctor"
|
|
20478
|
+
]);
|
|
20479
|
+
var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
|
|
18224
20480
|
function route(argv) {
|
|
18225
20481
|
if (argv.length === 0) return { name: "help", args: [] };
|
|
18226
20482
|
const [first, ...rest] = argv;
|
|
20483
|
+
if (first && VERSION_FLAGS.has(first)) return { name: "version", args: [] };
|
|
18227
20484
|
if (!first || !KNOWN_COMMANDS.has(first)) {
|
|
18228
20485
|
return { name: "help", args: [] };
|
|
18229
20486
|
}
|
|
@@ -18287,6 +20544,18 @@ async function main(argv) {
|
|
|
18287
20544
|
const { dispatchDaemon: dispatchDaemon2 } = await Promise.resolve().then(() => (init_dispatch2(), dispatch_exports2));
|
|
18288
20545
|
return await dispatchDaemon2(sub);
|
|
18289
20546
|
}
|
|
20547
|
+
case "version": {
|
|
20548
|
+
const { readDaemonVersion: readDaemonVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
|
|
20549
|
+
const v = await readDaemonVersion2();
|
|
20550
|
+
if (!v) {
|
|
20551
|
+
console.error(
|
|
20552
|
+
"archme: could not read its own version \u2014 the install looks incomplete.\nThe daemon reports this as no version at all, and a configured minimum-version\nfloor will refuse its job claims. Reinstall with `npm i -g @archme/cli`."
|
|
20553
|
+
);
|
|
20554
|
+
return 1;
|
|
20555
|
+
}
|
|
20556
|
+
console.log(v);
|
|
20557
|
+
return 0;
|
|
20558
|
+
}
|
|
18290
20559
|
case "login":
|
|
18291
20560
|
await runLogin();
|
|
18292
20561
|
return 0;
|
|
@@ -18302,6 +20571,14 @@ async function main(argv) {
|
|
|
18302
20571
|
const { dispatchInit: dispatchInit2 } = await Promise.resolve().then(() => (init_dispatch4(), dispatch_exports4));
|
|
18303
20572
|
return await dispatchInit2(r.args);
|
|
18304
20573
|
}
|
|
20574
|
+
case "local-watch": {
|
|
20575
|
+
const { dispatchLocalWatch: dispatchLocalWatch2 } = await Promise.resolve().then(() => (init_localWatchDispatch(), localWatchDispatch_exports));
|
|
20576
|
+
return await dispatchLocalWatch2(r.args);
|
|
20577
|
+
}
|
|
20578
|
+
case "doctor": {
|
|
20579
|
+
const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
|
|
20580
|
+
return await dispatchDoctor2(r.args);
|
|
20581
|
+
}
|
|
18305
20582
|
case "logout": {
|
|
18306
20583
|
const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
18307
20584
|
return await runLogout2(r.args);
|
|
@@ -18324,7 +20601,12 @@ Other commands:
|
|
|
18324
20601
|
disable|install|uninstall|run)
|
|
18325
20602
|
archme analyze <subcommand> Code analysis (outline|repo|diff|match|prefetch)
|
|
18326
20603
|
archme scan <repo-path> Single-pass scan into an explicit project/branch
|
|
20604
|
+
archme local-watch [repo-path] Watch a repo without a GitHub App
|
|
20605
|
+
(--list|--stop|--resume)
|
|
20606
|
+
archme doctor Check this machine's setup and explain it
|
|
18327
20607
|
archme login | logout Authenticate with archme
|
|
20608
|
+
archme keys list|create|revoke Manage MCP API keys
|
|
20609
|
+
archme --version Print the installed version
|
|
18328
20610
|
|
|
18329
20611
|
Run 'archme init --help', 'archme daemon --help', 'archme analyze --help', or
|
|
18330
20612
|
'archme scan --help' for subcommand help.`);
|