@archme/cli 0.4.2 → 0.4.4
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 +2625 -322
- 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({
|
|
@@ -9504,6 +9581,9 @@ var init_daemonModel = __esm({
|
|
|
9504
9581
|
});
|
|
9505
9582
|
|
|
9506
9583
|
// src/scan/spawn-claude.ts
|
|
9584
|
+
function stripNulBytes(arg) {
|
|
9585
|
+
return arg.includes("\0") ? arg.replaceAll("\0", "\uFFFD") : arg;
|
|
9586
|
+
}
|
|
9507
9587
|
function buildClaudeArgs(opts) {
|
|
9508
9588
|
const tools = (opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS).join(",");
|
|
9509
9589
|
const args = [
|
|
@@ -9530,9 +9610,10 @@ function buildClaudeArgs(opts) {
|
|
|
9530
9610
|
if (opts.model) {
|
|
9531
9611
|
args.push("--model", opts.model);
|
|
9532
9612
|
}
|
|
9533
|
-
return args;
|
|
9613
|
+
return args.map(stripNulBytes);
|
|
9534
9614
|
}
|
|
9535
9615
|
function spawnClaude(opts) {
|
|
9616
|
+
requireBinary("claude");
|
|
9536
9617
|
const env = scrubPrivilegedEnv(process.env);
|
|
9537
9618
|
const args = buildClaudeArgs({ ...opts, model: opts.model || DEFAULT_CLAUDE_MODEL });
|
|
9538
9619
|
if (opts.streamJson && !opts.interactive) {
|
|
@@ -9571,6 +9652,7 @@ var init_spawn_claude = __esm({
|
|
|
9571
9652
|
init_execa();
|
|
9572
9653
|
init_env_scrub();
|
|
9573
9654
|
init_mcp_config();
|
|
9655
|
+
init_preflight();
|
|
9574
9656
|
init_daemonModel();
|
|
9575
9657
|
init_daemonModel();
|
|
9576
9658
|
mcpTool = (name) => `mcp__${MCP_SERVER_NAME}__${name}`;
|
|
@@ -9743,7 +9825,7 @@ var init_digestPrompt = __esm({
|
|
|
9743
9825
|
// src/daemon/digest.ts
|
|
9744
9826
|
import { mkdtemp as mkdtemp2, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
|
|
9745
9827
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
9746
|
-
import { join as
|
|
9828
|
+
import { join as join7 } from "node:path";
|
|
9747
9829
|
function isCleanDigestExit(res) {
|
|
9748
9830
|
return !res.timedOut && res.signal == null && res.exitCode === 0;
|
|
9749
9831
|
}
|
|
@@ -9777,12 +9859,12 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
|
|
|
9777
9859
|
}
|
|
9778
9860
|
let dir;
|
|
9779
9861
|
try {
|
|
9780
|
-
dir = await mkdtemp2(
|
|
9862
|
+
dir = await mkdtemp2(join7(tmpdir2(), "archme-digest-"));
|
|
9781
9863
|
let mcpConfigPath;
|
|
9782
9864
|
if (mcp) {
|
|
9783
9865
|
mcpConfigPath = mcp.configPath;
|
|
9784
9866
|
} else {
|
|
9785
|
-
mcpConfigPath =
|
|
9867
|
+
mcpConfigPath = join7(dir, "mcp.json");
|
|
9786
9868
|
await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: {} }));
|
|
9787
9869
|
}
|
|
9788
9870
|
const model = await resolveModelPin(ctx.supabase);
|
|
@@ -9813,8 +9895,8 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
|
|
|
9813
9895
|
}
|
|
9814
9896
|
await completeDigest(ctx, job.id, deviceId, narrative);
|
|
9815
9897
|
console.log(`[${job.id}] digest synthesized (${narrative.length} chars) for ${payload.projectName}`);
|
|
9816
|
-
} catch (
|
|
9817
|
-
await failDigest(ctx, job.id, deviceId,
|
|
9898
|
+
} catch (err2) {
|
|
9899
|
+
await failDigest(ctx, job.id, deviceId, err2 instanceof Error ? err2.message : String(err2));
|
|
9818
9900
|
} finally {
|
|
9819
9901
|
inFlight2?.delete(job.id);
|
|
9820
9902
|
if (dir) await rm5(dir, { recursive: true, force: true }).catch(() => {
|
|
@@ -11774,8 +11856,8 @@ function extractParticipantsFromSequence(sequence) {
|
|
|
11774
11856
|
return [...participants];
|
|
11775
11857
|
}
|
|
11776
11858
|
function componentScopeSegments(componentPath) {
|
|
11777
|
-
const
|
|
11778
|
-
return
|
|
11859
|
+
const resolved2 = resolveWaypoint(componentPath, []);
|
|
11860
|
+
return resolved2.leafId ? [...resolved2.scopePath, resolved2.leafId] : resolved2.scopePath;
|
|
11779
11861
|
}
|
|
11780
11862
|
function isPrefix(prefix, full) {
|
|
11781
11863
|
return full.length >= prefix.length && prefix.every((s, i2) => full[i2] === s);
|
|
@@ -12270,10 +12352,10 @@ async function ensureBugsFolder(client, projectId) {
|
|
|
12270
12352
|
const createdId = created?.id ?? null;
|
|
12271
12353
|
if (createdId) cache.set(projectId, createdId);
|
|
12272
12354
|
return createdId;
|
|
12273
|
-
} catch (
|
|
12355
|
+
} catch (err2) {
|
|
12274
12356
|
console.warn(
|
|
12275
12357
|
`[bugs-folder] could not resolve/create the "Bugs" folder for project ${projectId}; bug filing falls back to agent find-or-create:`,
|
|
12276
|
-
|
|
12358
|
+
err2 instanceof Error ? err2.message : err2
|
|
12277
12359
|
);
|
|
12278
12360
|
return null;
|
|
12279
12361
|
}
|
|
@@ -12334,17 +12416,17 @@ var init_lang_detect = __esm({
|
|
|
12334
12416
|
|
|
12335
12417
|
// src/analyze/tree-sitter/cache.ts
|
|
12336
12418
|
import { mkdir as mkdir5, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
12337
|
-
import { dirname as dirname7, join as
|
|
12419
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
12338
12420
|
import { homedir as homedir3 } from "node:os";
|
|
12339
12421
|
import { createHash } from "node:crypto";
|
|
12340
12422
|
function defaultCacheRoot() {
|
|
12341
|
-
return process.env.ARCHME_TOOLS_DIR ??
|
|
12423
|
+
return process.env.ARCHME_TOOLS_DIR ?? join8(homedir3(), ".archme", "tools");
|
|
12342
12424
|
}
|
|
12343
12425
|
function grammarCacheDir(cacheRoot) {
|
|
12344
|
-
return
|
|
12426
|
+
return join8(cacheRoot, "grammars");
|
|
12345
12427
|
}
|
|
12346
12428
|
function cachedGrammarPath(cacheRoot, lang, version) {
|
|
12347
|
-
return
|
|
12429
|
+
return join8(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
|
|
12348
12430
|
}
|
|
12349
12431
|
async function fileExists(path7) {
|
|
12350
12432
|
try {
|
|
@@ -12354,6 +12436,12 @@ async function fileExists(path7) {
|
|
|
12354
12436
|
return false;
|
|
12355
12437
|
}
|
|
12356
12438
|
}
|
|
12439
|
+
function describeFetchFailure(err2) {
|
|
12440
|
+
const top = err2 instanceof Error ? err2.message : String(err2);
|
|
12441
|
+
const cause = err2?.cause;
|
|
12442
|
+
const inner = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
|
|
12443
|
+
return inner && inner !== top ? `${top} (${inner})` : top;
|
|
12444
|
+
}
|
|
12357
12445
|
async function ensureGrammar(opts) {
|
|
12358
12446
|
const target = cachedGrammarPath(opts.cacheDir, opts.lang, opts.version);
|
|
12359
12447
|
if (await fileExists(target)) {
|
|
@@ -12363,11 +12451,25 @@ async function ensureGrammar(opts) {
|
|
|
12363
12451
|
const url = `${opts.urlBase}/${opts.filename}`;
|
|
12364
12452
|
process.stderr.write(`fetching grammar: ${opts.lang}@${opts.version} (${url})
|
|
12365
12453
|
`);
|
|
12366
|
-
|
|
12454
|
+
let res;
|
|
12455
|
+
try {
|
|
12456
|
+
res = await fetch(url);
|
|
12457
|
+
} catch (err2) {
|
|
12458
|
+
throw new GrammarFetchError({
|
|
12459
|
+
lang: opts.lang,
|
|
12460
|
+
version: opts.version,
|
|
12461
|
+
url,
|
|
12462
|
+
reason: describeFetchFailure(err2),
|
|
12463
|
+
cause: err2
|
|
12464
|
+
});
|
|
12465
|
+
}
|
|
12367
12466
|
if (!res.ok) {
|
|
12368
|
-
throw new
|
|
12369
|
-
|
|
12370
|
-
|
|
12467
|
+
throw new GrammarFetchError({
|
|
12468
|
+
lang: opts.lang,
|
|
12469
|
+
version: opts.version,
|
|
12470
|
+
url,
|
|
12471
|
+
reason: `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`
|
|
12472
|
+
});
|
|
12371
12473
|
}
|
|
12372
12474
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
12373
12475
|
const actualHash = createHash("sha256").update(buf).digest("hex");
|
|
@@ -12379,9 +12481,41 @@ async function ensureGrammar(opts) {
|
|
|
12379
12481
|
await writeFile6(target, buf);
|
|
12380
12482
|
return { path: target, downloaded: true };
|
|
12381
12483
|
}
|
|
12484
|
+
var GrammarFetchError;
|
|
12382
12485
|
var init_cache = __esm({
|
|
12383
12486
|
"src/analyze/tree-sitter/cache.ts"() {
|
|
12384
12487
|
"use strict";
|
|
12488
|
+
GrammarFetchError = class extends Error {
|
|
12489
|
+
lang;
|
|
12490
|
+
url;
|
|
12491
|
+
constructor(opts) {
|
|
12492
|
+
super(
|
|
12493
|
+
[
|
|
12494
|
+
`Failed to download the ${opts.lang} tree-sitter grammar (${opts.lang}@${opts.version}).`,
|
|
12495
|
+
` URL: ${opts.url}`,
|
|
12496
|
+
` Reason: ${opts.reason}`,
|
|
12497
|
+
"",
|
|
12498
|
+
"archme fetches language grammars on first use and caches them under",
|
|
12499
|
+
"~/.archme/tools/grammars (override with ARCHME_TOOLS_DIR). This host could",
|
|
12500
|
+
"not reach the CDN.",
|
|
12501
|
+
"",
|
|
12502
|
+
"If you're behind a proxy, export it before re-running:",
|
|
12503
|
+
" export HTTPS_PROXY=http://proxy.example.com:8080",
|
|
12504
|
+
" export HTTP_PROXY=$HTTPS_PROXY",
|
|
12505
|
+
"",
|
|
12506
|
+
"Then warm the cache once (it is reused by every later run):",
|
|
12507
|
+
" archme analyze prefetch --all",
|
|
12508
|
+
"",
|
|
12509
|
+
"Offline/air-gapped: copy a populated ~/.archme/tools/grammars directory",
|
|
12510
|
+
"from a machine that has network access, or point ARCHME_TOOLS_DIR at one."
|
|
12511
|
+
].join("\n"),
|
|
12512
|
+
opts.cause === void 0 ? void 0 : { cause: opts.cause }
|
|
12513
|
+
);
|
|
12514
|
+
this.name = "GrammarFetchError";
|
|
12515
|
+
this.lang = opts.lang;
|
|
12516
|
+
this.url = opts.url;
|
|
12517
|
+
}
|
|
12518
|
+
};
|
|
12385
12519
|
}
|
|
12386
12520
|
});
|
|
12387
12521
|
|
|
@@ -12864,13 +12998,13 @@ async function outlineSource(source, file) {
|
|
|
12864
12998
|
confidence: 0.8,
|
|
12865
12999
|
data: { file, symbols }
|
|
12866
13000
|
});
|
|
12867
|
-
} catch (
|
|
13001
|
+
} catch (err2) {
|
|
12868
13002
|
return envelope({
|
|
12869
13003
|
tool: "file",
|
|
12870
13004
|
tier: 0,
|
|
12871
13005
|
confidence: 0.2,
|
|
12872
13006
|
data: { file, symbols: [] },
|
|
12873
|
-
warnings: [`parse failed, falling back to file tier: ${
|
|
13007
|
+
warnings: [`parse failed, falling back to file tier: ${err2.message}`]
|
|
12874
13008
|
});
|
|
12875
13009
|
}
|
|
12876
13010
|
}
|
|
@@ -12887,13 +13021,13 @@ async function runOutline(file) {
|
|
|
12887
13021
|
let source;
|
|
12888
13022
|
try {
|
|
12889
13023
|
source = await readFile6(file, "utf8");
|
|
12890
|
-
} catch (
|
|
13024
|
+
} catch (err2) {
|
|
12891
13025
|
return envelope({
|
|
12892
13026
|
tool: "file",
|
|
12893
13027
|
tier: 0,
|
|
12894
13028
|
confidence: 0,
|
|
12895
13029
|
data: { file, symbols: [] },
|
|
12896
|
-
errors: [`cannot read ${file}: ${
|
|
13030
|
+
errors: [`cannot read ${file}: ${err2.message}`]
|
|
12897
13031
|
});
|
|
12898
13032
|
}
|
|
12899
13033
|
return outlineSource(source, file);
|
|
@@ -13496,11 +13630,11 @@ var require_ignore = __commonJS({
|
|
|
13496
13630
|
|
|
13497
13631
|
// src/analyze/repo-walk.ts
|
|
13498
13632
|
import { readdir, readFile as readFile7, stat as stat4 } from "node:fs/promises";
|
|
13499
|
-
import { join as
|
|
13633
|
+
import { join as join9, relative } from "node:path";
|
|
13500
13634
|
async function loadGitignore(root) {
|
|
13501
13635
|
const ig = (0, import_ignore.default)();
|
|
13502
13636
|
try {
|
|
13503
|
-
const text = await readFile7(
|
|
13637
|
+
const text = await readFile7(join9(root, ".gitignore"), "utf8");
|
|
13504
13638
|
ig.add(text);
|
|
13505
13639
|
} catch {
|
|
13506
13640
|
ig.add(["node_modules", "dist", "build", ".next", "coverage", ".turbo"]);
|
|
@@ -13514,7 +13648,7 @@ async function* walkRepoFiles(root, filter2 = {}) {
|
|
|
13514
13648
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
13515
13649
|
for (const e of entries) {
|
|
13516
13650
|
if (ALWAYS_SKIP.has(e.name)) continue;
|
|
13517
|
-
const full =
|
|
13651
|
+
const full = join9(dir, e.name);
|
|
13518
13652
|
const rel = relative(root, full);
|
|
13519
13653
|
if (e.isDirectory()) {
|
|
13520
13654
|
if (ig.ignores(rel + "/")) continue;
|
|
@@ -13538,7 +13672,7 @@ async function collectRepoFiles(root, filter2 = {}) {
|
|
|
13538
13672
|
}
|
|
13539
13673
|
async function gitListFiles(root) {
|
|
13540
13674
|
try {
|
|
13541
|
-
await stat4(
|
|
13675
|
+
await stat4(join9(root, ".git"));
|
|
13542
13676
|
} catch {
|
|
13543
13677
|
return null;
|
|
13544
13678
|
}
|
|
@@ -13579,11 +13713,11 @@ var init_repo_walk = __esm({
|
|
|
13579
13713
|
});
|
|
13580
13714
|
|
|
13581
13715
|
// src/analyze/commands/repo.ts
|
|
13582
|
-
import { join as
|
|
13716
|
+
import { join as join10 } from "node:path";
|
|
13583
13717
|
async function* walkRepo(root, opts = {}) {
|
|
13584
13718
|
const files = await collectRepoFilesWithGit(root, opts);
|
|
13585
13719
|
for (const rel of files) {
|
|
13586
|
-
const env = await runOutline(
|
|
13720
|
+
const env = await runOutline(join10(root, rel));
|
|
13587
13721
|
yield env;
|
|
13588
13722
|
}
|
|
13589
13723
|
}
|
|
@@ -13597,9 +13731,9 @@ var init_repo = __esm({
|
|
|
13597
13731
|
|
|
13598
13732
|
// src/analyze/matchers/file.ts
|
|
13599
13733
|
import { stat as stat5 } from "node:fs/promises";
|
|
13600
|
-
import { join as
|
|
13734
|
+
import { join as join11 } from "node:path";
|
|
13601
13735
|
async function matchFileAnchor(anchor, repoRoot) {
|
|
13602
|
-
const full =
|
|
13736
|
+
const full = join11(repoRoot, anchor.path);
|
|
13603
13737
|
try {
|
|
13604
13738
|
const s = await stat5(full);
|
|
13605
13739
|
if (!s.isFile()) {
|
|
@@ -13630,9 +13764,9 @@ var init_file = __esm({
|
|
|
13630
13764
|
|
|
13631
13765
|
// src/analyze/matchers/directory.ts
|
|
13632
13766
|
import { stat as stat6 } from "node:fs/promises";
|
|
13633
|
-
import { join as
|
|
13767
|
+
import { join as join12 } from "node:path";
|
|
13634
13768
|
async function matchDirectoryAnchor(anchor, repoRoot) {
|
|
13635
|
-
const full =
|
|
13769
|
+
const full = join12(repoRoot, anchor.path);
|
|
13636
13770
|
try {
|
|
13637
13771
|
const s = await stat6(full);
|
|
13638
13772
|
if (!s.isDirectory()) {
|
|
@@ -13701,9 +13835,9 @@ var init_findSymbol = __esm({
|
|
|
13701
13835
|
|
|
13702
13836
|
// src/analyze/matchers/symbol.ts
|
|
13703
13837
|
import { stat as stat7 } from "node:fs/promises";
|
|
13704
|
-
import { join as
|
|
13838
|
+
import { join as join13 } from "node:path";
|
|
13705
13839
|
async function matchSymbolAnchor(anchor, repoRoot) {
|
|
13706
|
-
const full =
|
|
13840
|
+
const full = join13(repoRoot, anchor.path);
|
|
13707
13841
|
try {
|
|
13708
13842
|
await stat7(full);
|
|
13709
13843
|
} catch {
|
|
@@ -13805,8 +13939,8 @@ async function runPrefetch(langs) {
|
|
|
13805
13939
|
} else {
|
|
13806
13940
|
result.skipped.push(lang);
|
|
13807
13941
|
}
|
|
13808
|
-
} catch (
|
|
13809
|
-
result.errors.push({ lang, message:
|
|
13942
|
+
} catch (err2) {
|
|
13943
|
+
result.errors.push({ lang, message: err2.message });
|
|
13810
13944
|
}
|
|
13811
13945
|
}
|
|
13812
13946
|
return result;
|
|
@@ -14027,8 +14161,8 @@ async function acquireSingleInstanceLock(pidfile) {
|
|
|
14027
14161
|
await mkdir6(dirname8(pidfile), { recursive: true });
|
|
14028
14162
|
try {
|
|
14029
14163
|
await writeFile7(pidfile, String(process.pid), { flag: "wx" });
|
|
14030
|
-
} catch (
|
|
14031
|
-
if (
|
|
14164
|
+
} catch (err2) {
|
|
14165
|
+
if (err2.code !== "EEXIST") throw err2;
|
|
14032
14166
|
}
|
|
14033
14167
|
let release;
|
|
14034
14168
|
try {
|
|
@@ -14097,7 +14231,7 @@ var init_semaphore = __esm({
|
|
|
14097
14231
|
acquire() {
|
|
14098
14232
|
const slot = this.tryAcquire();
|
|
14099
14233
|
if (slot) return Promise.resolve(slot);
|
|
14100
|
-
return new Promise((
|
|
14234
|
+
return new Promise((resolve5) => this.waiters.push(resolve5));
|
|
14101
14235
|
}
|
|
14102
14236
|
makeRelease() {
|
|
14103
14237
|
let released = false;
|
|
@@ -14144,11 +14278,11 @@ async function refreshOAuthCredentialsWith(refreshToken, cfg) {
|
|
|
14144
14278
|
client_id: cfg.clientId
|
|
14145
14279
|
})
|
|
14146
14280
|
});
|
|
14147
|
-
} catch (
|
|
14281
|
+
} catch (err2) {
|
|
14148
14282
|
return {
|
|
14149
14283
|
ok: false,
|
|
14150
14284
|
permanent: false,
|
|
14151
|
-
error: `network error: ${
|
|
14285
|
+
error: `network error: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
14152
14286
|
};
|
|
14153
14287
|
}
|
|
14154
14288
|
const body = await parseJsonResponse(response);
|
|
@@ -14182,11 +14316,11 @@ async function getOAuthUserInfoWith(accessToken, cfg) {
|
|
|
14182
14316
|
function resolveUserId(userInfo2, storedUserId) {
|
|
14183
14317
|
return userInfo2.sub ?? storedUserId ?? null;
|
|
14184
14318
|
}
|
|
14185
|
-
function isAuthError(
|
|
14186
|
-
if (!
|
|
14187
|
-
const msg = ("message" in
|
|
14188
|
-
const code = ("code" in
|
|
14189
|
-
const status = "status" in
|
|
14319
|
+
function isAuthError(err2) {
|
|
14320
|
+
if (!err2 || typeof err2 !== "object") return false;
|
|
14321
|
+
const msg = ("message" in err2 ? String(err2.message) : "").toLowerCase();
|
|
14322
|
+
const code = ("code" in err2 ? String(err2.code) : "").toLowerCase();
|
|
14323
|
+
const status = "status" in err2 ? Number(err2.status) : 0;
|
|
14190
14324
|
return status === 401 || code === "pgrst301" || msg.includes("jwt expired") || msg.includes("invalid jwt") || msg.includes("not authenticated");
|
|
14191
14325
|
}
|
|
14192
14326
|
async function withAuthRetry(label, fn, reLogin) {
|
|
@@ -14195,8 +14329,8 @@ async function withAuthRetry(label, fn, reLogin) {
|
|
|
14195
14329
|
console.warn(`${label}: auth error on first attempt, re-logging in and retrying once`);
|
|
14196
14330
|
try {
|
|
14197
14331
|
await reLogin();
|
|
14198
|
-
} catch (
|
|
14199
|
-
console.error(`${label}: re-login failed:`,
|
|
14332
|
+
} catch (err2) {
|
|
14333
|
+
console.error(`${label}: re-login failed:`, err2);
|
|
14200
14334
|
return first;
|
|
14201
14335
|
}
|
|
14202
14336
|
return await fn();
|
|
@@ -14207,8 +14341,8 @@ async function fetchWithAuthRetry(label, doFetch, reLogin) {
|
|
|
14207
14341
|
console.warn(`${label}: 401 on first attempt, re-logging in and retrying once`);
|
|
14208
14342
|
try {
|
|
14209
14343
|
await reLogin();
|
|
14210
|
-
} catch (
|
|
14211
|
-
console.error(`${label}: re-login failed:`,
|
|
14344
|
+
} catch (err2) {
|
|
14345
|
+
console.error(`${label}: re-login failed:`, err2);
|
|
14212
14346
|
return first;
|
|
14213
14347
|
}
|
|
14214
14348
|
return await doFetch();
|
|
@@ -14315,8 +14449,8 @@ var init_sessionAuth = __esm({
|
|
|
14315
14449
|
for (const l of this.listeners) {
|
|
14316
14450
|
try {
|
|
14317
14451
|
l(token);
|
|
14318
|
-
} catch (
|
|
14319
|
-
console.error("sessionAuth: listener error:",
|
|
14452
|
+
} catch (err2) {
|
|
14453
|
+
console.error("sessionAuth: listener error:", err2);
|
|
14320
14454
|
}
|
|
14321
14455
|
}
|
|
14322
14456
|
}
|
|
@@ -14362,7 +14496,7 @@ var init_sessionAuth = __esm({
|
|
|
14362
14496
|
armRefreshTimer(delayMs) {
|
|
14363
14497
|
if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
14364
14498
|
this.refreshTimer = setTimeout(() => {
|
|
14365
|
-
this.runScheduledRefresh().catch((
|
|
14499
|
+
this.runScheduledRefresh().catch((err2) => console.error("auth: token refresh failed:", err2));
|
|
14366
14500
|
}, delayMs);
|
|
14367
14501
|
}
|
|
14368
14502
|
// Translate a token lifetime (seconds) into the timer delay: fire 5 min before
|
|
@@ -14404,9 +14538,9 @@ var init_sessionAuth = __esm({
|
|
|
14404
14538
|
try {
|
|
14405
14539
|
await this.coalescedRefresh("token refresh");
|
|
14406
14540
|
this.rearmFromLiveExpiry();
|
|
14407
|
-
} catch (
|
|
14408
|
-
const detail =
|
|
14409
|
-
const permanent =
|
|
14541
|
+
} catch (err2) {
|
|
14542
|
+
const detail = err2 instanceof Error ? err2.message : String(err2);
|
|
14543
|
+
const permanent = err2 instanceof LoadStoredAndRefreshError && err2.permanent;
|
|
14410
14544
|
if (permanent) {
|
|
14411
14545
|
console.error(`auth: token refresh failed permanently; auto-refresh stopped: ${detail}`);
|
|
14412
14546
|
return;
|
|
@@ -14750,9 +14884,13 @@ function rateLimitDeferPlan(text, nowMs = Date.now(), defaultTimeZone = localTim
|
|
|
14750
14884
|
}
|
|
14751
14885
|
return null;
|
|
14752
14886
|
}
|
|
14887
|
+
function stripPromptEcho(haystack, prompt) {
|
|
14888
|
+
if (!prompt || prompt.length < 40) return haystack;
|
|
14889
|
+
return haystack.split(prompt).join(" ");
|
|
14890
|
+
}
|
|
14753
14891
|
function rateLimitDeferPlanForExit(channels, nowMs = Date.now(), defaultTimeZone = localTimeZone()) {
|
|
14754
|
-
const haystack = `${channels.message}
|
|
14755
|
-
${channels.stderr}
|
|
14892
|
+
const haystack = stripPromptEcho(`${channels.message}
|
|
14893
|
+
${channels.stderr}`, channels.prompt);
|
|
14756
14894
|
return rateLimitDeferPlan(haystack, nowMs, defaultTimeZone);
|
|
14757
14895
|
}
|
|
14758
14896
|
var RATE_LIMIT_RE, MONTHS, RATE_LIMIT_TENTATIVE_PAUSE_MS, DATED_RESET_ROLLOVER_GRACE_MS, RATE_LIMIT_PAUSE_PADDING_MS, RATE_LIMIT_SHAPED_RE;
|
|
@@ -14781,15 +14919,50 @@ var init_rateLimit = __esm({
|
|
|
14781
14919
|
}
|
|
14782
14920
|
});
|
|
14783
14921
|
|
|
14922
|
+
// src/packageRoot.ts
|
|
14923
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
14924
|
+
import { dirname as dirname9, join as join14, parse } from "node:path";
|
|
14925
|
+
function defaultReadFile(path7) {
|
|
14926
|
+
return readFileSync4(path7, "utf8");
|
|
14927
|
+
}
|
|
14928
|
+
function findPackageRoot2(startDir, opts = {}) {
|
|
14929
|
+
const readFile14 = opts.readFile ?? defaultReadFile;
|
|
14930
|
+
const stopAt = parse(startDir).root;
|
|
14931
|
+
let dir = startDir;
|
|
14932
|
+
for (; ; ) {
|
|
14933
|
+
try {
|
|
14934
|
+
const parsed = JSON.parse(readFile14(join14(dir, "package.json")));
|
|
14935
|
+
if (parsed.name === PACKAGE_NAME) return dir;
|
|
14936
|
+
} catch {
|
|
14937
|
+
}
|
|
14938
|
+
if (dir === stopAt) return null;
|
|
14939
|
+
const parent = dirname9(dir);
|
|
14940
|
+
if (parent === dir) return null;
|
|
14941
|
+
dir = parent;
|
|
14942
|
+
}
|
|
14943
|
+
}
|
|
14944
|
+
var PACKAGE_NAME;
|
|
14945
|
+
var init_packageRoot = __esm({
|
|
14946
|
+
"src/packageRoot.ts"() {
|
|
14947
|
+
"use strict";
|
|
14948
|
+
PACKAGE_NAME = "@archme/cli";
|
|
14949
|
+
}
|
|
14950
|
+
});
|
|
14951
|
+
|
|
14784
14952
|
// src/daemon/version.ts
|
|
14953
|
+
var version_exports = {};
|
|
14954
|
+
__export(version_exports, {
|
|
14955
|
+
readDaemonVersion: () => readDaemonVersion
|
|
14956
|
+
});
|
|
14785
14957
|
import { readFile as readFile10 } from "node:fs/promises";
|
|
14786
|
-
import { dirname as
|
|
14958
|
+
import { dirname as dirname10, join as join15 } from "node:path";
|
|
14787
14959
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
14788
14960
|
async function readDaemonVersion() {
|
|
14789
14961
|
try {
|
|
14790
|
-
const here =
|
|
14791
|
-
const
|
|
14792
|
-
|
|
14962
|
+
const here = dirname10(fileURLToPath4(import.meta.url));
|
|
14963
|
+
const root = findPackageRoot2(here);
|
|
14964
|
+
if (!root) return null;
|
|
14965
|
+
const raw = await readFile10(join15(root, "package.json"), "utf8");
|
|
14793
14966
|
const parsed = JSON.parse(raw);
|
|
14794
14967
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
14795
14968
|
} catch {
|
|
@@ -14799,12 +14972,13 @@ async function readDaemonVersion() {
|
|
|
14799
14972
|
var init_version = __esm({
|
|
14800
14973
|
"src/daemon/version.ts"() {
|
|
14801
14974
|
"use strict";
|
|
14975
|
+
init_packageRoot();
|
|
14802
14976
|
}
|
|
14803
14977
|
});
|
|
14804
14978
|
|
|
14805
14979
|
// src/daemon/materializeDiff.ts
|
|
14806
14980
|
import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
|
|
14807
|
-
import { join as
|
|
14981
|
+
import { join as join16 } from "node:path";
|
|
14808
14982
|
import { createHash as createHash2 } from "node:crypto";
|
|
14809
14983
|
function splitPatchByFile(patch) {
|
|
14810
14984
|
if (!patch.trim()) return [];
|
|
@@ -14889,18 +15063,18 @@ function buildManifest(files, rangeLabel) {
|
|
|
14889
15063
|
}
|
|
14890
15064
|
async function materializeDiff(worktreeDir, patch, rangeLabel) {
|
|
14891
15065
|
const files = splitPatchByFile(patch);
|
|
14892
|
-
const dir =
|
|
14893
|
-
const byFileDir =
|
|
15066
|
+
const dir = join16(worktreeDir, CHANGES_DIR);
|
|
15067
|
+
const byFileDir = join16(dir, "by-file");
|
|
14894
15068
|
await mkdir7(byFileDir, { recursive: true });
|
|
14895
|
-
await writeFile8(
|
|
15069
|
+
await writeFile8(join16(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
|
|
14896
15070
|
`, "utf8");
|
|
14897
15071
|
for (const f of files) {
|
|
14898
15072
|
const rel = `by-file/${hashPath(f.path)}.diff`;
|
|
14899
15073
|
f.diffPath = rel;
|
|
14900
|
-
await writeFile8(
|
|
15074
|
+
await writeFile8(join16(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
|
|
14901
15075
|
`, "utf8");
|
|
14902
15076
|
}
|
|
14903
|
-
await writeFile8(
|
|
15077
|
+
await writeFile8(join16(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
|
|
14904
15078
|
return { dir, fileCount: files.length };
|
|
14905
15079
|
}
|
|
14906
15080
|
var CHANGES_DIR, STATUS_LABEL;
|
|
@@ -14912,6 +15086,583 @@ var init_materializeDiff = __esm({
|
|
|
14912
15086
|
}
|
|
14913
15087
|
});
|
|
14914
15088
|
|
|
15089
|
+
// src/daemon/branchMarker.ts
|
|
15090
|
+
async function readBranchMarker(client, projectId, archmeBranch) {
|
|
15091
|
+
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();
|
|
15092
|
+
if (error || !data) return { sha: null, repo: null };
|
|
15093
|
+
return { sha: data.last_source_sha ?? null, repo: data.last_source_repo ?? null };
|
|
15094
|
+
}
|
|
15095
|
+
var init_branchMarker = __esm({
|
|
15096
|
+
"src/daemon/branchMarker.ts"() {
|
|
15097
|
+
"use strict";
|
|
15098
|
+
}
|
|
15099
|
+
});
|
|
15100
|
+
|
|
15101
|
+
// src/daemon/localRepos.ts
|
|
15102
|
+
import { chmod as chmod2, mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
|
|
15103
|
+
import { homedir as homedir4 } from "node:os";
|
|
15104
|
+
import { dirname as dirname11, join as join17 } from "node:path";
|
|
15105
|
+
function localReposPath(env = process.env) {
|
|
15106
|
+
return env.ARCHME_LOCAL_REPOS_PATH ?? join17(homedir4(), ".archme", "repos.json");
|
|
15107
|
+
}
|
|
15108
|
+
function isEntry(v) {
|
|
15109
|
+
if (!v || typeof v !== "object") return false;
|
|
15110
|
+
const o2 = v;
|
|
15111
|
+
return REQUIRED_KEYS.every((k) => typeof o2[k] === "string" && o2[k].length > 0);
|
|
15112
|
+
}
|
|
15113
|
+
function parseLocalRepos(raw) {
|
|
15114
|
+
let parsed;
|
|
15115
|
+
try {
|
|
15116
|
+
parsed = JSON.parse(raw);
|
|
15117
|
+
} catch {
|
|
15118
|
+
return [];
|
|
15119
|
+
}
|
|
15120
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
|
|
15121
|
+
const env = parsed;
|
|
15122
|
+
if (env.v !== LOCAL_REPOS_VERSION) return [];
|
|
15123
|
+
if (!Array.isArray(env.repos)) return [];
|
|
15124
|
+
return env.repos.filter(isEntry).map((e) => {
|
|
15125
|
+
const entry = {
|
|
15126
|
+
watchedRepoId: e.watchedRepoId,
|
|
15127
|
+
mirrorPath: e.mirrorPath,
|
|
15128
|
+
sourcePath: e.sourcePath,
|
|
15129
|
+
repoOwner: e.repoOwner,
|
|
15130
|
+
repoName: e.repoName
|
|
15131
|
+
};
|
|
15132
|
+
const raw2 = e;
|
|
15133
|
+
if (typeof raw2.haltedAt === "string" && raw2.haltedAt) entry.haltedAt = raw2.haltedAt;
|
|
15134
|
+
if (typeof raw2.haltReason === "string" && raw2.haltReason) entry.haltReason = raw2.haltReason;
|
|
15135
|
+
if (typeof raw2.haltCode === "string" && raw2.haltCode) entry.haltCode = raw2.haltCode;
|
|
15136
|
+
return entry;
|
|
15137
|
+
});
|
|
15138
|
+
}
|
|
15139
|
+
function serializeLocalRepos(repos) {
|
|
15140
|
+
return JSON.stringify({ v: LOCAL_REPOS_VERSION, repos }, null, 2);
|
|
15141
|
+
}
|
|
15142
|
+
function keyOf(e) {
|
|
15143
|
+
return e.watchedRepoId;
|
|
15144
|
+
}
|
|
15145
|
+
function upsertLocalRepo(repos, entry) {
|
|
15146
|
+
const k = keyOf(entry);
|
|
15147
|
+
const next = repos.filter((r) => keyOf(r) !== k);
|
|
15148
|
+
next.push(entry);
|
|
15149
|
+
return next;
|
|
15150
|
+
}
|
|
15151
|
+
function slugOf(e) {
|
|
15152
|
+
return `${e.repoOwner}/${e.repoName}`;
|
|
15153
|
+
}
|
|
15154
|
+
function findLocalReposBySlug(repos, slug) {
|
|
15155
|
+
const want = slug.toLowerCase();
|
|
15156
|
+
return repos.filter((r) => slugOf(r).toLowerCase() === want);
|
|
15157
|
+
}
|
|
15158
|
+
function removeLocalReposBySlug(repos, slug) {
|
|
15159
|
+
const want = slug.toLowerCase();
|
|
15160
|
+
return repos.filter((r) => slugOf(r).toLowerCase() !== want);
|
|
15161
|
+
}
|
|
15162
|
+
function markHalted(repos, watchedRepoId, reason, at, code) {
|
|
15163
|
+
return repos.map(
|
|
15164
|
+
(r) => r.watchedRepoId === watchedRepoId ? { ...r, haltedAt: at, haltReason: reason, haltCode: code } : r
|
|
15165
|
+
);
|
|
15166
|
+
}
|
|
15167
|
+
function clearHalt(repos, watchedRepoId) {
|
|
15168
|
+
return repos.map((r) => {
|
|
15169
|
+
if (r.watchedRepoId !== watchedRepoId) return r;
|
|
15170
|
+
const { haltedAt: _at, haltReason: _reason, haltCode: _code, ...rest } = r;
|
|
15171
|
+
return rest;
|
|
15172
|
+
});
|
|
15173
|
+
}
|
|
15174
|
+
function findLocalRepoForJob(repos, job) {
|
|
15175
|
+
if (!job.watched_repo_id) return null;
|
|
15176
|
+
const match2 = repos.find((r) => r.watchedRepoId === job.watched_repo_id);
|
|
15177
|
+
if (!match2) return null;
|
|
15178
|
+
const sameRepo = match2.repoOwner.toLowerCase() === job.repo_owner.toLowerCase() && match2.repoName.toLowerCase() === job.repo_name.toLowerCase();
|
|
15179
|
+
return sameRepo ? match2 : null;
|
|
15180
|
+
}
|
|
15181
|
+
async function loadLocalRepos(env = process.env) {
|
|
15182
|
+
try {
|
|
15183
|
+
return parseLocalRepos(await readFile11(localReposPath(env), "utf8"));
|
|
15184
|
+
} catch {
|
|
15185
|
+
return [];
|
|
15186
|
+
}
|
|
15187
|
+
}
|
|
15188
|
+
async function saveLocalRepos(repos, env = process.env) {
|
|
15189
|
+
const path7 = localReposPath(env);
|
|
15190
|
+
await mkdir8(dirname11(path7), { recursive: true, mode: 448 });
|
|
15191
|
+
await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
|
|
15192
|
+
await chmod2(path7, 384).catch(() => {
|
|
15193
|
+
});
|
|
15194
|
+
}
|
|
15195
|
+
async function registerLocalRepo(entry, env = process.env) {
|
|
15196
|
+
await saveLocalRepos(upsertLocalRepo(await loadLocalRepos(env), entry), env);
|
|
15197
|
+
}
|
|
15198
|
+
var LOCAL_REPOS_VERSION, REQUIRED_KEYS;
|
|
15199
|
+
var init_localRepos = __esm({
|
|
15200
|
+
"src/daemon/localRepos.ts"() {
|
|
15201
|
+
"use strict";
|
|
15202
|
+
LOCAL_REPOS_VERSION = 1;
|
|
15203
|
+
REQUIRED_KEYS = [
|
|
15204
|
+
"watchedRepoId",
|
|
15205
|
+
"mirrorPath",
|
|
15206
|
+
"sourcePath",
|
|
15207
|
+
"repoOwner",
|
|
15208
|
+
"repoName"
|
|
15209
|
+
];
|
|
15210
|
+
}
|
|
15211
|
+
});
|
|
15212
|
+
|
|
15213
|
+
// ../shared/commitAdvance.ts
|
|
15214
|
+
function planAdvance(input) {
|
|
15215
|
+
const { marker, upstreamTip, markerIsAncestorOfTip, tipIsAncestorOfMarker, firstParentShas } = input;
|
|
15216
|
+
if (!marker) return { kind: "bootstrap-needed" };
|
|
15217
|
+
if (marker === upstreamTip) return { kind: "up-to-date" };
|
|
15218
|
+
if (markerIsAncestorOfTip) {
|
|
15219
|
+
return firstParentShas.length === 0 ? { kind: "force-push", tip: upstreamTip } : { kind: "fast-forward", shas: firstParentShas };
|
|
15220
|
+
}
|
|
15221
|
+
if (tipIsAncestorOfMarker) return { kind: "upstream-behind", tip: upstreamTip };
|
|
15222
|
+
return { kind: "force-push", tip: upstreamTip };
|
|
15223
|
+
}
|
|
15224
|
+
var init_commitAdvance = __esm({
|
|
15225
|
+
"../shared/commitAdvance.ts"() {
|
|
15226
|
+
}
|
|
15227
|
+
});
|
|
15228
|
+
|
|
15229
|
+
// ../shared/commitGate.ts
|
|
15230
|
+
function decideGate(input) {
|
|
15231
|
+
const { targetSha, markerSha, job, elapsedMs, timeoutMs } = input;
|
|
15232
|
+
const pushedNewRef = input.pushedNewRef ?? true;
|
|
15233
|
+
const noopGraceMs = input.noopGraceMs ?? 5 * 60 * 1e3;
|
|
15234
|
+
if (markerSha && markerSha === targetSha) {
|
|
15235
|
+
return {
|
|
15236
|
+
action: "advance",
|
|
15237
|
+
code: "marker-advanced",
|
|
15238
|
+
reason: "marker advanced to target (clean completion)"
|
|
15239
|
+
};
|
|
15240
|
+
}
|
|
15241
|
+
if (job && (job.status === "failed" || job.status === "cancelled")) {
|
|
15242
|
+
return {
|
|
15243
|
+
action: "stop",
|
|
15244
|
+
code: "job-terminal",
|
|
15245
|
+
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`
|
|
15246
|
+
};
|
|
15247
|
+
}
|
|
15248
|
+
if (!pushedNewRef && job === null && elapsedMs >= noopGraceMs) {
|
|
15249
|
+
return {
|
|
15250
|
+
action: "stop",
|
|
15251
|
+
code: "noop-push-no-job",
|
|
15252
|
+
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`
|
|
15253
|
+
};
|
|
15254
|
+
}
|
|
15255
|
+
if (elapsedMs >= timeoutMs) {
|
|
15256
|
+
return {
|
|
15257
|
+
action: "stop",
|
|
15258
|
+
code: "timeout",
|
|
15259
|
+
reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${short(targetSha)} (job: ${job ? job.status : "not enqueued"})`
|
|
15260
|
+
};
|
|
15261
|
+
}
|
|
15262
|
+
return {
|
|
15263
|
+
action: "wait",
|
|
15264
|
+
code: "waiting",
|
|
15265
|
+
reason: job ? `job ${job.status}` : "job not enqueued yet"
|
|
15266
|
+
};
|
|
15267
|
+
}
|
|
15268
|
+
function short(sha) {
|
|
15269
|
+
return sha.slice(0, 7);
|
|
15270
|
+
}
|
|
15271
|
+
var init_commitGate = __esm({
|
|
15272
|
+
"../shared/commitGate.ts"() {
|
|
15273
|
+
}
|
|
15274
|
+
});
|
|
15275
|
+
|
|
15276
|
+
// src/daemon/localPoll.ts
|
|
15277
|
+
function planPollTick(input) {
|
|
15278
|
+
const { marker, repo } = input;
|
|
15279
|
+
if (!marker.sha) {
|
|
15280
|
+
return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
|
|
15281
|
+
}
|
|
15282
|
+
if (marker.repo !== repo) {
|
|
15283
|
+
return {
|
|
15284
|
+
kind: "halt",
|
|
15285
|
+
code: "job-terminal",
|
|
15286
|
+
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)`
|
|
15287
|
+
};
|
|
15288
|
+
}
|
|
15289
|
+
const plan = planAdvance({
|
|
15290
|
+
marker: marker.sha,
|
|
15291
|
+
upstreamTip: input.upstreamTip,
|
|
15292
|
+
markerIsAncestorOfTip: input.markerIsAncestorOfTip,
|
|
15293
|
+
tipIsAncestorOfMarker: input.tipIsAncestorOfMarker,
|
|
15294
|
+
firstParentShas: input.firstParentShas
|
|
15295
|
+
});
|
|
15296
|
+
let target;
|
|
15297
|
+
switch (plan.kind) {
|
|
15298
|
+
case "bootstrap-needed":
|
|
15299
|
+
return { kind: "idle", reason: "waiting for the initial scan to establish the diff marker" };
|
|
15300
|
+
case "up-to-date":
|
|
15301
|
+
return { kind: "idle", reason: "up to date" };
|
|
15302
|
+
case "upstream-behind":
|
|
15303
|
+
return {
|
|
15304
|
+
kind: "idle",
|
|
15305
|
+
reason: `upstream is behind the marker (reset to ${short2(plan.tip)}); nothing to do`
|
|
15306
|
+
};
|
|
15307
|
+
case "force-push":
|
|
15308
|
+
target = plan.tip;
|
|
15309
|
+
break;
|
|
15310
|
+
case "fast-forward":
|
|
15311
|
+
target = plan.shas[0];
|
|
15312
|
+
break;
|
|
15313
|
+
}
|
|
15314
|
+
if (!input.jobForTarget) {
|
|
15315
|
+
return { kind: "enqueue", beforeSha: marker.sha, afterSha: target };
|
|
15316
|
+
}
|
|
15317
|
+
const decision = decideGate({
|
|
15318
|
+
targetSha: target,
|
|
15319
|
+
markerSha: marker.sha,
|
|
15320
|
+
job: { status: input.jobForTarget.status, attemptCount: input.jobForTarget.attemptCount },
|
|
15321
|
+
elapsedMs: input.nowMs - input.jobForTarget.createdAtMs,
|
|
15322
|
+
timeoutMs: input.gateTimeoutMs,
|
|
15323
|
+
// The poller enqueues directly, so the "no-op push fired no webhook" case
|
|
15324
|
+
// decideGate guards for cannot arise here.
|
|
15325
|
+
pushedNewRef: true
|
|
15326
|
+
});
|
|
15327
|
+
switch (decision.action) {
|
|
15328
|
+
case "stop":
|
|
15329
|
+
return {
|
|
15330
|
+
kind: "halt",
|
|
15331
|
+
code: decision.code,
|
|
15332
|
+
reason: localHaltReason(decision, input.jobForTarget, target)
|
|
15333
|
+
};
|
|
15334
|
+
case "advance":
|
|
15335
|
+
return { kind: "idle", reason: decision.reason };
|
|
15336
|
+
case "wait":
|
|
15337
|
+
return { kind: "idle", reason: `${short2(target)}: ${decision.reason}` };
|
|
15338
|
+
}
|
|
15339
|
+
}
|
|
15340
|
+
function localHaltReason(decision, job, targetSha) {
|
|
15341
|
+
if (decision.code !== "job-terminal" || !job) return decision.reason;
|
|
15342
|
+
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.
|
|
15343
|
+
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.`;
|
|
15344
|
+
}
|
|
15345
|
+
function short2(sha) {
|
|
15346
|
+
return sha.slice(0, 7);
|
|
15347
|
+
}
|
|
15348
|
+
var init_localPoll = __esm({
|
|
15349
|
+
"src/daemon/localPoll.ts"() {
|
|
15350
|
+
"use strict";
|
|
15351
|
+
init_commitAdvance();
|
|
15352
|
+
init_commitGate();
|
|
15353
|
+
}
|
|
15354
|
+
});
|
|
15355
|
+
|
|
15356
|
+
// src/daemon/defaultBranch.ts
|
|
15357
|
+
function pickDefaultBranch(input) {
|
|
15358
|
+
const onOrigin = new Set(input.remoteBranches);
|
|
15359
|
+
if (input.symbolicRef) {
|
|
15360
|
+
const slash = input.symbolicRef.indexOf("/");
|
|
15361
|
+
const named = slash >= 0 ? input.symbolicRef.slice(slash + 1) : null;
|
|
15362
|
+
if (named && onOrigin.has(named)) return named;
|
|
15363
|
+
}
|
|
15364
|
+
for (const candidate of CONVENTIONAL) {
|
|
15365
|
+
if (onOrigin.has(candidate)) return candidate;
|
|
15366
|
+
}
|
|
15367
|
+
if (input.currentBranch && onOrigin.has(input.currentBranch)) return input.currentBranch;
|
|
15368
|
+
return null;
|
|
15369
|
+
}
|
|
15370
|
+
var CONVENTIONAL;
|
|
15371
|
+
var init_defaultBranch = __esm({
|
|
15372
|
+
"src/daemon/defaultBranch.ts"() {
|
|
15373
|
+
"use strict";
|
|
15374
|
+
CONVENTIONAL = ["main", "master"];
|
|
15375
|
+
}
|
|
15376
|
+
});
|
|
15377
|
+
|
|
15378
|
+
// src/daemon/localGit.ts
|
|
15379
|
+
var localGit_exports = {};
|
|
15380
|
+
__export(localGit_exports, {
|
|
15381
|
+
createMirrorClone: () => createMirrorClone,
|
|
15382
|
+
detectDefaultBranch: () => detectDefaultBranch,
|
|
15383
|
+
fetchOrigin: () => fetchOrigin,
|
|
15384
|
+
firstParentRange: () => firstParentRange,
|
|
15385
|
+
isAncestor: () => isAncestor,
|
|
15386
|
+
readCommitMeta: () => readCommitMeta,
|
|
15387
|
+
readOriginUrl: () => readOriginUrl,
|
|
15388
|
+
revParse: () => revParse,
|
|
15389
|
+
setOriginUrl: () => setOriginUrl
|
|
15390
|
+
});
|
|
15391
|
+
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
15392
|
+
import { dirname as dirname12 } from "node:path";
|
|
15393
|
+
async function git(dir, args, opts = {}) {
|
|
15394
|
+
requireBinary("git");
|
|
15395
|
+
const res = await execa("git", args, {
|
|
15396
|
+
cwd: dir ?? void 0,
|
|
15397
|
+
reject: false,
|
|
15398
|
+
timeout: GIT_TIMEOUT_MS,
|
|
15399
|
+
// Kill the whole process group: a hung `git fetch` has spawned helper
|
|
15400
|
+
// processes (ssh, credential helpers) that keep the pipe open, so killing
|
|
15401
|
+
// git alone can leave the await pending on a dead parent.
|
|
15402
|
+
killSignal: "SIGKILL",
|
|
15403
|
+
// A large `rev-list` on a busy repo can be megabytes.
|
|
15404
|
+
maxBuffer: 64 * 1024 * 1024
|
|
15405
|
+
});
|
|
15406
|
+
const exitCode = res.exitCode ?? 1;
|
|
15407
|
+
if (!opts.allowFail && exitCode !== 0) {
|
|
15408
|
+
throw new Error(`git ${args.join(" ")} failed (${exitCode}): ${(res.stderr || "").trim()}`);
|
|
15409
|
+
}
|
|
15410
|
+
return { exitCode, stdout: res.stdout || "", stderr: res.stderr || "" };
|
|
15411
|
+
}
|
|
15412
|
+
async function createMirrorClone(sourcePath, mirrorPath, originUrl) {
|
|
15413
|
+
await mkdir9(dirname12(mirrorPath), { recursive: true });
|
|
15414
|
+
await git(null, ["clone", "--local", "--no-hardlinks", sourcePath, mirrorPath]);
|
|
15415
|
+
await git(mirrorPath, ["remote", "set-url", "origin", originUrl]);
|
|
15416
|
+
}
|
|
15417
|
+
async function setOriginUrl(dir, originUrl) {
|
|
15418
|
+
await git(dir, ["remote", "set-url", "origin", originUrl]);
|
|
15419
|
+
}
|
|
15420
|
+
async function readOriginUrl(dir) {
|
|
15421
|
+
const { exitCode, stdout } = await git(dir, ["remote", "get-url", "origin"], { allowFail: true });
|
|
15422
|
+
if (exitCode !== 0) return null;
|
|
15423
|
+
return stdout.trim() || null;
|
|
15424
|
+
}
|
|
15425
|
+
async function fetchOrigin(dir, branch) {
|
|
15426
|
+
await git(dir, ["fetch", "--quiet", "--no-tags", "origin", branch]);
|
|
15427
|
+
}
|
|
15428
|
+
async function revParse(dir, ref) {
|
|
15429
|
+
return (await git(dir, ["rev-parse", ref])).stdout.trim();
|
|
15430
|
+
}
|
|
15431
|
+
async function isAncestor(dir, a2, b) {
|
|
15432
|
+
return (await git(dir, ["merge-base", "--is-ancestor", a2, b], { allowFail: true })).exitCode === 0;
|
|
15433
|
+
}
|
|
15434
|
+
async function firstParentRange(dir, marker, tip) {
|
|
15435
|
+
const { stdout } = await git(dir, [
|
|
15436
|
+
"rev-list",
|
|
15437
|
+
"--reverse",
|
|
15438
|
+
"--first-parent",
|
|
15439
|
+
`${marker}..${tip}`
|
|
15440
|
+
]);
|
|
15441
|
+
return stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
15442
|
+
}
|
|
15443
|
+
async function readCommitMeta(dir, sha) {
|
|
15444
|
+
const { exitCode, stdout } = await git(
|
|
15445
|
+
dir,
|
|
15446
|
+
["show", "-s", "--format=%s%n%an%n%cI", sha],
|
|
15447
|
+
{ allowFail: true }
|
|
15448
|
+
);
|
|
15449
|
+
if (exitCode !== 0) return { message: "", authorName: null, committedAt: null };
|
|
15450
|
+
const [message = "", authorName = "", committedAt = ""] = stdout.split("\n");
|
|
15451
|
+
return {
|
|
15452
|
+
message: message.trim(),
|
|
15453
|
+
authorName: authorName.trim() || null,
|
|
15454
|
+
committedAt: committedAt.trim() || null
|
|
15455
|
+
};
|
|
15456
|
+
}
|
|
15457
|
+
async function detectDefaultBranch(dir) {
|
|
15458
|
+
const symbolic = await git(dir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], {
|
|
15459
|
+
allowFail: true
|
|
15460
|
+
});
|
|
15461
|
+
const remotes = await git(
|
|
15462
|
+
dir,
|
|
15463
|
+
["for-each-ref", "--format=%(refname:strip=3)", "refs/remotes/origin"],
|
|
15464
|
+
{ allowFail: true }
|
|
15465
|
+
);
|
|
15466
|
+
const current = await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"], { allowFail: true });
|
|
15467
|
+
return pickDefaultBranch({
|
|
15468
|
+
symbolicRef: symbolic.exitCode === 0 ? symbolic.stdout.trim() : null,
|
|
15469
|
+
remoteBranches: remotes.exitCode === 0 ? remotes.stdout.split("\n").map((l) => l.trim()).filter((l) => l && l !== "HEAD") : [],
|
|
15470
|
+
// Detached HEAD reports the literal "HEAD", which is not a branch name.
|
|
15471
|
+
currentBranch: current.exitCode === 0 && current.stdout.trim() !== "HEAD" ? current.stdout.trim() : null
|
|
15472
|
+
});
|
|
15473
|
+
}
|
|
15474
|
+
var GIT_TIMEOUT_MS;
|
|
15475
|
+
var init_localGit = __esm({
|
|
15476
|
+
"src/daemon/localGit.ts"() {
|
|
15477
|
+
"use strict";
|
|
15478
|
+
init_execa();
|
|
15479
|
+
init_preflight();
|
|
15480
|
+
init_defaultBranch();
|
|
15481
|
+
GIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
15482
|
+
}
|
|
15483
|
+
});
|
|
15484
|
+
|
|
15485
|
+
// src/daemon/localPoller.ts
|
|
15486
|
+
async function pollRepoOnce(entry, deps) {
|
|
15487
|
+
const repo = `${entry.repoOwner}/${entry.repoName}`;
|
|
15488
|
+
const gateTimeoutMs = deps.gateTimeoutMs ?? DEFAULT_GATE_TIMEOUT_MS;
|
|
15489
|
+
const row = await deps.readWatchedRepo(entry.watchedRepoId);
|
|
15490
|
+
if (!row) {
|
|
15491
|
+
return {
|
|
15492
|
+
kind: "halt",
|
|
15493
|
+
// Not a gate outcome — the repo is gone from under us. Reuses the
|
|
15494
|
+
// terminal code because the consequence is the same: this will not fix
|
|
15495
|
+
// itself, and `--resume` cannot clear it.
|
|
15496
|
+
code: "job-terminal",
|
|
15497
|
+
reason: `${repo} is no longer registered on the server (the watched repo was removed). Stop watching it here with \`archme local-watch --stop ${repo}\`.`
|
|
15498
|
+
};
|
|
15499
|
+
}
|
|
15500
|
+
if (row.sourceKind !== "local") {
|
|
15501
|
+
return {
|
|
15502
|
+
kind: "halt",
|
|
15503
|
+
code: "job-terminal",
|
|
15504
|
+
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}\`.`
|
|
15505
|
+
};
|
|
15506
|
+
}
|
|
15507
|
+
if (!row.enabled) {
|
|
15508
|
+
return { kind: "idle", reason: `${repo}: watching is disabled on the server` };
|
|
15509
|
+
}
|
|
15510
|
+
await deps.git.fetchOrigin(entry.mirrorPath, row.repoBranch);
|
|
15511
|
+
const marker = await deps.readMarker(row.projectId, row.archmeBranch);
|
|
15512
|
+
const upstreamTip = await deps.git.revParse(entry.mirrorPath, `origin/${row.repoBranch}`);
|
|
15513
|
+
const base = {
|
|
15514
|
+
repo,
|
|
15515
|
+
marker,
|
|
15516
|
+
upstreamTip,
|
|
15517
|
+
nowMs: deps.now(),
|
|
15518
|
+
gateTimeoutMs
|
|
15519
|
+
};
|
|
15520
|
+
if (!marker.sha) {
|
|
15521
|
+
return planPollTick({
|
|
15522
|
+
...base,
|
|
15523
|
+
markerIsAncestorOfTip: false,
|
|
15524
|
+
tipIsAncestorOfMarker: false,
|
|
15525
|
+
firstParentShas: [],
|
|
15526
|
+
jobForTarget: null
|
|
15527
|
+
});
|
|
15528
|
+
}
|
|
15529
|
+
const markerIsAncestorOfTip = await deps.git.isAncestor(entry.mirrorPath, marker.sha, upstreamTip);
|
|
15530
|
+
const tipIsAncestorOfMarker = await deps.git.isAncestor(entry.mirrorPath, upstreamTip, marker.sha);
|
|
15531
|
+
const firstParentShas = markerIsAncestorOfTip ? await deps.git.firstParentRange(entry.mirrorPath, marker.sha, upstreamTip) : [];
|
|
15532
|
+
const ancestry = {
|
|
15533
|
+
...base,
|
|
15534
|
+
markerIsAncestorOfTip,
|
|
15535
|
+
tipIsAncestorOfMarker,
|
|
15536
|
+
firstParentShas
|
|
15537
|
+
};
|
|
15538
|
+
const first = planPollTick({ ...ancestry, jobForTarget: null });
|
|
15539
|
+
if (first.kind !== "enqueue") return first;
|
|
15540
|
+
const jobForTarget = await deps.readJobForSha(row.projectId, row.archmeBranch, first.afterSha);
|
|
15541
|
+
const observedAtMs = deps.firstObservedAt(
|
|
15542
|
+
`${entry.watchedRepoId}:${first.afterSha}`,
|
|
15543
|
+
deps.now()
|
|
15544
|
+
);
|
|
15545
|
+
const decision = planPollTick({
|
|
15546
|
+
...ancestry,
|
|
15547
|
+
jobForTarget: jobForTarget ? { ...jobForTarget, createdAtMs: observedAtMs } : null
|
|
15548
|
+
});
|
|
15549
|
+
if (decision.kind !== "enqueue") return decision;
|
|
15550
|
+
const result = await deps.enqueue(entry, row, decision.beforeSha, decision.afterSha);
|
|
15551
|
+
if (result.dedup) {
|
|
15552
|
+
deps.log(` ${repo}: ${short3(decision.afterSha)} already enqueued elsewhere`);
|
|
15553
|
+
} else {
|
|
15554
|
+
deps.log(` ${repo}: enqueued ${short3(decision.afterSha)} (job ${result.jobId ?? "?"})`);
|
|
15555
|
+
}
|
|
15556
|
+
return decision;
|
|
15557
|
+
}
|
|
15558
|
+
async function pollAllRepos(repos, deps) {
|
|
15559
|
+
for (const entry of repos) {
|
|
15560
|
+
const key = `${entry.repoOwner}/${entry.repoName} (${entry.watchedRepoId.slice(0, 8)})`;
|
|
15561
|
+
if (entry.haltedAt) continue;
|
|
15562
|
+
try {
|
|
15563
|
+
const action = await pollRepoOnce(entry, deps);
|
|
15564
|
+
if (action.kind === "halt") {
|
|
15565
|
+
await deps.persistHalt(entry, action.reason, action.code);
|
|
15566
|
+
deps.log(
|
|
15567
|
+
`
|
|
15568
|
+
\u26A0 local poll HALTED for ${key}: ${action.reason}
|
|
15569
|
+
No further commits will be processed for this repo.
|
|
15570
|
+
Advancing past a failed commit would fold it into the next diff and break the
|
|
15571
|
+
one-ArchMe-commit-per-git-commit guarantee, so this stops instead.
|
|
15572
|
+
When the cause is fixed: archme local-watch --resume ${entry.repoOwner}/${entry.repoName}
|
|
15573
|
+
`
|
|
15574
|
+
);
|
|
15575
|
+
}
|
|
15576
|
+
} catch (err2) {
|
|
15577
|
+
deps.log(` local poll ${key} failed (will retry): ${err2.message}`);
|
|
15578
|
+
}
|
|
15579
|
+
}
|
|
15580
|
+
}
|
|
15581
|
+
function makePollerDeps(client, log, gateTimeoutMs) {
|
|
15582
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
15583
|
+
return {
|
|
15584
|
+
git: localGit_exports,
|
|
15585
|
+
log,
|
|
15586
|
+
now: () => Date.now(),
|
|
15587
|
+
gateTimeoutMs,
|
|
15588
|
+
firstObservedAt(key, nowMs) {
|
|
15589
|
+
const existing = firstSeen.get(key);
|
|
15590
|
+
if (existing !== void 0) return existing;
|
|
15591
|
+
firstSeen.set(key, nowMs);
|
|
15592
|
+
return nowMs;
|
|
15593
|
+
},
|
|
15594
|
+
async persistHalt(entry, reason, code) {
|
|
15595
|
+
const repos = await loadLocalRepos();
|
|
15596
|
+
await saveLocalRepos(
|
|
15597
|
+
markHalted(repos, entry.watchedRepoId, reason, (/* @__PURE__ */ new Date()).toISOString(), code)
|
|
15598
|
+
);
|
|
15599
|
+
},
|
|
15600
|
+
async readWatchedRepo(watchedRepoId) {
|
|
15601
|
+
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();
|
|
15602
|
+
if (error || !data) return null;
|
|
15603
|
+
const r = data;
|
|
15604
|
+
return {
|
|
15605
|
+
projectId: r.project_id,
|
|
15606
|
+
repoBranch: r.branch,
|
|
15607
|
+
// Mirrors the webhook's own fallback (`commit_update_archme_branch ??
|
|
15608
|
+
// branch`), so both producers resolve the same lane for the same row.
|
|
15609
|
+
archmeBranch: r.commit_update_archme_branch ?? r.branch,
|
|
15610
|
+
sourceKind: r.source_kind,
|
|
15611
|
+
enabled: r.enabled
|
|
15612
|
+
};
|
|
15613
|
+
},
|
|
15614
|
+
// Shared with the job runner and local-watch's readiness check: three
|
|
15615
|
+
// readers of the same marker drifting apart is how one of them ends up
|
|
15616
|
+
// deciding against a value the others don't see.
|
|
15617
|
+
readMarker: (projectId, archmeBranch) => readBranchMarker(client, projectId, archmeBranch),
|
|
15618
|
+
async readJobForSha(projectId, archmeBranch, sha) {
|
|
15619
|
+
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);
|
|
15620
|
+
if (error || !data || data.length === 0) return null;
|
|
15621
|
+
const row = data[0];
|
|
15622
|
+
return {
|
|
15623
|
+
status: row.status,
|
|
15624
|
+
attemptCount: row.attempt_count ?? 0,
|
|
15625
|
+
createdAtMs: new Date(row.created_at).getTime()
|
|
15626
|
+
};
|
|
15627
|
+
},
|
|
15628
|
+
async enqueue(entry, row, beforeSha, afterSha) {
|
|
15629
|
+
const meta = await readCommitMeta(entry.mirrorPath, afterSha);
|
|
15630
|
+
const { data, error } = await client.rpc("enqueue_local_archme_job", {
|
|
15631
|
+
p_watched_repo_id: entry.watchedRepoId,
|
|
15632
|
+
// From the ROW, not the registry: these decide which lane the job lands
|
|
15633
|
+
// in, and a stale copy would enqueue against the wrong branch.
|
|
15634
|
+
p_repo_branch: row.repoBranch,
|
|
15635
|
+
p_archme_branch: row.archmeBranch,
|
|
15636
|
+
p_before_sha: beforeSha,
|
|
15637
|
+
p_after_sha: afterSha,
|
|
15638
|
+
p_commit_message: meta.message || null,
|
|
15639
|
+
p_head_commit_timestamp: meta.committedAt,
|
|
15640
|
+
p_author_name: meta.authorName,
|
|
15641
|
+
// `author_login` is a GitHub concept with no git equivalent. The webhook
|
|
15642
|
+
// path fills it from the push payload; local mode leaves it null.
|
|
15643
|
+
p_author_login: null
|
|
15644
|
+
});
|
|
15645
|
+
if (error) throw new Error(`enqueue_local_archme_job: ${error.message}`);
|
|
15646
|
+
return data ?? {};
|
|
15647
|
+
}
|
|
15648
|
+
};
|
|
15649
|
+
}
|
|
15650
|
+
function short3(sha) {
|
|
15651
|
+
return sha.slice(0, 7);
|
|
15652
|
+
}
|
|
15653
|
+
var DEFAULT_POLL_INTERVAL_MS, DEFAULT_GATE_TIMEOUT_MS;
|
|
15654
|
+
var init_localPoller = __esm({
|
|
15655
|
+
"src/daemon/localPoller.ts"() {
|
|
15656
|
+
"use strict";
|
|
15657
|
+
init_localPoll();
|
|
15658
|
+
init_localRepos();
|
|
15659
|
+
init_branchMarker();
|
|
15660
|
+
init_localGit();
|
|
15661
|
+
DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
15662
|
+
DEFAULT_GATE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
15663
|
+
}
|
|
15664
|
+
});
|
|
15665
|
+
|
|
14915
15666
|
// src/daemon/run.ts
|
|
14916
15667
|
var run_exports = {};
|
|
14917
15668
|
__export(run_exports, {
|
|
@@ -14933,11 +15684,11 @@ __export(run_exports, {
|
|
|
14933
15684
|
});
|
|
14934
15685
|
import { createClient } from "@supabase/supabase-js";
|
|
14935
15686
|
import { randomUUID } from "node:crypto";
|
|
14936
|
-
import { mkdtemp as mkdtemp3, mkdir as
|
|
15687
|
+
import { mkdtemp as mkdtemp3, mkdir as mkdir10, writeFile as writeFile10, rm as rm6, stat as stat8, chmod as chmod3 } from "node:fs/promises";
|
|
14937
15688
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
14938
|
-
import { join as
|
|
15689
|
+
import { join as join18 } from "node:path";
|
|
14939
15690
|
import { pathToFileURL } from "node:url";
|
|
14940
|
-
import { homedir as
|
|
15691
|
+
import { homedir as homedir5 } from "node:os";
|
|
14941
15692
|
function attributeKillCause(result, recorded) {
|
|
14942
15693
|
if (recorded) return recorded;
|
|
14943
15694
|
if (result.signal === "SIGTERM") return "timeout";
|
|
@@ -14968,7 +15719,7 @@ async function lookupProjectSlug(client, projectId) {
|
|
|
14968
15719
|
return `${orgSlug}/${data.slug}`;
|
|
14969
15720
|
}
|
|
14970
15721
|
function cloneDirFor(baseDir, owner, name) {
|
|
14971
|
-
return
|
|
15722
|
+
return join18(baseDir, owner, name);
|
|
14972
15723
|
}
|
|
14973
15724
|
async function exists(p) {
|
|
14974
15725
|
try {
|
|
@@ -15013,9 +15764,9 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
|
|
|
15013
15764
|
};
|
|
15014
15765
|
}
|
|
15015
15766
|
async function buildGitEnv(credentials) {
|
|
15016
|
-
const dir = await mkdtemp3(
|
|
15017
|
-
const askpassPath =
|
|
15018
|
-
await
|
|
15767
|
+
const dir = await mkdtemp3(join18(tmpdir3(), "archme-git-askpass-"));
|
|
15768
|
+
const askpassPath = join18(dir, "askpass.sh");
|
|
15769
|
+
await writeFile10(
|
|
15019
15770
|
askpassPath,
|
|
15020
15771
|
[
|
|
15021
15772
|
"#!/bin/sh",
|
|
@@ -15028,7 +15779,7 @@ async function buildGitEnv(credentials) {
|
|
|
15028
15779
|
].join("\n"),
|
|
15029
15780
|
{ mode: 448 }
|
|
15030
15781
|
);
|
|
15031
|
-
await
|
|
15782
|
+
await chmod3(askpassPath, 448).catch(() => {
|
|
15032
15783
|
});
|
|
15033
15784
|
const gitConfigEntries = [
|
|
15034
15785
|
["credential.helper", ""],
|
|
@@ -15053,16 +15804,38 @@ async function buildGitEnv(credentials) {
|
|
|
15053
15804
|
return { env, cleanup: () => rm6(dir, { recursive: true, force: true }).catch(() => {
|
|
15054
15805
|
}) };
|
|
15055
15806
|
}
|
|
15807
|
+
async function ensureLocalCloneAndFetch(job, entry) {
|
|
15808
|
+
const dir = entry.mirrorPath;
|
|
15809
|
+
await cloneMutex.runExclusive(dir, async () => {
|
|
15810
|
+
if (!await exists(join18(dir, ".git"))) {
|
|
15811
|
+
throw new Error(
|
|
15812
|
+
`local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
|
|
15813
|
+
);
|
|
15814
|
+
}
|
|
15815
|
+
await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, reject: false });
|
|
15816
|
+
const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, reject: false });
|
|
15817
|
+
if (afterCheck.exitCode !== 0) {
|
|
15818
|
+
await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
|
|
15819
|
+
}
|
|
15820
|
+
}).catch((err2) => {
|
|
15821
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
15822
|
+
throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
|
|
15823
|
+
});
|
|
15824
|
+
return { dir, gitEnv: process.env, cleanupGitEnv: async () => {
|
|
15825
|
+
} };
|
|
15826
|
+
}
|
|
15056
15827
|
async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
15828
|
+
const localEntry = findLocalRepoForJob(await loadLocalRepos(), job);
|
|
15829
|
+
if (localEntry) return await ensureLocalCloneAndFetch(job, localEntry);
|
|
15057
15830
|
const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
|
|
15058
15831
|
if (!deviceId) throw new Error("daemon is not signed in with a registered device");
|
|
15059
15832
|
const credentials = await getRepoCredentials(ctx, job.id, deviceId);
|
|
15060
|
-
await
|
|
15833
|
+
await mkdir10(ctx.cloneBaseDir, { recursive: true });
|
|
15061
15834
|
const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
|
|
15062
15835
|
const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
|
|
15063
15836
|
await cloneMutex.runExclusive(dir, async () => {
|
|
15064
|
-
if (!await exists(
|
|
15065
|
-
await
|
|
15837
|
+
if (!await exists(join18(dir, ".git"))) {
|
|
15838
|
+
await mkdir10(join18(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
|
|
15066
15839
|
console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
|
|
15067
15840
|
await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
|
|
15068
15841
|
} else {
|
|
@@ -15083,22 +15856,22 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
|
15083
15856
|
await execa("git", ["fetch", "origin", job.before_sha], { cwd: dir, env, reject: false });
|
|
15084
15857
|
}
|
|
15085
15858
|
}
|
|
15086
|
-
}).catch(async (
|
|
15859
|
+
}).catch(async (err2) => {
|
|
15087
15860
|
await cleanupGitEnv();
|
|
15088
|
-
const msg =
|
|
15861
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
15089
15862
|
throw new Error(`GitHub App cannot access ${job.repo_owner}/${job.repo_name}: ${msg}`);
|
|
15090
15863
|
});
|
|
15091
15864
|
return { dir, gitEnv: env, cleanupGitEnv };
|
|
15092
15865
|
}
|
|
15093
15866
|
async function createJobWorktree(repoDir, job) {
|
|
15094
|
-
const parentDir = await mkdtemp3(
|
|
15095
|
-
const worktreeDir =
|
|
15867
|
+
const parentDir = await mkdtemp3(join18(tmpdir3(), "archme-job-worktree-"));
|
|
15868
|
+
const worktreeDir = join18(parentDir, "repo");
|
|
15096
15869
|
try {
|
|
15097
15870
|
await cloneMutex.runExclusive(repoDir, () => execa("git", ["worktree", "add", "--detach", worktreeDir, job.after_sha], { cwd: repoDir, reject: true }));
|
|
15098
|
-
} catch (
|
|
15871
|
+
} catch (err2) {
|
|
15099
15872
|
await rm6(parentDir, { recursive: true, force: true }).catch(() => {
|
|
15100
15873
|
});
|
|
15101
|
-
throw
|
|
15874
|
+
throw err2;
|
|
15102
15875
|
}
|
|
15103
15876
|
return {
|
|
15104
15877
|
worktreeDir,
|
|
@@ -15117,7 +15890,7 @@ function noUsableBaseError(branch, markerSha, detail) {
|
|
|
15117
15890
|
);
|
|
15118
15891
|
}
|
|
15119
15892
|
async function isShaReachable(repoDir, sha) {
|
|
15120
|
-
const { exitCode } = await execa("git", ["cat-file", "-e", sha], { cwd: repoDir, reject: false });
|
|
15893
|
+
const { exitCode } = await execa("git", ["cat-file", "-e", `${sha}^{commit}`], { cwd: repoDir, reject: false });
|
|
15121
15894
|
return exitCode === 0;
|
|
15122
15895
|
}
|
|
15123
15896
|
async function buildDiffPayload(repoDir, before, after) {
|
|
@@ -15140,7 +15913,16 @@ async function forcePushProvenance(repoDir, base, after) {
|
|
|
15140
15913
|
const forkNote = fork.trim() ? ` (history diverged at \`${fork.trim().slice(0, 8)}\`)` : "";
|
|
15141
15914
|
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
15915
|
}
|
|
15916
|
+
function isGitSha(v) {
|
|
15917
|
+
return typeof v === "string" && GIT_SHA_RE.test(v);
|
|
15918
|
+
}
|
|
15919
|
+
function assertGitSha(v, field) {
|
|
15920
|
+
if (!isGitSha(v)) {
|
|
15921
|
+
throw new Error(`${field} ${v ?? "(null)"} is not a 40-character git SHA`);
|
|
15922
|
+
}
|
|
15923
|
+
}
|
|
15143
15924
|
async function computeDiff(job, repoDir, marker, gitEnv) {
|
|
15925
|
+
assertGitSha(job.after_sha, "after_sha");
|
|
15144
15926
|
if (!await isShaReachable(repoDir, job.after_sha)) {
|
|
15145
15927
|
throw new Error(`after_sha ${job.after_sha} is not reachable after fetch`);
|
|
15146
15928
|
}
|
|
@@ -15163,6 +15945,9 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
|
|
|
15163
15945
|
if (marker.repo !== repo) {
|
|
15164
15946
|
throw noUsableBaseError(job.archme_branch, marker.sha, `marker repo ${marker.repo ?? "n/a"} does not match job repo ${repo}`);
|
|
15165
15947
|
}
|
|
15948
|
+
if (!isGitSha(marker.sha)) {
|
|
15949
|
+
throw noUsableBaseError(job.archme_branch, marker.sha, `marker SHA ${marker.sha} is not a 40-character git SHA`);
|
|
15950
|
+
}
|
|
15166
15951
|
const base = marker.sha;
|
|
15167
15952
|
if (base === after) return "skip";
|
|
15168
15953
|
if (!await isShaReachable(repoDir, base)) {
|
|
@@ -15177,11 +15962,6 @@ async function computeDiff(job, repoDir, marker, gitEnv) {
|
|
|
15177
15962
|
if (job.forced) payload.provenance = await forcePushProvenance(repoDir, base, after);
|
|
15178
15963
|
return payload;
|
|
15179
15964
|
}
|
|
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
15965
|
function buildDiffPrompt(job, projectSlug, diff, checkoutDir, affectedSeed, bugsFolderId) {
|
|
15186
15966
|
const sentinel = `UNTRUSTED-DIFF-${randomUUID()}`;
|
|
15187
15967
|
const seedBlock = affectedSeed ? `
|
|
@@ -15515,9 +16295,9 @@ async function probeMcpAuth(mcpUrl, bearer, opts = {}) {
|
|
|
15515
16295
|
};
|
|
15516
16296
|
}
|
|
15517
16297
|
lastDetail = "MCP returned 401 \u2014 token rejected by JWKS verification";
|
|
15518
|
-
} catch (
|
|
16298
|
+
} catch (err2) {
|
|
15519
16299
|
lastStatus = null;
|
|
15520
|
-
lastDetail = `network error reaching MCP: ${
|
|
16300
|
+
lastDetail = `network error reaching MCP: ${err2 instanceof Error ? err2.message : String(err2)}`;
|
|
15521
16301
|
}
|
|
15522
16302
|
}
|
|
15523
16303
|
if (lastStatus === 401) {
|
|
@@ -15655,6 +16435,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15655
16435
|
};
|
|
15656
16436
|
let metrics = { durations_ms: timings };
|
|
15657
16437
|
let logs = { stdout: "", stderr: "" };
|
|
16438
|
+
let sentPrompt;
|
|
15658
16439
|
const cleanupFns = [];
|
|
15659
16440
|
const heartbeat = setInterval(() => {
|
|
15660
16441
|
rpcHeartbeat(ctx, job.id, deviceId);
|
|
@@ -15765,7 +16546,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15765
16546
|
const affected = resolveAffected(seedArch, diff.changedFiles, changedSymbolsByFile);
|
|
15766
16547
|
affectedForComplete = affected;
|
|
15767
16548
|
const symbolsInFile = async (file) => {
|
|
15768
|
-
const outline = await runOutline(
|
|
16549
|
+
const outline = await runOutline(join18(worktreeDir, file));
|
|
15769
16550
|
if (outline.tier !== 2) return null;
|
|
15770
16551
|
return outline.data.symbols.map((s) => s.name);
|
|
15771
16552
|
};
|
|
@@ -15788,6 +16569,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15788
16569
|
}
|
|
15789
16570
|
const bugsFolderId = await ensureBugsFolder(ctx.supabase, job.project_id);
|
|
15790
16571
|
const prompt = buildDiffPrompt(job, projectSlug, diff, worktreeDir, affectedSeed, bugsFolderId);
|
|
16572
|
+
sentPrompt = prompt;
|
|
15791
16573
|
if (cancelled.has(job.id)) {
|
|
15792
16574
|
log(`cancelled before claude spawn, aborting`);
|
|
15793
16575
|
return;
|
|
@@ -15893,9 +16675,9 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15893
16675
|
true,
|
|
15894
16676
|
affectedFieldForComplete(job.source_mode, affectedForComplete)
|
|
15895
16677
|
);
|
|
15896
|
-
} catch (
|
|
16678
|
+
} catch (err2) {
|
|
15897
16679
|
if (cancelled.has(job.id)) {
|
|
15898
|
-
console.log(`[${job.id}] cancelled, swallowing error: ${
|
|
16680
|
+
console.log(`[${job.id}] cancelled, swallowing error: ${err2 instanceof Error ? err2.message : err2}`);
|
|
15899
16681
|
await rpcRollback(ctx, job.id, deviceId);
|
|
15900
16682
|
return;
|
|
15901
16683
|
}
|
|
@@ -15903,10 +16685,15 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
15903
16685
|
console.log(`[${job.id}] shutdown \u2014 routed to retry by shutdown handler`);
|
|
15904
16686
|
return;
|
|
15905
16687
|
}
|
|
15906
|
-
const msg =
|
|
15907
|
-
const haystack = `${msg}
|
|
15908
|
-
${logs.stderr}
|
|
15909
|
-
const plan = rateLimitDeferPlanForExit({
|
|
16688
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
16689
|
+
const haystack = stripPromptEcho(`${msg}
|
|
16690
|
+
${logs.stderr}`, sentPrompt);
|
|
16691
|
+
const plan = rateLimitDeferPlanForExit({
|
|
16692
|
+
message: msg,
|
|
16693
|
+
stderr: logs.stderr,
|
|
16694
|
+
stdout: logs.stdout,
|
|
16695
|
+
prompt: sentPrompt
|
|
16696
|
+
});
|
|
15910
16697
|
if (plan) {
|
|
15911
16698
|
const { parsed, pauseUntil, info } = plan;
|
|
15912
16699
|
if (!parsed) {
|
|
@@ -15975,7 +16762,7 @@ async function dispatch(sched) {
|
|
|
15975
16762
|
}
|
|
15976
16763
|
ctxByJob.set(claimed.job.id, claimed.ctx);
|
|
15977
16764
|
deviceByJob.set(claimed.job.id, claimed.deviceId);
|
|
15978
|
-
const p = sched.run(claimed.ctx, claimed.job, claimed.deviceId).catch((
|
|
16765
|
+
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
16766
|
ctxByJob.delete(claimed.job.id);
|
|
15980
16767
|
deviceByJob.delete(claimed.job.id);
|
|
15981
16768
|
slot();
|
|
@@ -16011,7 +16798,7 @@ function subscribe(client, sched) {
|
|
|
16011
16798
|
"postgres_changes",
|
|
16012
16799
|
{ event: "INSERT", schema: "public", table: "archme_jobs" },
|
|
16013
16800
|
(payload) => {
|
|
16014
|
-
if (payload.new.status === "pending") dispatch(sched).catch((
|
|
16801
|
+
if (payload.new.status === "pending") dispatch(sched).catch((err2) => console.error("dispatch:", err2));
|
|
16015
16802
|
}
|
|
16016
16803
|
).on(
|
|
16017
16804
|
"postgres_changes",
|
|
@@ -16028,12 +16815,12 @@ function subscribe(client, sched) {
|
|
|
16028
16815
|
console.log(`[${payload.new.id}] received cancellation (pre-spawn), will abort`);
|
|
16029
16816
|
}
|
|
16030
16817
|
} else if (payload.new.status === "pending") {
|
|
16031
|
-
dispatch(sched).catch((
|
|
16818
|
+
dispatch(sched).catch((err2) => console.error("dispatch:", err2));
|
|
16032
16819
|
}
|
|
16033
16820
|
}
|
|
16034
16821
|
).subscribe((status) => {
|
|
16035
16822
|
console.log(`realtime channel: ${status}`);
|
|
16036
|
-
if (status === "SUBSCRIBED") dispatch(sched).catch((
|
|
16823
|
+
if (status === "SUBSCRIBED") dispatch(sched).catch((err2) => console.error("startup dispatch:", err2));
|
|
16037
16824
|
});
|
|
16038
16825
|
return () => {
|
|
16039
16826
|
client.removeChannel(channel);
|
|
@@ -16074,9 +16861,9 @@ async function signInAccount(creds, makeAuth) {
|
|
|
16074
16861
|
active = { auth, cred, result };
|
|
16075
16862
|
usedFallback = attempts > 1;
|
|
16076
16863
|
break;
|
|
16077
|
-
} catch (
|
|
16864
|
+
} catch (err2) {
|
|
16078
16865
|
auth.clearRefreshTimer();
|
|
16079
|
-
lastErr =
|
|
16866
|
+
lastErr = err2;
|
|
16080
16867
|
}
|
|
16081
16868
|
}
|
|
16082
16869
|
if (active) {
|
|
@@ -16093,19 +16880,19 @@ async function runDaemon() {
|
|
|
16093
16880
|
let lockHandle = null;
|
|
16094
16881
|
try {
|
|
16095
16882
|
lockHandle = await acquireSingleInstanceLock(resolvePidfilePath(process.env));
|
|
16096
|
-
} catch (
|
|
16097
|
-
if (
|
|
16098
|
-
console.error(String(
|
|
16883
|
+
} catch (err2) {
|
|
16884
|
+
if (err2 instanceof SingleInstanceLockedError) {
|
|
16885
|
+
console.error(String(err2.message));
|
|
16099
16886
|
process.exit(64);
|
|
16100
16887
|
}
|
|
16101
|
-
throw
|
|
16888
|
+
throw err2;
|
|
16102
16889
|
}
|
|
16103
16890
|
let cfg;
|
|
16104
16891
|
try {
|
|
16105
16892
|
cfg = resolveConfig();
|
|
16106
|
-
} catch (
|
|
16107
|
-
if (
|
|
16108
|
-
console.error(
|
|
16893
|
+
} catch (err2) {
|
|
16894
|
+
if (err2 instanceof MissingConfigError) {
|
|
16895
|
+
console.error(err2.message);
|
|
16109
16896
|
if (lockHandle) {
|
|
16110
16897
|
try {
|
|
16111
16898
|
await lockHandle.release();
|
|
@@ -16114,7 +16901,7 @@ async function runDaemon() {
|
|
|
16114
16901
|
}
|
|
16115
16902
|
process.exit(1);
|
|
16116
16903
|
}
|
|
16117
|
-
throw
|
|
16904
|
+
throw err2;
|
|
16118
16905
|
}
|
|
16119
16906
|
SUPABASE_URL = cfg.supabaseUrl;
|
|
16120
16907
|
SUPABASE_ANON_KEY = cfg.supabaseAnonKey;
|
|
@@ -16148,9 +16935,9 @@ async function runDaemon() {
|
|
|
16148
16935
|
let credentials;
|
|
16149
16936
|
try {
|
|
16150
16937
|
credentials = assertSingleAccount(loaded);
|
|
16151
|
-
} catch (
|
|
16152
|
-
if (
|
|
16153
|
-
console.error(
|
|
16938
|
+
} catch (err2) {
|
|
16939
|
+
if (err2 instanceof MultipleAccountsError) {
|
|
16940
|
+
console.error(err2.message);
|
|
16154
16941
|
if (lockHandle) {
|
|
16155
16942
|
try {
|
|
16156
16943
|
await lockHandle.release();
|
|
@@ -16159,7 +16946,7 @@ async function runDaemon() {
|
|
|
16159
16946
|
}
|
|
16160
16947
|
process.exit(78);
|
|
16161
16948
|
}
|
|
16162
|
-
throw
|
|
16949
|
+
throw err2;
|
|
16163
16950
|
}
|
|
16164
16951
|
const devices = buildDevices(credentials);
|
|
16165
16952
|
console.log(` booting 1 session across ${devices.length} org(s)`);
|
|
@@ -16251,16 +17038,49 @@ async function runDaemon() {
|
|
|
16251
17038
|
console.warn(`daemon_pauses lookup failed (proceeding unpaused): ${e.message}`);
|
|
16252
17039
|
}
|
|
16253
17040
|
const sweepTimer = setInterval(
|
|
16254
|
-
() => dispatch(sched).catch((
|
|
17041
|
+
() => dispatch(sched).catch((err2) => console.error("safety sweep:", err2)),
|
|
16255
17042
|
SAFETY_SWEEP_MS
|
|
16256
17043
|
);
|
|
16257
|
-
const
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
|
|
17044
|
+
const localPollDeps = makePollerDeps(client, (m) => console.log(m));
|
|
17045
|
+
const localPollIntervalMs = Number.parseInt(process.env.ARCHME_LOCAL_POLL_INTERVAL_MS ?? "", 10);
|
|
17046
|
+
const pollEveryMs = Number.isFinite(localPollIntervalMs) && localPollIntervalMs > 0 ? localPollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
|
|
17047
|
+
let localPollInFlight = false;
|
|
17048
|
+
const runLocalPoll = async () => {
|
|
17049
|
+
if (localPollInFlight) return;
|
|
17050
|
+
localPollInFlight = true;
|
|
17051
|
+
try {
|
|
17052
|
+
const repos = await loadLocalRepos();
|
|
17053
|
+
if (repos.length === 0) return;
|
|
17054
|
+
await pollAllRepos(repos, localPollDeps);
|
|
17055
|
+
} finally {
|
|
17056
|
+
localPollInFlight = false;
|
|
17057
|
+
}
|
|
17058
|
+
};
|
|
17059
|
+
const initialLocalRepos = await loadLocalRepos();
|
|
17060
|
+
if (initialLocalRepos.length > 0) {
|
|
17061
|
+
const halted = initialLocalRepos.filter((r) => r.haltedAt);
|
|
17062
|
+
console.log(
|
|
17063
|
+
` local-poll: ${initialLocalRepos.length} repo(s) every ${Math.round(pollEveryMs / 1e3)}s`
|
|
17064
|
+
);
|
|
17065
|
+
for (const r of halted) {
|
|
17066
|
+
console.log(
|
|
17067
|
+
` \u26A0 ${r.repoOwner}/${r.repoName} is HALTED (${r.haltReason ?? "unknown reason"}) \u2014 resume with \`archme local-watch --resume ${r.repoOwner}/${r.repoName}\``
|
|
17068
|
+
);
|
|
17069
|
+
}
|
|
17070
|
+
}
|
|
17071
|
+
const localPollTimer = setInterval(
|
|
17072
|
+
() => void runLocalPoll().catch((err2) => console.error("local poll:", err2)),
|
|
17073
|
+
pollEveryMs
|
|
17074
|
+
);
|
|
17075
|
+
void runLocalPoll().catch((err2) => console.error("local poll:", err2));
|
|
17076
|
+
const shutdown = async (signal) => {
|
|
17077
|
+
if (shuttingDown) return;
|
|
17078
|
+
shuttingDown = true;
|
|
17079
|
+
console.log(`
|
|
17080
|
+
${signal} received, shutting down`);
|
|
16262
17081
|
for (const ctx of sessions) ctx.auth.clearRefreshTimer();
|
|
16263
17082
|
clearInterval(sweepTimer);
|
|
17083
|
+
clearInterval(localPollTimer);
|
|
16264
17084
|
for (const u2 of unsubscribers) u2();
|
|
16265
17085
|
const SHUTDOWN_DEADLINE_MS = 3e4;
|
|
16266
17086
|
const deadline = setTimeout(() => {
|
|
@@ -16300,7 +17120,7 @@ ${signal} received, shutting down`);
|
|
|
16300
17120
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
16301
17121
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
16302
17122
|
}
|
|
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;
|
|
17123
|
+
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
17124
|
var init_run = __esm({
|
|
16305
17125
|
"src/daemon/run.ts"() {
|
|
16306
17126
|
"use strict";
|
|
@@ -16331,6 +17151,10 @@ var init_run = __esm({
|
|
|
16331
17151
|
init_rateLimit();
|
|
16332
17152
|
init_version();
|
|
16333
17153
|
init_materializeDiff();
|
|
17154
|
+
init_branchMarker();
|
|
17155
|
+
init_branchMarker();
|
|
17156
|
+
init_localRepos();
|
|
17157
|
+
init_localPoller();
|
|
16334
17158
|
HEARTBEAT_INTERVAL_MS = 6e4;
|
|
16335
17159
|
CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
16336
17160
|
CLAUDE_LARGE_DIFF_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
@@ -16351,8 +17175,9 @@ var init_run = __esm({
|
|
|
16351
17175
|
MAX_CONCURRENT_JOBS = Math.max(1, Number(process.env.MAX_CONCURRENT_JOBS) || 3);
|
|
16352
17176
|
ctxByJob = /* @__PURE__ */ new Map();
|
|
16353
17177
|
deviceByJob = /* @__PURE__ */ new Map();
|
|
16354
|
-
DEFAULT_CLONE_BASE_DIR = `${
|
|
17178
|
+
DEFAULT_CLONE_BASE_DIR = `${homedir5()}/.archme-job-runner/clones`;
|
|
16355
17179
|
INLINE_PATCH_BYTE_CAP = 2e5;
|
|
17180
|
+
GIT_SHA_RE = /^[0-9a-f]{40}$/;
|
|
16356
17181
|
CURATOR_PREAMBLE = `## Your role
|
|
16357
17182
|
|
|
16358
17183
|
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 +17342,8 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
16517
17342
|
}
|
|
16518
17343
|
};
|
|
16519
17344
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
16520
|
-
runDaemon().catch((
|
|
16521
|
-
console.error("fatal:",
|
|
17345
|
+
runDaemon().catch((err2) => {
|
|
17346
|
+
console.error("fatal:", err2);
|
|
16522
17347
|
process.exit(1);
|
|
16523
17348
|
});
|
|
16524
17349
|
}
|
|
@@ -16557,12 +17382,12 @@ async function dispatchDaemon(route2) {
|
|
|
16557
17382
|
console.log(`Logs: ${out.logPath}`);
|
|
16558
17383
|
console.log(`State: ${out.result.actions.join(" \u2192 ")}`);
|
|
16559
17384
|
return 0;
|
|
16560
|
-
} catch (
|
|
16561
|
-
if (
|
|
16562
|
-
console.error(String(
|
|
17385
|
+
} catch (err2) {
|
|
17386
|
+
if (err2 instanceof NotSignedInError) {
|
|
17387
|
+
console.error(String(err2.message));
|
|
16563
17388
|
return 75;
|
|
16564
17389
|
}
|
|
16565
|
-
console.error(String(
|
|
17390
|
+
console.error(String(err2));
|
|
16566
17391
|
return 1;
|
|
16567
17392
|
}
|
|
16568
17393
|
}
|
|
@@ -16575,13 +17400,13 @@ async function dispatchDaemon(route2) {
|
|
|
16575
17400
|
case "restart": {
|
|
16576
17401
|
try {
|
|
16577
17402
|
await ensureStartPrerequisites({ noLogin: route2.noLogin });
|
|
16578
|
-
} catch (
|
|
16579
|
-
if (
|
|
16580
|
-
console.error(String(
|
|
17403
|
+
} catch (err2) {
|
|
17404
|
+
if (err2 instanceof NotSignedInError) {
|
|
17405
|
+
console.error(String(err2.message));
|
|
16581
17406
|
console.error("(daemon left running unchanged)");
|
|
16582
17407
|
return 75;
|
|
16583
17408
|
}
|
|
16584
|
-
console.error(String(
|
|
17409
|
+
console.error(String(err2));
|
|
16585
17410
|
console.error("(daemon left running unchanged)");
|
|
16586
17411
|
return 1;
|
|
16587
17412
|
}
|
|
@@ -16669,9 +17494,9 @@ async function defaultLoadSupabaseState(_deviceId) {
|
|
|
16669
17494
|
async function defaultLoadUptime(pid) {
|
|
16670
17495
|
if (process.platform === "linux") {
|
|
16671
17496
|
try {
|
|
16672
|
-
const { readFile:
|
|
16673
|
-
const
|
|
16674
|
-
const parts =
|
|
17497
|
+
const { readFile: readFile14 } = await import("node:fs/promises");
|
|
17498
|
+
const stat11 = await readFile14(`/proc/${pid}/stat`, "utf8");
|
|
17499
|
+
const parts = stat11.split(") ").pop()?.split(" ") ?? [];
|
|
16675
17500
|
const ticks = Number(parts[19]);
|
|
16676
17501
|
if (!Number.isFinite(ticks)) return null;
|
|
16677
17502
|
const { readFile: rf } = await import("node:fs/promises");
|
|
@@ -16738,8 +17563,8 @@ async function createAuthedClient(opts) {
|
|
|
16738
17563
|
let loaded;
|
|
16739
17564
|
try {
|
|
16740
17565
|
loaded = await loadStoredAndRefresh();
|
|
16741
|
-
} catch (
|
|
16742
|
-
const msg =
|
|
17566
|
+
} catch (err2) {
|
|
17567
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
16743
17568
|
throw new Error(`${msg}
|
|
16744
17569
|
\u2192 run \`archme login\` first`);
|
|
16745
17570
|
}
|
|
@@ -16770,9 +17595,9 @@ function deriveWebOrigin(apiUrl, onMalformed = "throw") {
|
|
|
16770
17595
|
let parsed;
|
|
16771
17596
|
try {
|
|
16772
17597
|
parsed = new URL(apiUrl);
|
|
16773
|
-
} catch (
|
|
17598
|
+
} catch (err2) {
|
|
16774
17599
|
if (onMalformed === "throw") {
|
|
16775
|
-
throw new Error(`could not parse API URL \`${apiUrl}\`: ${
|
|
17600
|
+
throw new Error(`could not parse API URL \`${apiUrl}\`: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
16776
17601
|
}
|
|
16777
17602
|
return "http://localhost:5173";
|
|
16778
17603
|
}
|
|
@@ -16981,8 +17806,8 @@ var init_transcript_signals = __esm({
|
|
|
16981
17806
|
});
|
|
16982
17807
|
|
|
16983
17808
|
// src/scan/report.ts
|
|
16984
|
-
import { mkdir as
|
|
16985
|
-
import { dirname as
|
|
17809
|
+
import { mkdir as mkdir11, writeFile as writeFile11 } from "node:fs/promises";
|
|
17810
|
+
import { dirname as dirname13 } from "node:path";
|
|
16986
17811
|
async function buildReport(input) {
|
|
16987
17812
|
return {
|
|
16988
17813
|
schema_version: REPORT_SCHEMA_VERSION,
|
|
@@ -17016,10 +17841,10 @@ async function buildReport(input) {
|
|
|
17016
17841
|
};
|
|
17017
17842
|
}
|
|
17018
17843
|
async function writeReport(reportPath, orphansTxtPath, report, orphansFull) {
|
|
17019
|
-
await
|
|
17020
|
-
await
|
|
17021
|
-
await
|
|
17022
|
-
await
|
|
17844
|
+
await mkdir11(dirname13(reportPath), { recursive: true });
|
|
17845
|
+
await writeFile11(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
17846
|
+
await mkdir11(dirname13(orphansTxtPath), { recursive: true });
|
|
17847
|
+
await writeFile11(
|
|
17023
17848
|
orphansTxtPath,
|
|
17024
17849
|
orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
|
|
17025
17850
|
"utf8"
|
|
@@ -17044,10 +17869,26 @@ var init_github_remote = __esm({
|
|
|
17044
17869
|
}
|
|
17045
17870
|
});
|
|
17046
17871
|
|
|
17872
|
+
// src/scan/claimDefaultProject.ts
|
|
17873
|
+
function isClaimableDefaultProject(c3) {
|
|
17874
|
+
if (c3.slug !== SIGNUP_DEFAULT_SLUG) return false;
|
|
17875
|
+
if (c3.name.trim().toLowerCase() !== SIGNUP_DEFAULT_NAME) return false;
|
|
17876
|
+
if (c3.fileCount === null || c3.commitCount === null) return false;
|
|
17877
|
+
return c3.fileCount === 0 && c3.commitCount === 0;
|
|
17878
|
+
}
|
|
17879
|
+
var SIGNUP_DEFAULT_SLUG, SIGNUP_DEFAULT_NAME;
|
|
17880
|
+
var init_claimDefaultProject = __esm({
|
|
17881
|
+
"src/scan/claimDefaultProject.ts"() {
|
|
17882
|
+
"use strict";
|
|
17883
|
+
SIGNUP_DEFAULT_SLUG = "default";
|
|
17884
|
+
SIGNUP_DEFAULT_NAME = "default";
|
|
17885
|
+
}
|
|
17886
|
+
});
|
|
17887
|
+
|
|
17047
17888
|
// src/scan/run-scan.ts
|
|
17048
17889
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
17049
|
-
import { mkdir as
|
|
17050
|
-
import { basename as basename2, join as
|
|
17890
|
+
import { mkdir as mkdir12, readFile as readFile12 } from "node:fs/promises";
|
|
17891
|
+
import { basename as basename2, join as join19 } from "node:path";
|
|
17051
17892
|
function buildBootstrapPrompt(opts = {}) {
|
|
17052
17893
|
const { promptVariant, interactive, briefing } = opts;
|
|
17053
17894
|
let base = interactive ? briefing ? `${BOOTSTRAP_INTERACTIVE}
|
|
@@ -17132,6 +17973,8 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
|
|
|
17132
17973
|
if (!membership) {
|
|
17133
17974
|
throw new Error(`you are not a member of org \`${orgSlug}\`; cannot create project there`);
|
|
17134
17975
|
}
|
|
17976
|
+
const claimed = await claimSignupDefaultProject(supabase, org, projectSlug);
|
|
17977
|
+
if (claimed) return claimed;
|
|
17135
17978
|
const { data: created, error: cErr } = await supabase.from("projects").insert({ name: projectSlug, org_id: org.id, slug: projectSlug }).select("id").single();
|
|
17136
17979
|
if (cErr) {
|
|
17137
17980
|
if (cErr.code === "23505" || /duplicate key/i.test(cErr.message ?? "")) {
|
|
@@ -17164,6 +18007,43 @@ async function resolveOrCreateProject(supabase, userId, fullSlug) {
|
|
|
17164
18007
|
created: true
|
|
17165
18008
|
};
|
|
17166
18009
|
}
|
|
18010
|
+
async function claimSignupDefaultProject(supabase, org, projectSlug) {
|
|
18011
|
+
if (projectSlug === SIGNUP_DEFAULT_SLUG) return null;
|
|
18012
|
+
try {
|
|
18013
|
+
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();
|
|
18014
|
+
if (!candidate) return null;
|
|
18015
|
+
const row = candidate;
|
|
18016
|
+
const [{ count: fileCount }, { count: commitCount }] = await Promise.all([
|
|
18017
|
+
supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", row.id),
|
|
18018
|
+
supabase.from("commits").select("id", { count: "exact", head: true }).eq("project_id", row.id)
|
|
18019
|
+
]);
|
|
18020
|
+
if (!isClaimableDefaultProject({
|
|
18021
|
+
slug: row.slug,
|
|
18022
|
+
name: row.name,
|
|
18023
|
+
fileCount: fileCount ?? null,
|
|
18024
|
+
commitCount: commitCount ?? null
|
|
18025
|
+
})) {
|
|
18026
|
+
return null;
|
|
18027
|
+
}
|
|
18028
|
+
const { data: branches } = await supabase.from("branches").select("name, is_default").eq("project_id", row.id).is("deleted_at", null);
|
|
18029
|
+
const existing = branches ?? [];
|
|
18030
|
+
if (existing.length === 0) {
|
|
18031
|
+
const { error: bErr } = await supabase.from("branches").insert({ project_id: row.id, name: "main", is_default: true });
|
|
18032
|
+
if (bErr) return null;
|
|
18033
|
+
}
|
|
18034
|
+
const { error: renameErr } = await supabase.from("projects").update({ name: projectSlug, slug: projectSlug }).eq("id", row.id);
|
|
18035
|
+
if (renameErr) return null;
|
|
18036
|
+
return {
|
|
18037
|
+
orgSlug: org.slug,
|
|
18038
|
+
projectSlug,
|
|
18039
|
+
projectId: row.id,
|
|
18040
|
+
defaultBranch: existing.find((b) => b.is_default)?.name ?? "main",
|
|
18041
|
+
created: true
|
|
18042
|
+
};
|
|
18043
|
+
} catch {
|
|
18044
|
+
return null;
|
|
18045
|
+
}
|
|
18046
|
+
}
|
|
17167
18047
|
async function fetchExistingProject(supabase, org, projectSlug) {
|
|
17168
18048
|
const project = await mustQuery(
|
|
17169
18049
|
supabase.from("projects").select("id, slug").eq("org_id", org.id).eq("slug", projectSlug).is("deleted_at", null).maybeSingle(),
|
|
@@ -17269,8 +18149,8 @@ async function runScan(opts, deps = {}) {
|
|
|
17269
18149
|
let headSha;
|
|
17270
18150
|
try {
|
|
17271
18151
|
headSha = await repoHeadSha(opts.repoPath);
|
|
17272
|
-
} catch (
|
|
17273
|
-
const msg =
|
|
18152
|
+
} catch (err2) {
|
|
18153
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
17274
18154
|
throw new Error(`could not read git HEAD at \`${opts.repoPath}\` (is this a git repo?): ${msg}`);
|
|
17275
18155
|
}
|
|
17276
18156
|
let fullSlug;
|
|
@@ -17287,19 +18167,19 @@ async function runScan(opts, deps = {}) {
|
|
|
17287
18167
|
}
|
|
17288
18168
|
fullSlug = defaultProjectSlug(orgSlug, opts.repoPath);
|
|
17289
18169
|
}
|
|
17290
|
-
const
|
|
17291
|
-
if (
|
|
17292
|
-
log(` project: ${
|
|
18170
|
+
const resolved2 = await resolveOrCreateProject(supabase, userId, fullSlug);
|
|
18171
|
+
if (resolved2.created) {
|
|
18172
|
+
log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (created)`);
|
|
17293
18173
|
} else {
|
|
17294
|
-
log(` project: ${
|
|
18174
|
+
log(` project: ${resolved2.orgSlug}/${resolved2.projectSlug} (using existing \u2014 pass --project <unique-slug> to start fresh)`);
|
|
17295
18175
|
}
|
|
17296
|
-
log(` default branch: ${
|
|
18176
|
+
log(` default branch: ${resolved2.defaultBranch}`);
|
|
17297
18177
|
const branchName = opts.branch ? normalizeScanBranchName(opts.branch) : defaultBranchName(headSha);
|
|
17298
18178
|
log(` scan branch: ${branchName}`);
|
|
17299
18179
|
const bearer = await getMcpAccessToken();
|
|
17300
18180
|
const { configPath, cleanup: cleanupMcp } = await writeMcpConfig({
|
|
17301
18181
|
baseUrl: mcpUrl,
|
|
17302
|
-
projectSlug: `${
|
|
18182
|
+
projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
|
|
17303
18183
|
branch: branchName,
|
|
17304
18184
|
bearer,
|
|
17305
18185
|
// No source-SHA block (no job id for a local run), but DO tag these as scan
|
|
@@ -17334,9 +18214,9 @@ async function runScan(opts, deps = {}) {
|
|
|
17334
18214
|
// Incompatible with an interactive TTY session.
|
|
17335
18215
|
streamJson: !interactive
|
|
17336
18216
|
});
|
|
17337
|
-
const transcriptDir =
|
|
17338
|
-
const transcriptPath =
|
|
17339
|
-
await
|
|
18217
|
+
const transcriptDir = join19(opts.repoPath, ".archme", "scan");
|
|
18218
|
+
const transcriptPath = join19(transcriptDir, "transcript.log");
|
|
18219
|
+
await mkdir12(transcriptDir, { recursive: true });
|
|
17340
18220
|
const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
|
|
17341
18221
|
let finalResultText = "";
|
|
17342
18222
|
if (proc.stdout) {
|
|
@@ -17370,19 +18250,19 @@ async function runScan(opts, deps = {}) {
|
|
|
17370
18250
|
} else {
|
|
17371
18251
|
log(`claude exited ${exitCode}`);
|
|
17372
18252
|
}
|
|
17373
|
-
const counts = await countArchitecture(supabase,
|
|
18253
|
+
const counts = await countArchitecture(supabase, resolved2.projectId, branchName);
|
|
17374
18254
|
log(` components: ${counts.components}`);
|
|
17375
18255
|
log(` stories: ${counts.stories}`);
|
|
17376
18256
|
let signals2 = null;
|
|
17377
18257
|
try {
|
|
17378
|
-
await new Promise((
|
|
17379
|
-
transcriptStream.end((
|
|
18258
|
+
await new Promise((resolve5, reject) => {
|
|
18259
|
+
transcriptStream.end((err2) => err2 ? reject(err2) : resolve5());
|
|
17380
18260
|
});
|
|
17381
|
-
const reportPath =
|
|
17382
|
-
const orphansTxtPath =
|
|
17383
|
-
const transcript = await
|
|
18261
|
+
const reportPath = join19(transcriptDir, "report.json");
|
|
18262
|
+
const orphansTxtPath = join19(transcriptDir, "orphans.txt");
|
|
18263
|
+
const transcript = await readFile12(transcriptPath, "utf8").catch(() => "");
|
|
17384
18264
|
signals2 = extractTranscriptSignals(transcript);
|
|
17385
|
-
const architecture = await fetchArchitectureDetail(supabase,
|
|
18265
|
+
const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName);
|
|
17386
18266
|
const orphans = await classifyOrphans({
|
|
17387
18267
|
repoRoot: opts.repoPath,
|
|
17388
18268
|
fileAnchors: architecture.fileAnchors,
|
|
@@ -17391,7 +18271,7 @@ async function runScan(opts, deps = {}) {
|
|
|
17391
18271
|
excludeGlobs: DEFAULT_EXCLUDE_GLOBS
|
|
17392
18272
|
});
|
|
17393
18273
|
const report = await buildReport({
|
|
17394
|
-
projectSlug: `${
|
|
18274
|
+
projectSlug: `${resolved2.orgSlug}/${resolved2.projectSlug}`,
|
|
17395
18275
|
branch: branchName,
|
|
17396
18276
|
transcriptPath,
|
|
17397
18277
|
reportPath,
|
|
@@ -17407,8 +18287,8 @@ async function runScan(opts, deps = {}) {
|
|
|
17407
18287
|
log(` coverage: ${report.repo.anchor_coverage_ratio} orphans: ${orphans.orphan_files_full.length}`);
|
|
17408
18288
|
log(` report: ${reportPath}`);
|
|
17409
18289
|
log(` transcript: ${transcriptPath}`);
|
|
17410
|
-
} catch (
|
|
17411
|
-
const msg =
|
|
18290
|
+
} catch (err2) {
|
|
18291
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
17412
18292
|
log(`warning: failed to build scan report: ${msg}`);
|
|
17413
18293
|
}
|
|
17414
18294
|
if (signals2 !== null && shouldStampMarker(signals2, exitCode, signal)) {
|
|
@@ -17418,15 +18298,15 @@ async function runScan(opts, deps = {}) {
|
|
|
17418
18298
|
repo = stdout ? parseGitHubRepo(stdout) : null;
|
|
17419
18299
|
} catch {
|
|
17420
18300
|
}
|
|
17421
|
-
const { error: markerErr } = await supabase.from("branches").update({ last_source_sha: headSha, last_source_repo: repo }).eq("project_id",
|
|
18301
|
+
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
18302
|
if (markerErr) log(` marker advance failed (next daemon job has no usable base): ${markerErr.message}`);
|
|
17423
18303
|
else log(` marker advanced: ${branchName} \u2192 ${headSha.slice(0, 8)}${repo ? ` (${repo})` : ""}`);
|
|
17424
18304
|
}
|
|
17425
|
-
const diffBase =
|
|
18305
|
+
const diffBase = resolved2.defaultBranch;
|
|
17426
18306
|
const webUrl = buildWebUrl({
|
|
17427
18307
|
apiUrl: mcpUrl,
|
|
17428
|
-
orgSlug:
|
|
17429
|
-
projectSlug:
|
|
18308
|
+
orgSlug: resolved2.orgSlug,
|
|
18309
|
+
projectSlug: resolved2.projectSlug,
|
|
17430
18310
|
branchName,
|
|
17431
18311
|
defaultBranch: diffBase,
|
|
17432
18312
|
// CLI: throw on malformed mcpUrl so misconfiguration is loud.
|
|
@@ -17439,8 +18319,8 @@ async function runScan(opts, deps = {}) {
|
|
|
17439
18319
|
components: counts.components,
|
|
17440
18320
|
stories: counts.stories,
|
|
17441
18321
|
branchName,
|
|
17442
|
-
orgSlug:
|
|
17443
|
-
projectSlug:
|
|
18322
|
+
orgSlug: resolved2.orgSlug,
|
|
18323
|
+
projectSlug: resolved2.projectSlug,
|
|
17444
18324
|
webUrl,
|
|
17445
18325
|
summary
|
|
17446
18326
|
};
|
|
@@ -17466,6 +18346,7 @@ var init_run_scan = __esm({
|
|
|
17466
18346
|
init_transcript_signals();
|
|
17467
18347
|
init_report();
|
|
17468
18348
|
init_github_remote();
|
|
18349
|
+
init_claimDefaultProject();
|
|
17469
18350
|
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
18351
|
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
18352
|
|
|
@@ -17475,6 +18356,62 @@ Before you call \`start_scan\`, show the user this briefing verbatim and ask the
|
|
|
17475
18356
|
}
|
|
17476
18357
|
});
|
|
17477
18358
|
|
|
18359
|
+
// src/scan/scanOutcome.ts
|
|
18360
|
+
function scanSucceeded(r) {
|
|
18361
|
+
return r.exitCode === 0 && !r.signal && r.components > 0;
|
|
18362
|
+
}
|
|
18363
|
+
function nextSteps() {
|
|
18364
|
+
return [
|
|
18365
|
+
"",
|
|
18366
|
+
"This is a snapshot of your architecture as of now. It will not follow your",
|
|
18367
|
+
"code unless something keeps it up to date:",
|
|
18368
|
+
"",
|
|
18369
|
+
" archme init walks you through the options",
|
|
18370
|
+
"",
|
|
18371
|
+
" \xB7 GitHub App \u2014 a couple of minutes, PR-aware, works for your whole team.",
|
|
18372
|
+
" Needs someone who can grant the app read access to the repo.",
|
|
18373
|
+
" \xB7 archme local-watch \u2014 no GitHub App and no approval, but updates only",
|
|
18374
|
+
" happen while your daemon is running on a machine that stays on."
|
|
18375
|
+
];
|
|
18376
|
+
}
|
|
18377
|
+
function formatScanOutcome(r) {
|
|
18378
|
+
const lines = [""];
|
|
18379
|
+
if (!scanSucceeded(r)) {
|
|
18380
|
+
if (r.signal || r.exitCode !== 0) {
|
|
18381
|
+
lines.push("scan did not complete");
|
|
18382
|
+
lines.push(r.signal ? ` killed by ${r.signal}` : ` exit ${r.exitCode}`);
|
|
18383
|
+
} else {
|
|
18384
|
+
lines.push("scan finished but wrote no components");
|
|
18385
|
+
}
|
|
18386
|
+
lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
|
|
18387
|
+
lines.push(` components: ${r.components}`);
|
|
18388
|
+
lines.push(` stories: ${r.stories}`);
|
|
18389
|
+
lines.push("");
|
|
18390
|
+
lines.push("If Claude Code is installed but not signed in, run `claude` once and");
|
|
18391
|
+
lines.push("authenticate, then re-run this command.");
|
|
18392
|
+
} else {
|
|
18393
|
+
lines.push("scan complete");
|
|
18394
|
+
lines.push(` components: ${r.components}`);
|
|
18395
|
+
lines.push(` stories: ${r.stories}`);
|
|
18396
|
+
lines.push(` branch: ${r.orgSlug}/${r.projectSlug}@${r.branchName}`);
|
|
18397
|
+
lines.push(` web UI: ${r.webUrl}`);
|
|
18398
|
+
}
|
|
18399
|
+
if (r.summary) {
|
|
18400
|
+
const tail = r.summary.length > SUMMARY_TAIL_CHARS ? `\u2026${r.summary.slice(-SUMMARY_TAIL_CHARS)}` : r.summary;
|
|
18401
|
+
lines.push("", `Claude summary:
|
|
18402
|
+
${tail}`);
|
|
18403
|
+
}
|
|
18404
|
+
if (scanSucceeded(r)) lines.push(...nextSteps());
|
|
18405
|
+
return lines.join("\n");
|
|
18406
|
+
}
|
|
18407
|
+
var SUMMARY_TAIL_CHARS;
|
|
18408
|
+
var init_scanOutcome = __esm({
|
|
18409
|
+
"src/scan/scanOutcome.ts"() {
|
|
18410
|
+
"use strict";
|
|
18411
|
+
SUMMARY_TAIL_CHARS = 2e3;
|
|
18412
|
+
}
|
|
18413
|
+
});
|
|
18414
|
+
|
|
17478
18415
|
// src/scan/dispatch.ts
|
|
17479
18416
|
var dispatch_exports3 = {};
|
|
17480
18417
|
__export(dispatch_exports3, {
|
|
@@ -17546,22 +18483,10 @@ async function dispatchScan(args) {
|
|
|
17546
18483
|
opts.repoPath = resolve2(repoPath);
|
|
17547
18484
|
try {
|
|
17548
18485
|
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
|
-
}
|
|
18486
|
+
console.log(formatScanOutcome(result));
|
|
17562
18487
|
return dispatchExitCode(result);
|
|
17563
|
-
} catch (
|
|
17564
|
-
const msg =
|
|
18488
|
+
} catch (err2) {
|
|
18489
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
17565
18490
|
console.error(`archme scan: ${msg}`);
|
|
17566
18491
|
return EX_INFRA;
|
|
17567
18492
|
}
|
|
@@ -17573,7 +18498,7 @@ Usage:
|
|
|
17573
18498
|
archme scan <repo-path> [flags]
|
|
17574
18499
|
|
|
17575
18500
|
Flags:
|
|
17576
|
-
--project <org/slug> Target project. Defaults to
|
|
18501
|
+
--project <org/slug> Target project. Defaults to <your-org>/<basename>.
|
|
17577
18502
|
--branch <name> Target scan branch. Defaults to <date>-<sha7>-scan.
|
|
17578
18503
|
--no-interactive Run headless (no questions). For CI/automation.
|
|
17579
18504
|
--prompt-variant <id> Scan prompt variant. Defaults to latest.
|
|
@@ -17582,23 +18507,23 @@ Flags:
|
|
|
17582
18507
|
Reads OAuth credentials from the local keychain (run \`archme login\` first).
|
|
17583
18508
|
Outputs component / story counts and the web-UI URL on completion.`);
|
|
17584
18509
|
}
|
|
17585
|
-
var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE,
|
|
18510
|
+
var EX_OK, EX_SIGNAL, EX_INFRA, EX_USAGE, PROJECT_SLUG_RE;
|
|
17586
18511
|
var init_dispatch3 = __esm({
|
|
17587
18512
|
"src/scan/dispatch.ts"() {
|
|
17588
18513
|
"use strict";
|
|
17589
18514
|
init_run_scan();
|
|
18515
|
+
init_scanOutcome();
|
|
17590
18516
|
EX_OK = 0;
|
|
17591
18517
|
EX_SIGNAL = 2;
|
|
17592
18518
|
EX_INFRA = 3;
|
|
17593
18519
|
EX_USAGE = 78;
|
|
17594
|
-
SUMMARY_TAIL_CHARS = 2e3;
|
|
17595
18520
|
PROJECT_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
17596
18521
|
}
|
|
17597
18522
|
});
|
|
17598
18523
|
|
|
17599
18524
|
// src/init/repoSize.ts
|
|
17600
18525
|
import { stat as stat9 } from "node:fs/promises";
|
|
17601
|
-
import { extname, join as
|
|
18526
|
+
import { extname, join as join20 } from "node:path";
|
|
17602
18527
|
async function measureRepo(repoPath) {
|
|
17603
18528
|
const files = await gitListFiles(repoPath);
|
|
17604
18529
|
if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
|
|
@@ -17607,7 +18532,7 @@ async function measureRepo(repoPath) {
|
|
|
17607
18532
|
for (const rel of files) {
|
|
17608
18533
|
if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
|
|
17609
18534
|
try {
|
|
17610
|
-
const s = await stat9(
|
|
18535
|
+
const s = await stat9(join20(repoPath, rel));
|
|
17611
18536
|
if (!s.isFile()) continue;
|
|
17612
18537
|
sourceFiles++;
|
|
17613
18538
|
sourceBytes += s.size;
|
|
@@ -17726,7 +18651,7 @@ async function watchRepoForProject(input) {
|
|
|
17726
18651
|
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
18652
|
};
|
|
17728
18653
|
}
|
|
17729
|
-
const { error } = await supabase.rpc("watch_repo", {
|
|
18654
|
+
const { data: watchedRepoId, error } = await supabase.rpc("watch_repo", {
|
|
17730
18655
|
p_org_id: orgId,
|
|
17731
18656
|
p_project_id: projectId,
|
|
17732
18657
|
p_repo_owner: owner,
|
|
@@ -17737,6 +18662,17 @@ async function watchRepoForProject(input) {
|
|
|
17737
18662
|
if (error) {
|
|
17738
18663
|
return { watched: false, reason: `could not watch the repo (${error.message}).` };
|
|
17739
18664
|
}
|
|
18665
|
+
const { error: enableErr } = await supabase.from("watched_repos").update({
|
|
18666
|
+
commit_update_enabled: true,
|
|
18667
|
+
commit_update_archme_branch: input.archmeBranch,
|
|
18668
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
18669
|
+
}).eq("id", watchedRepoId);
|
|
18670
|
+
if (enableErr) {
|
|
18671
|
+
return {
|
|
18672
|
+
watched: false,
|
|
18673
|
+
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.`
|
|
18674
|
+
};
|
|
18675
|
+
}
|
|
17740
18676
|
return { watched: true };
|
|
17741
18677
|
}
|
|
17742
18678
|
var init_watchRepo = __esm({
|
|
@@ -17746,21 +18682,21 @@ var init_watchRepo = __esm({
|
|
|
17746
18682
|
});
|
|
17747
18683
|
|
|
17748
18684
|
// src/init/gitContext.ts
|
|
17749
|
-
async function
|
|
18685
|
+
async function git2(cwd, args) {
|
|
17750
18686
|
const { stdout } = await execa("git", args, { cwd });
|
|
17751
18687
|
return stdout.trim();
|
|
17752
18688
|
}
|
|
17753
18689
|
async function resolveGitContext(repoPath) {
|
|
17754
18690
|
let root;
|
|
17755
18691
|
try {
|
|
17756
|
-
root = await
|
|
18692
|
+
root = await git2(repoPath, ["rev-parse", "--show-toplevel"]);
|
|
17757
18693
|
} catch {
|
|
17758
18694
|
throw new Error(
|
|
17759
18695
|
`\`${repoPath}\` is not inside a git repository \u2014 run \`archme init\` from a repo, or pass its path`
|
|
17760
18696
|
);
|
|
17761
18697
|
}
|
|
17762
18698
|
try {
|
|
17763
|
-
await
|
|
18699
|
+
await git2(root, ["rev-parse", "HEAD"]);
|
|
17764
18700
|
} catch {
|
|
17765
18701
|
throw new Error(
|
|
17766
18702
|
`\`${root}\` is a git repository with no commits yet \u2014 make a commit first, then run \`archme init\``
|
|
@@ -17768,26 +18704,29 @@ async function resolveGitContext(repoPath) {
|
|
|
17768
18704
|
}
|
|
17769
18705
|
let branch = "";
|
|
17770
18706
|
try {
|
|
17771
|
-
branch = await
|
|
18707
|
+
branch = await git2(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
17772
18708
|
} catch {
|
|
17773
18709
|
branch = "";
|
|
17774
18710
|
}
|
|
17775
18711
|
if (!branch || branch === "HEAD") {
|
|
17776
18712
|
try {
|
|
17777
|
-
const ref = await
|
|
18713
|
+
const ref = await git2(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
17778
18714
|
branch = ref.replace(/^refs\/remotes\/origin\//, "");
|
|
17779
18715
|
} catch {
|
|
17780
18716
|
branch = "main";
|
|
17781
18717
|
}
|
|
17782
18718
|
}
|
|
17783
18719
|
let repoSlug = null;
|
|
18720
|
+
let originUrl = null;
|
|
17784
18721
|
try {
|
|
17785
|
-
const url = await
|
|
18722
|
+
const url = await git2(root, ["remote", "get-url", "origin"]);
|
|
18723
|
+
originUrl = url || null;
|
|
17786
18724
|
repoSlug = url ? parseGitHubRepo(url) : null;
|
|
17787
18725
|
} catch {
|
|
17788
18726
|
repoSlug = null;
|
|
18727
|
+
originUrl = null;
|
|
17789
18728
|
}
|
|
17790
|
-
return { root, branch, repoSlug };
|
|
18729
|
+
return { root, branch, repoSlug, originUrl };
|
|
17791
18730
|
}
|
|
17792
18731
|
var init_gitContext = __esm({
|
|
17793
18732
|
"src/init/gitContext.ts"() {
|
|
@@ -17797,6 +18736,661 @@ var init_gitContext = __esm({
|
|
|
17797
18736
|
}
|
|
17798
18737
|
});
|
|
17799
18738
|
|
|
18739
|
+
// src/init/keepCurrent.ts
|
|
18740
|
+
async function chooseKeepCurrent(deps) {
|
|
18741
|
+
if (deps.yes) {
|
|
18742
|
+
return { detection: deps.alreadyWatched ? "github-app" : "local-watch", runner: "self" };
|
|
18743
|
+
}
|
|
18744
|
+
const detection = deps.alreadyWatched ? "github-app" : await deps.choose(
|
|
18745
|
+
"How should ArchMe detect new commits?",
|
|
18746
|
+
DETECTION_OPTIONS
|
|
18747
|
+
);
|
|
18748
|
+
const runner = await deps.choose(
|
|
18749
|
+
"Something has to process each update. Who runs it?",
|
|
18750
|
+
RUNNER_OPTIONS
|
|
18751
|
+
);
|
|
18752
|
+
return { detection, runner };
|
|
18753
|
+
}
|
|
18754
|
+
function formatKeepCurrentPlan(plan, ctx) {
|
|
18755
|
+
const steps = [];
|
|
18756
|
+
if (plan.detection === "local-watch" && !ctx.localWatchRegistered) {
|
|
18757
|
+
steps.push({
|
|
18758
|
+
title: "Register this repo so your machine watches it:",
|
|
18759
|
+
command: "archme local-watch"
|
|
18760
|
+
});
|
|
18761
|
+
}
|
|
18762
|
+
if (plan.runner === "self") {
|
|
18763
|
+
steps.push({
|
|
18764
|
+
title: "Start the daemon \u2014 nothing processes the queue until you do:",
|
|
18765
|
+
command: "archme daemon start",
|
|
18766
|
+
notes: [
|
|
18767
|
+
"Keep it running on a machine that stays on. Each update spends tokens",
|
|
18768
|
+
"from your Claude subscription, the same way this scan did. Your code is",
|
|
18769
|
+
"not sent to our servers \u2014 only the resulting architecture."
|
|
18770
|
+
]
|
|
18771
|
+
});
|
|
18772
|
+
} else {
|
|
18773
|
+
steps.push({
|
|
18774
|
+
title: `Email ${CONTACT_EMAIL} and we'll set up the daemon for you.`,
|
|
18775
|
+
notes: [
|
|
18776
|
+
ctx.repoSlug ? `Mention ${ctx.repoSlug} so we know which repo you mean.` : "",
|
|
18777
|
+
// Deliberately not "we'll be in touch": running this command notifies
|
|
18778
|
+
// nobody. Promising contact that depends on the user sending the mail
|
|
18779
|
+
// is how someone ends up waiting on a team that never heard.
|
|
18780
|
+
"Until then this architecture stays as it is \u2014 it won't track new commits."
|
|
18781
|
+
].filter(Boolean)
|
|
18782
|
+
});
|
|
18783
|
+
}
|
|
18784
|
+
const lines = ["", "Keeping it current:"];
|
|
18785
|
+
if (ctx.localWatchRegistered) {
|
|
18786
|
+
lines.push("", ` \u2713 ${ctx.repoSlug ?? "This repo"} is registered \u2014 your machine is watching it.`);
|
|
18787
|
+
}
|
|
18788
|
+
const numbered = steps.length > 1;
|
|
18789
|
+
steps.forEach((step, i2) => {
|
|
18790
|
+
const bullet = numbered ? ` ${i2 + 1}. ` : " ";
|
|
18791
|
+
const indent = " ".repeat(bullet.length);
|
|
18792
|
+
lines.push("", `${bullet}${step.title}`);
|
|
18793
|
+
if (step.command) lines.push("", `${indent} ${step.command}`);
|
|
18794
|
+
if (step.notes?.length) {
|
|
18795
|
+
lines.push("");
|
|
18796
|
+
for (const note of step.notes) lines.push(`${indent}${note}`);
|
|
18797
|
+
}
|
|
18798
|
+
});
|
|
18799
|
+
if (ctx.boardUrl) lines.push("", ` Your board: ${ctx.boardUrl}`);
|
|
18800
|
+
return lines.join("\n");
|
|
18801
|
+
}
|
|
18802
|
+
var CONTACT_EMAIL, DETECTION_OPTIONS, RUNNER_OPTIONS;
|
|
18803
|
+
var init_keepCurrent = __esm({
|
|
18804
|
+
"src/init/keepCurrent.ts"() {
|
|
18805
|
+
"use strict";
|
|
18806
|
+
CONTACT_EMAIL = "signup@archme.io";
|
|
18807
|
+
DETECTION_OPTIONS = [
|
|
18808
|
+
{
|
|
18809
|
+
// First, and named as recommended: it is less work per-commit, survives
|
|
18810
|
+
// this laptop being closed, and is the only path that sees pull requests.
|
|
18811
|
+
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.",
|
|
18812
|
+
value: "github-app"
|
|
18813
|
+
},
|
|
18814
|
+
{
|
|
18815
|
+
label: "Local polling \u2014 no GitHub App and nobody's approval needed. This machine watches the repo instead. No pull-request awareness.",
|
|
18816
|
+
value: "local-watch"
|
|
18817
|
+
}
|
|
18818
|
+
];
|
|
18819
|
+
RUNNER_OPTIONS = [
|
|
18820
|
+
{
|
|
18821
|
+
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.",
|
|
18822
|
+
value: "self"
|
|
18823
|
+
},
|
|
18824
|
+
{
|
|
18825
|
+
label: "ArchMe runs it \u2014 if you don't have a machine that's always on, or a Claude subscription to spend on it.",
|
|
18826
|
+
value: "archme"
|
|
18827
|
+
}
|
|
18828
|
+
];
|
|
18829
|
+
}
|
|
18830
|
+
});
|
|
18831
|
+
|
|
18832
|
+
// src/init/keepCurrentFlow.ts
|
|
18833
|
+
async function applyKeepCurrentPlan(plan, deps, alreadyWatched) {
|
|
18834
|
+
const base = { detection: plan.detection, runner: plan.runner };
|
|
18835
|
+
if (plan.detection === "github-app") {
|
|
18836
|
+
if (alreadyWatched) {
|
|
18837
|
+
return { ...base, wired: true, localWatchRegistered: false };
|
|
18838
|
+
}
|
|
18839
|
+
const url = await deps.startGitHubAppInstall();
|
|
18840
|
+
deps.log("");
|
|
18841
|
+
deps.log(" Install the ArchMe GitHub App here, then come back \u2014 I'll wait:");
|
|
18842
|
+
deps.log("");
|
|
18843
|
+
deps.log(` ${url}`);
|
|
18844
|
+
deps.log("");
|
|
18845
|
+
const installed = await deps.waitForInstallation();
|
|
18846
|
+
if (!installed) {
|
|
18847
|
+
return {
|
|
18848
|
+
...base,
|
|
18849
|
+
wired: false,
|
|
18850
|
+
localWatchRegistered: false,
|
|
18851
|
+
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."
|
|
18852
|
+
};
|
|
18853
|
+
}
|
|
18854
|
+
const watched = await deps.watchRepo();
|
|
18855
|
+
if (!watched.watched) {
|
|
18856
|
+
return {
|
|
18857
|
+
...base,
|
|
18858
|
+
wired: false,
|
|
18859
|
+
localWatchRegistered: false,
|
|
18860
|
+
reason: watched.reason ?? "the repo could not be linked to this project."
|
|
18861
|
+
};
|
|
18862
|
+
}
|
|
18863
|
+
deps.log(" \u2713 GitHub App installed and the repo is linked.");
|
|
18864
|
+
return { ...base, wired: true, localWatchRegistered: false };
|
|
18865
|
+
}
|
|
18866
|
+
const registered = await deps.registerLocalWatch();
|
|
18867
|
+
if (!registered.ok) {
|
|
18868
|
+
return {
|
|
18869
|
+
...base,
|
|
18870
|
+
wired: false,
|
|
18871
|
+
localWatchRegistered: false,
|
|
18872
|
+
reason: registered.reason ?? "the repo could not be registered for local polling."
|
|
18873
|
+
};
|
|
18874
|
+
}
|
|
18875
|
+
return { ...base, wired: true, localWatchRegistered: true };
|
|
18876
|
+
}
|
|
18877
|
+
var init_keepCurrentFlow = __esm({
|
|
18878
|
+
"src/init/keepCurrentFlow.ts"() {
|
|
18879
|
+
"use strict";
|
|
18880
|
+
}
|
|
18881
|
+
});
|
|
18882
|
+
|
|
18883
|
+
// src/init/githubAppSetup.ts
|
|
18884
|
+
async function waitForInstallation(deps) {
|
|
18885
|
+
const deadline = deps.now() + INSTALL_WAIT_TIMEOUT_MS;
|
|
18886
|
+
let announced = false;
|
|
18887
|
+
for (; ; ) {
|
|
18888
|
+
try {
|
|
18889
|
+
if (await deps.hasInstallation()) return true;
|
|
18890
|
+
} catch {
|
|
18891
|
+
}
|
|
18892
|
+
if (deps.now() >= deadline) return false;
|
|
18893
|
+
if (!announced) {
|
|
18894
|
+
deps.log(" waiting for the installation to come through\u2026");
|
|
18895
|
+
announced = true;
|
|
18896
|
+
}
|
|
18897
|
+
await deps.sleep(INSTALL_POLL_INTERVAL_MS);
|
|
18898
|
+
}
|
|
18899
|
+
}
|
|
18900
|
+
async function buildInstallUrl(opts) {
|
|
18901
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
18902
|
+
const res = await doFetch(`${opts.supabaseUrl}/functions/v1/github/install-state`, {
|
|
18903
|
+
method: "POST",
|
|
18904
|
+
headers: {
|
|
18905
|
+
"Content-Type": "application/json",
|
|
18906
|
+
Authorization: `Bearer ${opts.accessToken}`
|
|
18907
|
+
},
|
|
18908
|
+
body: JSON.stringify({ orgId: opts.orgId, env: opts.env ?? "prod" })
|
|
18909
|
+
});
|
|
18910
|
+
if (!res.ok) {
|
|
18911
|
+
const body = await res.json().catch(() => ({}));
|
|
18912
|
+
throw new Error(body.error ?? `could not start the GitHub App install (${res.status})`);
|
|
18913
|
+
}
|
|
18914
|
+
const { token, url } = await res.json();
|
|
18915
|
+
if (url) return url;
|
|
18916
|
+
if (!token) throw new Error("install-state returned neither a url nor a token");
|
|
18917
|
+
const slug = process.env.ARCHME_GITHUB_APP_SLUG || "architectme";
|
|
18918
|
+
return `https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(token)}`;
|
|
18919
|
+
}
|
|
18920
|
+
async function orgHasInstallation(supabase, orgId) {
|
|
18921
|
+
const { data, error } = await supabase.from("org_github_installations").select("installation_id").eq("org_id", orgId).limit(1);
|
|
18922
|
+
if (error) throw new Error(error.message);
|
|
18923
|
+
return (data?.length ?? 0) > 0;
|
|
18924
|
+
}
|
|
18925
|
+
var INSTALL_WAIT_TIMEOUT_MS, INSTALL_POLL_INTERVAL_MS;
|
|
18926
|
+
var init_githubAppSetup = __esm({
|
|
18927
|
+
"src/init/githubAppSetup.ts"() {
|
|
18928
|
+
"use strict";
|
|
18929
|
+
INSTALL_WAIT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
18930
|
+
INSTALL_POLL_INTERVAL_MS = 3e3;
|
|
18931
|
+
}
|
|
18932
|
+
});
|
|
18933
|
+
|
|
18934
|
+
// src/daemon/prompt.ts
|
|
18935
|
+
import { createInterface } from "node:readline/promises";
|
|
18936
|
+
async function withReadline(fn) {
|
|
18937
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
18938
|
+
try {
|
|
18939
|
+
return await fn(rl);
|
|
18940
|
+
} finally {
|
|
18941
|
+
rl.close();
|
|
18942
|
+
}
|
|
18943
|
+
}
|
|
18944
|
+
async function promptLine(question) {
|
|
18945
|
+
return await withReadline(async (rl) => {
|
|
18946
|
+
try {
|
|
18947
|
+
return await rl.question(`${question}: `);
|
|
18948
|
+
} catch {
|
|
18949
|
+
return "";
|
|
18950
|
+
}
|
|
18951
|
+
});
|
|
18952
|
+
}
|
|
18953
|
+
async function promptChoice(question, options) {
|
|
18954
|
+
return await withReadline(async (rl) => {
|
|
18955
|
+
console.log(`
|
|
18956
|
+
${question}
|
|
18957
|
+
`);
|
|
18958
|
+
options.forEach((o2, i2) => console.log(` ${i2 + 1}. ${o2.label}
|
|
18959
|
+
`));
|
|
18960
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
18961
|
+
let answer;
|
|
18962
|
+
try {
|
|
18963
|
+
answer = (await rl.question(`Choose 1-${options.length} [1]: `)).trim();
|
|
18964
|
+
} catch {
|
|
18965
|
+
return options[0].value;
|
|
18966
|
+
}
|
|
18967
|
+
if (answer === "") return options[0].value;
|
|
18968
|
+
const n2 = Number.parseInt(answer, 10);
|
|
18969
|
+
if (Number.isInteger(n2) && n2 >= 1 && n2 <= options.length) return options[n2 - 1].value;
|
|
18970
|
+
console.log(` "${answer}" isn't one of the options.`);
|
|
18971
|
+
}
|
|
18972
|
+
return options[0].value;
|
|
18973
|
+
});
|
|
18974
|
+
}
|
|
18975
|
+
async function promptYesNo(question) {
|
|
18976
|
+
return await withReadline(async (rl) => {
|
|
18977
|
+
try {
|
|
18978
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
18979
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
18980
|
+
} catch {
|
|
18981
|
+
return false;
|
|
18982
|
+
}
|
|
18983
|
+
});
|
|
18984
|
+
}
|
|
18985
|
+
var init_prompt = __esm({
|
|
18986
|
+
"src/daemon/prompt.ts"() {
|
|
18987
|
+
"use strict";
|
|
18988
|
+
}
|
|
18989
|
+
});
|
|
18990
|
+
|
|
18991
|
+
// src/daemon/watchReadiness.ts
|
|
18992
|
+
function evaluateWatchReadiness(input) {
|
|
18993
|
+
const { marker, repo, archmeBranch } = input;
|
|
18994
|
+
if (!marker.sha) {
|
|
18995
|
+
return {
|
|
18996
|
+
ok: false,
|
|
18997
|
+
reason: `${repo} has no architecture on \`${archmeBranch}\` yet, so there is nothing to keep up to date and no commit to diff against.
|
|
18998
|
+
Run \`archme init\` in this repo first \u2014 it creates the architecture, then comes back here.`
|
|
18999
|
+
};
|
|
19000
|
+
}
|
|
19001
|
+
if (!GIT_SHA_RE2.test(marker.sha)) {
|
|
19002
|
+
return {
|
|
19003
|
+
ok: false,
|
|
19004
|
+
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.`
|
|
19005
|
+
};
|
|
19006
|
+
}
|
|
19007
|
+
if (!marker.repo) {
|
|
19008
|
+
return {
|
|
19009
|
+
ok: false,
|
|
19010
|
+
reason: `the architecture on \`${archmeBranch}\` has no source repo recorded, so updates cannot be matched to ${repo}.
|
|
19011
|
+
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.`
|
|
19012
|
+
};
|
|
19013
|
+
}
|
|
19014
|
+
if (marker.repo !== repo) {
|
|
19015
|
+
return {
|
|
19016
|
+
ok: false,
|
|
19017
|
+
reason: `\`${archmeBranch}\` is already tracking ${marker.repo}, not ${repo}. Watching it from here would make every update fail on the mismatch.
|
|
19018
|
+
Use \`--project <org>/<slug>\` to pick a different project, or a different branch.`
|
|
19019
|
+
};
|
|
19020
|
+
}
|
|
19021
|
+
return { ok: true };
|
|
19022
|
+
}
|
|
19023
|
+
var GIT_SHA_RE2;
|
|
19024
|
+
var init_watchReadiness = __esm({
|
|
19025
|
+
"src/daemon/watchReadiness.ts"() {
|
|
19026
|
+
"use strict";
|
|
19027
|
+
GIT_SHA_RE2 = /^[0-9a-f]{40}$/;
|
|
19028
|
+
}
|
|
19029
|
+
});
|
|
19030
|
+
|
|
19031
|
+
// src/daemon/localWatchList.ts
|
|
19032
|
+
function formatWatchList(repos) {
|
|
19033
|
+
if (repos.length === 0) {
|
|
19034
|
+
return [
|
|
19035
|
+
"Not watching any repos on this machine.",
|
|
19036
|
+
"",
|
|
19037
|
+
" archme local-watch register the repo you're in"
|
|
19038
|
+
].join("\n");
|
|
19039
|
+
}
|
|
19040
|
+
const lines = [
|
|
19041
|
+
`Watching ${repos.length} repo${repos.length === 1 ? "" : "s"} on this machine:`,
|
|
19042
|
+
""
|
|
19043
|
+
];
|
|
19044
|
+
for (const r of repos) {
|
|
19045
|
+
const slug = slugOf(r);
|
|
19046
|
+
lines.push(` ${slug}`);
|
|
19047
|
+
lines.push(` mirror: ${r.mirrorPath}`);
|
|
19048
|
+
if (r.haltedAt) {
|
|
19049
|
+
lines.push(` HALTED at ${r.haltedAt}: ${r.haltReason ?? "unknown reason"}`);
|
|
19050
|
+
lines.push(` no new commits are being picked up. Resume with:`);
|
|
19051
|
+
lines.push(` archme local-watch --resume ${slug}`);
|
|
19052
|
+
}
|
|
19053
|
+
lines.push("");
|
|
19054
|
+
}
|
|
19055
|
+
if (repos.every((r) => !r.haltedAt)) {
|
|
19056
|
+
lines.push("Updates only happen while the daemon is running: archme daemon status");
|
|
19057
|
+
}
|
|
19058
|
+
return lines.join("\n").trimEnd();
|
|
19059
|
+
}
|
|
19060
|
+
var init_localWatchList = __esm({
|
|
19061
|
+
"src/daemon/localWatchList.ts"() {
|
|
19062
|
+
"use strict";
|
|
19063
|
+
init_localRepos();
|
|
19064
|
+
}
|
|
19065
|
+
});
|
|
19066
|
+
|
|
19067
|
+
// src/daemon/localWatchResolve.ts
|
|
19068
|
+
function confirmationFor(target, repoPath) {
|
|
19069
|
+
return `Watch ${target.repo} (branch ${target.repoBranch}) for architecture updates?
|
|
19070
|
+
checkout: ${repoPath}`;
|
|
19071
|
+
}
|
|
19072
|
+
async function evaluate(path7, deps) {
|
|
19073
|
+
const inspected = await deps.inspect(path7);
|
|
19074
|
+
if (!inspected.ok) return { ok: false, reason: inspected.error };
|
|
19075
|
+
const resolved2 = resolveInitTarget({
|
|
19076
|
+
originUrl: inspected.originUrl,
|
|
19077
|
+
branchOverride: deps.branch,
|
|
19078
|
+
detectedDefaultBranch: inspected.detectedDefaultBranch
|
|
19079
|
+
});
|
|
19080
|
+
if (!resolved2.ok) return { ok: false, reason: resolved2.error };
|
|
19081
|
+
return {
|
|
19082
|
+
ok: true,
|
|
19083
|
+
// The ROOT, never the cwd — registering a subdirectory would mirror a
|
|
19084
|
+
// partial tree and record a source path that isn't the repo.
|
|
19085
|
+
repoPath: inspected.root,
|
|
19086
|
+
originUrl: inspected.originUrl,
|
|
19087
|
+
target: resolved2.target
|
|
19088
|
+
};
|
|
19089
|
+
}
|
|
19090
|
+
async function resolveRepoInteractive(deps) {
|
|
19091
|
+
const first = deps.explicitPath ?? deps.cwd;
|
|
19092
|
+
let evaluated = await evaluate(first, deps);
|
|
19093
|
+
if (deps.yes) {
|
|
19094
|
+
if (!evaluated.ok) return { ok: false, message: evaluated.reason };
|
|
19095
|
+
return evaluated;
|
|
19096
|
+
}
|
|
19097
|
+
if (evaluated.ok) {
|
|
19098
|
+
if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
|
|
19099
|
+
}
|
|
19100
|
+
let reason = evaluated.ok ? "Which repo should ArchMe watch? Enter its path" : `${evaluated.reason}
|
|
19101
|
+
Which repo should ArchMe watch? Enter its path`;
|
|
19102
|
+
for (let attempt = 0; attempt < MAX_REPO_PROMPTS; attempt++) {
|
|
19103
|
+
const answer = (await deps.ask(reason)).trim();
|
|
19104
|
+
if (!answer) return { ok: false, message: "cancelled \u2014 nothing was registered" };
|
|
19105
|
+
evaluated = await evaluate(answer, deps);
|
|
19106
|
+
if (evaluated.ok) {
|
|
19107
|
+
if (await deps.confirm(confirmationFor(evaluated.target, evaluated.repoPath))) return evaluated;
|
|
19108
|
+
reason = "Which repo should ArchMe watch? Enter its path";
|
|
19109
|
+
continue;
|
|
19110
|
+
}
|
|
19111
|
+
reason = `${evaluated.reason}
|
|
19112
|
+
Which repo should ArchMe watch? Enter its path`;
|
|
19113
|
+
}
|
|
19114
|
+
return {
|
|
19115
|
+
ok: false,
|
|
19116
|
+
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."
|
|
19117
|
+
};
|
|
19118
|
+
}
|
|
19119
|
+
var MAX_REPO_PROMPTS;
|
|
19120
|
+
var init_localWatchResolve = __esm({
|
|
19121
|
+
"src/daemon/localWatchResolve.ts"() {
|
|
19122
|
+
"use strict";
|
|
19123
|
+
init_localWatch();
|
|
19124
|
+
MAX_REPO_PROMPTS = 3;
|
|
19125
|
+
}
|
|
19126
|
+
});
|
|
19127
|
+
|
|
19128
|
+
// src/daemon/localWatch.ts
|
|
19129
|
+
var localWatch_exports = {};
|
|
19130
|
+
__export(localWatch_exports, {
|
|
19131
|
+
DEFAULT_CLONE_BASE_DIR: () => DEFAULT_CLONE_BASE_DIR2,
|
|
19132
|
+
inspectRepoPath: () => inspectRepoPath,
|
|
19133
|
+
mirrorPathFor: () => mirrorPathFor,
|
|
19134
|
+
registerLocalWatch: () => registerLocalWatch,
|
|
19135
|
+
resolveInitTarget: () => resolveInitTarget,
|
|
19136
|
+
runLocalWatch: () => runLocalWatch,
|
|
19137
|
+
runLocalWatchList: () => runLocalWatchList,
|
|
19138
|
+
runLocalWatchResume: () => runLocalWatchResume,
|
|
19139
|
+
runLocalWatchStop: () => runLocalWatchStop
|
|
19140
|
+
});
|
|
19141
|
+
import { homedir as homedir6 } from "node:os";
|
|
19142
|
+
import { join as join21, resolve as resolve3 } from "node:path";
|
|
19143
|
+
import { stat as stat10 } from "node:fs/promises";
|
|
19144
|
+
function mirrorPathFor(cloneBaseDir, owner, name) {
|
|
19145
|
+
return join21(cloneBaseDir, owner, name);
|
|
19146
|
+
}
|
|
19147
|
+
function resolveInitTarget(input) {
|
|
19148
|
+
if (!input.originUrl) {
|
|
19149
|
+
return {
|
|
19150
|
+
ok: false,
|
|
19151
|
+
error: "no `origin` remote found \u2014 archme local-watch needs a clone with a GitHub origin"
|
|
19152
|
+
};
|
|
19153
|
+
}
|
|
19154
|
+
const repo = parseGitHubRepo(input.originUrl);
|
|
19155
|
+
if (!repo) {
|
|
19156
|
+
return {
|
|
19157
|
+
ok: false,
|
|
19158
|
+
error: `origin ${input.originUrl} is not a github.com remote.
|
|
19159
|
+
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.`
|
|
19160
|
+
};
|
|
19161
|
+
}
|
|
19162
|
+
const repoBranch = input.branchOverride ?? input.detectedDefaultBranch;
|
|
19163
|
+
if (!repoBranch) {
|
|
19164
|
+
return {
|
|
19165
|
+
ok: false,
|
|
19166
|
+
error: "could not determine the default branch \u2014 pass --branch <name>"
|
|
19167
|
+
};
|
|
19168
|
+
}
|
|
19169
|
+
const [repoOwner, repoName] = repo.split("/");
|
|
19170
|
+
return {
|
|
19171
|
+
ok: true,
|
|
19172
|
+
target: {
|
|
19173
|
+
repo,
|
|
19174
|
+
repoOwner,
|
|
19175
|
+
repoName,
|
|
19176
|
+
repoBranch,
|
|
19177
|
+
// Scanned projects live on a `<default>-scan` companion branch; the poller
|
|
19178
|
+
// must enqueue against the SAME branch the scan stamped the marker on.
|
|
19179
|
+
archmeBranch: input.archmeBranchOverride ?? normalizeScanBranchName(repoBranch)
|
|
19180
|
+
}
|
|
19181
|
+
};
|
|
19182
|
+
}
|
|
19183
|
+
async function inspectRepoPath(path7) {
|
|
19184
|
+
const abs = resolve3(path7);
|
|
19185
|
+
const st = await stat10(abs).catch(() => null);
|
|
19186
|
+
if (!st?.isDirectory()) return { ok: false, error: `${abs} is not a directory` };
|
|
19187
|
+
const toplevel = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: abs, reject: false });
|
|
19188
|
+
if (toplevel.exitCode !== 0) return { ok: false, error: `${abs} is not a git repository` };
|
|
19189
|
+
const root = toplevel.stdout.trim() || abs;
|
|
19190
|
+
return {
|
|
19191
|
+
ok: true,
|
|
19192
|
+
root,
|
|
19193
|
+
originUrl: await readOriginUrl(root),
|
|
19194
|
+
detectedDefaultBranch: await detectDefaultBranch(root)
|
|
19195
|
+
};
|
|
19196
|
+
}
|
|
19197
|
+
async function runLocalWatch(opts) {
|
|
19198
|
+
const log = opts.log ?? ((m) => console.log(m));
|
|
19199
|
+
const cloneBaseDir = opts.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
|
|
19200
|
+
const resolved2 = await resolveRepoInteractive({
|
|
19201
|
+
cwd: process.cwd(),
|
|
19202
|
+
explicitPath: opts.repoPath,
|
|
19203
|
+
branch: opts.branch,
|
|
19204
|
+
yes: opts.yes ?? false,
|
|
19205
|
+
inspect: inspectRepoPath,
|
|
19206
|
+
ask: promptLine,
|
|
19207
|
+
confirm: promptYesNo,
|
|
19208
|
+
log
|
|
19209
|
+
});
|
|
19210
|
+
if (!resolved2.ok) {
|
|
19211
|
+
console.error(`archme local-watch: ${resolved2.message}`);
|
|
19212
|
+
return 78;
|
|
19213
|
+
}
|
|
19214
|
+
const { repoPath, originUrl, target } = resolved2;
|
|
19215
|
+
log(` repo: ${target.repo}`);
|
|
19216
|
+
log(` branch: ${target.repoBranch} \u2192 archme branch ${target.archmeBranch}`);
|
|
19217
|
+
const cfg = resolveConfig();
|
|
19218
|
+
let authed;
|
|
19219
|
+
try {
|
|
19220
|
+
authed = await createAuthedClient({
|
|
19221
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
19222
|
+
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
19223
|
+
clientId: cfg.archmeDaemonClientId,
|
|
19224
|
+
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
19225
|
+
});
|
|
19226
|
+
} catch (err2) {
|
|
19227
|
+
console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
19228
|
+
return 3;
|
|
19229
|
+
}
|
|
19230
|
+
const { supabase, userId } = authed;
|
|
19231
|
+
const orgId = authed.orgId;
|
|
19232
|
+
if (!orgId) {
|
|
19233
|
+
console.error("archme local-watch: this login is not paired with an org \u2014 run `archme login`");
|
|
19234
|
+
return 3;
|
|
19235
|
+
}
|
|
19236
|
+
const orgSlug = await resolveCurrentOrgSlug(supabase, orgId);
|
|
19237
|
+
if (!opts.project && !orgSlug) {
|
|
19238
|
+
console.error(
|
|
19239
|
+
"archme local-watch: could not resolve your organization slug, so the project name can't be derived \u2014 pass --project <org/slug>"
|
|
19240
|
+
);
|
|
19241
|
+
return 3;
|
|
19242
|
+
}
|
|
19243
|
+
const fullSlug = opts.project ?? defaultProjectSlug(orgSlug, repoPath);
|
|
19244
|
+
const project = await resolveOrCreateProject(supabase, userId, fullSlug);
|
|
19245
|
+
log(` project: ${project.orgSlug}/${project.projectSlug}${project.created ? " (created)" : ""}`);
|
|
19246
|
+
const registered = await registerLocalWatch({
|
|
19247
|
+
supabase,
|
|
19248
|
+
orgId,
|
|
19249
|
+
projectId: project.projectId,
|
|
19250
|
+
repoPath,
|
|
19251
|
+
originUrl,
|
|
19252
|
+
target,
|
|
19253
|
+
cloneBaseDir,
|
|
19254
|
+
log
|
|
19255
|
+
});
|
|
19256
|
+
if (!registered.ok) {
|
|
19257
|
+
console.error(`archme local-watch: ${registered.reason}`);
|
|
19258
|
+
return registered.exitCode;
|
|
19259
|
+
}
|
|
19260
|
+
log(`
|
|
19261
|
+
\u2713 ${target.repo} is now watched locally.`);
|
|
19262
|
+
log(`
|
|
19263
|
+
Start the daemon if it isn't running: archme daemon start`);
|
|
19264
|
+
log(` New commits on ${target.repoBranch} will appear as ArchMe commits within a minute or two.`);
|
|
19265
|
+
return 0;
|
|
19266
|
+
}
|
|
19267
|
+
async function registerLocalWatch(args) {
|
|
19268
|
+
const log = args.log ?? (() => {
|
|
19269
|
+
});
|
|
19270
|
+
const { target, supabase, orgId, projectId, repoPath, originUrl } = args;
|
|
19271
|
+
const cloneBaseDir = args.cloneBaseDir ?? process.env.CLONE_BASE_DIR ?? DEFAULT_CLONE_BASE_DIR2;
|
|
19272
|
+
const marker = await readBranchMarker(supabase, projectId, target.archmeBranch);
|
|
19273
|
+
const readiness = evaluateWatchReadiness({
|
|
19274
|
+
repo: target.repo,
|
|
19275
|
+
archmeBranch: target.archmeBranch,
|
|
19276
|
+
marker
|
|
19277
|
+
});
|
|
19278
|
+
if (!readiness.ok) return { ok: false, reason: readiness.reason, exitCode: 78 };
|
|
19279
|
+
const { data: watchedRepoId, error: watchErr } = await supabase.rpc("watch_local_repo", {
|
|
19280
|
+
p_org_id: orgId,
|
|
19281
|
+
p_project_id: projectId,
|
|
19282
|
+
p_repo_owner: target.repoOwner,
|
|
19283
|
+
p_repo_name: target.repoName,
|
|
19284
|
+
p_branch: target.repoBranch,
|
|
19285
|
+
// The ArchMe branch has to be RECORDED, not re-derived by each reader.
|
|
19286
|
+
// The poller resolves it from the row (`commit_update_archme_branch ??
|
|
19287
|
+
// branch`, mirroring the webhook); with the column unset it fell back to
|
|
19288
|
+
// the git branch, so registration checked `main-scan` while polling read
|
|
19289
|
+
// `main`, found no marker, and idled forever with everything looking
|
|
19290
|
+
// healthy. Migration 111.
|
|
19291
|
+
p_archme_branch: target.archmeBranch
|
|
19292
|
+
});
|
|
19293
|
+
if (watchErr || typeof watchedRepoId !== "string") {
|
|
19294
|
+
return {
|
|
19295
|
+
ok: false,
|
|
19296
|
+
reason: `could not register the repo: ${watchErr?.message ?? "no id returned"}`,
|
|
19297
|
+
exitCode: 3
|
|
19298
|
+
};
|
|
19299
|
+
}
|
|
19300
|
+
const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
|
|
19301
|
+
const mirrorExists = await stat10(join21(mirrorPath, ".git")).then(() => true).catch(() => false);
|
|
19302
|
+
if (mirrorExists) {
|
|
19303
|
+
log(` mirror: ${mirrorPath} (reusing)`);
|
|
19304
|
+
await setOriginUrl(mirrorPath, originUrl);
|
|
19305
|
+
} else {
|
|
19306
|
+
log(` mirror: ${mirrorPath}`);
|
|
19307
|
+
await createMirrorClone(repoPath, mirrorPath, originUrl);
|
|
19308
|
+
}
|
|
19309
|
+
await execa("git", ["fetch", "--no-tags", "origin", target.repoBranch], {
|
|
19310
|
+
cwd: mirrorPath,
|
|
19311
|
+
reject: true
|
|
19312
|
+
});
|
|
19313
|
+
const entry = {
|
|
19314
|
+
watchedRepoId,
|
|
19315
|
+
mirrorPath,
|
|
19316
|
+
sourcePath: repoPath,
|
|
19317
|
+
repoOwner: target.repoOwner,
|
|
19318
|
+
repoName: target.repoName
|
|
19319
|
+
};
|
|
19320
|
+
await registerLocalRepo(entry);
|
|
19321
|
+
return { ok: true, entry };
|
|
19322
|
+
}
|
|
19323
|
+
async function runLocalWatchList() {
|
|
19324
|
+
console.log(formatWatchList(await loadLocalRepos()));
|
|
19325
|
+
return 0;
|
|
19326
|
+
}
|
|
19327
|
+
async function runLocalWatchStop(slug) {
|
|
19328
|
+
const repos = await loadLocalRepos();
|
|
19329
|
+
const matches = findLocalReposBySlug(repos, slug);
|
|
19330
|
+
if (matches.length === 0) {
|
|
19331
|
+
console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
|
|
19332
|
+
console.error("Run `archme local-watch --list` to see what is.");
|
|
19333
|
+
return 78;
|
|
19334
|
+
}
|
|
19335
|
+
await saveLocalRepos(removeLocalReposBySlug(repos, slug));
|
|
19336
|
+
for (const m of matches) {
|
|
19337
|
+
console.log(`Stopped watching ${slugOf(m)}.`);
|
|
19338
|
+
}
|
|
19339
|
+
console.log(
|
|
19340
|
+
`
|
|
19341
|
+
The architecture already on the board is untouched \u2014 this only stops new commits
|
|
19342
|
+
from being picked up. Re-watch with: archme local-watch ${matches[0].sourcePath}`
|
|
19343
|
+
);
|
|
19344
|
+
return 0;
|
|
19345
|
+
}
|
|
19346
|
+
async function runLocalWatchResume(slug) {
|
|
19347
|
+
const repos = await loadLocalRepos();
|
|
19348
|
+
const matches = findLocalReposBySlug(repos, slug);
|
|
19349
|
+
if (matches.length === 0) {
|
|
19350
|
+
console.error(`archme local-watch: ${slug} is not being watched on this machine.`);
|
|
19351
|
+
return 78;
|
|
19352
|
+
}
|
|
19353
|
+
const halted = matches.filter((m) => m.haltedAt);
|
|
19354
|
+
if (halted.length === 0) {
|
|
19355
|
+
console.log(`${slug} is not halted \u2014 nothing to resume.`);
|
|
19356
|
+
return 0;
|
|
19357
|
+
}
|
|
19358
|
+
let next = repos;
|
|
19359
|
+
for (const m of halted) next = clearHalt(next, m.watchedRepoId);
|
|
19360
|
+
await saveLocalRepos(next);
|
|
19361
|
+
for (const m of halted) {
|
|
19362
|
+
console.log(`Resumed ${slugOf(m)} \u2014 was: ${m.haltReason ?? "unknown reason"}`);
|
|
19363
|
+
}
|
|
19364
|
+
const stillTerminal = halted.some((m) => m.haltCode === "job-terminal");
|
|
19365
|
+
if (stillTerminal) {
|
|
19366
|
+
console.log(
|
|
19367
|
+
"\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."
|
|
19368
|
+
);
|
|
19369
|
+
} else {
|
|
19370
|
+
console.log("\nPolling resumes from the recorded commit on the next tick.");
|
|
19371
|
+
}
|
|
19372
|
+
return 0;
|
|
19373
|
+
}
|
|
19374
|
+
var DEFAULT_CLONE_BASE_DIR2;
|
|
19375
|
+
var init_localWatch = __esm({
|
|
19376
|
+
"src/daemon/localWatch.ts"() {
|
|
19377
|
+
"use strict";
|
|
19378
|
+
init_execa();
|
|
19379
|
+
init_defaults();
|
|
19380
|
+
init_github_remote();
|
|
19381
|
+
init_run_scan();
|
|
19382
|
+
init_authedClient();
|
|
19383
|
+
init_watchReadiness();
|
|
19384
|
+
init_localGit();
|
|
19385
|
+
init_localRepos();
|
|
19386
|
+
init_branchMarker();
|
|
19387
|
+
init_localWatchList();
|
|
19388
|
+
init_localWatchResolve();
|
|
19389
|
+
init_prompt();
|
|
19390
|
+
DEFAULT_CLONE_BASE_DIR2 = join21(homedir6(), ".archme-job-runner", "clones");
|
|
19391
|
+
}
|
|
19392
|
+
});
|
|
19393
|
+
|
|
17800
19394
|
// src/init/plan.ts
|
|
17801
19395
|
function shouldRunScan(d) {
|
|
17802
19396
|
if (d.skipScan) return false;
|
|
@@ -17826,9 +19420,9 @@ var init_plan = __esm({
|
|
|
17826
19420
|
});
|
|
17827
19421
|
|
|
17828
19422
|
// src/init/run-init.ts
|
|
17829
|
-
import { mkdtemp as mkdtemp4, rm as rm8, writeFile as
|
|
19423
|
+
import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile12 } from "node:fs/promises";
|
|
17830
19424
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
17831
|
-
import { join as
|
|
19425
|
+
import { join as join22 } from "node:path";
|
|
17832
19426
|
function initScanBranch(defaultBranch) {
|
|
17833
19427
|
return normalizeScanBranchName(defaultBranch);
|
|
17834
19428
|
}
|
|
@@ -17840,10 +19434,10 @@ async function defaultFetchInstallScript(url) {
|
|
|
17840
19434
|
return await resp.text();
|
|
17841
19435
|
}
|
|
17842
19436
|
async function defaultRunInstallScript(script, cwd) {
|
|
17843
|
-
const dir = await mkdtemp4(
|
|
17844
|
-
const scriptPath =
|
|
19437
|
+
const dir = await mkdtemp4(join22(tmpdir4(), "archme-init-"));
|
|
19438
|
+
const scriptPath = join22(dir, "install-agent.sh");
|
|
17845
19439
|
try {
|
|
17846
|
-
await
|
|
19440
|
+
await writeFile12(scriptPath, script, { mode: 448 });
|
|
17847
19441
|
await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
|
|
17848
19442
|
} finally {
|
|
17849
19443
|
await rm8(dir, { recursive: true, force: true });
|
|
@@ -17864,8 +19458,8 @@ async function runInit(opts, deps = {}) {
|
|
|
17864
19458
|
const measure = deps.measure ?? measureRepo;
|
|
17865
19459
|
const watchRepo = deps.watchRepo ?? watchRepoForProject;
|
|
17866
19460
|
const cfg = resolveConfig();
|
|
17867
|
-
const
|
|
17868
|
-
const repoPath =
|
|
19461
|
+
const git4 = await gitContext(opts.repoPath);
|
|
19462
|
+
const repoPath = git4.root;
|
|
17869
19463
|
const { credentials } = await loadCreds(cfg.supabaseUrl);
|
|
17870
19464
|
const signedIn = credentials.length > 0;
|
|
17871
19465
|
if (!signedIn) {
|
|
@@ -17924,8 +19518,9 @@ async function runInit(opts, deps = {}) {
|
|
|
17924
19518
|
supabase: authed.supabase,
|
|
17925
19519
|
orgId: authed.orgId,
|
|
17926
19520
|
projectId: project.projectId,
|
|
17927
|
-
repoSlug:
|
|
17928
|
-
branch:
|
|
19521
|
+
repoSlug: git4.repoSlug,
|
|
19522
|
+
branch: git4.branch,
|
|
19523
|
+
archmeBranch: scanBranch,
|
|
17929
19524
|
log
|
|
17930
19525
|
});
|
|
17931
19526
|
if (!watched.watched && watched.reason) {
|
|
@@ -17975,28 +19570,79 @@ Your board: ${webUrl}`);
|
|
|
17975
19570
|
log(`
|
|
17976
19571
|
\u2713 Done. Your architecture: ${finalUrl}`);
|
|
17977
19572
|
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
|
-
|
|
19573
|
+
const plan = await chooseKeepCurrent({
|
|
19574
|
+
alreadyWatched: watched.watched,
|
|
19575
|
+
yes: Boolean(opts.yes),
|
|
19576
|
+
repoSlug: git4.repoSlug,
|
|
19577
|
+
choose: deps.choose ?? promptChoice,
|
|
19578
|
+
log
|
|
19579
|
+
});
|
|
19580
|
+
const outcome = await applyKeepCurrentPlan(
|
|
19581
|
+
plan,
|
|
19582
|
+
{
|
|
19583
|
+
startGitHubAppInstall: () => (deps.buildInstallUrl ?? buildInstallUrl)({
|
|
19584
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
19585
|
+
accessToken: getCurrentAccessToken() ?? "",
|
|
19586
|
+
orgId: authed.orgId
|
|
19587
|
+
}),
|
|
19588
|
+
waitForInstallation: () => (deps.waitForInstallation ?? waitForInstallation)({
|
|
19589
|
+
hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed.supabase, authed.orgId),
|
|
19590
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
19591
|
+
now: () => Date.now(),
|
|
19592
|
+
log
|
|
19593
|
+
}),
|
|
19594
|
+
watchRepo: async () => {
|
|
19595
|
+
const w = await watchRepo({
|
|
19596
|
+
supabase: authed.supabase,
|
|
19597
|
+
orgId: authed.orgId,
|
|
19598
|
+
projectId: project.projectId,
|
|
19599
|
+
repoSlug: git4.repoSlug,
|
|
19600
|
+
branch: git4.branch,
|
|
19601
|
+
archmeBranch: scanBranch,
|
|
19602
|
+
log
|
|
19603
|
+
});
|
|
19604
|
+
return { watched: w.watched, reason: w.reason };
|
|
19605
|
+
},
|
|
19606
|
+
registerLocalWatch: async () => {
|
|
19607
|
+
const target = resolveInitTarget({
|
|
19608
|
+
originUrl: git4.originUrl,
|
|
19609
|
+
detectedDefaultBranch: git4.branch,
|
|
19610
|
+
// The branch this run actually scanned. Deriving it from the git
|
|
19611
|
+
// branch instead would point the poller at `<checkout>-scan` while
|
|
19612
|
+
// the marker sits on `<default>-scan` — so registering would fail
|
|
19613
|
+
// on a feature-branch checkout, immediately after a scan that
|
|
19614
|
+
// succeeded.
|
|
19615
|
+
archmeBranchOverride: scanBranch
|
|
19616
|
+
});
|
|
19617
|
+
if (!target.ok) return { ok: false, reason: target.error };
|
|
19618
|
+
const res = await (deps.registerLocalWatch ?? registerLocalWatch)({
|
|
19619
|
+
supabase: authed.supabase,
|
|
19620
|
+
orgId: authed.orgId,
|
|
19621
|
+
projectId: project.projectId,
|
|
19622
|
+
repoPath: git4.root,
|
|
19623
|
+
originUrl: git4.originUrl,
|
|
19624
|
+
target: target.target,
|
|
19625
|
+
log
|
|
19626
|
+
});
|
|
19627
|
+
return res.ok ? { ok: true } : { ok: false, reason: res.reason };
|
|
19628
|
+
},
|
|
19629
|
+
log
|
|
19630
|
+
},
|
|
19631
|
+
watched.watched
|
|
19632
|
+
);
|
|
19633
|
+
if (!outcome.wired && outcome.reason) {
|
|
17994
19634
|
log("");
|
|
17995
19635
|
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.");
|
|
19636
|
+
log(` ${outcome.reason}`);
|
|
19637
|
+
log(" Until that is resolved, this architecture will not track new commits.");
|
|
17999
19638
|
}
|
|
19639
|
+
log(
|
|
19640
|
+
formatKeepCurrentPlan(plan, {
|
|
19641
|
+
repoSlug: git4.repoSlug,
|
|
19642
|
+
boardUrl: finalUrl,
|
|
19643
|
+
localWatchRegistered: outcome.localWatchRegistered
|
|
19644
|
+
})
|
|
19645
|
+
);
|
|
18000
19646
|
}
|
|
18001
19647
|
return {
|
|
18002
19648
|
exitCode: result.exitCode,
|
|
@@ -18020,6 +19666,12 @@ var init_run_init = __esm({
|
|
|
18020
19666
|
init_watchRepo();
|
|
18021
19667
|
init_gitContext();
|
|
18022
19668
|
init_web_url();
|
|
19669
|
+
init_bearer();
|
|
19670
|
+
init_keepCurrent();
|
|
19671
|
+
init_keepCurrentFlow();
|
|
19672
|
+
init_githubAppSetup();
|
|
19673
|
+
init_prompt();
|
|
19674
|
+
init_localWatch();
|
|
18023
19675
|
init_plan();
|
|
18024
19676
|
}
|
|
18025
19677
|
});
|
|
@@ -18111,7 +19763,7 @@ __export(dispatch_exports4, {
|
|
|
18111
19763
|
dispatchInit: () => dispatchInit,
|
|
18112
19764
|
printInitHelp: () => printInitHelp
|
|
18113
19765
|
});
|
|
18114
|
-
import { resolve as
|
|
19766
|
+
import { resolve as resolve4 } from "node:path";
|
|
18115
19767
|
function printInitHelp() {
|
|
18116
19768
|
console.log(`archme init \u2014 guided first-time setup for this repo
|
|
18117
19769
|
|
|
@@ -18149,7 +19801,7 @@ async function dispatchInit(argv) {
|
|
|
18149
19801
|
}
|
|
18150
19802
|
try {
|
|
18151
19803
|
const result = await runInit({
|
|
18152
|
-
repoPath:
|
|
19804
|
+
repoPath: resolve4(parsed.args.repoPath ?? process.cwd()),
|
|
18153
19805
|
project: parsed.args.project,
|
|
18154
19806
|
appOrigin: parsed.args.appOrigin,
|
|
18155
19807
|
force: parsed.args.force,
|
|
@@ -18157,8 +19809,8 @@ async function dispatchInit(argv) {
|
|
|
18157
19809
|
yes: parsed.args.yes || parsed.args.noInteractive
|
|
18158
19810
|
});
|
|
18159
19811
|
return result.exitCode;
|
|
18160
|
-
} catch (
|
|
18161
|
-
console.error(`archme init: ${
|
|
19812
|
+
} catch (err2) {
|
|
19813
|
+
console.error(`archme init: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
18162
19814
|
return EX_INFRA2;
|
|
18163
19815
|
}
|
|
18164
19816
|
}
|
|
@@ -18174,6 +19826,620 @@ var init_dispatch4 = __esm({
|
|
|
18174
19826
|
}
|
|
18175
19827
|
});
|
|
18176
19828
|
|
|
19829
|
+
// src/daemon/localWatchDispatch.ts
|
|
19830
|
+
var localWatchDispatch_exports = {};
|
|
19831
|
+
__export(localWatchDispatch_exports, {
|
|
19832
|
+
EX_INFRA: () => EX_INFRA3,
|
|
19833
|
+
EX_OK: () => EX_OK3,
|
|
19834
|
+
EX_USAGE: () => EX_USAGE3,
|
|
19835
|
+
dispatchLocalWatch: () => dispatchLocalWatch,
|
|
19836
|
+
parseLocalWatchArgs: () => parseLocalWatchArgs,
|
|
19837
|
+
printLocalWatchHelp: () => printLocalWatchHelp
|
|
19838
|
+
});
|
|
19839
|
+
function err(message) {
|
|
19840
|
+
return { kind: "error", message: `archme local-watch: ${message}` };
|
|
19841
|
+
}
|
|
19842
|
+
function parseLocalWatchArgs(argv) {
|
|
19843
|
+
if (argv.some((a2) => a2 === "--help" || a2 === "-h" || a2 === "help")) return { kind: "help" };
|
|
19844
|
+
let repoPath;
|
|
19845
|
+
let project;
|
|
19846
|
+
let branch;
|
|
19847
|
+
let yes = false;
|
|
19848
|
+
let list = false;
|
|
19849
|
+
let stop;
|
|
19850
|
+
let resume;
|
|
19851
|
+
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
19852
|
+
const arg = argv[i2];
|
|
19853
|
+
const next = argv[i2 + 1];
|
|
19854
|
+
if (arg === "--project") {
|
|
19855
|
+
if (!next) return err("--project needs an org/slug");
|
|
19856
|
+
if (!PROJECT_SLUG_RE3.test(next)) {
|
|
19857
|
+
return err(
|
|
19858
|
+
`--project '${next}' must be \`org/slug\` (exactly two non-empty segments separated by /)`
|
|
19859
|
+
);
|
|
19860
|
+
}
|
|
19861
|
+
project = next;
|
|
19862
|
+
i2++;
|
|
19863
|
+
} else if (arg === "--branch") {
|
|
19864
|
+
if (!next) return err("--branch needs a branch name");
|
|
19865
|
+
branch = next;
|
|
19866
|
+
i2++;
|
|
19867
|
+
} else if (arg === "--yes" || arg === "--no-interactive") {
|
|
19868
|
+
yes = true;
|
|
19869
|
+
} else if (arg === "--list") {
|
|
19870
|
+
list = true;
|
|
19871
|
+
} else if (arg === "--stop" || arg === "--resume") {
|
|
19872
|
+
if (!next || next.startsWith("-")) return err(`${arg} needs a repo (owner/name)`);
|
|
19873
|
+
if (!REPO_SLUG_RE.test(next)) {
|
|
19874
|
+
return err(`${arg} '${next}' must be \`owner/name\` \u2014 that is how repos are registered`);
|
|
19875
|
+
}
|
|
19876
|
+
if (arg === "--stop") stop = next;
|
|
19877
|
+
else resume = next;
|
|
19878
|
+
i2++;
|
|
19879
|
+
} else if (arg && arg.startsWith("-")) {
|
|
19880
|
+
return err(`unknown flag '${arg}'`);
|
|
19881
|
+
} else if (arg && repoPath === void 0) {
|
|
19882
|
+
repoPath = arg;
|
|
19883
|
+
} else if (arg) {
|
|
19884
|
+
return err(`unexpected argument '${arg}'`);
|
|
19885
|
+
}
|
|
19886
|
+
}
|
|
19887
|
+
const modes = [list && "--list", stop && "--stop", resume && "--resume"].filter(Boolean);
|
|
19888
|
+
if (modes.length > 1) {
|
|
19889
|
+
return err(`${modes.join(" and ")} do different things \u2014 pass one of them at once, not both`);
|
|
19890
|
+
}
|
|
19891
|
+
if (list) return { kind: "list" };
|
|
19892
|
+
if (stop) return { kind: "stop", repo: stop };
|
|
19893
|
+
if (resume) return { kind: "resume", repo: resume };
|
|
19894
|
+
return { kind: "register", repoPath, project, branch, yes };
|
|
19895
|
+
}
|
|
19896
|
+
function printLocalWatchHelp() {
|
|
19897
|
+
console.log(`archme local-watch \u2014 keep ArchMe current without a GitHub App
|
|
19898
|
+
|
|
19899
|
+
Registers a repo so this machine polls \`origin\` and feeds new commits into
|
|
19900
|
+
ArchMe, one commit at a time. No GitHub App, no admin approval \u2014 but the
|
|
19901
|
+
updates only happen while your daemon is running. Requires a github.com origin.
|
|
19902
|
+
|
|
19903
|
+
Your working checkout is read exactly once (to find its origin URL and seed a
|
|
19904
|
+
mirror); everything afterwards runs against a daemon-owned copy, so ArchMe
|
|
19905
|
+
never touches your working tree.
|
|
19906
|
+
|
|
19907
|
+
Usage:
|
|
19908
|
+
archme local-watch [repo-path] Register a repo (defaults to the one you're in)
|
|
19909
|
+
archme local-watch --list Show what this machine is watching
|
|
19910
|
+
archme local-watch --stop <owner/name>
|
|
19911
|
+
archme local-watch --resume <owner/name>
|
|
19912
|
+
|
|
19913
|
+
Options:
|
|
19914
|
+
--project <org/slug> Project to write to (default: derived from the repo name)
|
|
19915
|
+
--branch <name> Branch to track (default: the repo's default branch)
|
|
19916
|
+
--yes Don't ask for confirmation (needs an explicit repo-path)
|
|
19917
|
+
|
|
19918
|
+
Watching queues work; the daemon does it. After registering: archme daemon start`);
|
|
19919
|
+
}
|
|
19920
|
+
async function dispatchLocalWatch(argv) {
|
|
19921
|
+
const parsed = parseLocalWatchArgs(argv);
|
|
19922
|
+
if (parsed.kind === "help") {
|
|
19923
|
+
printLocalWatchHelp();
|
|
19924
|
+
return EX_OK3;
|
|
19925
|
+
}
|
|
19926
|
+
if (parsed.kind === "error") {
|
|
19927
|
+
console.error(parsed.message);
|
|
19928
|
+
printLocalWatchHelp();
|
|
19929
|
+
return EX_USAGE3;
|
|
19930
|
+
}
|
|
19931
|
+
const mod = await Promise.resolve().then(() => (init_localWatch(), localWatch_exports));
|
|
19932
|
+
switch (parsed.kind) {
|
|
19933
|
+
case "list":
|
|
19934
|
+
return await mod.runLocalWatchList();
|
|
19935
|
+
case "stop":
|
|
19936
|
+
return await mod.runLocalWatchStop(parsed.repo);
|
|
19937
|
+
case "resume":
|
|
19938
|
+
return await mod.runLocalWatchResume(parsed.repo);
|
|
19939
|
+
case "register":
|
|
19940
|
+
return await mod.runLocalWatch(parsed);
|
|
19941
|
+
}
|
|
19942
|
+
}
|
|
19943
|
+
var EX_OK3, EX_INFRA3, EX_USAGE3, PROJECT_SLUG_RE3, REPO_SLUG_RE;
|
|
19944
|
+
var init_localWatchDispatch = __esm({
|
|
19945
|
+
"src/daemon/localWatchDispatch.ts"() {
|
|
19946
|
+
"use strict";
|
|
19947
|
+
EX_OK3 = 0;
|
|
19948
|
+
EX_INFRA3 = 3;
|
|
19949
|
+
EX_USAGE3 = 78;
|
|
19950
|
+
PROJECT_SLUG_RE3 = /^[^/\s]+\/[^/\s]+$/;
|
|
19951
|
+
REPO_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
19952
|
+
}
|
|
19953
|
+
});
|
|
19954
|
+
|
|
19955
|
+
// src/doctor/collect.ts
|
|
19956
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
19957
|
+
import { join as join23 } from "node:path";
|
|
19958
|
+
function binaryPresent(name) {
|
|
19959
|
+
try {
|
|
19960
|
+
requireBinary(name);
|
|
19961
|
+
return true;
|
|
19962
|
+
} catch {
|
|
19963
|
+
return false;
|
|
19964
|
+
}
|
|
19965
|
+
}
|
|
19966
|
+
async function git3(cwd, args) {
|
|
19967
|
+
const r = await execa("git", args, { cwd, reject: false });
|
|
19968
|
+
return r.exitCode === 0 ? r.stdout.trim() || null : null;
|
|
19969
|
+
}
|
|
19970
|
+
async function readMcpPin(repoRoot) {
|
|
19971
|
+
const empty = { installed: false, pin: { project: null, branch: null } };
|
|
19972
|
+
try {
|
|
19973
|
+
const raw = await readFile13(join23(repoRoot, ".mcp.json"), "utf8");
|
|
19974
|
+
const parsed = JSON.parse(raw);
|
|
19975
|
+
const servers = parsed.mcpServers ?? {};
|
|
19976
|
+
const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
|
|
19977
|
+
if (!entry) return empty;
|
|
19978
|
+
const url = entry[1]?.url;
|
|
19979
|
+
if (!url) return { installed: true, pin: { project: null, branch: null } };
|
|
19980
|
+
const params = new URL(url).searchParams;
|
|
19981
|
+
return {
|
|
19982
|
+
installed: true,
|
|
19983
|
+
pin: { project: params.get("project"), branch: params.get("branch") }
|
|
19984
|
+
};
|
|
19985
|
+
} catch {
|
|
19986
|
+
return empty;
|
|
19987
|
+
}
|
|
19988
|
+
}
|
|
19989
|
+
async function isDaemonRunning() {
|
|
19990
|
+
const r = await execa("pgrep", ["-f", "archme.*daemon"], { reject: false });
|
|
19991
|
+
return r.exitCode === 0 && Boolean(r.stdout.trim());
|
|
19992
|
+
}
|
|
19993
|
+
async function collectFacts(cwd) {
|
|
19994
|
+
const facts = {
|
|
19995
|
+
cliVersion: await readDaemonVersion(),
|
|
19996
|
+
// `requireBinary` throws when absent; doctor's whole job is to report that
|
|
19997
|
+
// calmly rather than die on it.
|
|
19998
|
+
claudeInstalled: binaryPresent("claude"),
|
|
19999
|
+
gitInstalled: binaryPresent("git"),
|
|
20000
|
+
signedIn: false,
|
|
20001
|
+
identity: null,
|
|
20002
|
+
orgId: null,
|
|
20003
|
+
inRepo: false,
|
|
20004
|
+
repoRoot: null,
|
|
20005
|
+
repoSlug: null,
|
|
20006
|
+
gitBranch: null,
|
|
20007
|
+
agentConfigInstalled: false,
|
|
20008
|
+
pinnedProject: null,
|
|
20009
|
+
pinnedBranch: null,
|
|
20010
|
+
pinnedOrgVisible: null,
|
|
20011
|
+
projectExists: null,
|
|
20012
|
+
componentCount: null,
|
|
20013
|
+
markerSha: null,
|
|
20014
|
+
watchedBy: "unknown",
|
|
20015
|
+
localRepos: [],
|
|
20016
|
+
daemonRunning: await isDaemonRunning()
|
|
20017
|
+
};
|
|
20018
|
+
const root = facts.gitInstalled ? await git3(cwd, ["rev-parse", "--show-toplevel"]) : null;
|
|
20019
|
+
if (root) {
|
|
20020
|
+
facts.inRepo = true;
|
|
20021
|
+
facts.repoRoot = root;
|
|
20022
|
+
facts.gitBranch = await git3(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
20023
|
+
const originUrl = await git3(root, ["remote", "get-url", "origin"]);
|
|
20024
|
+
facts.repoSlug = originUrl ? parseGitHubRepo(originUrl) : null;
|
|
20025
|
+
const mcp = await readMcpPin(root);
|
|
20026
|
+
facts.agentConfigInstalled = mcp.installed;
|
|
20027
|
+
facts.pinnedProject = mcp.pin.project;
|
|
20028
|
+
facts.pinnedBranch = mcp.pin.branch;
|
|
20029
|
+
}
|
|
20030
|
+
const registry = await loadLocalRepos();
|
|
20031
|
+
const forThisRepo = facts.repoSlug ? registry.filter((r) => slugOf(r).toLowerCase() === facts.repoSlug.toLowerCase()) : [];
|
|
20032
|
+
facts.localRepos = forThisRepo.map(
|
|
20033
|
+
(r) => ({
|
|
20034
|
+
slug: slugOf(r),
|
|
20035
|
+
halted: Boolean(r.haltedAt),
|
|
20036
|
+
haltReason: r.haltReason
|
|
20037
|
+
})
|
|
20038
|
+
);
|
|
20039
|
+
try {
|
|
20040
|
+
const cfg = resolveConfig();
|
|
20041
|
+
const authed = await createAuthedClient({
|
|
20042
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
20043
|
+
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
20044
|
+
clientId: cfg.archmeDaemonClientId,
|
|
20045
|
+
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
20046
|
+
});
|
|
20047
|
+
if (!authed.orgId) return facts;
|
|
20048
|
+
facts.signedIn = true;
|
|
20049
|
+
facts.orgId = authed.orgId;
|
|
20050
|
+
facts.identity = authed.identity;
|
|
20051
|
+
const supabase = authed.supabase;
|
|
20052
|
+
const slug = facts.pinnedProject;
|
|
20053
|
+
if (slug) {
|
|
20054
|
+
const [orgSlug, projectSlug] = slug.split("/");
|
|
20055
|
+
const { data: org } = await supabase.from("organizations").select("id").eq("slug", orgSlug ?? "").maybeSingle();
|
|
20056
|
+
facts.pinnedOrgVisible = Boolean(org);
|
|
20057
|
+
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 };
|
|
20058
|
+
if (!org) {
|
|
20059
|
+
facts.projectExists = null;
|
|
20060
|
+
} else if (!project) {
|
|
20061
|
+
facts.projectExists = false;
|
|
20062
|
+
} else {
|
|
20063
|
+
facts.projectExists = true;
|
|
20064
|
+
const branch = facts.pinnedBranch ?? "main";
|
|
20065
|
+
const { count: count2 } = await supabase.from("project_files").select("id", { count: "exact", head: true }).eq("project_id", project.id).eq("branch", branch);
|
|
20066
|
+
facts.componentCount = count2 ?? null;
|
|
20067
|
+
const marker = await readBranchMarker(supabase, project.id, branch);
|
|
20068
|
+
facts.markerSha = marker.sha;
|
|
20069
|
+
}
|
|
20070
|
+
}
|
|
20071
|
+
if (facts.localRepos.length > 0) {
|
|
20072
|
+
facts.watchedBy = "local";
|
|
20073
|
+
} else if (facts.repoSlug) {
|
|
20074
|
+
const [owner, name] = facts.repoSlug.split("/");
|
|
20075
|
+
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);
|
|
20076
|
+
const rows = watched ?? [];
|
|
20077
|
+
const live = rows.find((r) => r.commit_update_enabled);
|
|
20078
|
+
facts.watchedBy = live ? live.source_kind === "local" ? "local" : "github" : "none";
|
|
20079
|
+
}
|
|
20080
|
+
} catch {
|
|
20081
|
+
}
|
|
20082
|
+
return facts;
|
|
20083
|
+
}
|
|
20084
|
+
var init_collect = __esm({
|
|
20085
|
+
"src/doctor/collect.ts"() {
|
|
20086
|
+
"use strict";
|
|
20087
|
+
init_execa();
|
|
20088
|
+
init_defaults();
|
|
20089
|
+
init_version();
|
|
20090
|
+
init_preflight();
|
|
20091
|
+
init_branchMarker();
|
|
20092
|
+
init_localRepos();
|
|
20093
|
+
init_github_remote();
|
|
20094
|
+
init_authedClient();
|
|
20095
|
+
}
|
|
20096
|
+
});
|
|
20097
|
+
|
|
20098
|
+
// src/doctor/facts.ts
|
|
20099
|
+
function diagnose(f) {
|
|
20100
|
+
const issues = [];
|
|
20101
|
+
if (!f.claudeInstalled) {
|
|
20102
|
+
issues.push({
|
|
20103
|
+
severity: "error",
|
|
20104
|
+
title: "Claude Code is not installed",
|
|
20105
|
+
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.",
|
|
20106
|
+
fix: "npm i -g @anthropic-ai/claude-code"
|
|
20107
|
+
});
|
|
20108
|
+
}
|
|
20109
|
+
if (!f.gitInstalled) {
|
|
20110
|
+
issues.push({
|
|
20111
|
+
severity: "error",
|
|
20112
|
+
title: "git is not installed",
|
|
20113
|
+
detail: "archme clones and diffs repositories with git.",
|
|
20114
|
+
fix: "macOS: xcode-select --install \xB7 Debian/Ubuntu: sudo apt-get install git"
|
|
20115
|
+
});
|
|
20116
|
+
}
|
|
20117
|
+
if (f.cliVersion === null) {
|
|
20118
|
+
issues.push({
|
|
20119
|
+
severity: "error",
|
|
20120
|
+
title: "this install cannot read its own version",
|
|
20121
|
+
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.",
|
|
20122
|
+
fix: "npm i -g @archme/cli"
|
|
20123
|
+
});
|
|
20124
|
+
}
|
|
20125
|
+
if (!f.signedIn) {
|
|
20126
|
+
issues.push({
|
|
20127
|
+
severity: "error",
|
|
20128
|
+
title: "not signed in",
|
|
20129
|
+
detail: "Your project, architecture and watch status can't be checked until this machine is paired with an ArchMe account.",
|
|
20130
|
+
fix: "archme login"
|
|
20131
|
+
});
|
|
20132
|
+
return issues;
|
|
20133
|
+
}
|
|
20134
|
+
if (!f.inRepo) {
|
|
20135
|
+
issues.push({
|
|
20136
|
+
severity: "warn",
|
|
20137
|
+
title: "not inside a git repository",
|
|
20138
|
+
detail: "Run this from the repo you want ArchMe to track, so it can check that repo's setup.",
|
|
20139
|
+
fix: "cd <your repo> && archme doctor"
|
|
20140
|
+
});
|
|
20141
|
+
return issues;
|
|
20142
|
+
}
|
|
20143
|
+
if (f.pinnedOrgVisible === false) {
|
|
20144
|
+
issues.push({
|
|
20145
|
+
severity: "error",
|
|
20146
|
+
title: "this repo is pinned to a project your account cannot see",
|
|
20147
|
+
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.`,
|
|
20148
|
+
fix: "archme login (choose the org that owns the project), or re-run `archme init` to pin one you can reach"
|
|
20149
|
+
});
|
|
20150
|
+
return issues;
|
|
20151
|
+
}
|
|
20152
|
+
if (f.projectExists === false || f.componentCount === 0) {
|
|
20153
|
+
issues.push({
|
|
20154
|
+
severity: "error",
|
|
20155
|
+
title: "this repo has no architecture yet",
|
|
20156
|
+
detail: "Nothing has been scanned, so there is nothing for updates to keep current.",
|
|
20157
|
+
fix: "archme init"
|
|
20158
|
+
});
|
|
20159
|
+
return issues;
|
|
20160
|
+
}
|
|
20161
|
+
if (!f.agentConfigInstalled) {
|
|
20162
|
+
issues.push({
|
|
20163
|
+
severity: "warn",
|
|
20164
|
+
title: "agent config is missing from this repo",
|
|
20165
|
+
detail: "Without `.mcp.json`, Claude Code in this repo can't read or write the architecture while you work. Updates from commits still happen.",
|
|
20166
|
+
fix: "archme init"
|
|
20167
|
+
});
|
|
20168
|
+
}
|
|
20169
|
+
if (f.projectExists === true && f.markerSha === null) {
|
|
20170
|
+
issues.push({
|
|
20171
|
+
severity: "error",
|
|
20172
|
+
title: "the last scan left no resume point",
|
|
20173
|
+
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.',
|
|
20174
|
+
fix: "archme init --force"
|
|
20175
|
+
});
|
|
20176
|
+
}
|
|
20177
|
+
if (f.watchedBy === "none") {
|
|
20178
|
+
issues.push({
|
|
20179
|
+
severity: "error",
|
|
20180
|
+
title: "nothing is watching this repo",
|
|
20181
|
+
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.",
|
|
20182
|
+
fix: "archme init"
|
|
20183
|
+
});
|
|
20184
|
+
}
|
|
20185
|
+
const halted = f.localRepos.filter((r) => r.halted);
|
|
20186
|
+
for (const r of halted) {
|
|
20187
|
+
issues.push({
|
|
20188
|
+
severity: "error",
|
|
20189
|
+
title: `local polling is halted for ${r.slug}`,
|
|
20190
|
+
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.`,
|
|
20191
|
+
fix: `archme local-watch --resume ${r.slug}`
|
|
20192
|
+
});
|
|
20193
|
+
}
|
|
20194
|
+
if (!f.daemonRunning && f.watchedBy !== "none" && f.watchedBy !== "unknown") {
|
|
20195
|
+
issues.push({
|
|
20196
|
+
severity: "error",
|
|
20197
|
+
title: "the daemon is not running",
|
|
20198
|
+
detail: "Commits are queueing up, but nothing is processing them. Updates only happen while the daemon runs, on a machine that stays on.",
|
|
20199
|
+
fix: "archme daemon start"
|
|
20200
|
+
});
|
|
20201
|
+
}
|
|
20202
|
+
return issues;
|
|
20203
|
+
}
|
|
20204
|
+
function yesNo(v) {
|
|
20205
|
+
return v ? "yes" : "no";
|
|
20206
|
+
}
|
|
20207
|
+
function formatFacts(f) {
|
|
20208
|
+
const lines = ["ArchMe setup"];
|
|
20209
|
+
lines.push("", " This machine");
|
|
20210
|
+
lines.push(` archme version: ${f.cliVersion ?? "UNKNOWN (cannot read its own version)"}`);
|
|
20211
|
+
lines.push(` claude installed: ${yesNo(f.claudeInstalled)}`);
|
|
20212
|
+
lines.push(` git installed: ${yesNo(f.gitInstalled)}`);
|
|
20213
|
+
lines.push(` signed in: ${f.signedIn ? f.identity ?? "yes" : "no"}`);
|
|
20214
|
+
lines.push(` daemon running: ${yesNo(f.daemonRunning)}`);
|
|
20215
|
+
lines.push("", " This repo");
|
|
20216
|
+
if (!f.inRepo) {
|
|
20217
|
+
lines.push(" not inside a git repository");
|
|
20218
|
+
} else {
|
|
20219
|
+
lines.push(` path: ${f.repoRoot}`);
|
|
20220
|
+
lines.push(` repo: ${f.repoSlug ?? "no github.com remote"}`);
|
|
20221
|
+
lines.push(` branch: ${f.gitBranch ?? "unknown"}`);
|
|
20222
|
+
lines.push(` .mcp.json: ${f.agentConfigInstalled ? "installed" : "missing"}`);
|
|
20223
|
+
if (f.pinnedProject) lines.push(` pinned to: ${f.pinnedProject} @ ${f.pinnedBranch ?? "?"}`);
|
|
20224
|
+
}
|
|
20225
|
+
lines.push("", " Architecture");
|
|
20226
|
+
if (!f.signedIn) {
|
|
20227
|
+
lines.push(" unknown \u2014 not signed in");
|
|
20228
|
+
} else {
|
|
20229
|
+
if (f.pinnedOrgVisible === false) {
|
|
20230
|
+
lines.push(` NOT VISIBLE \u2014 ${f.pinnedProject} belongs to another organisation`);
|
|
20231
|
+
}
|
|
20232
|
+
lines.push(` project exists: ${f.projectExists === null ? "unknown" : yesNo(f.projectExists)}`);
|
|
20233
|
+
lines.push(` components: ${f.componentCount === null ? "unknown" : f.componentCount}`);
|
|
20234
|
+
lines.push(
|
|
20235
|
+
` last scanned at: ${f.markerSha ? f.markerSha.slice(0, 7) : "never recorded"}`
|
|
20236
|
+
);
|
|
20237
|
+
lines.push(` updates from: ${describeWatch(f)}`);
|
|
20238
|
+
}
|
|
20239
|
+
for (const r of f.localRepos) {
|
|
20240
|
+
lines.push(
|
|
20241
|
+
` \xB7 ${r.slug}${r.halted ? ` HALTED: ${r.haltReason ?? "unknown"}` : ""}`
|
|
20242
|
+
);
|
|
20243
|
+
}
|
|
20244
|
+
return lines.join("\n");
|
|
20245
|
+
}
|
|
20246
|
+
function describeWatch(f) {
|
|
20247
|
+
switch (f.watchedBy) {
|
|
20248
|
+
case "github":
|
|
20249
|
+
return "GitHub App (pushes and pull requests)";
|
|
20250
|
+
case "local":
|
|
20251
|
+
return "local polling on this machine";
|
|
20252
|
+
case "none":
|
|
20253
|
+
return "NOTHING \u2014 this architecture will not track new commits";
|
|
20254
|
+
default:
|
|
20255
|
+
return "unknown";
|
|
20256
|
+
}
|
|
20257
|
+
}
|
|
20258
|
+
var init_facts = __esm({
|
|
20259
|
+
"src/doctor/facts.ts"() {
|
|
20260
|
+
"use strict";
|
|
20261
|
+
}
|
|
20262
|
+
});
|
|
20263
|
+
|
|
20264
|
+
// src/doctor/prompt.ts
|
|
20265
|
+
function buildDoctorPrompt(facts, issues) {
|
|
20266
|
+
const problems = issues.length === 0 ? "No problems were found." : issues.map((i2) => `- [${i2.severity}] ${i2.title}
|
|
20267
|
+
${i2.detail}
|
|
20268
|
+
Fix: ${i2.fix}`).join("\n");
|
|
20269
|
+
return `You are helping someone understand their ArchMe setup. This is the
|
|
20270
|
+
\`archme doctor\` command.
|
|
20271
|
+
|
|
20272
|
+
${HOW_IT_WORKS}
|
|
20273
|
+
|
|
20274
|
+
## What was measured on this machine
|
|
20275
|
+
|
|
20276
|
+
These facts were gathered by the CLI before you started. They are accurate.
|
|
20277
|
+
Do NOT re-derive them, and do not go looking for them in the filesystem \u2014 you
|
|
20278
|
+
have everything already.
|
|
20279
|
+
|
|
20280
|
+
\`\`\`
|
|
20281
|
+
${formatFacts(facts)}
|
|
20282
|
+
\`\`\`
|
|
20283
|
+
|
|
20284
|
+
## Problems found
|
|
20285
|
+
|
|
20286
|
+
${problems}
|
|
20287
|
+
|
|
20288
|
+
## Your job
|
|
20289
|
+
|
|
20290
|
+
The report above has ALREADY been printed to the user's terminal. Do not repeat
|
|
20291
|
+
it back to them.
|
|
20292
|
+
|
|
20293
|
+
Open with two or three sentences: where they are in the flow above, and the
|
|
20294
|
+
single most important thing to do next (or that nothing needs doing). Then stop
|
|
20295
|
+
and let them ask.
|
|
20296
|
+
|
|
20297
|
+
Rules:
|
|
20298
|
+
- You are READ-ONLY. You cannot fix anything, and you must not offer to. Give
|
|
20299
|
+
them the command to run.
|
|
20300
|
+
- Do not invent state. If they ask something the facts above do not answer, say
|
|
20301
|
+
so and tell them which command would show it.
|
|
20302
|
+
- Never tell them their setup is fine if a problem was listed.
|
|
20303
|
+
- Be brief. They ran a diagnostic, not a tutorial. Depth on request.`;
|
|
20304
|
+
}
|
|
20305
|
+
var HOW_IT_WORKS;
|
|
20306
|
+
var init_prompt2 = __esm({
|
|
20307
|
+
"src/doctor/prompt.ts"() {
|
|
20308
|
+
"use strict";
|
|
20309
|
+
init_facts();
|
|
20310
|
+
HOW_IT_WORKS = `## How ArchMe is meant to work
|
|
20311
|
+
|
|
20312
|
+
1. **Scan** \u2014 \`archme init\` maps the repo into an architecture on the board.
|
|
20313
|
+
This is a one-time snapshot. It runs Claude Code locally, on the user's own
|
|
20314
|
+
Claude subscription. Their code is never sent to ArchMe's servers; only the
|
|
20315
|
+
resulting architecture is.
|
|
20316
|
+
|
|
20317
|
+
2. **Detection** \u2014 something has to notice new commits. Two options:
|
|
20318
|
+
- **GitHub App**: webhooks. Sees pushes AND pull requests, works for the
|
|
20319
|
+
whole team, nothing to keep running. Needs someone to grant repo read
|
|
20320
|
+
access.
|
|
20321
|
+
- **Local polling** (\`archme local-watch\`): this machine fetches the repo on
|
|
20322
|
+
a timer. No GitHub App, no approvals, but no pull-request awareness.
|
|
20323
|
+
|
|
20324
|
+
3. **Execution** \u2014 something has to PROCESS each detected commit. This is the
|
|
20325
|
+
step people miss. Detection only queues work; the queue does nothing on its
|
|
20326
|
+
own. Either the user runs \`archme daemon start\` on a machine that stays on,
|
|
20327
|
+
or they email signup@archme.io and ArchMe runs it for them.
|
|
20328
|
+
|
|
20329
|
+
The failure mode that matters most: an architecture that exists but is not
|
|
20330
|
+
being updated. The board still looks current, so nobody notices it is stale.
|
|
20331
|
+
Detection without execution produces exactly that, which is why step 3 is not
|
|
20332
|
+
optional.`;
|
|
20333
|
+
}
|
|
20334
|
+
});
|
|
20335
|
+
|
|
20336
|
+
// src/doctor/dispatch.ts
|
|
20337
|
+
var dispatch_exports5 = {};
|
|
20338
|
+
__export(dispatch_exports5, {
|
|
20339
|
+
EX_INFRA: () => EX_INFRA4,
|
|
20340
|
+
EX_OK: () => EX_OK4,
|
|
20341
|
+
EX_USAGE: () => EX_USAGE4,
|
|
20342
|
+
dispatchDoctor: () => dispatchDoctor,
|
|
20343
|
+
parseDoctorArgs: () => parseDoctorArgs,
|
|
20344
|
+
printDoctorHelp: () => printDoctorHelp
|
|
20345
|
+
});
|
|
20346
|
+
import { mkdtemp as mkdtemp5, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
|
|
20347
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
20348
|
+
import { join as join24 } from "node:path";
|
|
20349
|
+
function parseDoctorArgs(argv) {
|
|
20350
|
+
const args = { plain: false, help: false };
|
|
20351
|
+
for (const a2 of argv) {
|
|
20352
|
+
if (a2 === "--help" || a2 === "-h" || a2 === "help") args.help = true;
|
|
20353
|
+
else if (a2 === "--plain" || a2 === "--no-agent") args.plain = true;
|
|
20354
|
+
else return { ok: false, message: `archme doctor: unknown flag '${a2}'` };
|
|
20355
|
+
}
|
|
20356
|
+
return { ok: true, args };
|
|
20357
|
+
}
|
|
20358
|
+
function printDoctorHelp() {
|
|
20359
|
+
console.log(`archme doctor \u2014 check this machine's ArchMe setup and explain it
|
|
20360
|
+
|
|
20361
|
+
Reports what is installed, signed in, scanned, watched and running, then hands
|
|
20362
|
+
the results to Claude Code so you can ask about them.
|
|
20363
|
+
|
|
20364
|
+
Read-only: it never changes anything. Anything that needs doing is printed as a
|
|
20365
|
+
command for you to run.
|
|
20366
|
+
|
|
20367
|
+
Usage:
|
|
20368
|
+
archme doctor [--plain]
|
|
20369
|
+
|
|
20370
|
+
Options:
|
|
20371
|
+
--plain Just print the report \u2014 don't start Claude. Use this when Claude
|
|
20372
|
+
Code itself is what's broken.
|
|
20373
|
+
-h, --help`);
|
|
20374
|
+
}
|
|
20375
|
+
async function dispatchDoctor(argv) {
|
|
20376
|
+
const parsed = parseDoctorArgs(argv);
|
|
20377
|
+
if (!parsed.ok) {
|
|
20378
|
+
console.error(parsed.message);
|
|
20379
|
+
printDoctorHelp();
|
|
20380
|
+
return EX_USAGE4;
|
|
20381
|
+
}
|
|
20382
|
+
if (parsed.args.help) {
|
|
20383
|
+
printDoctorHelp();
|
|
20384
|
+
return EX_OK4;
|
|
20385
|
+
}
|
|
20386
|
+
const facts = await collectFacts(process.cwd());
|
|
20387
|
+
const issues = diagnose(facts);
|
|
20388
|
+
console.log(formatFacts(facts));
|
|
20389
|
+
if (issues.length === 0) {
|
|
20390
|
+
console.log("\n\u2713 Everything checks out.");
|
|
20391
|
+
} else {
|
|
20392
|
+
console.log("");
|
|
20393
|
+
for (const issue of issues) {
|
|
20394
|
+
console.log(`${issue.severity === "error" ? "\u2717" : "!"} ${issue.title}`);
|
|
20395
|
+
console.log(` ${issue.detail}`);
|
|
20396
|
+
console.log(` \u2192 ${issue.fix}`);
|
|
20397
|
+
console.log("");
|
|
20398
|
+
}
|
|
20399
|
+
}
|
|
20400
|
+
if (parsed.args.plain) return EX_OK4;
|
|
20401
|
+
try {
|
|
20402
|
+
requireBinary("claude");
|
|
20403
|
+
} catch {
|
|
20404
|
+
console.log(
|
|
20405
|
+
"Install Claude Code to ask about any of this: npm i -g @anthropic-ai/claude-code"
|
|
20406
|
+
);
|
|
20407
|
+
return EX_OK4;
|
|
20408
|
+
}
|
|
20409
|
+
const dir = await mkdtemp5(join24(tmpdir5(), "archme-doctor-"));
|
|
20410
|
+
const mcpConfigPath = join24(dir, "mcp.json");
|
|
20411
|
+
await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
|
|
20412
|
+
try {
|
|
20413
|
+
const result = await spawnClaude({
|
|
20414
|
+
prompt: buildDoctorPrompt(facts, issues),
|
|
20415
|
+
cwd: facts.repoRoot ?? process.cwd(),
|
|
20416
|
+
mcpConfigPath,
|
|
20417
|
+
interactive: true,
|
|
20418
|
+
// Read-only, and no MCP: doctor diagnoses, it does not repair. An LLM
|
|
20419
|
+
// with write or shell access on a machine whose setup is already broken
|
|
20420
|
+
// is a much larger surface than the problem justifies.
|
|
20421
|
+
allowedTools: ["Read", "Glob", "Grep"]
|
|
20422
|
+
});
|
|
20423
|
+
return result.exitCode === 0 ? EX_OK4 : EX_INFRA4;
|
|
20424
|
+
} finally {
|
|
20425
|
+
await rm9(dir, { recursive: true, force: true });
|
|
20426
|
+
}
|
|
20427
|
+
}
|
|
20428
|
+
var EX_OK4, EX_INFRA4, EX_USAGE4;
|
|
20429
|
+
var init_dispatch5 = __esm({
|
|
20430
|
+
"src/doctor/dispatch.ts"() {
|
|
20431
|
+
"use strict";
|
|
20432
|
+
init_collect();
|
|
20433
|
+
init_facts();
|
|
20434
|
+
init_prompt2();
|
|
20435
|
+
init_spawn_claude();
|
|
20436
|
+
init_preflight();
|
|
20437
|
+
EX_OK4 = 0;
|
|
20438
|
+
EX_INFRA4 = 3;
|
|
20439
|
+
EX_USAGE4 = 78;
|
|
20440
|
+
}
|
|
20441
|
+
});
|
|
20442
|
+
|
|
18177
20443
|
// src/auth/logout.ts
|
|
18178
20444
|
var logout_exports = {};
|
|
18179
20445
|
__export(logout_exports, {
|
|
@@ -18187,12 +20453,12 @@ async function runLogout(args) {
|
|
|
18187
20453
|
let url;
|
|
18188
20454
|
try {
|
|
18189
20455
|
url = resolveConfig().supabaseUrl;
|
|
18190
|
-
} catch (
|
|
18191
|
-
if (
|
|
18192
|
-
console.error(
|
|
20456
|
+
} catch (err2) {
|
|
20457
|
+
if (err2 instanceof MissingConfigError) {
|
|
20458
|
+
console.error(err2.message);
|
|
18193
20459
|
return 1;
|
|
18194
20460
|
}
|
|
18195
|
-
throw
|
|
20461
|
+
throw err2;
|
|
18196
20462
|
}
|
|
18197
20463
|
const target = args.find((a2) => !a2.startsWith("-"));
|
|
18198
20464
|
if (!target || args.includes("--all")) {
|
|
@@ -18220,10 +20486,22 @@ var init_logout = __esm({
|
|
|
18220
20486
|
|
|
18221
20487
|
// src/cli.ts
|
|
18222
20488
|
init_enroll();
|
|
18223
|
-
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
20489
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
20490
|
+
"daemon",
|
|
20491
|
+
"analyze",
|
|
20492
|
+
"login",
|
|
20493
|
+
"logout",
|
|
20494
|
+
"keys",
|
|
20495
|
+
"scan",
|
|
20496
|
+
"init",
|
|
20497
|
+
"local-watch",
|
|
20498
|
+
"doctor"
|
|
20499
|
+
]);
|
|
20500
|
+
var VERSION_FLAGS = /* @__PURE__ */ new Set(["--version", "-v"]);
|
|
18224
20501
|
function route(argv) {
|
|
18225
20502
|
if (argv.length === 0) return { name: "help", args: [] };
|
|
18226
20503
|
const [first, ...rest] = argv;
|
|
20504
|
+
if (first && VERSION_FLAGS.has(first)) return { name: "version", args: [] };
|
|
18227
20505
|
if (!first || !KNOWN_COMMANDS.has(first)) {
|
|
18228
20506
|
return { name: "help", args: [] };
|
|
18229
20507
|
}
|
|
@@ -18287,6 +20565,18 @@ async function main(argv) {
|
|
|
18287
20565
|
const { dispatchDaemon: dispatchDaemon2 } = await Promise.resolve().then(() => (init_dispatch2(), dispatch_exports2));
|
|
18288
20566
|
return await dispatchDaemon2(sub);
|
|
18289
20567
|
}
|
|
20568
|
+
case "version": {
|
|
20569
|
+
const { readDaemonVersion: readDaemonVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
|
|
20570
|
+
const v = await readDaemonVersion2();
|
|
20571
|
+
if (!v) {
|
|
20572
|
+
console.error(
|
|
20573
|
+
"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`."
|
|
20574
|
+
);
|
|
20575
|
+
return 1;
|
|
20576
|
+
}
|
|
20577
|
+
console.log(v);
|
|
20578
|
+
return 0;
|
|
20579
|
+
}
|
|
18290
20580
|
case "login":
|
|
18291
20581
|
await runLogin();
|
|
18292
20582
|
return 0;
|
|
@@ -18302,6 +20592,14 @@ async function main(argv) {
|
|
|
18302
20592
|
const { dispatchInit: dispatchInit2 } = await Promise.resolve().then(() => (init_dispatch4(), dispatch_exports4));
|
|
18303
20593
|
return await dispatchInit2(r.args);
|
|
18304
20594
|
}
|
|
20595
|
+
case "local-watch": {
|
|
20596
|
+
const { dispatchLocalWatch: dispatchLocalWatch2 } = await Promise.resolve().then(() => (init_localWatchDispatch(), localWatchDispatch_exports));
|
|
20597
|
+
return await dispatchLocalWatch2(r.args);
|
|
20598
|
+
}
|
|
20599
|
+
case "doctor": {
|
|
20600
|
+
const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
|
|
20601
|
+
return await dispatchDoctor2(r.args);
|
|
20602
|
+
}
|
|
18305
20603
|
case "logout": {
|
|
18306
20604
|
const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
18307
20605
|
return await runLogout2(r.args);
|
|
@@ -18324,7 +20622,12 @@ Other commands:
|
|
|
18324
20622
|
disable|install|uninstall|run)
|
|
18325
20623
|
archme analyze <subcommand> Code analysis (outline|repo|diff|match|prefetch)
|
|
18326
20624
|
archme scan <repo-path> Single-pass scan into an explicit project/branch
|
|
20625
|
+
archme local-watch [repo-path] Watch a repo without a GitHub App
|
|
20626
|
+
(--list|--stop|--resume)
|
|
20627
|
+
archme doctor Check this machine's setup and explain it
|
|
18327
20628
|
archme login | logout Authenticate with archme
|
|
20629
|
+
archme keys list|create|revoke Manage MCP API keys
|
|
20630
|
+
archme --version Print the installed version
|
|
18328
20631
|
|
|
18329
20632
|
Run 'archme init --help', 'archme daemon --help', 'archme analyze --help', or
|
|
18330
20633
|
'archme scan --help' for subcommand help.`);
|