@archme/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +2318 -597
  2. 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 = (open2, close) => {
723
+ format = (open3, close) => {
724
724
  if (!hasColors) {
725
725
  return (input) => input;
726
726
  }
727
- const openCode = `\x1B[${open2}m`;
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, short4, valueString) => {
4061
- if ((result.length === 0 || !options.colonNotation) && isZero(value) && !(options.colonNotation && short4 === "m")) {
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) : short4;
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
- if (incoming.bindings?.length) return incoming;
8365
- if (prior?.bindings?.length) return { ...incoming, bindings: prior.bindings };
8366
- return incoming;
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 homedir2 } from "node:os";
8493
- import { dirname as dirname2, join as join2 } from "node:path";
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(dirname2(LOCAL_CONFIG_PATH), { recursive: true });
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 = join2(homedir2(), ".archme", "local-config.json");
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 runLogin() {
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,57 +9087,312 @@ async function runLogin() {
8615
9087
  const data2 = await response.json();
8616
9088
  return { status: response.status, data: data2 };
8617
9089
  }
8618
- const deviceName = process.env.ARCHME_DAEMON_DEVICE_NAME || hostname();
9090
+ const deviceName = defaultDeviceName();
8619
9091
  const { status, data } = await postJson("/start", {
8620
9092
  device_name: deviceName
8621
9093
  });
8622
9094
  if (status !== 200) {
8623
9095
  throw new Error(`Failed to start enrollment: ${JSON.stringify(data)}`);
8624
9096
  }
8625
- const bareUrl = data.verification_uri.split("?")[0] ?? data.verification_uri;
8626
- console.log("ArchitectMe daemon enrollment");
8627
- console.log("");
8628
- console.log(` Device: ${deviceName}`);
8629
- console.log(` Code: ${data.user_code.slice(0, 4)}-${data.user_code.slice(4)}`);
8630
- console.log(` URL: ${bareUrl}`);
8631
- console.log("");
8632
- console.log("Paste the code above into the pairing page.");
8633
- if (platform2() === "darwin" && !process.argv.includes("--no-open")) {
8634
- await execa("open", [bareUrl], { reject: false });
9097
+ const bareUrl = data.verification_uri.split("?")[0] ?? data.verification_uri;
9098
+ console.log("ArchitectMe daemon enrollment");
9099
+ console.log("");
9100
+ console.log(` Device: ${deviceName}`);
9101
+ console.log(` Code: ${data.user_code.slice(0, 4)}-${data.user_code.slice(4)}`);
9102
+ console.log(` URL: ${bareUrl}`);
9103
+ console.log("");
9104
+ console.log("Paste the code above into the pairing page.");
9105
+ if (platform2() === "darwin" && !process.argv.includes("--no-open")) {
9106
+ await execa("open", [bareUrl], { reject: false });
9107
+ }
9108
+ const expiresAt = Date.now() + data.expires_in * 1e3;
9109
+ let intervalMs = Math.max(data.interval, 1) * 1e3;
9110
+ while (Date.now() < expiresAt) {
9111
+ await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
9112
+ const poll = await postJson("/poll", {
9113
+ device_code: data.device_code
9114
+ });
9115
+ if (poll.status === 200 && "refresh_token" in poll.data) {
9116
+ const { bindings, source } = await reconcileEnrollment(SUPABASE_URL2, poll.data);
9117
+ console.log(`Enrollment complete. Credentials stored in ${describeCredentialSource(source)}.`);
9118
+ console.log(`Daemon will serve 1 account across ${bindings.length} org(s).`);
9119
+ return;
9120
+ }
9121
+ if (poll.status === 200 && "status" in poll.data && poll.data.status === "pending") {
9122
+ continue;
9123
+ }
9124
+ if ("error" in poll.data && poll.data.error === "slow_down") {
9125
+ intervalMs = Math.max(poll.data.interval ?? data.interval, data.interval) * 1e3;
9126
+ continue;
9127
+ }
9128
+ if ("error" in poll.data && poll.data.error === "expired") {
9129
+ throw new Error("Pairing code expired. Run `archme login` again.");
9130
+ }
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 })));
9316
+ }
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
+ );
8635
9321
  }
8636
- const expiresAt = Date.now() + data.expires_in * 1e3;
8637
- let intervalMs = Math.max(data.interval, 1) * 1e3;
8638
- while (Date.now() < expiresAt) {
8639
- await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
8640
- const poll = await postJson("/poll", {
8641
- device_code: data.device_code
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
8642
9341
  });
8643
- if (poll.status === 200 && "refresh_token" in poll.data) {
8644
- const { bindings, source } = await reconcileEnrollment(SUPABASE_URL2, poll.data);
8645
- console.log(`Enrollment complete. Credentials stored in ${describeCredentialSource(source)}.`);
8646
- console.log(`Daemon will serve 1 account across ${bindings.length} org(s).`);
8647
- return;
8648
- }
8649
- if (poll.status === 200 && "status" in poll.data && poll.data.status === "pending") {
8650
- continue;
8651
- }
8652
- if ("error" in poll.data && poll.data.error === "slow_down") {
8653
- intervalMs = Math.max(poll.data.interval ?? data.interval, data.interval) * 1e3;
8654
- continue;
8655
- }
8656
- if ("error" in poll.data && poll.data.error === "expired") {
8657
- throw new Error("Pairing code expired. Run `archme login` again.");
8658
- }
8659
- throw new Error(`Enrollment failed: ${JSON.stringify(poll.data)}`);
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
+ );
8660
9346
  }
8661
- throw new Error("Pairing code expired. Run `archme login` again.");
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 join3 } from "node:path";
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 join3(home(env), "Library", "Logs", LABEL);
8696
- const xdgState = env.XDG_STATE_HOME ?? join3(home(env), ".local", "state");
8697
- return join3(xdgState, "archme", "logs");
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: join3(dir, "daemon.log"), stderr: join3(dir, "daemon.err") };
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 join3(home(env), "Library", "LaunchAgents", `${LABEL}.plist`);
9433
+ return join4(home(env), "Library", "LaunchAgents", `${LABEL}.plist`);
8707
9434
  }
8708
- return join3(home(env), ".config", "systemd", "user", SYSTEMD_UNIT_NAME);
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 join3(home(env), ".archme", "daemon.pid");
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 mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
8837
- import { dirname as dirname3 } from "node:path";
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 mkdir2(dirname3(this.opts.plistPath), { recursive: true });
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 rename(tmp, this.opts.plistPath);
9580
+ await rename2(tmp, this.opts.plistPath);
8854
9581
  }
8855
9582
  async uninstall() {
8856
- await rm2(this.opts.plistPath, { force: true });
9583
+ await rm3(this.opts.plistPath, { force: true });
8857
9584
  }
8858
9585
  async unitExists() {
8859
9586
  try {
8860
- await readFile2(this.opts.plistPath);
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 readFile2(this.opts.plistPath, "utf8");
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 mkdir3, readFile as readFile3, rename as rename2, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
8936
- import { dirname as dirname4 } from "node:path";
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 mkdir3(dirname4(this.opts.unitPath), { recursive: true });
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 rename2(tmp, this.opts.unitPath);
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 rm3(this.opts.unitPath, { force: true });
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 readFile3(this.opts.unitPath);
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 readFile3(this.opts.unitPath, "utf8");
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 dirname5, join as join4 } from "node:path";
9113
- import { mkdir as mkdir4 } from "node:fs/promises";
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(join4(dir, "package.json"))) return dir;
9126
- const parent = dirname5(dir);
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 = join4(packageRoot, PUBLISHED_BIN_RELATIVE);
9872
+ const bundle = join5(packageRoot, PUBLISHED_BIN_RELATIVE);
9137
9873
  if (exists2(bundle)) return bundle;
9138
- const shim = join4(packageRoot, DEV_SHIM_RELATIVE);
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 = dirname5(fileURLToPath3(import.meta.url));
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 mkdir4(resolveLogDir(platform3, env), { recursive: true });
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 dirname6 } from "node:path";
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 open(path7, "r");
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 = dirname6(path7);
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 readFile4, stat as stat2 } from "node:fs/promises";
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 readFile4(stdout, "utf8");
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 rm4, writeFile as writeFile4 } from "node:fs/promises";
10191
+ import { mkdtemp, rm as rm5, writeFile as writeFile4 } from "node:fs/promises";
9456
10192
  import { tmpdir } from "node:os";
9457
- import { join as join5 } from "node:path";
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(join5(tmpdir(), "archme-mcp-"));
9487
- const configPath = join5(dir, "mcp.json");
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: () => rm4(dir, { recursive: true, force: true }) };
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 join6 } from "node:path";
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 = join6(dir, `${name}${ext2}`);
10289
+ const candidate = join7(dir, `${name}${ext2}`);
9554
10290
  if (isExecutable(candidate)) return candidate;
9555
10291
  }
9556
10292
  }
@@ -9611,7 +10347,7 @@ var init_preflight = __esm({
9611
10347
  var DEFAULT_CLAUDE_MODEL;
9612
10348
  var init_daemonModel = __esm({
9613
10349
  "../shared/daemonModel.ts"() {
9614
- DEFAULT_CLAUDE_MODEL = "claude-opus-4-8";
10350
+ DEFAULT_CLAUDE_MODEL = "claude-opus-5-5";
9615
10351
  }
9616
10352
  });
9617
10353
 
@@ -9866,9 +10602,9 @@ var init_digestPrompt = __esm({
9866
10602
  });
9867
10603
 
9868
10604
  // src/daemon/digest.ts
9869
- import { mkdtemp as mkdtemp2, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
10605
+ import { mkdtemp as mkdtemp2, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
9870
10606
  import { tmpdir as tmpdir2 } from "node:os";
9871
- import { join as join7 } from "node:path";
10607
+ import { join as join8 } from "node:path";
9872
10608
  function isCleanDigestExit(res) {
9873
10609
  return !res.timedOut && res.signal == null && res.exitCode === 0;
9874
10610
  }
@@ -9902,12 +10638,12 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
9902
10638
  }
9903
10639
  let dir;
9904
10640
  try {
9905
- dir = await mkdtemp2(join7(tmpdir2(), "archme-digest-"));
10641
+ dir = await mkdtemp2(join8(tmpdir2(), "archme-digest-"));
9906
10642
  let mcpConfigPath;
9907
10643
  if (mcp) {
9908
10644
  mcpConfigPath = mcp.configPath;
9909
10645
  } else {
9910
- mcpConfigPath = join7(dir, "mcp.json");
10646
+ mcpConfigPath = join8(dir, "mcp.json");
9911
10647
  await writeFile5(mcpConfigPath, JSON.stringify({ mcpServers: {} }));
9912
10648
  }
9913
10649
  const model = await resolveModelPin(ctx.supabase);
@@ -9942,7 +10678,7 @@ async function runDigestJob(ctx, job, deviceId, inFlight2, mcp) {
9942
10678
  await failDigest(ctx, job.id, deviceId, err2 instanceof Error ? err2.message : String(err2));
9943
10679
  } finally {
9944
10680
  inFlight2?.delete(job.id);
9945
- if (dir) await rm5(dir, { recursive: true, force: true }).catch(() => {
10681
+ if (dir) await rm6(dir, { recursive: true, force: true }).catch(() => {
9946
10682
  });
9947
10683
  }
9948
10684
  }
@@ -9996,37 +10732,37 @@ var init_heartbeatHealth = __esm({
9996
10732
  var balanced, maybeMatch, range;
9997
10733
  var init_esm = __esm({
9998
10734
  "node_modules/balanced-match/dist/esm/index.js"() {
9999
- balanced = (a2, b, str) => {
10000
- const ma = a2 instanceof RegExp ? maybeMatch(a2, str) : a2;
10001
- const mb = b instanceof RegExp ? maybeMatch(b, str) : b;
10002
- const r = ma !== null && mb != null && range(ma, mb, str);
10735
+ balanced = (a2, b, str2) => {
10736
+ const ma = a2 instanceof RegExp ? maybeMatch(a2, str2) : a2;
10737
+ const mb = b instanceof RegExp ? maybeMatch(b, str2) : b;
10738
+ const r = ma !== null && mb != null && range(ma, mb, str2);
10003
10739
  return r && {
10004
10740
  start: r[0],
10005
10741
  end: r[1],
10006
- pre: str.slice(0, r[0]),
10007
- body: str.slice(r[0] + ma.length, r[1]),
10008
- post: str.slice(r[1] + mb.length)
10742
+ pre: str2.slice(0, r[0]),
10743
+ body: str2.slice(r[0] + ma.length, r[1]),
10744
+ post: str2.slice(r[1] + mb.length)
10009
10745
  };
10010
10746
  };
10011
- maybeMatch = (reg, str) => {
10012
- const m = str.match(reg);
10747
+ maybeMatch = (reg, str2) => {
10748
+ const m = str2.match(reg);
10013
10749
  return m ? m[0] : null;
10014
10750
  };
10015
- range = (a2, b, str) => {
10751
+ range = (a2, b, str2) => {
10016
10752
  let begs, beg, left, right = void 0, result;
10017
- let ai = str.indexOf(a2);
10018
- let bi = str.indexOf(b, ai + 1);
10753
+ let ai = str2.indexOf(a2);
10754
+ let bi = str2.indexOf(b, ai + 1);
10019
10755
  let i2 = ai;
10020
10756
  if (ai >= 0 && bi > 0) {
10021
10757
  if (a2 === b) {
10022
10758
  return [ai, bi];
10023
10759
  }
10024
10760
  begs = [];
10025
- left = str.length;
10761
+ left = str2.length;
10026
10762
  while (i2 >= 0 && !result) {
10027
10763
  if (i2 === ai) {
10028
10764
  begs.push(i2);
10029
- ai = str.indexOf(a2, i2 + 1);
10765
+ ai = str2.indexOf(a2, i2 + 1);
10030
10766
  } else if (begs.length === 1) {
10031
10767
  const r = begs.pop();
10032
10768
  if (r !== void 0)
@@ -10037,7 +10773,7 @@ var init_esm = __esm({
10037
10773
  left = beg;
10038
10774
  right = bi;
10039
10775
  }
10040
- bi = str.indexOf(b, i2 + 1);
10776
+ bi = str2.indexOf(b, i2 + 1);
10041
10777
  }
10042
10778
  i2 = ai < bi && ai >= 0 ? ai : bi;
10043
10779
  }
@@ -10051,23 +10787,23 @@ var init_esm = __esm({
10051
10787
  });
10052
10788
 
10053
10789
  // node_modules/brace-expansion/dist/esm/index.js
10054
- function numeric(str) {
10055
- return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0);
10790
+ function numeric(str2) {
10791
+ return !isNaN(str2) ? parseInt(str2, 10) : str2.charCodeAt(0);
10056
10792
  }
10057
- function escapeBraces(str) {
10058
- return str.replace(slashPattern, escSlash).replace(openPattern, escOpen).replace(closePattern, escClose).replace(commaPattern, escComma).replace(periodPattern, escPeriod);
10793
+ function escapeBraces(str2) {
10794
+ return str2.replace(slashPattern, escSlash).replace(openPattern, escOpen).replace(closePattern, escClose).replace(commaPattern, escComma).replace(periodPattern, escPeriod);
10059
10795
  }
10060
- function unescapeBraces(str) {
10061
- return str.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
10796
+ function unescapeBraces(str2) {
10797
+ return str2.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, ".");
10062
10798
  }
10063
- function parseCommaParts(str) {
10064
- if (!str) {
10799
+ function parseCommaParts(str2) {
10800
+ if (!str2) {
10065
10801
  return [""];
10066
10802
  }
10067
10803
  const parts = [];
10068
- const m = balanced("{", "}", str);
10804
+ const m = balanced("{", "}", str2);
10069
10805
  if (!m) {
10070
- return str.split(",");
10806
+ return str2.split(",");
10071
10807
  }
10072
10808
  const { pre, body, post } = m;
10073
10809
  const p = pre.split(",");
@@ -10081,18 +10817,18 @@ function parseCommaParts(str) {
10081
10817
  parts.push.apply(parts, p);
10082
10818
  return parts;
10083
10819
  }
10084
- function expand(str, options = {}) {
10085
- if (!str) {
10820
+ function expand(str2, options = {}) {
10821
+ if (!str2) {
10086
10822
  return [];
10087
10823
  }
10088
10824
  const { max = EXPANSION_MAX } = options;
10089
- if (str.slice(0, 2) === "{}") {
10090
- str = "\\{\\}" + str.slice(2);
10825
+ if (str2.slice(0, 2) === "{}") {
10826
+ str2 = "\\{\\}" + str2.slice(2);
10091
10827
  }
10092
- return expand_(escapeBraces(str), max, true).map(unescapeBraces);
10828
+ return expand_(escapeBraces(str2), max, true).map(unescapeBraces);
10093
10829
  }
10094
- function embrace(str) {
10095
- return "{" + str + "}";
10830
+ function embrace(str2) {
10831
+ return "{" + str2 + "}";
10096
10832
  }
10097
10833
  function isPadded(el) {
10098
10834
  return /^-?0\d/.test(el);
@@ -10103,11 +10839,11 @@ function lte(i2, y) {
10103
10839
  function gte(i2, y) {
10104
10840
  return i2 >= y;
10105
10841
  }
10106
- function expand_(str, max, isTop) {
10842
+ function expand_(str2, max, isTop) {
10107
10843
  const expansions = [];
10108
- const m = balanced("{", "}", str);
10844
+ const m = balanced("{", "}", str2);
10109
10845
  if (!m)
10110
- return [str];
10846
+ return [str2];
10111
10847
  const pre = m.pre;
10112
10848
  const post = m.post.length ? expand_(m.post, max, false) : [""];
10113
10849
  if (/\$$/.test(m.pre)) {
@@ -10122,10 +10858,10 @@ function expand_(str, max, isTop) {
10122
10858
  const isOptions = m.body.indexOf(",") >= 0;
10123
10859
  if (!isSequence && !isOptions) {
10124
10860
  if (m.post.match(/,(?!,).*\}/)) {
10125
- str = m.pre + "{" + m.body + escClose + m.post;
10126
- return expand_(str, max, true);
10861
+ str2 = m.pre + "{" + m.body + escClose + m.post;
10862
+ return expand_(str2, max, true);
10127
10863
  }
10128
- return [str];
10864
+ return [str2];
10129
10865
  }
10130
10866
  let n2;
10131
10867
  if (isSequence) {
@@ -10570,7 +11306,7 @@ var init_ast = __esm({
10570
11306
  }
10571
11307
  return c3;
10572
11308
  }
10573
- static #parseAST(str, ast, pos, opt, extDepth) {
11309
+ static #parseAST(str2, ast, pos, opt, extDepth) {
10574
11310
  const maxDepth = opt.maxExtglobRecursion ?? 2;
10575
11311
  let escaping = false;
10576
11312
  let inBrace = false;
@@ -10579,8 +11315,8 @@ var init_ast = __esm({
10579
11315
  if (ast.type === null) {
10580
11316
  let i3 = pos;
10581
11317
  let acc2 = "";
10582
- while (i3 < str.length) {
10583
- const c3 = str.charAt(i3++);
11318
+ while (i3 < str2.length) {
11319
+ const c3 = str2.charAt(i3++);
10584
11320
  if (escaping || c3 === "\\") {
10585
11321
  escaping = !escaping;
10586
11322
  acc2 += c3;
@@ -10603,12 +11339,12 @@ var init_ast = __esm({
10603
11339
  acc2 += c3;
10604
11340
  continue;
10605
11341
  }
10606
- const doRecurse = !opt.noext && isExtglobType(c3) && str.charAt(i3) === "(" && extDepth <= maxDepth;
11342
+ const doRecurse = !opt.noext && isExtglobType(c3) && str2.charAt(i3) === "(" && extDepth <= maxDepth;
10607
11343
  if (doRecurse) {
10608
11344
  ast.push(acc2);
10609
11345
  acc2 = "";
10610
11346
  const ext2 = new _a(c3, ast);
10611
- i3 = _a.#parseAST(str, ext2, i3, opt, extDepth + 1);
11347
+ i3 = _a.#parseAST(str2, ext2, i3, opt, extDepth + 1);
10612
11348
  ast.push(ext2);
10613
11349
  continue;
10614
11350
  }
@@ -10621,8 +11357,8 @@ var init_ast = __esm({
10621
11357
  let part = new _a(null, ast);
10622
11358
  const parts = [];
10623
11359
  let acc = "";
10624
- while (i2 < str.length) {
10625
- const c3 = str.charAt(i2++);
11360
+ while (i2 < str2.length) {
11361
+ const c3 = str2.charAt(i2++);
10626
11362
  if (escaping || c3 === "\\") {
10627
11363
  escaping = !escaping;
10628
11364
  acc += c3;
@@ -10645,7 +11381,7 @@ var init_ast = __esm({
10645
11381
  acc += c3;
10646
11382
  continue;
10647
11383
  }
10648
- const doRecurse = !opt.noext && isExtglobType(c3) && str.charAt(i2) === "(" && /* c8 ignore start - the maxDepth is sufficient here */
11384
+ const doRecurse = !opt.noext && isExtglobType(c3) && str2.charAt(i2) === "(" && /* c8 ignore start - the maxDepth is sufficient here */
10649
11385
  (extDepth <= maxDepth || ast && ast.#canAdoptType(c3));
10650
11386
  if (doRecurse) {
10651
11387
  const depthAdd = ast && ast.#canAdoptType(c3) ? 0 : 1;
@@ -10653,7 +11389,7 @@ var init_ast = __esm({
10653
11389
  acc = "";
10654
11390
  const ext2 = new _a(c3, part);
10655
11391
  part.push(ext2);
10656
- i2 = _a.#parseAST(str, ext2, i2, opt, extDepth + depthAdd);
11392
+ i2 = _a.#parseAST(str2, ext2, i2, opt, extDepth + depthAdd);
10657
11393
  continue;
10658
11394
  }
10659
11395
  if (c3 === "|") {
@@ -10676,7 +11412,7 @@ var init_ast = __esm({
10676
11412
  }
10677
11413
  ast.type = null;
10678
11414
  ast.#hasMagic = void 0;
10679
- ast.#parts = [str.substring(pos - 1)];
11415
+ ast.#parts = [str2.substring(pos - 1)];
10680
11416
  return i2;
10681
11417
  }
10682
11418
  #canAdoptWithSpace(child) {
@@ -11782,10 +12518,10 @@ var init_esm3 = __esm({
11782
12518
  }
11783
12519
  return filtered.join("/");
11784
12520
  }).join("|");
11785
- const [open2, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
11786
- re = "^" + open2 + re + close + "$";
12521
+ const [open3, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
12522
+ re = "^" + open3 + re + close + "$";
11787
12523
  if (this.partial) {
11788
- re = "^(?:\\/|" + open2 + re.slice(1, -1) + close + ")$";
12524
+ re = "^(?:\\/|" + open3 + re.slice(1, -1) + close + ")$";
11789
12525
  }
11790
12526
  if (this.negate)
11791
12527
  re = "^(?!" + re + ").+$";
@@ -12618,18 +13354,18 @@ var init_lang_detect = __esm({
12618
13354
  });
12619
13355
 
12620
13356
  // src/analyze/tree-sitter/cache.ts
12621
- import { mkdir as mkdir5, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
12622
- import { dirname as dirname7, join as join8 } from "node:path";
12623
- import { homedir as homedir3 } from "node:os";
13357
+ import { mkdir as mkdir6, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
13358
+ import { dirname as dirname8, join as join9 } from "node:path";
13359
+ import { homedir as homedir4 } from "node:os";
12624
13360
  import { createHash } from "node:crypto";
12625
13361
  function defaultCacheRoot() {
12626
- return process.env.ARCHME_TOOLS_DIR ?? join8(homedir3(), ".archme", "tools");
13362
+ return process.env.ARCHME_TOOLS_DIR ?? join9(homedir4(), ".archme", "tools");
12627
13363
  }
12628
13364
  function grammarCacheDir(cacheRoot) {
12629
- return join8(cacheRoot, "grammars");
13365
+ return join9(cacheRoot, "grammars");
12630
13366
  }
12631
13367
  function cachedGrammarPath(cacheRoot, lang, version) {
12632
- return join8(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
13368
+ return join9(grammarCacheDir(cacheRoot), `${lang}-${version}.wasm`);
12633
13369
  }
12634
13370
  async function fileExists(path7) {
12635
13371
  try {
@@ -12650,7 +13386,7 @@ async function ensureGrammar(opts) {
12650
13386
  if (await fileExists(target)) {
12651
13387
  return { path: target, downloaded: false };
12652
13388
  }
12653
- await mkdir5(dirname7(target), { recursive: true });
13389
+ await mkdir6(dirname8(target), { recursive: true });
12654
13390
  const url = `${opts.urlBase}/${opts.filename}`;
12655
13391
  process.stderr.write(`fetching grammar: ${opts.lang}@${opts.version} (${url})
12656
13392
  `);
@@ -12779,7 +13515,7 @@ var init_grammars = __esm({
12779
13515
  });
12780
13516
 
12781
13517
  // src/analyze/tree-sitter/parser.ts
12782
- import { readFile as readFile5 } from "node:fs/promises";
13518
+ import { readFile as readFile6 } from "node:fs/promises";
12783
13519
  import { Parser, Language } from "web-tree-sitter";
12784
13520
  async function init() {
12785
13521
  if (initialized) return;
@@ -12802,7 +13538,7 @@ async function getLanguage(lang, cacheRoot = defaultCacheRoot()) {
12802
13538
  filename: entry.filename,
12803
13539
  sha256: entry.sha256
12804
13540
  });
12805
- const wasm = await readFile5(wasmPath);
13541
+ const wasm = await readFile6(wasmPath);
12806
13542
  const language = await Language.load(wasm);
12807
13543
  languageCache.set(lang, language);
12808
13544
  return language;
@@ -13198,7 +13934,7 @@ var init_output2 = __esm({
13198
13934
  });
13199
13935
 
13200
13936
  // src/analyze/commands/outline.ts
13201
- import { readFile as readFile6 } from "node:fs/promises";
13937
+ import { readFile as readFile7 } from "node:fs/promises";
13202
13938
  async function outlineSource(source, file) {
13203
13939
  const lang = detectLanguage(file);
13204
13940
  if (!lang) {
@@ -13251,7 +13987,7 @@ async function runOutline(file) {
13251
13987
  }
13252
13988
  let source;
13253
13989
  try {
13254
- source = await readFile6(file, "utf8");
13990
+ source = await readFile7(file, "utf8");
13255
13991
  } catch (err2) {
13256
13992
  return envelope({
13257
13993
  tool: "file",
@@ -13535,7 +14271,7 @@ var require_ignore = __commonJS({
13535
14271
  // Zero, one or several directories
13536
14272
  // should not use '*', or it will be replaced by the next replacer
13537
14273
  // Check if it is not the last `'/**'`
13538
- (_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
14274
+ (_, index, str2) => index + 6 < str2.length ? "(?:\\/[^\\/]+)*" : "\\/.+"
13539
14275
  ],
13540
14276
  // normal intermediate wildcards
13541
14277
  [
@@ -13634,12 +14370,12 @@ var require_ignore = __commonJS({
13634
14370
  return this._make(MODE_CHECK_IGNORE, key);
13635
14371
  }
13636
14372
  _make(mode, key) {
13637
- const str = this.regexPrefix.replace(
14373
+ const str2 = this.regexPrefix.replace(
13638
14374
  REGEX_REPLACE_TRAILING_WILDCARD,
13639
14375
  // It does not need to bind pattern
13640
14376
  TRAILING_WILD_CARD_REPLACERS[mode]
13641
14377
  );
13642
- const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
14378
+ const regex = this.ignoreCase ? new RegExp(str2, "i") : new RegExp(str2);
13643
14379
  return define(this, key, regex);
13644
14380
  }
13645
14381
  };
@@ -13843,7 +14579,7 @@ var require_ignore = __commonJS({
13843
14579
  var factory = (options) => new Ignore(options);
13844
14580
  var isPathValid = (path7) => checkPath(path7 && checkPath.convert(path7), path7, RETURN_FALSE);
13845
14581
  var setupWindows = () => {
13846
- const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
14582
+ const makePosix = (str2) => /^\\\\\?\\/.test(str2) || /["<>|\u0000-\u001F]+/u.test(str2) ? str2 : str2.replace(/\\/g, "/");
13847
14583
  checkPath.convert = makePosix;
13848
14584
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
13849
14585
  checkPath.isNotRelative = (path7) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path7) || isNotRelative(path7);
@@ -13862,12 +14598,12 @@ var require_ignore = __commonJS({
13862
14598
  });
13863
14599
 
13864
14600
  // src/analyze/repo-walk.ts
13865
- import { readdir, readFile as readFile7, stat as stat4 } from "node:fs/promises";
13866
- import { join as join9, relative } from "node:path";
14601
+ import { readdir, readFile as readFile8, stat as stat4 } from "node:fs/promises";
14602
+ import { join as join10, relative } from "node:path";
13867
14603
  async function loadGitignore(root) {
13868
14604
  const ig = (0, import_ignore.default)();
13869
14605
  try {
13870
- const text = await readFile7(join9(root, ".gitignore"), "utf8");
14606
+ const text = await readFile8(join10(root, ".gitignore"), "utf8");
13871
14607
  ig.add(text);
13872
14608
  } catch {
13873
14609
  ig.add(["node_modules", "dist", "build", ".next", "coverage", ".turbo"]);
@@ -13881,7 +14617,7 @@ async function* walkRepoFiles(root, filter2 = {}) {
13881
14617
  const entries = await readdir(dir, { withFileTypes: true });
13882
14618
  for (const e of entries) {
13883
14619
  if (ALWAYS_SKIP.has(e.name)) continue;
13884
- const full = join9(dir, e.name);
14620
+ const full = join10(dir, e.name);
13885
14621
  const rel = relative(root, full);
13886
14622
  if (e.isDirectory()) {
13887
14623
  if (ig.ignores(rel + "/")) continue;
@@ -13905,7 +14641,7 @@ async function collectRepoFiles(root, filter2 = {}) {
13905
14641
  }
13906
14642
  async function gitListFiles(root) {
13907
14643
  try {
13908
- await stat4(join9(root, ".git"));
14644
+ await stat4(join10(root, ".git"));
13909
14645
  } catch {
13910
14646
  return null;
13911
14647
  }
@@ -13946,11 +14682,11 @@ var init_repo_walk = __esm({
13946
14682
  });
13947
14683
 
13948
14684
  // src/analyze/commands/repo.ts
13949
- import { join as join10 } from "node:path";
14685
+ import { join as join11 } from "node:path";
13950
14686
  async function* walkRepo(root, opts = {}) {
13951
14687
  const files = await collectRepoFilesWithGit(root, opts);
13952
14688
  for (const rel of files) {
13953
- const env = await runOutline(join10(root, rel));
14689
+ const env = await runOutline(join11(root, rel));
13954
14690
  yield env;
13955
14691
  }
13956
14692
  }
@@ -13964,9 +14700,9 @@ var init_repo = __esm({
13964
14700
 
13965
14701
  // src/analyze/matchers/file.ts
13966
14702
  import { stat as stat5 } from "node:fs/promises";
13967
- import { join as join11 } from "node:path";
14703
+ import { join as join12 } from "node:path";
13968
14704
  async function matchFileAnchor(anchor, repoRoot) {
13969
- const full = join11(repoRoot, anchor.path);
14705
+ const full = join12(repoRoot, anchor.path);
13970
14706
  try {
13971
14707
  const s = await stat5(full);
13972
14708
  if (!s.isFile()) {
@@ -13997,9 +14733,9 @@ var init_file = __esm({
13997
14733
 
13998
14734
  // src/analyze/matchers/directory.ts
13999
14735
  import { stat as stat6 } from "node:fs/promises";
14000
- import { join as join12 } from "node:path";
14736
+ import { join as join13 } from "node:path";
14001
14737
  async function matchDirectoryAnchor(anchor, repoRoot) {
14002
- const full = join12(repoRoot, anchor.path);
14738
+ const full = join13(repoRoot, anchor.path);
14003
14739
  try {
14004
14740
  const s = await stat6(full);
14005
14741
  if (!s.isDirectory()) {
@@ -14068,9 +14804,9 @@ var init_findSymbol = __esm({
14068
14804
 
14069
14805
  // src/analyze/matchers/symbol.ts
14070
14806
  import { stat as stat7 } from "node:fs/promises";
14071
- import { join as join13 } from "node:path";
14807
+ import { join as join14 } from "node:path";
14072
14808
  async function matchSymbolAnchor(anchor, repoRoot) {
14073
- const full = join13(repoRoot, anchor.path);
14809
+ const full = join14(repoRoot, anchor.path);
14074
14810
  try {
14075
14811
  await stat7(full);
14076
14812
  } catch {
@@ -14194,7 +14930,7 @@ __export(dispatch_exports, {
14194
14930
  augmentChangesWithSymbolsAtSha: () => augmentChangesWithSymbolsAtSha,
14195
14931
  dispatchAnalyze: () => dispatchAnalyze
14196
14932
  });
14197
- import { readFile as readFile8 } from "node:fs/promises";
14933
+ import { readFile as readFile9 } from "node:fs/promises";
14198
14934
  import { resolve } from "node:path";
14199
14935
  async function dispatchAnalyze(args) {
14200
14936
  const sub = args[0];
@@ -14335,7 +15071,7 @@ async function cmdMatch(args) {
14335
15071
  console.error("archme analyze match: <anchor-spec.json> required");
14336
15072
  return 2;
14337
15073
  }
14338
- const text = await readFile8(path7, "utf8");
15074
+ const text = await readFile9(path7, "utf8");
14339
15075
  const anchors = JSON.parse(text);
14340
15076
  if (!Array.isArray(anchors)) {
14341
15077
  console.error("archme analyze match: spec must be a JSON array of anchors");
@@ -14385,235 +15121,103 @@ var init_dispatch = __esm({
14385
15121
  init_match();
14386
15122
  init_prefetch();
14387
15123
  init_output2();
14388
- }
14389
- });
14390
-
14391
- // src/daemon/service/singleInstanceLock.ts
14392
- import { mkdir as mkdir6, readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
14393
- import { dirname as dirname8 } from "node:path";
14394
- import { lock } from "proper-lockfile";
14395
- async function acquireSingleInstanceLock(pidfile) {
14396
- await mkdir6(dirname8(pidfile), { recursive: true });
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`);
15124
+ }
15125
+ });
15126
+
15127
+ // src/daemon/service/singleInstanceLock.ts
15128
+ import { mkdir as mkdir7, readFile as readFile10, writeFile as writeFile7 } from "node:fs/promises";
15129
+ import { dirname as dirname9 } from "node:path";
15130
+ import { lock } from "proper-lockfile";
15131
+ async function acquireSingleInstanceLock(pidfile) {
15132
+ await mkdir7(dirname9(pidfile), { recursive: true });
14579
15133
  try {
14580
- await reLogin();
15134
+ await writeFile7(pidfile, String(process.pid), { flag: "wx" });
14581
15135
  } catch (err2) {
14582
- console.error(`${label}: re-login failed:`, err2);
14583
- return first;
15136
+ if (err2.code !== "EEXIST") throw err2;
14584
15137
  }
14585
- return await doFetch();
14586
- }
14587
- async function parseJsonResponse(response) {
14588
- const text = await response.text();
14589
- if (!text) return {};
15138
+ let release;
14590
15139
  try {
14591
- return JSON.parse(text);
15140
+ release = await lock(pidfile, {
15141
+ // `stale` here is proper-lockfile's own mtime-refresh window — unrelated
15142
+ // to the daemon's Supabase heartbeat. Auto-released after 30s of mtime
15143
+ // staleness if the previous holder crashed without releasing.
15144
+ retries: 0,
15145
+ stale: 3e4
15146
+ });
14592
15147
  } catch {
14593
- return { error: text, error_description: text, msg: text };
15148
+ let heldByPid = null;
15149
+ try {
15150
+ heldByPid = Number((await readFile10(pidfile, "utf8")).trim()) || null;
15151
+ } catch {
15152
+ heldByPid = null;
15153
+ }
15154
+ throw new SingleInstanceLockedError(pidfile, heldByPid);
14594
15155
  }
15156
+ await writeFile7(pidfile, String(process.pid), { flag: "w" });
15157
+ return {
15158
+ async release() {
15159
+ try {
15160
+ await release();
15161
+ } catch {
15162
+ }
15163
+ }
15164
+ };
14595
15165
  }
14596
- function responseError(body, fallback) {
14597
- for (const key of ["error_description", "error", "msg"]) {
14598
- const value = body[key];
14599
- if (typeof value === "string" && value) return value;
15166
+ var SINGLE_INSTANCE_EXIT_CODE, LOCK_HELD_MESSAGE, SingleInstanceLockedError;
15167
+ var init_singleInstanceLock = __esm({
15168
+ "src/daemon/service/singleInstanceLock.ts"() {
15169
+ "use strict";
15170
+ SINGLE_INSTANCE_EXIT_CODE = 64;
15171
+ LOCK_HELD_MESSAGE = "already holds";
15172
+ SingleInstanceLockedError = class extends Error {
15173
+ heldByPid;
15174
+ constructor(pidfile, heldByPid) {
15175
+ super(
15176
+ heldByPid != null ? `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile} (pid ${heldByPid})` : `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile}`
15177
+ );
15178
+ this.heldByPid = heldByPid;
15179
+ this.name = "SingleInstanceLockedError";
15180
+ }
15181
+ };
14600
15182
  }
14601
- return fallback;
14602
- }
14603
- var DEFAULT_TOKEN_EXPIRES_IN_SECONDS, REFRESH_RETRY_BACKOFF_MS, LoadStoredAndRefreshError;
14604
- var init_oauthClient = __esm({
14605
- "src/scan/oauthClient.ts"() {
15183
+ });
15184
+
15185
+ // src/daemon/semaphore.ts
15186
+ var Semaphore;
15187
+ var init_semaphore = __esm({
15188
+ "src/daemon/semaphore.ts"() {
14606
15189
  "use strict";
14607
- DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600;
14608
- REFRESH_RETRY_BACKOFF_MS = 6e4;
14609
- LoadStoredAndRefreshError = class extends Error {
14610
- code;
14611
- permanent;
14612
- constructor(code, message) {
14613
- super(message);
14614
- this.name = "LoadStoredAndRefreshError";
14615
- this.code = code;
14616
- this.permanent = code !== "transient";
15190
+ Semaphore = class {
15191
+ constructor(size) {
15192
+ this.size = size;
15193
+ }
15194
+ inUse = 0;
15195
+ waiters = [];
15196
+ available() {
15197
+ return Math.max(0, this.size - this.inUse);
15198
+ }
15199
+ tryAcquire() {
15200
+ if (this.inUse >= this.size) return null;
15201
+ this.inUse++;
15202
+ return this.makeRelease();
15203
+ }
15204
+ acquire() {
15205
+ const slot = this.tryAcquire();
15206
+ if (slot) return Promise.resolve(slot);
15207
+ return new Promise((resolve6) => this.waiters.push(resolve6));
15208
+ }
15209
+ makeRelease() {
15210
+ let released = false;
15211
+ return () => {
15212
+ if (released) return;
15213
+ released = true;
15214
+ const next = this.waiters.shift();
15215
+ if (next) {
15216
+ next(this.makeRelease());
15217
+ } else {
15218
+ this.inUse--;
15219
+ }
15220
+ };
14617
15221
  }
14618
15222
  };
14619
15223
  }
@@ -14625,6 +15229,9 @@ var init_sessionAuth = __esm({
14625
15229
  "src/scan/sessionAuth.ts"() {
14626
15230
  "use strict";
14627
15231
  init_credentials();
15232
+ init_keyRing();
15233
+ init_accountKey();
15234
+ init_keySession();
14628
15235
  init_oauthClient();
14629
15236
  SessionAuth = class _SessionAuth {
14630
15237
  constructor(cfg) {
@@ -14637,6 +15244,9 @@ var init_sessionAuth = __esm({
14637
15244
  deviceId = null;
14638
15245
  orgId = null;
14639
15246
  userId = null;
15247
+ // "key" sessions are plain GoTrue sessions (password grant); everything else
15248
+ // is the OAuth server's. Set from the stored credential at sign-in.
15249
+ kind = "oauth";
14640
15250
  // The userinfo resolved at first sign-in. The user_id is stable for the
14641
15251
  // session's lifetime, so once it's known we reuse this and skip the per-refresh
14642
15252
  // userinfo round-trip (the recurring refresh path discards the returned value;
@@ -14663,6 +15273,9 @@ var init_sessionAuth = __esm({
14663
15273
  getRefreshToken() {
14664
15274
  return this.refreshToken;
14665
15275
  }
15276
+ getKind() {
15277
+ return this.kind;
15278
+ }
14666
15279
  onAccessTokenChange(listener) {
14667
15280
  this.listeners.add(listener);
14668
15281
  return () => this.listeners.delete(listener);
@@ -14675,6 +15288,7 @@ var init_sessionAuth = __esm({
14675
15288
  }
14676
15289
  /** Test seam: seed state without going through the store. */
14677
15290
  __seedForTesting(state) {
15291
+ if (state.kind !== void 0) this.kind = state.kind;
14678
15292
  if (state.accessToken !== void 0) this.accessToken = state.accessToken;
14679
15293
  if (state.accessTokenExpiresAtMs !== void 0) this.accessTokenExpiresAtMs = state.accessTokenExpiresAtMs;
14680
15294
  if (state.refreshToken !== void 0) this.refreshToken = state.refreshToken;
@@ -14712,11 +15326,12 @@ var init_sessionAuth = __esm({
14712
15326
  this.refreshToken = nextRefreshToken;
14713
15327
  let userInfo2 = this.lastUserInfo;
14714
15328
  if (this.userId === null || userInfo2 === null) {
14715
- userInfo2 = await getOAuthUserInfoWith(this.accessToken, this.cfg);
15329
+ userInfo2 = this.kind === "key" ? await getKeyUserInfo(this.accessToken, this.cfg) : await getOAuthUserInfoWith(this.accessToken, this.cfg);
14716
15330
  this.userId = resolveUserId(userInfo2, ctx.fallbackUserId);
14717
15331
  this.lastUserInfo = userInfo2;
14718
15332
  }
14719
15333
  const saved = await saveCredentials({
15334
+ kind: this.kind,
14720
15335
  refresh_token: nextRefreshToken,
14721
15336
  access_token: this.accessToken,
14722
15337
  org_id: ctx.orgId,
@@ -14813,14 +15428,14 @@ var init_sessionAuth = __esm({
14813
15428
  if (!refreshToken || !deviceId || !orgId) {
14814
15429
  throw new LoadStoredAndRefreshError("missing_device", "not signed in; cannot mint MCP bearer");
14815
15430
  }
14816
- const refreshed = await refreshOAuthCredentialsWith(refreshToken, this.cfg);
15431
+ const refreshed = await this.refreshOrRecover(refreshToken, this.userId);
14817
15432
  if (!refreshed.ok) {
14818
15433
  if (refreshed.permanent) {
14819
15434
  await clearCredential(this.cfg.supabaseUrl, deviceId);
14820
15435
  this.clearTokensOnly();
14821
15436
  throw new LoadStoredAndRefreshError(
14822
15437
  "invalid_grant",
14823
- `${errorPrefix} failed permanently: ${refreshed.error}. Run \`archme login\` to pair again.`
15438
+ `${errorPrefix} failed permanently: ${refreshed.error}. ${this.reloginHint()}`
14824
15439
  );
14825
15440
  }
14826
15441
  throw new LoadStoredAndRefreshError("transient", `${errorPrefix} failed: ${refreshed.error}`);
@@ -14838,6 +15453,33 @@ var init_sessionAuth = __esm({
14838
15453
  if (this.refreshInFlight === p) this.refreshInFlight = null;
14839
15454
  });
14840
15455
  }
15456
+ async refreshOrRecover(refreshToken, userId) {
15457
+ if (this.kind !== "key") return refreshOAuthCredentialsWith(refreshToken, this.cfg);
15458
+ const refreshed = await refreshKeySession(refreshToken, this.cfg);
15459
+ if (refreshed.ok || !refreshed.permanent) return refreshed;
15460
+ return this.signInWithSavedKey(userId, refreshed.error);
15461
+ }
15462
+ reloginHint() {
15463
+ return this.kind === "key" ? "Run `archme login --key` to sign in with your key again." : "Run `archme login` to pair again.";
15464
+ }
15465
+ // A keyed account's refresh token can die (reuse detection, a long sleep) while
15466
+ // its key is still good, so sign in again from the key ring rather than
15467
+ // clearing. Permanent only when there is no saved key or the key is refused;
15468
+ // the key ring itself is never modified here.
15469
+ async signInWithSavedKey(userId, refreshError) {
15470
+ let saved;
15471
+ try {
15472
+ saved = userId ? await findKey(this.cfg.supabaseUrl, userId) : null;
15473
+ } catch (err2) {
15474
+ return { ok: false, permanent: false, error: `${refreshError}; ${err2 instanceof Error ? err2.message : String(err2)}` };
15475
+ }
15476
+ const parsed = parseAccountKey(saved?.key);
15477
+ if (!parsed) return { ok: false, permanent: true, error: `${refreshError}; no saved key for this account` };
15478
+ console.error(`auth: refresh token rejected (${refreshError}); signing in again with the saved key for ${parsed.handle}`);
15479
+ const granted = await passwordGrant(parsed.email, parsed.secret, this.cfg);
15480
+ if (granted.ok) return granted;
15481
+ return granted.permanent ? { ok: false, permanent: true, error: `the saved key for ${parsed.handle} was rejected: ${granted.error}` } : granted;
15482
+ }
14841
15483
  async getMcpAccessToken() {
14842
15484
  if (this.accessToken && this.accessTokenExpiresAtMs - Date.now() >= this.cfg.preSpawnMinLifetimeMs) {
14843
15485
  return this.accessToken;
@@ -14857,13 +15499,14 @@ var init_sessionAuth = __esm({
14857
15499
  "stored archme credentials are missing device registration; run `archme login` again"
14858
15500
  );
14859
15501
  }
14860
- const refreshed = await refreshOAuthCredentialsWith(stored.refresh_token, this.cfg);
15502
+ this.kind = credentialKind(stored);
15503
+ const refreshed = await this.refreshOrRecover(stored.refresh_token, stored.user_id ?? null);
14861
15504
  if (!refreshed.ok) {
14862
15505
  if (refreshed.permanent) {
14863
15506
  await clearCredential(this.cfg.supabaseUrl, stored.device_id);
14864
15507
  throw new LoadStoredAndRefreshError(
14865
15508
  "invalid_grant",
14866
- `stored archme credentials are invalid or revoked: ${refreshed.error}. Run \`archme login\` to pair again.`
15509
+ `stored archme credentials are invalid or revoked: ${refreshed.error}. ${this.reloginHint()}`
14867
15510
  );
14868
15511
  }
14869
15512
  throw new LoadStoredAndRefreshError("transient", `token refresh failed (transient): ${refreshed.error}`);
@@ -14876,6 +15519,7 @@ var init_sessionAuth = __esm({
14876
15519
  });
14877
15520
  return {
14878
15521
  user: userInfo2,
15522
+ kind: this.kind,
14879
15523
  orgId: stored.org_id,
14880
15524
  deviceId: stored.device_id,
14881
15525
  source,
@@ -14900,6 +15544,41 @@ var init_sessionAuth = __esm({
14900
15544
  });
14901
15545
 
14902
15546
  // src/scan/bearer.ts
15547
+ var bearer_exports = {};
15548
+ __export(bearer_exports, {
15549
+ LoadStoredAndRefreshError: () => LoadStoredAndRefreshError,
15550
+ REFRESH_RETRY_BACKOFF_MS: () => REFRESH_RETRY_BACKOFF_MS,
15551
+ __seedBearerForTesting: () => __seedBearerForTesting,
15552
+ clearRefreshTimer: () => clearRefreshTimer,
15553
+ computeExpiresAt: () => computeExpiresAt,
15554
+ fetchWithAuthRetry: () => fetchWithAuthRetry,
15555
+ getCurrentAccessToken: () => getCurrentAccessToken,
15556
+ getCurrentDeviceId: () => getCurrentDeviceId,
15557
+ getCurrentOrgId: () => getCurrentOrgId,
15558
+ getCurrentRefreshToken: () => getCurrentRefreshToken,
15559
+ getCurrentUserId: () => getCurrentUserId,
15560
+ getMcpAccessToken: () => getMcpAccessToken,
15561
+ getOAuthUserInfo: () => getOAuthUserInfo,
15562
+ initBearer: () => initBearer,
15563
+ isAuthError: () => isAuthError,
15564
+ isPermanentRefreshError: () => isPermanentRefreshError,
15565
+ loadStoredAndRefresh: () => loadStoredAndRefresh,
15566
+ onAccessTokenChange: () => onAccessTokenChange,
15567
+ parseJsonResponse: () => parseJsonResponse,
15568
+ refreshOAuthCredentials: () => refreshOAuthCredentials,
15569
+ resetBearer: () => resetBearer,
15570
+ resolveUserId: () => resolveUserId,
15571
+ responseError: () => responseError,
15572
+ runScheduledRefresh: () => runScheduledRefresh,
15573
+ scheduleCredentialRefresh: () => scheduleCredentialRefresh,
15574
+ withAuthRetry: () => withAuthRetry
15575
+ });
15576
+ async function refreshOAuthCredentials(refreshToken) {
15577
+ return refreshOAuthCredentialsWith(refreshToken, requireConfig());
15578
+ }
15579
+ async function getOAuthUserInfo(accessToken) {
15580
+ return getOAuthUserInfoWith(accessToken, requireConfig());
15581
+ }
14903
15582
  function configsEqual(a2, b) {
14904
15583
  return a2.supabaseUrl === b.supabaseUrl && a2.anonKey === b.anonKey && a2.clientId === b.clientId && a2.preSpawnMinLifetimeMs === b.preSpawnMinLifetimeMs;
14905
15584
  }
@@ -14909,6 +15588,11 @@ function initBearer(cfg) {
14909
15588
  config = cfg;
14910
15589
  defaultSession = new SessionAuth(cfg);
14911
15590
  }
15591
+ function resetBearer() {
15592
+ if (defaultSession) defaultSession.clearRefreshTimer();
15593
+ config = null;
15594
+ defaultSession = null;
15595
+ }
14912
15596
  function requireConfig() {
14913
15597
  if (!config) throw new Error("bearer module not initialized; call initBearer() first");
14914
15598
  return config;
@@ -14920,15 +15604,36 @@ function requireSession() {
14920
15604
  function getCurrentAccessToken() {
14921
15605
  return defaultSession?.getAccessToken() ?? null;
14922
15606
  }
15607
+ function getCurrentDeviceId() {
15608
+ return defaultSession?.getDeviceId() ?? null;
15609
+ }
14923
15610
  function getCurrentOrgId() {
14924
15611
  return defaultSession?.getOrgId() ?? null;
14925
15612
  }
14926
15613
  function getCurrentUserId() {
14927
15614
  return defaultSession?.getUserId() ?? null;
14928
15615
  }
15616
+ function getCurrentRefreshToken() {
15617
+ return defaultSession?.getRefreshToken() ?? null;
15618
+ }
15619
+ function clearRefreshTimer() {
15620
+ defaultSession?.clearRefreshTimer();
15621
+ }
15622
+ function onAccessTokenChange(listener) {
15623
+ return requireSession().onAccessTokenChange(listener);
15624
+ }
14929
15625
  function getMcpAccessToken() {
14930
15626
  return requireSession().getMcpAccessToken();
14931
15627
  }
15628
+ function scheduleCredentialRefresh(rt, d, o2, e) {
15629
+ requireSession().scheduleCredentialRefresh(rt, d, o2, e);
15630
+ }
15631
+ function runScheduledRefresh(rt, d, o2) {
15632
+ return requireSession().runScheduledRefresh(rt, d, o2);
15633
+ }
15634
+ function __seedBearerForTesting(state) {
15635
+ requireSession().__seedForTesting(state);
15636
+ }
14932
15637
  async function loadStoredAndRefresh() {
14933
15638
  const cfg = requireConfig();
14934
15639
  const stored = await loadCredentials(cfg.supabaseUrl);
@@ -15158,22 +15863,22 @@ var init_rateLimit = __esm({
15158
15863
 
15159
15864
  // src/packageRoot.ts
15160
15865
  import { readFileSync as readFileSync4 } from "node:fs";
15161
- import { dirname as dirname9, join as join14, parse } from "node:path";
15866
+ import { dirname as dirname10, join as join15, parse } from "node:path";
15162
15867
  function defaultReadFile(path7) {
15163
15868
  return readFileSync4(path7, "utf8");
15164
15869
  }
15165
15870
  function findPackageRoot2(startDir, opts = {}) {
15166
- const readFile14 = opts.readFile ?? defaultReadFile;
15871
+ const readFile16 = opts.readFile ?? defaultReadFile;
15167
15872
  const stopAt = parse(startDir).root;
15168
15873
  let dir = startDir;
15169
15874
  for (; ; ) {
15170
15875
  try {
15171
- const parsed = JSON.parse(readFile14(join14(dir, "package.json")));
15876
+ const parsed = JSON.parse(readFile16(join15(dir, "package.json")));
15172
15877
  if (parsed.name === PACKAGE_NAME) return dir;
15173
15878
  } catch {
15174
15879
  }
15175
15880
  if (dir === stopAt) return null;
15176
- const parent = dirname9(dir);
15881
+ const parent = dirname10(dir);
15177
15882
  if (parent === dir) return null;
15178
15883
  dir = parent;
15179
15884
  }
@@ -15191,15 +15896,15 @@ var version_exports = {};
15191
15896
  __export(version_exports, {
15192
15897
  readDaemonVersion: () => readDaemonVersion
15193
15898
  });
15194
- import { readFile as readFile10 } from "node:fs/promises";
15195
- import { dirname as dirname10, join as join15 } from "node:path";
15899
+ import { readFile as readFile11 } from "node:fs/promises";
15900
+ import { dirname as dirname11, join as join16 } from "node:path";
15196
15901
  import { fileURLToPath as fileURLToPath4 } from "node:url";
15197
15902
  async function readDaemonVersion() {
15198
15903
  try {
15199
- const here = dirname10(fileURLToPath4(import.meta.url));
15904
+ const here = dirname11(fileURLToPath4(import.meta.url));
15200
15905
  const root = findPackageRoot2(here);
15201
15906
  if (!root) return null;
15202
- const raw = await readFile10(join15(root, "package.json"), "utf8");
15907
+ const raw = await readFile11(join16(root, "package.json"), "utf8");
15203
15908
  const parsed = JSON.parse(raw);
15204
15909
  return typeof parsed.version === "string" ? parsed.version : null;
15205
15910
  } catch {
@@ -15214,8 +15919,8 @@ var init_version = __esm({
15214
15919
  });
15215
15920
 
15216
15921
  // src/daemon/materializeDiff.ts
15217
- import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
15218
- import { join as join16 } from "node:path";
15922
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
15923
+ import { join as join17 } from "node:path";
15219
15924
  import { createHash as createHash2 } from "node:crypto";
15220
15925
  function splitPatchByFile(patch) {
15221
15926
  if (!patch.trim()) return [];
@@ -15300,18 +16005,18 @@ function buildManifest(files, rangeLabel) {
15300
16005
  }
15301
16006
  async function materializeDiff(worktreeDir, patch, rangeLabel) {
15302
16007
  const files = splitPatchByFile(patch);
15303
- const dir = join16(worktreeDir, CHANGES_DIR);
15304
- const byFileDir = join16(dir, "by-file");
15305
- await mkdir7(byFileDir, { recursive: true });
15306
- await writeFile8(join16(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
16008
+ const dir = join17(worktreeDir, CHANGES_DIR);
16009
+ const byFileDir = join17(dir, "by-file");
16010
+ await mkdir8(byFileDir, { recursive: true });
16011
+ await writeFile8(join17(dir, "FULL.diff"), patch.endsWith("\n") ? patch : `${patch}
15307
16012
  `, "utf8");
15308
16013
  for (const f of files) {
15309
16014
  const rel = `by-file/${hashPath(f.path)}.diff`;
15310
16015
  f.diffPath = rel;
15311
- await writeFile8(join16(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
16016
+ await writeFile8(join17(dir, rel), f.diff.endsWith("\n") ? f.diff : `${f.diff}
15312
16017
  `, "utf8");
15313
16018
  }
15314
- await writeFile8(join16(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
16019
+ await writeFile8(join17(dir, "MANIFEST.md"), buildManifest(files, rangeLabel), "utf8");
15315
16020
  return { dir, fileCount: files.length };
15316
16021
  }
15317
16022
  var CHANGES_DIR, STATUS_LABEL;
@@ -15335,12 +16040,127 @@ var init_branchMarker = __esm({
15335
16040
  }
15336
16041
  });
15337
16042
 
16043
+ // src/daemon/localPrPoll.ts
16044
+ import { createHash as createHash3 } from "node:crypto";
16045
+ function observeGithubPull(p) {
16046
+ return {
16047
+ number: p.number,
16048
+ state: p.merged_at ? "merged" : p.state === "closed" ? "closed" : "open",
16049
+ headSha: p.head.sha,
16050
+ baseSha: p.base.sha,
16051
+ baseRef: p.base.ref,
16052
+ title: p.title ?? null,
16053
+ authorLogin: p.user?.login ?? null,
16054
+ body: p.body ?? null,
16055
+ createdAt: p.created_at ?? null,
16056
+ updatedAt: p.updated_at ?? null,
16057
+ mergedAt: p.merged_at ?? null,
16058
+ closedAt: p.closed_at ?? null
16059
+ };
16060
+ }
16061
+ function metaKey(o2) {
16062
+ return createHash3("sha256").update(JSON.stringify([o2.title, o2.body])).digest("hex").slice(0, 16);
16063
+ }
16064
+ function record(o2, prev, action) {
16065
+ const r = { head: o2.headSha, state: o2.state, meta: metaKey(o2) };
16066
+ const seq = action === "synchronize" ? (prev?.seq ?? 0) + 1 : prev?.seq;
16067
+ if (seq !== void 0) r.seq = seq;
16068
+ return r;
16069
+ }
16070
+ function decideAction(prev, o2) {
16071
+ if (!prev) return o2.state === "open" ? "opened" : null;
16072
+ const wasOpen = prev.state === "open";
16073
+ const isOpen = o2.state === "open";
16074
+ if (wasOpen && !isOpen) return "closed";
16075
+ if (!wasOpen && isOpen) return "reopened";
16076
+ if (isOpen && prev.head !== o2.headSha) return "synchronize";
16077
+ if (!isOpen && prev.state !== o2.state) return "edited";
16078
+ if (prev.meta !== void 0 && prev.meta !== metaKey(o2)) return "edited";
16079
+ return null;
16080
+ }
16081
+ function toItem(o2, action, sent) {
16082
+ return {
16083
+ number: o2.number,
16084
+ action,
16085
+ head_sha: o2.headSha,
16086
+ base_sha: o2.baseSha,
16087
+ base_ref: o2.baseRef,
16088
+ title: o2.title,
16089
+ author_login: o2.authorLogin,
16090
+ created_at: o2.createdAt,
16091
+ merged_at: o2.mergedAt,
16092
+ closed_at: o2.closedAt,
16093
+ body: o2.body,
16094
+ ...action === "synchronize" && sent.seq !== void 0 ? { seq: sent.seq } : {}
16095
+ };
16096
+ }
16097
+ function updatedMs(o2) {
16098
+ const t = o2.updatedAt ? Date.parse(o2.updatedAt) : NaN;
16099
+ return Number.isFinite(t) ? t : 0;
16100
+ }
16101
+ function planPrSync(input) {
16102
+ const { state, observations, nowMs } = input;
16103
+ const next = { prs: { ...state?.prs ?? {} } };
16104
+ const candidates = [];
16105
+ const backfill = !state || input.resumed ? new Set(
16106
+ 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)
16107
+ ) : null;
16108
+ for (const o2 of observations) {
16109
+ const prev = state?.prs[String(o2.number)];
16110
+ if (!prev && backfill) {
16111
+ if (backfill.has(o2.number)) candidates.push({ o: o2, action: "opened" });
16112
+ else if (o2.state === "open") next.prs[String(o2.number)] = record(o2);
16113
+ continue;
16114
+ }
16115
+ const action = decideAction(prev, o2);
16116
+ if (action) candidates.push({ o: o2, action, prev });
16117
+ }
16118
+ const ranked = [
16119
+ ...candidates.filter((c3) => c3.action !== "edited"),
16120
+ ...candidates.filter((c3) => c3.action === "edited")
16121
+ ].slice(0, MAX_PRS_PER_TICK);
16122
+ const send = [];
16123
+ for (const c3 of ranked) {
16124
+ const sent = record(c3.o, c3.prev, c3.action);
16125
+ next.prs[String(c3.o.number)] = sent;
16126
+ send.push(toItem(c3.o, c3.action, sent));
16127
+ }
16128
+ return { send, next };
16129
+ }
16130
+ function parsePrPollState(raw) {
16131
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
16132
+ const o2 = raw;
16133
+ const prs = {};
16134
+ if (o2.prs && typeof o2.prs === "object" && !Array.isArray(o2.prs)) {
16135
+ for (const [k, v] of Object.entries(o2.prs)) {
16136
+ if (!/^\d+$/.test(k) || !v || typeof v !== "object") continue;
16137
+ const r = v;
16138
+ if (typeof r.head !== "string" || !STATES.has(r.state)) continue;
16139
+ const entry = { head: r.head, state: r.state };
16140
+ if (typeof r.meta === "string") entry.meta = r.meta;
16141
+ if (Number.isInteger(r.seq) && r.seq > 0) entry.seq = r.seq;
16142
+ prs[k] = entry;
16143
+ }
16144
+ }
16145
+ return { prs };
16146
+ }
16147
+ var BACKFILL_WINDOW_MS, BACKFILL_MAX, MAX_PRS_PER_TICK, STATES;
16148
+ var init_localPrPoll = __esm({
16149
+ "src/daemon/localPrPoll.ts"() {
16150
+ "use strict";
16151
+ BACKFILL_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
16152
+ BACKFILL_MAX = 5;
16153
+ MAX_PRS_PER_TICK = 50;
16154
+ STATES = /* @__PURE__ */ new Set(["open", "merged", "closed"]);
16155
+ }
16156
+ });
16157
+
15338
16158
  // src/daemon/localRepos.ts
15339
- import { chmod as chmod2, mkdir as mkdir8, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
15340
- import { homedir as homedir4 } from "node:os";
15341
- import { dirname as dirname11, join as join17 } from "node:path";
16159
+ import { chmod as chmod3, mkdir as mkdir9, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
16160
+ import { homedir as homedir5 } from "node:os";
16161
+ import { dirname as dirname12, join as join18 } from "node:path";
15342
16162
  function localReposPath(env = process.env) {
15343
- return env.ARCHME_LOCAL_REPOS_PATH ?? join17(homedir4(), ".archme", "repos.json");
16163
+ return env.ARCHME_LOCAL_REPOS_PATH ?? join18(homedir5(), ".archme", "repos.json");
15344
16164
  }
15345
16165
  function isEntry(v) {
15346
16166
  if (!v || typeof v !== "object") return false;
@@ -15370,6 +16190,9 @@ function parseLocalRepos(raw) {
15370
16190
  if (typeof raw2.haltedAt === "string" && raw2.haltedAt) entry.haltedAt = raw2.haltedAt;
15371
16191
  if (typeof raw2.haltReason === "string" && raw2.haltReason) entry.haltReason = raw2.haltReason;
15372
16192
  if (typeof raw2.haltCode === "string" && raw2.haltCode) entry.haltCode = raw2.haltCode;
16193
+ const prPoll = parsePrPollState(raw2.prPoll);
16194
+ if (prPoll) entry.prPoll = prPoll;
16195
+ if (typeof raw2.prPausedAt === "string" && raw2.prPausedAt) entry.prPausedAt = raw2.prPausedAt;
15373
16196
  return entry;
15374
16197
  });
15375
16198
  }
@@ -15401,6 +16224,16 @@ function markHalted(repos, watchedRepoId, reason, at, code) {
15401
16224
  (r) => r.watchedRepoId === watchedRepoId ? { ...r, haltedAt: at, haltReason: reason, haltCode: code } : r
15402
16225
  );
15403
16226
  }
16227
+ function setPrPollState(repos, watchedRepoId, prPoll) {
16228
+ return repos.map((r) => {
16229
+ if (r.watchedRepoId !== watchedRepoId) return r;
16230
+ const { prPoll: _was, prPausedAt: _paused, ...rest } = r;
16231
+ return prPoll ? { ...rest, prPoll } : rest;
16232
+ });
16233
+ }
16234
+ function markPrPaused(repos, watchedRepoId, at) {
16235
+ return repos.map((r) => r.watchedRepoId === watchedRepoId ? { ...r, prPausedAt: at } : r);
16236
+ }
15404
16237
  function clearHalt(repos, watchedRepoId) {
15405
16238
  return repos.map((r) => {
15406
16239
  if (r.watchedRepoId !== watchedRepoId) return r;
@@ -15417,16 +16250,16 @@ function findLocalRepoForJob(repos, job) {
15417
16250
  }
15418
16251
  async function loadLocalRepos(env = process.env) {
15419
16252
  try {
15420
- return parseLocalRepos(await readFile11(localReposPath(env), "utf8"));
16253
+ return parseLocalRepos(await readFile12(localReposPath(env), "utf8"));
15421
16254
  } catch {
15422
16255
  return [];
15423
16256
  }
15424
16257
  }
15425
16258
  async function saveLocalRepos(repos, env = process.env) {
15426
16259
  const path7 = localReposPath(env);
15427
- await mkdir8(dirname11(path7), { recursive: true, mode: 448 });
16260
+ await mkdir9(dirname12(path7), { recursive: true, mode: 448 });
15428
16261
  await writeFile9(path7, serializeLocalRepos(repos), { mode: 384 });
15429
- await chmod2(path7, 384).catch(() => {
16262
+ await chmod3(path7, 384).catch(() => {
15430
16263
  });
15431
16264
  }
15432
16265
  async function registerLocalRepo(entry, env = process.env) {
@@ -15436,6 +16269,7 @@ var LOCAL_REPOS_VERSION, REQUIRED_KEYS;
15436
16269
  var init_localRepos = __esm({
15437
16270
  "src/daemon/localRepos.ts"() {
15438
16271
  "use strict";
16272
+ init_localPrPoll();
15439
16273
  LOCAL_REPOS_VERSION = 1;
15440
16274
  REQUIRED_KEYS = [
15441
16275
  "watchedRepoId",
@@ -15447,6 +16281,59 @@ var init_localRepos = __esm({
15447
16281
  }
15448
16282
  });
15449
16283
 
16284
+ // src/daemon/localPrJob.ts
16285
+ function assertLocalJobShape(job) {
16286
+ if (!SHA_RE.test(job.after_sha)) throw new LocalPrJobRejectedError(`after_sha ${job.after_sha} is not a 40-character git SHA`);
16287
+ if (!job.repo_branch || job.repo_branch.startsWith("-")) {
16288
+ throw new LocalPrJobRejectedError(`branch ${JSON.stringify(job.repo_branch)} is not a branch name`);
16289
+ }
16290
+ if (job.source_mode !== "pr_update") return;
16291
+ if (!job.before_sha || !SHA_RE.test(job.before_sha)) {
16292
+ throw new LocalPrJobRejectedError(`before_sha ${job.before_sha ?? "(null)"} is not a 40-character git SHA`);
16293
+ }
16294
+ if (!Number.isInteger(job.pr_number) || job.pr_number <= 0) {
16295
+ throw new LocalPrJobRejectedError(`PR number ${String(job.pr_number)} is not a pull request number`);
16296
+ }
16297
+ }
16298
+ async function verifyLocalPrJob(job, git4) {
16299
+ const n2 = job.pr_number;
16300
+ const before = job.before_sha;
16301
+ const base = `refs/remotes/origin/${job.repo_branch}`;
16302
+ const ref = `refs/archme/pull/${n2}/head`;
16303
+ const fetched = await git4(["fetch", "--no-tags", "origin", `+refs/pull/${n2}/head:${ref}`]);
16304
+ if (fetched.exitCode !== 0) throw new Error(`could not fetch PR #${n2}'s head from origin: ${fetched.stderr.trim()}`);
16305
+ const head = (await git4(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`])).stdout.trim();
16306
+ if (head !== job.after_sha) {
16307
+ throw new LocalPrJobRejectedError(
16308
+ `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`
16309
+ );
16310
+ }
16311
+ if ((await git4(["merge-base", "--is-ancestor", before, base])).exitCode !== 0) {
16312
+ throw new LocalPrJobRejectedError(`PR #${n2}'s base ${short(before)} is not on origin/${job.repo_branch}`);
16313
+ }
16314
+ const fromJob = (await git4(["merge-base", before, job.after_sha])).stdout.trim();
16315
+ const fromBranch = (await git4(["merge-base", base, job.after_sha])).stdout.trim();
16316
+ if (!fromJob || fromJob !== fromBranch) {
16317
+ throw new LocalPrJobRejectedError(
16318
+ `PR #${n2}'s base ${short(before)} is not where it branched from origin/${job.repo_branch} (${fromBranch ? short(fromBranch) : "no common commit"})`
16319
+ );
16320
+ }
16321
+ }
16322
+ var LocalPrJobRejectedError, SHA_RE, short;
16323
+ var init_localPrJob = __esm({
16324
+ "src/daemon/localPrJob.ts"() {
16325
+ "use strict";
16326
+ LocalPrJobRejectedError = class extends Error {
16327
+ constructor(message) {
16328
+ super(`refusing local job: ${message}`);
16329
+ this.name = "LocalPrJobRejectedError";
16330
+ }
16331
+ };
16332
+ SHA_RE = /^[0-9a-f]{40}$/;
16333
+ short = (sha) => sha.slice(0, 8);
16334
+ }
16335
+ });
16336
+
15450
16337
  // ../shared/commitAdvance.ts
15451
16338
  function planAdvance(input) {
15452
16339
  const { marker, upstreamTip, markerIsAncestorOfTip, tipIsAncestorOfMarker, firstParentShas } = input;
@@ -15479,21 +16366,21 @@ function decideGate(input) {
15479
16366
  return {
15480
16367
  action: "stop",
15481
16368
  code: "job-terminal",
15482
- reason: `job ${job.status} after ${job.attemptCount} attempt(s); marker did not advance \u2014 re-push commit ${short(targetSha)} (fresh delivery) or admin-requeue, do not fold forward`
16369
+ 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
16370
  };
15484
16371
  }
15485
16372
  if (!pushedNewRef && job === null && elapsedMs >= noopGraceMs) {
15486
16373
  return {
15487
16374
  action: "stop",
15488
16375
  code: "noop-push-no-job",
15489
- reason: `no webhook fired for ${short(targetSha)} (fork ref already there) and no job appeared in ${Math.round(noopGraceMs / 1e3)}s \u2014 likely a lost webhook or rolled-back job; re-push with a fresh delivery (ref-bounce) or admin-requeue`
16376
+ 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
16377
  };
15491
16378
  }
15492
16379
  if (elapsedMs >= timeoutMs) {
15493
16380
  return {
15494
16381
  action: "stop",
15495
16382
  code: "timeout",
15496
- reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${short(targetSha)} (job: ${job ? job.status : "not enqueued"})`
16383
+ reason: `timed out after ${Math.round(elapsedMs / 1e3)}s waiting for marker to reach ${short2(targetSha)} (job: ${job ? job.status : "not enqueued"})`
15497
16384
  };
15498
16385
  }
15499
16386
  return {
@@ -15502,7 +16389,7 @@ function decideGate(input) {
15502
16389
  reason: job ? `job ${job.status}` : "job not enqueued yet"
15503
16390
  };
15504
16391
  }
15505
- function short(sha) {
16392
+ function short2(sha) {
15506
16393
  return sha.slice(0, 7);
15507
16394
  }
15508
16395
  var init_commitGate = __esm({
@@ -15539,7 +16426,7 @@ function planPollTick(input) {
15539
16426
  case "upstream-behind":
15540
16427
  return {
15541
16428
  kind: "idle",
15542
- reason: `upstream is behind the marker (reset to ${short2(plan.tip)}); nothing to do`
16429
+ reason: `upstream is behind the marker (reset to ${short3(plan.tip)}); nothing to do`
15543
16430
  };
15544
16431
  case "force-push":
15545
16432
  target = plan.tip;
@@ -15571,15 +16458,15 @@ function planPollTick(input) {
15571
16458
  case "advance":
15572
16459
  return { kind: "idle", reason: decision.reason };
15573
16460
  case "wait":
15574
- return { kind: "idle", reason: `${short2(target)}: ${decision.reason}` };
16461
+ return { kind: "idle", reason: `${short3(target)}: ${decision.reason}` };
15575
16462
  }
15576
16463
  }
15577
16464
  function localHaltReason(decision, job, targetSha) {
15578
16465
  if (decision.code !== "job-terminal" || !job) return decision.reason;
15579
- return `commit ${short2(targetSha)} could not be processed (job ${job.status} after ${job.attemptCount} attempt(s)). Polling has stopped here rather than skipping it, because skipping would fold its changes into the next update.
16466
+ 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
16467
  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
16468
  }
15582
- function short2(sha) {
16469
+ function short3(sha) {
15583
16470
  return sha.slice(0, 7);
15584
16471
  }
15585
16472
  var init_localPoll = __esm({
@@ -15625,8 +16512,8 @@ __export(localGit_exports, {
15625
16512
  revParse: () => revParse,
15626
16513
  setOriginUrl: () => setOriginUrl
15627
16514
  });
15628
- import { mkdir as mkdir9 } from "node:fs/promises";
15629
- import { dirname as dirname12 } from "node:path";
16515
+ import { mkdir as mkdir10 } from "node:fs/promises";
16516
+ import { dirname as dirname13 } from "node:path";
15630
16517
  async function git(dir, args, opts = {}) {
15631
16518
  requireBinary("git");
15632
16519
  const res = await execa("git", args, {
@@ -15647,7 +16534,7 @@ async function git(dir, args, opts = {}) {
15647
16534
  return { exitCode, stdout: res.stdout || "", stderr: res.stderr || "" };
15648
16535
  }
15649
16536
  async function createMirrorClone(sourcePath, mirrorPath, originUrl) {
15650
- await mkdir9(dirname12(mirrorPath), { recursive: true });
16537
+ await mkdir10(dirname13(mirrorPath), { recursive: true });
15651
16538
  await git(null, ["clone", "--local", "--no-hardlinks", sourcePath, mirrorPath]);
15652
16539
  await git(mirrorPath, ["remote", "set-url", "origin", originUrl]);
15653
16540
  }
@@ -15719,6 +16606,244 @@ var init_localGit = __esm({
15719
16606
  }
15720
16607
  });
15721
16608
 
16609
+ // src/daemon/localPrAdapters.ts
16610
+ function parseCredentialFill(stdout) {
16611
+ for (const line of stdout.split("\n")) {
16612
+ if (line.startsWith("password=")) return line.slice("password=".length).trim() || null;
16613
+ }
16614
+ return null;
16615
+ }
16616
+ async function resolveGithubTokens(deps = {}) {
16617
+ const run = deps.run ?? defaultExec;
16618
+ const ghPath = deps.ghPath ?? (() => findOnPath("gh"));
16619
+ const tiers = [];
16620
+ const clean = (t) => t && /^\S+$/.test(t) ? t : null;
16621
+ if (ghPath()) {
16622
+ try {
16623
+ const r = await run("gh", ["auth", "token", "--hostname", "github.com"]);
16624
+ const token = r.exitCode === 0 ? clean(r.stdout.trim()) : null;
16625
+ if (token) tiers.push({ tier: "gh", token });
16626
+ } catch {
16627
+ }
16628
+ }
16629
+ try {
16630
+ const r = await run("git", ["credential", "fill"], {
16631
+ input: "protocol=https\nhost=github.com\n\n",
16632
+ env: {
16633
+ ...process.env,
16634
+ GIT_TERMINAL_PROMPT: "0",
16635
+ GCM_INTERACTIVE: "never",
16636
+ GIT_ASKPASS: "",
16637
+ SSH_ASKPASS: ""
16638
+ }
16639
+ });
16640
+ const token = r.exitCode === 0 ? clean(parseCredentialFill(r.stdout)) : null;
16641
+ if (token) tiers.push({ tier: "git-credential", token });
16642
+ } catch {
16643
+ }
16644
+ tiers.push({ tier: "none", token: null });
16645
+ return tiers;
16646
+ }
16647
+ async function listGithubPulls(args) {
16648
+ const f = args.fetch ?? fetch;
16649
+ const headers = {
16650
+ Accept: "application/vnd.github+json",
16651
+ "X-GitHub-Api-Version": "2022-11-28",
16652
+ "User-Agent": "archme-cli"
16653
+ };
16654
+ if (args.token) headers.Authorization = `Bearer ${args.token}`;
16655
+ if (args.etag) headers["If-None-Match"] = args.etag;
16656
+ const url = `https://api.github.com/repos/${encodeURIComponent(args.owner)}/${encodeURIComponent(args.name)}/pulls?state=all&sort=updated&direction=desc&per_page=100`;
16657
+ const resp = await f(url, { headers, signal: AbortSignal.timeout(3e4) });
16658
+ if (resp.status === 304) return { kind: "not-modified" };
16659
+ if (resp.status === 429 || resp.status === 403 && resp.headers.get("x-ratelimit-remaining") === "0") {
16660
+ const reset2 = Number(resp.headers.get("x-ratelimit-reset"));
16661
+ return { kind: "rate-limited", untilMs: Number.isFinite(reset2) && reset2 > 0 ? reset2 * 1e3 : Date.now() + 60 * 6e4 };
16662
+ }
16663
+ if (resp.status === 401 || resp.status === 403 || resp.status === 404) {
16664
+ return { kind: "denied", status: resp.status };
16665
+ }
16666
+ if (!resp.ok) throw new Error(`GitHub pulls list returned ${resp.status}`);
16667
+ const body = await resp.json();
16668
+ if (!Array.isArray(body)) throw new Error("GitHub pulls list was not an array");
16669
+ return { kind: "ok", pulls: body, etag: resp.headers.get("etag") };
16670
+ }
16671
+ function rateLimitedReason(untilMs) {
16672
+ return `GitHub's API rate limit is used up until ${new Date(untilMs).toISOString()}`;
16673
+ }
16674
+ async function listPrsFromGithub(args) {
16675
+ const key = `${args.owner}/${args.name}`.toLowerCase();
16676
+ const c3 = args.cache.get(key) ?? { denied: {} };
16677
+ args.cache.set(key, c3);
16678
+ const ok = (obs) => ({
16679
+ kind: "ok",
16680
+ observations: obs.filter((o2) => o2.baseRef === args.baseRef)
16681
+ });
16682
+ const bestTier = args.tiers.find((t) => (c3.denied[t.tier] ?? 0) <= args.nowMs)?.tier;
16683
+ if (bestTier === "none" && c3.tier === "none" && c3.observations && args.nowMs - (c3.fetchedAtMs ?? 0) < UNAUTHENTICATED_MIN_INTERVAL_MS) {
16684
+ return ok(c3.observations);
16685
+ }
16686
+ if (c3.rateLimitedUntil && args.nowMs < c3.rateLimitedUntil) {
16687
+ return { kind: "unavailable", reason: rateLimitedReason(c3.rateLimitedUntil) };
16688
+ }
16689
+ for (const t of args.tiers) {
16690
+ if ((c3.denied[t.tier] ?? 0) > args.nowMs) continue;
16691
+ let r;
16692
+ try {
16693
+ r = await listGithubPulls({
16694
+ owner: args.owner,
16695
+ name: args.name,
16696
+ token: t.token,
16697
+ etag: c3.tier === t.tier && c3.observations ? c3.etag ?? null : null,
16698
+ fetch: args.fetch
16699
+ });
16700
+ } catch (err2) {
16701
+ return { kind: "unavailable", reason: `GitHub's API could not be reached (${err2.message})` };
16702
+ }
16703
+ if (r.kind === "ok") {
16704
+ c3.tier = t.tier;
16705
+ c3.etag = r.etag;
16706
+ c3.observations = r.pulls.map(observeGithubPull);
16707
+ c3.fetchedAtMs = args.nowMs;
16708
+ return ok(c3.observations);
16709
+ }
16710
+ if (r.kind === "not-modified" && c3.observations) {
16711
+ c3.fetchedAtMs = args.nowMs;
16712
+ return ok(c3.observations);
16713
+ }
16714
+ if (r.kind === "rate-limited") {
16715
+ c3.rateLimitedUntil = r.untilMs;
16716
+ return { kind: "unavailable", reason: rateLimitedReason(r.untilMs) };
16717
+ }
16718
+ if (r.kind === "denied") c3.denied[t.tier] = args.nowMs + DENY_BACKOFF_MS;
16719
+ }
16720
+ return { kind: "unavailable", reason: DENIED_REASON };
16721
+ }
16722
+ async function postLocalPrSync(client, payload) {
16723
+ const { data, error } = await client.functions.invoke("github/local-pr-sync", { body: payload });
16724
+ if (error) {
16725
+ let detail = error.message;
16726
+ const ctx = error.context;
16727
+ if (ctx instanceof Response) {
16728
+ const body = await ctx.json().catch(() => null);
16729
+ if (typeof body?.error === "string") detail = `${ctx.status} ${body.error}`;
16730
+ }
16731
+ throw new Error(`local-pr-sync: ${detail}`);
16732
+ }
16733
+ const results = data?.results;
16734
+ if (!Array.isArray(results)) throw new Error("local-pr-sync: malformed response");
16735
+ return results;
16736
+ }
16737
+ async function pollPrsOnce(entry, deps) {
16738
+ const row = await deps.readWatchedRepo(entry.watchedRepoId);
16739
+ if (row && row.prUpdateMode !== "auto") {
16740
+ if (entry.prPoll && !entry.prPausedAt) await deps.pause(entry, new Date(deps.now()).toISOString());
16741
+ return;
16742
+ }
16743
+ if (!row || row.sourceKind !== "local" || !row.enabled) return;
16744
+ const repo = `${entry.repoOwner}/${entry.repoName}`;
16745
+ const sameRepo = row.repoOwner?.toLowerCase() === entry.repoOwner.toLowerCase() && row.repoName?.toLowerCase() === entry.repoName.toLowerCase();
16746
+ if (!sameRepo) {
16747
+ deps.logOnChange(
16748
+ `repo-mismatch:${entry.watchedRepoId}`,
16749
+ true,
16750
+ ` ${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.`
16751
+ );
16752
+ return;
16753
+ }
16754
+ const listing = await deps.listPrs(entry, row);
16755
+ const unavailableKey = `pr-listing:${entry.watchedRepoId}`;
16756
+ if (listing.kind === "unavailable") {
16757
+ deps.logOnChange(
16758
+ unavailableKey,
16759
+ true,
16760
+ ` ${repo}: PR reviews on hold \u2014 ${listing.reason}.`
16761
+ );
16762
+ return;
16763
+ }
16764
+ deps.logOnChange(unavailableKey, false, ` ${repo}: PR reviews resumed \u2014 GitHub's API answered again.`);
16765
+ const resumed = entry.prPausedAt !== void 0;
16766
+ const plan = planPrSync({ state: entry.prPoll, observations: listing.observations, nowMs: deps.now(), resumed });
16767
+ if (plan.send.length > 0) {
16768
+ const results = await deps.send(entry, plan.send);
16769
+ const kinds = new Map(results.map((r) => [r.number, r.kind]));
16770
+ for (const item of plan.send) {
16771
+ const kind = kinds.get(item.number);
16772
+ if (kind !== void 0) {
16773
+ deps.log(` ${repo}: PR #${item.number} ${item.action} \u2192 ${kind}`);
16774
+ }
16775
+ if (kind !== void 0 && !UNDELIVERED.has(kind)) continue;
16776
+ const key = String(item.number);
16777
+ const prev = entry.prPoll?.prs[key];
16778
+ if (prev) plan.next.prs[key] = prev;
16779
+ else delete plan.next.prs[key];
16780
+ }
16781
+ } else if (!resumed && JSON.stringify(plan.next) === JSON.stringify(entry.prPoll)) {
16782
+ return;
16783
+ }
16784
+ await deps.saveState(entry, plan.next);
16785
+ }
16786
+ function makePrPollDeps(client, log, readWatchedRepo, io = {}) {
16787
+ const cache2 = /* @__PURE__ */ new Map();
16788
+ const on6 = /* @__PURE__ */ new Set();
16789
+ return {
16790
+ readWatchedRepo,
16791
+ log,
16792
+ logOnChange(key, value, msg) {
16793
+ if (on6.has(key) === value) return;
16794
+ if (value) on6.add(key);
16795
+ else on6.delete(key);
16796
+ log(msg);
16797
+ },
16798
+ now: () => Date.now(),
16799
+ async listPrs(entry, row) {
16800
+ return listPrsFromGithub({
16801
+ owner: entry.repoOwner,
16802
+ name: entry.repoName,
16803
+ baseRef: row.repoBranch,
16804
+ tiers: await (io.tiers ?? resolveGithubTokens)(),
16805
+ cache: cache2,
16806
+ nowMs: Date.now(),
16807
+ fetch: io.fetch
16808
+ });
16809
+ },
16810
+ send: (entry, items) => postLocalPrSync(client, { watched_repo_id: entry.watchedRepoId, prs: items }),
16811
+ // Read-modify-write, as halts are: `local-watch` may have changed the
16812
+ // registry since this tick read it.
16813
+ async saveState(entry, state) {
16814
+ await saveLocalRepos(setPrPollState(await loadLocalRepos(), entry.watchedRepoId, state));
16815
+ },
16816
+ async pause(entry, at) {
16817
+ await saveLocalRepos(markPrPaused(await loadLocalRepos(), entry.watchedRepoId, at));
16818
+ }
16819
+ };
16820
+ }
16821
+ var TOKEN_TIMEOUT_MS, defaultExec, DENY_BACKOFF_MS, UNAUTHENTICATED_MIN_INTERVAL_MS, DENIED_REASON, UNDELIVERED;
16822
+ var init_localPrAdapters = __esm({
16823
+ "src/daemon/localPrAdapters.ts"() {
16824
+ "use strict";
16825
+ init_execa();
16826
+ init_preflight();
16827
+ init_localPrPoll();
16828
+ init_localRepos();
16829
+ TOKEN_TIMEOUT_MS = 15e3;
16830
+ defaultExec = async (cmd, args, opts) => {
16831
+ const r = await execa(cmd, args, {
16832
+ reject: false,
16833
+ timeout: TOKEN_TIMEOUT_MS,
16834
+ env: opts?.env,
16835
+ input: opts?.input,
16836
+ stdin: opts?.input === void 0 ? "ignore" : void 0
16837
+ });
16838
+ return { exitCode: r.exitCode ?? 1, stdout: typeof r.stdout === "string" ? r.stdout : "" };
16839
+ };
16840
+ DENY_BACKOFF_MS = 60 * 6e4;
16841
+ UNAUTHENTICATED_MIN_INTERVAL_MS = 5 * 6e4;
16842
+ 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)";
16843
+ UNDELIVERED = /* @__PURE__ */ new Set(["error", "invalid"]);
16844
+ }
16845
+ });
16846
+
15722
16847
  // src/daemon/localPoller.ts
15723
16848
  async function pollRepoOnce(entry, deps) {
15724
16849
  const repo = `${entry.repoOwner}/${entry.repoName}`;
@@ -15786,9 +16911,9 @@ async function pollRepoOnce(entry, deps) {
15786
16911
  if (decision.kind !== "enqueue") return decision;
15787
16912
  const result = await deps.enqueue(entry, row, decision.beforeSha, decision.afterSha);
15788
16913
  if (result.dedup) {
15789
- deps.log(` ${repo}: ${short3(decision.afterSha)} already enqueued elsewhere`);
16914
+ deps.log(` ${repo}: ${short4(decision.afterSha)} already enqueued elsewhere`);
15790
16915
  } else {
15791
- deps.log(` ${repo}: enqueued ${short3(decision.afterSha)} (job ${result.jobId ?? "?"})`);
16916
+ deps.log(` ${repo}: enqueued ${short4(decision.afterSha)} (job ${result.jobId ?? "?"})`);
15792
16917
  }
15793
16918
  return decision;
15794
16919
  }
@@ -15809,15 +16934,23 @@ async function pollAllRepos(repos, deps) {
15809
16934
  When the cause is fixed: archme local-watch --resume ${entry.repoOwner}/${entry.repoName}
15810
16935
  `
15811
16936
  );
16937
+ continue;
15812
16938
  }
15813
16939
  } catch (err2) {
15814
16940
  deps.log(` local poll ${key} failed (will retry): ${err2.message}`);
16941
+ continue;
16942
+ }
16943
+ if (!deps.pollPrs) continue;
16944
+ try {
16945
+ await deps.pollPrs(entry);
16946
+ } catch (err2) {
16947
+ deps.log(` PR poll ${key} failed (will retry): ${err2.message}`);
15815
16948
  }
15816
16949
  }
15817
16950
  }
15818
16951
  function makePollerDeps(client, log, gateTimeoutMs) {
15819
16952
  const firstSeen = /* @__PURE__ */ new Map();
15820
- return {
16953
+ const deps = {
15821
16954
  git: localGit_exports,
15822
16955
  log,
15823
16956
  now: () => Date.now(),
@@ -15835,7 +16968,7 @@ function makePollerDeps(client, log, gateTimeoutMs) {
15835
16968
  );
15836
16969
  },
15837
16970
  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();
16971
+ 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
16972
  if (error || !data) return null;
15840
16973
  const r = data;
15841
16974
  return {
@@ -15845,7 +16978,10 @@ function makePollerDeps(client, log, gateTimeoutMs) {
15845
16978
  // branch`), so both producers resolve the same lane for the same row.
15846
16979
  archmeBranch: r.commit_update_archme_branch ?? r.branch,
15847
16980
  sourceKind: r.source_kind,
15848
- enabled: r.enabled
16981
+ enabled: r.enabled,
16982
+ prUpdateMode: r.pr_update_mode ?? "disabled",
16983
+ repoOwner: r.repo_owner,
16984
+ repoName: r.repo_name
15849
16985
  };
15850
16986
  },
15851
16987
  // Shared with the job runner and local-watch's readiness check: three
@@ -15883,8 +17019,11 @@ function makePollerDeps(client, log, gateTimeoutMs) {
15883
17019
  return data ?? {};
15884
17020
  }
15885
17021
  };
17022
+ const prDeps = makePrPollDeps(client, log, (id) => deps.readWatchedRepo(id));
17023
+ deps.pollPrs = (entry) => pollPrsOnce(entry, prDeps);
17024
+ return deps;
15886
17025
  }
15887
- function short3(sha) {
17026
+ function short4(sha) {
15888
17027
  return sha.slice(0, 7);
15889
17028
  }
15890
17029
  var DEFAULT_POLL_INTERVAL_MS, DEFAULT_GATE_TIMEOUT_MS;
@@ -15895,6 +17034,7 @@ var init_localPoller = __esm({
15895
17034
  init_localRepos();
15896
17035
  init_branchMarker();
15897
17036
  init_localGit();
17037
+ init_localPrAdapters();
15898
17038
  DEFAULT_POLL_INTERVAL_MS = 6e4;
15899
17039
  DEFAULT_GATE_TIMEOUT_MS = 4 * 60 * 60 * 1e3;
15900
17040
  }
@@ -15921,10 +17061,10 @@ __export(run_exports, {
15921
17061
  });
15922
17062
  import { createClient } from "@supabase/supabase-js";
15923
17063
  import { randomUUID } from "node:crypto";
15924
- import { mkdtemp as mkdtemp3, mkdir as mkdir10, writeFile as writeFile10, rm as rm6, stat as stat8, chmod as chmod3 } from "node:fs/promises";
17064
+ import { mkdtemp as mkdtemp3, mkdir as mkdir11, writeFile as writeFile10, rm as rm7, stat as stat8, chmod as chmod4 } from "node:fs/promises";
15925
17065
  import { tmpdir as tmpdir3 } from "node:os";
15926
- import { join as join18 } from "node:path";
15927
- import { homedir as homedir5 } from "node:os";
17066
+ import { join as join19 } from "node:path";
17067
+ import { homedir as homedir6 } from "node:os";
15928
17068
  function attributeKillCause(result, recorded) {
15929
17069
  if (recorded) return recorded;
15930
17070
  if (result.signal === "SIGTERM") return "timeout";
@@ -15955,7 +17095,7 @@ async function lookupProjectSlug(client, projectId) {
15955
17095
  return `${orgSlug}/${data.slug}`;
15956
17096
  }
15957
17097
  function cloneDirFor(baseDir, owner, name) {
15958
- return join18(baseDir, owner, name);
17098
+ return join19(baseDir, owner, name);
15959
17099
  }
15960
17100
  async function exists(p) {
15961
17101
  try {
@@ -16000,8 +17140,8 @@ async function getRepoCredentials(ctx, jobId, deviceId) {
16000
17140
  };
16001
17141
  }
16002
17142
  async function buildGitEnv(credentials) {
16003
- const dir = await mkdtemp3(join18(tmpdir3(), "archme-git-askpass-"));
16004
- const askpassPath = join18(dir, "askpass.sh");
17143
+ const dir = await mkdtemp3(join19(tmpdir3(), "archme-git-askpass-"));
17144
+ const askpassPath = join19(dir, "askpass.sh");
16005
17145
  await writeFile10(
16006
17146
  askpassPath,
16007
17147
  [
@@ -16015,7 +17155,7 @@ async function buildGitEnv(credentials) {
16015
17155
  ].join("\n"),
16016
17156
  { mode: 448 }
16017
17157
  );
16018
- await chmod3(askpassPath, 448).catch(() => {
17158
+ await chmod4(askpassPath, 448).catch(() => {
16019
17159
  });
16020
17160
  const gitConfigEntries = [
16021
17161
  ["credential.helper", ""],
@@ -16037,23 +17177,32 @@ async function buildGitEnv(credentials) {
16037
17177
  GIT_USERNAME: credentials.username,
16038
17178
  GIT_PASSWORD: credentials.password
16039
17179
  };
16040
- return { env, cleanup: () => rm6(dir, { recursive: true, force: true }).catch(() => {
17180
+ return { env, cleanup: () => rm7(dir, { recursive: true, force: true }).catch(() => {
16041
17181
  }) };
16042
17182
  }
16043
17183
  async function ensureLocalCloneAndFetch(job, entry) {
16044
17184
  const dir = entry.mirrorPath;
17185
+ assertLocalJobShape(job);
16045
17186
  await cloneMutex.runExclusive(dir, async () => {
16046
- if (!await exists(join18(dir, ".git"))) {
17187
+ if (!await exists(join19(dir, ".git"))) {
16047
17188
  throw new Error(
16048
17189
  `local mirror ${dir} is missing or not a git repo \u2014 re-run \`archme init ${entry.sourcePath}\``
16049
17190
  );
16050
17191
  }
16051
17192
  await execa("git", ["fetch", "--no-tags", "origin", job.repo_branch], { cwd: dir, reject: false });
17193
+ if (job.source_mode === "pr_update") {
17194
+ await verifyLocalPrJob(job, async (args) => {
17195
+ const r = await execa("git", args, { cwd: dir, reject: false });
17196
+ return { exitCode: r.exitCode ?? 1, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") };
17197
+ });
17198
+ return;
17199
+ }
16052
17200
  const afterCheck = await execa("git", ["cat-file", "-e", job.after_sha], { cwd: dir, reject: false });
16053
17201
  if (afterCheck.exitCode !== 0) {
16054
17202
  await execa("git", ["fetch", "origin"], { cwd: dir, reject: false });
16055
17203
  }
16056
17204
  }).catch((err2) => {
17205
+ if (err2 instanceof LocalPrJobRejectedError) throw err2;
16057
17206
  const msg = err2 instanceof Error ? err2.message : String(err2);
16058
17207
  throw new Error(`local repo ${job.repo_owner}/${job.repo_name} is not usable: ${msg}`);
16059
17208
  });
@@ -16063,15 +17212,16 @@ async function ensureLocalCloneAndFetch(job, entry) {
16063
17212
  async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
16064
17213
  const localEntry = findLocalRepoForJob(await loadLocalRepos(), job);
16065
17214
  if (localEntry) return await ensureLocalCloneAndFetch(job, localEntry);
17215
+ assertLocalJobShape(job);
16066
17216
  const deviceId = claimDeviceId ?? ctx.deviceForOrg(job.org_id) ?? ctx.auth.getDeviceId();
16067
17217
  if (!deviceId) throw new Error("daemon is not signed in with a registered device");
16068
17218
  const credentials = await getRepoCredentials(ctx, job.id, deviceId);
16069
- await mkdir10(ctx.cloneBaseDir, { recursive: true });
17219
+ await mkdir11(ctx.cloneBaseDir, { recursive: true });
16070
17220
  const dir = cloneDirFor(ctx.cloneBaseDir, job.repo_owner, job.repo_name);
16071
17221
  const { env, cleanup: cleanupGitEnv } = await buildGitEnv(credentials);
16072
17222
  await cloneMutex.runExclusive(dir, async () => {
16073
- if (!await exists(join18(dir, ".git"))) {
16074
- await mkdir10(join18(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
17223
+ if (!await exists(join19(dir, ".git"))) {
17224
+ await mkdir11(join19(ctx.cloneBaseDir, job.repo_owner), { recursive: true });
16075
17225
  console.log(` cloning ${job.repo_owner}/${job.repo_name} \u2192 ${dir}`);
16076
17226
  await execa("git", ["clone", "--no-tags", credentials.https_url, dir], { env, reject: true });
16077
17227
  } else {
@@ -16100,12 +17250,12 @@ async function ensureCloneAndFetch(ctx, job, claimDeviceId) {
16100
17250
  return { dir, gitEnv: env, cleanupGitEnv };
16101
17251
  }
16102
17252
  async function createJobWorktree(repoDir, job) {
16103
- const parentDir = await mkdtemp3(join18(tmpdir3(), "archme-job-worktree-"));
16104
- const worktreeDir = join18(parentDir, "repo");
17253
+ const parentDir = await mkdtemp3(join19(tmpdir3(), "archme-job-worktree-"));
17254
+ const worktreeDir = join19(parentDir, "repo");
16105
17255
  try {
16106
17256
  await cloneMutex.runExclusive(repoDir, () => execa("git", ["worktree", "add", "--detach", worktreeDir, job.after_sha], { cwd: repoDir, reject: true }));
16107
17257
  } catch (err2) {
16108
- await rm6(parentDir, { recursive: true, force: true }).catch(() => {
17258
+ await rm7(parentDir, { recursive: true, force: true }).catch(() => {
16109
17259
  });
16110
17260
  throw err2;
16111
17261
  }
@@ -16116,7 +17266,7 @@ async function createJobWorktree(repoDir, job) {
16116
17266
  await execa("git", ["worktree", "remove", "--force", worktreeDir], { cwd: repoDir, reject: false });
16117
17267
  await execa("git", ["worktree", "prune"], { cwd: repoDir, reject: false });
16118
17268
  });
16119
- await rm6(parentDir, { recursive: true, force: true });
17269
+ await rm7(parentDir, { recursive: true, force: true });
16120
17270
  }
16121
17271
  };
16122
17272
  }
@@ -16696,7 +17846,28 @@ async function runJob(ctx, job, claimDeviceId) {
16696
17846
  }, HEARTBEAT_INTERVAL_MS);
16697
17847
  try {
16698
17848
  const projectSlug = await lookupProjectSlug(ctx.supabase, job.project_id);
16699
- const { dir: repoDir, gitEnv, cleanupGitEnv } = await ensureCloneAndFetch(ctx, job, deviceId);
17849
+ let clone;
17850
+ try {
17851
+ clone = await ensureCloneAndFetch(ctx, job, deviceId);
17852
+ } catch (err2) {
17853
+ if (!(err2 instanceof LocalPrJobRejectedError)) throw err2;
17854
+ log(`${err2.message} \u2014 completing no_op without advancing the marker`);
17855
+ markPhase("clone_fetch", { mode: "rejected" });
17856
+ await rpcComplete(
17857
+ ctx,
17858
+ job.id,
17859
+ deviceId,
17860
+ err2.message,
17861
+ "no_op",
17862
+ false,
17863
+ { mode: "rejected", durations_ms: timings },
17864
+ { stdout: "", stderr: "" },
17865
+ false
17866
+ /* advance_marker */
17867
+ );
17868
+ return;
17869
+ }
17870
+ const { dir: repoDir, gitEnv, cleanupGitEnv } = clone;
16700
17871
  cleanupFns.push(cleanupGitEnv);
16701
17872
  activeCleanups.add(cleanupGitEnv);
16702
17873
  markPhase("clone_fetch");
@@ -16807,7 +17978,7 @@ async function runJob(ctx, job, claimDeviceId) {
16807
17978
  affectedForComplete = affected;
16808
17979
  const symbolsInFile = async (file) => {
16809
17980
  log(`parse ${file} (removed-symbol scan)`);
16810
- const outline = await runOutline(join18(worktreeDir, file));
17981
+ const outline = await runOutline(join19(worktreeDir, file));
16811
17982
  if (outline.tier !== 2) return null;
16812
17983
  return outline.data.symbols.map((s) => s.name);
16813
17984
  };
@@ -17424,6 +18595,7 @@ var init_run = __esm({
17424
18595
  init_branchMarker();
17425
18596
  init_branchMarker();
17426
18597
  init_localRepos();
18598
+ init_localPrJob();
17427
18599
  init_localPoller();
17428
18600
  HEARTBEAT_INTERVAL_MS = 6e4;
17429
18601
  CLAUDE_TIMEOUT_MS = 30 * 60 * 1e3;
@@ -17445,7 +18617,7 @@ var init_run = __esm({
17445
18617
  MAX_CONCURRENT_JOBS = Math.max(1, Number(process.env.MAX_CONCURRENT_JOBS) || 3);
17446
18618
  ctxByJob = /* @__PURE__ */ new Map();
17447
18619
  deviceByJob = /* @__PURE__ */ new Map();
17448
- DEFAULT_CLONE_BASE_DIR = `${homedir5()}/.archme-job-runner/clones`;
18620
+ DEFAULT_CLONE_BASE_DIR = `${homedir6()}/.archme-job-runner/clones`;
17449
18621
  INLINE_PATCH_BYTE_CAP = 2e5;
17450
18622
  GIT_SHA_RE = /^[0-9a-f]{40}$/;
17451
18623
  CURATOR_PREAMBLE = `## Your role
@@ -17573,6 +18745,7 @@ ${ANCHOR_VERIFICATION}`;
17573
18745
  claimedDeviceForJob(jobId) {
17574
18746
  return deviceByJob.get(jobId);
17575
18747
  },
18748
+ runJob,
17576
18749
  // Exposes the clone/fetch entrypoint so a test can assert that the repo-token
17577
18750
  // mint (getRepoCredentials -> agents-device/repo-token, gated on
17578
18751
  // claimed_by_device_id) uses the CLAIM-time device rather than recomputing via
@@ -17622,7 +18795,7 @@ var dispatch_exports2 = {};
17622
18795
  __export(dispatch_exports2, {
17623
18796
  dispatchDaemon: () => dispatchDaemon
17624
18797
  });
17625
- import { rm as rm7 } from "node:fs/promises";
18798
+ import { rm as rm8 } from "node:fs/promises";
17626
18799
  async function dispatchDaemon(route2) {
17627
18800
  switch (route2.action) {
17628
18801
  case "help":
@@ -17720,7 +18893,7 @@ async function dispatchDaemon(route2) {
17720
18893
  await orchestrateStop(c3);
17721
18894
  await c3.uninstall();
17722
18895
  if (route2.purge) {
17723
- await rm7(resolveLogDir(process.platform, process.env), { recursive: true, force: true });
18896
+ await rm8(resolveLogDir(process.platform, process.env), { recursive: true, force: true });
17724
18897
  }
17725
18898
  console.log(
17726
18899
  route2.purge ? "Uninstalled. Logs purged." : "Uninstalled. Logs preserved (use --purge to remove)."
@@ -17761,8 +18934,8 @@ async function defaultLoadSupabaseState(_deviceId) {
17761
18934
  async function defaultLoadUptime(pid) {
17762
18935
  if (process.platform === "linux") {
17763
18936
  try {
17764
- const { readFile: readFile14 } = await import("node:fs/promises");
17765
- const stat11 = await readFile14(`/proc/${pid}/stat`, "utf8");
18937
+ const { readFile: readFile16 } = await import("node:fs/promises");
18938
+ const stat11 = await readFile16(`/proc/${pid}/stat`, "utf8");
17766
18939
  const parts = stat11.split(") ").pop()?.split(" ") ?? [];
17767
18940
  const ticks = Number(parts[19]);
17768
18941
  if (!Number.isFinite(ticks)) return null;
@@ -17818,7 +18991,47 @@ var init_dispatch2 = __esm({
17818
18991
  }
17819
18992
  });
17820
18993
 
18994
+ // src/auth/identity.ts
18995
+ function decodeJwtClaims(jwt) {
18996
+ const segment = jwt?.split(".")[1];
18997
+ if (!segment) return null;
18998
+ try {
18999
+ const parsed = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
19000
+ return parsed && typeof parsed === "object" ? parsed : null;
19001
+ } catch {
19002
+ return null;
19003
+ }
19004
+ }
19005
+ function str(v) {
19006
+ return typeof v === "string" && v.trim() ? v.trim() : void 0;
19007
+ }
19008
+ function describeAccount(input) {
19009
+ const meta = input.claims?.user_metadata ?? {};
19010
+ const email = str(input.email) ?? str(input.claims?.email);
19011
+ const name = str(meta.name) ?? str(meta.full_name);
19012
+ const keyed = input.kind === "key" || isKeyedAccountEmail(email) || input.claims?.app_metadata?.account_kind === "key";
19013
+ if (keyed && email) {
19014
+ const handle = email.slice(0, email.indexOf("@"));
19015
+ return name ? `${name} (keyed account ${handle})` : `keyed account ${handle}`;
19016
+ }
19017
+ const login = str(meta.user_name) ?? str(meta.preferred_username);
19018
+ if (login) return `${name ?? email ?? login} (GitHub @${login})`;
19019
+ return email ?? str(input.sub) ?? str(input.claims?.sub) ?? "unknown account";
19020
+ }
19021
+ var init_identity = __esm({
19022
+ "src/auth/identity.ts"() {
19023
+ "use strict";
19024
+ init_accountKey();
19025
+ }
19026
+ });
19027
+
17821
19028
  // src/scan/authedClient.ts
19029
+ var authedClient_exports = {};
19030
+ __export(authedClient_exports, {
19031
+ SCAN_CLAUDE_TIMEOUT_MS: () => SCAN_CLAUDE_TIMEOUT_MS,
19032
+ SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS: () => SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS,
19033
+ createAuthedClient: () => createAuthedClient
19034
+ });
17822
19035
  import { createClient as createClient2 } from "@supabase/supabase-js";
17823
19036
  async function createAuthedClient(opts) {
17824
19037
  initBearer({
@@ -17844,7 +19057,13 @@ async function createAuthedClient(opts) {
17844
19057
  supabase,
17845
19058
  userId,
17846
19059
  orgId: getCurrentOrgId(),
17847
- identity: `${loaded.user.email ?? "OAuth user"} (${loaded.user.sub ?? "unknown"})`
19060
+ identity: describeAccount({
19061
+ kind: loaded.kind,
19062
+ email: loaded.user.email,
19063
+ sub: loaded.user.sub,
19064
+ claims: decodeJwtClaims(getCurrentAccessToken())
19065
+ }),
19066
+ accountKind: loaded.kind ?? "oauth"
17848
19067
  };
17849
19068
  }
17850
19069
  var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS;
@@ -17852,12 +19071,18 @@ var init_authedClient = __esm({
17852
19071
  "src/scan/authedClient.ts"() {
17853
19072
  "use strict";
17854
19073
  init_bearer();
19074
+ init_identity();
17855
19075
  SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
17856
19076
  SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
17857
19077
  }
17858
19078
  });
17859
19079
 
17860
19080
  // src/scan/web-url.ts
19081
+ var web_url_exports = {};
19082
+ __export(web_url_exports, {
19083
+ buildWebUrl: () => buildWebUrl,
19084
+ deriveWebOrigin: () => deriveWebOrigin
19085
+ });
17861
19086
  function deriveWebOrigin(apiUrl, onMalformed = "throw") {
17862
19087
  let parsed;
17863
19088
  try {
@@ -17883,6 +19108,277 @@ var init_web_url = __esm({
17883
19108
  }
17884
19109
  });
17885
19110
 
19111
+ // src/account/open.ts
19112
+ var open_exports = {};
19113
+ __export(open_exports, {
19114
+ boardPathFromMcpJson: () => boardPathFromMcpJson,
19115
+ buildKeyedOpenUrl: () => buildKeyedOpenUrl,
19116
+ defaultOpenBrowser: () => defaultOpenBrowser,
19117
+ defaultReadMcpJson: () => defaultReadMcpJson,
19118
+ dispatchOpen: () => dispatchOpen,
19119
+ freshAccessToken: () => freshAccessToken,
19120
+ postBoardLinkWith: () => postBoardLinkWith,
19121
+ runOpen: () => runOpen,
19122
+ safeNextPath: () => safeNextPath
19123
+ });
19124
+ import { readFile as readFile13 } from "node:fs/promises";
19125
+ import { join as join20 } from "node:path";
19126
+ function safeNextPath(path7) {
19127
+ if (!path7 || !path7.startsWith("/") || path7.startsWith("//") || path7.startsWith("/\\")) return "/";
19128
+ return path7;
19129
+ }
19130
+ function boardPathFromMcpJson(content) {
19131
+ if (!content) return "/";
19132
+ try {
19133
+ const servers = JSON.parse(content).mcpServers ?? {};
19134
+ const entry = Object.entries(servers).find(([name]) => /archme/i.test(name))?.[1];
19135
+ if (typeof entry?.url !== "string") return "/";
19136
+ const params = new URL(entry.url).searchParams;
19137
+ const [org, slug, ...rest] = (params.get("project") ?? "").split("/");
19138
+ if (!org || !slug || rest.length) return "/";
19139
+ const branch = params.get("branch");
19140
+ const segments = [org, slug, ...branch ? [branch] : []].map(encodeURIComponent);
19141
+ return `/a/${segments.join("/")}`;
19142
+ } catch {
19143
+ return "/";
19144
+ }
19145
+ }
19146
+ function buildKeyedOpenUrl(webOrigin, link, next) {
19147
+ const fragment = new URLSearchParams({
19148
+ token_hash: link.token_hash,
19149
+ u: link.user_id,
19150
+ label: link.handle,
19151
+ next: safeNextPath(next)
19152
+ });
19153
+ return `${webOrigin.replace(/\/$/, "")}/auth/key#${fragment.toString()}`;
19154
+ }
19155
+ async function runOpen(opts, deps) {
19156
+ const active = await deps.loadActive();
19157
+ if (!active) {
19158
+ deps.error("Not signed in. Run `archme login` first.");
19159
+ return 1;
19160
+ }
19161
+ const next = safeNextPath(opts.next ?? boardPathFromMcpJson(await deps.readMcpJson(opts.cwd ?? process.cwd())));
19162
+ const boardUrl = `${deps.webOrigin.replace(/\/$/, "")}${next}`;
19163
+ if (credentialKind(active) !== "key") return await show(boardUrl, opts, deps, "Your board");
19164
+ let link = null;
19165
+ let failure = "";
19166
+ try {
19167
+ const res = await deps.postBoardLink(await deps.getAccessToken());
19168
+ const data = res.data;
19169
+ if (res.status === 200 && data.token_hash && data.user_id && data.handle) link = data;
19170
+ else failure = `${res.status} ${data.error ?? JSON.stringify(res.data)}`;
19171
+ } catch (err2) {
19172
+ failure = err2 instanceof Error ? err2.message : String(err2);
19173
+ }
19174
+ if (!link) {
19175
+ deps.error(`Could not create a sign-in link (${failure}). Opening the board without one.`);
19176
+ await show(boardUrl, opts, deps, "Your board");
19177
+ return 1;
19178
+ }
19179
+ const signedIn = buildKeyedOpenUrl(deps.webOrigin, link, next);
19180
+ if (opts.hideLink) {
19181
+ if (await deps.openBrowser(signedIn)) {
19182
+ deps.log(`Opened your board in a browser, signed in as ${link.handle}.`);
19183
+ } else {
19184
+ deps.log(`Your board: ${boardUrl}`);
19185
+ deps.log(" Run `archme open` to sign this browser in.");
19186
+ }
19187
+ return 0;
19188
+ }
19189
+ if (!opts.print && await deps.openBrowser(signedIn)) {
19190
+ deps.log(`Opened your board in a browser, signed in as ${link.handle}.`);
19191
+ return 0;
19192
+ }
19193
+ deps.log(`Your board, signed in as ${link.handle}: ${signedIn}`);
19194
+ deps.log(" (The link signs you in once and expires soon. Don't share it.)");
19195
+ return 0;
19196
+ }
19197
+ async function show(url, opts, deps, label) {
19198
+ deps.log(`${label}: ${url}`);
19199
+ if (!opts.print) await deps.openBrowser(url);
19200
+ return 0;
19201
+ }
19202
+ async function defaultOpenBrowser(url) {
19203
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
19204
+ if (!cmd) return false;
19205
+ if (cmd === "xdg-open" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
19206
+ const { execa: execa2 } = await Promise.resolve().then(() => (init_execa(), execa_exports));
19207
+ const r = await execa2(cmd, [url], { reject: false, stdio: "ignore" });
19208
+ return r.exitCode === 0;
19209
+ }
19210
+ async function defaultReadMcpJson(cwd) {
19211
+ try {
19212
+ return await readFile13(join20(cwd, ".mcp.json"), "utf8");
19213
+ } catch {
19214
+ return null;
19215
+ }
19216
+ }
19217
+ async function postBoardLinkWith(supabaseUrl, accessToken) {
19218
+ const { parseJsonResponse: parseJsonResponse2 } = await Promise.resolve().then(() => (init_oauthClient(), oauthClient_exports));
19219
+ const response = await fetch(`${supabaseUrl.replace(/\/$/, "")}/functions/v1/agents-device/board-link`, {
19220
+ method: "POST",
19221
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
19222
+ body: "{}"
19223
+ });
19224
+ return { status: response.status, data: await parseJsonResponse2(response) };
19225
+ }
19226
+ async function freshAccessToken() {
19227
+ const { resolveConfig: resolveConfig2 } = await Promise.resolve().then(() => (init_defaults(), defaults_exports));
19228
+ 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));
19229
+ const { getCurrentAccessToken: getCurrentAccessToken2 } = await Promise.resolve().then(() => (init_bearer(), bearer_exports));
19230
+ const cfg = resolveConfig2();
19231
+ await createAuthedClient2({
19232
+ supabaseUrl: cfg.supabaseUrl,
19233
+ supabaseAnonKey: cfg.supabaseAnonKey,
19234
+ clientId: cfg.archmeDaemonClientId,
19235
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS2
19236
+ });
19237
+ const token = getCurrentAccessToken2();
19238
+ if (!token) throw new Error("signed in, but no access token was issued");
19239
+ return token;
19240
+ }
19241
+ async function dispatchOpen(argv) {
19242
+ const unknown = argv.filter((a2) => a2 !== "--print");
19243
+ if (unknown.length) {
19244
+ console.error(`archme open: unexpected argument \`${unknown[0]}\`
19245
+ Usage: archme open [--print]`);
19246
+ return 2;
19247
+ }
19248
+ const { resolveConfig: resolveConfig2 } = await Promise.resolve().then(() => (init_defaults(), defaults_exports));
19249
+ const { deriveWebOrigin: deriveWebOrigin2 } = await Promise.resolve().then(() => (init_web_url(), web_url_exports));
19250
+ const cfg = resolveConfig2();
19251
+ return await runOpen({ print: argv.includes("--print") }, {
19252
+ webOrigin: deriveWebOrigin2(cfg.archmeMcpBaseUrl),
19253
+ loadActive: async () => (await loadCredentials(cfg.supabaseUrl)).credentials,
19254
+ getAccessToken: freshAccessToken,
19255
+ postBoardLink: (token) => postBoardLinkWith(cfg.supabaseUrl, token),
19256
+ readMcpJson: defaultReadMcpJson,
19257
+ openBrowser: defaultOpenBrowser,
19258
+ log: (m) => console.log(m),
19259
+ error: (m) => console.error(m)
19260
+ });
19261
+ }
19262
+ var init_open = __esm({
19263
+ "src/account/open.ts"() {
19264
+ "use strict";
19265
+ init_credentials();
19266
+ }
19267
+ });
19268
+
19269
+ // src/account/key.ts
19270
+ var key_exports = {};
19271
+ __export(key_exports, {
19272
+ dispatchKey: () => dispatchKey,
19273
+ parseKeyArgs: () => parseKeyArgs,
19274
+ runKey: () => runKey
19275
+ });
19276
+ function parseKeyArgs(argv) {
19277
+ const cmd = { action: "list", yes: false };
19278
+ let sawAction = false;
19279
+ for (let i2 = 0; i2 < argv.length; i2++) {
19280
+ const arg = argv[i2];
19281
+ if (arg === "--yes") {
19282
+ cmd.yes = true;
19283
+ continue;
19284
+ }
19285
+ if (arg === "--handle" || arg.startsWith("--handle=")) {
19286
+ const value = arg.startsWith("--handle=") ? arg.slice("--handle=".length) : argv[++i2];
19287
+ if (!value || value.startsWith("-")) return { ok: false, error: "--handle requires a value" };
19288
+ cmd.handle = value;
19289
+ continue;
19290
+ }
19291
+ if (!sawAction && (arg === "list" || arg === "show" || arg === "forget")) {
19292
+ cmd.action = arg;
19293
+ sawAction = true;
19294
+ continue;
19295
+ }
19296
+ return { ok: false, error: `unexpected argument \`${arg}\`` };
19297
+ }
19298
+ return { ok: true, cmd };
19299
+ }
19300
+ function pickKey(keys, handle, active) {
19301
+ if (handle) return keys.find((k) => k.handle === handle) ?? `No saved key for ${handle}. \`archme key list\` shows what's saved.`;
19302
+ const mine = keys.find((k) => k.user_id === active?.user_id);
19303
+ if (mine) return mine;
19304
+ if (keys.length === 1) return keys[0];
19305
+ return `Several keys are saved (${keys.map((k) => k.handle).join(", ")}). Pick one with --handle <handle>.`;
19306
+ }
19307
+ async function runKey(cmd, deps) {
19308
+ const keys = await deps.listKeys();
19309
+ const active = await deps.loadActive();
19310
+ if (cmd.action === "list") {
19311
+ if (keys.length === 0) {
19312
+ deps.log("No saved keys on this machine.");
19313
+ return 0;
19314
+ }
19315
+ for (const k of [...keys].reverse()) {
19316
+ const mark = k.user_id === active?.user_id ? " (signed in)" : "";
19317
+ deps.log(`${k.handle} saved ${k.saved_at.slice(0, 10)}${mark}`);
19318
+ }
19319
+ return 0;
19320
+ }
19321
+ if (keys.length === 0) {
19322
+ deps.error("No saved keys on this machine.");
19323
+ return 1;
19324
+ }
19325
+ const picked = pickKey(keys, cmd.handle, active);
19326
+ if (typeof picked === "string") {
19327
+ deps.error(picked);
19328
+ return 1;
19329
+ }
19330
+ if (cmd.action === "show") {
19331
+ deps.log(picked.key);
19332
+ return 0;
19333
+ }
19334
+ const signedInAsIt = active?.user_id === picked.user_id;
19335
+ deps.error(
19336
+ `Warning: this key is the only way into ${picked.handle} and its boards. Copy it somewhere safe first (\`archme key show --handle ` + picked.handle + "`)."
19337
+ );
19338
+ if (!cmd.yes) {
19339
+ if (!deps.interactive) {
19340
+ deps.error("Not forgetting without confirmation. Re-run with --yes.");
19341
+ return 2;
19342
+ }
19343
+ const answer = (await deps.promptLine(`Forget the key for ${picked.handle}? [y/N]`)).trim().toLowerCase();
19344
+ if (answer !== "y" && answer !== "yes") {
19345
+ deps.log("Kept.");
19346
+ return 2;
19347
+ }
19348
+ }
19349
+ await deps.forgetKey(picked.user_id);
19350
+ deps.log(`Forgot the key for ${picked.handle}.`);
19351
+ if (signedInAsIt) deps.log("You're still signed in as it on this machine until you `archme logout` or sign in as someone else.");
19352
+ return 0;
19353
+ }
19354
+ async function dispatchKey(argv) {
19355
+ const parsed = parseKeyArgs(argv);
19356
+ if (!parsed.ok) {
19357
+ console.error(`archme key: ${parsed.error}
19358
+ Usage: archme key [list|show|forget] [--handle <handle>] [--yes]`);
19359
+ return 2;
19360
+ }
19361
+ const { resolveConfig: resolveConfig2 } = await Promise.resolve().then(() => (init_defaults(), defaults_exports));
19362
+ const { listKeys: listKeys2, forgetKey: forgetKey2 } = await Promise.resolve().then(() => (init_keyRing(), keyRing_exports));
19363
+ const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
19364
+ const { promptLine: promptLine2, stdinIsInteractive: stdinIsInteractive2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
19365
+ const cfg = resolveConfig2();
19366
+ return await runKey(parsed.cmd, {
19367
+ listKeys: () => listKeys2(cfg.supabaseUrl),
19368
+ forgetKey: (userId) => forgetKey2(cfg.supabaseUrl, userId),
19369
+ loadActive: async () => (await loadCredentials2(cfg.supabaseUrl)).credentials,
19370
+ interactive: stdinIsInteractive2(),
19371
+ promptLine: promptLine2,
19372
+ log: (m) => console.log(m),
19373
+ error: (m) => console.error(m)
19374
+ });
19375
+ }
19376
+ var init_key = __esm({
19377
+ "src/account/key.ts"() {
19378
+ "use strict";
19379
+ }
19380
+ });
19381
+
17886
19382
  // src/scan/architecture-detail.ts
17887
19383
  function tryParse2(s) {
17888
19384
  try {
@@ -18073,8 +19569,8 @@ var init_transcript_signals = __esm({
18073
19569
  });
18074
19570
 
18075
19571
  // src/scan/report.ts
18076
- import { mkdir as mkdir11, writeFile as writeFile11 } from "node:fs/promises";
18077
- import { dirname as dirname13 } from "node:path";
19572
+ import { mkdir as mkdir12, writeFile as writeFile11 } from "node:fs/promises";
19573
+ import { dirname as dirname14 } from "node:path";
18078
19574
  async function buildReport(input) {
18079
19575
  return {
18080
19576
  schema_version: REPORT_SCHEMA_VERSION,
@@ -18108,9 +19604,9 @@ async function buildReport(input) {
18108
19604
  };
18109
19605
  }
18110
19606
  async function writeReport(reportPath, orphansTxtPath, report, orphansFull) {
18111
- await mkdir11(dirname13(reportPath), { recursive: true });
19607
+ await mkdir12(dirname14(reportPath), { recursive: true });
18112
19608
  await writeFile11(reportPath, JSON.stringify(report, null, 2) + "\n", "utf8");
18113
- await mkdir11(dirname13(orphansTxtPath), { recursive: true });
19609
+ await mkdir12(dirname14(orphansTxtPath), { recursive: true });
18114
19610
  await writeFile11(
18115
19611
  orphansTxtPath,
18116
19612
  orphansFull.join("\n") + (orphansFull.length ? "\n" : ""),
@@ -18154,8 +19650,8 @@ var init_claimDefaultProject = __esm({
18154
19650
 
18155
19651
  // src/scan/run-scan.ts
18156
19652
  import { createWriteStream as createWriteStream2 } from "node:fs";
18157
- import { mkdir as mkdir12, readFile as readFile12 } from "node:fs/promises";
18158
- import { basename as basename2, join as join19 } from "node:path";
19653
+ import { mkdir as mkdir13, readFile as readFile14 } from "node:fs/promises";
19654
+ import { basename as basename2, join as join21 } from "node:path";
18159
19655
  function buildBootstrapPrompt(opts = {}) {
18160
19656
  const { promptVariant, interactive, briefing } = opts;
18161
19657
  let base = interactive ? briefing ? `${BOOTSTRAP_INTERACTIVE}
@@ -18183,6 +19679,16 @@ async function repoHeadSha(repoPath) {
18183
19679
  const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd: repoPath });
18184
19680
  return stdout.trim();
18185
19681
  }
19682
+ async function buildScanSource(repoPath, repo, headSha) {
19683
+ const { committedAt } = await readCommitMeta(repoPath, headSha);
19684
+ const at = committedAt ? new Date(committedAt) : null;
19685
+ return {
19686
+ repo,
19687
+ before_sha: null,
19688
+ after_sha: headSha,
19689
+ ...at && !Number.isNaN(at.getTime()) ? { committed_at: at.toISOString() } : {}
19690
+ };
19691
+ }
18186
19692
  function defaultBranchName(headSha, date = todayDate()) {
18187
19693
  return `${date}-${headSha.slice(0, 7)}-scan`;
18188
19694
  }
@@ -18474,7 +19980,7 @@ async function runScan(opts, deps = {}) {
18474
19980
  // derives the `commits.source_sha` column from the very metadata it stores,
18475
19981
  // so the two can never disagree. A hand-written UPDATE that sets one and
18476
19982
  // not the other is what broke a deep link once.
18477
- ...sourceRepo ? { source: { repo: sourceRepo, before_sha: null, after_sha: headSha } } : {}
19983
+ ...sourceRepo ? { source: await buildScanSource(opts.repoPath, sourceRepo, headSha) } : {}
18478
19984
  });
18479
19985
  try {
18480
19986
  const model = await resolveModelPin(supabase);
@@ -18503,9 +20009,9 @@ async function runScan(opts, deps = {}) {
18503
20009
  // Incompatible with an interactive TTY session.
18504
20010
  streamJson: !interactive
18505
20011
  });
18506
- const transcriptDir = join19(opts.repoPath, ".archme", "scan");
18507
- const transcriptPath = join19(transcriptDir, "transcript.log");
18508
- await mkdir12(transcriptDir, { recursive: true });
20012
+ const transcriptDir = join21(opts.repoPath, ".archme", "scan");
20013
+ const transcriptPath = join21(transcriptDir, "transcript.log");
20014
+ await mkdir13(transcriptDir, { recursive: true });
18509
20015
  const transcriptStream = createWriteStream2(transcriptPath, { flags: "w" });
18510
20016
  let finalResultText = "";
18511
20017
  if (proc.stdout) {
@@ -18547,9 +20053,9 @@ async function runScan(opts, deps = {}) {
18547
20053
  await new Promise((resolve6, reject) => {
18548
20054
  transcriptStream.end((err2) => err2 ? reject(err2) : resolve6());
18549
20055
  });
18550
- const reportPath = join19(transcriptDir, "report.json");
18551
- const orphansTxtPath = join19(transcriptDir, "orphans.txt");
18552
- const transcript = await readFile12(transcriptPath, "utf8").catch(() => "");
20056
+ const reportPath = join21(transcriptDir, "report.json");
20057
+ const orphansTxtPath = join21(transcriptDir, "orphans.txt");
20058
+ const transcript = await readFile14(transcriptPath, "utf8").catch(() => "");
18553
20059
  signals2 = extractTranscriptSignals(transcript);
18554
20060
  const architecture = await fetchArchitectureDetail(supabase, resolved2.projectId, branchName);
18555
20061
  const orphans = await classifyOrphans({
@@ -18621,6 +20127,7 @@ var init_run_scan = __esm({
18621
20127
  init_bearer();
18622
20128
  init_authedClient();
18623
20129
  init_mcp_config();
20130
+ init_localGit();
18624
20131
  init_spawn_claude();
18625
20132
  init_model_pin();
18626
20133
  init_web_url();
@@ -18807,7 +20314,7 @@ var init_dispatch3 = __esm({
18807
20314
 
18808
20315
  // src/init/repoSize.ts
18809
20316
  import { stat as stat9 } from "node:fs/promises";
18810
- import { extname, join as join20 } from "node:path";
20317
+ import { extname, join as join22 } from "node:path";
18811
20318
  async function measureRepo(repoPath) {
18812
20319
  const files = await gitListFiles(repoPath);
18813
20320
  if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
@@ -18816,7 +20323,7 @@ async function measureRepo(repoPath) {
18816
20323
  for (const rel of files) {
18817
20324
  if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
18818
20325
  try {
18819
- const s = await stat9(join20(repoPath, rel));
20326
+ const s = await stat9(join22(repoPath, rel));
18820
20327
  if (!s.isFile()) continue;
18821
20328
  sourceFiles++;
18822
20329
  sourceBytes += s.size;
@@ -19025,7 +20532,7 @@ async function chooseKeepCurrent(deps) {
19025
20532
  if (deps.yes) {
19026
20533
  return { detection: deps.alreadyWatched ? "github-app" : "local-watch", runner: "self" };
19027
20534
  }
19028
- const detection = deps.alreadyWatched ? "github-app" : await deps.choose(
20535
+ const detection = deps.alreadyWatched ? "github-app" : deps.keyed ? "local-watch" : await deps.choose(
19029
20536
  "How should ArchMe detect new commits?",
19030
20537
  DETECTION_OPTIONS
19031
20538
  );
@@ -19043,16 +20550,19 @@ function formatKeepCurrentPlan(plan, ctx) {
19043
20550
  command: "archme local-watch"
19044
20551
  });
19045
20552
  }
20553
+ const costNotes = [
20554
+ "Keep it running on a machine that stays on. Each update spends tokens",
20555
+ "from your Claude subscription, the same way this scan did. Your code is",
20556
+ "not sent to our servers \u2014 only the resulting architecture."
20557
+ ];
19046
20558
  if (plan.runner === "self") {
19047
- steps.push({
19048
- title: "Start the daemon \u2014 nothing processes the queue until you do:",
19049
- command: "archme daemon start",
19050
- notes: [
19051
- "Keep it running on a machine that stays on. Each update spends tokens",
19052
- "from your Claude subscription, the same way this scan did. Your code is",
19053
- "not sent to our servers \u2014 only the resulting architecture."
19054
- ]
19055
- });
20559
+ if (!ctx.daemonStarted) {
20560
+ steps.push({
20561
+ title: "Start the daemon \u2014 nothing processes the queue until you do:",
20562
+ command: "archme daemon start",
20563
+ notes: costNotes
20564
+ });
20565
+ }
19056
20566
  } else {
19057
20567
  steps.push({
19058
20568
  title: `Email ${CONTACT_EMAIL} and we'll set up the daemon for you.`,
@@ -19069,6 +20579,10 @@ function formatKeepCurrentPlan(plan, ctx) {
19069
20579
  if (ctx.localWatchRegistered) {
19070
20580
  lines.push("", ` \u2713 ${ctx.repoSlug ?? "This repo"} is registered \u2014 your machine is watching it.`);
19071
20581
  }
20582
+ if (plan.runner === "self" && ctx.daemonStarted) {
20583
+ lines.push("", " \u2713 The daemon is running in the background (`archme daemon status`).", "");
20584
+ for (const note of costNotes) lines.push(` ${note}`);
20585
+ }
19072
20586
  const numbered = steps.length > 1;
19073
20587
  steps.forEach((step, i2) => {
19074
20588
  const bullet = numbered ? ` ${i2 + 1}. ` : " ";
@@ -19096,7 +20610,7 @@ var init_keepCurrent = __esm({
19096
20610
  value: "github-app"
19097
20611
  },
19098
20612
  {
19099
- label: "Local polling \u2014 no GitHub App and nobody's approval needed. This machine watches the repo instead. No pull-request awareness.",
20613
+ 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
20614
  value: "local-watch"
19101
20615
  }
19102
20616
  ];
@@ -19215,66 +20729,6 @@ var init_githubAppSetup = __esm({
19215
20729
  }
19216
20730
  });
19217
20731
 
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
20732
  // src/daemon/watchReadiness.ts
19279
20733
  function evaluateWatchReadiness(input) {
19280
20734
  const { marker, repo, archmeBranch } = input;
@@ -19412,6 +20866,69 @@ var init_localWatchResolve = __esm({
19412
20866
  }
19413
20867
  });
19414
20868
 
20869
+ // src/daemon/localPrSetup.ts
20870
+ async function askForPrReviews(deps = {}) {
20871
+ if (deps.flag !== void 0) return deps.flag;
20872
+ if (deps.current === "auto") return null;
20873
+ if (!(deps.interactive ?? stdinIsInteractive())) return null;
20874
+ const confirm = deps.confirm ?? ((q) => promptYesNo(q, { defaultYes: false }));
20875
+ return await confirm(PR_REVIEWS_QUESTION);
20876
+ }
20877
+ async function setPrReviewMode(supabase, watchedRepoId, enabled) {
20878
+ 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");
20879
+ if (error || !Array.isArray(data) || data.length === 0) {
20880
+ throw new Error(`could not update PR reviews for this repo: ${error?.message ?? "no access to the row"}`);
20881
+ }
20882
+ }
20883
+ async function readPrReviewMode(supabase, watchedRepoId) {
20884
+ const { data, error } = await supabase.from("watched_repos").select("pr_update_mode").eq("id", watchedRepoId).maybeSingle();
20885
+ if (error || !data) return null;
20886
+ return data.pr_update_mode ?? "disabled";
20887
+ }
20888
+ function ghInstallHint(platform3 = process.platform) {
20889
+ if (platform3 === "darwin") return "brew install gh";
20890
+ if (platform3 === "win32") return "winget install --id GitHub.cli";
20891
+ return "see https://cli.github.com for your distribution";
20892
+ }
20893
+ async function ghLoginPrompt(deps = {}) {
20894
+ if (!(deps.interactive ?? stdinIsInteractive())) return "non-interactive";
20895
+ const log = deps.log ?? ((m) => console.log(m));
20896
+ const hasToken = deps.hasToken ?? (async () => (await resolveGithubTokens()).some((t) => t.token !== null));
20897
+ if (await hasToken()) return "has-token";
20898
+ const ghPath = deps.ghPath ?? (() => findOnPath("gh"));
20899
+ if (!ghPath()) {
20900
+ log(`
20901
+ ${NEEDS_TOKEN}`);
20902
+ log(` To sign in, install the GitHub CLI (${ghInstallHint(deps.platform)}), then run: gh auth login`);
20903
+ return "gh-missing";
20904
+ }
20905
+ log(`
20906
+ ${NEEDS_TOKEN}`);
20907
+ const confirm = deps.confirm ?? ((q) => promptYesNo(q));
20908
+ if (!await confirm("Sign in with `gh auth login` now?")) {
20909
+ log(" You can do it any time: gh auth login");
20910
+ return "declined";
20911
+ }
20912
+ const run = deps.runGhLogin ?? (async () => (await execa("gh", ["auth", "login", "--hostname", "github.com"], { stdio: "inherit", reject: false })).exitCode ?? 1);
20913
+ const code = await run();
20914
+ if (code === 0 && await hasToken()) return "logged-in";
20915
+ log(" `gh auth login` did not finish \u2014 a private repo's pull requests can't be reviewed until it does.");
20916
+ return "failed";
20917
+ }
20918
+ var PR_REVIEWS_QUESTION, PR_REVIEWS_ALREADY_ON, NEEDS_TOKEN;
20919
+ var init_localPrSetup = __esm({
20920
+ "src/daemon/localPrSetup.ts"() {
20921
+ "use strict";
20922
+ init_execa();
20923
+ init_preflight();
20924
+ init_localPrAdapters();
20925
+ init_prompt();
20926
+ PR_REVIEWS_QUESTION = "Also review pull requests? Each PR push is one run on your Claude subscription.";
20927
+ PR_REVIEWS_ALREADY_ON = "Pull requests are already reviewed for this repo (`--no-prs` turns that off).";
20928
+ 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).";
20929
+ }
20930
+ });
20931
+
19415
20932
  // src/daemon/localWatch.ts
19416
20933
  var localWatch_exports = {};
19417
20934
  __export(localWatch_exports, {
@@ -19425,11 +20942,11 @@ __export(localWatch_exports, {
19425
20942
  runLocalWatchResume: () => runLocalWatchResume,
19426
20943
  runLocalWatchStop: () => runLocalWatchStop
19427
20944
  });
19428
- import { homedir as homedir6 } from "node:os";
19429
- import { join as join21, resolve as resolve3 } from "node:path";
20945
+ import { homedir as homedir7 } from "node:os";
20946
+ import { join as join23, resolve as resolve3 } from "node:path";
19430
20947
  import { stat as stat10 } from "node:fs/promises";
19431
20948
  function mirrorPathFor(cloneBaseDir, owner, name) {
19432
- return join21(cloneBaseDir, owner, name);
20949
+ return join23(cloneBaseDir, owner, name);
19433
20950
  }
19434
20951
  function resolveInitTarget(input) {
19435
20952
  if (!input.originUrl) {
@@ -19546,6 +21063,19 @@ async function runLocalWatch(opts) {
19546
21063
  }
19547
21064
  log(`
19548
21065
  \u2713 ${target.repo} is now watched locally.`);
21066
+ const interactive = !opts.yes && stdinIsInteractive();
21067
+ const current = await readPrReviewMode(supabase, registered.entry.watchedRepoId);
21068
+ const reviewPrs = await askForPrReviews({ flag: opts.prs, interactive, current });
21069
+ if (reviewPrs === null && current === "auto") log(` \u2713 ${PR_REVIEWS_ALREADY_ON}`);
21070
+ if (reviewPrs !== null) {
21071
+ try {
21072
+ await setPrReviewMode(supabase, registered.entry.watchedRepoId, reviewPrs);
21073
+ log(reviewPrs ? " \u2713 Pull requests will be reviewed too." : " Pull requests will not be reviewed.");
21074
+ if (reviewPrs) await ghLoginPrompt({ interactive, log });
21075
+ } catch (err2) {
21076
+ console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
21077
+ }
21078
+ }
19549
21079
  log(`
19550
21080
  Start the daemon if it isn't running: archme daemon start`);
19551
21081
  log(` New commits on ${target.repoBranch} will appear as ArchMe commits within a minute or two.`);
@@ -19585,7 +21115,7 @@ async function registerLocalWatch(args) {
19585
21115
  };
19586
21116
  }
19587
21117
  const mirrorPath = mirrorPathFor(cloneBaseDir, target.repoOwner, target.repoName);
19588
- const mirrorExists = await stat10(join21(mirrorPath, ".git")).then(() => true).catch(() => false);
21118
+ const mirrorExists = await stat10(join23(mirrorPath, ".git")).then(() => true).catch(() => false);
19589
21119
  if (mirrorExists) {
19590
21120
  log(` mirror: ${mirrorPath} (reusing)`);
19591
21121
  await setOriginUrl(mirrorPath, originUrl);
@@ -19604,6 +21134,9 @@ async function registerLocalWatch(args) {
19604
21134
  repoOwner: target.repoOwner,
19605
21135
  repoName: target.repoName
19606
21136
  };
21137
+ const prior = (await loadLocalRepos()).find((r) => r.watchedRepoId === watchedRepoId);
21138
+ if (prior?.prPoll) entry.prPoll = prior.prPoll;
21139
+ if (prior?.prPausedAt) entry.prPausedAt = prior.prPausedAt;
19607
21140
  await registerLocalRepo(entry);
19608
21141
  return { ok: true, entry };
19609
21142
  }
@@ -19674,7 +21207,8 @@ var init_localWatch = __esm({
19674
21207
  init_localWatchList();
19675
21208
  init_localWatchResolve();
19676
21209
  init_prompt();
19677
- DEFAULT_CLONE_BASE_DIR2 = join21(homedir6(), ".archme-job-runner", "clones");
21210
+ init_localPrSetup();
21211
+ DEFAULT_CLONE_BASE_DIR2 = join23(homedir7(), ".archme-job-runner", "clones");
19678
21212
  }
19679
21213
  });
19680
21214
 
@@ -19707,9 +21241,42 @@ var init_plan = __esm({
19707
21241
  });
19708
21242
 
19709
21243
  // src/init/run-init.ts
19710
- import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile12 } from "node:fs/promises";
21244
+ import { mkdtemp as mkdtemp4, rm as rm9, writeFile as writeFile12 } from "node:fs/promises";
19711
21245
  import { tmpdir as tmpdir4 } from "node:os";
19712
- import { join as join22 } from "node:path";
21246
+ import { join as join24 } from "node:path";
21247
+ async function defaultStartDaemon() {
21248
+ const { runStartFlow: runStartFlow2 } = await Promise.resolve().then(() => (init_install(), install_exports));
21249
+ const out = await runStartFlow2({ noLogin: true });
21250
+ return `${out.result.actions.join(" \u2192 ")} (logs: ${out.logPath})`;
21251
+ }
21252
+ async function defaultOpenBoard(opts) {
21253
+ const { runOpen: runOpen2, defaultOpenBrowser: defaultOpenBrowser2, defaultReadMcpJson: defaultReadMcpJson2, postBoardLinkWith: postBoardLinkWith2 } = await Promise.resolve().then(() => (init_open(), open_exports));
21254
+ const { loadCredentials: loadCredentials2 } = await Promise.resolve().then(() => (init_credentials(), credentials_exports));
21255
+ const { deriveWebOrigin: deriveWebOrigin2 } = await Promise.resolve().then(() => (init_web_url(), web_url_exports));
21256
+ const cfg = resolveConfig();
21257
+ return await runOpen2(opts, {
21258
+ webOrigin: deriveWebOrigin2(cfg.archmeMcpBaseUrl),
21259
+ loadActive: async () => (await loadCredentials2(cfg.supabaseUrl)).credentials,
21260
+ // The session init just authenticated; refreshing again would only rotate it.
21261
+ getAccessToken: async () => {
21262
+ const token = getCurrentAccessToken();
21263
+ if (!token) throw new Error("no active session");
21264
+ return token;
21265
+ },
21266
+ postBoardLink: (token) => postBoardLinkWith2(cfg.supabaseUrl, token),
21267
+ readMcpJson: defaultReadMcpJson2,
21268
+ openBrowser: defaultOpenBrowser2,
21269
+ log: (m) => console.log(m),
21270
+ error: (m) => console.error(m)
21271
+ });
21272
+ }
21273
+ function boardPath(url) {
21274
+ try {
21275
+ return new URL(url).pathname;
21276
+ } catch {
21277
+ return "/";
21278
+ }
21279
+ }
19713
21280
  function initScanBranch(defaultBranch) {
19714
21281
  return normalizeScanBranchName(defaultBranch);
19715
21282
  }
@@ -19721,13 +21288,13 @@ async function defaultFetchInstallScript(url) {
19721
21288
  return await resp.text();
19722
21289
  }
19723
21290
  async function defaultRunInstallScript(script, cwd) {
19724
- const dir = await mkdtemp4(join22(tmpdir4(), "archme-init-"));
19725
- const scriptPath = join22(dir, "install-agent.sh");
21291
+ const dir = await mkdtemp4(join24(tmpdir4(), "archme-init-"));
21292
+ const scriptPath = join24(dir, "install-agent.sh");
19726
21293
  try {
19727
21294
  await writeFile12(scriptPath, script, { mode: 448 });
19728
21295
  await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
19729
21296
  } finally {
19730
- await rm8(dir, { recursive: true, force: true });
21297
+ await rm9(dir, { recursive: true, force: true });
19731
21298
  }
19732
21299
  }
19733
21300
  async function runInit(opts, deps = {}) {
@@ -19744,6 +21311,8 @@ async function runInit(opts, deps = {}) {
19744
21311
  const gitContext = deps.gitContext ?? resolveGitContext;
19745
21312
  const measure = deps.measure ?? measureRepo;
19746
21313
  const watchRepo = deps.watchRepo ?? watchRepoForProject;
21314
+ const interactive = !opts.yes && (deps.interactive ?? stdinIsInteractive());
21315
+ const openBoard = deps.openBoard ?? defaultOpenBoard;
19747
21316
  const cfg = resolveConfig();
19748
21317
  const git4 = await gitContext(opts.repoPath);
19749
21318
  const repoPath = git4.root;
@@ -19751,9 +21320,10 @@ async function runInit(opts, deps = {}) {
19751
21320
  const signedIn = credentials.length > 0;
19752
21321
  if (!signedIn) {
19753
21322
  log("\u2192 signing you in\u2026");
19754
- await login();
21323
+ await login({ ...opts.login, ...opts.yes ? { interactive: false } : {} });
19755
21324
  } else {
19756
21325
  log("\u2713 already signed in");
21326
+ if (opts.login?.method) log(` (ignoring --${opts.login.method}: \`archme login\` switches accounts)`);
19757
21327
  }
19758
21328
  const authed2 = await authenticate({
19759
21329
  supabaseUrl: cfg.supabaseUrl,
@@ -19761,7 +21331,8 @@ async function runInit(opts, deps = {}) {
19761
21331
  clientId: cfg.archmeDaemonClientId,
19762
21332
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
19763
21333
  });
19764
- log(` signed in as ${authed2.identity}`);
21334
+ log(` signed in as ${authed2.identity} \u2014 not you? \`archme login\``);
21335
+ const keyed = authed2.accountKind === "key";
19765
21336
  let fullSlug = opts.project;
19766
21337
  if (!fullSlug) {
19767
21338
  const orgId = authed2.orgId;
@@ -19832,6 +21403,7 @@ async function runInit(opts, deps = {}) {
19832
21403
  }
19833
21404
  log(`
19834
21405
  Your board: ${webUrl}`);
21406
+ if (keyed) await openBoard({ next: boardPath(webUrl), hideLink: !interactive });
19835
21407
  return { exitCode: 0, project: fullSlug, branch: scanBranch, webUrl, ran };
19836
21408
  }
19837
21409
  const measurement = await measure(repoPath);
@@ -19860,10 +21432,12 @@ Your board: ${webUrl}`);
19860
21432
  const plan = await chooseKeepCurrent({
19861
21433
  alreadyWatched: watched.watched,
19862
21434
  yes: Boolean(opts.yes),
21435
+ keyed,
19863
21436
  repoSlug: git4.repoSlug,
19864
21437
  choose: deps.choose ?? promptChoice,
19865
21438
  log
19866
21439
  });
21440
+ const registered = { watchedRepoId: null };
19867
21441
  const outcome = await applyKeepCurrentPlan(
19868
21442
  plan,
19869
21443
  {
@@ -19911,12 +21485,38 @@ Your board: ${webUrl}`);
19911
21485
  target: target.target,
19912
21486
  log
19913
21487
  });
19914
- return res.ok ? { ok: true } : { ok: false, reason: res.reason };
21488
+ if (!res.ok) return { ok: false, reason: res.reason };
21489
+ registered.watchedRepoId = res.entry.watchedRepoId;
21490
+ return { ok: true };
19915
21491
  },
19916
21492
  log
19917
21493
  },
19918
21494
  watched.watched
19919
21495
  );
21496
+ if (outcome.localWatchRegistered && registered.watchedRepoId && deps.offerPrReviews) {
21497
+ await deps.offerPrReviews({
21498
+ supabase: authed2.supabase,
21499
+ orgId: authed2.orgId,
21500
+ projectId: project.projectId,
21501
+ watchedRepoId: registered.watchedRepoId,
21502
+ repoSlug: git4.repoSlug,
21503
+ repoPath: git4.root,
21504
+ interactive,
21505
+ log
21506
+ });
21507
+ }
21508
+ let daemonStarted = false;
21509
+ if (plan.runner === "self" && interactive) {
21510
+ if (await (deps.confirm ?? promptYesNo)(DAEMON_QUESTION)) {
21511
+ try {
21512
+ log("\u2192 starting the daemon\u2026");
21513
+ log(` \u2713 ${await (deps.startDaemon ?? defaultStartDaemon)()}`);
21514
+ daemonStarted = true;
21515
+ } catch (err2) {
21516
+ log(` Couldn't start the daemon: ${err2 instanceof Error ? err2.message : String(err2)}`);
21517
+ }
21518
+ }
21519
+ }
19920
21520
  if (!outcome.wired && outcome.reason) {
19921
21521
  log("");
19922
21522
  log("Not yet set up for automatic updates:");
@@ -19927,9 +21527,11 @@ Your board: ${webUrl}`);
19927
21527
  formatKeepCurrentPlan(plan, {
19928
21528
  repoSlug: git4.repoSlug,
19929
21529
  boardUrl: finalUrl,
19930
- localWatchRegistered: outcome.localWatchRegistered
21530
+ localWatchRegistered: outcome.localWatchRegistered,
21531
+ daemonStarted
19931
21532
  })
19932
21533
  );
21534
+ if (keyed) await openBoard({ next: boardPath(finalUrl), hideLink: !interactive });
19933
21535
  }
19934
21536
  return {
19935
21537
  exitCode: result.exitCode,
@@ -19939,6 +21541,7 @@ Your board: ${webUrl}`);
19939
21541
  ran
19940
21542
  };
19941
21543
  }
21544
+ var DAEMON_QUESTION;
19942
21545
  var init_run_init = __esm({
19943
21546
  "src/init/run-init.ts"() {
19944
21547
  "use strict";
@@ -19960,6 +21563,36 @@ var init_run_init = __esm({
19960
21563
  init_prompt();
19961
21564
  init_localWatch();
19962
21565
  init_plan();
21566
+ DAEMON_QUESTION = "Keep the board updating in the background?";
21567
+ }
21568
+ });
21569
+
21570
+ // src/init/prReviews.ts
21571
+ async function offerPrReviewsForInit(ctx, flag, deps = {}) {
21572
+ const readMode = deps.readMode ?? readPrReviewMode;
21573
+ const ask = deps.ask ?? askForPrReviews;
21574
+ const setMode = deps.setMode ?? setPrReviewMode;
21575
+ const ghLogin = deps.ghLogin ?? ghLoginPrompt;
21576
+ const current = await readMode(ctx.supabase, ctx.watchedRepoId);
21577
+ const want = await ask({ flag, interactive: ctx.interactive, current });
21578
+ if (want === null) {
21579
+ if (current === "auto") ctx.log(`\u2713 ${PR_REVIEWS_ALREADY_ON}`);
21580
+ return;
21581
+ }
21582
+ try {
21583
+ await setMode(ctx.supabase, ctx.watchedRepoId, want);
21584
+ } catch (err2) {
21585
+ ctx.log(`! ${err2 instanceof Error ? err2.message : String(err2)} \u2014 run \`archme local-watch --prs\` to try again.`);
21586
+ return;
21587
+ }
21588
+ if (!want) return;
21589
+ ctx.log("\u2713 Pull requests will be reviewed too.");
21590
+ await ghLogin({ interactive: ctx.interactive, log: ctx.log });
21591
+ }
21592
+ var init_prReviews = __esm({
21593
+ "src/init/prReviews.ts"() {
21594
+ "use strict";
21595
+ init_localPrSetup();
19963
21596
  }
19964
21597
  });
19965
21598
 
@@ -19979,6 +21612,25 @@ function parseInitArgs(argv) {
19979
21612
  args.help = true;
19980
21613
  return { ok: true, args };
19981
21614
  }
21615
+ const loginFlag = arg.startsWith("--key=") ? "--key" : arg;
21616
+ const loginMethod = LOGIN_FLAGS[loginFlag];
21617
+ if (loginMethod) {
21618
+ if (args.login && args.login.method !== loginMethod) {
21619
+ return { ok: false, error: `${loginFlag} cannot be combined with another sign-in option` };
21620
+ }
21621
+ args.login = { method: loginMethod };
21622
+ if (arg.startsWith("--key=")) args.login.key = arg.slice("--key=".length);
21623
+ else if (loginMethod === "key" && argv[i2 + 1]?.startsWith("amk_")) args.login.key = argv[++i2];
21624
+ continue;
21625
+ }
21626
+ if (arg === "--prs" || arg === "--no-prs") {
21627
+ const value = arg === "--prs";
21628
+ if (args.prs !== void 0 && args.prs !== value) {
21629
+ return { ok: false, error: "--prs cannot be combined with --no-prs" };
21630
+ }
21631
+ args.prs = value;
21632
+ continue;
21633
+ }
19982
21634
  if (BOOLEAN_FLAGS.has(arg)) {
19983
21635
  if (arg === "--force") args.force = true;
19984
21636
  else if (arg === "--yes") args.yes = true;
@@ -20025,7 +21677,7 @@ function parseInitArgs(argv) {
20025
21677
  args.repoPath = positionals[0];
20026
21678
  return { ok: true, args };
20027
21679
  }
20028
- var BOOLEAN_FLAGS, VALUE_FLAGS, HELP_FLAGS, PROJECT_SLUG_RE2;
21680
+ var BOOLEAN_FLAGS, VALUE_FLAGS, HELP_FLAGS, LOGIN_FLAGS, PROJECT_SLUG_RE2;
20029
21681
  var init_args = __esm({
20030
21682
  "src/init/args.ts"() {
20031
21683
  "use strict";
@@ -20037,6 +21689,11 @@ var init_args = __esm({
20037
21689
  ]);
20038
21690
  VALUE_FLAGS = /* @__PURE__ */ new Set(["--project", "--app-origin"]);
20039
21691
  HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h", "help"]);
21692
+ LOGIN_FLAGS = {
21693
+ "--no-account": "no-account",
21694
+ "--github": "github",
21695
+ "--key": "key"
21696
+ };
20040
21697
  PROJECT_SLUG_RE2 = /^[^/\s]+\/[^/\s]+$/;
20041
21698
  }
20042
21699
  });
@@ -20054,9 +21711,10 @@ import { resolve as resolve4 } from "node:path";
20054
21711
  function printInitHelp() {
20055
21712
  console.log(`archme init \u2014 guided first-time setup for this repo
20056
21713
 
20057
- Signs you in, creates a project named after the repo, installs agent config
20058
- (.mcp.json, CLAUDE.md block, architecture skill), then hands over to an
20059
- interactive Claude Code session that confirms the cost and runs the scan.
21714
+ Signs you in (or continues without an account), creates a project named after
21715
+ the repo, installs agent config (.mcp.json, CLAUDE.md block, architecture
21716
+ skill), then hands over to an interactive Claude Code session that confirms the
21717
+ cost and runs the scan.
20060
21718
 
20061
21719
  Usage:
20062
21720
  archme init [repo-path] [options]
@@ -20070,6 +21728,15 @@ Options:
20070
21728
  --yes Run unattended (headless scan, no questions)
20071
21729
  --no-interactive Same as --yes; for automation
20072
21730
  --app-origin <url> Origin serving /api/install-agent (local development)
21731
+ --no-account If not signed in: continue without an account (a key
21732
+ saved on this machine signs you in; archme key show
21733
+ prints it)
21734
+ --github If not signed in: sign in with GitHub
21735
+ --key If not signed in: use an existing key (ARCHME_KEY, else
21736
+ the saved one). Implied when ARCHME_KEY is set
21737
+ --prs / --no-prs With local polling: also review pull requests (each PR
21738
+ push is one run on your Claude subscription; a private
21739
+ repo needs gh auth login)
20073
21740
  -h, --help Show this help
20074
21741
 
20075
21742
  Requires Claude Code to be installed and authenticated \u2014 the scan runs on your
@@ -20093,7 +21760,10 @@ async function dispatchInit(argv) {
20093
21760
  appOrigin: parsed.args.appOrigin,
20094
21761
  force: parsed.args.force,
20095
21762
  skipScan: parsed.args.skipScan,
20096
- yes: parsed.args.yes || parsed.args.noInteractive
21763
+ yes: parsed.args.yes || parsed.args.noInteractive,
21764
+ login: parsed.args.login
21765
+ }, {
21766
+ offerPrReviews: (ctx) => offerPrReviewsForInit(ctx, parsed.args.prs)
20097
21767
  });
20098
21768
  return result.exitCode;
20099
21769
  } catch (err2) {
@@ -20106,6 +21776,7 @@ var init_dispatch4 = __esm({
20106
21776
  "src/init/dispatch.ts"() {
20107
21777
  "use strict";
20108
21778
  init_run_init();
21779
+ init_prReviews();
20109
21780
  init_args();
20110
21781
  EX_OK2 = 0;
20111
21782
  EX_INFRA2 = 3;
@@ -20329,7 +22000,7 @@ __export(run_uninstall_exports, {
20329
22000
  runUninstall: () => runUninstall
20330
22001
  });
20331
22002
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
20332
- import { join as join23 } from "node:path";
22003
+ import { join as join25 } from "node:path";
20333
22004
  function readIfExists(path7) {
20334
22005
  try {
20335
22006
  return existsSync3(path7) ? readFileSync5(path7, "utf8") : null;
@@ -20355,7 +22026,7 @@ async function runUninstall(opts) {
20355
22026
  const backend = opts.backend ?? await defaultBackend();
20356
22027
  const repoPath = await backend.gitRoot(opts.repoPath).catch(() => null) ?? opts.repoPath;
20357
22028
  if (repoPath !== opts.repoPath) log(`Using the repo root: ${repoPath}`);
20358
- const abs = (rel) => join23(repoPath, rel);
22029
+ const abs = (rel) => join25(repoPath, rel);
20359
22030
  const actions = [];
20360
22031
  const skipped = [];
20361
22032
  const mcpRaw = readIfExists(abs(".mcp.json"));
@@ -20698,6 +22369,7 @@ function parseLocalWatchArgs(argv) {
20698
22369
  let list = false;
20699
22370
  let stop;
20700
22371
  let resume;
22372
+ let prs;
20701
22373
  for (let i2 = 0; i2 < argv.length; i2++) {
20702
22374
  const arg = argv[i2];
20703
22375
  const next = argv[i2 + 1];
@@ -20716,6 +22388,10 @@ function parseLocalWatchArgs(argv) {
20716
22388
  i2++;
20717
22389
  } else if (arg === "--yes" || arg === "--no-interactive") {
20718
22390
  yes = true;
22391
+ } else if (arg === "--prs" || arg === "--no-prs") {
22392
+ const want = arg === "--prs";
22393
+ if (prs !== void 0 && prs !== want) return err("--prs and --no-prs contradict each other");
22394
+ prs = want;
20719
22395
  } else if (arg === "--list") {
20720
22396
  list = true;
20721
22397
  } else if (arg === "--stop" || arg === "--resume") {
@@ -20741,7 +22417,7 @@ function parseLocalWatchArgs(argv) {
20741
22417
  if (list) return { kind: "list" };
20742
22418
  if (stop) return { kind: "stop", repo: stop };
20743
22419
  if (resume) return { kind: "resume", repo: resume };
20744
- return { kind: "register", repoPath, project, branch, yes };
22420
+ return { kind: "register", repoPath, project, branch, yes, prs };
20745
22421
  }
20746
22422
  function printLocalWatchHelp() {
20747
22423
  console.log(`archme local-watch \u2014 keep ArchMe current without a GitHub App
@@ -20764,6 +22440,8 @@ Options:
20764
22440
  --project <org/slug> Project to write to (default: derived from the repo name)
20765
22441
  --branch <name> Branch to track (default: the repo's default branch)
20766
22442
  --yes Don't ask for confirmation (needs an explicit repo-path)
22443
+ --prs / --no-prs Also review pull requests (each PR push is one Claude run;
22444
+ a private repo needs gh auth login)
20767
22445
 
20768
22446
  Watching queues work; the daemon does it. After registering: archme daemon start`);
20769
22447
  }
@@ -20803,8 +22481,8 @@ var init_localWatchDispatch = __esm({
20803
22481
  });
20804
22482
 
20805
22483
  // src/doctor/collect.ts
20806
- import { readFile as readFile13 } from "node:fs/promises";
20807
- import { join as join24 } from "node:path";
22484
+ import { readFile as readFile15 } from "node:fs/promises";
22485
+ import { join as join26 } from "node:path";
20808
22486
  function binaryPresent(name) {
20809
22487
  try {
20810
22488
  requireBinary(name);
@@ -20820,7 +22498,7 @@ async function git3(cwd, args) {
20820
22498
  async function readMcpPin(repoRoot) {
20821
22499
  const empty = { installed: false, pin: { project: null, branch: null } };
20822
22500
  try {
20823
- const raw = await readFile13(join24(repoRoot, ".mcp.json"), "utf8");
22501
+ const raw = await readFile15(join26(repoRoot, ".mcp.json"), "utf8");
20824
22502
  const parsed = JSON.parse(raw);
20825
22503
  const servers = parsed.mcpServers ?? {};
20826
22504
  const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
@@ -21169,7 +22847,8 @@ var init_prompt2 = __esm({
21169
22847
  whole team, nothing to keep running. Needs someone to grant repo read
21170
22848
  access.
21171
22849
  - **Local polling** (\`archme local-watch\`): this machine fetches the repo on
21172
- a timer. No GitHub App, no approvals, but no pull-request awareness.
22850
+ a timer. No GitHub App, no approvals. Pull requests only with \`--prs\`,
22851
+ and a private repo needs \`gh auth login\`.
21173
22852
 
21174
22853
  3. **Execution** \u2014 something has to PROCESS each detected commit. This is the
21175
22854
  step people miss. Detection only queues work; the queue does nothing on its
@@ -21193,9 +22872,9 @@ __export(dispatch_exports6, {
21193
22872
  parseDoctorArgs: () => parseDoctorArgs,
21194
22873
  printDoctorHelp: () => printDoctorHelp
21195
22874
  });
21196
- import { mkdtemp as mkdtemp5, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
22875
+ import { mkdtemp as mkdtemp5, rm as rm10, writeFile as writeFile13 } from "node:fs/promises";
21197
22876
  import { tmpdir as tmpdir5 } from "node:os";
21198
- import { join as join25 } from "node:path";
22877
+ import { join as join27 } from "node:path";
21199
22878
  function parseDoctorArgs(argv) {
21200
22879
  const args = { plain: false, help: false };
21201
22880
  for (const a2 of argv) {
@@ -21256,8 +22935,8 @@ async function dispatchDoctor(argv) {
21256
22935
  );
21257
22936
  return EX_OK6;
21258
22937
  }
21259
- const dir = await mkdtemp5(join25(tmpdir5(), "archme-doctor-"));
21260
- const mcpConfigPath = join25(dir, "mcp.json");
22938
+ const dir = await mkdtemp5(join27(tmpdir5(), "archme-doctor-"));
22939
+ const mcpConfigPath = join27(dir, "mcp.json");
21261
22940
  await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
21262
22941
  try {
21263
22942
  const result = await spawnClaude({
@@ -21272,7 +22951,7 @@ async function dispatchDoctor(argv) {
21272
22951
  });
21273
22952
  return result.exitCode === 0 ? EX_OK6 : EX_INFRA5;
21274
22953
  } finally {
21275
- await rm9(dir, { recursive: true, force: true });
22954
+ await rm10(dir, { recursive: true, force: true });
21276
22955
  }
21277
22956
  }
21278
22957
  var EX_OK6, EX_INFRA5, EX_USAGE6;
@@ -21314,6 +22993,7 @@ async function runLogout(args) {
21314
22993
  if (!target || args.includes("--all")) {
21315
22994
  await clearCredentials(url);
21316
22995
  console.log("Logged out: removed all stored identities.");
22996
+ await mentionKeptKeys(url);
21317
22997
  return 0;
21318
22998
  }
21319
22999
  const all = (await loadAllCredentials(url)).credentials;
@@ -21324,24 +23004,36 @@ async function runLogout(args) {
21324
23004
  }
21325
23005
  for (const c3 of matches) await clearCredential(url, c3.device_id);
21326
23006
  console.log(`Logged out: removed ${matches.length} device(s) for "${target}".`);
23007
+ await mentionKeptKeys(url);
21327
23008
  return 0;
21328
23009
  }
23010
+ async function mentionKeptKeys(url) {
23011
+ const keys = await listKeys(url).catch(() => []);
23012
+ if (keys.length === 0) return;
23013
+ console.log(
23014
+ `Saved keys are kept (${keys.map((k) => k.handle).join(", ")}): \`archme login --key\` signs back in, \`archme key list\` shows them.`
23015
+ );
23016
+ }
21329
23017
  var init_logout = __esm({
21330
23018
  "src/auth/logout.ts"() {
21331
23019
  "use strict";
21332
23020
  init_defaults();
21333
23021
  init_credentials();
23022
+ init_keyRing();
21334
23023
  }
21335
23024
  });
21336
23025
 
21337
23026
  // src/cli.ts
21338
23027
  init_enroll();
23028
+ init_keyRing();
21339
23029
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
21340
23030
  "daemon",
21341
23031
  "analyze",
21342
23032
  "login",
21343
23033
  "logout",
21344
23034
  "keys",
23035
+ "key",
23036
+ "open",
21345
23037
  "scan",
21346
23038
  "init",
21347
23039
  "uninstall",
@@ -21428,9 +23120,32 @@ async function main(argv) {
21428
23120
  console.log(v);
21429
23121
  return 0;
21430
23122
  }
21431
- case "login":
21432
- await runLogin();
23123
+ case "login": {
23124
+ const parsed = parseLoginArgs(r.args);
23125
+ if (!parsed.ok) {
23126
+ console.error(`archme login: ${parsed.error}
23127
+ Usage: archme login [--no-account | --github | --key] (--key reads ARCHME_KEY, a saved key, or a prompt; ARCHME_KEY alone implies --key)`);
23128
+ return 2;
23129
+ }
23130
+ try {
23131
+ await runLogin(parsed.opts);
23132
+ } catch (err2) {
23133
+ if (err2 instanceof LoginError || err2 instanceof KeyRingUnreadableError) {
23134
+ console.error(`archme login: ${err2.message}`);
23135
+ return 1;
23136
+ }
23137
+ throw err2;
23138
+ }
21433
23139
  return 0;
23140
+ }
23141
+ case "open": {
23142
+ const { dispatchOpen: dispatchOpen2 } = await Promise.resolve().then(() => (init_open(), open_exports));
23143
+ return await dispatchOpen2(r.args);
23144
+ }
23145
+ case "key": {
23146
+ const { dispatchKey: dispatchKey2 } = await Promise.resolve().then(() => (init_key(), key_exports));
23147
+ return await dispatchKey2(r.args);
23148
+ }
21434
23149
  case "analyze": {
21435
23150
  const { dispatchAnalyze: dispatchAnalyze2 } = await Promise.resolve().then(() => (init_dispatch(), dispatch_exports));
21436
23151
  return await dispatchAnalyze2(r.args);
@@ -21482,7 +23197,13 @@ Other commands:
21482
23197
  archme uninstall [repo-path] Stop ArchitectMe for a repo: config,
21483
23198
  watching, and the daemon if it's now idle
21484
23199
  archme doctor Check this machine's setup and explain it
21485
- archme login | logout Authenticate with archme
23200
+ archme login [--no-account|--github|--key]
23201
+ Sign in (or continue without an account);
23202
+ --key reads ARCHME_KEY, a saved key, or a
23203
+ prompt (ARCHME_KEY alone implies --key)
23204
+ archme logout Sign out here (saved keys are kept)
23205
+ archme open [--print] Open your board in the browser, signed in
23206
+ archme key [list|show|forget] Account keys saved on this machine
21486
23207
  archme keys list|create|revoke Manage MCP API keys
21487
23208
  archme --version Print the installed version
21488
23209