@archme/cli 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2374 -569
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -720,11 +720,11 @@ var hasColors, format, reset, bold, dim, italic, underline, overline, inverse, h
|
|
|
720
720
|
var init_base = __esm({
|
|
721
721
|
"node_modules/yoctocolors/base.js"() {
|
|
722
722
|
hasColors = tty?.WriteStream?.prototype?.hasColors?.() ?? false;
|
|
723
|
-
format = (
|
|
723
|
+
format = (open3, close) => {
|
|
724
724
|
if (!hasColors) {
|
|
725
725
|
return (input) => input;
|
|
726
726
|
}
|
|
727
|
-
const openCode = `\x1B[${
|
|
727
|
+
const openCode = `\x1B[${open3}m`;
|
|
728
728
|
const closeCode = `\x1B[${close}m`;
|
|
729
729
|
return (input) => {
|
|
730
730
|
const string = input + "";
|
|
@@ -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, short5, valueString) => {
|
|
4061
|
+
if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation && short5 === "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) : short5;
|
|
4071
4071
|
}
|
|
4072
4072
|
result.push(valueString);
|
|
4073
4073
|
};
|
|
@@ -8293,9 +8293,11 @@ var credentials_exports = {};
|
|
|
8293
8293
|
__export(credentials_exports, {
|
|
8294
8294
|
clearCredential: () => clearCredential,
|
|
8295
8295
|
clearCredentials: () => clearCredentials,
|
|
8296
|
+
credentialKind: () => credentialKind,
|
|
8296
8297
|
describeCredentialSource: () => describeCredentialSource,
|
|
8297
8298
|
loadAllCredentials: () => loadAllCredentials,
|
|
8298
8299
|
loadCredentials: () => loadCredentials,
|
|
8300
|
+
loadKeytar: () => loadKeytar,
|
|
8299
8301
|
replaceUrlCredentials: () => replaceUrlCredentials,
|
|
8300
8302
|
saveCredentials: () => saveCredentials
|
|
8301
8303
|
});
|
|
@@ -8312,6 +8314,9 @@ function withStoreLock(fn) {
|
|
|
8312
8314
|
});
|
|
8313
8315
|
return run;
|
|
8314
8316
|
}
|
|
8317
|
+
function credentialKind(c3) {
|
|
8318
|
+
return c3.kind === "key" ? "key" : "oauth";
|
|
8319
|
+
}
|
|
8315
8320
|
function isStored(x) {
|
|
8316
8321
|
if (!x || typeof x !== "object") return false;
|
|
8317
8322
|
const c3 = x;
|
|
@@ -8319,6 +8324,9 @@ function isStored(x) {
|
|
|
8319
8324
|
if (c3.bindings !== void 0 && !Array.isArray(c3.bindings)) {
|
|
8320
8325
|
c3.bindings = void 0;
|
|
8321
8326
|
}
|
|
8327
|
+
if (c3.kind !== void 0 && c3.kind !== "oauth" && c3.kind !== "key") {
|
|
8328
|
+
delete c3.kind;
|
|
8329
|
+
}
|
|
8322
8330
|
return true;
|
|
8323
8331
|
}
|
|
8324
8332
|
function parseStore(raw) {
|
|
@@ -8361,9 +8369,10 @@ async function loadCredentials(supabaseUrl) {
|
|
|
8361
8369
|
return { credentials: list[list.length - 1] ?? null, source, path: path7 };
|
|
8362
8370
|
}
|
|
8363
8371
|
function mergeBindings(incoming, prior) {
|
|
8364
|
-
|
|
8365
|
-
if (
|
|
8366
|
-
return
|
|
8372
|
+
const withKind = incoming.kind === void 0 && prior?.kind !== void 0 ? { ...incoming, kind: prior.kind } : incoming;
|
|
8373
|
+
if (withKind.bindings?.length) return withKind;
|
|
8374
|
+
if (prior?.bindings?.length) return { ...withKind, bindings: prior.bindings };
|
|
8375
|
+
return withKind;
|
|
8367
8376
|
}
|
|
8368
8377
|
async function saveCredentials(credentials) {
|
|
8369
8378
|
return withStoreLock(async () => {
|
|
@@ -8480,6 +8489,156 @@ var init_credentials = __esm({
|
|
|
8480
8489
|
}
|
|
8481
8490
|
});
|
|
8482
8491
|
|
|
8492
|
+
// src/auth/keyRing.ts
|
|
8493
|
+
var keyRing_exports = {};
|
|
8494
|
+
__export(keyRing_exports, {
|
|
8495
|
+
KeyRingUnreadableError: () => KeyRingUnreadableError,
|
|
8496
|
+
findKey: () => findKey,
|
|
8497
|
+
forgetKey: () => forgetKey,
|
|
8498
|
+
keyRingLockPath: () => keyRingLockPath,
|
|
8499
|
+
keyRingPath: () => keyRingPath,
|
|
8500
|
+
listKeys: () => listKeys,
|
|
8501
|
+
saveKey: () => saveKey
|
|
8502
|
+
});
|
|
8503
|
+
import { randomBytes } from "node:crypto";
|
|
8504
|
+
import { chmod as chmod2, mkdir as mkdir2, open, readFile as readFile2, rename, rm as rm2 } from "node:fs/promises";
|
|
8505
|
+
import { homedir as homedir2 } from "node:os";
|
|
8506
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
8507
|
+
import { lock as lockFile } from "proper-lockfile";
|
|
8508
|
+
function keyRingPath() {
|
|
8509
|
+
return process.env.ARCHME_KEYS_PATH ?? join2(homedir2(), ".archme", "keys.json");
|
|
8510
|
+
}
|
|
8511
|
+
function keyRingLockPath() {
|
|
8512
|
+
return join2(dirname2(keyRingPath()), "keys.lock");
|
|
8513
|
+
}
|
|
8514
|
+
async function withFileLock(fn) {
|
|
8515
|
+
const dir = dirname2(keyRingPath());
|
|
8516
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
8517
|
+
const release = await lockFile(dir, {
|
|
8518
|
+
lockfilePath: keyRingLockPath(),
|
|
8519
|
+
realpath: false,
|
|
8520
|
+
stale: 1e4,
|
|
8521
|
+
retries: { retries: 100, minTimeout: 20, maxTimeout: 250 },
|
|
8522
|
+
// The default throws from a timer, which kills the process mid-command.
|
|
8523
|
+
onCompromised: () => {
|
|
8524
|
+
}
|
|
8525
|
+
});
|
|
8526
|
+
try {
|
|
8527
|
+
return await fn();
|
|
8528
|
+
} finally {
|
|
8529
|
+
await release().catch(() => {
|
|
8530
|
+
});
|
|
8531
|
+
}
|
|
8532
|
+
}
|
|
8533
|
+
function withRingLock(fn) {
|
|
8534
|
+
const locked = () => withFileLock(fn);
|
|
8535
|
+
const run = ringMutex.then(locked, locked);
|
|
8536
|
+
ringMutex = run.then(() => {
|
|
8537
|
+
}, () => {
|
|
8538
|
+
});
|
|
8539
|
+
return run;
|
|
8540
|
+
}
|
|
8541
|
+
function isSavedKey(x) {
|
|
8542
|
+
if (!x || typeof x !== "object") return false;
|
|
8543
|
+
const k = x;
|
|
8544
|
+
return typeof k.user_id === "string" && !!k.user_id && typeof k.key === "string" && !!k.key && typeof k.handle === "string" && typeof k.saved_at === "string";
|
|
8545
|
+
}
|
|
8546
|
+
async function readEntries() {
|
|
8547
|
+
const where = keyRingPath();
|
|
8548
|
+
let raw;
|
|
8549
|
+
try {
|
|
8550
|
+
raw = await readFile2(where, "utf8");
|
|
8551
|
+
} catch (err2) {
|
|
8552
|
+
if (err2.code === "ENOENT") return [];
|
|
8553
|
+
throw new KeyRingUnreadableError(where, err2.message);
|
|
8554
|
+
}
|
|
8555
|
+
let doc;
|
|
8556
|
+
try {
|
|
8557
|
+
doc = JSON.parse(raw);
|
|
8558
|
+
} catch {
|
|
8559
|
+
throw new KeyRingUnreadableError(where, raw === "" ? "it is empty" : "it is not valid JSON");
|
|
8560
|
+
}
|
|
8561
|
+
const d = doc;
|
|
8562
|
+
if (!d || typeof d !== "object" || !Array.isArray(d.keys)) {
|
|
8563
|
+
throw new KeyRingUnreadableError(where, "it is not a key ring");
|
|
8564
|
+
}
|
|
8565
|
+
if (d.v !== 1) throw new KeyRingUnreadableError(where, `it is version ${String(d.v)}; this archme reads version 1`);
|
|
8566
|
+
return d.keys;
|
|
8567
|
+
}
|
|
8568
|
+
function isFileEntry(k) {
|
|
8569
|
+
return isSavedKey(k) && typeof k.supabase_url === "string";
|
|
8570
|
+
}
|
|
8571
|
+
function isFor(k, supabaseUrl, userId) {
|
|
8572
|
+
if (!k || typeof k !== "object") return false;
|
|
8573
|
+
const e = k;
|
|
8574
|
+
return e.user_id === userId && e.supabase_url === supabaseUrl;
|
|
8575
|
+
}
|
|
8576
|
+
async function writeEntries(entries) {
|
|
8577
|
+
const path7 = keyRingPath();
|
|
8578
|
+
if (entries.length === 0) {
|
|
8579
|
+
await rm2(path7, { force: true });
|
|
8580
|
+
return;
|
|
8581
|
+
}
|
|
8582
|
+
await mkdir2(dirname2(path7), { recursive: true, mode: 448 });
|
|
8583
|
+
await chmod2(dirname2(path7), 448).catch(() => {
|
|
8584
|
+
});
|
|
8585
|
+
const tmp = join2(dirname2(path7), `.keys.json.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
8586
|
+
try {
|
|
8587
|
+
const fh = await open(tmp, "wx", 384);
|
|
8588
|
+
try {
|
|
8589
|
+
await fh.writeFile(JSON.stringify({ v: 1, keys: entries }, null, 2));
|
|
8590
|
+
await fh.chmod(384);
|
|
8591
|
+
await fh.sync();
|
|
8592
|
+
} finally {
|
|
8593
|
+
await fh.close();
|
|
8594
|
+
}
|
|
8595
|
+
await rename(tmp, path7);
|
|
8596
|
+
} catch (err2) {
|
|
8597
|
+
await rm2(tmp, { force: true }).catch(() => {
|
|
8598
|
+
});
|
|
8599
|
+
throw err2;
|
|
8600
|
+
}
|
|
8601
|
+
}
|
|
8602
|
+
function strip(e) {
|
|
8603
|
+
return { user_id: e.user_id, key: e.key, handle: e.handle, saved_at: e.saved_at };
|
|
8604
|
+
}
|
|
8605
|
+
async function listKeys(supabaseUrl) {
|
|
8606
|
+
return (await readEntries()).filter(isFileEntry).filter((e) => e.supabase_url === supabaseUrl).map(strip);
|
|
8607
|
+
}
|
|
8608
|
+
async function findKey(supabaseUrl, userId) {
|
|
8609
|
+
return (await listKeys(supabaseUrl)).find((k) => k.user_id === userId) ?? null;
|
|
8610
|
+
}
|
|
8611
|
+
async function saveKey(supabaseUrl, toSave) {
|
|
8612
|
+
const saved = { ...toSave, saved_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
8613
|
+
return withRingLock(async () => {
|
|
8614
|
+
const others = (await readEntries()).filter((k) => !isFor(k, supabaseUrl, saved.user_id));
|
|
8615
|
+
await writeEntries([...others, { ...saved, supabase_url: supabaseUrl }]);
|
|
8616
|
+
return saved;
|
|
8617
|
+
});
|
|
8618
|
+
}
|
|
8619
|
+
async function forgetKey(supabaseUrl, userId) {
|
|
8620
|
+
return withRingLock(async () => {
|
|
8621
|
+
const all = await readEntries();
|
|
8622
|
+
const remaining = all.filter((e) => !isFor(e, supabaseUrl, userId));
|
|
8623
|
+
if (remaining.length === all.length) return false;
|
|
8624
|
+
await writeEntries(remaining);
|
|
8625
|
+
return true;
|
|
8626
|
+
});
|
|
8627
|
+
}
|
|
8628
|
+
var ringMutex, KeyRingUnreadableError;
|
|
8629
|
+
var init_keyRing = __esm({
|
|
8630
|
+
"src/auth/keyRing.ts"() {
|
|
8631
|
+
"use strict";
|
|
8632
|
+
ringMutex = Promise.resolve();
|
|
8633
|
+
KeyRingUnreadableError = class extends Error {
|
|
8634
|
+
constructor(where, why) {
|
|
8635
|
+
super(`Your saved ArchMe keys in ${where} could not be read (${why}). Nothing was written. Fix or move it aside, then try again.`);
|
|
8636
|
+
this.name = "KeyRingUnreadableError";
|
|
8637
|
+
}
|
|
8638
|
+
};
|
|
8639
|
+
}
|
|
8640
|
+
});
|
|
8641
|
+
|
|
8483
8642
|
// src/config/defaults.ts
|
|
8484
8643
|
var defaults_exports = {};
|
|
8485
8644
|
__export(defaults_exports, {
|
|
@@ -8489,8 +8648,8 @@ __export(defaults_exports, {
|
|
|
8489
8648
|
writeLocalConfig: () => writeLocalConfig
|
|
8490
8649
|
});
|
|
8491
8650
|
import { existsSync, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8492
|
-
import { homedir as
|
|
8493
|
-
import { dirname as
|
|
8651
|
+
import { homedir as homedir3 } from "node:os";
|
|
8652
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
8494
8653
|
function readLocalConfig() {
|
|
8495
8654
|
if (!existsSync(LOCAL_CONFIG_PATH)) return {};
|
|
8496
8655
|
try {
|
|
@@ -8530,7 +8689,7 @@ function resolveConfig() {
|
|
|
8530
8689
|
};
|
|
8531
8690
|
}
|
|
8532
8691
|
function writeLocalConfig(config2) {
|
|
8533
|
-
mkdirSync(
|
|
8692
|
+
mkdirSync(dirname3(LOCAL_CONFIG_PATH), { recursive: true });
|
|
8534
8693
|
writeFileSync2(LOCAL_CONFIG_PATH, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
|
|
8535
8694
|
return LOCAL_CONFIG_PATH;
|
|
8536
8695
|
}
|
|
@@ -8554,7 +8713,7 @@ var init_defaults = __esm({
|
|
|
8554
8713
|
// ~/.archme/local-config.json (written by setup:local-daemon-oauth).
|
|
8555
8714
|
}
|
|
8556
8715
|
};
|
|
8557
|
-
LOCAL_CONFIG_PATH =
|
|
8716
|
+
LOCAL_CONFIG_PATH = join3(homedir3(), ".archme", "local-config.json");
|
|
8558
8717
|
MissingConfigError = class extends Error {
|
|
8559
8718
|
archmeEnv;
|
|
8560
8719
|
missing;
|
|
@@ -8571,10 +8730,332 @@ var init_defaults = __esm({
|
|
|
8571
8730
|
}
|
|
8572
8731
|
});
|
|
8573
8732
|
|
|
8733
|
+
// ../shared/accountKey.ts
|
|
8734
|
+
function keyedAccountEmail(handle) {
|
|
8735
|
+
return `${handle}@${KEYED_EMAIL_DOMAIN}`;
|
|
8736
|
+
}
|
|
8737
|
+
function isKeyedAccountEmail(email) {
|
|
8738
|
+
return !!email && email.toLowerCase().endsWith(`@${KEYED_EMAIL_DOMAIN}`);
|
|
8739
|
+
}
|
|
8740
|
+
function formatAccountKey(handle, secret) {
|
|
8741
|
+
return `${ACCOUNT_KEY_PREFIX}${handle}_${secret}`;
|
|
8742
|
+
}
|
|
8743
|
+
function parseAccountKey(raw) {
|
|
8744
|
+
const m = KEY_RE.exec((raw ?? "").trim());
|
|
8745
|
+
if (!m) return null;
|
|
8746
|
+
const handle = m[1];
|
|
8747
|
+
return { handle, secret: m[2], email: keyedAccountEmail(handle) };
|
|
8748
|
+
}
|
|
8749
|
+
var KEYED_EMAIL_DOMAIN, ACCOUNT_KEY_PREFIX, KEY_RE;
|
|
8750
|
+
var init_accountKey = __esm({
|
|
8751
|
+
"../shared/accountKey.ts"() {
|
|
8752
|
+
KEYED_EMAIL_DOMAIN = "keys.archme.io";
|
|
8753
|
+
ACCOUNT_KEY_PREFIX = "amk_";
|
|
8754
|
+
KEY_RE = /^amk_([a-z0-9](?:[a-z0-9-]*[a-z0-9])?-[0-9a-f]{6})_([A-Za-z0-9_-]{43})$/;
|
|
8755
|
+
}
|
|
8756
|
+
});
|
|
8757
|
+
|
|
8758
|
+
// src/scan/oauthClient.ts
|
|
8759
|
+
var oauthClient_exports = {};
|
|
8760
|
+
__export(oauthClient_exports, {
|
|
8761
|
+
DEFAULT_TOKEN_EXPIRES_IN_SECONDS: () => DEFAULT_TOKEN_EXPIRES_IN_SECONDS,
|
|
8762
|
+
LoadStoredAndRefreshError: () => LoadStoredAndRefreshError,
|
|
8763
|
+
REFRESH_RETRY_BACKOFF_MS: () => REFRESH_RETRY_BACKOFF_MS,
|
|
8764
|
+
computeExpiresAt: () => computeExpiresAt,
|
|
8765
|
+
fetchWithAuthRetry: () => fetchWithAuthRetry,
|
|
8766
|
+
getOAuthUserInfoWith: () => getOAuthUserInfoWith,
|
|
8767
|
+
isAuthError: () => isAuthError,
|
|
8768
|
+
isPermanentRefreshError: () => isPermanentRefreshError,
|
|
8769
|
+
parseJsonResponse: () => parseJsonResponse,
|
|
8770
|
+
refreshOAuthCredentialsWith: () => refreshOAuthCredentialsWith,
|
|
8771
|
+
resolveUserId: () => resolveUserId,
|
|
8772
|
+
responseError: () => responseError,
|
|
8773
|
+
withAuthRetry: () => withAuthRetry
|
|
8774
|
+
});
|
|
8775
|
+
function computeExpiresAt(expiresInSeconds) {
|
|
8776
|
+
const seconds = expiresInSeconds ?? DEFAULT_TOKEN_EXPIRES_IN_SECONDS;
|
|
8777
|
+
return Date.now() + seconds * 1e3;
|
|
8778
|
+
}
|
|
8779
|
+
function isPermanentRefreshError(body, status) {
|
|
8780
|
+
if (status >= 500) return false;
|
|
8781
|
+
if (body.error === "invalid_grant") return true;
|
|
8782
|
+
const errorCode = typeof body.error_code === "string" ? body.error_code.toLowerCase() : "";
|
|
8783
|
+
if (errorCode.startsWith("refresh_token")) return true;
|
|
8784
|
+
const message = [body.error_description, body.msg, body.error, body.message].filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
8785
|
+
return /invalid refresh token|refresh token not found|refresh token already used/.test(message);
|
|
8786
|
+
}
|
|
8787
|
+
async function refreshOAuthCredentialsWith(refreshToken, cfg) {
|
|
8788
|
+
let response;
|
|
8789
|
+
try {
|
|
8790
|
+
response = await fetch(`${cfg.supabaseUrl}/auth/v1/oauth/token`, {
|
|
8791
|
+
method: "POST",
|
|
8792
|
+
headers: {
|
|
8793
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
8794
|
+
apikey: cfg.anonKey
|
|
8795
|
+
},
|
|
8796
|
+
body: new URLSearchParams({
|
|
8797
|
+
grant_type: "refresh_token",
|
|
8798
|
+
refresh_token: refreshToken,
|
|
8799
|
+
client_id: cfg.clientId
|
|
8800
|
+
})
|
|
8801
|
+
});
|
|
8802
|
+
} catch (err2) {
|
|
8803
|
+
return {
|
|
8804
|
+
ok: false,
|
|
8805
|
+
permanent: false,
|
|
8806
|
+
error: `network error: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
8807
|
+
};
|
|
8808
|
+
}
|
|
8809
|
+
const body = await parseJsonResponse(response);
|
|
8810
|
+
if (!response.ok) {
|
|
8811
|
+
return {
|
|
8812
|
+
ok: false,
|
|
8813
|
+
permanent: isPermanentRefreshError(body, response.status),
|
|
8814
|
+
error: responseError(body, `refresh failed (${response.status})`)
|
|
8815
|
+
};
|
|
8816
|
+
}
|
|
8817
|
+
if (typeof body.access_token !== "string" || !body.access_token) {
|
|
8818
|
+
return { ok: false, permanent: false, error: "refresh response did not include an access token" };
|
|
8819
|
+
}
|
|
8820
|
+
return {
|
|
8821
|
+
ok: true,
|
|
8822
|
+
tokens: {
|
|
8823
|
+
access_token: body.access_token,
|
|
8824
|
+
refresh_token: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
|
|
8825
|
+
expires_in: typeof body.expires_in === "number" ? body.expires_in : Number(body.expires_in) || void 0,
|
|
8826
|
+
token_type: typeof body.token_type === "string" ? body.token_type : void 0
|
|
8827
|
+
}
|
|
8828
|
+
};
|
|
8829
|
+
}
|
|
8830
|
+
async function getOAuthUserInfoWith(accessToken, cfg) {
|
|
8831
|
+
const response = await fetch(`${cfg.supabaseUrl}/auth/v1/oauth/userinfo`, {
|
|
8832
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
8833
|
+
});
|
|
8834
|
+
if (!response.ok) return {};
|
|
8835
|
+
return await parseJsonResponse(response);
|
|
8836
|
+
}
|
|
8837
|
+
function resolveUserId(userInfo2, storedUserId) {
|
|
8838
|
+
return userInfo2.sub ?? storedUserId ?? null;
|
|
8839
|
+
}
|
|
8840
|
+
function isAuthError(err2) {
|
|
8841
|
+
if (!err2 || typeof err2 !== "object") return false;
|
|
8842
|
+
const msg = ("message" in err2 ? String(err2.message) : "").toLowerCase();
|
|
8843
|
+
const code = ("code" in err2 ? String(err2.code) : "").toLowerCase();
|
|
8844
|
+
const status = "status" in err2 ? Number(err2.status) : 0;
|
|
8845
|
+
return status === 401 || code === "pgrst301" || msg.includes("jwt expired") || msg.includes("invalid jwt") || msg.includes("not authenticated");
|
|
8846
|
+
}
|
|
8847
|
+
async function withAuthRetry(label, fn, reLogin) {
|
|
8848
|
+
const first = await fn();
|
|
8849
|
+
if (!first.error || !isAuthError(first.error)) return first;
|
|
8850
|
+
console.warn(`${label}: auth error on first attempt, re-logging in and retrying once`);
|
|
8851
|
+
try {
|
|
8852
|
+
await reLogin();
|
|
8853
|
+
} catch (err2) {
|
|
8854
|
+
console.error(`${label}: re-login failed:`, err2);
|
|
8855
|
+
return first;
|
|
8856
|
+
}
|
|
8857
|
+
return await fn();
|
|
8858
|
+
}
|
|
8859
|
+
async function fetchWithAuthRetry(label, doFetch, reLogin) {
|
|
8860
|
+
const first = await doFetch();
|
|
8861
|
+
if (first.status !== 401) return first;
|
|
8862
|
+
console.warn(`${label}: 401 on first attempt, re-logging in and retrying once`);
|
|
8863
|
+
try {
|
|
8864
|
+
await reLogin();
|
|
8865
|
+
} catch (err2) {
|
|
8866
|
+
console.error(`${label}: re-login failed:`, err2);
|
|
8867
|
+
return first;
|
|
8868
|
+
}
|
|
8869
|
+
return await doFetch();
|
|
8870
|
+
}
|
|
8871
|
+
async function parseJsonResponse(response) {
|
|
8872
|
+
const text = await response.text();
|
|
8873
|
+
if (!text) return {};
|
|
8874
|
+
try {
|
|
8875
|
+
return JSON.parse(text);
|
|
8876
|
+
} catch {
|
|
8877
|
+
return { error: text, error_description: text, msg: text };
|
|
8878
|
+
}
|
|
8879
|
+
}
|
|
8880
|
+
function responseError(body, fallback) {
|
|
8881
|
+
for (const key of ["error_description", "error", "msg"]) {
|
|
8882
|
+
const value = body[key];
|
|
8883
|
+
if (typeof value === "string" && value) return value;
|
|
8884
|
+
}
|
|
8885
|
+
return fallback;
|
|
8886
|
+
}
|
|
8887
|
+
var DEFAULT_TOKEN_EXPIRES_IN_SECONDS, REFRESH_RETRY_BACKOFF_MS, LoadStoredAndRefreshError;
|
|
8888
|
+
var init_oauthClient = __esm({
|
|
8889
|
+
"src/scan/oauthClient.ts"() {
|
|
8890
|
+
"use strict";
|
|
8891
|
+
DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;
|
|
8892
|
+
REFRESH_RETRY_BACKOFF_MS = 6e4;
|
|
8893
|
+
LoadStoredAndRefreshError = class extends Error {
|
|
8894
|
+
code;
|
|
8895
|
+
permanent;
|
|
8896
|
+
constructor(code, message) {
|
|
8897
|
+
super(message);
|
|
8898
|
+
this.name = "LoadStoredAndRefreshError";
|
|
8899
|
+
this.code = code;
|
|
8900
|
+
this.permanent = code !== "transient";
|
|
8901
|
+
}
|
|
8902
|
+
};
|
|
8903
|
+
}
|
|
8904
|
+
});
|
|
8905
|
+
|
|
8906
|
+
// src/scan/keySession.ts
|
|
8907
|
+
function isRejectedKeyError(body, status) {
|
|
8908
|
+
if (status >= 500 || status === 429) return false;
|
|
8909
|
+
if (body.error === "invalid_grant") return true;
|
|
8910
|
+
const code = typeof body.error_code === "string" ? body.error_code.toLowerCase() : "";
|
|
8911
|
+
if (REJECTED_CODES.has(code)) return true;
|
|
8912
|
+
const message = [body.error_description, body.msg, body.message].filter((v) => typeof v === "string").join(" ");
|
|
8913
|
+
return /invalid login credentials/i.test(message);
|
|
8914
|
+
}
|
|
8915
|
+
async function tokenGrant(grant, body, cfg, isPermanent) {
|
|
8916
|
+
let response;
|
|
8917
|
+
try {
|
|
8918
|
+
response = await fetch(`${cfg.supabaseUrl}/auth/v1/token?grant_type=${grant}`, {
|
|
8919
|
+
method: "POST",
|
|
8920
|
+
headers: { "Content-Type": "application/json", apikey: cfg.anonKey },
|
|
8921
|
+
body: JSON.stringify(body)
|
|
8922
|
+
});
|
|
8923
|
+
} catch (err2) {
|
|
8924
|
+
return { ok: false, permanent: false, error: `network error: ${err2 instanceof Error ? err2.message : String(err2)}` };
|
|
8925
|
+
}
|
|
8926
|
+
const parsed = await parseJsonResponse(response);
|
|
8927
|
+
if (!response.ok) {
|
|
8928
|
+
return {
|
|
8929
|
+
ok: false,
|
|
8930
|
+
permanent: isPermanent(parsed, response.status),
|
|
8931
|
+
error: responseError(parsed, `${grant} grant failed (${response.status})`)
|
|
8932
|
+
};
|
|
8933
|
+
}
|
|
8934
|
+
if (typeof parsed.access_token !== "string" || !parsed.access_token) {
|
|
8935
|
+
return { ok: false, permanent: false, error: "token response did not include an access token" };
|
|
8936
|
+
}
|
|
8937
|
+
const user = parsed.user;
|
|
8938
|
+
return {
|
|
8939
|
+
ok: true,
|
|
8940
|
+
tokens: {
|
|
8941
|
+
access_token: parsed.access_token,
|
|
8942
|
+
refresh_token: typeof parsed.refresh_token === "string" ? parsed.refresh_token : void 0,
|
|
8943
|
+
expires_in: typeof parsed.expires_in === "number" ? parsed.expires_in : Number(parsed.expires_in) || void 0,
|
|
8944
|
+
token_type: typeof parsed.token_type === "string" ? parsed.token_type : void 0
|
|
8945
|
+
},
|
|
8946
|
+
...typeof user?.id === "string" ? { userId: user.id } : {}
|
|
8947
|
+
};
|
|
8948
|
+
}
|
|
8949
|
+
function passwordGrant(email, secret, cfg) {
|
|
8950
|
+
return tokenGrant("password", { email, password: secret }, cfg, isRejectedKeyError);
|
|
8951
|
+
}
|
|
8952
|
+
function refreshKeySession(refreshToken, cfg) {
|
|
8953
|
+
return tokenGrant("refresh_token", { refresh_token: refreshToken }, cfg, isPermanentRefreshError);
|
|
8954
|
+
}
|
|
8955
|
+
async function fetchAuthUser(accessToken, cfg) {
|
|
8956
|
+
try {
|
|
8957
|
+
const response = await fetch(`${cfg.supabaseUrl}/auth/v1/user`, {
|
|
8958
|
+
headers: { Authorization: `Bearer ${accessToken}`, apikey: cfg.anonKey }
|
|
8959
|
+
});
|
|
8960
|
+
if (!response.ok) return null;
|
|
8961
|
+
const body = await parseJsonResponse(response);
|
|
8962
|
+
return typeof body.id === "string" ? body : null;
|
|
8963
|
+
} catch {
|
|
8964
|
+
return null;
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8967
|
+
async function getKeyUserInfo(accessToken, cfg) {
|
|
8968
|
+
const user = await fetchAuthUser(accessToken, cfg);
|
|
8969
|
+
if (!user) return {};
|
|
8970
|
+
return { sub: user.id, ...user.email ? { email: user.email } : {} };
|
|
8971
|
+
}
|
|
8972
|
+
var REJECTED_CODES;
|
|
8973
|
+
var init_keySession = __esm({
|
|
8974
|
+
"src/scan/keySession.ts"() {
|
|
8975
|
+
"use strict";
|
|
8976
|
+
init_oauthClient();
|
|
8977
|
+
REJECTED_CODES = /* @__PURE__ */ new Set(["invalid_credentials", "user_banned", "user_not_found", "email_not_confirmed"]);
|
|
8978
|
+
}
|
|
8979
|
+
});
|
|
8980
|
+
|
|
8981
|
+
// src/daemon/prompt.ts
|
|
8982
|
+
var prompt_exports = {};
|
|
8983
|
+
__export(prompt_exports, {
|
|
8984
|
+
promptChoice: () => promptChoice,
|
|
8985
|
+
promptLine: () => promptLine,
|
|
8986
|
+
promptYesNo: () => promptYesNo,
|
|
8987
|
+
stdinIsInteractive: () => stdinIsInteractive
|
|
8988
|
+
});
|
|
8989
|
+
import { createInterface } from "node:readline/promises";
|
|
8990
|
+
function stdinIsInteractive() {
|
|
8991
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
8992
|
+
}
|
|
8993
|
+
async function withReadline(fn) {
|
|
8994
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8995
|
+
try {
|
|
8996
|
+
return await fn(rl);
|
|
8997
|
+
} finally {
|
|
8998
|
+
rl.close();
|
|
8999
|
+
}
|
|
9000
|
+
}
|
|
9001
|
+
async function promptLine(question) {
|
|
9002
|
+
return await withReadline(async (rl) => {
|
|
9003
|
+
try {
|
|
9004
|
+
return await rl.question(`${question}: `);
|
|
9005
|
+
} catch {
|
|
9006
|
+
return "";
|
|
9007
|
+
}
|
|
9008
|
+
});
|
|
9009
|
+
}
|
|
9010
|
+
async function promptChoice(question, options) {
|
|
9011
|
+
return await withReadline(async (rl) => {
|
|
9012
|
+
console.log(`
|
|
9013
|
+
${question}
|
|
9014
|
+
`);
|
|
9015
|
+
options.forEach((o2, i2) => console.log(` ${i2 + 1}. ${o2.label}
|
|
9016
|
+
`));
|
|
9017
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
9018
|
+
let answer;
|
|
9019
|
+
try {
|
|
9020
|
+
answer = (await rl.question(`Choose 1-${options.length} [1]: `)).trim();
|
|
9021
|
+
} catch {
|
|
9022
|
+
return options[0].value;
|
|
9023
|
+
}
|
|
9024
|
+
if (answer === "") return options[0].value;
|
|
9025
|
+
const n2 = Number.parseInt(answer, 10);
|
|
9026
|
+
if (Number.isInteger(n2) && n2 >= 1 && n2 <= options.length) return options[n2 - 1].value;
|
|
9027
|
+
console.log(` "${answer}" isn't one of the options.`);
|
|
9028
|
+
}
|
|
9029
|
+
return options[0].value;
|
|
9030
|
+
});
|
|
9031
|
+
}
|
|
9032
|
+
async function promptYesNo(question, opts = {}) {
|
|
9033
|
+
const defaultYes = opts.defaultYes ?? true;
|
|
9034
|
+
return await withReadline(async (rl) => {
|
|
9035
|
+
try {
|
|
9036
|
+
const answer = (await rl.question(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
|
|
9037
|
+
if (answer === "") return defaultYes;
|
|
9038
|
+
return answer === "y" || answer === "yes";
|
|
9039
|
+
} catch {
|
|
9040
|
+
return false;
|
|
9041
|
+
}
|
|
9042
|
+
});
|
|
9043
|
+
}
|
|
9044
|
+
var init_prompt = __esm({
|
|
9045
|
+
"src/daemon/prompt.ts"() {
|
|
9046
|
+
"use strict";
|
|
9047
|
+
}
|
|
9048
|
+
});
|
|
9049
|
+
|
|
8574
9050
|
// src/auth/enroll.ts
|
|
8575
9051
|
var enroll_exports = {};
|
|
8576
9052
|
__export(enroll_exports, {
|
|
9053
|
+
LoginError: () => LoginError,
|
|
9054
|
+
decideLoginMethod: () => decideLoginMethod,
|
|
9055
|
+
defaultDeviceName: () => defaultDeviceName,
|
|
9056
|
+
parseLoginArgs: () => parseLoginArgs,
|
|
8577
9057
|
reconcileEnrollment: () => reconcileEnrollment,
|
|
9058
|
+
runGitHubPairing: () => runGitHubPairing,
|
|
8578
9059
|
runLogin: () => runLogin
|
|
8579
9060
|
});
|
|
8580
9061
|
import { hostname, platform as platform2 } from "node:os";
|
|
@@ -8582,6 +9063,7 @@ async function reconcileEnrollment(supabaseUrl, poll) {
|
|
|
8582
9063
|
const bindings = poll.devices?.length ? poll.devices : [{ org_id: poll.org_id, device_id: poll.device_id }];
|
|
8583
9064
|
const primary = bindings[0];
|
|
8584
9065
|
const source = await replaceUrlCredentials(supabaseUrl, [{
|
|
9066
|
+
kind: "oauth",
|
|
8585
9067
|
refresh_token: poll.refresh_token,
|
|
8586
9068
|
access_token: poll.access_token,
|
|
8587
9069
|
org_id: primary.org_id,
|
|
@@ -8592,17 +9074,7 @@ async function reconcileEnrollment(supabaseUrl, poll) {
|
|
|
8592
9074
|
}]);
|
|
8593
9075
|
return { bindings, source };
|
|
8594
9076
|
}
|
|
8595
|
-
async function
|
|
8596
|
-
let SUPABASE_URL2;
|
|
8597
|
-
try {
|
|
8598
|
-
SUPABASE_URL2 = resolveConfig().supabaseUrl;
|
|
8599
|
-
} catch (err2) {
|
|
8600
|
-
if (err2 instanceof MissingConfigError) {
|
|
8601
|
-
console.error(err2.message);
|
|
8602
|
-
process.exit(1);
|
|
8603
|
-
}
|
|
8604
|
-
throw err2;
|
|
8605
|
-
}
|
|
9077
|
+
async function runGitHubPairing(SUPABASE_URL2) {
|
|
8606
9078
|
function functionsBaseUrl() {
|
|
8607
9079
|
return `${SUPABASE_URL2.replace(/\/$/, "")}/functions/v1/agents-device`;
|
|
8608
9080
|
}
|
|
@@ -8615,7 +9087,7 @@ async function runLogin() {
|
|
|
8615
9087
|
const data2 = await response.json();
|
|
8616
9088
|
return { status: response.status, data: data2 };
|
|
8617
9089
|
}
|
|
8618
|
-
const deviceName =
|
|
9090
|
+
const deviceName = defaultDeviceName();
|
|
8619
9091
|
const { status, data } = await postJson("/start", {
|
|
8620
9092
|
device_name: deviceName
|
|
8621
9093
|
});
|
|
@@ -8656,16 +9128,271 @@ async function runLogin() {
|
|
|
8656
9128
|
if ("error" in poll.data && poll.data.error === "expired") {
|
|
8657
9129
|
throw new Error("Pairing code expired. Run `archme login` again.");
|
|
8658
9130
|
}
|
|
8659
|
-
throw new Error(`Enrollment failed: ${JSON.stringify(poll.data)}`);
|
|
9131
|
+
throw new Error(`Enrollment failed: ${JSON.stringify(poll.data)}`);
|
|
9132
|
+
}
|
|
9133
|
+
throw new Error("Pairing code expired. Run `archme login` again.");
|
|
9134
|
+
}
|
|
9135
|
+
function parseLoginArgs(argv) {
|
|
9136
|
+
const opts = {};
|
|
9137
|
+
const setMethod = (m, flag) => {
|
|
9138
|
+
if (opts.method && opts.method !== m) return `${flag} cannot be combined with another sign-in option`;
|
|
9139
|
+
opts.method = m;
|
|
9140
|
+
return null;
|
|
9141
|
+
};
|
|
9142
|
+
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
9143
|
+
const arg = argv[i2];
|
|
9144
|
+
if (arg === "--no-open") continue;
|
|
9145
|
+
if (arg.startsWith("--key=")) {
|
|
9146
|
+
const err2 = setMethod("key", "--key");
|
|
9147
|
+
if (err2) return { ok: false, error: err2 };
|
|
9148
|
+
opts.key = arg.slice("--key=".length);
|
|
9149
|
+
continue;
|
|
9150
|
+
}
|
|
9151
|
+
const method = METHOD_FLAGS[arg];
|
|
9152
|
+
if (method) {
|
|
9153
|
+
const err2 = setMethod(method, arg);
|
|
9154
|
+
if (err2) return { ok: false, error: err2 };
|
|
9155
|
+
if (method === "key" && argv[i2 + 1] !== void 0 && !argv[i2 + 1].startsWith("-")) opts.key = argv[++i2];
|
|
9156
|
+
continue;
|
|
9157
|
+
}
|
|
9158
|
+
return { ok: false, error: `unknown option \`${arg}\`` };
|
|
9159
|
+
}
|
|
9160
|
+
return { ok: true, opts };
|
|
9161
|
+
}
|
|
9162
|
+
function decideLoginMethod(input) {
|
|
9163
|
+
if (input.opts.method) return { method: input.opts.method };
|
|
9164
|
+
if (input.envKey) return { method: "key" };
|
|
9165
|
+
const hasStored = input.hasActiveLogin || input.savedKeys > 0;
|
|
9166
|
+
if (input.interactive) return { ask: hasStored ? ["github", "key"] : ["no-account", "github", "key"] };
|
|
9167
|
+
if (!hasStored) return { method: "no-account" };
|
|
9168
|
+
if (!input.hasActiveLogin && input.savedKeys === 1) return { method: "key" };
|
|
9169
|
+
return { method: "github" };
|
|
9170
|
+
}
|
|
9171
|
+
function defaultDeviceName() {
|
|
9172
|
+
return process.env.ARCHME_DAEMON_DEVICE_NAME || hostname();
|
|
9173
|
+
}
|
|
9174
|
+
async function defaultLoginDeps() {
|
|
9175
|
+
let cfg;
|
|
9176
|
+
try {
|
|
9177
|
+
cfg = resolveConfig();
|
|
9178
|
+
} catch (err2) {
|
|
9179
|
+
if (err2 instanceof MissingConfigError) {
|
|
9180
|
+
console.error(err2.message);
|
|
9181
|
+
process.exit(1);
|
|
9182
|
+
}
|
|
9183
|
+
throw err2;
|
|
9184
|
+
}
|
|
9185
|
+
const { supabaseUrl, supabaseAnonKey } = cfg;
|
|
9186
|
+
const { promptChoice: promptChoice2, promptLine: promptLine2, stdinIsInteractive: stdinIsInteractive2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
|
|
9187
|
+
return {
|
|
9188
|
+
supabaseUrl,
|
|
9189
|
+
interactive: stdinIsInteractive2(),
|
|
9190
|
+
deviceName: defaultDeviceName(),
|
|
9191
|
+
hasActiveLogin: async () => (await loadAllCredentials(supabaseUrl)).credentials.length > 0,
|
|
9192
|
+
listKeys,
|
|
9193
|
+
saveKey,
|
|
9194
|
+
replaceCredentials: replaceUrlCredentials,
|
|
9195
|
+
passwordGrant: (email, secret) => passwordGrant(email, secret, { supabaseUrl, anonKey: supabaseAnonKey }),
|
|
9196
|
+
postAgentsDevice: async (path7, body, bearer) => {
|
|
9197
|
+
const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device${path7}`, {
|
|
9198
|
+
method: "POST",
|
|
9199
|
+
headers: {
|
|
9200
|
+
"Content-Type": "application/json",
|
|
9201
|
+
...bearer ? { Authorization: `Bearer ${bearer}` } : {}
|
|
9202
|
+
},
|
|
9203
|
+
body: JSON.stringify(body)
|
|
9204
|
+
});
|
|
9205
|
+
return { status: response.status, data: await parseJsonResponse(response) };
|
|
9206
|
+
},
|
|
9207
|
+
gitUserName: async () => {
|
|
9208
|
+
const r = await execa("git", ["config", "user.name"], { reject: false });
|
|
9209
|
+
return r.exitCode === 0 && r.stdout.trim() ? r.stdout.trim() : null;
|
|
9210
|
+
},
|
|
9211
|
+
choose: promptChoice2,
|
|
9212
|
+
promptLine: promptLine2,
|
|
9213
|
+
githubPairing: () => runGitHubPairing(supabaseUrl),
|
|
9214
|
+
log: (msg) => console.log(msg),
|
|
9215
|
+
envKey: process.env.ARCHME_KEY,
|
|
9216
|
+
keyRingPath: keyRingPath()
|
|
9217
|
+
};
|
|
9218
|
+
}
|
|
9219
|
+
async function runLogin(opts = {}, injected) {
|
|
9220
|
+
const base = injected ?? await defaultLoginDeps();
|
|
9221
|
+
const deps = opts.interactive === false ? { ...base, interactive: false } : base;
|
|
9222
|
+
const envKey = deps.envKey || void 0;
|
|
9223
|
+
const given = opts.method === "key" || !opts.method && envKey ? opts.key || envKey : void 0;
|
|
9224
|
+
const ring = opts.method === "github" || given ? await deps.listKeys(deps.supabaseUrl).catch(() => []) : await deps.listKeys(deps.supabaseUrl);
|
|
9225
|
+
const decision = decideLoginMethod({
|
|
9226
|
+
opts,
|
|
9227
|
+
interactive: deps.interactive,
|
|
9228
|
+
hasActiveLogin: await deps.hasActiveLogin(),
|
|
9229
|
+
savedKeys: ring.length,
|
|
9230
|
+
envKey: !!envKey
|
|
9231
|
+
});
|
|
9232
|
+
const method = "method" in decision ? decision.method : await deps.choose(
|
|
9233
|
+
"How do you want to sign in?",
|
|
9234
|
+
decision.ask.map((m) => ({ label: METHOD_LABELS[m], value: m }))
|
|
9235
|
+
);
|
|
9236
|
+
if (method === "github") {
|
|
9237
|
+
await deps.githubPairing();
|
|
9238
|
+
return { method };
|
|
9239
|
+
}
|
|
9240
|
+
if (method === "no-account") return await mintKeyedAccount(deps);
|
|
9241
|
+
const raw = given ?? await pickSavedKey(deps, ring);
|
|
9242
|
+
const result = await signInWithKey(deps, raw);
|
|
9243
|
+
if (!given && ring.some((k) => k.key === raw)) {
|
|
9244
|
+
deps.log(` Used the key saved on this machine for ${result.handle}. \`archme login --github\` signs in with GitHub instead.`);
|
|
9245
|
+
}
|
|
9246
|
+
return result;
|
|
9247
|
+
}
|
|
9248
|
+
function redactKey(data) {
|
|
9249
|
+
const text = JSON.stringify(data && typeof data === "object" && "key" in data ? { ...data, key: "[redacted]" } : data);
|
|
9250
|
+
return (text ?? String(data)).replace(/amk_[A-Za-z0-9_-]+/g, "[redacted]");
|
|
9251
|
+
}
|
|
9252
|
+
async function mintKeyedAccount(deps) {
|
|
9253
|
+
const displayName = await deps.gitUserName();
|
|
9254
|
+
const res = await deps.postAgentsDevice("/key-account", {
|
|
9255
|
+
device_name: deps.deviceName,
|
|
9256
|
+
...displayName ? { display_name: displayName } : {}
|
|
9257
|
+
});
|
|
9258
|
+
const errorCode = res.data?.error;
|
|
9259
|
+
if (res.status === 429 && errorCode === "busy") {
|
|
9260
|
+
throw new LoginError(
|
|
9261
|
+
"ArchMe is creating a lot of accounts right now. Try again in a few minutes, or sign in with GitHub: archme login --github"
|
|
9262
|
+
);
|
|
9263
|
+
}
|
|
9264
|
+
if (res.status === 429) {
|
|
9265
|
+
throw new LoginError(
|
|
9266
|
+
"Too many accounts have been created from this network in the last hour. Sign in with GitHub instead: archme login --github"
|
|
9267
|
+
);
|
|
9268
|
+
}
|
|
9269
|
+
if (res.status >= 500 && (errorCode === "not_configured" || errorCode === "rate_check_failed")) {
|
|
9270
|
+
throw new LoginError(
|
|
9271
|
+
`ArchMe couldn't create an account just now (a temporary server problem: ${errorCode}). Try again in a few minutes, or sign in with GitHub: archme login --github`
|
|
9272
|
+
);
|
|
9273
|
+
}
|
|
9274
|
+
const data = res.data;
|
|
9275
|
+
const parsed = parseAccountKey(data.key);
|
|
9276
|
+
if (res.status !== 200 || !parsed || !data.user_id) {
|
|
9277
|
+
throw new LoginError(`Could not create an account (${res.status}): ${redactKey(res.data)}`);
|
|
9278
|
+
}
|
|
9279
|
+
let saved = false;
|
|
9280
|
+
try {
|
|
9281
|
+
await deps.saveKey(deps.supabaseUrl, { user_id: data.user_id, key: data.key, handle: parsed.handle });
|
|
9282
|
+
saved = true;
|
|
9283
|
+
} catch (err2) {
|
|
9284
|
+
deps.log(`! Created ${parsed.handle}, but its key could not be written to ${deps.keyRingPath}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
9285
|
+
deps.log(" It is the only way back into this account. Save this now (a password manager is a good place):");
|
|
9286
|
+
deps.log("");
|
|
9287
|
+
deps.log(` ${data.key}`);
|
|
9288
|
+
deps.log("");
|
|
9289
|
+
deps.log(` Later, sign in with it: ${KEY_VIA_ENV}`);
|
|
9290
|
+
}
|
|
9291
|
+
const grant = await deps.passwordGrant(parsed.email, parsed.secret);
|
|
9292
|
+
if (!grant.ok) {
|
|
9293
|
+
throw new LoginError(
|
|
9294
|
+
`Created ${parsed.handle}${saved ? " and saved its key" : ""}, but signing in failed: ${grant.error}. ` + (saved ? "Run `archme login --key` to try again." : `Try again with the key printed above: ${KEY_VIA_ENV}.`)
|
|
9295
|
+
);
|
|
9296
|
+
}
|
|
9297
|
+
await storeKeyedLogin(deps, grant, data);
|
|
9298
|
+
deps.log(`\u2713 Created your ArchMe account ${parsed.handle} \u2014 no email or password, just a key.`);
|
|
9299
|
+
if (saved) {
|
|
9300
|
+
deps.log(` The key is saved in ${deps.keyRingPath}. Save it in your password manager too: it is the`);
|
|
9301
|
+
deps.log(" only way back in, including from another computer. `archme key show` prints it.");
|
|
9302
|
+
}
|
|
9303
|
+
deps.log(" `archme open` opens your board, signed in.");
|
|
9304
|
+
return { method: "no-account", userId: data.user_id, handle: parsed.handle };
|
|
9305
|
+
}
|
|
9306
|
+
async function pickSavedKey(deps, ring) {
|
|
9307
|
+
if (ring.length === 1) return ring[0].key;
|
|
9308
|
+
if (ring.length > 1) {
|
|
9309
|
+
const newestFirst = [...ring].reverse();
|
|
9310
|
+
if (!deps.interactive) {
|
|
9311
|
+
throw new LoginError(
|
|
9312
|
+
`Several keys are saved (${newestFirst.map((k) => k.handle).join(", ")}). Run \`archme login --key\` at a terminal to pick one, or pass it with ${KEY_VIA_ENV} (\`archme key show --handle <handle>\` prints it).`
|
|
9313
|
+
);
|
|
9314
|
+
}
|
|
9315
|
+
return await deps.choose("Which account?", newestFirst.map((k) => ({ label: k.handle, value: k.key })));
|
|
8660
9316
|
}
|
|
8661
|
-
|
|
9317
|
+
if (!deps.interactive) {
|
|
9318
|
+
throw new LoginError(
|
|
9319
|
+
`No saved keys on this machine. Run \`archme login --key\` at a terminal to paste yours, or pass it with ${KEY_VIA_ENV}.`
|
|
9320
|
+
);
|
|
9321
|
+
}
|
|
9322
|
+
return await deps.promptLine("Paste your key (amk_\u2026)");
|
|
9323
|
+
}
|
|
9324
|
+
async function signInWithKey(deps, raw) {
|
|
9325
|
+
const parsed = parseAccountKey(raw);
|
|
9326
|
+
if (!parsed) throw new LoginError("That doesn't look like an ArchMe key (amk_<handle>_<secret>).");
|
|
9327
|
+
const grant = await deps.passwordGrant(parsed.email, parsed.secret);
|
|
9328
|
+
if (!grant.ok) {
|
|
9329
|
+
throw new LoginError(grant.permanent ? `The key for ${parsed.handle} was rejected. Check it and try again.` : `Could not sign in: ${grant.error}`);
|
|
9330
|
+
}
|
|
9331
|
+
const reg = await deps.postAgentsDevice("/register-device", { device_name: deps.deviceName }, grant.tokens.access_token);
|
|
9332
|
+
const data = reg.data;
|
|
9333
|
+
if (reg.status !== 200 || !data.user_id) {
|
|
9334
|
+
throw new LoginError(`Could not register this machine (${reg.status}): ${redactKey(reg.data)}`);
|
|
9335
|
+
}
|
|
9336
|
+
try {
|
|
9337
|
+
await deps.saveKey(deps.supabaseUrl, {
|
|
9338
|
+
user_id: data.user_id,
|
|
9339
|
+
key: formatAccountKey(parsed.handle, parsed.secret),
|
|
9340
|
+
handle: parsed.handle
|
|
9341
|
+
});
|
|
9342
|
+
} catch (err2) {
|
|
9343
|
+
deps.log(
|
|
9344
|
+
`! The key for ${parsed.handle} could not be saved on this machine (${err2 instanceof Error ? err2.message : String(err2)}). Signing in anyway; if the session lapses, sign in with the key again.`
|
|
9345
|
+
);
|
|
9346
|
+
}
|
|
9347
|
+
await storeKeyedLogin(deps, grant, data);
|
|
9348
|
+
deps.log(`\u2713 Signed in as keyed account ${parsed.handle}.`);
|
|
9349
|
+
return { method: "key", userId: data.user_id, handle: parsed.handle };
|
|
9350
|
+
}
|
|
9351
|
+
async function storeKeyedLogin(deps, grant, account) {
|
|
9352
|
+
const devices = account.devices ?? [];
|
|
9353
|
+
const primary = devices.find((d) => d.org_id === account.org_id) ?? devices[0];
|
|
9354
|
+
if (!primary || !grant.tokens.refresh_token) {
|
|
9355
|
+
throw new LoginError("Sign-in succeeded but the server registered no device for this machine. Try `archme login` again.");
|
|
9356
|
+
}
|
|
9357
|
+
await deps.replaceCredentials(deps.supabaseUrl, [{
|
|
9358
|
+
kind: "key",
|
|
9359
|
+
refresh_token: grant.tokens.refresh_token,
|
|
9360
|
+
access_token: grant.tokens.access_token,
|
|
9361
|
+
user_id: account.user_id,
|
|
9362
|
+
org_id: primary.org_id,
|
|
9363
|
+
device_id: primary.device_id,
|
|
9364
|
+
bindings: [primary, ...devices.filter((d) => d !== primary)],
|
|
9365
|
+
supabase_url: deps.supabaseUrl
|
|
9366
|
+
}]);
|
|
8662
9367
|
}
|
|
9368
|
+
var LoginError, METHOD_FLAGS, METHOD_LABELS, KEY_VIA_ENV;
|
|
8663
9369
|
var init_enroll = __esm({
|
|
8664
9370
|
"src/auth/enroll.ts"() {
|
|
8665
9371
|
"use strict";
|
|
8666
9372
|
init_execa();
|
|
8667
9373
|
init_credentials();
|
|
9374
|
+
init_keyRing();
|
|
8668
9375
|
init_defaults();
|
|
9376
|
+
init_accountKey();
|
|
9377
|
+
init_keySession();
|
|
9378
|
+
init_oauthClient();
|
|
9379
|
+
LoginError = class extends Error {
|
|
9380
|
+
constructor(message) {
|
|
9381
|
+
super(message);
|
|
9382
|
+
this.name = "LoginError";
|
|
9383
|
+
}
|
|
9384
|
+
};
|
|
9385
|
+
METHOD_FLAGS = {
|
|
9386
|
+
"--no-account": "no-account",
|
|
9387
|
+
"--github": "github",
|
|
9388
|
+
"--key": "key"
|
|
9389
|
+
};
|
|
9390
|
+
METHOD_LABELS = {
|
|
9391
|
+
"no-account": "Continue without an account \u2014 a key saved on this machine is your way back in (`archme key show` prints it).",
|
|
9392
|
+
github: "Sign in with GitHub",
|
|
9393
|
+
key: "Use an existing key"
|
|
9394
|
+
};
|
|
9395
|
+
KEY_VIA_ENV = "`ARCHME_KEY=\u2026 archme login --key`";
|
|
8669
9396
|
}
|
|
8670
9397
|
});
|
|
8671
9398
|
|
|
@@ -8679,7 +9406,7 @@ __export(paths_exports, {
|
|
|
8679
9406
|
resolvePidfilePath: () => resolvePidfilePath,
|
|
8680
9407
|
resolveUnitPath: () => resolveUnitPath
|
|
8681
9408
|
});
|
|
8682
|
-
import { join as
|
|
9409
|
+
import { join as join4 } from "node:path";
|
|
8683
9410
|
function home(env) {
|
|
8684
9411
|
const h2 = env.HOME;
|
|
8685
9412
|
if (!h2) throw new Error("HOME is not set");
|
|
@@ -8692,24 +9419,24 @@ function assertSupported(platform3) {
|
|
|
8692
9419
|
}
|
|
8693
9420
|
function resolveLogDir(platform3, env) {
|
|
8694
9421
|
assertSupported(platform3);
|
|
8695
|
-
if (platform3 === "darwin") return
|
|
8696
|
-
const xdgState = env.XDG_STATE_HOME ??
|
|
8697
|
-
return
|
|
9422
|
+
if (platform3 === "darwin") return join4(home(env), "Library", "Logs", LABEL);
|
|
9423
|
+
const xdgState = env.XDG_STATE_HOME ?? join4(home(env), ".local", "state");
|
|
9424
|
+
return join4(xdgState, "archme", "logs");
|
|
8698
9425
|
}
|
|
8699
9426
|
function resolveLogPath(platform3, env) {
|
|
8700
9427
|
const dir = resolveLogDir(platform3, env);
|
|
8701
|
-
return { stdout:
|
|
9428
|
+
return { stdout: join4(dir, "daemon.log"), stderr: join4(dir, "daemon.err") };
|
|
8702
9429
|
}
|
|
8703
9430
|
function resolveUnitPath(platform3, env) {
|
|
8704
9431
|
assertSupported(platform3);
|
|
8705
9432
|
if (platform3 === "darwin") {
|
|
8706
|
-
return
|
|
9433
|
+
return join4(home(env), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
8707
9434
|
}
|
|
8708
|
-
return
|
|
9435
|
+
return join4(home(env), ".config", "systemd", "user", SYSTEMD_UNIT_NAME);
|
|
8709
9436
|
}
|
|
8710
9437
|
function resolvePidfilePath(env) {
|
|
8711
9438
|
if (env.ARCHME_DAEMON_PIDFILE) return env.ARCHME_DAEMON_PIDFILE;
|
|
8712
|
-
return
|
|
9439
|
+
return join4(home(env), ".archme", "daemon.pid");
|
|
8713
9440
|
}
|
|
8714
9441
|
var LABEL, SYSTEMD_UNIT_NAME;
|
|
8715
9442
|
var init_paths = __esm({
|
|
@@ -8833,8 +9560,8 @@ var init_unitFiles = __esm({
|
|
|
8833
9560
|
});
|
|
8834
9561
|
|
|
8835
9562
|
// src/daemon/service/controllers/launchctl.ts
|
|
8836
|
-
import { mkdir as
|
|
8837
|
-
import { dirname as
|
|
9563
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
|
|
9564
|
+
import { dirname as dirname4 } from "node:path";
|
|
8838
9565
|
var SERVICE_TARGET, DOMAIN, LaunchctlController;
|
|
8839
9566
|
var init_launchctl = __esm({
|
|
8840
9567
|
"src/daemon/service/controllers/launchctl.ts"() {
|
|
@@ -8847,17 +9574,17 @@ var init_launchctl = __esm({
|
|
|
8847
9574
|
this.opts = opts;
|
|
8848
9575
|
}
|
|
8849
9576
|
async install(unitContent) {
|
|
8850
|
-
await
|
|
9577
|
+
await mkdir3(dirname4(this.opts.plistPath), { recursive: true });
|
|
8851
9578
|
const tmp = `${this.opts.plistPath}.tmp`;
|
|
8852
9579
|
await writeFile2(tmp, unitContent, { mode: 420 });
|
|
8853
|
-
await
|
|
9580
|
+
await rename2(tmp, this.opts.plistPath);
|
|
8854
9581
|
}
|
|
8855
9582
|
async uninstall() {
|
|
8856
|
-
await
|
|
9583
|
+
await rm3(this.opts.plistPath, { force: true });
|
|
8857
9584
|
}
|
|
8858
9585
|
async unitExists() {
|
|
8859
9586
|
try {
|
|
8860
|
-
await
|
|
9587
|
+
await readFile3(this.opts.plistPath);
|
|
8861
9588
|
return true;
|
|
8862
9589
|
} catch {
|
|
8863
9590
|
return false;
|
|
@@ -8865,7 +9592,7 @@ var init_launchctl = __esm({
|
|
|
8865
9592
|
}
|
|
8866
9593
|
async readUnitContent() {
|
|
8867
9594
|
try {
|
|
8868
|
-
return await
|
|
9595
|
+
return await readFile3(this.opts.plistPath, "utf8");
|
|
8869
9596
|
} catch {
|
|
8870
9597
|
return null;
|
|
8871
9598
|
}
|
|
@@ -8932,8 +9659,8 @@ var init_launchctl = __esm({
|
|
|
8932
9659
|
});
|
|
8933
9660
|
|
|
8934
9661
|
// src/daemon/service/controllers/systemctl.ts
|
|
8935
|
-
import { mkdir as
|
|
8936
|
-
import { dirname as
|
|
9662
|
+
import { mkdir as mkdir4, readFile as readFile4, rename as rename3, rm as rm4, writeFile as writeFile3 } from "node:fs/promises";
|
|
9663
|
+
import { dirname as dirname5 } from "node:path";
|
|
8937
9664
|
var SystemctlController;
|
|
8938
9665
|
var init_systemctl = __esm({
|
|
8939
9666
|
"src/daemon/service/controllers/systemctl.ts"() {
|
|
@@ -8950,20 +9677,20 @@ var init_systemctl = __esm({
|
|
|
8950
9677
|
}
|
|
8951
9678
|
}
|
|
8952
9679
|
async install(unitContent) {
|
|
8953
|
-
await
|
|
9680
|
+
await mkdir4(dirname5(this.opts.unitPath), { recursive: true });
|
|
8954
9681
|
const tmp = `${this.opts.unitPath}.tmp`;
|
|
8955
9682
|
await writeFile3(tmp, unitContent, { mode: 420 });
|
|
8956
|
-
await
|
|
9683
|
+
await rename3(tmp, this.opts.unitPath);
|
|
8957
9684
|
await this.sysctl(["daemon-reload"]);
|
|
8958
9685
|
}
|
|
8959
9686
|
async uninstall() {
|
|
8960
9687
|
const existed = await this.unitExists();
|
|
8961
|
-
await
|
|
9688
|
+
await rm4(this.opts.unitPath, { force: true });
|
|
8962
9689
|
if (existed) await this.sysctl(["daemon-reload"]);
|
|
8963
9690
|
}
|
|
8964
9691
|
async unitExists() {
|
|
8965
9692
|
try {
|
|
8966
|
-
await
|
|
9693
|
+
await readFile4(this.opts.unitPath);
|
|
8967
9694
|
return true;
|
|
8968
9695
|
} catch {
|
|
8969
9696
|
return false;
|
|
@@ -8971,7 +9698,7 @@ var init_systemctl = __esm({
|
|
|
8971
9698
|
}
|
|
8972
9699
|
async readUnitContent() {
|
|
8973
9700
|
try {
|
|
8974
|
-
return await
|
|
9701
|
+
return await readFile4(this.opts.unitPath, "utf8");
|
|
8975
9702
|
} catch {
|
|
8976
9703
|
return null;
|
|
8977
9704
|
}
|
|
@@ -9107,10 +9834,19 @@ var init_serviceController = __esm({
|
|
|
9107
9834
|
});
|
|
9108
9835
|
|
|
9109
9836
|
// src/daemon/service/install.ts
|
|
9837
|
+
var install_exports = {};
|
|
9838
|
+
__export(install_exports, {
|
|
9839
|
+
NotSignedInError: () => NotSignedInError,
|
|
9840
|
+
PUBLISHED_BIN_RELATIVE: () => PUBLISHED_BIN_RELATIVE,
|
|
9841
|
+
ensureStartPrerequisites: () => ensureStartPrerequisites,
|
|
9842
|
+
findPackageRoot: () => findPackageRoot,
|
|
9843
|
+
pickCliEntry: () => pickCliEntry,
|
|
9844
|
+
runStartFlow: () => runStartFlow
|
|
9845
|
+
});
|
|
9110
9846
|
import { isatty } from "node:tty";
|
|
9111
9847
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9112
|
-
import { dirname as
|
|
9113
|
-
import { mkdir as
|
|
9848
|
+
import { dirname as dirname6, join as join5 } from "node:path";
|
|
9849
|
+
import { mkdir as mkdir5 } from "node:fs/promises";
|
|
9114
9850
|
import { existsSync as existsSync2 } from "node:fs";
|
|
9115
9851
|
function captureEnv(env) {
|
|
9116
9852
|
const out = {};
|
|
@@ -9122,8 +9858,8 @@ function captureEnv(env) {
|
|
|
9122
9858
|
function findPackageRoot(startDir, exists2) {
|
|
9123
9859
|
let dir = startDir;
|
|
9124
9860
|
for (; ; ) {
|
|
9125
|
-
if (exists2(
|
|
9126
|
-
const parent =
|
|
9861
|
+
if (exists2(join5(dir, "package.json"))) return dir;
|
|
9862
|
+
const parent = dirname6(dir);
|
|
9127
9863
|
if (parent === dir) {
|
|
9128
9864
|
throw new Error(
|
|
9129
9865
|
`could not locate the @archme/cli package root above ${startDir}`
|
|
@@ -9133,16 +9869,16 @@ function findPackageRoot(startDir, exists2) {
|
|
|
9133
9869
|
}
|
|
9134
9870
|
}
|
|
9135
9871
|
function pickCliEntry(packageRoot, exists2) {
|
|
9136
|
-
const bundle =
|
|
9872
|
+
const bundle = join5(packageRoot, PUBLISHED_BIN_RELATIVE);
|
|
9137
9873
|
if (exists2(bundle)) return bundle;
|
|
9138
|
-
const shim =
|
|
9874
|
+
const shim = join5(packageRoot, DEV_SHIM_RELATIVE);
|
|
9139
9875
|
if (exists2(shim)) return shim;
|
|
9140
9876
|
throw new Error(
|
|
9141
9877
|
`no runnable CLI entry under ${packageRoot} (looked for ${PUBLISHED_BIN_RELATIVE} and ${DEV_SHIM_RELATIVE}); run \`npm run build\` first`
|
|
9142
9878
|
);
|
|
9143
9879
|
}
|
|
9144
9880
|
function resolveCliEntry() {
|
|
9145
|
-
const here =
|
|
9881
|
+
const here = dirname6(fileURLToPath3(import.meta.url));
|
|
9146
9882
|
return pickCliEntry(findPackageRoot(here, existsSync2), existsSync2);
|
|
9147
9883
|
}
|
|
9148
9884
|
async function ensureStartPrerequisites(opts = {}) {
|
|
@@ -9170,7 +9906,7 @@ async function runStartFlow(opts = {}) {
|
|
|
9170
9906
|
const env = opts.env ?? process.env;
|
|
9171
9907
|
await ensureStartPrerequisites(opts);
|
|
9172
9908
|
const logPaths = resolveLogPath(platform3, env);
|
|
9173
|
-
await
|
|
9909
|
+
await mkdir5(resolveLogDir(platform3, env), { recursive: true });
|
|
9174
9910
|
const renderOpts = {
|
|
9175
9911
|
nodeBin: process.execPath,
|
|
9176
9912
|
cliEntry: resolveCliEntry(),
|
|
@@ -9232,8 +9968,8 @@ var init_install = __esm({
|
|
|
9232
9968
|
|
|
9233
9969
|
// src/daemon/service/logs.ts
|
|
9234
9970
|
import { createReadStream as createReadStream2, watch } from "node:fs";
|
|
9235
|
-
import { open, stat } from "node:fs/promises";
|
|
9236
|
-
import { basename, dirname as
|
|
9971
|
+
import { open as open2, stat } from "node:fs/promises";
|
|
9972
|
+
import { basename, dirname as dirname7 } from "node:path";
|
|
9237
9973
|
async function tailLog(opts = {}) {
|
|
9238
9974
|
const platform3 = opts.platform ?? process.platform;
|
|
9239
9975
|
const env = opts.env ?? process.env;
|
|
@@ -9243,7 +9979,7 @@ async function tailLog(opts = {}) {
|
|
|
9243
9979
|
let offset = 0;
|
|
9244
9980
|
try {
|
|
9245
9981
|
const st = await stat(path7);
|
|
9246
|
-
const handle = await
|
|
9982
|
+
const handle = await open2(path7, "r");
|
|
9247
9983
|
try {
|
|
9248
9984
|
const slice = Math.min(64 * 1024, st.size);
|
|
9249
9985
|
const buf = Buffer.alloc(slice);
|
|
@@ -9269,7 +10005,7 @@ async function tailLog(opts = {}) {
|
|
|
9269
10005
|
await new Promise((resolve6, reject) => {
|
|
9270
10006
|
let fileWatcher = null;
|
|
9271
10007
|
let dirWatcher = null;
|
|
9272
|
-
const dir =
|
|
10008
|
+
const dir = dirname7(path7);
|
|
9273
10009
|
const name = basename(path7);
|
|
9274
10010
|
const onChange = async () => {
|
|
9275
10011
|
try {
|
|
@@ -9343,7 +10079,7 @@ var init_logs = __esm({
|
|
|
9343
10079
|
});
|
|
9344
10080
|
|
|
9345
10081
|
// src/daemon/service/status.ts
|
|
9346
|
-
import { readFile as
|
|
10082
|
+
import { readFile as readFile5, stat as stat2 } from "node:fs/promises";
|
|
9347
10083
|
function renderStatus(s) {
|
|
9348
10084
|
const lines = [];
|
|
9349
10085
|
if (!s.unitInstalled) {
|
|
@@ -9408,7 +10144,7 @@ function formatDuration(ms, opts = {}) {
|
|
|
9408
10144
|
async function readLastLogLine(platform3 = process.platform, env = process.env) {
|
|
9409
10145
|
try {
|
|
9410
10146
|
const { stdout } = resolveLogPath(platform3, env);
|
|
9411
|
-
const buf = await
|
|
10147
|
+
const buf = await readFile5(stdout, "utf8");
|
|
9412
10148
|
const lines = buf.trimEnd().split("\n");
|
|
9413
10149
|
return lines[lines.length - 1] ?? null;
|
|
9414
10150
|
} catch {
|
|
@@ -9452,9 +10188,9 @@ var init_status = __esm({
|
|
|
9452
10188
|
});
|
|
9453
10189
|
|
|
9454
10190
|
// src/scan/mcp-config.ts
|
|
9455
|
-
import { mkdtemp, rm as
|
|
10191
|
+
import { mkdtemp, rm as rm5, writeFile as writeFile4 } from "node:fs/promises";
|
|
9456
10192
|
import { tmpdir } from "node:os";
|
|
9457
|
-
import { join as
|
|
10193
|
+
import { join as join6 } from "node:path";
|
|
9458
10194
|
async function writeMcpConfig(opts) {
|
|
9459
10195
|
const params = new URLSearchParams({
|
|
9460
10196
|
project: opts.projectSlug,
|
|
@@ -9483,8 +10219,8 @@ async function writeMcpConfig(opts) {
|
|
|
9483
10219
|
}
|
|
9484
10220
|
}
|
|
9485
10221
|
const url = `${opts.baseUrl}?${params.toString()}`;
|
|
9486
|
-
const dir = await mkdtemp(
|
|
9487
|
-
const configPath =
|
|
10222
|
+
const dir = await mkdtemp(join6(tmpdir(), "archme-mcp-"));
|
|
10223
|
+
const configPath = join6(dir, "mcp.json");
|
|
9488
10224
|
const headers = {
|
|
9489
10225
|
Authorization: `Bearer ${opts.bearer}`
|
|
9490
10226
|
};
|
|
@@ -9496,7 +10232,7 @@ async function writeMcpConfig(opts) {
|
|
|
9496
10232
|
await writeFile4(configPath, JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: server } }, null, 2), {
|
|
9497
10233
|
mode: 384
|
|
9498
10234
|
});
|
|
9499
|
-
return { configPath, cleanup: () =>
|
|
10235
|
+
return { configPath, cleanup: () => rm5(dir, { recursive: true, force: true }) };
|
|
9500
10236
|
}
|
|
9501
10237
|
var MCP_SERVER_NAME;
|
|
9502
10238
|
var init_mcp_config = __esm({
|
|
@@ -9532,7 +10268,7 @@ var init_env_scrub = __esm({
|
|
|
9532
10268
|
|
|
9533
10269
|
// src/preflight.ts
|
|
9534
10270
|
import { accessSync, constants as constants4 } from "node:fs";
|
|
9535
|
-
import { delimiter as pathDelimiter, join as
|
|
10271
|
+
import { delimiter as pathDelimiter, join as join7 } from "node:path";
|
|
9536
10272
|
function defaultIsExecutable(path7) {
|
|
9537
10273
|
try {
|
|
9538
10274
|
accessSync(path7, constants4.X_OK);
|
|
@@ -9550,7 +10286,7 @@ function findOnPath(name, opts = {}) {
|
|
|
9550
10286
|
const exts = env.PATHEXT ? ["", ...env.PATHEXT.split(";").filter(Boolean)] : [""];
|
|
9551
10287
|
for (const dir of dirs) {
|
|
9552
10288
|
for (const ext2 of exts) {
|
|
9553
|
-
const candidate =
|
|
10289
|
+
const candidate = join7(dir, `${name}${ext2}`);
|
|
9554
10290
|
if (isExecutable(candidate)) return candidate;
|
|
9555
10291
|
}
|
|
9556
10292
|
}
|
|
@@ -9608,10 +10344,31 @@ var init_preflight = __esm({
|
|
|
9608
10344
|
});
|
|
9609
10345
|
|
|
9610
10346
|
// ../shared/daemonModel.ts
|
|
9611
|
-
|
|
10347
|
+
function parseClaudeCodeVersion(output) {
|
|
10348
|
+
return /(\d+)\.(\d+)\.(\d+)/.exec(output ?? "")?.[0] ?? null;
|
|
10349
|
+
}
|
|
10350
|
+
function versionBelow(version, minimum) {
|
|
10351
|
+
const a2 = version.split(".").map(Number);
|
|
10352
|
+
const b = minimum.split(".").map(Number);
|
|
10353
|
+
for (let i2 = 0; i2 < Math.max(a2.length, b.length); i2++) {
|
|
10354
|
+
const d = (a2[i2] ?? 0) - (b[i2] ?? 0);
|
|
10355
|
+
if (d !== 0) return d < 0;
|
|
10356
|
+
}
|
|
10357
|
+
return false;
|
|
10358
|
+
}
|
|
10359
|
+
function runnableModel(requested, installedVersion) {
|
|
10360
|
+
const needs = MIN_CLAUDE_CODE_VERSION[requested];
|
|
10361
|
+
if (!needs || !installedVersion || !versionBelow(installedVersion, needs)) return { model: requested };
|
|
10362
|
+
return { model: FALLBACK_CLAUDE_MODEL, fellBack: { from: requested, needs } };
|
|
10363
|
+
}
|
|
10364
|
+
var DEFAULT_CLAUDE_MODEL, FALLBACK_CLAUDE_MODEL, MIN_CLAUDE_CODE_VERSION;
|
|
9612
10365
|
var init_daemonModel = __esm({
|
|
9613
10366
|
"../shared/daemonModel.ts"() {
|
|
9614
|
-
DEFAULT_CLAUDE_MODEL = "claude-opus-
|
|
10367
|
+
DEFAULT_CLAUDE_MODEL = "claude-opus-5-5";
|
|
10368
|
+
FALLBACK_CLAUDE_MODEL = "claude-opus-4-8";
|
|
10369
|
+
MIN_CLAUDE_CODE_VERSION = {
|
|
10370
|
+
"claude-opus-5-5": "2.1.280"
|
|
10371
|
+
};
|
|
9615
10372
|
}
|
|
9616
10373
|
});
|
|
9617
10374
|
|
|
@@ -9773,6 +10530,39 @@ var init_spawn_claude = __esm({
|
|
|
9773
10530
|
}
|
|
9774
10531
|
});
|
|
9775
10532
|
|
|
10533
|
+
// src/scan/claudeVersion.ts
|
|
10534
|
+
async function runClaudeVersion() {
|
|
10535
|
+
const r = await execa("claude", ["--version"], { reject: false, timeout: 15e3 });
|
|
10536
|
+
return r.exitCode === 0 ? String(r.stdout) : null;
|
|
10537
|
+
}
|
|
10538
|
+
function createClaudeVersionProbe(opts = {}) {
|
|
10539
|
+
const exec = opts.exec ?? runClaudeVersion;
|
|
10540
|
+
const now = opts.now ?? Date.now;
|
|
10541
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
10542
|
+
let cached;
|
|
10543
|
+
return async () => {
|
|
10544
|
+
if (cached && now() - cached.at < ttlMs) return cached.version;
|
|
10545
|
+
let version = null;
|
|
10546
|
+
try {
|
|
10547
|
+
version = parseClaudeCodeVersion(await exec());
|
|
10548
|
+
} catch {
|
|
10549
|
+
version = null;
|
|
10550
|
+
}
|
|
10551
|
+
cached = { version, at: now() };
|
|
10552
|
+
return version;
|
|
10553
|
+
};
|
|
10554
|
+
}
|
|
10555
|
+
var DEFAULT_TTL_MS, claudeCodeVersion;
|
|
10556
|
+
var init_claudeVersion = __esm({
|
|
10557
|
+
"src/scan/claudeVersion.ts"() {
|
|
10558
|
+
"use strict";
|
|
10559
|
+
init_execa();
|
|
10560
|
+
init_daemonModel();
|
|
10561
|
+
DEFAULT_TTL_MS = 10 * 6e4;
|
|
10562
|
+
claudeCodeVersion = createClaudeVersionProbe();
|
|
10563
|
+
}
|
|
10564
|
+
});
|
|
10565
|
+
|
|
9776
10566
|
// src/scan/model-pin.ts
|
|
9777
10567
|
async function resolveModelPin(supabase) {
|
|
9778
10568
|
try {
|
|
@@ -9786,10 +10576,30 @@ async function resolveModelPin(supabase) {
|
|
|
9786
10576
|
return DEFAULT_CLAUDE_MODEL;
|
|
9787
10577
|
}
|
|
9788
10578
|
}
|
|
10579
|
+
async function resolveRunnableModel(supabase, deps = {}) {
|
|
10580
|
+
const requested = await resolveModelPin(supabase);
|
|
10581
|
+
const installed = await (deps.version ?? claudeCodeVersion)();
|
|
10582
|
+
const { model, fellBack } = runnableModel(requested, installed);
|
|
10583
|
+
if (fellBack) {
|
|
10584
|
+
const warned = deps.warned ?? warnedFallbacks;
|
|
10585
|
+
const key = `${fellBack.from}@${installed}`;
|
|
10586
|
+
if (!warned.has(key)) {
|
|
10587
|
+
warned.add(key);
|
|
10588
|
+
(deps.warn ?? console.warn)(
|
|
10589
|
+
`Claude Code ${installed} can't run ${fellBack.from} (needs ${fellBack.needs}+); running ${model} instead. Run \`claude update\` to use ${fellBack.from}.`
|
|
10590
|
+
);
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
return model;
|
|
10594
|
+
}
|
|
10595
|
+
var warnedFallbacks;
|
|
9789
10596
|
var init_model_pin = __esm({
|
|
9790
10597
|
"src/scan/model-pin.ts"() {
|
|
9791
10598
|
"use strict";
|
|
9792
10599
|
init_spawn_claude();
|
|
10600
|
+
init_daemonModel();
|
|
10601
|
+
init_claudeVersion();
|
|
10602
|
+
warnedFallbacks = /* @__PURE__ */ new Set();
|
|
9793
10603
|
}
|
|
9794
10604
|
});
|
|
9795
10605
|
|
|
@@ -9866,9 +10676,9 @@ var init_digestPrompt = __esm({
|
|
|
9866
10676
|
});
|
|
9867
10677
|
|
|
9868
10678
|
// src/daemon/digest.ts
|
|
9869
|
-
import { mkdtemp as mkdtemp2, rm as
|
|
10679
|
+
import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
|
|
9870
10680
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
9871
|
-
import { join as
|
|
10681
|
+
import { join as join8 } from "node:path";
|
|
9872
10682
|
function isCleanDigestExit(res) {
|
|
9873
10683
|
return !res.timedOut && res.signal == null && res.exitCode === 0;
|
|
9874
10684
|
}
|
|
@@ -9902,15 +10712,15 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
|
|
|
9902
10712
|
}
|
|
9903
10713
|
let dir;
|
|
9904
10714
|
try {
|
|
9905
|
-
dir = await mkdtemp2(
|
|
10715
|
+
dir = await mkdtemp2(join8(tmpdir2(), "archme-digest-"));
|
|
9906
10716
|
let mcpConfigPath;
|
|
9907
10717
|
if (mcp) {
|
|
9908
10718
|
mcpConfigPath = mcp.configPath;
|
|
9909
10719
|
} else {
|
|
9910
|
-
mcpConfigPath =
|
|
10720
|
+
mcpConfigPath = join8(dir, "mcp.json");
|
|
9911
10721
|
await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: {} }));
|
|
9912
10722
|
}
|
|
9913
|
-
const model = await
|
|
10723
|
+
const model = await resolveRunnableModel(ctx.supabase);
|
|
9914
10724
|
const proc = spawnClaude({
|
|
9915
10725
|
prompt: buildDigestPrompt(payload),
|
|
9916
10726
|
mcpConfigPath,
|
|
@@ -9942,7 +10752,7 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
|
|
|
9942
10752
|
await failDigest(ctx, job.id, deviceId, err2 instanceof Error ? err2.message : String(err2));
|
|
9943
10753
|
} finally {
|
|
9944
10754
|
inFlight2?.delete(job.id);
|
|
9945
|
-
if (dir) await
|
|
10755
|
+
if (dir) await rm6(dir, { recursive: true, force: true }).catch(() => {
|
|
9946
10756
|
});
|
|
9947
10757
|
}
|
|
9948
10758
|
}
|
|
@@ -9996,37 +10806,37 @@ var init_heartbeatHealth = __esm({
|
|
|
9996
10806
|
var balanced, maybeMatch, range;
|
|
9997
10807
|
var init_esm = __esm({
|
|
9998
10808
|
"node_modules/balanced-match/dist/esm/index.js"() {
|
|
9999
|
-
balanced = (a2, b,
|
|
10000
|
-
const ma = a2 instanceof RegExp ? maybeMatch(a2,
|
|
10001
|
-
const mb = b instanceof RegExp ? maybeMatch(b,
|
|
10002
|
-
const r = ma !== null && mb != null && range(ma, mb,
|
|
10809
|
+
balanced = (a2, b, str2) => {
|
|
10810
|
+
const ma = a2 instanceof RegExp ? maybeMatch(a2, str2) : a2;
|
|
10811
|
+
const mb = b instanceof RegExp ? maybeMatch(b, str2) : b;
|
|
10812
|
+
const r = ma !== null && mb != null && range(ma, mb, str2);
|
|
10003
10813
|
return r && {
|
|
10004
10814
|
start: r[0],
|
|
10005
10815
|
end: r[1],
|
|
10006
|
-
pre:
|
|
10007
|
-
body:
|
|
10008
|
-
post:
|
|
10816
|
+
pre: str2.slice(0, r[0]),
|
|
10817
|
+
body: str2.slice(r[0] + ma.length, r[1]),
|
|
10818
|
+
post: str2.slice(r[1] + mb.length)
|
|
10009
10819
|
};
|
|
10010
10820
|
};
|
|
10011
|
-
maybeMatch = (reg,
|
|
10012
|
-
const m =
|
|
10821
|
+
maybeMatch = (reg, str2) => {
|
|
10822
|
+
const m = str2.match(reg);
|
|
10013
10823
|
return m ? m[0] : null;
|
|
10014
10824
|
};
|
|
10015
|
-
range = (a2, b,
|
|
10825
|
+
range = (a2, b, str2) => {
|
|
10016
10826
|
let begs, beg, left, right = void 0, result;
|
|
10017
|
-
let ai =
|
|
10018
|
-
let bi =
|
|
10827
|
+
let ai = str2.indexOf(a2);
|
|
10828
|
+
let bi = str2.indexOf(b, ai + 1);
|
|
10019
10829
|
let i2 = ai;
|
|
10020
10830
|
if (ai >= 0 && bi > 0) {
|
|
10021
10831
|
if (a2 === b) {
|
|
10022
10832
|
return [ai, bi];
|
|
10023
10833
|
}
|
|
10024
10834
|
begs = [];
|
|
10025
|
-
left =
|
|
10835
|
+
left = str2.length;
|
|
10026
10836
|
while (i2 >= 0 && !result) {
|
|
10027
10837
|
if (i2 === ai) {
|
|
10028
10838
|
begs.push(i2);
|
|
10029
|
-
ai =
|
|
10839
|
+
ai = str2.indexOf(a2, i2 + 1);
|
|
10030
10840
|
} else if (begs.length === 1) {
|
|
10031
10841
|
const r = begs.pop();
|
|
10032
10842
|
if (r !== void 0)
|
|
@@ -10037,7 +10847,7 @@ var init_esm = __esm({
|
|
|
10037
10847
|
left = beg;
|
|
10038
10848
|
right = bi;
|
|
10039
10849
|
}
|
|
10040
|
-
bi =
|
|
10850
|
+
bi = str2.indexOf(b, i2 + 1);
|
|
10041
10851
|
}
|
|
10042
10852
|
i2 = ai < bi && ai >= 0 ? ai : bi;
|
|
10043
10853
|
}
|
|
@@ -10051,23 +10861,23 @@ var init_esm = __esm({
|
|
|
10051
10861
|
});
|
|
10052
10862
|
|
|
10053
10863
|
// node_modules/brace-expansion/dist/esm/index.js
|
|
10054
|
-
function numeric(
|
|
10055
|
-
return !isNaN(
|
|
10864
|
+
function numeric(str2) {
|
|
10865
|
+
return !isNaN(str2) ? parseInt(str2, 10) : str2.charCodeAt(0);
|
|
10056
10866
|
}
|
|
10057
|
-
function escapeBraces(
|
|
10058
|
-
return
|
|
10867
|
+
function escapeBraces(str2) {
|
|
10868
|
+
return str2.replace(slashPattern, escSlash).replace(openPattern, escOpen).replace(closePattern, escClose).replace(commaPattern, escComma).replace(periodPattern, escPeriod);
|
|
10059
10869
|
}
|
|
10060
|
-
function unescapeBraces(
|
|
10061
|
-
return
|
|
10870
|
+
function unescapeBraces(str2) {
|
|
10871
|
+
return str2.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
|
|
10062
10872
|
}
|
|
10063
|
-
function parseCommaParts(
|
|
10064
|
-
if (!
|
|
10873
|
+
function parseCommaParts(str2) {
|
|
10874
|
+
if (!str2) {
|
|
10065
10875
|
return [""];
|
|
10066
10876
|
}
|
|
10067
10877
|
const parts = [];
|
|
10068
|
-
const m = balanced("{", "}",
|
|
10878
|
+
const m = balanced("{", "}", str2);
|
|
10069
10879
|
if (!m) {
|
|
10070
|
-
return
|
|
10880
|
+
return str2.split(",");
|
|
10071
10881
|
}
|
|
10072
10882
|
const { pre, body, post } = m;
|
|
10073
10883
|
const p = pre.split(",");
|
|
@@ -10081,18 +10891,18 @@ function parseCommaParts(str) {
|
|
|
10081
10891
|
parts.push.apply(parts, p);
|
|
10082
10892
|
return parts;
|
|
10083
10893
|
}
|
|
10084
|
-
function expand(
|
|
10085
|
-
if (!
|
|
10894
|
+
function expand(str2, options = {}) {
|
|
10895
|
+
if (!str2) {
|
|
10086
10896
|
return [];
|
|
10087
10897
|
}
|
|
10088
10898
|
const { max = EXPANSION_MAX } = options;
|
|
10089
|
-
if (
|
|
10090
|
-
|
|
10899
|
+
if (str2.slice(0, 2) === "{}") {
|
|
10900
|
+
str2 = "\\{\\}" + str2.slice(2);
|
|
10091
10901
|
}
|
|
10092
|
-
return expand_(escapeBraces(
|
|
10902
|
+
return expand_(escapeBraces(str2), max, true).map(unescapeBraces);
|
|
10093
10903
|
}
|
|
10094
|
-
function embrace(
|
|
10095
|
-
return "{" +
|
|
10904
|
+
function embrace(str2) {
|
|
10905
|
+
return "{" + str2 + "}";
|
|
10096
10906
|
}
|
|
10097
10907
|
function isPadded(el) {
|
|
10098
10908
|
return /^-?0\d/.test(el);
|
|
@@ -10103,11 +10913,11 @@ function lte(i2, y) {
|
|
|
10103
10913
|
function gte(i2, y) {
|
|
10104
10914
|
return i2 >= y;
|
|
10105
10915
|
}
|
|
10106
|
-
function expand_(
|
|
10916
|
+
function expand_(str2, max, isTop) {
|
|
10107
10917
|
const expansions = [];
|
|
10108
|
-
const m = balanced("{", "}",
|
|
10918
|
+
const m = balanced("{", "}", str2);
|
|
10109
10919
|
if (!m)
|
|
10110
|
-
return [
|
|
10920
|
+
return [str2];
|
|
10111
10921
|
const pre = m.pre;
|
|
10112
10922
|
const post = m.post.length ? expand_(m.post, max, false) : [""];
|
|
10113
10923
|
if (/\$$/.test(m.pre)) {
|
|
@@ -10122,10 +10932,10 @@ function expand_(str, max, isTop) {
|
|
|
10122
10932
|
const isOptions = m.body.indexOf(",") >= 0;
|
|
10123
10933
|
if (!isSequence && !isOptions) {
|
|
10124
10934
|
if (m.post.match(/,(?!,).*\}/)) {
|
|
10125
|
-
|
|
10126
|
-
return expand_(
|
|
10935
|
+
str2 = m.pre + "{" + m.body + escClose + m.post;
|
|
10936
|
+
return expand_(str2, max, true);
|
|
10127
10937
|
}
|
|
10128
|
-
return [
|
|
10938
|
+
return [str2];
|
|
10129
10939
|
}
|
|
10130
10940
|
let n2;
|
|
10131
10941
|
if (isSequence) {
|
|
@@ -10570,7 +11380,7 @@ var init_ast = __esm({
|
|
|
10570
11380
|
}
|
|
10571
11381
|
return c3;
|
|
10572
11382
|
}
|
|
10573
|
-
static #parseAST(
|
|
11383
|
+
static #parseAST(str2, ast, pos, opt, extDepth) {
|
|
10574
11384
|
const maxDepth = opt.maxExtglobRecursion ?? 2;
|
|
10575
11385
|
let escaping = false;
|
|
10576
11386
|
let inBrace = false;
|
|
@@ -10579,8 +11389,8 @@ var init_ast = __esm({
|
|
|
10579
11389
|
if (ast.type === null) {
|
|
10580
11390
|
let i3 = pos;
|
|
10581
11391
|
let acc2 = "";
|
|
10582
|
-
while (i3 <
|
|
10583
|
-
const c3 =
|
|
11392
|
+
while (i3 < str2.length) {
|
|
11393
|
+
const c3 = str2.charAt(i3++);
|
|
10584
11394
|
if (escaping || c3 === "\\") {
|
|
10585
11395
|
escaping = !escaping;
|
|
10586
11396
|
acc2 += c3;
|
|
@@ -10603,12 +11413,12 @@ var init_ast = __esm({
|
|
|
10603
11413
|
acc2 += c3;
|
|
10604
11414
|
continue;
|
|
10605
11415
|
}
|
|
10606
|
-
const doRecurse = !opt.noext && isExtglobType(c3) &&
|
|
11416
|
+
const doRecurse = !opt.noext && isExtglobType(c3) && str2.charAt(i3) === "(" && extDepth <= maxDepth;
|
|
10607
11417
|
if (doRecurse) {
|
|
10608
11418
|
ast.push(acc2);
|
|
10609
11419
|
acc2 = "";
|
|
10610
11420
|
const ext2 = new _a(c3, ast);
|
|
10611
|
-
i3 = _a.#parseAST(
|
|
11421
|
+
i3 = _a.#parseAST(str2, ext2, i3, opt, extDepth + 1);
|
|
10612
11422
|
ast.push(ext2);
|
|
10613
11423
|
continue;
|
|
10614
11424
|
}
|
|
@@ -10621,8 +11431,8 @@ var init_ast = __esm({
|
|
|
10621
11431
|
let part = new _a(null, ast);
|
|
10622
11432
|
const parts = [];
|
|
10623
11433
|
let acc = "";
|
|
10624
|
-
while (i2 <
|
|
10625
|
-
const c3 =
|
|
11434
|
+
while (i2 < str2.length) {
|
|
11435
|
+
const c3 = str2.charAt(i2++);
|
|
10626
11436
|
if (escaping || c3 === "\\") {
|
|
10627
11437
|
escaping = !escaping;
|
|
10628
11438
|
acc += c3;
|
|
@@ -10645,7 +11455,7 @@ var init_ast = __esm({
|
|
|
10645
11455
|
acc += c3;
|
|
10646
11456
|
continue;
|
|
10647
11457
|
}
|
|
10648
|
-
const doRecurse = !opt.noext && isExtglobType(c3) &&
|
|
11458
|
+
const doRecurse = !opt.noext && isExtglobType(c3) && str2.charAt(i2) === "(" && /* c8 ignore start - the maxDepth is sufficient here */
|
|
10649
11459
|
(extDepth <= maxDepth || ast && ast.#canAdoptType(c3));
|
|
10650
11460
|
if (doRecurse) {
|
|
10651
11461
|
const depthAdd = ast && ast.#canAdoptType(c3) ? 0 : 1;
|
|
@@ -10653,7 +11463,7 @@ var init_ast = __esm({
|
|
|
10653
11463
|
acc = "";
|
|
10654
11464
|
const ext2 = new _a(c3, part);
|
|
10655
11465
|
part.push(ext2);
|
|
10656
|
-
i2 = _a.#parseAST(
|
|
11466
|
+
i2 = _a.#parseAST(str2, ext2, i2, opt, extDepth + depthAdd);
|
|
10657
11467
|
continue;
|
|
10658
11468
|
}
|
|
10659
11469
|
if (c3 === "|") {
|
|
@@ -10676,7 +11486,7 @@ var init_ast = __esm({
|
|
|
10676
11486
|
}
|
|
10677
11487
|
ast.type = null;
|
|
10678
11488
|
ast.#hasMagic = void 0;
|
|
10679
|
-
ast.#parts = [
|
|
11489
|
+
ast.#parts = [str2.substring(pos - 1)];
|
|
10680
11490
|
return i2;
|
|
10681
11491
|
}
|
|
10682
11492
|
#canAdoptWithSpace(child) {
|
|
@@ -11782,10 +12592,10 @@ var init_esm3 = __esm({
|
|
|
11782
12592
|
}
|
|
11783
12593
|
return filtered.join("/");
|
|
11784
12594
|
}).join("|");
|
|
11785
|
-
const [
|
|
11786
|
-
re = "^" +
|
|
12595
|
+
const [open3, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
|
|
12596
|
+
re = "^" + open3 + re + close + "$";
|
|
11787
12597
|
if (this.partial) {
|
|
11788
|
-
re = "^(?:\\/|" +
|
|
12598
|
+
re = "^(?:\\/|" + open3 + re.slice(1, -1) + close + ")$";
|
|
11789
12599
|
}
|
|
11790
12600
|
if (this.negate)
|
|
11791
12601
|
re = "^(?!" + re + ").+$";
|
|
@@ -12618,18 +13428,18 @@ var init_lang_detect = __esm({
|
|
|
12618
13428
|
});
|
|
12619
13429
|
|
|
12620
13430
|
// src/analyze/tree-sitter/cache.ts
|
|
12621
|
-
import { mkdir as
|
|
12622
|
-
import { dirname as
|
|
12623
|
-
import { homedir as
|
|
13431
|
+
import { mkdir as mkdir6, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
13432
|
+
import { dirname as dirname8, join as join9 } from "node:path";
|
|
13433
|
+
import { homedir as homedir4 } from "node:os";
|
|
12624
13434
|
import { createHash } from "node:crypto";
|
|
12625
13435
|
function defaultCacheRoot() {
|
|
12626
|
-
return process.env.ARCHME_TOOLS_DIR ??
|
|
13436
|
+
return process.env.ARCHME_TOOLS_DIR ?? join9(homedir4(), ".archme", "tools");
|
|
12627
13437
|
}
|
|
12628
13438
|
function grammarCacheDir(cacheRoot) {
|
|
12629
|
-
return
|
|
13439
|
+
return join9(cacheRoot, "grammars");
|
|
12630
13440
|
}
|
|
12631
13441
|
function cachedGrammarPath(cacheRoot, lang, version) {
|
|
12632
|
-
return
|
|
13442
|
+
return join9(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
|
|
12633
13443
|
}
|
|
12634
13444
|
async function fileExists(path7) {
|
|
12635
13445
|
try {
|
|
@@ -12650,7 +13460,7 @@ async function ensureGrammar(opts) {
|
|
|
12650
13460
|
if (await fileExists(target)) {
|
|
12651
13461
|
return { path: target, downloaded: false };
|
|
12652
13462
|
}
|
|
12653
|
-
await
|
|
13463
|
+
await mkdir6(dirname8(target), { recursive: true });
|
|
12654
13464
|
const url = `${opts.urlBase}/${opts.filename}`;
|
|
12655
13465
|
process.stderr.write(`fetching grammar: ${opts.lang}@${opts.version} (${url})
|
|
12656
13466
|
`);
|
|
@@ -12779,7 +13589,7 @@ var init_grammars = __esm({
|
|
|
12779
13589
|
});
|
|
12780
13590
|
|
|
12781
13591
|
// src/analyze/tree-sitter/parser.ts
|
|
12782
|
-
import { readFile as
|
|
13592
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
12783
13593
|
import { Parser, Language } from "web-tree-sitter";
|
|
12784
13594
|
async function init() {
|
|
12785
13595
|
if (initialized) return;
|
|
@@ -12802,7 +13612,7 @@ async function getLanguage(lang, cacheRoot = defaultCacheRoot()) {
|
|
|
12802
13612
|
filename: entry.filename,
|
|
12803
13613
|
sha256: entry.sha256
|
|
12804
13614
|
});
|
|
12805
|
-
const wasm = await
|
|
13615
|
+
const wasm = await readFile6(wasmPath);
|
|
12806
13616
|
const language = await Language.load(wasm);
|
|
12807
13617
|
languageCache.set(lang, language);
|
|
12808
13618
|
return language;
|
|
@@ -13198,7 +14008,7 @@ var init_output2 = __esm({
|
|
|
13198
14008
|
});
|
|
13199
14009
|
|
|
13200
14010
|
// src/analyze/commands/outline.ts
|
|
13201
|
-
import { readFile as
|
|
14011
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
13202
14012
|
async function outlineSource(source, file) {
|
|
13203
14013
|
const lang = detectLanguage(file);
|
|
13204
14014
|
if (!lang) {
|
|
@@ -13251,7 +14061,7 @@ async function runOutline(file) {
|
|
|
13251
14061
|
}
|
|
13252
14062
|
let source;
|
|
13253
14063
|
try {
|
|
13254
|
-
source = await
|
|
14064
|
+
source = await readFile7(file, "utf8");
|
|
13255
14065
|
} catch (err2) {
|
|
13256
14066
|
return envelope({
|
|
13257
14067
|
tool: "file",
|
|
@@ -13535,7 +14345,7 @@ var require_ignore = __commonJS({
|
|
|
13535
14345
|
// Zero, one or several directories
|
|
13536
14346
|
// should not use '*', or it will be replaced by the next replacer
|
|
13537
14347
|
// Check if it is not the last `'/**'`
|
|
13538
|
-
(_, index,
|
|
14348
|
+
(_, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
|
|
13539
14349
|
],
|
|
13540
14350
|
// normal intermediate wildcards
|
|
13541
14351
|
[
|
|
@@ -13634,12 +14444,12 @@ var require_ignore = __commonJS({
|
|
|
13634
14444
|
return this._make(MODE_CHECK_IGNORE, key);
|
|
13635
14445
|
}
|
|
13636
14446
|
_make(mode, key) {
|
|
13637
|
-
const
|
|
14447
|
+
const str2 = this.regexPrefix.replace(
|
|
13638
14448
|
REGEX_REPLACE_TRAILING_WILDCARD,
|
|
13639
14449
|
// It does not need to bind pattern
|
|
13640
14450
|
TRAILING_WILD_CARD_REPLACERS[mode]
|
|
13641
14451
|
);
|
|
13642
|
-
const regex = this.ignoreCase ? new RegExp(
|
|
14452
|
+
const regex = this.ignoreCase ? new RegExp(str2, "i") : new RegExp(str2);
|
|
13643
14453
|
return define(this, key, regex);
|
|
13644
14454
|
}
|
|
13645
14455
|
};
|
|
@@ -13843,7 +14653,7 @@ var require_ignore = __commonJS({
|
|
|
13843
14653
|
var factory = (options) => new Ignore(options);
|
|
13844
14654
|
var isPathValid = (path7) => checkPath(path7 && checkPath.convert(path7), path7, RETURN_FALSE);
|
|
13845
14655
|
var setupWindows = () => {
|
|
13846
|
-
const makePosix = (
|
|
14656
|
+
const makePosix = (str2) => /^\\\\\?\\/.test(str2) || /["<>|\u0000-\u001F]+/u.test(str2) ? str2 : str2.replace(/\\/g, "/");
|
|
13847
14657
|
checkPath.convert = makePosix;
|
|
13848
14658
|
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
13849
14659
|
checkPath.isNotRelative = (path7) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path7) || isNotRelative(path7);
|
|
@@ -13862,12 +14672,12 @@ var require_ignore = __commonJS({
|
|
|
13862
14672
|
});
|
|
13863
14673
|
|
|
13864
14674
|
// src/analyze/repo-walk.ts
|
|
13865
|
-
import { readdir, readFile as
|
|
13866
|
-
import { join as
|
|
14675
|
+
import { readdir, readFile as readFile8, stat as stat4 } from "node:fs/promises";
|
|
14676
|
+
import { join as join10, relative } from "node:path";
|
|
13867
14677
|
async function loadGitignore(root) {
|
|
13868
14678
|
const ig = (0, import_ignore.default)();
|
|
13869
14679
|
try {
|
|
13870
|
-
const text = await
|
|
14680
|
+
const text = await readFile8(join10(root, ".gitignore"), "utf8");
|
|
13871
14681
|
ig.add(text);
|
|
13872
14682
|
} catch {
|
|
13873
14683
|
ig.add(["node_modules", "dist", "build", ".next", "coverage", ".turbo"]);
|
|
@@ -13881,7 +14691,7 @@ async function* walkRepoFiles(root, filter2 = {}) {
|
|
|
13881
14691
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
13882
14692
|
for (const e of entries) {
|
|
13883
14693
|
if (ALWAYS_SKIP.has(e.name)) continue;
|
|
13884
|
-
const full =
|
|
14694
|
+
const full = join10(dir, e.name);
|
|
13885
14695
|
const rel = relative(root, full);
|
|
13886
14696
|
if (e.isDirectory()) {
|
|
13887
14697
|
if (ig.ignores(rel + "/")) continue;
|
|
@@ -13905,7 +14715,7 @@ async function collectRepoFiles(root, filter2 = {}) {
|
|
|
13905
14715
|
}
|
|
13906
14716
|
async function gitListFiles(root) {
|
|
13907
14717
|
try {
|
|
13908
|
-
await stat4(
|
|
14718
|
+
await stat4(join10(root, ".git"));
|
|
13909
14719
|
} catch {
|
|
13910
14720
|
return null;
|
|
13911
14721
|
}
|
|
@@ -13946,11 +14756,11 @@ var init_repo_walk = __esm({
|
|
|
13946
14756
|
});
|
|
13947
14757
|
|
|
13948
14758
|
// src/analyze/commands/repo.ts
|
|
13949
|
-
import { join as
|
|
14759
|
+
import { join as join11 } from "node:path";
|
|
13950
14760
|
async function* walkRepo(root, opts = {}) {
|
|
13951
14761
|
const files = await collectRepoFilesWithGit(root, opts);
|
|
13952
14762
|
for (const rel of files) {
|
|
13953
|
-
const env = await runOutline(
|
|
14763
|
+
const env = await runOutline(join11(root, rel));
|
|
13954
14764
|
yield env;
|
|
13955
14765
|
}
|
|
13956
14766
|
}
|
|
@@ -13964,9 +14774,9 @@ var init_repo = __esm({
|
|
|
13964
14774
|
|
|
13965
14775
|
// src/analyze/matchers/file.ts
|
|
13966
14776
|
import { stat as stat5 } from "node:fs/promises";
|
|
13967
|
-
import { join as
|
|
14777
|
+
import { join as join12 } from "node:path";
|
|
13968
14778
|
async function matchFileAnchor(anchor, repoRoot) {
|
|
13969
|
-
const full =
|
|
14779
|
+
const full = join12(repoRoot, anchor.path);
|
|
13970
14780
|
try {
|
|
13971
14781
|
const s = await stat5(full);
|
|
13972
14782
|
if (!s.isFile()) {
|
|
@@ -13997,9 +14807,9 @@ var init_file = __esm({
|
|
|
13997
14807
|
|
|
13998
14808
|
// src/analyze/matchers/directory.ts
|
|
13999
14809
|
import { stat as stat6 } from "node:fs/promises";
|
|
14000
|
-
import { join as
|
|
14810
|
+
import { join as join13 } from "node:path";
|
|
14001
14811
|
async function matchDirectoryAnchor(anchor, repoRoot) {
|
|
14002
|
-
const full =
|
|
14812
|
+
const full = join13(repoRoot, anchor.path);
|
|
14003
14813
|
try {
|
|
14004
14814
|
const s = await stat6(full);
|
|
14005
14815
|
if (!s.isDirectory()) {
|
|
@@ -14068,9 +14878,9 @@ var init_findSymbol = __esm({
|
|
|
14068
14878
|
|
|
14069
14879
|
// src/analyze/matchers/symbol.ts
|
|
14070
14880
|
import { stat as stat7 } from "node:fs/promises";
|
|
14071
|
-
import { join as
|
|
14881
|
+
import { join as join14 } from "node:path";
|
|
14072
14882
|
async function matchSymbolAnchor(anchor, repoRoot) {
|
|
14073
|
-
const full =
|
|
14883
|
+
const full = join14(repoRoot, anchor.path);
|
|
14074
14884
|
try {
|
|
14075
14885
|
await stat7(full);
|
|
14076
14886
|
} catch {
|
|
@@ -14194,7 +15004,7 @@ __export(dispatch_exports, {
|
|
|
14194
15004
|
augmentChangesWithSymbolsAtSha: () => augmentChangesWithSymbolsAtSha,
|
|
14195
15005
|
dispatchAnalyze: () => dispatchAnalyze
|
|
14196
15006
|
});
|
|
14197
|
-
import { readFile as
|
|
15007
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
14198
15008
|
import { resolve } from "node:path";
|
|
14199
15009
|
async function dispatchAnalyze(args) {
|
|
14200
15010
|
const sub = args[0];
|
|
@@ -14335,7 +15145,7 @@ async function cmdMatch(args) {
|
|
|
14335
15145
|
console.error("archme analyze match: <anchor-spec.json> required");
|
|
14336
15146
|
return 2;
|
|
14337
15147
|
}
|
|
14338
|
-
const text = await
|
|
15148
|
+
const text = await readFile9(path7, "utf8");
|
|
14339
15149
|
const anchors = JSON.parse(text);
|
|
14340
15150
|
if (!Array.isArray(anchors)) {
|
|
14341
15151
|
console.error("archme analyze match: spec must be a JSON array of anchors");
|
|
@@ -14386,234 +15196,102 @@ var init_dispatch = __esm({
|
|
|
14386
15196
|
init_prefetch();
|
|
14387
15197
|
init_output2();
|
|
14388
15198
|
}
|
|
14389
|
-
});
|
|
14390
|
-
|
|
14391
|
-
// src/daemon/service/singleInstanceLock.ts
|
|
14392
|
-
import { mkdir as
|
|
14393
|
-
import { dirname as
|
|
14394
|
-
import { lock } from "proper-lockfile";
|
|
14395
|
-
async function acquireSingleInstanceLock(pidfile) {
|
|
14396
|
-
await
|
|
14397
|
-
try {
|
|
14398
|
-
await writeFile7(pidfile, String(process.pid), { flag: "wx" });
|
|
14399
|
-
} catch (err2) {
|
|
14400
|
-
if (err2.code !== "EEXIST") throw err2;
|
|
14401
|
-
}
|
|
14402
|
-
let release;
|
|
14403
|
-
try {
|
|
14404
|
-
release = await lock(pidfile, {
|
|
14405
|
-
// `stale` here is proper-lockfile's own mtime-refresh window — unrelated
|
|
14406
|
-
// to the daemon's Supabase heartbeat. Auto-released after 30s of mtime
|
|
14407
|
-
// staleness if the previous holder crashed without releasing.
|
|
14408
|
-
retries: 0,
|
|
14409
|
-
stale: 3e4
|
|
14410
|
-
});
|
|
14411
|
-
} catch {
|
|
14412
|
-
let heldByPid = null;
|
|
14413
|
-
try {
|
|
14414
|
-
heldByPid = Number((await readFile9(pidfile, "utf8")).trim()) || null;
|
|
14415
|
-
} catch {
|
|
14416
|
-
heldByPid = null;
|
|
14417
|
-
}
|
|
14418
|
-
throw new SingleInstanceLockedError(pidfile, heldByPid);
|
|
14419
|
-
}
|
|
14420
|
-
await writeFile7(pidfile, String(process.pid), { flag: "w" });
|
|
14421
|
-
return {
|
|
14422
|
-
async release() {
|
|
14423
|
-
try {
|
|
14424
|
-
await release();
|
|
14425
|
-
} catch {
|
|
14426
|
-
}
|
|
14427
|
-
}
|
|
14428
|
-
};
|
|
14429
|
-
}
|
|
14430
|
-
var SINGLE_INSTANCE_EXIT_CODE, LOCK_HELD_MESSAGE, SingleInstanceLockedError;
|
|
14431
|
-
var init_singleInstanceLock = __esm({
|
|
14432
|
-
"src/daemon/service/singleInstanceLock.ts"() {
|
|
14433
|
-
"use strict";
|
|
14434
|
-
SINGLE_INSTANCE_EXIT_CODE = 64;
|
|
14435
|
-
LOCK_HELD_MESSAGE = "already holds";
|
|
14436
|
-
SingleInstanceLockedError = class extends Error {
|
|
14437
|
-
heldByPid;
|
|
14438
|
-
constructor(pidfile, heldByPid) {
|
|
14439
|
-
super(
|
|
14440
|
-
heldByPid != null ? `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile} (pid ${heldByPid})` : `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile}`
|
|
14441
|
-
);
|
|
14442
|
-
this.heldByPid = heldByPid;
|
|
14443
|
-
this.name = "SingleInstanceLockedError";
|
|
14444
|
-
}
|
|
14445
|
-
};
|
|
14446
|
-
}
|
|
14447
|
-
});
|
|
14448
|
-
|
|
14449
|
-
// src/daemon/semaphore.ts
|
|
14450
|
-
var Semaphore;
|
|
14451
|
-
var init_semaphore = __esm({
|
|
14452
|
-
"src/daemon/semaphore.ts"() {
|
|
14453
|
-
"use strict";
|
|
14454
|
-
Semaphore = class {
|
|
14455
|
-
constructor(size) {
|
|
14456
|
-
this.size = size;
|
|
14457
|
-
}
|
|
14458
|
-
inUse = 0;
|
|
14459
|
-
waiters = [];
|
|
14460
|
-
available() {
|
|
14461
|
-
return Math.max(0, this.size - this.inUse);
|
|
14462
|
-
}
|
|
14463
|
-
tryAcquire() {
|
|
14464
|
-
if (this.inUse >= this.size) return null;
|
|
14465
|
-
this.inUse++;
|
|
14466
|
-
return this.makeRelease();
|
|
14467
|
-
}
|
|
14468
|
-
acquire() {
|
|
14469
|
-
const slot = this.tryAcquire();
|
|
14470
|
-
if (slot) return Promise.resolve(slot);
|
|
14471
|
-
return new Promise((resolve6) => this.waiters.push(resolve6));
|
|
14472
|
-
}
|
|
14473
|
-
makeRelease() {
|
|
14474
|
-
let released = false;
|
|
14475
|
-
return () => {
|
|
14476
|
-
if (released) return;
|
|
14477
|
-
released = true;
|
|
14478
|
-
const next = this.waiters.shift();
|
|
14479
|
-
if (next) {
|
|
14480
|
-
next(this.makeRelease());
|
|
14481
|
-
} else {
|
|
14482
|
-
this.inUse--;
|
|
14483
|
-
}
|
|
14484
|
-
};
|
|
14485
|
-
}
|
|
14486
|
-
};
|
|
14487
|
-
}
|
|
14488
|
-
});
|
|
14489
|
-
|
|
14490
|
-
// src/scan/oauthClient.ts
|
|
14491
|
-
function computeExpiresAt(expiresInSeconds) {
|
|
14492
|
-
const seconds = expiresInSeconds ?? DEFAULT_TOKEN_EXPIRES_IN_SECONDS;
|
|
14493
|
-
return Date.now() + seconds * 1e3;
|
|
14494
|
-
}
|
|
14495
|
-
function isPermanentRefreshError(body, status) {
|
|
14496
|
-
if (status >= 500) return false;
|
|
14497
|
-
if (body.error === "invalid_grant") return true;
|
|
14498
|
-
const errorCode = typeof body.error_code === "string" ? body.error_code.toLowerCase() : "";
|
|
14499
|
-
if (errorCode.startsWith("refresh_token")) return true;
|
|
14500
|
-
const message = [body.error_description, body.msg, body.error, body.message].filter((v) => typeof v === "string").join(" ").toLowerCase();
|
|
14501
|
-
return /invalid refresh token|refresh token not found|refresh token already used/.test(message);
|
|
14502
|
-
}
|
|
14503
|
-
async function refreshOAuthCredentialsWith(refreshToken, cfg) {
|
|
14504
|
-
let response;
|
|
14505
|
-
try {
|
|
14506
|
-
response = await fetch(`${cfg.supabaseUrl}/auth/v1/oauth/token`, {
|
|
14507
|
-
method: "POST",
|
|
14508
|
-
headers: {
|
|
14509
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
14510
|
-
apikey: cfg.anonKey
|
|
14511
|
-
},
|
|
14512
|
-
body: new URLSearchParams({
|
|
14513
|
-
grant_type: "refresh_token",
|
|
14514
|
-
refresh_token: refreshToken,
|
|
14515
|
-
client_id: cfg.clientId
|
|
14516
|
-
})
|
|
14517
|
-
});
|
|
14518
|
-
} catch (err2) {
|
|
14519
|
-
return {
|
|
14520
|
-
ok: false,
|
|
14521
|
-
permanent: false,
|
|
14522
|
-
error: `network error: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
14523
|
-
};
|
|
14524
|
-
}
|
|
14525
|
-
const body = await parseJsonResponse(response);
|
|
14526
|
-
if (!response.ok) {
|
|
14527
|
-
return {
|
|
14528
|
-
ok: false,
|
|
14529
|
-
permanent: isPermanentRefreshError(body, response.status),
|
|
14530
|
-
error: responseError(body, `refresh failed (${response.status})`)
|
|
14531
|
-
};
|
|
14532
|
-
}
|
|
14533
|
-
if (typeof body.access_token !== "string" || !body.access_token) {
|
|
14534
|
-
return { ok: false, permanent: false, error: "refresh response did not include an access token" };
|
|
14535
|
-
}
|
|
14536
|
-
return {
|
|
14537
|
-
ok: true,
|
|
14538
|
-
tokens: {
|
|
14539
|
-
access_token: body.access_token,
|
|
14540
|
-
refresh_token: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
|
|
14541
|
-
expires_in: typeof body.expires_in === "number" ? body.expires_in : Number(body.expires_in) || void 0,
|
|
14542
|
-
token_type: typeof body.token_type === "string" ? body.token_type : void 0
|
|
14543
|
-
}
|
|
14544
|
-
};
|
|
14545
|
-
}
|
|
14546
|
-
async function getOAuthUserInfoWith(accessToken, cfg) {
|
|
14547
|
-
const response = await fetch(`${cfg.supabaseUrl}/auth/v1/oauth/userinfo`, {
|
|
14548
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
14549
|
-
});
|
|
14550
|
-
if (!response.ok) return {};
|
|
14551
|
-
return await parseJsonResponse(response);
|
|
14552
|
-
}
|
|
14553
|
-
function resolveUserId(userInfo2, storedUserId) {
|
|
14554
|
-
return userInfo2.sub ?? storedUserId ?? null;
|
|
14555
|
-
}
|
|
14556
|
-
function isAuthError(err2) {
|
|
14557
|
-
if (!err2 || typeof err2 !== "object") return false;
|
|
14558
|
-
const msg = ("message" in err2 ? String(err2.message) : "").toLowerCase();
|
|
14559
|
-
const code = ("code" in err2 ? String(err2.code) : "").toLowerCase();
|
|
14560
|
-
const status = "status" in err2 ? Number(err2.status) : 0;
|
|
14561
|
-
return status === 401 || code === "pgrst301" || msg.includes("jwt expired") || msg.includes("invalid jwt") || msg.includes("not authenticated");
|
|
14562
|
-
}
|
|
14563
|
-
async function withAuthRetry(label, fn, reLogin) {
|
|
14564
|
-
const first = await fn();
|
|
14565
|
-
if (!first.error || !isAuthError(first.error)) return first;
|
|
14566
|
-
console.warn(`${label}: auth error on first attempt, re-logging in and retrying once`);
|
|
14567
|
-
try {
|
|
14568
|
-
await reLogin();
|
|
14569
|
-
} catch (err2) {
|
|
14570
|
-
console.error(`${label}: re-login failed:`, err2);
|
|
14571
|
-
return first;
|
|
14572
|
-
}
|
|
14573
|
-
return await fn();
|
|
14574
|
-
}
|
|
14575
|
-
async function fetchWithAuthRetry(label, doFetch, reLogin) {
|
|
14576
|
-
const first = await doFetch();
|
|
14577
|
-
if (first.status !== 401) return first;
|
|
14578
|
-
console.warn(`${label}: 401 on first attempt, re-logging in and retrying once`);
|
|
15199
|
+
});
|
|
15200
|
+
|
|
15201
|
+
// src/daemon/service/singleInstanceLock.ts
|
|
15202
|
+
import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
|
|
15203
|
+
import { dirname as dirname9 } from "node:path";
|
|
15204
|
+
import { lock } from "proper-lockfile";
|
|
15205
|
+
async function acquireSingleInstanceLock(pidfile) {
|
|
15206
|
+
await mkdir7(dirname9(pidfile), { recursive: true });
|
|
14579
15207
|
try {
|
|
14580
|
-
await
|
|
15208
|
+
await writeFile7(pidfile, String(process.pid), { flag: "wx" });
|
|
14581
15209
|
} catch (err2) {
|
|
14582
|
-
|
|
14583
|
-
return first;
|
|
15210
|
+
if (err2.code !== "EEXIST") throw err2;
|
|
14584
15211
|
}
|
|
14585
|
-
|
|
14586
|
-
}
|
|
14587
|
-
async function parseJsonResponse(response) {
|
|
14588
|
-
const text = await response.text();
|
|
14589
|
-
if (!text) return {};
|
|
15212
|
+
let release;
|
|
14590
15213
|
try {
|
|
14591
|
-
|
|
15214
|
+
release = await lock(pidfile, {
|
|
15215
|
+
// `stale` here is proper-lockfile's own mtime-refresh window — unrelated
|
|
15216
|
+
// to the daemon's Supabase heartbeat. Auto-released after 30s of mtime
|
|
15217
|
+
// staleness if the previous holder crashed without releasing.
|
|
15218
|
+
retries: 0,
|
|
15219
|
+
stale: 3e4
|
|
15220
|
+
});
|
|
14592
15221
|
} catch {
|
|
14593
|
-
|
|
15222
|
+
let heldByPid = null;
|
|
15223
|
+
try {
|
|
15224
|
+
heldByPid = Number((await readFile10(pidfile, "utf8")).trim()) || null;
|
|
15225
|
+
} catch {
|
|
15226
|
+
heldByPid = null;
|
|
15227
|
+
}
|
|
15228
|
+
throw new SingleInstanceLockedError(pidfile, heldByPid);
|
|
14594
15229
|
}
|
|
15230
|
+
await writeFile7(pidfile, String(process.pid), { flag: "w" });
|
|
15231
|
+
return {
|
|
15232
|
+
async release() {
|
|
15233
|
+
try {
|
|
15234
|
+
await release();
|
|
15235
|
+
} catch {
|
|
15236
|
+
}
|
|
15237
|
+
}
|
|
15238
|
+
};
|
|
14595
15239
|
}
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
15240
|
+
var SINGLE_INSTANCE_EXIT_CODE, LOCK_HELD_MESSAGE, SingleInstanceLockedError;
|
|
15241
|
+
var init_singleInstanceLock = __esm({
|
|
15242
|
+
"src/daemon/service/singleInstanceLock.ts"() {
|
|
15243
|
+
"use strict";
|
|
15244
|
+
SINGLE_INSTANCE_EXIT_CODE = 64;
|
|
15245
|
+
LOCK_HELD_MESSAGE = "already holds";
|
|
15246
|
+
SingleInstanceLockedError = class extends Error {
|
|
15247
|
+
heldByPid;
|
|
15248
|
+
constructor(pidfile, heldByPid) {
|
|
15249
|
+
super(
|
|
15250
|
+
heldByPid != null ? `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile} (pid ${heldByPid})` : `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile}`
|
|
15251
|
+
);
|
|
15252
|
+
this.heldByPid = heldByPid;
|
|
15253
|
+
this.name = "SingleInstanceLockedError";
|
|
15254
|
+
}
|
|
15255
|
+
};
|
|
14600
15256
|
}
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
var
|
|
14605
|
-
|
|
15257
|
+
});
|
|
15258
|
+
|
|
15259
|
+
// src/daemon/semaphore.ts
|
|
15260
|
+
var Semaphore;
|
|
15261
|
+
var init_semaphore = __esm({
|
|
15262
|
+
"src/daemon/semaphore.ts"() {
|
|
14606
15263
|
"use strict";
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
this.
|
|
14615
|
-
|
|
14616
|
-
|
|
15264
|
+
Semaphore = class {
|
|
15265
|
+
constructor(size) {
|
|
15266
|
+
this.size = size;
|
|
15267
|
+
}
|
|
15268
|
+
inUse = 0;
|
|
15269
|
+
waiters = [];
|
|
15270
|
+
available() {
|
|
15271
|
+
return Math.max(0, this.size - this.inUse);
|
|
15272
|
+
}
|
|
15273
|
+
tryAcquire() {
|
|
15274
|
+
if (this.inUse >= this.size) return null;
|
|
15275
|
+
this.inUse++;
|
|
15276
|
+
return this.makeRelease();
|
|
15277
|
+
}
|
|
15278
|
+
acquire() {
|
|
15279
|
+
const slot = this.tryAcquire();
|
|
15280
|
+
if (slot) return Promise.resolve(slot);
|
|
15281
|
+
return new Promise((resolve6) => this.waiters.push(resolve6));
|
|
15282
|
+
}
|
|
15283
|
+
makeRelease() {
|
|
15284
|
+
let released = false;
|
|
15285
|
+
return () => {
|
|
15286
|
+
if (released) return;
|
|
15287
|
+
released = true;
|
|
15288
|
+
const next = this.waiters.shift();
|
|
15289
|
+
if (next) {
|
|
15290
|
+
next(this.makeRelease());
|
|
15291
|
+
} else {
|
|
15292
|
+
this.inUse--;
|
|
15293
|
+
}
|
|
15294
|
+
};
|
|
14617
15295
|
}
|
|
14618
15296
|
};
|
|
14619
15297
|
}
|
|
@@ -14625,6 +15303,9 @@ var init_sessionAuth = __esm({
|
|
|
14625
15303
|
"src/scan/sessionAuth.ts"() {
|
|
14626
15304
|
"use strict";
|
|
14627
15305
|
init_credentials();
|
|
15306
|
+
init_keyRing();
|
|
15307
|
+
init_accountKey();
|
|
15308
|
+
init_keySession();
|
|
14628
15309
|
init_oauthClient();
|
|
14629
15310
|
SessionAuth = class _SessionAuth {
|
|
14630
15311
|
constructor(cfg) {
|
|
@@ -14637,6 +15318,9 @@ var init_sessionAuth = __esm({
|
|
|
14637
15318
|
deviceId = null;
|
|
14638
15319
|
orgId = null;
|
|
14639
15320
|
userId = null;
|
|
15321
|
+
// "key" sessions are plain GoTrue sessions (password grant); everything else
|
|
15322
|
+
// is the OAuth server's. Set from the stored credential at sign-in.
|
|
15323
|
+
kind = "oauth";
|
|
14640
15324
|
// The userinfo resolved at first sign-in. The user_id is stable for the
|
|
14641
15325
|
// session's lifetime, so once it's known we reuse this and skip the per-refresh
|
|
14642
15326
|
// userinfo round-trip (the recurring refresh path discards the returned value;
|
|
@@ -14663,6 +15347,9 @@ var init_sessionAuth = __esm({
|
|
|
14663
15347
|
getRefreshToken() {
|
|
14664
15348
|
return this.refreshToken;
|
|
14665
15349
|
}
|
|
15350
|
+
getKind() {
|
|
15351
|
+
return this.kind;
|
|
15352
|
+
}
|
|
14666
15353
|
onAccessTokenChange(listener) {
|
|
14667
15354
|
this.listeners.add(listener);
|
|
14668
15355
|
return () => this.listeners.delete(listener);
|
|
@@ -14675,6 +15362,7 @@ var init_sessionAuth = __esm({
|
|
|
14675
15362
|
}
|
|
14676
15363
|
/** Test seam: seed state without going through the store. */
|
|
14677
15364
|
__seedForTesting(state) {
|
|
15365
|
+
if (state.kind !== void 0) this.kind = state.kind;
|
|
14678
15366
|
if (state.accessToken !== void 0) this.accessToken = state.accessToken;
|
|
14679
15367
|
if (state.accessTokenExpiresAtMs !== void 0) this.accessTokenExpiresAtMs = state.accessTokenExpiresAtMs;
|
|
14680
15368
|
if (state.refreshToken !== void 0) this.refreshToken = state.refreshToken;
|
|
@@ -14712,11 +15400,12 @@ var init_sessionAuth = __esm({
|
|
|
14712
15400
|
this.refreshToken = nextRefreshToken;
|
|
14713
15401
|
let userInfo2 = this.lastUserInfo;
|
|
14714
15402
|
if (this.userId === null || userInfo2 === null) {
|
|
14715
|
-
userInfo2 = await getOAuthUserInfoWith(this.accessToken, this.cfg);
|
|
15403
|
+
userInfo2 = this.kind === "key" ? await getKeyUserInfo(this.accessToken, this.cfg) : await getOAuthUserInfoWith(this.accessToken, this.cfg);
|
|
14716
15404
|
this.userId = resolveUserId(userInfo2, ctx.fallbackUserId);
|
|
14717
15405
|
this.lastUserInfo = userInfo2;
|
|
14718
15406
|
}
|
|
14719
15407
|
const saved = await saveCredentials({
|
|
15408
|
+
kind: this.kind,
|
|
14720
15409
|
refresh_token: nextRefreshToken,
|
|
14721
15410
|
access_token: this.accessToken,
|
|
14722
15411
|
org_id: ctx.orgId,
|
|
@@ -14813,14 +15502,14 @@ var init_sessionAuth = __esm({
|
|
|
14813
15502
|
if (!refreshToken || !deviceId || !orgId) {
|
|
14814
15503
|
throw new LoadStoredAndRefreshError("missing_device", "not signed in; cannot mint MCP bearer");
|
|
14815
15504
|
}
|
|
14816
|
-
const refreshed = await
|
|
15505
|
+
const refreshed = await this.refreshOrRecover(refreshToken, this.userId);
|
|
14817
15506
|
if (!refreshed.ok) {
|
|
14818
15507
|
if (refreshed.permanent) {
|
|
14819
15508
|
await clearCredential(this.cfg.supabaseUrl, deviceId);
|
|
14820
15509
|
this.clearTokensOnly();
|
|
14821
15510
|
throw new LoadStoredAndRefreshError(
|
|
14822
15511
|
"invalid_grant",
|
|
14823
|
-
`${errorPrefix} failed permanently: ${refreshed.error}.
|
|
15512
|
+
`${errorPrefix} failed permanently: ${refreshed.error}. ${this.reloginHint()}`
|
|
14824
15513
|
);
|
|
14825
15514
|
}
|
|
14826
15515
|
throw new LoadStoredAndRefreshError("transient", `${errorPrefix} failed: ${refreshed.error}`);
|
|
@@ -14838,6 +15527,33 @@ var init_sessionAuth = __esm({
|
|
|
14838
15527
|
if (this.refreshInFlight === p) this.refreshInFlight = null;
|
|
14839
15528
|
});
|
|
14840
15529
|
}
|
|
15530
|
+
async refreshOrRecover(refreshToken, userId) {
|
|
15531
|
+
if (this.kind !== "key") return refreshOAuthCredentialsWith(refreshToken, this.cfg);
|
|
15532
|
+
const refreshed = await refreshKeySession(refreshToken, this.cfg);
|
|
15533
|
+
if (refreshed.ok || !refreshed.permanent) return refreshed;
|
|
15534
|
+
return this.signInWithSavedKey(userId, refreshed.error);
|
|
15535
|
+
}
|
|
15536
|
+
reloginHint() {
|
|
15537
|
+
return this.kind === "key" ? "Run `archme login --key` to sign in with your key again." : "Run `archme login` to pair again.";
|
|
15538
|
+
}
|
|
15539
|
+
// A keyed account's refresh token can die (reuse detection, a long sleep) while
|
|
15540
|
+
// its key is still good, so sign in again from the key ring rather than
|
|
15541
|
+
// clearing. Permanent only when there is no saved key or the key is refused;
|
|
15542
|
+
// the key ring itself is never modified here.
|
|
15543
|
+
async signInWithSavedKey(userId, refreshError) {
|
|
15544
|
+
let saved;
|
|
15545
|
+
try {
|
|
15546
|
+
saved = userId ? await findKey(this.cfg.supabaseUrl, userId) : null;
|
|
15547
|
+
} catch (err2) {
|
|
15548
|
+
return { ok: false, permanent: false, error: `${refreshError}; ${err2 instanceof Error ? err2.message : String(err2)}` };
|
|
15549
|
+
}
|
|
15550
|
+
const parsed = parseAccountKey(saved?.key);
|
|
15551
|
+
if (!parsed) return { ok: false, permanent: true, error: `${refreshError}; no saved key for this account` };
|
|
15552
|
+
console.error(`auth: refresh token rejected (${refreshError}); signing in again with the saved key for ${parsed.handle}`);
|
|
15553
|
+
const granted = await passwordGrant(parsed.email, parsed.secret, this.cfg);
|
|
15554
|
+
if (granted.ok) return granted;
|
|
15555
|
+
return granted.permanent ? { ok: false, permanent: true, error: `the saved key for ${parsed.handle} was rejected: ${granted.error}` } : granted;
|
|
15556
|
+
}
|
|
14841
15557
|
async getMcpAccessToken() {
|
|
14842
15558
|
if (this.accessToken && this.accessTokenExpiresAtMs - Date.now() >= this.cfg.preSpawnMinLifetimeMs) {
|
|
14843
15559
|
return this.accessToken;
|
|
@@ -14857,13 +15573,14 @@ var init_sessionAuth = __esm({
|
|
|
14857
15573
|
"stored archme credentials are missing device registration; run `archme login` again"
|
|
14858
15574
|
);
|
|
14859
15575
|
}
|
|
14860
|
-
|
|
15576
|
+
this.kind = credentialKind(stored);
|
|
15577
|
+
const refreshed = await this.refreshOrRecover(stored.refresh_token, stored.user_id ?? null);
|
|
14861
15578
|
if (!refreshed.ok) {
|
|
14862
15579
|
if (refreshed.permanent) {
|
|
14863
15580
|
await clearCredential(this.cfg.supabaseUrl, stored.device_id);
|
|
14864
15581
|
throw new LoadStoredAndRefreshError(
|
|
14865
15582
|
"invalid_grant",
|
|
14866
|
-
`stored archme credentials are invalid or revoked: ${refreshed.error}.
|
|
15583
|
+
`stored archme credentials are invalid or revoked: ${refreshed.error}. ${this.reloginHint()}`
|
|
14867
15584
|
);
|
|
14868
15585
|
}
|
|
14869
15586
|
throw new LoadStoredAndRefreshError("transient", `token refresh failed (transient): ${refreshed.error}`);
|
|
@@ -14876,6 +15593,7 @@ var init_sessionAuth = __esm({
|
|
|
14876
15593
|
});
|
|
14877
15594
|
return {
|
|
14878
15595
|
user: userInfo2,
|
|
15596
|
+
kind: this.kind,
|
|
14879
15597
|
orgId: stored.org_id,
|
|
14880
15598
|
deviceId: stored.device_id,
|
|
14881
15599
|
source,
|
|
@@ -14900,6 +15618,41 @@ var init_sessionAuth = __esm({
|
|
|
14900
15618
|
});
|
|
14901
15619
|
|
|
14902
15620
|
// src/scan/bearer.ts
|
|
15621
|
+
var bearer_exports = {};
|
|
15622
|
+
__export(bearer_exports, {
|
|
15623
|
+
LoadStoredAndRefreshError: () => LoadStoredAndRefreshError,
|
|
15624
|
+
REFRESH_RETRY_BACKOFF_MS: () => REFRESH_RETRY_BACKOFF_MS,
|
|
15625
|
+
__seedBearerForTesting: () => __seedBearerForTesting,
|
|
15626
|
+
clearRefreshTimer: () => clearRefreshTimer,
|
|
15627
|
+
computeExpiresAt: () => computeExpiresAt,
|
|
15628
|
+
fetchWithAuthRetry: () => fetchWithAuthRetry,
|
|
15629
|
+
getCurrentAccessToken: () => getCurrentAccessToken,
|
|
15630
|
+
getCurrentDeviceId: () => getCurrentDeviceId,
|
|
15631
|
+
getCurrentOrgId: () => getCurrentOrgId,
|
|
15632
|
+
getCurrentRefreshToken: () => getCurrentRefreshToken,
|
|
15633
|
+
getCurrentUserId: () => getCurrentUserId,
|
|
15634
|
+
getMcpAccessToken: () => getMcpAccessToken,
|
|
15635
|
+
getOAuthUserInfo: () => getOAuthUserInfo,
|
|
15636
|
+
initBearer: () => initBearer,
|
|
15637
|
+
isAuthError: () => isAuthError,
|
|
15638
|
+
isPermanentRefreshError: () => isPermanentRefreshError,
|
|
15639
|
+
loadStoredAndRefresh: () => loadStoredAndRefresh,
|
|
15640
|
+
onAccessTokenChange: () => onAccessTokenChange,
|
|
15641
|
+
parseJsonResponse: () => parseJsonResponse,
|
|
15642
|
+
refreshOAuthCredentials: () => refreshOAuthCredentials,
|
|
15643
|
+
resetBearer: () => resetBearer,
|
|
15644
|
+
resolveUserId: () => resolveUserId,
|
|
15645
|
+
responseError: () => responseError,
|
|
15646
|
+
runScheduledRefresh: () => runScheduledRefresh,
|
|
15647
|
+
scheduleCredentialRefresh: () => scheduleCredentialRefresh,
|
|
15648
|
+
withAuthRetry: () => withAuthRetry
|
|
15649
|
+
});
|
|
15650
|
+
async function refreshOAuthCredentials(refreshToken) {
|
|
15651
|
+
return refreshOAuthCredentialsWith(refreshToken, requireConfig());
|
|
15652
|
+
}
|
|
15653
|
+
async function getOAuthUserInfo(accessToken) {
|
|
15654
|
+
return getOAuthUserInfoWith(accessToken, requireConfig());
|
|
15655
|
+
}
|
|
14903
15656
|
function configsEqual(a2, b) {
|
|
14904
15657
|
return a2.supabaseUrl === b.supabaseUrl && a2.anonKey === b.anonKey && a2.clientId === b.clientId && a2.preSpawnMinLifetimeMs === b.preSpawnMinLifetimeMs;
|
|
14905
15658
|
}
|
|
@@ -14909,6 +15662,11 @@ function initBearer(cfg) {
|
|
|
14909
15662
|
config = cfg;
|
|
14910
15663
|
defaultSession = new SessionAuth(cfg);
|
|
14911
15664
|
}
|
|
15665
|
+
function resetBearer() {
|
|
15666
|
+
if (defaultSession) defaultSession.clearRefreshTimer();
|
|
15667
|
+
config = null;
|
|
15668
|
+
defaultSession = null;
|
|
15669
|
+
}
|
|
14912
15670
|
function requireConfig() {
|
|
14913
15671
|
if (!config) throw new Error("bearer module not initialized; call initBearer() first");
|
|
14914
15672
|
return config;
|
|
@@ -14920,15 +15678,36 @@ function requireSession() {
|
|
|
14920
15678
|
function getCurrentAccessToken() {
|
|
14921
15679
|
return defaultSession?.getAccessToken() ?? null;
|
|
14922
15680
|
}
|
|
15681
|
+
function getCurrentDeviceId() {
|
|
15682
|
+
return defaultSession?.getDeviceId() ?? null;
|
|
15683
|
+
}
|
|
14923
15684
|
function getCurrentOrgId() {
|
|
14924
15685
|
return defaultSession?.getOrgId() ?? null;
|
|
14925
15686
|
}
|
|
14926
15687
|
function getCurrentUserId() {
|
|
14927
15688
|
return defaultSession?.getUserId() ?? null;
|
|
14928
15689
|
}
|
|
15690
|
+
function getCurrentRefreshToken() {
|
|
15691
|
+
return defaultSession?.getRefreshToken() ?? null;
|
|
15692
|
+
}
|
|
15693
|
+
function clearRefreshTimer() {
|
|
15694
|
+
defaultSession?.clearRefreshTimer();
|
|
15695
|
+
}
|
|
15696
|
+
function onAccessTokenChange(listener) {
|
|
15697
|
+
return requireSession().onAccessTokenChange(listener);
|
|
15698
|
+
}
|
|
14929
15699
|
function getMcpAccessToken() {
|
|
14930
15700
|
return requireSession().getMcpAccessToken();
|
|
14931
15701
|
}
|
|
15702
|
+
function scheduleCredentialRefresh(rt, d, o2, e) {
|
|
15703
|
+
requireSession().scheduleCredentialRefresh(rt, d, o2, e);
|
|
15704
|
+
}
|
|
15705
|
+
function runScheduledRefresh(rt, d, o2) {
|
|
15706
|
+
return requireSession().runScheduledRefresh(rt, d, o2);
|
|
15707
|
+
}
|
|
15708
|
+
function __seedBearerForTesting(state) {
|
|
15709
|
+
requireSession().__seedForTesting(state);
|
|
15710
|
+
}
|
|
14932
15711
|
async function loadStoredAndRefresh() {
|
|
14933
15712
|
const cfg = requireConfig();
|
|
14934
15713
|
const stored = await loadCredentials(cfg.supabaseUrl);
|
|
@@ -15158,22 +15937,22 @@ var init_rateLimit = __esm({
|
|
|
15158
15937
|
|
|
15159
15938
|
// src/packageRoot.ts
|
|
15160
15939
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
15161
|
-
import { dirname as
|
|
15940
|
+
import { dirname as dirname10, join as join15, parse } from "node:path";
|
|
15162
15941
|
function defaultReadFile(path7) {
|
|
15163
15942
|
return readFileSync4(path7, "utf8");
|
|
15164
15943
|
}
|
|
15165
15944
|
function findPackageRoot2(startDir, opts = {}) {
|
|
15166
|
-
const
|
|
15945
|
+
const readFile16 = opts.readFile ?? defaultReadFile;
|
|
15167
15946
|
const stopAt = parse(startDir).root;
|
|
15168
15947
|
let dir = startDir;
|
|
15169
15948
|
for (; ; ) {
|
|
15170
15949
|
try {
|
|
15171
|
-
const parsed = JSON.parse(
|
|
15950
|
+
const parsed = JSON.parse(readFile16(join15(dir, "package.json")));
|
|
15172
15951
|
if (parsed.name === PACKAGE_NAME) return dir;
|
|
15173
15952
|
} catch {
|
|
15174
15953
|
}
|
|
15175
15954
|
if (dir === stopAt) return null;
|
|
15176
|
-
const parent =
|
|
15955
|
+
const parent = dirname10(dir);
|
|
15177
15956
|
if (parent === dir) return null;
|
|
15178
15957
|
dir = parent;
|
|
15179
15958
|
}
|
|
@@ -15191,15 +15970,15 @@ var version_exports = {};
|
|
|
15191
15970
|
__export(version_exports, {
|
|
15192
15971
|
readDaemonVersion: () => readDaemonVersion
|
|
15193
15972
|
});
|
|
15194
|
-
import { readFile as
|
|
15195
|
-
import { dirname as
|
|
15973
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
15974
|
+
import { dirname as dirname11, join as join16 } from "node:path";
|
|
15196
15975
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
15197
15976
|
async function readDaemonVersion() {
|
|
15198
15977
|
try {
|
|
15199
|
-
const here =
|
|
15978
|
+
const here = dirname11(fileURLToPath4(import.meta.url));
|
|
15200
15979
|
const root = findPackageRoot2(here);
|
|
15201
15980
|
if (!root) return null;
|
|
15202
|
-
const raw = await
|
|
15981
|
+
const raw = await readFile11(join16(root, "package.json"), "utf8");
|
|
15203
15982
|
const parsed = JSON.parse(raw);
|
|
15204
15983
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
15205
15984
|
} catch {
|
|
@@ -15214,8 +15993,8 @@ var init_version = __esm({
|
|
|
15214
15993
|
});
|
|
15215
15994
|
|
|
15216
15995
|
// src/daemon/materializeDiff.ts
|
|
15217
|
-
import { mkdir as
|
|
15218
|
-
import { join as
|
|
15996
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
|
|
15997
|
+
import { join as join17 } from "node:path";
|
|
15219
15998
|
import { createHash as createHash2 } from "node:crypto";
|
|
15220
15999
|
function splitPatchByFile(patch) {
|
|
15221
16000
|
if (!patch.trim()) return [];
|
|
@@ -15300,18 +16079,18 @@ function buildManifest(files, rangeLabel) {
|
|
|
15300
16079
|
}
|
|
15301
16080
|
async function materializeDiff(worktreeDir, patch, rangeLabel) {
|
|
15302
16081
|
const files = splitPatchByFile(patch);
|
|
15303
|
-
const dir =
|
|
15304
|
-
const byFileDir =
|
|
15305
|
-
await
|
|
15306
|
-
await writeFile8(
|
|
16082
|
+
const dir = join17(worktreeDir, CHANGES_DIR);
|
|
16083
|
+
const byFileDir = join17(dir, "by-file");
|
|
16084
|
+
await mkdir8(byFileDir, { recursive: true });
|
|
16085
|
+
await writeFile8(join17(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
|
|
15307
16086
|
`, "utf8");
|
|
15308
16087
|
for (const f of files) {
|
|
15309
16088
|
const rel = `by-file/${hashPath(f.path)}.diff`;
|
|
15310
16089
|
f.diffPath = rel;
|
|
15311
|
-
await writeFile8(
|
|
16090
|
+
await writeFile8(join17(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
|
|
15312
16091
|
`, "utf8");
|
|
15313
16092
|
}
|
|
15314
|
-
await writeFile8(
|
|
16093
|
+
await writeFile8(join17(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
|
|
15315
16094
|
return { dir, fileCount: files.length };
|
|
15316
16095
|
}
|
|
15317
16096
|
var CHANGES_DIR, STATUS_LABEL;
|
|
@@ -15335,12 +16114,127 @@ var init_branchMarker = __esm({
|
|
|
15335
16114
|
}
|
|
15336
16115
|
});
|
|
15337
16116
|
|
|
16117
|
+
// src/daemon/localPrPoll.ts
|
|
16118
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
16119
|
+
function observeGithubPull(p) {
|
|
16120
|
+
return {
|
|
16121
|
+
number: p.number,
|
|
16122
|
+
state: p.merged_at ? "merged" : p.state === "closed" ? "closed" : "open",
|
|
16123
|
+
headSha: p.head.sha,
|
|
16124
|
+
baseSha: p.base.sha,
|
|
16125
|
+
baseRef: p.base.ref,
|
|
16126
|
+
title: p.title ?? null,
|
|
16127
|
+
authorLogin: p.user?.login ?? null,
|
|
16128
|
+
body: p.body ?? null,
|
|
16129
|
+
createdAt: p.created_at ?? null,
|
|
16130
|
+
updatedAt: p.updated_at ?? null,
|
|
16131
|
+
mergedAt: p.merged_at ?? null,
|
|
16132
|
+
closedAt: p.closed_at ?? null
|
|
16133
|
+
};
|
|
16134
|
+
}
|
|
16135
|
+
function metaKey(o2) {
|
|
16136
|
+
return createHash3("sha256").update(JSON.stringify([o2.title, o2.body])).digest("hex").slice(0, 16);
|
|
16137
|
+
}
|
|
16138
|
+
function record(o2, prev, action) {
|
|
16139
|
+
const r = { head: o2.headSha, state: o2.state, meta: metaKey(o2) };
|
|
16140
|
+
const seq = action === "synchronize" ? (prev?.seq ?? 0) + 1 : prev?.seq;
|
|
16141
|
+
if (seq !== void 0) r.seq = seq;
|
|
16142
|
+
return r;
|
|
16143
|
+
}
|
|
16144
|
+
function decideAction(prev, o2) {
|
|
16145
|
+
if (!prev) return o2.state === "open" ? "opened" : null;
|
|
16146
|
+
const wasOpen = prev.state === "open";
|
|
16147
|
+
const isOpen = o2.state === "open";
|
|
16148
|
+
if (wasOpen && !isOpen) return "closed";
|
|
16149
|
+
if (!wasOpen && isOpen) return "reopened";
|
|
16150
|
+
if (isOpen && prev.head !== o2.headSha) return "synchronize";
|
|
16151
|
+
if (!isOpen && prev.state !== o2.state) return "edited";
|
|
16152
|
+
if (prev.meta !== void 0 && prev.meta !== metaKey(o2)) return "edited";
|
|
16153
|
+
return null;
|
|
16154
|
+
}
|
|
16155
|
+
function toItem(o2, action, sent) {
|
|
16156
|
+
return {
|
|
16157
|
+
number: o2.number,
|
|
16158
|
+
action,
|
|
16159
|
+
head_sha: o2.headSha,
|
|
16160
|
+
base_sha: o2.baseSha,
|
|
16161
|
+
base_ref: o2.baseRef,
|
|
16162
|
+
title: o2.title,
|
|
16163
|
+
author_login: o2.authorLogin,
|
|
16164
|
+
created_at: o2.createdAt,
|
|
16165
|
+
merged_at: o2.mergedAt,
|
|
16166
|
+
closed_at: o2.closedAt,
|
|
16167
|
+
body: o2.body,
|
|
16168
|
+
...action === "synchronize" && sent.seq !== void 0 ? { seq: sent.seq } : {}
|
|
16169
|
+
};
|
|
16170
|
+
}
|
|
16171
|
+
function updatedMs(o2) {
|
|
16172
|
+
const t = o2.updatedAt ? Date.parse(o2.updatedAt) : NaN;
|
|
16173
|
+
return Number.isFinite(t) ? t : 0;
|
|
16174
|
+
}
|
|
16175
|
+
function planPrSync(input) {
|
|
16176
|
+
const { state, observations, nowMs } = input;
|
|
16177
|
+
const next = { prs: { ...state?.prs ?? {} } };
|
|
16178
|
+
const candidates = [];
|
|
16179
|
+
const backfill = !state || input.resumed ? new Set(
|
|
16180
|
+
observations.filter((o2) => !state?.prs[String(o2.number)]).filter((o2) => o2.state === "open" && nowMs - updatedMs(o2) <= BACKFILL_WINDOW_MS).sort((a2, b) => updatedMs(b) - updatedMs(a2)).slice(0, BACKFILL_MAX).map((o2) => o2.number)
|
|
16181
|
+
) : null;
|
|
16182
|
+
for (const o2 of observations) {
|
|
16183
|
+
const prev = state?.prs[String(o2.number)];
|
|
16184
|
+
if (!prev && backfill) {
|
|
16185
|
+
if (backfill.has(o2.number)) candidates.push({ o: o2, action: "opened" });
|
|
16186
|
+
else if (o2.state === "open") next.prs[String(o2.number)] = record(o2);
|
|
16187
|
+
continue;
|
|
16188
|
+
}
|
|
16189
|
+
const action = decideAction(prev, o2);
|
|
16190
|
+
if (action) candidates.push({ o: o2, action, prev });
|
|
16191
|
+
}
|
|
16192
|
+
const ranked = [
|
|
16193
|
+
...candidates.filter((c3) => c3.action !== "edited"),
|
|
16194
|
+
...candidates.filter((c3) => c3.action === "edited")
|
|
16195
|
+
].slice(0, MAX_PRS_PER_TICK);
|
|
16196
|
+
const send = [];
|
|
16197
|
+
for (const c3 of ranked) {
|
|
16198
|
+
const sent = record(c3.o, c3.prev, c3.action);
|
|
16199
|
+
next.prs[String(c3.o.number)] = sent;
|
|
16200
|
+
send.push(toItem(c3.o, c3.action, sent));
|
|
16201
|
+
}
|
|
16202
|
+
return { send, next };
|
|
16203
|
+
}
|
|
16204
|
+
function parsePrPollState(raw) {
|
|
16205
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
16206
|
+
const o2 = raw;
|
|
16207
|
+
const prs = {};
|
|
16208
|
+
if (o2.prs && typeof o2.prs === "object" && !Array.isArray(o2.prs)) {
|
|
16209
|
+
for (const [k, v] of Object.entries(o2.prs)) {
|
|
16210
|
+
if (!/^\d+$/.test(k) || !v || typeof v !== "object") continue;
|
|
16211
|
+
const r = v;
|
|
16212
|
+
if (typeof r.head !== "string" || !STATES.has(r.state)) continue;
|
|
16213
|
+
const entry = { head: r.head, state: r.state };
|
|
16214
|
+
if (typeof r.meta === "string") entry.meta = r.meta;
|
|
16215
|
+
if (Number.isInteger(r.seq) && r.seq > 0) entry.seq = r.seq;
|
|
16216
|
+
prs[k] = entry;
|
|
16217
|
+
}
|
|
16218
|
+
}
|
|
16219
|
+
return { prs };
|
|
16220
|
+
}
|
|
16221
|
+
var BACKFILL_WINDOW_MS, BACKFILL_MAX, MAX_PRS_PER_TICK, STATES;
|
|
16222
|
+
var init_localPrPoll = __esm({
|
|
16223
|
+
"src/daemon/localPrPoll.ts"() {
|
|
16224
|
+
"use strict";
|
|
16225
|
+
BACKFILL_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
16226
|
+
BACKFILL_MAX = 5;
|
|
16227
|
+
MAX_PRS_PER_TICK = 50;
|
|
16228
|
+
STATES = /* @__PURE__ */ new Set(["open", "merged", "closed"]);
|
|
16229
|
+
}
|
|
16230
|
+
});
|
|
16231
|
+
|
|
15338
16232
|
// src/daemon/localRepos.ts
|
|
15339
|
-
import { chmod as
|
|
15340
|
-
import { homedir as
|
|
15341
|
-
import { dirname as
|
|
16233
|
+
import { chmod as chmod3, mkdir as mkdir9, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
|
|
16234
|
+
import { homedir as homedir5 } from "node:os";
|
|
16235
|
+
import { dirname as dirname12, join as join18 } from "node:path";
|
|
15342
16236
|
function localReposPath(env = process.env) {
|
|
15343
|
-
return env.ARCHME_LOCAL_REPOS_PATH ??
|
|
16237
|
+
return env.ARCHME_LOCAL_REPOS_PATH ?? join18(homedir5(), ".archme", "repos.json");
|
|
15344
16238
|
}
|
|
15345
16239
|
function isEntry(v) {
|
|
15346
16240
|
if (!v || typeof v !== "object") return false;
|
|
@@ -15370,6 +16264,9 @@ function parseLocalRepos(raw) {
|
|
|
15370
16264
|
if (typeof raw2.haltedAt === "string" && raw2.haltedAt) entry.haltedAt = raw2.haltedAt;
|
|
15371
16265
|
if (typeof raw2.haltReason === "string" && raw2.haltReason) entry.haltReason = raw2.haltReason;
|
|
15372
16266
|
if (typeof raw2.haltCode === "string" && raw2.haltCode) entry.haltCode = raw2.haltCode;
|
|
16267
|
+
const prPoll = parsePrPollState(raw2.prPoll);
|
|
16268
|
+
if (prPoll) entry.prPoll = prPoll;
|
|
16269
|
+
if (typeof raw2.prPausedAt === "string" && raw2.prPausedAt) entry.prPausedAt = raw2.prPausedAt;
|
|
15373
16270
|
return entry;
|
|
15374
16271
|
});
|
|
15375
16272
|
}
|
|
@@ -15401,6 +16298,16 @@ function markHalted(repos, watchedRepoId, reason, at, code) {
|
|
|
15401
16298
|
(r) => r.watchedRepoId === watchedRepoId ? { ...r, haltedAt: at, haltReason: reason, haltCode: code } : r
|
|
15402
16299
|
);
|
|
15403
16300
|
}
|
|
16301
|
+
function setPrPollState(repos, watchedRepoId, prPoll) {
|
|
16302
|
+
return repos.map((r) => {
|
|
16303
|
+
if (r.watchedRepoId !== watchedRepoId) return r;
|
|
16304
|
+
const { prPoll: _was, prPausedAt: _paused, ...rest } = r;
|
|
16305
|
+
return prPoll ? { ...rest, prPoll } : rest;
|
|
16306
|
+
});
|
|
16307
|
+
}
|
|
16308
|
+
function markPrPaused(repos, watchedRepoId, at) {
|
|
16309
|
+
return repos.map((r) => r.watchedRepoId === watchedRepoId ? { ...r, prPausedAt: at } : r);
|
|
16310
|
+
}
|
|
15404
16311
|
function clearHalt(repos, watchedRepoId) {
|
|
15405
16312
|
return repos.map((r) => {
|
|
15406
16313
|
if (r.watchedRepoId !== watchedRepoId) return r;
|
|
@@ -15417,16 +16324,16 @@ function findLocalRepoForJob(repos, job) {
|
|
|
15417
16324
|
}
|
|
15418
16325
|
async function loadLocalRepos(env = process.env) {
|
|
15419
16326
|
try {
|
|
15420
|
-
return parseLocalRepos(await
|
|
16327
|
+
return parseLocalRepos(await readFile12(localReposPath(env), "utf8"));
|
|
15421
16328
|
} catch {
|
|
15422
16329
|
return [];
|
|
15423
16330
|
}
|
|
15424
16331
|
}
|
|
15425
16332
|
async function saveLocalRepos(repos, env = process.env) {
|
|
15426
16333
|
const path7 = localReposPath(env);
|
|
15427
|
-
await
|
|
16334
|
+
await mkdir9(dirname12(path7), { recursive: true, mode: 448 });
|
|
15428
16335
|
await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
|
|
15429
|
-
await
|
|
16336
|
+
await chmod3(path7, 384).catch(() => {
|
|
15430
16337
|
});
|
|
15431
16338
|
}
|
|
15432
16339
|
async function registerLocalRepo(entry, env = process.env) {
|
|
@@ -15436,6 +16343,7 @@ var LOCAL_REPOS_VERSION, REQUIRED_KEYS;
|
|
|
15436
16343
|
var init_localRepos = __esm({
|
|
15437
16344
|
"src/daemon/localRepos.ts"() {
|
|
15438
16345
|
"use strict";
|
|
16346
|
+
init_localPrPoll();
|
|
15439
16347
|
LOCAL_REPOS_VERSION = 1;
|
|
15440
16348
|
REQUIRED_KEYS = [
|
|
15441
16349
|
"watchedRepoId",
|
|
@@ -15447,6 +16355,59 @@ var init_localRepos = __esm({
|
|
|
15447
16355
|
}
|
|
15448
16356
|
});
|
|
15449
16357
|
|
|
16358
|
+
// src/daemon/localPrJob.ts
|
|
16359
|
+
function assertLocalJobShape(job) {
|
|
16360
|
+
if (!SHA_RE.test(job.after_sha)) throw new LocalPrJobRejectedError(`after_sha ${job.after_sha} is not a 40-character git SHA`);
|
|
16361
|
+
if (!job.repo_branch || job.repo_branch.startsWith("-")) {
|
|
16362
|
+
throw new LocalPrJobRejectedError(`branch ${JSON.stringify(job.repo_branch)} is not a branch name`);
|
|
16363
|
+
}
|
|
16364
|
+
if (job.source_mode !== "pr_update") return;
|
|
16365
|
+
if (!job.before_sha || !SHA_RE.test(job.before_sha)) {
|
|
16366
|
+
throw new LocalPrJobRejectedError(`before_sha ${job.before_sha ?? "(null)"} is not a 40-character git SHA`);
|
|
16367
|
+
}
|
|
16368
|
+
if (!Number.isInteger(job.pr_number) || job.pr_number <= 0) {
|
|
16369
|
+
throw new LocalPrJobRejectedError(`PR number ${String(job.pr_number)} is not a pull request number`);
|
|
16370
|
+
}
|
|
16371
|
+
}
|
|
16372
|
+
async function verifyLocalPrJob(job, git4) {
|
|
16373
|
+
const n2 = job.pr_number;
|
|
16374
|
+
const before = job.before_sha;
|
|
16375
|
+
const base = `refs/remotes/origin/${job.repo_branch}`;
|
|
16376
|
+
const ref = `refs/archme/pull/${n2}/head`;
|
|
16377
|
+
const fetched = await git4(["fetch", "--no-tags", "origin", `+refs/pull/${n2}/head:${ref}`]);
|
|
16378
|
+
if (fetched.exitCode !== 0) throw new Error(`could not fetch PR #${n2}'s head from origin: ${fetched.stderr.trim()}`);
|
|
16379
|
+
const head = (await git4(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`])).stdout.trim();
|
|
16380
|
+
if (head !== job.after_sha) {
|
|
16381
|
+
throw new LocalPrJobRejectedError(
|
|
16382
|
+
`after_sha ${short(job.after_sha)} is not PR #${n2}'s current head on origin (${head ? short(head) : "none"}); a newer push is reviewed by its own job`
|
|
16383
|
+
);
|
|
16384
|
+
}
|
|
16385
|
+
if ((await git4(["merge-base", "--is-ancestor", before, base])).exitCode !== 0) {
|
|
16386
|
+
throw new LocalPrJobRejectedError(`PR #${n2}'s base ${short(before)} is not on origin/${job.repo_branch}`);
|
|
16387
|
+
}
|
|
16388
|
+
const fromJob = (await git4(["merge-base", before, job.after_sha])).stdout.trim();
|
|
16389
|
+
const fromBranch = (await git4(["merge-base", base, job.after_sha])).stdout.trim();
|
|
16390
|
+
if (!fromJob || fromJob !== fromBranch) {
|
|
16391
|
+
throw new LocalPrJobRejectedError(
|
|
16392
|
+
`PR #${n2}'s base ${short(before)} is not where it branched from origin/${job.repo_branch} (${fromBranch ? short(fromBranch) : "no common commit"})`
|
|
16393
|
+
);
|
|
16394
|
+
}
|
|
16395
|
+
}
|
|
16396
|
+
var LocalPrJobRejectedError, SHA_RE, short;
|
|
16397
|
+
var init_localPrJob = __esm({
|
|
16398
|
+
"src/daemon/localPrJob.ts"() {
|
|
16399
|
+
"use strict";
|
|
16400
|
+
LocalPrJobRejectedError = class extends Error {
|
|
16401
|
+
constructor(message) {
|
|
16402
|
+
super(`refusing local job: ${message}`);
|
|
16403
|
+
this.name = "LocalPrJobRejectedError";
|
|
16404
|
+
}
|
|
16405
|
+
};
|
|
16406
|
+
SHA_RE = /^[0-9a-f]{40}$/;
|
|
16407
|
+
short = (sha) => sha.slice(0, 8);
|
|
16408
|
+
}
|
|
16409
|
+
});
|
|
16410
|
+
|
|
15450
16411
|
// ../shared/commitAdvance.ts
|
|
15451
16412
|
function planAdvance(input) {
|
|
15452
16413
|
const { marker, upstreamTip, markerIsAncestorOfTip, tipIsAncestorOfMarker, firstParentShas } = input;
|
|
@@ -15479,21 +16440,21 @@ function decideGate(input) {
|
|
|
15479
16440
|
return {
|
|
15480
16441
|
action: "stop",
|
|
15481
16442
|
code: "job-terminal",
|
|
15482
|
-
reason: `job ${job.status} after ${job.attemptCount} attempt(s); marker did not advance \u2014 re-push commit ${
|
|
16443
|
+
reason: `job ${job.status} after ${job.attemptCount} attempt(s); marker did not advance \u2014 re-push commit ${short2(targetSha)} (fresh delivery) or admin-requeue, do not fold forward`
|
|
15483
16444
|
};
|
|
15484
16445
|
}
|
|
15485
16446
|
if (!pushedNewRef && job === null && elapsedMs >= noopGraceMs) {
|
|
15486
16447
|
return {
|
|
15487
16448
|
action: "stop",
|
|
15488
16449
|
code: "noop-push-no-job",
|
|
15489
|
-
reason: `no webhook fired for ${
|
|
16450
|
+
reason: `no webhook fired for ${short2(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`
|
|
15490
16451
|
};
|
|
15491
16452
|
}
|
|
15492
16453
|
if (elapsedMs >= timeoutMs) {
|
|
15493
16454
|
return {
|
|
15494
16455
|
action: "stop",
|
|
15495
16456
|
code: "timeout",
|
|
15496
|
-
reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${
|
|
16457
|
+
reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${short2(targetSha)} (job: ${job ? job.status : "not enqueued"})`
|
|
15497
16458
|
};
|
|
15498
16459
|
}
|
|
15499
16460
|
return {
|
|
@@ -15502,7 +16463,7 @@ function decideGate(input) {
|
|
|
15502
16463
|
reason: job ? `job ${job.status}` : "job not enqueued yet"
|
|
15503
16464
|
};
|
|
15504
16465
|
}
|
|
15505
|
-
function
|
|
16466
|
+
function short2(sha) {
|
|
15506
16467
|
return sha.slice(0, 7);
|
|
15507
16468
|
}
|
|
15508
16469
|
var init_commitGate = __esm({
|
|
@@ -15539,7 +16500,7 @@ function planPollTick(input) {
|
|
|
15539
16500
|
case "upstream-behind":
|
|
15540
16501
|
return {
|
|
15541
16502
|
kind: "idle",
|
|
15542
|
-
reason: `upstream is behind the marker (reset to ${
|
|
16503
|
+
reason: `upstream is behind the marker (reset to ${short3(plan.tip)}); nothing to do`
|
|
15543
16504
|
};
|
|
15544
16505
|
case "force-push":
|
|
15545
16506
|
target = plan.tip;
|
|
@@ -15571,15 +16532,15 @@ function planPollTick(input) {
|
|
|
15571
16532
|
case "advance":
|
|
15572
16533
|
return { kind: "idle", reason: decision.reason };
|
|
15573
16534
|
case "wait":
|
|
15574
|
-
return { kind: "idle", reason: `${
|
|
16535
|
+
return { kind: "idle", reason: `${short3(target)}: ${decision.reason}` };
|
|
15575
16536
|
}
|
|
15576
16537
|
}
|
|
15577
16538
|
function localHaltReason(decision, job, targetSha) {
|
|
15578
16539
|
if (decision.code !== "job-terminal" || !job) return decision.reason;
|
|
15579
|
-
return `commit ${
|
|
16540
|
+
return `commit ${short3(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.
|
|
15580
16541
|
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.`;
|
|
15581
16542
|
}
|
|
15582
|
-
function
|
|
16543
|
+
function short3(sha) {
|
|
15583
16544
|
return sha.slice(0, 7);
|
|
15584
16545
|
}
|
|
15585
16546
|
var init_localPoll = __esm({
|
|
@@ -15625,8 +16586,8 @@ __export(localGit_exports, {
|
|
|
15625
16586
|
revParse: () => revParse,
|
|
15626
16587
|
setOriginUrl: () => setOriginUrl
|
|
15627
16588
|
});
|
|
15628
|
-
import { mkdir as
|
|
15629
|
-
import { dirname as
|
|
16589
|
+
import { mkdir as mkdir10 } from "node:fs/promises";
|
|
16590
|
+
import { dirname as dirname13 } from "node:path";
|
|
15630
16591
|
async function git(dir, args, opts = {}) {
|
|
15631
16592
|
requireBinary("git");
|
|
15632
16593
|
const res = await execa("git", args, {
|
|
@@ -15647,7 +16608,7 @@ async function git(dir, args, opts = {}) {
|
|
|
15647
16608
|
return { exitCode, stdout: res.stdout || "", stderr: res.stderr || "" };
|
|
15648
16609
|
}
|
|
15649
16610
|
async function createMirrorClone(sourcePath, mirrorPath, originUrl) {
|
|
15650
|
-
await
|
|
16611
|
+
await mkdir10(dirname13(mirrorPath), { recursive: true });
|
|
15651
16612
|
await git(null, ["clone", "--local", "--no-hardlinks", sourcePath, mirrorPath]);
|
|
15652
16613
|
await git(mirrorPath, ["remote", "set-url", "origin", originUrl]);
|
|
15653
16614
|
}
|
|
@@ -15719,6 +16680,244 @@ var init_localGit = __esm({
|
|
|
15719
16680
|
}
|
|
15720
16681
|
});
|
|
15721
16682
|
|
|
16683
|
+
// src/daemon/localPrAdapters.ts
|
|
16684
|
+
function parseCredentialFill(stdout) {
|
|
16685
|
+
for (const line of stdout.split("\n")) {
|
|
16686
|
+
if (line.startsWith("password=")) return line.slice("password=".length).trim() || null;
|
|
16687
|
+
}
|
|
16688
|
+
return null;
|
|
16689
|
+
}
|
|
16690
|
+
async function resolveGithubTokens(deps = {}) {
|
|
16691
|
+
const run = deps.run ?? defaultExec;
|
|
16692
|
+
const ghPath = deps.ghPath ?? (() => findOnPath("gh"));
|
|
16693
|
+
const tiers = [];
|
|
16694
|
+
const clean = (t) => t && /^\S+$/.test(t) ? t : null;
|
|
16695
|
+
if (ghPath()) {
|
|
16696
|
+
try {
|
|
16697
|
+
const r = await run("gh", ["auth", "token", "--hostname", "github.com"]);
|
|
16698
|
+
const token = r.exitCode === 0 ? clean(r.stdout.trim()) : null;
|
|
16699
|
+
if (token) tiers.push({ tier: "gh", token });
|
|
16700
|
+
} catch {
|
|
16701
|
+
}
|
|
16702
|
+
}
|
|
16703
|
+
try {
|
|
16704
|
+
const r = await run("git", ["credential", "fill"], {
|
|
16705
|
+
input: "protocol=https\nhost=github.com\n\n",
|
|
16706
|
+
env: {
|
|
16707
|
+
...process.env,
|
|
16708
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
16709
|
+
GCM_INTERACTIVE: "never",
|
|
16710
|
+
GIT_ASKPASS: "",
|
|
16711
|
+
SSH_ASKPASS: ""
|
|
16712
|
+
}
|
|
16713
|
+
});
|
|
16714
|
+
const token = r.exitCode === 0 ? clean(parseCredentialFill(r.stdout)) : null;
|
|
16715
|
+
if (token) tiers.push({ tier: "git-credential", token });
|
|
16716
|
+
} catch {
|
|
16717
|
+
}
|
|
16718
|
+
tiers.push({ tier: "none", token: null });
|
|
16719
|
+
return tiers;
|
|
16720
|
+
}
|
|
16721
|
+
async function listGithubPulls(args) {
|
|
16722
|
+
const f = args.fetch ?? fetch;
|
|
16723
|
+
const headers = {
|
|
16724
|
+
Accept: "application/vnd.github+json",
|
|
16725
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
16726
|
+
"User-Agent": "archme-cli"
|
|
16727
|
+
};
|
|
16728
|
+
if (args.token) headers.Authorization = `Bearer ${args.token}`;
|
|
16729
|
+
if (args.etag) headers["If-None-Match"] = args.etag;
|
|
16730
|
+
const url = `https://api.github.com/repos/${encodeURIComponent(args.owner)}/${encodeURIComponent(args.name)}/pulls?state=all&sort=updated&direction=desc&per_page=100`;
|
|
16731
|
+
const resp = await f(url, { headers, signal: AbortSignal.timeout(3e4) });
|
|
16732
|
+
if (resp.status === 304) return { kind: "not-modified" };
|
|
16733
|
+
if (resp.status === 429 || resp.status === 403 && resp.headers.get("x-ratelimit-remaining") === "0") {
|
|
16734
|
+
const reset2 = Number(resp.headers.get("x-ratelimit-reset"));
|
|
16735
|
+
return { kind: "rate-limited", untilMs: Number.isFinite(reset2) && reset2 > 0 ? reset2 * 1e3 : Date.now() + 60 * 6e4 };
|
|
16736
|
+
}
|
|
16737
|
+
if (resp.status === 401 || resp.status === 403 || resp.status === 404) {
|
|
16738
|
+
return { kind: "denied", status: resp.status };
|
|
16739
|
+
}
|
|
16740
|
+
if (!resp.ok) throw new Error(`GitHub pulls list returned ${resp.status}`);
|
|
16741
|
+
const body = await resp.json();
|
|
16742
|
+
if (!Array.isArray(body)) throw new Error("GitHub pulls list was not an array");
|
|
16743
|
+
return { kind: "ok", pulls: body, etag: resp.headers.get("etag") };
|
|
16744
|
+
}
|
|
16745
|
+
function rateLimitedReason(untilMs) {
|
|
16746
|
+
return `GitHub's API rate limit is used up until ${new Date(untilMs).toISOString()}`;
|
|
16747
|
+
}
|
|
16748
|
+
async function listPrsFromGithub(args) {
|
|
16749
|
+
const key = `${args.owner}/${args.name}`.toLowerCase();
|
|
16750
|
+
const c3 = args.cache.get(key) ?? { denied: {} };
|
|
16751
|
+
args.cache.set(key, c3);
|
|
16752
|
+
const ok = (obs) => ({
|
|
16753
|
+
kind: "ok",
|
|
16754
|
+
observations: obs.filter((o2) => o2.baseRef === args.baseRef)
|
|
16755
|
+
});
|
|
16756
|
+
const bestTier = args.tiers.find((t) => (c3.denied[t.tier] ?? 0) <= args.nowMs)?.tier;
|
|
16757
|
+
if (bestTier === "none" && c3.tier === "none" && c3.observations && args.nowMs - (c3.fetchedAtMs ?? 0) < UNAUTHENTICATED_MIN_INTERVAL_MS) {
|
|
16758
|
+
return ok(c3.observations);
|
|
16759
|
+
}
|
|
16760
|
+
if (c3.rateLimitedUntil && args.nowMs < c3.rateLimitedUntil) {
|
|
16761
|
+
return { kind: "unavailable", reason: rateLimitedReason(c3.rateLimitedUntil) };
|
|
16762
|
+
}
|
|
16763
|
+
for (const t of args.tiers) {
|
|
16764
|
+
if ((c3.denied[t.tier] ?? 0) > args.nowMs) continue;
|
|
16765
|
+
let r;
|
|
16766
|
+
try {
|
|
16767
|
+
r = await listGithubPulls({
|
|
16768
|
+
owner: args.owner,
|
|
16769
|
+
name: args.name,
|
|
16770
|
+
token: t.token,
|
|
16771
|
+
etag: c3.tier === t.tier && c3.observations ? c3.etag ?? null : null,
|
|
16772
|
+
fetch: args.fetch
|
|
16773
|
+
});
|
|
16774
|
+
} catch (err2) {
|
|
16775
|
+
return { kind: "unavailable", reason: `GitHub's API could not be reached (${err2.message})` };
|
|
16776
|
+
}
|
|
16777
|
+
if (r.kind === "ok") {
|
|
16778
|
+
c3.tier = t.tier;
|
|
16779
|
+
c3.etag = r.etag;
|
|
16780
|
+
c3.observations = r.pulls.map(observeGithubPull);
|
|
16781
|
+
c3.fetchedAtMs = args.nowMs;
|
|
16782
|
+
return ok(c3.observations);
|
|
16783
|
+
}
|
|
16784
|
+
if (r.kind === "not-modified" && c3.observations) {
|
|
16785
|
+
c3.fetchedAtMs = args.nowMs;
|
|
16786
|
+
return ok(c3.observations);
|
|
16787
|
+
}
|
|
16788
|
+
if (r.kind === "rate-limited") {
|
|
16789
|
+
c3.rateLimitedUntil = r.untilMs;
|
|
16790
|
+
return { kind: "unavailable", reason: rateLimitedReason(r.untilMs) };
|
|
16791
|
+
}
|
|
16792
|
+
if (r.kind === "denied") c3.denied[t.tier] = args.nowMs + DENY_BACKOFF_MS;
|
|
16793
|
+
}
|
|
16794
|
+
return { kind: "unavailable", reason: DENIED_REASON };
|
|
16795
|
+
}
|
|
16796
|
+
async function postLocalPrSync(client, payload) {
|
|
16797
|
+
const { data, error } = await client.functions.invoke("github/local-pr-sync", { body: payload });
|
|
16798
|
+
if (error) {
|
|
16799
|
+
let detail = error.message;
|
|
16800
|
+
const ctx = error.context;
|
|
16801
|
+
if (ctx instanceof Response) {
|
|
16802
|
+
const body = await ctx.json().catch(() => null);
|
|
16803
|
+
if (typeof body?.error === "string") detail = `${ctx.status} ${body.error}`;
|
|
16804
|
+
}
|
|
16805
|
+
throw new Error(`local-pr-sync: ${detail}`);
|
|
16806
|
+
}
|
|
16807
|
+
const results = data?.results;
|
|
16808
|
+
if (!Array.isArray(results)) throw new Error("local-pr-sync: malformed response");
|
|
16809
|
+
return results;
|
|
16810
|
+
}
|
|
16811
|
+
async function pollPrsOnce(entry, deps) {
|
|
16812
|
+
const row = await deps.readWatchedRepo(entry.watchedRepoId);
|
|
16813
|
+
if (row && row.prUpdateMode !== "auto") {
|
|
16814
|
+
if (entry.prPoll && !entry.prPausedAt) await deps.pause(entry, new Date(deps.now()).toISOString());
|
|
16815
|
+
return;
|
|
16816
|
+
}
|
|
16817
|
+
if (!row || row.sourceKind !== "local" || !row.enabled) return;
|
|
16818
|
+
const repo = `${entry.repoOwner}/${entry.repoName}`;
|
|
16819
|
+
const sameRepo = row.repoOwner?.toLowerCase() === entry.repoOwner.toLowerCase() && row.repoName?.toLowerCase() === entry.repoName.toLowerCase();
|
|
16820
|
+
if (!sameRepo) {
|
|
16821
|
+
deps.logOnChange(
|
|
16822
|
+
`repo-mismatch:${entry.watchedRepoId}`,
|
|
16823
|
+
true,
|
|
16824
|
+
` ${repo}: PR reviews skipped \u2014 the server now names ${row.repoOwner ?? "?"}/${row.repoName ?? "?"} for this watched repo, not the repo registered on this machine. Re-run \`archme local-watch\` in the right repo if that is intended.`
|
|
16825
|
+
);
|
|
16826
|
+
return;
|
|
16827
|
+
}
|
|
16828
|
+
const listing = await deps.listPrs(entry, row);
|
|
16829
|
+
const unavailableKey = `pr-listing:${entry.watchedRepoId}`;
|
|
16830
|
+
if (listing.kind === "unavailable") {
|
|
16831
|
+
deps.logOnChange(
|
|
16832
|
+
unavailableKey,
|
|
16833
|
+
true,
|
|
16834
|
+
` ${repo}: PR reviews on hold \u2014 ${listing.reason}.`
|
|
16835
|
+
);
|
|
16836
|
+
return;
|
|
16837
|
+
}
|
|
16838
|
+
deps.logOnChange(unavailableKey, false, ` ${repo}: PR reviews resumed \u2014 GitHub's API answered again.`);
|
|
16839
|
+
const resumed = entry.prPausedAt !== void 0;
|
|
16840
|
+
const plan = planPrSync({ state: entry.prPoll, observations: listing.observations, nowMs: deps.now(), resumed });
|
|
16841
|
+
if (plan.send.length > 0) {
|
|
16842
|
+
const results = await deps.send(entry, plan.send);
|
|
16843
|
+
const kinds = new Map(results.map((r) => [r.number, r.kind]));
|
|
16844
|
+
for (const item of plan.send) {
|
|
16845
|
+
const kind = kinds.get(item.number);
|
|
16846
|
+
if (kind !== void 0) {
|
|
16847
|
+
deps.log(` ${repo}: PR #${item.number} ${item.action} \u2192 ${kind}`);
|
|
16848
|
+
}
|
|
16849
|
+
if (kind !== void 0 && !UNDELIVERED.has(kind)) continue;
|
|
16850
|
+
const key = String(item.number);
|
|
16851
|
+
const prev = entry.prPoll?.prs[key];
|
|
16852
|
+
if (prev) plan.next.prs[key] = prev;
|
|
16853
|
+
else delete plan.next.prs[key];
|
|
16854
|
+
}
|
|
16855
|
+
} else if (!resumed && JSON.stringify(plan.next) === JSON.stringify(entry.prPoll)) {
|
|
16856
|
+
return;
|
|
16857
|
+
}
|
|
16858
|
+
await deps.saveState(entry, plan.next);
|
|
16859
|
+
}
|
|
16860
|
+
function makePrPollDeps(client, log, readWatchedRepo, io = {}) {
|
|
16861
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
16862
|
+
const on6 = /* @__PURE__ */ new Set();
|
|
16863
|
+
return {
|
|
16864
|
+
readWatchedRepo,
|
|
16865
|
+
log,
|
|
16866
|
+
logOnChange(key, value, msg) {
|
|
16867
|
+
if (on6.has(key) === value) return;
|
|
16868
|
+
if (value) on6.add(key);
|
|
16869
|
+
else on6.delete(key);
|
|
16870
|
+
log(msg);
|
|
16871
|
+
},
|
|
16872
|
+
now: () => Date.now(),
|
|
16873
|
+
async listPrs(entry, row) {
|
|
16874
|
+
return listPrsFromGithub({
|
|
16875
|
+
owner: entry.repoOwner,
|
|
16876
|
+
name: entry.repoName,
|
|
16877
|
+
baseRef: row.repoBranch,
|
|
16878
|
+
tiers: await (io.tiers ?? resolveGithubTokens)(),
|
|
16879
|
+
cache: cache2,
|
|
16880
|
+
nowMs: Date.now(),
|
|
16881
|
+
fetch: io.fetch
|
|
16882
|
+
});
|
|
16883
|
+
},
|
|
16884
|
+
send: (entry, items) => postLocalPrSync(client, { watched_repo_id: entry.watchedRepoId, prs: items }),
|
|
16885
|
+
// Read-modify-write, as halts are: `local-watch` may have changed the
|
|
16886
|
+
// registry since this tick read it.
|
|
16887
|
+
async saveState(entry, state) {
|
|
16888
|
+
await saveLocalRepos(setPrPollState(await loadLocalRepos(), entry.watchedRepoId, state));
|
|
16889
|
+
},
|
|
16890
|
+
async pause(entry, at) {
|
|
16891
|
+
await saveLocalRepos(markPrPaused(await loadLocalRepos(), entry.watchedRepoId, at));
|
|
16892
|
+
}
|
|
16893
|
+
};
|
|
16894
|
+
}
|
|
16895
|
+
var TOKEN_TIMEOUT_MS, defaultExec, DENY_BACKOFF_MS, UNAUTHENTICATED_MIN_INTERVAL_MS, DENIED_REASON, UNDELIVERED;
|
|
16896
|
+
var init_localPrAdapters = __esm({
|
|
16897
|
+
"src/daemon/localPrAdapters.ts"() {
|
|
16898
|
+
"use strict";
|
|
16899
|
+
init_execa();
|
|
16900
|
+
init_preflight();
|
|
16901
|
+
init_localPrPoll();
|
|
16902
|
+
init_localRepos();
|
|
16903
|
+
TOKEN_TIMEOUT_MS = 15e3;
|
|
16904
|
+
defaultExec = async (cmd, args, opts) => {
|
|
16905
|
+
const r = await execa(cmd, args, {
|
|
16906
|
+
reject: false,
|
|
16907
|
+
timeout: TOKEN_TIMEOUT_MS,
|
|
16908
|
+
env: opts?.env,
|
|
16909
|
+
input: opts?.input,
|
|
16910
|
+
stdin: opts?.input === void 0 ? "ignore" : void 0
|
|
16911
|
+
});
|
|
16912
|
+
return { exitCode: r.exitCode ?? 1, stdout: typeof r.stdout === "string" ? r.stdout : "" };
|
|
16913
|
+
};
|
|
16914
|
+
DENY_BACKOFF_MS = 60 * 6e4;
|
|
16915
|
+
UNAUTHENTICATED_MIN_INTERVAL_MS = 5 * 6e4;
|
|
16916
|
+
DENIED_REASON = "no credential on this machine can read its pull requests from GitHub's API; a private repo needs `gh auth login` (or a GitHub credential in git's credential helper)";
|
|
16917
|
+
UNDELIVERED = /* @__PURE__ */ new Set(["error", "invalid"]);
|
|
16918
|
+
}
|
|
16919
|
+
});
|
|
16920
|
+
|
|
15722
16921
|
// src/daemon/localPoller.ts
|
|
15723
16922
|
async function pollRepoOnce(entry, deps) {
|
|
15724
16923
|
const repo = `${entry.repoOwner}/${entry.repoName}`;
|
|
@@ -15786,9 +16985,9 @@ async function pollRepoOnce(entry, deps) {
|
|
|
15786
16985
|
if (decision.kind !== "enqueue") return decision;
|
|
15787
16986
|
const result = await deps.enqueue(entry, row, decision.beforeSha, decision.afterSha);
|
|
15788
16987
|
if (result.dedup) {
|
|
15789
|
-
deps.log(` ${repo}: ${
|
|
16988
|
+
deps.log(` ${repo}: ${short4(decision.afterSha)} already enqueued elsewhere`);
|
|
15790
16989
|
} else {
|
|
15791
|
-
deps.log(` ${repo}: enqueued ${
|
|
16990
|
+
deps.log(` ${repo}: enqueued ${short4(decision.afterSha)} (job ${result.jobId ?? "?"})`);
|
|
15792
16991
|
}
|
|
15793
16992
|
return decision;
|
|
15794
16993
|
}
|
|
@@ -15809,15 +17008,23 @@ async function pollAllRepos(repos, deps) {
|
|
|
15809
17008
|
When the cause is fixed: archme local-watch --resume ${entry.repoOwner}/${entry.repoName}
|
|
15810
17009
|
`
|
|
15811
17010
|
);
|
|
17011
|
+
continue;
|
|
15812
17012
|
}
|
|
15813
17013
|
} catch (err2) {
|
|
15814
17014
|
deps.log(` local poll ${key} failed (will retry): ${err2.message}`);
|
|
17015
|
+
continue;
|
|
17016
|
+
}
|
|
17017
|
+
if (!deps.pollPrs) continue;
|
|
17018
|
+
try {
|
|
17019
|
+
await deps.pollPrs(entry);
|
|
17020
|
+
} catch (err2) {
|
|
17021
|
+
deps.log(` PR poll ${key} failed (will retry): ${err2.message}`);
|
|
15815
17022
|
}
|
|
15816
17023
|
}
|
|
15817
17024
|
}
|
|
15818
17025
|
function makePollerDeps(client, log, gateTimeoutMs) {
|
|
15819
17026
|
const firstSeen = /* @__PURE__ */ new Map();
|
|
15820
|
-
|
|
17027
|
+
const deps = {
|
|
15821
17028
|
git: localGit_exports,
|
|
15822
17029
|
log,
|
|
15823
17030
|
now: () => Date.now(),
|
|
@@ -15835,7 +17042,7 @@ function makePollerDeps(client, log, gateTimeoutMs) {
|
|
|
15835
17042
|
);
|
|
15836
17043
|
},
|
|
15837
17044
|
async readWatchedRepo(watchedRepoId) {
|
|
15838
|
-
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();
|
|
17045
|
+
const { data, error } = await client.from("watched_repos").select("project_id, branch, commit_update_archme_branch, source_kind, enabled, pr_update_mode, repo_owner, repo_name").eq("id", watchedRepoId).is("deleted_at", null).maybeSingle();
|
|
15839
17046
|
if (error || !data) return null;
|
|
15840
17047
|
const r = data;
|
|
15841
17048
|
return {
|
|
@@ -15845,7 +17052,10 @@ function makePollerDeps(client, log, gateTimeoutMs) {
|
|
|
15845
17052
|
// branch`), so both producers resolve the same lane for the same row.
|
|
15846
17053
|
archmeBranch: r.commit_update_archme_branch ?? r.branch,
|
|
15847
17054
|
sourceKind: r.source_kind,
|
|
15848
|
-
enabled: r.enabled
|
|
17055
|
+
enabled: r.enabled,
|
|
17056
|
+
prUpdateMode: r.pr_update_mode ?? "disabled",
|
|
17057
|
+
repoOwner: r.repo_owner,
|
|
17058
|
+
repoName: r.repo_name
|
|
15849
17059
|
};
|
|
15850
17060
|
},
|
|
15851
17061
|
// Shared with the job runner and local-watch's readiness check: three
|
|
@@ -15883,8 +17093,11 @@ function makePollerDeps(client, log, gateTimeoutMs) {
|
|
|
15883
17093
|
return data ?? {};
|
|
15884
17094
|
}
|
|
15885
17095
|
};
|
|
17096
|
+
const prDeps = makePrPollDeps(client, log, (id) => deps.readWatchedRepo(id));
|
|
17097
|
+
deps.pollPrs = (entry) => pollPrsOnce(entry, prDeps);
|
|
17098
|
+
return deps;
|
|
15886
17099
|
}
|
|
15887
|
-
function
|
|
17100
|
+
function short4(sha) {
|
|
15888
17101
|
return sha.slice(0, 7);
|
|
15889
17102
|
}
|
|
15890
17103
|
var DEFAULT_POLL_INTERVAL_MS, DEFAULT_GATE_TIMEOUT_MS;
|
|
@@ -15895,6 +17108,7 @@ var init_localPoller = __esm({
|
|
|
15895
17108
|
init_localRepos();
|
|
15896
17109
|
init_branchMarker();
|
|
15897
17110
|
init_localGit();
|
|
17111
|
+
init_localPrAdapters();
|
|
15898
17112
|
DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
15899
17113
|
DEFAULT_GATE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
|
|
15900
17114
|
}
|
|
@@ -15911,6 +17125,7 @@ __export(run_exports, {
|
|
|
15911
17125
|
buildClaudeExitMessage: () => buildClaudeExitMessage,
|
|
15912
17126
|
buildDevices: () => buildDevices,
|
|
15913
17127
|
buildDiffPrompt: () => buildDiffPrompt,
|
|
17128
|
+
claudeFailureDetail: () => claudeFailureDetail,
|
|
15914
17129
|
computeDiff: () => computeDiff,
|
|
15915
17130
|
parseClaudeResult: () => parseClaudeResult,
|
|
15916
17131
|
probeMcpAuth: () => probeMcpAuth,
|
|
@@ -15921,10 +17136,10 @@ __export(run_exports, {
|
|
|
15921
17136
|
});
|
|
15922
17137
|
import { createClient } from "@supabase/supabase-js";
|
|
15923
17138
|
import { randomUUID } from "node:crypto";
|
|
15924
|
-
import { mkdtemp as mkdtemp3, mkdir as
|
|
17139
|
+
import { mkdtemp as mkdtemp3, mkdir as mkdir11, writeFile as writeFile10, rm as rm7, stat as stat8, chmod as chmod4 } from "node:fs/promises";
|
|
15925
17140
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15926
|
-
import { join as
|
|
15927
|
-
import { homedir as
|
|
17141
|
+
import { join as join19 } from "node:path";
|
|
17142
|
+
import { homedir as homedir6 } from "node:os";
|
|
15928
17143
|
function attributeKillCause(result, recorded) {
|
|
15929
17144
|
if (recorded) return recorded;
|
|
15930
17145
|
if (result.signal === "SIGTERM") return "timeout";
|
|
@@ -15934,6 +17149,12 @@ function attributeKillCause(result, recorded) {
|
|
|
15934
17149
|
function buildClaudeExitMessage(exitCode, causeSuffix, stderr) {
|
|
15935
17150
|
return `claude exited ${exitCode}${causeSuffix}: ${stderr.slice(0, 4e3)}`;
|
|
15936
17151
|
}
|
|
17152
|
+
function claudeFailureDetail(stderr, stdout) {
|
|
17153
|
+
if (stderr.trim() || !stdout.trim()) return null;
|
|
17154
|
+
const parsed = parseClaudeResult(stdout);
|
|
17155
|
+
const text = (parsed.parsed ? parsed.summary : stdout).trim();
|
|
17156
|
+
return text ? text.slice(-FAILURE_DETAIL_MAX) : null;
|
|
17157
|
+
}
|
|
15937
17158
|
async function routeShutdownInterrupt(id, cancelled2, fail, exitCode) {
|
|
15938
17159
|
if (cancelled2.has(id)) return;
|
|
15939
17160
|
if (exitCode === 0) {
|
|
@@ -15955,7 +17176,7 @@ async function lookupProjectSlug(client, projectId) {
|
|
|
15955
17176
|
return `${orgSlug}/${data.slug}`;
|
|
15956
17177
|
}
|
|
15957
17178
|
function cloneDirFor(baseDir, owner, name) {
|
|
15958
|
-
return
|
|
17179
|
+
return join19(baseDir, owner, name);
|
|
15959
17180
|
}
|
|
15960
17181
|
async function exists(p) {
|
|
15961
17182
|
try {
|
|
@@ -16000,8 +17221,8 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
|
|
|
16000
17221
|
};
|
|
16001
17222
|
}
|
|
16002
17223
|
async function buildGitEnv(credentials) {
|
|
16003
|
-
const dir = await mkdtemp3(
|
|
16004
|
-
const askpassPath =
|
|
17224
|
+
const dir = await mkdtemp3(join19(tmpdir3(), "archme-git-askpass-"));
|
|
17225
|
+
const askpassPath = join19(dir, "askpass.sh");
|
|
16005
17226
|
await writeFile10(
|
|
16006
17227
|
askpassPath,
|
|
16007
17228
|
[
|
|
@@ -16015,7 +17236,7 @@ async function buildGitEnv(credentials) {
|
|
|
16015
17236
|
].join("\n"),
|
|
16016
17237
|
{ mode: 448 }
|
|
16017
17238
|
);
|
|
16018
|
-
await
|
|
17239
|
+
await chmod4(askpassPath, 448).catch(() => {
|
|
16019
17240
|
});
|
|
16020
17241
|
const gitConfigEntries = [
|
|
16021
17242
|
["credential.helper", ""],
|
|
@@ -16037,23 +17258,32 @@ async function buildGitEnv(credentials) {
|
|
|
16037
17258
|
GIT_USERNAME: credentials.username,
|
|
16038
17259
|
GIT_PASSWORD: credentials.password
|
|
16039
17260
|
};
|
|
16040
|
-
return { env, cleanup: () =>
|
|
17261
|
+
return { env, cleanup: () => rm7(dir, { recursive: true, force: true }).catch(() => {
|
|
16041
17262
|
}) };
|
|
16042
17263
|
}
|
|
16043
17264
|
async function ensureLocalCloneAndFetch(job, entry) {
|
|
16044
17265
|
const dir = entry.mirrorPath;
|
|
17266
|
+
assertLocalJobShape(job);
|
|
16045
17267
|
await cloneMutex.runExclusive(dir, async () => {
|
|
16046
|
-
if (!await exists(
|
|
17268
|
+
if (!await exists(join19(dir, ".git"))) {
|
|
16047
17269
|
throw new Error(
|
|
16048
17270
|
`local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
|
|
16049
17271
|
);
|
|
16050
17272
|
}
|
|
16051
17273
|
await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, reject: false });
|
|
17274
|
+
if (job.source_mode === "pr_update") {
|
|
17275
|
+
await verifyLocalPrJob(job, async (args) => {
|
|
17276
|
+
const r = await execa("git", args, { cwd: dir, reject: false });
|
|
17277
|
+
return { exitCode: r.exitCode ?? 1, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") };
|
|
17278
|
+
});
|
|
17279
|
+
return;
|
|
17280
|
+
}
|
|
16052
17281
|
const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, reject: false });
|
|
16053
17282
|
if (afterCheck.exitCode !== 0) {
|
|
16054
17283
|
await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
|
|
16055
17284
|
}
|
|
16056
17285
|
}).catch((err2) => {
|
|
17286
|
+
if (err2 instanceof LocalPrJobRejectedError) throw err2;
|
|
16057
17287
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
16058
17288
|
throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
|
|
16059
17289
|
});
|
|
@@ -16063,15 +17293,16 @@ async function ensureLocalCloneAndFetch(job, entry) {
|
|
|
16063
17293
|
async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
16064
17294
|
const localEntry = findLocalRepoForJob(await loadLocalRepos(), job);
|
|
16065
17295
|
if (localEntry) return await ensureLocalCloneAndFetch(job, localEntry);
|
|
17296
|
+
assertLocalJobShape(job);
|
|
16066
17297
|
const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
|
|
16067
17298
|
if (!deviceId) throw new Error("daemon is not signed in with a registered device");
|
|
16068
17299
|
const credentials = await getRepoCredentials(ctx, job.id, deviceId);
|
|
16069
|
-
await
|
|
17300
|
+
await mkdir11(ctx.cloneBaseDir, { recursive: true });
|
|
16070
17301
|
const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
|
|
16071
17302
|
const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
|
|
16072
17303
|
await cloneMutex.runExclusive(dir, async () => {
|
|
16073
|
-
if (!await exists(
|
|
16074
|
-
await
|
|
17304
|
+
if (!await exists(join19(dir, ".git"))) {
|
|
17305
|
+
await mkdir11(join19(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
|
|
16075
17306
|
console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
|
|
16076
17307
|
await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
|
|
16077
17308
|
} else {
|
|
@@ -16100,12 +17331,12 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
|
|
|
16100
17331
|
return { dir, gitEnv: env, cleanupGitEnv };
|
|
16101
17332
|
}
|
|
16102
17333
|
async function createJobWorktree(repoDir, job) {
|
|
16103
|
-
const parentDir = await mkdtemp3(
|
|
16104
|
-
const worktreeDir =
|
|
17334
|
+
const parentDir = await mkdtemp3(join19(tmpdir3(), "archme-job-worktree-"));
|
|
17335
|
+
const worktreeDir = join19(parentDir, "repo");
|
|
16105
17336
|
try {
|
|
16106
17337
|
await cloneMutex.runExclusive(repoDir, () => execa("git", ["worktree", "add", "--detach", worktreeDir, job.after_sha], { cwd: repoDir, reject: true }));
|
|
16107
17338
|
} catch (err2) {
|
|
16108
|
-
await
|
|
17339
|
+
await rm7(parentDir, { recursive: true, force: true }).catch(() => {
|
|
16109
17340
|
});
|
|
16110
17341
|
throw err2;
|
|
16111
17342
|
}
|
|
@@ -16116,7 +17347,7 @@ async function createJobWorktree(repoDir, job) {
|
|
|
16116
17347
|
await execa("git", ["worktree", "remove", "--force", worktreeDir], { cwd: repoDir, reject: false });
|
|
16117
17348
|
await execa("git", ["worktree", "prune"], { cwd: repoDir, reject: false });
|
|
16118
17349
|
});
|
|
16119
|
-
await
|
|
17350
|
+
await rm7(parentDir, { recursive: true, force: true });
|
|
16120
17351
|
}
|
|
16121
17352
|
};
|
|
16122
17353
|
}
|
|
@@ -16696,7 +17927,28 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
16696
17927
|
}, HEARTBEAT_INTERVAL_MS);
|
|
16697
17928
|
try {
|
|
16698
17929
|
const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
|
|
16699
|
-
|
|
17930
|
+
let clone;
|
|
17931
|
+
try {
|
|
17932
|
+
clone = await ensureCloneAndFetch(ctx, job, deviceId);
|
|
17933
|
+
} catch (err2) {
|
|
17934
|
+
if (!(err2 instanceof LocalPrJobRejectedError)) throw err2;
|
|
17935
|
+
log(`${err2.message} \u2014 completing no_op without advancing the marker`);
|
|
17936
|
+
markPhase("clone_fetch", { mode: "rejected" });
|
|
17937
|
+
await rpcComplete(
|
|
17938
|
+
ctx,
|
|
17939
|
+
job.id,
|
|
17940
|
+
deviceId,
|
|
17941
|
+
err2.message,
|
|
17942
|
+
"no_op",
|
|
17943
|
+
false,
|
|
17944
|
+
{ mode: "rejected", durations_ms: timings },
|
|
17945
|
+
{ stdout: "", stderr: "" },
|
|
17946
|
+
false
|
|
17947
|
+
/* advance_marker */
|
|
17948
|
+
);
|
|
17949
|
+
return;
|
|
17950
|
+
}
|
|
17951
|
+
const { dir: repoDir, gitEnv, cleanupGitEnv } = clone;
|
|
16700
17952
|
cleanupFns.push(cleanupGitEnv);
|
|
16701
17953
|
activeCleanups.add(cleanupGitEnv);
|
|
16702
17954
|
markPhase("clone_fetch");
|
|
@@ -16807,7 +18059,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
16807
18059
|
affectedForComplete = affected;
|
|
16808
18060
|
const symbolsInFile = async (file) => {
|
|
16809
18061
|
log(`parse ${file} (removed-symbol scan)`);
|
|
16810
|
-
const outline = await runOutline(
|
|
18062
|
+
const outline = await runOutline(join19(worktreeDir, file));
|
|
16811
18063
|
if (outline.tier !== 2) return null;
|
|
16812
18064
|
return outline.data.symbols.map((s) => s.name);
|
|
16813
18065
|
};
|
|
@@ -16845,7 +18097,7 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
16845
18097
|
`MCP auth preflight failed after ${probe.attempts} attempt(s) (last status ${probe.lastStatus ?? "n/a"}): ${probe.detail}. Not spawning claude \u2014 the architecture would not be writable and the run would silently no-op. Failing the job so it retries with a freshly minted token.`
|
|
16846
18098
|
);
|
|
16847
18099
|
}
|
|
16848
|
-
const model = await
|
|
18100
|
+
const model = await resolveRunnableModel(ctx.supabase, { warn: log });
|
|
16849
18101
|
metrics.model = model;
|
|
16850
18102
|
log(`spawning claude (mcp config at ${configPath}, model ${model})`);
|
|
16851
18103
|
const proc = spawnClaude({
|
|
@@ -16883,6 +18135,8 @@ async function runJob(ctx, job, claimDeviceId) {
|
|
|
16883
18135
|
if (result.exitCode !== 0) {
|
|
16884
18136
|
const causeSuffix = cause ? ` (${result.signal ?? "exit"}, cause: ${cause})` : "";
|
|
16885
18137
|
const stderr = result.stderr ? String(result.stderr) : "";
|
|
18138
|
+
const detail = claudeFailureDetail(stderr, result.stdout ? String(result.stdout) : "");
|
|
18139
|
+
if (detail) log(`claude output: ${detail.replace(/\n/g, " ")}`);
|
|
16886
18140
|
throw new Error(buildClaudeExitMessage(result.exitCode, causeSuffix, stderr));
|
|
16887
18141
|
}
|
|
16888
18142
|
const runResult = parseClaudeResult(result.stdout ? String(result.stdout) : "");
|
|
@@ -17386,7 +18640,7 @@ ${signal} received, shutting down`);
|
|
|
17386
18640
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
17387
18641
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
17388
18642
|
}
|
|
17389
|
-
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, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
|
|
18643
|
+
var HEARTBEAT_INTERVAL_MS, CLAUDE_TIMEOUT_MS, CLAUDE_LARGE_DIFF_TIMEOUT_MS, SAFETY_SWEEP_MS, LOG_STDOUT_CAP_BYTES, LOG_STDERR_CAP_BYTES, FAILURE_DETAIL_MAX, 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, STORY_COVERAGE_GATE, NEW_STORY_GUIDANCE, STORY_DESCRIPTION_EDIT_RULE, PHASE_GUIDANCE, BASE_RULES, DIFF_RULES, finite, channelSeq, MultipleAccountsError, __testDispatch;
|
|
17390
18644
|
var init_run = __esm({
|
|
17391
18645
|
"src/daemon/run.ts"() {
|
|
17392
18646
|
"use strict";
|
|
@@ -17424,6 +18678,7 @@ var init_run = __esm({
|
|
|
17424
18678
|
init_branchMarker();
|
|
17425
18679
|
init_branchMarker();
|
|
17426
18680
|
init_localRepos();
|
|
18681
|
+
init_localPrJob();
|
|
17427
18682
|
init_localPoller();
|
|
17428
18683
|
HEARTBEAT_INTERVAL_MS = 6e4;
|
|
17429
18684
|
CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
@@ -17431,6 +18686,7 @@ var init_run = __esm({
|
|
|
17431
18686
|
SAFETY_SWEEP_MS = 3e4;
|
|
17432
18687
|
LOG_STDOUT_CAP_BYTES = 30 * 1024;
|
|
17433
18688
|
LOG_STDERR_CAP_BYTES = 30 * 1024;
|
|
18689
|
+
FAILURE_DETAIL_MAX = 400;
|
|
17434
18690
|
PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = CLAUDE_LARGE_DIFF_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
17435
18691
|
inFlight = /* @__PURE__ */ new Map();
|
|
17436
18692
|
cancelled = /* @__PURE__ */ new Set();
|
|
@@ -17445,7 +18701,7 @@ var init_run = __esm({
|
|
|
17445
18701
|
MAX_CONCURRENT_JOBS = Math.max(1, Number(process.env.MAX_CONCURRENT_JOBS) || 3);
|
|
17446
18702
|
ctxByJob = /* @__PURE__ */ new Map();
|
|
17447
18703
|
deviceByJob = /* @__PURE__ */ new Map();
|
|
17448
|
-
DEFAULT_CLONE_BASE_DIR = `${
|
|
18704
|
+
DEFAULT_CLONE_BASE_DIR = `${homedir6()}/.archme-job-runner/clones`;
|
|
17449
18705
|
INLINE_PATCH_BYTE_CAP = 2e5;
|
|
17450
18706
|
GIT_SHA_RE = /^[0-9a-f]{40}$/;
|
|
17451
18707
|
CURATOR_PREAMBLE = `## Your role
|
|
@@ -17573,6 +18829,7 @@ ${ANCHOR_VERIFICATION}`;
|
|
|
17573
18829
|
claimedDeviceForJob(jobId) {
|
|
17574
18830
|
return deviceByJob.get(jobId);
|
|
17575
18831
|
},
|
|
18832
|
+
runJob,
|
|
17576
18833
|
// Exposes the clone/fetch entrypoint so a test can assert that the repo-token
|
|
17577
18834
|
// mint (getRepoCredentials -> agents-device/repo-token, gated on
|
|
17578
18835
|
// claimed_by_device_id) uses the CLAIM-time device rather than recomputing via
|
|
@@ -17622,7 +18879,7 @@ var dispatch_exports2 = {};
|
|
|
17622
18879
|
__export(dispatch_exports2, {
|
|
17623
18880
|
dispatchDaemon: () => dispatchDaemon
|
|
17624
18881
|
});
|
|
17625
|
-
import { rm as
|
|
18882
|
+
import { rm as rm8 } from "node:fs/promises";
|
|
17626
18883
|
async function dispatchDaemon(route2) {
|
|
17627
18884
|
switch (route2.action) {
|
|
17628
18885
|
case "help":
|
|
@@ -17720,7 +18977,7 @@ async function dispatchDaemon(route2) {
|
|
|
17720
18977
|
await orchestrateStop(c3);
|
|
17721
18978
|
await c3.uninstall();
|
|
17722
18979
|
if (route2.purge) {
|
|
17723
|
-
await
|
|
18980
|
+
await rm8(resolveLogDir(process.platform, process.env), { recursive: true, force: true });
|
|
17724
18981
|
}
|
|
17725
18982
|
console.log(
|
|
17726
18983
|
route2.purge ? "Uninstalled. Logs purged." : "Uninstalled. Logs preserved (use --purge to remove)."
|
|
@@ -17761,8 +19018,8 @@ async function defaultLoadSupabaseState(_deviceId) {
|
|
|
17761
19018
|
async function defaultLoadUptime(pid) {
|
|
17762
19019
|
if (process.platform === "linux") {
|
|
17763
19020
|
try {
|
|
17764
|
-
const { readFile:
|
|
17765
|
-
const stat11 = await
|
|
19021
|
+
const { readFile: readFile16 } = await import("node:fs/promises");
|
|
19022
|
+
const stat11 = await readFile16(`/proc/${pid}/stat`, "utf8");
|
|
17766
19023
|
const parts = stat11.split(") ").pop()?.split(" ") ?? [];
|
|
17767
19024
|
const ticks = Number(parts[19]);
|
|
17768
19025
|
if (!Number.isFinite(ticks)) return null;
|
|
@@ -17818,7 +19075,47 @@ var init_dispatch2 = __esm({
|
|
|
17818
19075
|
}
|
|
17819
19076
|
});
|
|
17820
19077
|
|
|
19078
|
+
// src/auth/identity.ts
|
|
19079
|
+
function decodeJwtClaims(jwt) {
|
|
19080
|
+
const segment = jwt?.split(".")[1];
|
|
19081
|
+
if (!segment) return null;
|
|
19082
|
+
try {
|
|
19083
|
+
const parsed = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
19084
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
19085
|
+
} catch {
|
|
19086
|
+
return null;
|
|
19087
|
+
}
|
|
19088
|
+
}
|
|
19089
|
+
function str(v) {
|
|
19090
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
19091
|
+
}
|
|
19092
|
+
function describeAccount(input) {
|
|
19093
|
+
const meta = input.claims?.user_metadata ?? {};
|
|
19094
|
+
const email = str(input.email) ?? str(input.claims?.email);
|
|
19095
|
+
const name = str(meta.name) ?? str(meta.full_name);
|
|
19096
|
+
const keyed = input.kind === "key" || isKeyedAccountEmail(email) || input.claims?.app_metadata?.account_kind === "key";
|
|
19097
|
+
if (keyed && email) {
|
|
19098
|
+
const handle = email.slice(0, email.indexOf("@"));
|
|
19099
|
+
return name ? `${name} (keyed account ${handle})` : `keyed account ${handle}`;
|
|
19100
|
+
}
|
|
19101
|
+
const login = str(meta.user_name) ?? str(meta.preferred_username);
|
|
19102
|
+
if (login) return `${name ?? email ?? login} (GitHub @${login})`;
|
|
19103
|
+
return email ?? str(input.sub) ?? str(input.claims?.sub) ?? "unknown account";
|
|
19104
|
+
}
|
|
19105
|
+
var init_identity = __esm({
|
|
19106
|
+
"src/auth/identity.ts"() {
|
|
19107
|
+
"use strict";
|
|
19108
|
+
init_accountKey();
|
|
19109
|
+
}
|
|
19110
|
+
});
|
|
19111
|
+
|
|
17821
19112
|
// src/scan/authedClient.ts
|
|
19113
|
+
var authedClient_exports = {};
|
|
19114
|
+
__export(authedClient_exports, {
|
|
19115
|
+
SCAN_CLAUDE_TIMEOUT_MS: () => SCAN_CLAUDE_TIMEOUT_MS,
|
|
19116
|
+
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS: () => SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS,
|
|
19117
|
+
createAuthedClient: () => createAuthedClient
|
|
19118
|
+
});
|
|
17822
19119
|
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
17823
19120
|
async function createAuthedClient(opts) {
|
|
17824
19121
|
initBearer({
|
|
@@ -17844,7 +19141,13 @@ async function createAuthedClient(opts) {
|
|
|
17844
19141
|
supabase,
|
|
17845
19142
|
userId,
|
|
17846
19143
|
orgId: getCurrentOrgId(),
|
|
17847
|
-
identity:
|
|
19144
|
+
identity: describeAccount({
|
|
19145
|
+
kind: loaded.kind,
|
|
19146
|
+
email: loaded.user.email,
|
|
19147
|
+
sub: loaded.user.sub,
|
|
19148
|
+
claims: decodeJwtClaims(getCurrentAccessToken())
|
|
19149
|
+
}),
|
|
19150
|
+
accountKind: loaded.kind ?? "oauth"
|
|
17848
19151
|
};
|
|
17849
19152
|
}
|
|
17850
19153
|
var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS;
|
|
@@ -17852,12 +19155,18 @@ var init_authedClient = __esm({
|
|
|
17852
19155
|
"src/scan/authedClient.ts"() {
|
|
17853
19156
|
"use strict";
|
|
17854
19157
|
init_bearer();
|
|
19158
|
+
init_identity();
|
|
17855
19159
|
SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
|
|
17856
19160
|
SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
|
|
17857
19161
|
}
|
|
17858
19162
|
});
|
|
17859
19163
|
|
|
17860
19164
|
// src/scan/web-url.ts
|
|
19165
|
+
var web_url_exports = {};
|
|
19166
|
+
__export(web_url_exports, {
|
|
19167
|
+
buildWebUrl: () => buildWebUrl,
|
|
19168
|
+
deriveWebOrigin: () => deriveWebOrigin
|
|
19169
|
+
});
|
|
17861
19170
|
function deriveWebOrigin(apiUrl, onMalformed = "throw") {
|
|
17862
19171
|
let parsed;
|
|
17863
19172
|
try {
|
|
@@ -17883,6 +19192,277 @@ var init_web_url = __esm({
|
|
|
17883
19192
|
}
|
|
17884
19193
|
});
|
|
17885
19194
|
|
|
19195
|
+
// src/account/open.ts
|
|
19196
|
+
var open_exports = {};
|
|
19197
|
+
__export(open_exports, {
|
|
19198
|
+
boardPathFromMcpJson: () => boardPathFromMcpJson,
|
|
19199
|
+
buildKeyedOpenUrl: () => buildKeyedOpenUrl,
|
|
19200
|
+
defaultOpenBrowser: () => defaultOpenBrowser,
|
|
19201
|
+
defaultReadMcpJson: () => defaultReadMcpJson,
|
|
19202
|
+
dispatchOpen: () => dispatchOpen,
|
|
19203
|
+
freshAccessToken: () => freshAccessToken,
|
|
19204
|
+
postBoardLinkWith: () => postBoardLinkWith,
|
|
19205
|
+
runOpen: () => runOpen,
|
|
19206
|
+
safeNextPath: () => safeNextPath
|
|
19207
|
+
});
|
|
19208
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
19209
|
+
import { join as join20 } from "node:path";
|
|
19210
|
+
function safeNextPath(path7) {
|
|
19211
|
+
if (!path7 || !path7.startsWith("/") || path7.startsWith("//") || path7.startsWith("/\\")) return "/";
|
|
19212
|
+
return path7;
|
|
19213
|
+
}
|
|
19214
|
+
function boardPathFromMcpJson(content) {
|
|
19215
|
+
if (!content) return "/";
|
|
19216
|
+
try {
|
|
19217
|
+
const servers = JSON.parse(content).mcpServers ?? {};
|
|
19218
|
+
const entry = Object.entries(servers).find(([name]) => /archme/i.test(name))?.[1];
|
|
19219
|
+
if (typeof entry?.url !== "string") return "/";
|
|
19220
|
+
const params = new URL(entry.url).searchParams;
|
|
19221
|
+
const [org, slug, ...rest] = (params.get("project") ?? "").split("/");
|
|
19222
|
+
if (!org || !slug || rest.length) return "/";
|
|
19223
|
+
const branch = params.get("branch");
|
|
19224
|
+
const segments = [org, slug, ...branch ? [branch] : []].map(encodeURIComponent);
|
|
19225
|
+
return `/a/${segments.join("/")}`;
|
|
19226
|
+
} catch {
|
|
19227
|
+
return "/";
|
|
19228
|
+
}
|
|
19229
|
+
}
|
|
19230
|
+
function buildKeyedOpenUrl(webOrigin, link, next) {
|
|
19231
|
+
const fragment = new URLSearchParams({
|
|
19232
|
+
token_hash: link.token_hash,
|
|
19233
|
+
u: link.user_id,
|
|
19234
|
+
label: link.handle,
|
|
19235
|
+
next: safeNextPath(next)
|
|
19236
|
+
});
|
|
19237
|
+
return `${webOrigin.replace(/\/$/, "")}/auth/key#${fragment.toString()}`;
|
|
19238
|
+
}
|
|
19239
|
+
async function runOpen(opts, deps) {
|
|
19240
|
+
const active = await deps.loadActive();
|
|
19241
|
+
if (!active) {
|
|
19242
|
+
deps.error("Not signed in. Run `archme login` first.");
|
|
19243
|
+
return 1;
|
|
19244
|
+
}
|
|
19245
|
+
const next = safeNextPath(opts.next ?? boardPathFromMcpJson(await deps.readMcpJson(opts.cwd ?? process.cwd())));
|
|
19246
|
+
const boardUrl = `${deps.webOrigin.replace(/\/$/, "")}${next}`;
|
|
19247
|
+
if (credentialKind(active) !== "key") return await show(boardUrl, opts, deps, "Your board");
|
|
19248
|
+
let link = null;
|
|
19249
|
+
let failure = "";
|
|
19250
|
+
try {
|
|
19251
|
+
const res = await deps.postBoardLink(await deps.getAccessToken());
|
|
19252
|
+
const data = res.data;
|
|
19253
|
+
if (res.status === 200 && data.token_hash && data.user_id && data.handle) link = data;
|
|
19254
|
+
else failure = `${res.status} ${data.error ?? JSON.stringify(res.data)}`;
|
|
19255
|
+
} catch (err2) {
|
|
19256
|
+
failure = err2 instanceof Error ? err2.message : String(err2);
|
|
19257
|
+
}
|
|
19258
|
+
if (!link) {
|
|
19259
|
+
deps.error(`Could not create a sign-in link (${failure}). Opening the board without one.`);
|
|
19260
|
+
await show(boardUrl, opts, deps, "Your board");
|
|
19261
|
+
return 1;
|
|
19262
|
+
}
|
|
19263
|
+
const signedIn = buildKeyedOpenUrl(deps.webOrigin, link, next);
|
|
19264
|
+
if (opts.hideLink) {
|
|
19265
|
+
if (await deps.openBrowser(signedIn)) {
|
|
19266
|
+
deps.log(`Opened your board in a browser, signed in as ${link.handle}.`);
|
|
19267
|
+
} else {
|
|
19268
|
+
deps.log(`Your board: ${boardUrl}`);
|
|
19269
|
+
deps.log(" Run `archme open` to sign this browser in.");
|
|
19270
|
+
}
|
|
19271
|
+
return 0;
|
|
19272
|
+
}
|
|
19273
|
+
if (!opts.print && await deps.openBrowser(signedIn)) {
|
|
19274
|
+
deps.log(`Opened your board in a browser, signed in as ${link.handle}.`);
|
|
19275
|
+
return 0;
|
|
19276
|
+
}
|
|
19277
|
+
deps.log(`Your board, signed in as ${link.handle}: ${signedIn}`);
|
|
19278
|
+
deps.log(" (The link signs you in once and expires soon. Don't share it.)");
|
|
19279
|
+
return 0;
|
|
19280
|
+
}
|
|
19281
|
+
async function show(url, opts, deps, label) {
|
|
19282
|
+
deps.log(`${label}: ${url}`);
|
|
19283
|
+
if (!opts.print) await deps.openBrowser(url);
|
|
19284
|
+
return 0;
|
|
19285
|
+
}
|
|
19286
|
+
async function defaultOpenBrowser(url) {
|
|
19287
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
|
|
19288
|
+
if (!cmd) return false;
|
|
19289
|
+
if (cmd === "xdg-open" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
19290
|
+
const { execa: execa2 } = await Promise.resolve().then(() => (init_execa(), execa_exports));
|
|
19291
|
+
const r = await execa2(cmd, [url], { reject: false, stdio: "ignore" });
|
|
19292
|
+
return r.exitCode === 0;
|
|
19293
|
+
}
|
|
19294
|
+
async function defaultReadMcpJson(cwd) {
|
|
19295
|
+
try {
|
|
19296
|
+
return await readFile13(join20(cwd, ".mcp.json"), "utf8");
|
|
19297
|
+
} catch {
|
|
19298
|
+
return null;
|
|
19299
|
+
}
|
|
19300
|
+
}
|
|
19301
|
+
async function postBoardLinkWith(supabaseUrl, accessToken) {
|
|
19302
|
+
const { parseJsonResponse: parseJsonResponse2 } = await Promise.resolve().then(() => (init_oauthClient(), oauthClient_exports));
|
|
19303
|
+
const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device/board-link`, {
|
|
19304
|
+
method: "POST",
|
|
19305
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
|
|
19306
|
+
body: "{}"
|
|
19307
|
+
});
|
|
19308
|
+
return { status: response.status, data: await parseJsonResponse2(response) };
|
|
19309
|
+
}
|
|
19310
|
+
async function freshAccessToken() {
|
|
19311
|
+
const { resolveConfig: resolveConfig2 } = await Promise.resolve().then(() => (init_defaults(), defaults_exports));
|
|
19312
|
+
const { createAuthedClient: createAuthedClient2, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS2 } = await Promise.resolve().then(() => (init_authedClient(), authedClient_exports));
|
|
19313
|
+
const { getCurrentAccessToken: getCurrentAccessToken2 } = await Promise.resolve().then(() => (init_bearer(), bearer_exports));
|
|
19314
|
+
const cfg = resolveConfig2();
|
|
19315
|
+
await createAuthedClient2({
|
|
19316
|
+
supabaseUrl: cfg.supabaseUrl,
|
|
19317
|
+
supabaseAnonKey: cfg.supabaseAnonKey,
|
|
19318
|
+
clientId: cfg.archmeDaemonClientId,
|
|
19319
|
+
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS2
|
|
19320
|
+
});
|
|
19321
|
+
const token = getCurrentAccessToken2();
|
|
19322
|
+
if (!token) throw new Error("signed in, but no access token was issued");
|
|
19323
|
+
return token;
|
|
19324
|
+
}
|
|
19325
|
+
async function dispatchOpen(argv) {
|
|
19326
|
+
const unknown = argv.filter((a2) => a2 !== "--print");
|
|
19327
|
+
if (unknown.length) {
|
|
19328
|
+
console.error(`archme open: unexpected argument \`${unknown[0]}\`
|
|
19329
|
+
Usage: archme open [--print]`);
|
|
19330
|
+
return 2;
|
|
19331
|
+
}
|
|
19332
|
+
const { resolveConfig: resolveConfig2 } = await Promise.resolve().then(() => (init_defaults(), defaults_exports));
|
|
19333
|
+
const { deriveWebOrigin: deriveWebOrigin2 } = await Promise.resolve().then(() => (init_web_url(), web_url_exports));
|
|
19334
|
+
const cfg = resolveConfig2();
|
|
19335
|
+
return await runOpen({ print: argv.includes("--print") }, {
|
|
19336
|
+
webOrigin: deriveWebOrigin2(cfg.archmeMcpBaseUrl),
|
|
19337
|
+
loadActive: async () => (await loadCredentials(cfg.supabaseUrl)).credentials,
|
|
19338
|
+
getAccessToken: freshAccessToken,
|
|
19339
|
+
postBoardLink: (token) => postBoardLinkWith(cfg.supabaseUrl, token),
|
|
19340
|
+
readMcpJson: defaultReadMcpJson,
|
|
19341
|
+
openBrowser: defaultOpenBrowser,
|
|
19342
|
+
log: (m) => console.log(m),
|
|
19343
|
+
error: (m) => console.error(m)
|
|
19344
|
+
});
|
|
19345
|
+
}
|
|
19346
|
+
var init_open = __esm({
|
|
19347
|
+
"src/account/open.ts"() {
|
|
19348
|
+
"use strict";
|
|
19349
|
+
init_credentials();
|
|
19350
|
+
}
|
|
19351
|
+
});
|
|
19352
|
+
|
|
19353
|
+
// src/account/key.ts
|
|
19354
|
+
var key_exports = {};
|
|
19355
|
+
__export(key_exports, {
|
|
19356
|
+
dispatchKey: () => dispatchKey,
|
|
19357
|
+
parseKeyArgs: () => parseKeyArgs,
|
|
19358
|
+
runKey: () => runKey
|
|
19359
|
+
});
|
|
19360
|
+
function parseKeyArgs(argv) {
|
|
19361
|
+
const cmd = { action: "list", yes: false };
|
|
19362
|
+
let sawAction = false;
|
|
19363
|
+
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
19364
|
+
const arg = argv[i2];
|
|
19365
|
+
if (arg === "--yes") {
|
|
19366
|
+
cmd.yes = true;
|
|
19367
|
+
continue;
|
|
19368
|
+
}
|
|
19369
|
+
if (arg === "--handle" || arg.startsWith("--handle=")) {
|
|
19370
|
+
const value = arg.startsWith("--handle=") ? arg.slice("--handle=".length) : argv[++i2];
|
|
19371
|
+
if (!value || value.startsWith("-")) return { ok: false, error: "--handle requires a value" };
|
|
19372
|
+
cmd.handle = value;
|
|
19373
|
+
continue;
|
|
19374
|
+
}
|
|
19375
|
+
if (!sawAction && (arg === "list" || arg === "show" || arg === "forget")) {
|
|
19376
|
+
cmd.action = arg;
|
|
19377
|
+
sawAction = true;
|
|
19378
|
+
continue;
|
|
19379
|
+
}
|
|
19380
|
+
return { ok: false, error: `unexpected argument \`${arg}\`` };
|
|
19381
|
+
}
|
|
19382
|
+
return { ok: true, cmd };
|
|
19383
|
+
}
|
|
19384
|
+
function pickKey(keys, handle, active) {
|
|
19385
|
+
if (handle) return keys.find((k) => k.handle === handle) ?? `No saved key for ${handle}. \`archme key list\` shows what's saved.`;
|
|
19386
|
+
const mine = keys.find((k) => k.user_id === active?.user_id);
|
|
19387
|
+
if (mine) return mine;
|
|
19388
|
+
if (keys.length === 1) return keys[0];
|
|
19389
|
+
return `Several keys are saved (${keys.map((k) => k.handle).join(", ")}). Pick one with --handle <handle>.`;
|
|
19390
|
+
}
|
|
19391
|
+
async function runKey(cmd, deps) {
|
|
19392
|
+
const keys = await deps.listKeys();
|
|
19393
|
+
const active = await deps.loadActive();
|
|
19394
|
+
if (cmd.action === "list") {
|
|
19395
|
+
if (keys.length === 0) {
|
|
19396
|
+
deps.log("No saved keys on this machine.");
|
|
19397
|
+
return 0;
|
|
19398
|
+
}
|
|
19399
|
+
for (const k of [...keys].reverse()) {
|
|
19400
|
+
const mark = k.user_id === active?.user_id ? " (signed in)" : "";
|
|
19401
|
+
deps.log(`${k.handle} saved ${k.saved_at.slice(0, 10)}${mark}`);
|
|
19402
|
+
}
|
|
19403
|
+
return 0;
|
|
19404
|
+
}
|
|
19405
|
+
if (keys.length === 0) {
|
|
19406
|
+
deps.error("No saved keys on this machine.");
|
|
19407
|
+
return 1;
|
|
19408
|
+
}
|
|
19409
|
+
const picked = pickKey(keys, cmd.handle, active);
|
|
19410
|
+
if (typeof picked === "string") {
|
|
19411
|
+
deps.error(picked);
|
|
19412
|
+
return 1;
|
|
19413
|
+
}
|
|
19414
|
+
if (cmd.action === "show") {
|
|
19415
|
+
deps.log(picked.key);
|
|
19416
|
+
return 0;
|
|
19417
|
+
}
|
|
19418
|
+
const signedInAsIt = active?.user_id === picked.user_id;
|
|
19419
|
+
deps.error(
|
|
19420
|
+
`Warning: this key is the only way into ${picked.handle} and its boards. Copy it somewhere safe first (\`archme key show --handle ` + picked.handle + "`)."
|
|
19421
|
+
);
|
|
19422
|
+
if (!cmd.yes) {
|
|
19423
|
+
if (!deps.interactive) {
|
|
19424
|
+
deps.error("Not forgetting without confirmation. Re-run with --yes.");
|
|
19425
|
+
return 2;
|
|
19426
|
+
}
|
|
19427
|
+
const answer = (await deps.promptLine(`Forget the key for ${picked.handle}? [y/N]`)).trim().toLowerCase();
|
|
19428
|
+
if (answer !== "y" && answer !== "yes") {
|
|
19429
|
+
deps.log("Kept.");
|
|
19430
|
+
return 2;
|
|
19431
|
+
}
|
|
19432
|
+
}
|
|
19433
|
+
await deps.forgetKey(picked.user_id);
|
|
19434
|
+
deps.log(`Forgot the key for ${picked.handle}.`);
|
|
19435
|
+
if (signedInAsIt) deps.log("You're still signed in as it on this machine until you `archme logout` or sign in as someone else.");
|
|
19436
|
+
return 0;
|
|
19437
|
+
}
|
|
19438
|
+
async function dispatchKey(argv) {
|
|
19439
|
+
const parsed = parseKeyArgs(argv);
|
|
19440
|
+
if (!parsed.ok) {
|
|
19441
|
+
console.error(`archme key: ${parsed.error}
|
|
19442
|
+
Usage: archme key [list|show|forget] [--handle <handle>] [--yes]`);
|
|
19443
|
+
return 2;
|
|
19444
|
+
}
|
|
19445
|
+
const { resolveConfig: resolveConfig2 } = await Promise.resolve().then(() => (init_defaults(), defaults_exports));
|
|
19446
|
+
const { listKeys: listKeys2, forgetKey: forgetKey2 } = await Promise.resolve().then(() => (init_keyRing(), keyRing_exports));
|
|
19447
|
+
const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
19448
|
+
const { promptLine: promptLine2, stdinIsInteractive: stdinIsInteractive2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
|
|
19449
|
+
const cfg = resolveConfig2();
|
|
19450
|
+
return await runKey(parsed.cmd, {
|
|
19451
|
+
listKeys: () => listKeys2(cfg.supabaseUrl),
|
|
19452
|
+
forgetKey: (userId) => forgetKey2(cfg.supabaseUrl, userId),
|
|
19453
|
+
loadActive: async () => (await loadCredentials2(cfg.supabaseUrl)).credentials,
|
|
19454
|
+
interactive: stdinIsInteractive2(),
|
|
19455
|
+
promptLine: promptLine2,
|
|
19456
|
+
log: (m) => console.log(m),
|
|
19457
|
+
error: (m) => console.error(m)
|
|
19458
|
+
});
|
|
19459
|
+
}
|
|
19460
|
+
var init_key = __esm({
|
|
19461
|
+
"src/account/key.ts"() {
|
|
19462
|
+
"use strict";
|
|
19463
|
+
}
|
|
19464
|
+
});
|
|
19465
|
+
|
|
17886
19466
|
// src/scan/architecture-detail.ts
|
|
17887
19467
|
function tryParse2(s) {
|
|
17888
19468
|
try {
|
|
@@ -18073,8 +19653,8 @@ var init_transcript_signals = __esm({
|
|
|
18073
19653
|
});
|
|
18074
19654
|
|
|
18075
19655
|
// src/scan/report.ts
|
|
18076
|
-
import { mkdir as
|
|
18077
|
-
import { dirname as
|
|
19656
|
+
import { mkdir as mkdir12, writeFile as writeFile11 } from "node:fs/promises";
|
|
19657
|
+
import { dirname as dirname14 } from "node:path";
|
|
18078
19658
|
async function buildReport(input) {
|
|
18079
19659
|
return {
|
|
18080
19660
|
schema_version: REPORT_SCHEMA_VERSION,
|
|
@@ -18108,9 +19688,9 @@ async function buildReport(input) {
|
|
|
18108
19688
|
};
|
|
18109
19689
|
}
|
|
18110
19690
|
async function writeReport(reportPath, orphansTxtPath, report, orphansFull) {
|
|
18111
|
-
await
|
|
19691
|
+
await mkdir12(dirname14(reportPath), { recursive: true });
|
|
18112
19692
|
await writeFile11(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
18113
|
-
await
|
|
19693
|
+
await mkdir12(dirname14(orphansTxtPath), { recursive: true });
|
|
18114
19694
|
await writeFile11(
|
|
18115
19695
|
orphansTxtPath,
|
|
18116
19696
|
orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
|
|
@@ -18154,8 +19734,8 @@ var init_claimDefaultProject = __esm({
|
|
|
18154
19734
|
|
|
18155
19735
|
// src/scan/run-scan.ts
|
|
18156
19736
|
import { createWriteStream as createWriteStream2 } from "node:fs";
|
|
18157
|
-
import { mkdir as
|
|
18158
|
-
import { basename as basename2, join as
|
|
19737
|
+
import { mkdir as mkdir13, readFile as readFile14 } from "node:fs/promises";
|
|
19738
|
+
import { basename as basename2, join as join21 } from "node:path";
|
|
18159
19739
|
function buildBootstrapPrompt(opts = {}) {
|
|
18160
19740
|
const { promptVariant, interactive, briefing } = opts;
|
|
18161
19741
|
let base = interactive ? briefing ? `${BOOTSTRAP_INTERACTIVE}
|
|
@@ -18183,6 +19763,16 @@ async function repoHeadSha(repoPath) {
|
|
|
18183
19763
|
const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd: repoPath });
|
|
18184
19764
|
return stdout.trim();
|
|
18185
19765
|
}
|
|
19766
|
+
async function buildScanSource(repoPath, repo, headSha) {
|
|
19767
|
+
const { committedAt } = await readCommitMeta(repoPath, headSha);
|
|
19768
|
+
const at = committedAt ? new Date(committedAt) : null;
|
|
19769
|
+
return {
|
|
19770
|
+
repo,
|
|
19771
|
+
before_sha: null,
|
|
19772
|
+
after_sha: headSha,
|
|
19773
|
+
...at && !Number.isNaN(at.getTime()) ? { committed_at: at.toISOString() } : {}
|
|
19774
|
+
};
|
|
19775
|
+
}
|
|
18186
19776
|
function defaultBranchName(headSha, date = todayDate()) {
|
|
18187
19777
|
return `${date}-${headSha.slice(0, 7)}-scan`;
|
|
18188
19778
|
}
|
|
@@ -18474,10 +20064,10 @@ async function runScan(opts, deps = {}) {
|
|
|
18474
20064
|
// derives the `commits.source_sha` column from the very metadata it stores,
|
|
18475
20065
|
// so the two can never disagree. A hand-written UPDATE that sets one and
|
|
18476
20066
|
// not the other is what broke a deep link once.
|
|
18477
|
-
...sourceRepo ? { source:
|
|
20067
|
+
...sourceRepo ? { source: await buildScanSource(opts.repoPath, sourceRepo, headSha) } : {}
|
|
18478
20068
|
});
|
|
18479
20069
|
try {
|
|
18480
|
-
const model = await
|
|
20070
|
+
const model = await resolveRunnableModel(supabase, { warn: log });
|
|
18481
20071
|
const interactive = opts.interactive !== false;
|
|
18482
20072
|
log(
|
|
18483
20073
|
interactive ? `starting claude (mcp config at ${configPath}, model ${model}) \u2014 this session is interactive; answer its questions as they come` : `spawning claude (mcp config at ${configPath}, model ${model}, timeout ${Math.round(SCAN_CLAUDE_TIMEOUT_MS / 6e4)} min)`
|
|
@@ -18503,9 +20093,9 @@ async function runScan(opts, deps = {}) {
|
|
|
18503
20093
|
// Incompatible with an interactive TTY session.
|
|
18504
20094
|
streamJson: !interactive
|
|
18505
20095
|
});
|
|
18506
|
-
const transcriptDir =
|
|
18507
|
-
const transcriptPath =
|
|
18508
|
-
await
|
|
20096
|
+
const transcriptDir = join21(opts.repoPath, ".archme", "scan");
|
|
20097
|
+
const transcriptPath = join21(transcriptDir, "transcript.log");
|
|
20098
|
+
await mkdir13(transcriptDir, { recursive: true });
|
|
18509
20099
|
const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
|
|
18510
20100
|
let finalResultText = "";
|
|
18511
20101
|
if (proc.stdout) {
|
|
@@ -18547,9 +20137,9 @@ async function runScan(opts, deps = {}) {
|
|
|
18547
20137
|
await new Promise((resolve6, reject) => {
|
|
18548
20138
|
transcriptStream.end((err2) => err2 ? reject(err2) : resolve6());
|
|
18549
20139
|
});
|
|
18550
|
-
const reportPath =
|
|
18551
|
-
const orphansTxtPath =
|
|
18552
|
-
const transcript = await
|
|
20140
|
+
const reportPath = join21(transcriptDir, "report.json");
|
|
20141
|
+
const orphansTxtPath = join21(transcriptDir, "orphans.txt");
|
|
20142
|
+
const transcript = await readFile14(transcriptPath, "utf8").catch(() => "");
|
|
18553
20143
|
signals2 = extractTranscriptSignals(transcript);
|
|
18554
20144
|
const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName);
|
|
18555
20145
|
const orphans = await classifyOrphans({
|
|
@@ -18621,6 +20211,7 @@ var init_run_scan = __esm({
|
|
|
18621
20211
|
init_bearer();
|
|
18622
20212
|
init_authedClient();
|
|
18623
20213
|
init_mcp_config();
|
|
20214
|
+
init_localGit();
|
|
18624
20215
|
init_spawn_claude();
|
|
18625
20216
|
init_model_pin();
|
|
18626
20217
|
init_web_url();
|
|
@@ -18807,7 +20398,7 @@ var init_dispatch3 = __esm({
|
|
|
18807
20398
|
|
|
18808
20399
|
// src/init/repoSize.ts
|
|
18809
20400
|
import { stat as stat9 } from "node:fs/promises";
|
|
18810
|
-
import { extname, join as
|
|
20401
|
+
import { extname, join as join22 } from "node:path";
|
|
18811
20402
|
async function measureRepo(repoPath) {
|
|
18812
20403
|
const files = await gitListFiles(repoPath);
|
|
18813
20404
|
if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
|
|
@@ -18816,7 +20407,7 @@ async function measureRepo(repoPath) {
|
|
|
18816
20407
|
for (const rel of files) {
|
|
18817
20408
|
if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
|
|
18818
20409
|
try {
|
|
18819
|
-
const s = await stat9(
|
|
20410
|
+
const s = await stat9(join22(repoPath, rel));
|
|
18820
20411
|
if (!s.isFile()) continue;
|
|
18821
20412
|
sourceFiles++;
|
|
18822
20413
|
sourceBytes += s.size;
|
|
@@ -19025,7 +20616,7 @@ async function chooseKeepCurrent(deps) {
|
|
|
19025
20616
|
if (deps.yes) {
|
|
19026
20617
|
return { detection: deps.alreadyWatched ? "github-app" : "local-watch", runner: "self" };
|
|
19027
20618
|
}
|
|
19028
|
-
const detection = deps.alreadyWatched ? "github-app" : await deps.choose(
|
|
20619
|
+
const detection = deps.alreadyWatched ? "github-app" : deps.keyed ? "local-watch" : await deps.choose(
|
|
19029
20620
|
"How should ArchMe detect new commits?",
|
|
19030
20621
|
DETECTION_OPTIONS
|
|
19031
20622
|
);
|
|
@@ -19043,16 +20634,19 @@ function formatKeepCurrentPlan(plan, ctx) {
|
|
|
19043
20634
|
command: "archme local-watch"
|
|
19044
20635
|
});
|
|
19045
20636
|
}
|
|
20637
|
+
const costNotes = [
|
|
20638
|
+
"Keep it running on a machine that stays on. Each update spends tokens",
|
|
20639
|
+
"from your Claude subscription, the same way this scan did. Your code is",
|
|
20640
|
+
"not sent to our servers \u2014 only the resulting architecture."
|
|
20641
|
+
];
|
|
19046
20642
|
if (plan.runner === "self") {
|
|
19047
|
-
|
|
19048
|
-
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19053
|
-
|
|
19054
|
-
]
|
|
19055
|
-
});
|
|
20643
|
+
if (!ctx.daemonStarted) {
|
|
20644
|
+
steps.push({
|
|
20645
|
+
title: "Start the daemon \u2014 nothing processes the queue until you do:",
|
|
20646
|
+
command: "archme daemon start",
|
|
20647
|
+
notes: costNotes
|
|
20648
|
+
});
|
|
20649
|
+
}
|
|
19056
20650
|
} else {
|
|
19057
20651
|
steps.push({
|
|
19058
20652
|
title: `Email ${CONTACT_EMAIL} and we'll set up the daemon for you.`,
|
|
@@ -19069,6 +20663,10 @@ function formatKeepCurrentPlan(plan, ctx) {
|
|
|
19069
20663
|
if (ctx.localWatchRegistered) {
|
|
19070
20664
|
lines.push("", ` \u2713 ${ctx.repoSlug ?? "This repo"} is registered \u2014 your machine is watching it.`);
|
|
19071
20665
|
}
|
|
20666
|
+
if (plan.runner === "self" && ctx.daemonStarted) {
|
|
20667
|
+
lines.push("", " \u2713 The daemon is running in the background (`archme daemon status`).", "");
|
|
20668
|
+
for (const note of costNotes) lines.push(` ${note}`);
|
|
20669
|
+
}
|
|
19072
20670
|
const numbered = steps.length > 1;
|
|
19073
20671
|
steps.forEach((step, i2) => {
|
|
19074
20672
|
const bullet = numbered ? ` ${i2 + 1}. ` : " ";
|
|
@@ -19096,7 +20694,7 @@ var init_keepCurrent = __esm({
|
|
|
19096
20694
|
value: "github-app"
|
|
19097
20695
|
},
|
|
19098
20696
|
{
|
|
19099
|
-
label: "Local polling \u2014 no GitHub App and nobody's approval needed. This machine watches the repo instead.
|
|
20697
|
+
label: "Local polling \u2014 no GitHub App and nobody's approval needed. This machine watches the repo instead. Pull requests are optional and, on a private repo, need `gh auth login`.",
|
|
19100
20698
|
value: "local-watch"
|
|
19101
20699
|
}
|
|
19102
20700
|
];
|
|
@@ -19215,66 +20813,6 @@ var init_githubAppSetup = __esm({
|
|
|
19215
20813
|
}
|
|
19216
20814
|
});
|
|
19217
20815
|
|
|
19218
|
-
// src/daemon/prompt.ts
|
|
19219
|
-
import { createInterface } from "node:readline/promises";
|
|
19220
|
-
function stdinIsInteractive() {
|
|
19221
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
19222
|
-
}
|
|
19223
|
-
async function withReadline(fn) {
|
|
19224
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
19225
|
-
try {
|
|
19226
|
-
return await fn(rl);
|
|
19227
|
-
} finally {
|
|
19228
|
-
rl.close();
|
|
19229
|
-
}
|
|
19230
|
-
}
|
|
19231
|
-
async function promptLine(question) {
|
|
19232
|
-
return await withReadline(async (rl) => {
|
|
19233
|
-
try {
|
|
19234
|
-
return await rl.question(`${question}: `);
|
|
19235
|
-
} catch {
|
|
19236
|
-
return "";
|
|
19237
|
-
}
|
|
19238
|
-
});
|
|
19239
|
-
}
|
|
19240
|
-
async function promptChoice(question, options) {
|
|
19241
|
-
return await withReadline(async (rl) => {
|
|
19242
|
-
console.log(`
|
|
19243
|
-
${question}
|
|
19244
|
-
`);
|
|
19245
|
-
options.forEach((o2, i2) => console.log(` ${i2 + 1}. ${o2.label}
|
|
19246
|
-
`));
|
|
19247
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
19248
|
-
let answer;
|
|
19249
|
-
try {
|
|
19250
|
-
answer = (await rl.question(`Choose 1-${options.length} [1]: `)).trim();
|
|
19251
|
-
} catch {
|
|
19252
|
-
return options[0].value;
|
|
19253
|
-
}
|
|
19254
|
-
if (answer === "") return options[0].value;
|
|
19255
|
-
const n2 = Number.parseInt(answer, 10);
|
|
19256
|
-
if (Number.isInteger(n2) && n2 >= 1 && n2 <= options.length) return options[n2 - 1].value;
|
|
19257
|
-
console.log(` "${answer}" isn't one of the options.`);
|
|
19258
|
-
}
|
|
19259
|
-
return options[0].value;
|
|
19260
|
-
});
|
|
19261
|
-
}
|
|
19262
|
-
async function promptYesNo(question) {
|
|
19263
|
-
return await withReadline(async (rl) => {
|
|
19264
|
-
try {
|
|
19265
|
-
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
19266
|
-
return answer === "" || answer === "y" || answer === "yes";
|
|
19267
|
-
} catch {
|
|
19268
|
-
return false;
|
|
19269
|
-
}
|
|
19270
|
-
});
|
|
19271
|
-
}
|
|
19272
|
-
var init_prompt = __esm({
|
|
19273
|
-
"src/daemon/prompt.ts"() {
|
|
19274
|
-
"use strict";
|
|
19275
|
-
}
|
|
19276
|
-
});
|
|
19277
|
-
|
|
19278
20816
|
// src/daemon/watchReadiness.ts
|
|
19279
20817
|
function evaluateWatchReadiness(input) {
|
|
19280
20818
|
const { marker, repo, archmeBranch } = input;
|
|
@@ -19412,6 +20950,69 @@ var init_localWatchResolve = __esm({
|
|
|
19412
20950
|
}
|
|
19413
20951
|
});
|
|
19414
20952
|
|
|
20953
|
+
// src/daemon/localPrSetup.ts
|
|
20954
|
+
async function askForPrReviews(deps = {}) {
|
|
20955
|
+
if (deps.flag !== void 0) return deps.flag;
|
|
20956
|
+
if (deps.current === "auto") return null;
|
|
20957
|
+
if (!(deps.interactive ?? stdinIsInteractive())) return null;
|
|
20958
|
+
const confirm = deps.confirm ?? ((q) => promptYesNo(q, { defaultYes: false }));
|
|
20959
|
+
return await confirm(PR_REVIEWS_QUESTION);
|
|
20960
|
+
}
|
|
20961
|
+
async function setPrReviewMode(supabase, watchedRepoId, enabled) {
|
|
20962
|
+
const { data, error } = await supabase.from("watched_repos").update({ pr_update_mode: enabled ? "auto" : "disabled", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", watchedRepoId).select("id");
|
|
20963
|
+
if (error || !Array.isArray(data) || data.length === 0) {
|
|
20964
|
+
throw new Error(`could not update PR reviews for this repo: ${error?.message ?? "no access to the row"}`);
|
|
20965
|
+
}
|
|
20966
|
+
}
|
|
20967
|
+
async function readPrReviewMode(supabase, watchedRepoId) {
|
|
20968
|
+
const { data, error } = await supabase.from("watched_repos").select("pr_update_mode").eq("id", watchedRepoId).maybeSingle();
|
|
20969
|
+
if (error || !data) return null;
|
|
20970
|
+
return data.pr_update_mode ?? "disabled";
|
|
20971
|
+
}
|
|
20972
|
+
function ghInstallHint(platform3 = process.platform) {
|
|
20973
|
+
if (platform3 === "darwin") return "brew install gh";
|
|
20974
|
+
if (platform3 === "win32") return "winget install --id GitHub.cli";
|
|
20975
|
+
return "see https://cli.github.com for your distribution";
|
|
20976
|
+
}
|
|
20977
|
+
async function ghLoginPrompt(deps = {}) {
|
|
20978
|
+
if (!(deps.interactive ?? stdinIsInteractive())) return "non-interactive";
|
|
20979
|
+
const log = deps.log ?? ((m) => console.log(m));
|
|
20980
|
+
const hasToken = deps.hasToken ?? (async () => (await resolveGithubTokens()).some((t) => t.token !== null));
|
|
20981
|
+
if (await hasToken()) return "has-token";
|
|
20982
|
+
const ghPath = deps.ghPath ?? (() => findOnPath("gh"));
|
|
20983
|
+
if (!ghPath()) {
|
|
20984
|
+
log(`
|
|
20985
|
+
${NEEDS_TOKEN}`);
|
|
20986
|
+
log(` To sign in, install the GitHub CLI (${ghInstallHint(deps.platform)}), then run: gh auth login`);
|
|
20987
|
+
return "gh-missing";
|
|
20988
|
+
}
|
|
20989
|
+
log(`
|
|
20990
|
+
${NEEDS_TOKEN}`);
|
|
20991
|
+
const confirm = deps.confirm ?? ((q) => promptYesNo(q));
|
|
20992
|
+
if (!await confirm("Sign in with `gh auth login` now?")) {
|
|
20993
|
+
log(" You can do it any time: gh auth login");
|
|
20994
|
+
return "declined";
|
|
20995
|
+
}
|
|
20996
|
+
const run = deps.runGhLogin ?? (async () => (await execa("gh", ["auth", "login", "--hostname", "github.com"], { stdio: "inherit", reject: false })).exitCode ?? 1);
|
|
20997
|
+
const code = await run();
|
|
20998
|
+
if (code === 0 && await hasToken()) return "logged-in";
|
|
20999
|
+
log(" `gh auth login` did not finish \u2014 a private repo's pull requests can't be reviewed until it does.");
|
|
21000
|
+
return "failed";
|
|
21001
|
+
}
|
|
21002
|
+
var PR_REVIEWS_QUESTION, PR_REVIEWS_ALREADY_ON, NEEDS_TOKEN;
|
|
21003
|
+
var init_localPrSetup = __esm({
|
|
21004
|
+
"src/daemon/localPrSetup.ts"() {
|
|
21005
|
+
"use strict";
|
|
21006
|
+
init_execa();
|
|
21007
|
+
init_preflight();
|
|
21008
|
+
init_localPrAdapters();
|
|
21009
|
+
init_prompt();
|
|
21010
|
+
PR_REVIEWS_QUESTION = "Also review pull requests? Each PR push is one run on your Claude subscription.";
|
|
21011
|
+
PR_REVIEWS_ALREADY_ON = "Pull requests are already reviewed for this repo (`--no-prs` turns that off).";
|
|
21012
|
+
NEEDS_TOKEN = "PR reviews read pull requests from GitHub's API. Without a GitHub token only a public repo's can be read \u2014 a private repo needs `gh auth login` (or a GitHub credential in git's credential helper).";
|
|
21013
|
+
}
|
|
21014
|
+
});
|
|
21015
|
+
|
|
19415
21016
|
// src/daemon/localWatch.ts
|
|
19416
21017
|
var localWatch_exports = {};
|
|
19417
21018
|
__export(localWatch_exports, {
|
|
@@ -19425,11 +21026,11 @@ __export(localWatch_exports, {
|
|
|
19425
21026
|
runLocalWatchResume: () => runLocalWatchResume,
|
|
19426
21027
|
runLocalWatchStop: () => runLocalWatchStop
|
|
19427
21028
|
});
|
|
19428
|
-
import { homedir as
|
|
19429
|
-
import { join as
|
|
21029
|
+
import { homedir as homedir7 } from "node:os";
|
|
21030
|
+
import { join as join23, resolve as resolve3 } from "node:path";
|
|
19430
21031
|
import { stat as stat10 } from "node:fs/promises";
|
|
19431
21032
|
function mirrorPathFor(cloneBaseDir, owner, name) {
|
|
19432
|
-
return
|
|
21033
|
+
return join23(cloneBaseDir, owner, name);
|
|
19433
21034
|
}
|
|
19434
21035
|
function resolveInitTarget(input) {
|
|
19435
21036
|
if (!input.originUrl) {
|
|
@@ -19546,6 +21147,19 @@ async function runLocalWatch(opts) {
|
|
|
19546
21147
|
}
|
|
19547
21148
|
log(`
|
|
19548
21149
|
\u2713 ${target.repo} is now watched locally.`);
|
|
21150
|
+
const interactive = !opts.yes && stdinIsInteractive();
|
|
21151
|
+
const current = await readPrReviewMode(supabase, registered.entry.watchedRepoId);
|
|
21152
|
+
const reviewPrs = await askForPrReviews({ flag: opts.prs, interactive, current });
|
|
21153
|
+
if (reviewPrs === null && current === "auto") log(` \u2713 ${PR_REVIEWS_ALREADY_ON}`);
|
|
21154
|
+
if (reviewPrs !== null) {
|
|
21155
|
+
try {
|
|
21156
|
+
await setPrReviewMode(supabase, registered.entry.watchedRepoId, reviewPrs);
|
|
21157
|
+
log(reviewPrs ? " \u2713 Pull requests will be reviewed too." : " Pull requests will not be reviewed.");
|
|
21158
|
+
if (reviewPrs) await ghLoginPrompt({ interactive, log });
|
|
21159
|
+
} catch (err2) {
|
|
21160
|
+
console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
21161
|
+
}
|
|
21162
|
+
}
|
|
19549
21163
|
log(`
|
|
19550
21164
|
Start the daemon if it isn't running: archme daemon start`);
|
|
19551
21165
|
log(` New commits on ${target.repoBranch} will appear as ArchMe commits within a minute or two.`);
|
|
@@ -19585,7 +21199,7 @@ async function registerLocalWatch(args) {
|
|
|
19585
21199
|
};
|
|
19586
21200
|
}
|
|
19587
21201
|
const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
|
|
19588
|
-
const mirrorExists = await stat10(
|
|
21202
|
+
const mirrorExists = await stat10(join23(mirrorPath, ".git")).then(() => true).catch(() => false);
|
|
19589
21203
|
if (mirrorExists) {
|
|
19590
21204
|
log(` mirror: ${mirrorPath} (reusing)`);
|
|
19591
21205
|
await setOriginUrl(mirrorPath, originUrl);
|
|
@@ -19604,6 +21218,9 @@ async function registerLocalWatch(args) {
|
|
|
19604
21218
|
repoOwner: target.repoOwner,
|
|
19605
21219
|
repoName: target.repoName
|
|
19606
21220
|
};
|
|
21221
|
+
const prior = (await loadLocalRepos()).find((r) => r.watchedRepoId === watchedRepoId);
|
|
21222
|
+
if (prior?.prPoll) entry.prPoll = prior.prPoll;
|
|
21223
|
+
if (prior?.prPausedAt) entry.prPausedAt = prior.prPausedAt;
|
|
19607
21224
|
await registerLocalRepo(entry);
|
|
19608
21225
|
return { ok: true, entry };
|
|
19609
21226
|
}
|
|
@@ -19674,7 +21291,8 @@ var init_localWatch = __esm({
|
|
|
19674
21291
|
init_localWatchList();
|
|
19675
21292
|
init_localWatchResolve();
|
|
19676
21293
|
init_prompt();
|
|
19677
|
-
|
|
21294
|
+
init_localPrSetup();
|
|
21295
|
+
DEFAULT_CLONE_BASE_DIR2 = join23(homedir7(), ".archme-job-runner", "clones");
|
|
19678
21296
|
}
|
|
19679
21297
|
});
|
|
19680
21298
|
|
|
@@ -19707,9 +21325,42 @@ var init_plan = __esm({
|
|
|
19707
21325
|
});
|
|
19708
21326
|
|
|
19709
21327
|
// src/init/run-init.ts
|
|
19710
|
-
import { mkdtemp as mkdtemp4, rm as
|
|
21328
|
+
import { mkdtemp as mkdtemp4, rm as rm9, writeFile as writeFile12 } from "node:fs/promises";
|
|
19711
21329
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
19712
|
-
import { join as
|
|
21330
|
+
import { join as join24 } from "node:path";
|
|
21331
|
+
async function defaultStartDaemon() {
|
|
21332
|
+
const { runStartFlow: runStartFlow2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
21333
|
+
const out = await runStartFlow2({ noLogin: true });
|
|
21334
|
+
return `${out.result.actions.join(" \u2192 ")} (logs: ${out.logPath})`;
|
|
21335
|
+
}
|
|
21336
|
+
async function defaultOpenBoard(opts) {
|
|
21337
|
+
const { runOpen: runOpen2, defaultOpenBrowser: defaultOpenBrowser2, defaultReadMcpJson: defaultReadMcpJson2, postBoardLinkWith: postBoardLinkWith2 } = await Promise.resolve().then(() => (init_open(), open_exports));
|
|
21338
|
+
const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
|
|
21339
|
+
const { deriveWebOrigin: deriveWebOrigin2 } = await Promise.resolve().then(() => (init_web_url(), web_url_exports));
|
|
21340
|
+
const cfg = resolveConfig();
|
|
21341
|
+
return await runOpen2(opts, {
|
|
21342
|
+
webOrigin: deriveWebOrigin2(cfg.archmeMcpBaseUrl),
|
|
21343
|
+
loadActive: async () => (await loadCredentials2(cfg.supabaseUrl)).credentials,
|
|
21344
|
+
// The session init just authenticated; refreshing again would only rotate it.
|
|
21345
|
+
getAccessToken: async () => {
|
|
21346
|
+
const token = getCurrentAccessToken();
|
|
21347
|
+
if (!token) throw new Error("no active session");
|
|
21348
|
+
return token;
|
|
21349
|
+
},
|
|
21350
|
+
postBoardLink: (token) => postBoardLinkWith2(cfg.supabaseUrl, token),
|
|
21351
|
+
readMcpJson: defaultReadMcpJson2,
|
|
21352
|
+
openBrowser: defaultOpenBrowser2,
|
|
21353
|
+
log: (m) => console.log(m),
|
|
21354
|
+
error: (m) => console.error(m)
|
|
21355
|
+
});
|
|
21356
|
+
}
|
|
21357
|
+
function boardPath(url) {
|
|
21358
|
+
try {
|
|
21359
|
+
return new URL(url).pathname;
|
|
21360
|
+
} catch {
|
|
21361
|
+
return "/";
|
|
21362
|
+
}
|
|
21363
|
+
}
|
|
19713
21364
|
function initScanBranch(defaultBranch) {
|
|
19714
21365
|
return normalizeScanBranchName(defaultBranch);
|
|
19715
21366
|
}
|
|
@@ -19721,13 +21372,13 @@ async function defaultFetchInstallScript(url) {
|
|
|
19721
21372
|
return await resp.text();
|
|
19722
21373
|
}
|
|
19723
21374
|
async function defaultRunInstallScript(script, cwd) {
|
|
19724
|
-
const dir = await mkdtemp4(
|
|
19725
|
-
const scriptPath =
|
|
21375
|
+
const dir = await mkdtemp4(join24(tmpdir4(), "archme-init-"));
|
|
21376
|
+
const scriptPath = join24(dir, "install-agent.sh");
|
|
19726
21377
|
try {
|
|
19727
21378
|
await writeFile12(scriptPath, script, { mode: 448 });
|
|
19728
21379
|
await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
|
|
19729
21380
|
} finally {
|
|
19730
|
-
await
|
|
21381
|
+
await rm9(dir, { recursive: true, force: true });
|
|
19731
21382
|
}
|
|
19732
21383
|
}
|
|
19733
21384
|
async function runInit(opts, deps = {}) {
|
|
@@ -19744,6 +21395,8 @@ async function runInit(opts, deps = {}) {
|
|
|
19744
21395
|
const gitContext = deps.gitContext ?? resolveGitContext;
|
|
19745
21396
|
const measure = deps.measure ?? measureRepo;
|
|
19746
21397
|
const watchRepo = deps.watchRepo ?? watchRepoForProject;
|
|
21398
|
+
const interactive = !opts.yes && (deps.interactive ?? stdinIsInteractive());
|
|
21399
|
+
const openBoard = deps.openBoard ?? defaultOpenBoard;
|
|
19747
21400
|
const cfg = resolveConfig();
|
|
19748
21401
|
const git4 = await gitContext(opts.repoPath);
|
|
19749
21402
|
const repoPath = git4.root;
|
|
@@ -19751,9 +21404,10 @@ async function runInit(opts, deps = {}) {
|
|
|
19751
21404
|
const signedIn = credentials.length > 0;
|
|
19752
21405
|
if (!signedIn) {
|
|
19753
21406
|
log("\u2192 signing you in\u2026");
|
|
19754
|
-
await login();
|
|
21407
|
+
await login({ ...opts.login, ...opts.yes ? { interactive: false } : {} });
|
|
19755
21408
|
} else {
|
|
19756
21409
|
log("\u2713 already signed in");
|
|
21410
|
+
if (opts.login?.method) log(` (ignoring --${opts.login.method}: \`archme login\` switches accounts)`);
|
|
19757
21411
|
}
|
|
19758
21412
|
const authed2 = await authenticate({
|
|
19759
21413
|
supabaseUrl: cfg.supabaseUrl,
|
|
@@ -19761,7 +21415,8 @@ async function runInit(opts, deps = {}) {
|
|
|
19761
21415
|
clientId: cfg.archmeDaemonClientId,
|
|
19762
21416
|
preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
|
|
19763
21417
|
});
|
|
19764
|
-
log(` signed in as ${authed2.identity}
|
|
21418
|
+
log(` signed in as ${authed2.identity} \u2014 not you? \`archme login\``);
|
|
21419
|
+
const keyed = authed2.accountKind === "key";
|
|
19765
21420
|
let fullSlug = opts.project;
|
|
19766
21421
|
if (!fullSlug) {
|
|
19767
21422
|
const orgId = authed2.orgId;
|
|
@@ -19832,6 +21487,7 @@ async function runInit(opts, deps = {}) {
|
|
|
19832
21487
|
}
|
|
19833
21488
|
log(`
|
|
19834
21489
|
Your board: ${webUrl}`);
|
|
21490
|
+
if (keyed) await openBoard({ next: boardPath(webUrl), hideLink: !interactive });
|
|
19835
21491
|
return { exitCode: 0, project: fullSlug, branch: scanBranch, webUrl, ran };
|
|
19836
21492
|
}
|
|
19837
21493
|
const measurement = await measure(repoPath);
|
|
@@ -19860,10 +21516,12 @@ Your board: ${webUrl}`);
|
|
|
19860
21516
|
const plan = await chooseKeepCurrent({
|
|
19861
21517
|
alreadyWatched: watched.watched,
|
|
19862
21518
|
yes: Boolean(opts.yes),
|
|
21519
|
+
keyed,
|
|
19863
21520
|
repoSlug: git4.repoSlug,
|
|
19864
21521
|
choose: deps.choose ?? promptChoice,
|
|
19865
21522
|
log
|
|
19866
21523
|
});
|
|
21524
|
+
const registered = { watchedRepoId: null };
|
|
19867
21525
|
const outcome = await applyKeepCurrentPlan(
|
|
19868
21526
|
plan,
|
|
19869
21527
|
{
|
|
@@ -19911,12 +21569,38 @@ Your board: ${webUrl}`);
|
|
|
19911
21569
|
target: target.target,
|
|
19912
21570
|
log
|
|
19913
21571
|
});
|
|
19914
|
-
|
|
21572
|
+
if (!res.ok) return { ok: false, reason: res.reason };
|
|
21573
|
+
registered.watchedRepoId = res.entry.watchedRepoId;
|
|
21574
|
+
return { ok: true };
|
|
19915
21575
|
},
|
|
19916
21576
|
log
|
|
19917
21577
|
},
|
|
19918
21578
|
watched.watched
|
|
19919
21579
|
);
|
|
21580
|
+
if (outcome.localWatchRegistered && registered.watchedRepoId && deps.offerPrReviews) {
|
|
21581
|
+
await deps.offerPrReviews({
|
|
21582
|
+
supabase: authed2.supabase,
|
|
21583
|
+
orgId: authed2.orgId,
|
|
21584
|
+
projectId: project.projectId,
|
|
21585
|
+
watchedRepoId: registered.watchedRepoId,
|
|
21586
|
+
repoSlug: git4.repoSlug,
|
|
21587
|
+
repoPath: git4.root,
|
|
21588
|
+
interactive,
|
|
21589
|
+
log
|
|
21590
|
+
});
|
|
21591
|
+
}
|
|
21592
|
+
let daemonStarted = false;
|
|
21593
|
+
if (plan.runner === "self" && interactive) {
|
|
21594
|
+
if (await (deps.confirm ?? promptYesNo)(DAEMON_QUESTION)) {
|
|
21595
|
+
try {
|
|
21596
|
+
log("\u2192 starting the daemon\u2026");
|
|
21597
|
+
log(` \u2713 ${await (deps.startDaemon ?? defaultStartDaemon)()}`);
|
|
21598
|
+
daemonStarted = true;
|
|
21599
|
+
} catch (err2) {
|
|
21600
|
+
log(` Couldn't start the daemon: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
21601
|
+
}
|
|
21602
|
+
}
|
|
21603
|
+
}
|
|
19920
21604
|
if (!outcome.wired && outcome.reason) {
|
|
19921
21605
|
log("");
|
|
19922
21606
|
log("Not yet set up for automatic updates:");
|
|
@@ -19927,9 +21611,11 @@ Your board: ${webUrl}`);
|
|
|
19927
21611
|
formatKeepCurrentPlan(plan, {
|
|
19928
21612
|
repoSlug: git4.repoSlug,
|
|
19929
21613
|
boardUrl: finalUrl,
|
|
19930
|
-
localWatchRegistered: outcome.localWatchRegistered
|
|
21614
|
+
localWatchRegistered: outcome.localWatchRegistered,
|
|
21615
|
+
daemonStarted
|
|
19931
21616
|
})
|
|
19932
21617
|
);
|
|
21618
|
+
if (keyed) await openBoard({ next: boardPath(finalUrl), hideLink: !interactive });
|
|
19933
21619
|
}
|
|
19934
21620
|
return {
|
|
19935
21621
|
exitCode: result.exitCode,
|
|
@@ -19939,6 +21625,7 @@ Your board: ${webUrl}`);
|
|
|
19939
21625
|
ran
|
|
19940
21626
|
};
|
|
19941
21627
|
}
|
|
21628
|
+
var DAEMON_QUESTION;
|
|
19942
21629
|
var init_run_init = __esm({
|
|
19943
21630
|
"src/init/run-init.ts"() {
|
|
19944
21631
|
"use strict";
|
|
@@ -19960,6 +21647,36 @@ var init_run_init = __esm({
|
|
|
19960
21647
|
init_prompt();
|
|
19961
21648
|
init_localWatch();
|
|
19962
21649
|
init_plan();
|
|
21650
|
+
DAEMON_QUESTION = "Keep the board updating in the background?";
|
|
21651
|
+
}
|
|
21652
|
+
});
|
|
21653
|
+
|
|
21654
|
+
// src/init/prReviews.ts
|
|
21655
|
+
async function offerPrReviewsForInit(ctx, flag, deps = {}) {
|
|
21656
|
+
const readMode = deps.readMode ?? readPrReviewMode;
|
|
21657
|
+
const ask = deps.ask ?? askForPrReviews;
|
|
21658
|
+
const setMode = deps.setMode ?? setPrReviewMode;
|
|
21659
|
+
const ghLogin = deps.ghLogin ?? ghLoginPrompt;
|
|
21660
|
+
const current = await readMode(ctx.supabase, ctx.watchedRepoId);
|
|
21661
|
+
const want = await ask({ flag, interactive: ctx.interactive, current });
|
|
21662
|
+
if (want === null) {
|
|
21663
|
+
if (current === "auto") ctx.log(`\u2713 ${PR_REVIEWS_ALREADY_ON}`);
|
|
21664
|
+
return;
|
|
21665
|
+
}
|
|
21666
|
+
try {
|
|
21667
|
+
await setMode(ctx.supabase, ctx.watchedRepoId, want);
|
|
21668
|
+
} catch (err2) {
|
|
21669
|
+
ctx.log(`! ${err2 instanceof Error ? err2.message : String(err2)} \u2014 run \`archme local-watch --prs\` to try again.`);
|
|
21670
|
+
return;
|
|
21671
|
+
}
|
|
21672
|
+
if (!want) return;
|
|
21673
|
+
ctx.log("\u2713 Pull requests will be reviewed too.");
|
|
21674
|
+
await ghLogin({ interactive: ctx.interactive, log: ctx.log });
|
|
21675
|
+
}
|
|
21676
|
+
var init_prReviews = __esm({
|
|
21677
|
+
"src/init/prReviews.ts"() {
|
|
21678
|
+
"use strict";
|
|
21679
|
+
init_localPrSetup();
|
|
19963
21680
|
}
|
|
19964
21681
|
});
|
|
19965
21682
|
|
|
@@ -19979,6 +21696,25 @@ function parseInitArgs(argv) {
|
|
|
19979
21696
|
args.help = true;
|
|
19980
21697
|
return { ok: true, args };
|
|
19981
21698
|
}
|
|
21699
|
+
const loginFlag = arg.startsWith("--key=") ? "--key" : arg;
|
|
21700
|
+
const loginMethod = LOGIN_FLAGS[loginFlag];
|
|
21701
|
+
if (loginMethod) {
|
|
21702
|
+
if (args.login && args.login.method !== loginMethod) {
|
|
21703
|
+
return { ok: false, error: `${loginFlag} cannot be combined with another sign-in option` };
|
|
21704
|
+
}
|
|
21705
|
+
args.login = { method: loginMethod };
|
|
21706
|
+
if (arg.startsWith("--key=")) args.login.key = arg.slice("--key=".length);
|
|
21707
|
+
else if (loginMethod === "key" && argv[i2 + 1]?.startsWith("amk_")) args.login.key = argv[++i2];
|
|
21708
|
+
continue;
|
|
21709
|
+
}
|
|
21710
|
+
if (arg === "--prs" || arg === "--no-prs") {
|
|
21711
|
+
const value = arg === "--prs";
|
|
21712
|
+
if (args.prs !== void 0 && args.prs !== value) {
|
|
21713
|
+
return { ok: false, error: "--prs cannot be combined with --no-prs" };
|
|
21714
|
+
}
|
|
21715
|
+
args.prs = value;
|
|
21716
|
+
continue;
|
|
21717
|
+
}
|
|
19982
21718
|
if (BOOLEAN_FLAGS.has(arg)) {
|
|
19983
21719
|
if (arg === "--force") args.force = true;
|
|
19984
21720
|
else if (arg === "--yes") args.yes = true;
|
|
@@ -20025,7 +21761,7 @@ function parseInitArgs(argv) {
|
|
|
20025
21761
|
args.repoPath = positionals[0];
|
|
20026
21762
|
return { ok: true, args };
|
|
20027
21763
|
}
|
|
20028
|
-
var BOOLEAN_FLAGS, VALUE_FLAGS, HELP_FLAGS, PROJECT_SLUG_RE2;
|
|
21764
|
+
var BOOLEAN_FLAGS, VALUE_FLAGS, HELP_FLAGS, LOGIN_FLAGS, PROJECT_SLUG_RE2;
|
|
20029
21765
|
var init_args = __esm({
|
|
20030
21766
|
"src/init/args.ts"() {
|
|
20031
21767
|
"use strict";
|
|
@@ -20037,6 +21773,11 @@ var init_args = __esm({
|
|
|
20037
21773
|
]);
|
|
20038
21774
|
VALUE_FLAGS = /* @__PURE__ */ new Set(["--project", "--app-origin"]);
|
|
20039
21775
|
HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h", "help"]);
|
|
21776
|
+
LOGIN_FLAGS = {
|
|
21777
|
+
"--no-account": "no-account",
|
|
21778
|
+
"--github": "github",
|
|
21779
|
+
"--key": "key"
|
|
21780
|
+
};
|
|
20040
21781
|
PROJECT_SLUG_RE2 = /^[^/\s]+\/[^/\s]+$/;
|
|
20041
21782
|
}
|
|
20042
21783
|
});
|
|
@@ -20054,9 +21795,10 @@ import { resolve as resolve4 } from "node:path";
|
|
|
20054
21795
|
function printInitHelp() {
|
|
20055
21796
|
console.log(`archme init \u2014 guided first-time setup for this repo
|
|
20056
21797
|
|
|
20057
|
-
Signs you in, creates a project named after
|
|
20058
|
-
(.mcp.json, CLAUDE.md block, architecture
|
|
20059
|
-
interactive Claude Code session that confirms the
|
|
21798
|
+
Signs you in (or continues without an account), creates a project named after
|
|
21799
|
+
the repo, installs agent config (.mcp.json, CLAUDE.md block, architecture
|
|
21800
|
+
skill), then hands over to an interactive Claude Code session that confirms the
|
|
21801
|
+
cost and runs the scan.
|
|
20060
21802
|
|
|
20061
21803
|
Usage:
|
|
20062
21804
|
archme init [repo-path] [options]
|
|
@@ -20070,6 +21812,15 @@ Options:
|
|
|
20070
21812
|
--yes Run unattended (headless scan, no questions)
|
|
20071
21813
|
--no-interactive Same as --yes; for automation
|
|
20072
21814
|
--app-origin <url> Origin serving /api/install-agent (local development)
|
|
21815
|
+
--no-account If not signed in: continue without an account (a key
|
|
21816
|
+
saved on this machine signs you in; archme key show
|
|
21817
|
+
prints it)
|
|
21818
|
+
--github If not signed in: sign in with GitHub
|
|
21819
|
+
--key If not signed in: use an existing key (ARCHME_KEY, else
|
|
21820
|
+
the saved one). Implied when ARCHME_KEY is set
|
|
21821
|
+
--prs / --no-prs With local polling: also review pull requests (each PR
|
|
21822
|
+
push is one run on your Claude subscription; a private
|
|
21823
|
+
repo needs gh auth login)
|
|
20073
21824
|
-h, --help Show this help
|
|
20074
21825
|
|
|
20075
21826
|
Requires Claude Code to be installed and authenticated \u2014 the scan runs on your
|
|
@@ -20093,7 +21844,10 @@ async function dispatchInit(argv) {
|
|
|
20093
21844
|
appOrigin: parsed.args.appOrigin,
|
|
20094
21845
|
force: parsed.args.force,
|
|
20095
21846
|
skipScan: parsed.args.skipScan,
|
|
20096
|
-
yes: parsed.args.yes || parsed.args.noInteractive
|
|
21847
|
+
yes: parsed.args.yes || parsed.args.noInteractive,
|
|
21848
|
+
login: parsed.args.login
|
|
21849
|
+
}, {
|
|
21850
|
+
offerPrReviews: (ctx) => offerPrReviewsForInit(ctx, parsed.args.prs)
|
|
20097
21851
|
});
|
|
20098
21852
|
return result.exitCode;
|
|
20099
21853
|
} catch (err2) {
|
|
@@ -20106,6 +21860,7 @@ var init_dispatch4 = __esm({
|
|
|
20106
21860
|
"src/init/dispatch.ts"() {
|
|
20107
21861
|
"use strict";
|
|
20108
21862
|
init_run_init();
|
|
21863
|
+
init_prReviews();
|
|
20109
21864
|
init_args();
|
|
20110
21865
|
EX_OK2 = 0;
|
|
20111
21866
|
EX_INFRA2 = 3;
|
|
@@ -20329,7 +22084,7 @@ __export(run_uninstall_exports, {
|
|
|
20329
22084
|
runUninstall: () => runUninstall
|
|
20330
22085
|
});
|
|
20331
22086
|
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
20332
|
-
import { join as
|
|
22087
|
+
import { join as join25 } from "node:path";
|
|
20333
22088
|
function readIfExists(path7) {
|
|
20334
22089
|
try {
|
|
20335
22090
|
return existsSync3(path7) ? readFileSync5(path7, "utf8") : null;
|
|
@@ -20355,7 +22110,7 @@ async function runUninstall(opts) {
|
|
|
20355
22110
|
const backend = opts.backend ?? await defaultBackend();
|
|
20356
22111
|
const repoPath = await backend.gitRoot(opts.repoPath).catch(() => null) ?? opts.repoPath;
|
|
20357
22112
|
if (repoPath !== opts.repoPath) log(`Using the repo root: ${repoPath}`);
|
|
20358
|
-
const abs = (rel) =>
|
|
22113
|
+
const abs = (rel) => join25(repoPath, rel);
|
|
20359
22114
|
const actions = [];
|
|
20360
22115
|
const skipped = [];
|
|
20361
22116
|
const mcpRaw = readIfExists(abs(".mcp.json"));
|
|
@@ -20698,6 +22453,7 @@ function parseLocalWatchArgs(argv) {
|
|
|
20698
22453
|
let list = false;
|
|
20699
22454
|
let stop;
|
|
20700
22455
|
let resume;
|
|
22456
|
+
let prs;
|
|
20701
22457
|
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
20702
22458
|
const arg = argv[i2];
|
|
20703
22459
|
const next = argv[i2 + 1];
|
|
@@ -20716,6 +22472,10 @@ function parseLocalWatchArgs(argv) {
|
|
|
20716
22472
|
i2++;
|
|
20717
22473
|
} else if (arg === "--yes" || arg === "--no-interactive") {
|
|
20718
22474
|
yes = true;
|
|
22475
|
+
} else if (arg === "--prs" || arg === "--no-prs") {
|
|
22476
|
+
const want = arg === "--prs";
|
|
22477
|
+
if (prs !== void 0 && prs !== want) return err("--prs and --no-prs contradict each other");
|
|
22478
|
+
prs = want;
|
|
20719
22479
|
} else if (arg === "--list") {
|
|
20720
22480
|
list = true;
|
|
20721
22481
|
} else if (arg === "--stop" || arg === "--resume") {
|
|
@@ -20741,7 +22501,7 @@ function parseLocalWatchArgs(argv) {
|
|
|
20741
22501
|
if (list) return { kind: "list" };
|
|
20742
22502
|
if (stop) return { kind: "stop", repo: stop };
|
|
20743
22503
|
if (resume) return { kind: "resume", repo: resume };
|
|
20744
|
-
return { kind: "register", repoPath, project, branch, yes };
|
|
22504
|
+
return { kind: "register", repoPath, project, branch, yes, prs };
|
|
20745
22505
|
}
|
|
20746
22506
|
function printLocalWatchHelp() {
|
|
20747
22507
|
console.log(`archme local-watch \u2014 keep ArchMe current without a GitHub App
|
|
@@ -20764,6 +22524,8 @@ Options:
|
|
|
20764
22524
|
--project <org/slug> Project to write to (default: derived from the repo name)
|
|
20765
22525
|
--branch <name> Branch to track (default: the repo's default branch)
|
|
20766
22526
|
--yes Don't ask for confirmation (needs an explicit repo-path)
|
|
22527
|
+
--prs / --no-prs Also review pull requests (each PR push is one Claude run;
|
|
22528
|
+
a private repo needs gh auth login)
|
|
20767
22529
|
|
|
20768
22530
|
Watching queues work; the daemon does it. After registering: archme daemon start`);
|
|
20769
22531
|
}
|
|
@@ -20803,8 +22565,8 @@ var init_localWatchDispatch = __esm({
|
|
|
20803
22565
|
});
|
|
20804
22566
|
|
|
20805
22567
|
// src/doctor/collect.ts
|
|
20806
|
-
import { readFile as
|
|
20807
|
-
import { join as
|
|
22568
|
+
import { readFile as readFile15 } from "node:fs/promises";
|
|
22569
|
+
import { join as join26 } from "node:path";
|
|
20808
22570
|
function binaryPresent(name) {
|
|
20809
22571
|
try {
|
|
20810
22572
|
requireBinary(name);
|
|
@@ -20820,7 +22582,7 @@ async function git3(cwd, args) {
|
|
|
20820
22582
|
async function readMcpPin(repoRoot) {
|
|
20821
22583
|
const empty = { installed: false, pin: { project: null, branch: null } };
|
|
20822
22584
|
try {
|
|
20823
|
-
const raw = await
|
|
22585
|
+
const raw = await readFile15(join26(repoRoot, ".mcp.json"), "utf8");
|
|
20824
22586
|
const parsed = JSON.parse(raw);
|
|
20825
22587
|
const servers = parsed.mcpServers ?? {};
|
|
20826
22588
|
const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
|
|
@@ -21169,7 +22931,8 @@ var init_prompt2 = __esm({
|
|
|
21169
22931
|
whole team, nothing to keep running. Needs someone to grant repo read
|
|
21170
22932
|
access.
|
|
21171
22933
|
- **Local polling** (\`archme local-watch\`): this machine fetches the repo on
|
|
21172
|
-
a timer. No GitHub App, no approvals
|
|
22934
|
+
a timer. No GitHub App, no approvals. Pull requests only with \`--prs\`,
|
|
22935
|
+
and a private repo needs \`gh auth login\`.
|
|
21173
22936
|
|
|
21174
22937
|
3. **Execution** \u2014 something has to PROCESS each detected commit. This is the
|
|
21175
22938
|
step people miss. Detection only queues work; the queue does nothing on its
|
|
@@ -21193,9 +22956,9 @@ __export(dispatch_exports6, {
|
|
|
21193
22956
|
parseDoctorArgs: () => parseDoctorArgs,
|
|
21194
22957
|
printDoctorHelp: () => printDoctorHelp
|
|
21195
22958
|
});
|
|
21196
|
-
import { mkdtemp as mkdtemp5, rm as
|
|
22959
|
+
import { mkdtemp as mkdtemp5, rm as rm10, writeFile as writeFile13 } from "node:fs/promises";
|
|
21197
22960
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
21198
|
-
import { join as
|
|
22961
|
+
import { join as join27 } from "node:path";
|
|
21199
22962
|
function parseDoctorArgs(argv) {
|
|
21200
22963
|
const args = { plain: false, help: false };
|
|
21201
22964
|
for (const a2 of argv) {
|
|
@@ -21256,8 +23019,8 @@ async function dispatchDoctor(argv) {
|
|
|
21256
23019
|
);
|
|
21257
23020
|
return EX_OK6;
|
|
21258
23021
|
}
|
|
21259
|
-
const dir = await mkdtemp5(
|
|
21260
|
-
const mcpConfigPath =
|
|
23022
|
+
const dir = await mkdtemp5(join27(tmpdir5(), "archme-doctor-"));
|
|
23023
|
+
const mcpConfigPath = join27(dir, "mcp.json");
|
|
21261
23024
|
await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
|
|
21262
23025
|
try {
|
|
21263
23026
|
const result = await spawnClaude({
|
|
@@ -21272,7 +23035,7 @@ async function dispatchDoctor(argv) {
|
|
|
21272
23035
|
});
|
|
21273
23036
|
return result.exitCode === 0 ? EX_OK6 : EX_INFRA5;
|
|
21274
23037
|
} finally {
|
|
21275
|
-
await
|
|
23038
|
+
await rm10(dir, { recursive: true, force: true });
|
|
21276
23039
|
}
|
|
21277
23040
|
}
|
|
21278
23041
|
var EX_OK6, EX_INFRA5, EX_USAGE6;
|
|
@@ -21314,6 +23077,7 @@ async function runLogout(args) {
|
|
|
21314
23077
|
if (!target || args.includes("--all")) {
|
|
21315
23078
|
await clearCredentials(url);
|
|
21316
23079
|
console.log("Logged out: removed all stored identities.");
|
|
23080
|
+
await mentionKeptKeys(url);
|
|
21317
23081
|
return 0;
|
|
21318
23082
|
}
|
|
21319
23083
|
const all = (await loadAllCredentials(url)).credentials;
|
|
@@ -21324,24 +23088,36 @@ async function runLogout(args) {
|
|
|
21324
23088
|
}
|
|
21325
23089
|
for (const c3 of matches) await clearCredential(url, c3.device_id);
|
|
21326
23090
|
console.log(`Logged out: removed ${matches.length} device(s) for "${target}".`);
|
|
23091
|
+
await mentionKeptKeys(url);
|
|
21327
23092
|
return 0;
|
|
21328
23093
|
}
|
|
23094
|
+
async function mentionKeptKeys(url) {
|
|
23095
|
+
const keys = await listKeys(url).catch(() => []);
|
|
23096
|
+
if (keys.length === 0) return;
|
|
23097
|
+
console.log(
|
|
23098
|
+
`Saved keys are kept (${keys.map((k) => k.handle).join(", ")}): \`archme login --key\` signs back in, \`archme key list\` shows them.`
|
|
23099
|
+
);
|
|
23100
|
+
}
|
|
21329
23101
|
var init_logout = __esm({
|
|
21330
23102
|
"src/auth/logout.ts"() {
|
|
21331
23103
|
"use strict";
|
|
21332
23104
|
init_defaults();
|
|
21333
23105
|
init_credentials();
|
|
23106
|
+
init_keyRing();
|
|
21334
23107
|
}
|
|
21335
23108
|
});
|
|
21336
23109
|
|
|
21337
23110
|
// src/cli.ts
|
|
21338
23111
|
init_enroll();
|
|
23112
|
+
init_keyRing();
|
|
21339
23113
|
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
21340
23114
|
"daemon",
|
|
21341
23115
|
"analyze",
|
|
21342
23116
|
"login",
|
|
21343
23117
|
"logout",
|
|
21344
23118
|
"keys",
|
|
23119
|
+
"key",
|
|
23120
|
+
"open",
|
|
21345
23121
|
"scan",
|
|
21346
23122
|
"init",
|
|
21347
23123
|
"uninstall",
|
|
@@ -21428,9 +23204,32 @@ async function main(argv) {
|
|
|
21428
23204
|
console.log(v);
|
|
21429
23205
|
return 0;
|
|
21430
23206
|
}
|
|
21431
|
-
case "login":
|
|
21432
|
-
|
|
23207
|
+
case "login": {
|
|
23208
|
+
const parsed = parseLoginArgs(r.args);
|
|
23209
|
+
if (!parsed.ok) {
|
|
23210
|
+
console.error(`archme login: ${parsed.error}
|
|
23211
|
+
Usage: archme login [--no-account | --github | --key] (--key reads ARCHME_KEY, a saved key, or a prompt; ARCHME_KEY alone implies --key)`);
|
|
23212
|
+
return 2;
|
|
23213
|
+
}
|
|
23214
|
+
try {
|
|
23215
|
+
await runLogin(parsed.opts);
|
|
23216
|
+
} catch (err2) {
|
|
23217
|
+
if (err2 instanceof LoginError || err2 instanceof KeyRingUnreadableError) {
|
|
23218
|
+
console.error(`archme login: ${err2.message}`);
|
|
23219
|
+
return 1;
|
|
23220
|
+
}
|
|
23221
|
+
throw err2;
|
|
23222
|
+
}
|
|
21433
23223
|
return 0;
|
|
23224
|
+
}
|
|
23225
|
+
case "open": {
|
|
23226
|
+
const { dispatchOpen: dispatchOpen2 } = await Promise.resolve().then(() => (init_open(), open_exports));
|
|
23227
|
+
return await dispatchOpen2(r.args);
|
|
23228
|
+
}
|
|
23229
|
+
case "key": {
|
|
23230
|
+
const { dispatchKey: dispatchKey2 } = await Promise.resolve().then(() => (init_key(), key_exports));
|
|
23231
|
+
return await dispatchKey2(r.args);
|
|
23232
|
+
}
|
|
21434
23233
|
case "analyze": {
|
|
21435
23234
|
const { dispatchAnalyze: dispatchAnalyze2 } = await Promise.resolve().then(() => (init_dispatch(), dispatch_exports));
|
|
21436
23235
|
return await dispatchAnalyze2(r.args);
|
|
@@ -21482,7 +23281,13 @@ Other commands:
|
|
|
21482
23281
|
archme uninstall [repo-path] Stop ArchitectMe for a repo: config,
|
|
21483
23282
|
watching, and the daemon if it's now idle
|
|
21484
23283
|
archme doctor Check this machine's setup and explain it
|
|
21485
|
-
archme login
|
|
23284
|
+
archme login [--no-account|--github|--key]
|
|
23285
|
+
Sign in (or continue without an account);
|
|
23286
|
+
--key reads ARCHME_KEY, a saved key, or a
|
|
23287
|
+
prompt (ARCHME_KEY alone implies --key)
|
|
23288
|
+
archme logout Sign out here (saved keys are kept)
|
|
23289
|
+
archme open [--print] Open your board in the browser, signed in
|
|
23290
|
+
archme key [list|show|forget] Account keys saved on this machine
|
|
21486
23291
|
archme keys list|create|revoke Manage MCP API keys
|
|
21487
23292
|
archme --version Print the installed version
|
|
21488
23293
|
|