@archme/cli 0.4.4 → 0.5.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 (3) hide show
  1. package/README.md +26 -0
  2. package/dist/cli.js +717 -111
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,6 +42,28 @@ Watching a repo needs the ArchMe GitHub App installed on it; `archme init` tells
42
42
  you if that step is still missing. Not up for hosting the agent yourself? Get in
43
43
  touch at [archme.io](https://archme.io) and we'll run it for you.
44
44
 
45
+ ## 3. Remove it
46
+
47
+ ```bash
48
+ archme uninstall
49
+ ```
50
+
51
+ From inside the repo. It lists everything it will change and asks before doing
52
+ any of it:
53
+
54
+ - **The agent config** — the `archme` MCP server from `.mcp.json`, the ArchMe
55
+ block from `CLAUDE.md`, the architecture skill. Your own config in those files
56
+ is left alone, and a file we created is deleted rather than left empty.
57
+ - **Watching** — so commits stop updating the architecture. Removing the config
58
+ on its own would leave the board moving while looking uninstalled, which is
59
+ why this is the default; `--keep-watching` opts out.
60
+ - **The daemon** — but only if no other repo is left for it to serve. It's one
61
+ service per account, so uninstalling one repo never stops the others.
62
+
63
+ Your architecture on archme.io and this machine's login survive (`archme logout`
64
+ for the latter). The ArchMe GitHub App installation is org-wide, so it stays —
65
+ remove it on github.com if you want it gone.
66
+
45
67
  ---
46
68
 
47
69
  ## Other commands
@@ -49,6 +71,7 @@ touch at [archme.io](https://archme.io) and we'll run it for you.
49
71
  | Command | Purpose |
50
72
  |---|---|
51
73
  | `archme init [repo-path]` | Guided setup + scan (see above); `--force` re-scans |
74
+ | `archme uninstall [repo-path]` | Stop ArchMe for a repo: agent config, watching, and the daemon if idle |
52
75
  | `archme scan <repo-path>` | Low-level single-pass scan into an explicit `--project` / `--branch` |
53
76
  | `archme daemon <sub>` | `start` · `stop` · `restart` · `status` · `logs` · `enable` · `disable` · `install` · `uninstall` · `run` |
54
77
  | `archme analyze <sub>` | Tree-sitter code analysis: `outline` · `repo` · `diff` · `match` · `prefetch` |
@@ -61,6 +84,9 @@ Every command takes `--help`. Useful flags:
61
84
  (scans are interactive by default, so you can steer them as they go).
62
85
  - `archme daemon status` — is it installed, running, and signed in?
63
86
  `archme daemon logs -f` follows the log.
87
+ - `archme uninstall --keep-watching` — removes the agent config but leaves the
88
+ repo watched and the daemon running (see above); `--yes` skips the
89
+ confirmation. Run it from anywhere in the repo — it resolves the repo root.
64
90
  - `archme analyze outline <file>` — JSON symbol outline for one file; `repo`
65
91
  outlines everything, `diff <range>` only what a git range touched. Supports
66
92
  TypeScript, TSX, JavaScript, Python, Go, Java, Ruby, Rust, Swift, and Kotlin.
package/dist/cli.js CHANGED
@@ -1091,12 +1091,12 @@ var require_isexe = __commonJS({
1091
1091
  if (typeof Promise !== "function") {
1092
1092
  throw new TypeError("callback not provided");
1093
1093
  }
1094
- return new Promise(function(resolve5, reject) {
1094
+ return new Promise(function(resolve6, reject) {
1095
1095
  isexe(path7, options || {}, function(er, is) {
1096
1096
  if (er) {
1097
1097
  reject(er);
1098
1098
  } else {
1099
- resolve5(is);
1099
+ resolve6(is);
1100
1100
  }
1101
1101
  });
1102
1102
  });
@@ -1162,27 +1162,27 @@ var require_which = __commonJS({
1162
1162
  opt = {};
1163
1163
  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
1164
1164
  const found = [];
1165
- const step = (i2) => new Promise((resolve5, reject) => {
1165
+ const step = (i2) => new Promise((resolve6, reject) => {
1166
1166
  if (i2 === pathEnv.length)
1167
- return opt.all && found.length ? resolve5(found) : reject(getNotFoundError(cmd));
1167
+ return opt.all && found.length ? resolve6(found) : reject(getNotFoundError(cmd));
1168
1168
  const ppRaw = pathEnv[i2];
1169
1169
  const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
1170
1170
  const pCmd = path7.join(pathPart, cmd);
1171
1171
  const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
1172
- resolve5(subStep(p, i2, 0));
1172
+ resolve6(subStep(p, i2, 0));
1173
1173
  });
1174
- const subStep = (p, i2, ii) => new Promise((resolve5, reject) => {
1174
+ const subStep = (p, i2, ii) => new Promise((resolve6, reject) => {
1175
1175
  if (ii === pathExt.length)
1176
- return resolve5(step(i2 + 1));
1176
+ return resolve6(step(i2 + 1));
1177
1177
  const ext2 = pathExt[ii];
1178
1178
  isexe(p + ext2, { pathExt: pathExtExe }, (er, is) => {
1179
1179
  if (!er && is) {
1180
1180
  if (opt.all)
1181
1181
  found.push(p + ext2);
1182
1182
  else
1183
- return resolve5(p + ext2);
1183
+ return resolve6(p + ext2);
1184
1184
  }
1185
- return resolve5(subStep(p, i2, ii + 1));
1185
+ return resolve6(subStep(p, i2, ii + 1));
1186
1186
  });
1187
1187
  });
1188
1188
  return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
@@ -2262,8 +2262,8 @@ var init_deferred = __esm({
2262
2262
  "node_modules/execa/lib/utils/deferred.js"() {
2263
2263
  createDeferred = () => {
2264
2264
  const methods = {};
2265
- const promise = new Promise((resolve5, reject) => {
2266
- Object.assign(methods, { resolve: resolve5, reject });
2265
+ const promise = new Promise((resolve6, reject) => {
2266
+ Object.assign(methods, { resolve: resolve6, reject });
2267
2267
  });
2268
2268
  return Object.assign(promise, methods);
2269
2269
  };
@@ -7560,11 +7560,11 @@ var init_concurrent = __esm({
7560
7560
  const promises = weakMap.get(stream);
7561
7561
  const promise = createDeferred();
7562
7562
  promises.push(promise);
7563
- const resolve5 = promise.resolve.bind(promise);
7564
- return { resolve: resolve5, promises };
7563
+ const resolve6 = promise.resolve.bind(promise);
7564
+ return { resolve: resolve6, promises };
7565
7565
  };
7566
- waitForConcurrentStreams = async ({ resolve: resolve5, promises }, subprocess) => {
7567
- resolve5();
7566
+ waitForConcurrentStreams = async ({ resolve: resolve6, promises }, subprocess) => {
7567
+ resolve6();
7568
7568
  const [isSubprocessExit] = await Promise.race([
7569
7569
  Promise.allSettled([true, subprocess]),
7570
7570
  Promise.all([false, ...promises])
@@ -8636,7 +8636,7 @@ async function runLogin() {
8636
8636
  const expiresAt = Date.now() + data.expires_in * 1e3;
8637
8637
  let intervalMs = Math.max(data.interval, 1) * 1e3;
8638
8638
  while (Date.now() < expiresAt) {
8639
- await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
8639
+ await new Promise((resolve6) => setTimeout(resolve6, intervalMs));
8640
8640
  const poll = await postJson("/poll", {
8641
8641
  device_code: data.device_code
8642
8642
  });
@@ -8666,12 +8666,6 @@ var init_enroll = __esm({
8666
8666
  init_execa();
8667
8667
  init_credentials();
8668
8668
  init_defaults();
8669
- if (import.meta.url === `file://${process.argv[1]}`) {
8670
- runLogin().catch((err2) => {
8671
- console.error(err2 instanceof Error ? err2.message : err2);
8672
- process.exit(1);
8673
- });
8674
- }
8675
8669
  }
8676
8670
  });
8677
8671
 
@@ -8726,13 +8720,43 @@ var init_paths = __esm({
8726
8720
  }
8727
8721
  });
8728
8722
 
8723
+ // src/daemon/supervision.ts
8724
+ function isSupervised(env) {
8725
+ return env[SUPERVISED_ENV_VAR] === "1";
8726
+ }
8727
+ function resolveExitCode(code, env) {
8728
+ return isSupervised(env) && PERMANENT_EXIT_CODES.has(code) ? 0 : code;
8729
+ }
8730
+ function supervisedStopNotice(code, env) {
8731
+ if (!isSupervised(env) || !PERMANENT_EXIT_CODES.has(code)) return null;
8732
+ return "this cannot be fixed by restarting, so the service is stopping rather than retrying; re-run `archme daemon start` once resolved";
8733
+ }
8734
+ var PERMANENT_EXIT_CODES, SUPERVISED_ENV_VAR;
8735
+ var init_supervision = __esm({
8736
+ "src/daemon/supervision.ts"() {
8737
+ "use strict";
8738
+ PERMANENT_EXIT_CODES = /* @__PURE__ */ new Set([
8739
+ 1,
8740
+ // missing/unreadable config
8741
+ 2,
8742
+ // no stored credentials — needs `archme login`
8743
+ 78
8744
+ // several accounts on one machine — needs a human to choose
8745
+ ]);
8746
+ SUPERVISED_ENV_VAR = "ARCHME_SUPERVISED";
8747
+ }
8748
+ });
8749
+
8729
8750
  // src/daemon/service/unitFiles.ts
8751
+ function unitEnv(env) {
8752
+ return { ...env, [SUPERVISED_ENV_VAR]: "1" };
8753
+ }
8730
8754
  function xmlEscape(s) {
8731
8755
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8732
8756
  }
8733
8757
  function renderLaunchdPlist(opts) {
8734
8758
  const argLines = [opts.nodeBin, opts.cliEntry, "daemon", "run"].map((a2) => ` <string>${xmlEscape(a2)}</string>`).join("\n");
8735
- const envLines = Object.entries(opts.env).map(
8759
+ const envLines = Object.entries(unitEnv(opts.env)).map(
8736
8760
  ([k, v]) => ` <key>${xmlEscape(k)}</key>
8737
8761
  <string>${xmlEscape(v)}</string>`
8738
8762
  ).join("\n");
@@ -8750,8 +8774,8 @@ ${argLines}
8750
8774
  <true/>
8751
8775
  <key>KeepAlive</key>
8752
8776
  <dict>
8753
- <key>Crashed</key>
8754
- <true/>
8777
+ <key>SuccessfulExit</key>
8778
+ <false/>
8755
8779
  </dict>
8756
8780
  <key>ThrottleInterval</key>
8757
8781
  <integer>10</integer>
@@ -8774,7 +8798,7 @@ function systemdArgQuote(s) {
8774
8798
  return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
8775
8799
  }
8776
8800
  function renderSystemdUnit(opts) {
8777
- const envLines = Object.entries(opts.env).map(([k, v]) => `Environment="${k}=${systemdEnvEscape(v)}"`).join("\n");
8801
+ const envLines = Object.entries(unitEnv(opts.env)).map(([k, v]) => `Environment="${k}=${systemdEnvEscape(v)}"`).join("\n");
8778
8802
  const execStart = [
8779
8803
  systemdArgQuote(opts.nodeBin),
8780
8804
  systemdArgQuote(opts.cliEntry),
@@ -8801,6 +8825,7 @@ var init_unitFiles = __esm({
8801
8825
  "src/daemon/service/unitFiles.ts"() {
8802
8826
  "use strict";
8803
8827
  init_paths();
8828
+ init_supervision();
8804
8829
  }
8805
8830
  });
8806
8831
 
@@ -8852,7 +8877,7 @@ var init_launchctl = __esm({
8852
8877
  if (r.exitCode === 0 || r.exitCode === 17) return;
8853
8878
  if (r.exitCode === 5) {
8854
8879
  await this.opts.spawner.run("launchctl", ["bootout", SERVICE_TARGET(this.opts.uid)]);
8855
- await new Promise((resolve5) => setTimeout(resolve5, 500));
8880
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
8856
8881
  r = await bootstrap();
8857
8882
  if (r.exitCode === 0 || r.exitCode === 17) return;
8858
8883
  }
@@ -8997,6 +9022,13 @@ var init_systemctl = __esm({
8997
9022
  });
8998
9023
 
8999
9024
  // src/daemon/service/serviceController.ts
9025
+ var serviceController_exports = {};
9026
+ __export(serviceController_exports, {
9027
+ orchestrateStart: () => orchestrateStart,
9028
+ orchestrateStop: () => orchestrateStop,
9029
+ pickController: () => pickController,
9030
+ reinstallIfChanged: () => reinstallIfChanged
9031
+ });
9000
9032
  import { userInfo } from "node:os";
9001
9033
  function pickController(platform3 = process.platform, env = process.env, spawner = execaSpawner) {
9002
9034
  const unitPath = resolveUnitPath(platform3, env);
@@ -9231,7 +9263,7 @@ async function tailLog(opts = {}) {
9231
9263
  }
9232
9264
  }
9233
9265
  if (!opts.follow) return;
9234
- await new Promise((resolve5, reject) => {
9266
+ await new Promise((resolve6, reject) => {
9235
9267
  let fileWatcher = null;
9236
9268
  let dirWatcher = null;
9237
9269
  const dir = dirname6(path7);
@@ -9295,7 +9327,7 @@ async function tailLog(opts = {}) {
9295
9327
  const handleSigint = () => {
9296
9328
  fileWatcher?.close();
9297
9329
  dirWatcher?.close();
9298
- resolve5();
9330
+ resolve6();
9299
9331
  };
9300
9332
  process.once("SIGINT", handleSigint);
9301
9333
  });
@@ -14192,15 +14224,17 @@ async function acquireSingleInstanceLock(pidfile) {
14192
14224
  }
14193
14225
  };
14194
14226
  }
14195
- var SingleInstanceLockedError;
14227
+ var SINGLE_INSTANCE_EXIT_CODE, LOCK_HELD_MESSAGE, SingleInstanceLockedError;
14196
14228
  var init_singleInstanceLock = __esm({
14197
14229
  "src/daemon/service/singleInstanceLock.ts"() {
14198
14230
  "use strict";
14231
+ SINGLE_INSTANCE_EXIT_CODE = 64;
14232
+ LOCK_HELD_MESSAGE = "already holds";
14199
14233
  SingleInstanceLockedError = class extends Error {
14200
14234
  heldByPid;
14201
14235
  constructor(pidfile, heldByPid) {
14202
14236
  super(
14203
- heldByPid != null ? `another archme daemon already holds ${pidfile} (pid ${heldByPid})` : `another archme daemon already holds ${pidfile}`
14237
+ heldByPid != null ? `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile} (pid ${heldByPid})` : `another archme daemon ${LOCK_HELD_MESSAGE} ${pidfile}`
14204
14238
  );
14205
14239
  this.heldByPid = heldByPid;
14206
14240
  this.name = "SingleInstanceLockedError";
@@ -14231,7 +14265,7 @@ var init_semaphore = __esm({
14231
14265
  acquire() {
14232
14266
  const slot = this.tryAcquire();
14233
14267
  if (slot) return Promise.resolve(slot);
14234
- return new Promise((resolve5) => this.waiters.push(resolve5));
14268
+ return new Promise((resolve6) => this.waiters.push(resolve6));
14235
14269
  }
14236
14270
  makeRelease() {
14237
14271
  let released = false;
@@ -15687,7 +15721,6 @@ import { randomUUID } from "node:crypto";
15687
15721
  import { mkdtemp as mkdtemp3, mkdir as mkdir10, writeFile as writeFile10, rm as rm6, stat as stat8, chmod as chmod3 } from "node:fs/promises";
15688
15722
  import { tmpdir as tmpdir3 } from "node:os";
15689
15723
  import { join as join18 } from "node:path";
15690
- import { pathToFileURL } from "node:url";
15691
15724
  import { homedir as homedir5 } from "node:os";
15692
15725
  function attributeKillCause(result, recorded) {
15693
15726
  if (recorded) return recorded;
@@ -16092,12 +16125,12 @@ async function rpcHeartbeat(ctx, jobId, deviceId) {
16092
16125
  );
16093
16126
  if (error) console.error(`[${jobId}] heartbeat error:`, error.message);
16094
16127
  }
16095
- async function rpcComplete(ctx, jobId, deviceId, summary, kind, changed, metrics, logs, advanceMarker = true, affected) {
16128
+ async function rpcComplete(ctx, jobId, deviceId, summary2, kind, changed, metrics, logs, advanceMarker = true, affected) {
16096
16129
  const extra = {
16097
16130
  // `kind` drives lifecycle (squash, marker, retry). `changed` is the
16098
16131
  // decoupled what-changed verdict the Check Run / sticky comment render
16099
16132
  // (migration 083): a structural-only job is kind=applied but changed=false.
16100
- result: { summary, kind, changed },
16133
+ result: { summary: summary2, kind, changed },
16101
16134
  metrics,
16102
16135
  logs,
16103
16136
  advance_marker: advanceMarker
@@ -16631,8 +16664,8 @@ async function runJob(ctx, job, claimDeviceId) {
16631
16664
  if (runResult.isError) {
16632
16665
  throw new Error(`claude reported is_error in its JSON result (no successful completion)`);
16633
16666
  }
16634
- const summary = runResult.summary.trim().slice(0, 4e3);
16635
- if (summary) log(`claude> ${summary.slice(0, 500).replace(/\n/g, " ")}`);
16667
+ const summary2 = runResult.summary.trim().slice(0, 4e3);
16668
+ if (summary2) log(`claude> ${summary2.slice(0, 500).replace(/\n/g, " ")}`);
16636
16669
  metrics.model_served = runResult.modelServed;
16637
16670
  metrics.usage = runResult.usage;
16638
16671
  metrics.cost_usd = runResult.costUsd;
@@ -16659,7 +16692,7 @@ async function runJob(ctx, job, claimDeviceId) {
16659
16692
  throw new Error(outcome.reason);
16660
16693
  }
16661
16694
  const changed = outcome.changed;
16662
- log(`done (${outcome.kind}, changed=${changed}, ${writes} commit(s) [${diffWrites} diff / ${structuralWrites} structural], ${reads ?? "?"} mcp tool call(s)): ${summary.slice(0, 200).replace(/\n/g, " ")}`);
16695
+ log(`done (${outcome.kind}, changed=${changed}, ${writes} commit(s) [${diffWrites} diff / ${structuralWrites} structural], ${reads ?? "?"} mcp tool call(s)): ${summary2.slice(0, 200).replace(/\n/g, " ")}`);
16663
16696
  const totalMs = Date.now() - startedAt;
16664
16697
  markPhase("complete", { kind: outcome.kind, changed, total_ms: totalMs });
16665
16698
  metrics.total_ms = totalMs;
@@ -16667,7 +16700,7 @@ async function runJob(ctx, job, claimDeviceId) {
16667
16700
  ctx,
16668
16701
  job.id,
16669
16702
  deviceId,
16670
- summary,
16703
+ summary2,
16671
16704
  outcome.kind,
16672
16705
  changed,
16673
16706
  metrics,
@@ -16876,6 +16909,11 @@ async function signInAccount(creds, makeAuth) {
16876
16909
  }
16877
16910
  return { active, lastErr, usedFallback };
16878
16911
  }
16912
+ function exitDaemon(code) {
16913
+ const notice = supervisedStopNotice(code, process.env);
16914
+ if (notice) console.error(notice);
16915
+ process.exit(resolveExitCode(code, process.env));
16916
+ }
16879
16917
  async function runDaemon() {
16880
16918
  let lockHandle = null;
16881
16919
  try {
@@ -16883,7 +16921,7 @@ async function runDaemon() {
16883
16921
  } catch (err2) {
16884
16922
  if (err2 instanceof SingleInstanceLockedError) {
16885
16923
  console.error(String(err2.message));
16886
- process.exit(64);
16924
+ process.exit(SINGLE_INSTANCE_EXIT_CODE);
16887
16925
  }
16888
16926
  throw err2;
16889
16927
  }
@@ -16899,7 +16937,7 @@ async function runDaemon() {
16899
16937
  } catch {
16900
16938
  }
16901
16939
  }
16902
- process.exit(1);
16940
+ exitDaemon(1);
16903
16941
  }
16904
16942
  throw err2;
16905
16943
  }
@@ -16930,7 +16968,7 @@ async function runDaemon() {
16930
16968
  } catch {
16931
16969
  }
16932
16970
  }
16933
- process.exit(2);
16971
+ exitDaemon(2);
16934
16972
  }
16935
16973
  let credentials;
16936
16974
  try {
@@ -16944,7 +16982,7 @@ async function runDaemon() {
16944
16982
  } catch {
16945
16983
  }
16946
16984
  }
16947
- process.exit(78);
16985
+ exitDaemon(78);
16948
16986
  }
16949
16987
  throw err2;
16950
16988
  }
@@ -16959,7 +16997,7 @@ async function runDaemon() {
16959
16997
  } catch {
16960
16998
  }
16961
16999
  }
16962
- process.exit(75);
17000
+ exitDaemon(75);
16963
17001
  }
16964
17002
  const auth = active.auth;
16965
17003
  const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
@@ -17141,6 +17179,7 @@ var init_run = __esm({
17141
17179
  init_dispatch();
17142
17180
  init_defaults();
17143
17181
  init_singleInstanceLock();
17182
+ init_supervision();
17144
17183
  init_paths();
17145
17184
  init_semaphore();
17146
17185
  init_bearer();
@@ -17341,12 +17380,6 @@ ${ANCHOR_VERIFICATION}`;
17341
17380
  return daemonVersion;
17342
17381
  }
17343
17382
  };
17344
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
17345
- runDaemon().catch((err2) => {
17346
- console.error("fatal:", err2);
17347
- process.exit(1);
17348
- });
17349
- }
17350
17383
  }
17351
17384
  });
17352
17385
 
@@ -18138,14 +18171,14 @@ async function runScan(opts, deps = {}) {
18138
18171
  log(` supabase: ${supabaseUrl}`);
18139
18172
  log(` mcp base: ${mcpUrl}`);
18140
18173
  log(` repo: ${opts.repoPath}`);
18141
- const authed = await createAuthedClient({
18174
+ const authed2 = await createAuthedClient({
18142
18175
  supabaseUrl,
18143
18176
  supabaseAnonKey: supabaseAnon,
18144
18177
  clientId,
18145
18178
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
18146
18179
  });
18147
- log(` signed in as ${authed.identity}`);
18148
- const { supabase, userId } = authed;
18180
+ log(` signed in as ${authed2.identity}`);
18181
+ const { supabase, userId } = authed2;
18149
18182
  let headSha;
18150
18183
  try {
18151
18184
  headSha = await repoHeadSha(opts.repoPath);
@@ -18244,7 +18277,7 @@ async function runScan(opts, deps = {}) {
18244
18277
  const result = await proc;
18245
18278
  const exitCode = result.exitCode ?? 0;
18246
18279
  const signal = result.signal ?? null;
18247
- const summary = (finalResultText || (result.stdout ? String(result.stdout) : "")).trim().slice(-4e3);
18280
+ const summary2 = (finalResultText || (result.stdout ? String(result.stdout) : "")).trim().slice(-4e3);
18248
18281
  if (signal) {
18249
18282
  log(`claude exited ${exitCode} (signal: ${signal})`);
18250
18283
  } else {
@@ -18255,8 +18288,8 @@ async function runScan(opts, deps = {}) {
18255
18288
  log(` stories: ${counts.stories}`);
18256
18289
  let signals2 = null;
18257
18290
  try {
18258
- await new Promise((resolve5, reject) => {
18259
- transcriptStream.end((err2) => err2 ? reject(err2) : resolve5());
18291
+ await new Promise((resolve6, reject) => {
18292
+ transcriptStream.end((err2) => err2 ? reject(err2) : resolve6());
18260
18293
  });
18261
18294
  const reportPath = join19(transcriptDir, "report.json");
18262
18295
  const orphansTxtPath = join19(transcriptDir, "orphans.txt");
@@ -18322,7 +18355,7 @@ async function runScan(opts, deps = {}) {
18322
18355
  orgSlug: resolved2.orgSlug,
18323
18356
  projectSlug: resolved2.projectSlug,
18324
18357
  webUrl,
18325
- summary
18358
+ summary: summary2
18326
18359
  };
18327
18360
  } finally {
18328
18361
  await cleanupMcp().catch(() => {
@@ -18933,6 +18966,9 @@ var init_githubAppSetup = __esm({
18933
18966
 
18934
18967
  // src/daemon/prompt.ts
18935
18968
  import { createInterface } from "node:readline/promises";
18969
+ function stdinIsInteractive() {
18970
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
18971
+ }
18936
18972
  async function withReadline(fn) {
18937
18973
  const rl = createInterface({ input: process.stdin, output: process.stdout });
18938
18974
  try {
@@ -19215,9 +19251,9 @@ async function runLocalWatch(opts) {
19215
19251
  log(` repo: ${target.repo}`);
19216
19252
  log(` branch: ${target.repoBranch} \u2192 archme branch ${target.archmeBranch}`);
19217
19253
  const cfg = resolveConfig();
19218
- let authed;
19254
+ let authed2;
19219
19255
  try {
19220
- authed = await createAuthedClient({
19256
+ authed2 = await createAuthedClient({
19221
19257
  supabaseUrl: cfg.supabaseUrl,
19222
19258
  supabaseAnonKey: cfg.supabaseAnonKey,
19223
19259
  clientId: cfg.archmeDaemonClientId,
@@ -19227,8 +19263,8 @@ async function runLocalWatch(opts) {
19227
19263
  console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
19228
19264
  return 3;
19229
19265
  }
19230
- const { supabase, userId } = authed;
19231
- const orgId = authed.orgId;
19266
+ const { supabase, userId } = authed2;
19267
+ const orgId = authed2.orgId;
19232
19268
  if (!orgId) {
19233
19269
  console.error("archme local-watch: this login is not paired with an org \u2014 run `archme login`");
19234
19270
  return 3;
@@ -19468,22 +19504,22 @@ async function runInit(opts, deps = {}) {
19468
19504
  } else {
19469
19505
  log("\u2713 already signed in");
19470
19506
  }
19471
- const authed = await authenticate({
19507
+ const authed2 = await authenticate({
19472
19508
  supabaseUrl: cfg.supabaseUrl,
19473
19509
  supabaseAnonKey: cfg.supabaseAnonKey,
19474
19510
  clientId: cfg.archmeDaemonClientId,
19475
19511
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
19476
19512
  });
19477
- log(` signed in as ${authed.identity}`);
19513
+ log(` signed in as ${authed2.identity}`);
19478
19514
  let fullSlug = opts.project;
19479
19515
  if (!fullSlug) {
19480
- const orgId = authed.orgId;
19516
+ const orgId = authed2.orgId;
19481
19517
  if (!orgId) {
19482
19518
  throw new Error(
19483
19519
  "could not resolve your organization \u2014 run `archme login` again, or pass --project <org>/<slug>"
19484
19520
  );
19485
19521
  }
19486
- const orgSlug = await resolveOrgSlug(authed.supabase, orgId);
19522
+ const orgSlug = await resolveOrgSlug(authed2.supabase, orgId);
19487
19523
  if (!orgSlug) {
19488
19524
  throw new Error(
19489
19525
  `paired org ${orgId} no longer exists \u2014 run \`archme login\` again, or pass --project <org>/<slug>`
@@ -19492,8 +19528,8 @@ async function runInit(opts, deps = {}) {
19492
19528
  fullSlug = defaultProjectSlug(orgSlug, repoPath);
19493
19529
  }
19494
19530
  const project = await resolveProject(
19495
- authed.supabase,
19496
- authed.userId,
19531
+ authed2.supabase,
19532
+ authed2.userId,
19497
19533
  fullSlug
19498
19534
  );
19499
19535
  log(
@@ -19501,7 +19537,7 @@ async function runInit(opts, deps = {}) {
19501
19537
  );
19502
19538
  const scanBranch = initScanBranch(project.defaultBranch);
19503
19539
  const detail = await fetchArchitecture(
19504
- authed.supabase,
19540
+ authed2.supabase,
19505
19541
  project.projectId,
19506
19542
  scanBranch
19507
19543
  );
@@ -19515,8 +19551,8 @@ async function runInit(opts, deps = {}) {
19515
19551
  await runInstallScript(script, repoPath);
19516
19552
  ran.push("agent-config");
19517
19553
  const watched = await watchRepo({
19518
- supabase: authed.supabase,
19519
- orgId: authed.orgId,
19554
+ supabase: authed2.supabase,
19555
+ orgId: authed2.orgId,
19520
19556
  projectId: project.projectId,
19521
19557
  repoSlug: git4.repoSlug,
19522
19558
  branch: git4.branch,
@@ -19583,18 +19619,18 @@ Your board: ${webUrl}`);
19583
19619
  startGitHubAppInstall: () => (deps.buildInstallUrl ?? buildInstallUrl)({
19584
19620
  supabaseUrl: cfg.supabaseUrl,
19585
19621
  accessToken: getCurrentAccessToken() ?? "",
19586
- orgId: authed.orgId
19622
+ orgId: authed2.orgId
19587
19623
  }),
19588
19624
  waitForInstallation: () => (deps.waitForInstallation ?? waitForInstallation)({
19589
- hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed.supabase, authed.orgId),
19625
+ hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed2.supabase, authed2.orgId),
19590
19626
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
19591
19627
  now: () => Date.now(),
19592
19628
  log
19593
19629
  }),
19594
19630
  watchRepo: async () => {
19595
19631
  const w = await watchRepo({
19596
- supabase: authed.supabase,
19597
- orgId: authed.orgId,
19632
+ supabase: authed2.supabase,
19633
+ orgId: authed2.orgId,
19598
19634
  projectId: project.projectId,
19599
19635
  repoSlug: git4.repoSlug,
19600
19636
  branch: git4.branch,
@@ -19616,8 +19652,8 @@ Your board: ${webUrl}`);
19616
19652
  });
19617
19653
  if (!target.ok) return { ok: false, reason: target.error };
19618
19654
  const res = await (deps.registerLocalWatch ?? registerLocalWatch)({
19619
- supabase: authed.supabase,
19620
- orgId: authed.orgId,
19655
+ supabase: authed2.supabase,
19656
+ orgId: authed2.orgId,
19621
19657
  projectId: project.projectId,
19622
19658
  repoPath: git4.root,
19623
19659
  originUrl: git4.originUrl,
@@ -19826,12 +19862,575 @@ var init_dispatch4 = __esm({
19826
19862
  }
19827
19863
  });
19828
19864
 
19865
+ // src/uninstall/edits.ts
19866
+ function stripArchmeClaudeBlock(content) {
19867
+ const start = content.indexOf(START_MARKER);
19868
+ if (start === -1) return UNCHANGED;
19869
+ const end = content.indexOf(END_MARKER, start);
19870
+ if (end === -1) return UNCHANGED;
19871
+ const before = content.slice(0, start).replace(/\n+$/, "");
19872
+ const after = content.slice(end + END_MARKER.length).replace(/^\n+/, "");
19873
+ if (!before.trim() && !after.trim()) return { kind: "delete" };
19874
+ if (!before.trim()) return { kind: "rewrite", content: `${after.replace(/\n*$/, "")}
19875
+ ` };
19876
+ if (!after.trim()) return { kind: "rewrite", content: `${before}
19877
+ ` };
19878
+ return { kind: "rewrite", content: `${before}
19879
+
19880
+ ${after.replace(/\n*$/, "")}
19881
+ ` };
19882
+ }
19883
+ function parseObject(raw) {
19884
+ try {
19885
+ const parsed = JSON.parse(raw);
19886
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
19887
+ return parsed;
19888
+ } catch {
19889
+ return null;
19890
+ }
19891
+ }
19892
+ function render(value) {
19893
+ return `${JSON.stringify(value, null, 2)}
19894
+ `;
19895
+ }
19896
+ function stripArchmeMcpServer(content) {
19897
+ const root = parseObject(content);
19898
+ if (!root) return UNCHANGED;
19899
+ const servers = root.mcpServers;
19900
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) return UNCHANGED;
19901
+ const entries = servers;
19902
+ if (!(MCP_SERVER_NAME2 in entries)) return UNCHANGED;
19903
+ delete entries[MCP_SERVER_NAME2];
19904
+ const onlyKey = Object.keys(root).length === 1 && "mcpServers" in root;
19905
+ if (onlyKey && Object.keys(entries).length === 0) return { kind: "delete" };
19906
+ return { kind: "rewrite", content: render(root) };
19907
+ }
19908
+ function isArchmeHook(hook) {
19909
+ const command = hook?.command;
19910
+ return typeof command === "string" && command.includes(NUDGE_HOOK_FILE);
19911
+ }
19912
+ function stripArchmeHooks(content) {
19913
+ const root = parseObject(content);
19914
+ if (!root) return UNCHANGED;
19915
+ const hooks = root.hooks;
19916
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return UNCHANGED;
19917
+ const byEvent = hooks;
19918
+ const pre = byEvent.PreToolUse;
19919
+ if (!Array.isArray(pre)) return UNCHANGED;
19920
+ let changed = false;
19921
+ const kept = [];
19922
+ for (const entry of pre) {
19923
+ const inner = entry?.hooks;
19924
+ if (!Array.isArray(inner) || !inner.some(isArchmeHook)) {
19925
+ kept.push(entry);
19926
+ continue;
19927
+ }
19928
+ changed = true;
19929
+ const survivors = inner.filter((h2) => !isArchmeHook(h2));
19930
+ if (survivors.length > 0) kept.push({ ...entry, hooks: survivors });
19931
+ }
19932
+ if (!changed) return UNCHANGED;
19933
+ if (kept.length > 0) byEvent.PreToolUse = kept;
19934
+ else delete byEvent.PreToolUse;
19935
+ if (Object.keys(byEvent).length === 0) delete root.hooks;
19936
+ if (Object.keys(root).length === 0) return { kind: "delete" };
19937
+ return { kind: "rewrite", content: render(root) };
19938
+ }
19939
+ var UNCHANGED, START_MARKER, END_MARKER, NUDGE_HOOK_FILE, MCP_SERVER_NAME2;
19940
+ var init_edits = __esm({
19941
+ "src/uninstall/edits.ts"() {
19942
+ "use strict";
19943
+ UNCHANGED = { kind: "unchanged" };
19944
+ START_MARKER = "<!-- ARCHME-START -->";
19945
+ END_MARKER = "<!-- ARCHME-END -->";
19946
+ NUDGE_HOOK_FILE = "archme-search-nudge.mjs";
19947
+ MCP_SERVER_NAME2 = "archme";
19948
+ }
19949
+ });
19950
+
19951
+ // src/uninstall/watching.ts
19952
+ function decideDaemonRemoval(input) {
19953
+ if (!input.installed) {
19954
+ return { action: "keep", reason: "no daemon service is installed on this machine" };
19955
+ }
19956
+ if (input.keepWatching) {
19957
+ return { action: "keep", reason: "--keep-watching: this repo is still being watched" };
19958
+ }
19959
+ if (input.remaining.watchedRepos === null) {
19960
+ return {
19961
+ action: "keep",
19962
+ reason: "could not check what else it serves, so it was left running"
19963
+ };
19964
+ }
19965
+ const { watchedRepos, localEntries } = input.remaining;
19966
+ if (watchedRepos > 0 || localEntries > 0) {
19967
+ const parts = [];
19968
+ if (watchedRepos > 0) parts.push(`${watchedRepos} watched repo${watchedRepos === 1 ? "" : "s"}`);
19969
+ if (localEntries > 0) {
19970
+ parts.push(`${localEntries} locally-polled repo${localEntries === 1 ? "" : "s"}`);
19971
+ }
19972
+ return { action: "keep", reason: `still serving ${parts.join(" and ")}` };
19973
+ }
19974
+ return { action: "remove", reason: "nothing is left for it to do" };
19975
+ }
19976
+ function rowsForSlug(rows, slug) {
19977
+ const wanted = slug.toLowerCase();
19978
+ return rows.filter((r) => r.slug.toLowerCase() === wanted);
19979
+ }
19980
+ var init_watching = __esm({
19981
+ "src/uninstall/watching.ts"() {
19982
+ "use strict";
19983
+ }
19984
+ });
19985
+
19986
+ // src/uninstall/backend.ts
19987
+ var backend_exports = {};
19988
+ __export(backend_exports, {
19989
+ createDefaultBackend: () => createDefaultBackend
19990
+ });
19991
+ function labelFor(row) {
19992
+ const org = row.projects?.organizations?.slug;
19993
+ const project = row.projects?.slug;
19994
+ const where = org && project ? `${org}/${project}` : `${row.repo_owner}/${row.repo_name}`;
19995
+ return `${where} @ ${row.branch}`;
19996
+ }
19997
+ function createDefaultBackend() {
19998
+ let clientPromise = null;
19999
+ const supabase = () => clientPromise ??= authed();
20000
+ return {
20001
+ async gitRoot(dir) {
20002
+ try {
20003
+ const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: dir });
20004
+ return stdout.trim() || null;
20005
+ } catch {
20006
+ return null;
20007
+ }
20008
+ },
20009
+ async repoSlug(repoPath) {
20010
+ const origin = await readOriginUrl(repoPath);
20011
+ return origin ? parseGitHubRepo(origin) : null;
20012
+ },
20013
+ async listWatchedRepos() {
20014
+ const { supabase: client } = await supabase();
20015
+ const { data, error } = await client.from("watched_repos").select("id, repo_owner, repo_name, branch, projects(slug, organizations(slug))").is("deleted_at", null);
20016
+ if (error) throw new Error(error.message);
20017
+ return (data ?? []).map((row) => ({
20018
+ id: row.id,
20019
+ slug: `${row.repo_owner}/${row.repo_name}`,
20020
+ label: labelFor(row)
20021
+ }));
20022
+ },
20023
+ async unwatch(ids) {
20024
+ if (ids.length === 0) return;
20025
+ const { supabase: client } = await supabase();
20026
+ const now = (/* @__PURE__ */ new Date()).toISOString();
20027
+ const { error } = await client.from("watched_repos").update({ deleted_at: now, updated_at: now }).in("id", ids);
20028
+ if (error) throw new Error(error.message);
20029
+ },
20030
+ async localRepoSlugs() {
20031
+ return (await loadLocalRepos()).map((e) => slugOf(e));
20032
+ },
20033
+ async removeLocalRepos(slug) {
20034
+ const repos = await loadLocalRepos();
20035
+ const kept = removeLocalReposBySlug(repos, slug);
20036
+ const dropped = repos.length - kept.length;
20037
+ if (dropped > 0) await saveLocalRepos(kept);
20038
+ return dropped;
20039
+ },
20040
+ async daemonInstalled() {
20041
+ const { pickController: pickController2 } = await Promise.resolve().then(() => (init_serviceController(), serviceController_exports));
20042
+ return await pickController2().unitExists();
20043
+ },
20044
+ async removeDaemon() {
20045
+ const { pickController: pickController2, orchestrateStop: orchestrateStop2 } = await Promise.resolve().then(() => (init_serviceController(), serviceController_exports));
20046
+ const controller = pickController2();
20047
+ await orchestrateStop2(controller);
20048
+ await controller.uninstall();
20049
+ }
20050
+ };
20051
+ }
20052
+ async function authed() {
20053
+ const cfg = resolveConfig();
20054
+ return await createAuthedClient({
20055
+ supabaseUrl: cfg.supabaseUrl,
20056
+ supabaseAnonKey: cfg.supabaseAnonKey,
20057
+ clientId: cfg.archmeDaemonClientId,
20058
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
20059
+ });
20060
+ }
20061
+ var init_backend = __esm({
20062
+ "src/uninstall/backend.ts"() {
20063
+ "use strict";
20064
+ init_execa();
20065
+ init_authedClient();
20066
+ init_defaults();
20067
+ init_localGit();
20068
+ init_github_remote();
20069
+ init_localRepos();
20070
+ }
20071
+ });
20072
+
20073
+ // src/uninstall/run-uninstall.ts
20074
+ var run_uninstall_exports = {};
20075
+ __export(run_uninstall_exports, {
20076
+ EX_OK: () => EX_OK3,
20077
+ EX_USAGE: () => EX_USAGE3,
20078
+ runUninstall: () => runUninstall
20079
+ });
20080
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
20081
+ import { join as join23 } from "node:path";
20082
+ function readIfExists(path7) {
20083
+ try {
20084
+ return existsSync3(path7) ? readFileSync5(path7, "utf8") : null;
20085
+ } catch {
20086
+ return null;
20087
+ }
20088
+ }
20089
+ function isParseableObject(raw) {
20090
+ try {
20091
+ const parsed = JSON.parse(raw);
20092
+ return Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed);
20093
+ } catch {
20094
+ return false;
20095
+ }
20096
+ }
20097
+ function applyEdit(absolute, edit) {
20098
+ if (edit.kind === "rewrite") writeFileSync3(absolute, edit.content);
20099
+ else if (edit.kind === "delete") rmSync(absolute, { force: true });
20100
+ }
20101
+ async function runUninstall(opts) {
20102
+ const log = opts.log ?? ((m) => console.log(m));
20103
+ const error = opts.error ?? ((m) => console.error(m));
20104
+ const backend = opts.backend ?? await defaultBackend();
20105
+ const repoPath = await backend.gitRoot(opts.repoPath).catch(() => null) ?? opts.repoPath;
20106
+ if (repoPath !== opts.repoPath) log(`Using the repo root: ${repoPath}`);
20107
+ const abs = (rel) => join23(repoPath, rel);
20108
+ const actions = [];
20109
+ const skipped = [];
20110
+ const mcpRaw = readIfExists(abs(".mcp.json"));
20111
+ if (mcpRaw !== null) {
20112
+ const edit = stripArchmeMcpServer(mcpRaw);
20113
+ if (edit.kind !== "unchanged") {
20114
+ actions.push({
20115
+ path: ".mcp.json",
20116
+ label: edit.kind === "delete" ? `delete (it holds only the ${MCP_SERVER_NAME2} server)` : `remove the ${MCP_SERVER_NAME2} server`,
20117
+ apply: () => applyEdit(abs(".mcp.json"), edit)
20118
+ });
20119
+ } else if (!isParseableObject(mcpRaw)) {
20120
+ skipped.push(
20121
+ `.mcp.json \u2014 could not be parsed as JSON; if it has an ${MCP_SERVER_NAME2} server, remove it by hand`
20122
+ );
20123
+ }
20124
+ }
20125
+ const claudeRaw = readIfExists(abs("CLAUDE.md"));
20126
+ if (claudeRaw !== null) {
20127
+ const edit = stripArchmeClaudeBlock(claudeRaw);
20128
+ if (edit.kind !== "unchanged") {
20129
+ actions.push({
20130
+ path: "CLAUDE.md",
20131
+ label: edit.kind === "delete" ? "delete (it holds only the ArchitectMe block)" : "remove the ArchitectMe block",
20132
+ apply: () => applyEdit(abs("CLAUDE.md"), edit)
20133
+ });
20134
+ } else if (claudeRaw.includes(START_MARKER)) {
20135
+ skipped.push("CLAUDE.md \u2014 the ArchitectMe block has no closing marker; remove it by hand");
20136
+ }
20137
+ }
20138
+ const skillPath = ".claude/skills/architecture/SKILL.md";
20139
+ if (existsSync3(abs(skillPath))) {
20140
+ actions.push({
20141
+ path: skillPath,
20142
+ label: "delete the architecture skill",
20143
+ apply: () => rmSync(abs(skillPath), { force: true })
20144
+ });
20145
+ }
20146
+ const hookPath = `.claude/hooks/${NUDGE_HOOK_FILE}`;
20147
+ if (existsSync3(abs(hookPath))) {
20148
+ actions.push({
20149
+ path: hookPath,
20150
+ label: "delete the retired search-nudge hook",
20151
+ apply: () => rmSync(abs(hookPath), { force: true })
20152
+ });
20153
+ }
20154
+ const settingsRaw = readIfExists(abs(".claude/settings.json"));
20155
+ if (settingsRaw !== null) {
20156
+ const edit = stripArchmeHooks(settingsRaw);
20157
+ if (edit.kind !== "unchanged") {
20158
+ actions.push({
20159
+ path: ".claude/settings.json",
20160
+ label: edit.kind === "delete" ? "delete (it holds only the ArchitectMe hook)" : "unregister the search-nudge hook",
20161
+ apply: () => applyEdit(abs(".claude/settings.json"), edit)
20162
+ });
20163
+ } else if (!isParseableObject(settingsRaw)) {
20164
+ skipped.push(
20165
+ ".claude/settings.json \u2014 could not be parsed as JSON; if it registers the ArchMe hook, remove that entry by hand"
20166
+ );
20167
+ }
20168
+ }
20169
+ for (const note of skipped) log(`Left alone: ${note}`);
20170
+ const keepWatching = opts.keepWatching ?? false;
20171
+ const slug = keepWatching ? null : await backend.repoSlug(repoPath).catch(() => null);
20172
+ let allWatched = null;
20173
+ let watchLookupError = null;
20174
+ if (slug) {
20175
+ try {
20176
+ allWatched = await backend.listWatchedRepos();
20177
+ } catch (err2) {
20178
+ watchLookupError = err2 instanceof Error ? err2.message : String(err2);
20179
+ }
20180
+ }
20181
+ const toUnwatch = allWatched ? rowsForSlug(allWatched, slug) : [];
20182
+ let localForSlug = 0;
20183
+ let allLocal = [];
20184
+ if (slug) {
20185
+ allLocal = await backend.localRepoSlugs().catch(() => []);
20186
+ localForSlug = allLocal.filter((s) => s.toLowerCase() === slug.toLowerCase()).length;
20187
+ }
20188
+ if (actions.length === 0 && toUnwatch.length === 0 && localForSlug === 0) {
20189
+ log(`No ArchitectMe agent config found in ${repoPath}. Nothing to remove.`);
20190
+ if (watchLookupError) log(watchingUnknownNote(watchLookupError));
20191
+ return { exitCode: EX_OK3, removed: [], unwatched: [] };
20192
+ }
20193
+ const daemonInstalled = await backend.daemonInstalled().catch(() => false);
20194
+ const plan = [
20195
+ ...actions.map((a2) => ` ${a2.path} \u2014 ${a2.label}`),
20196
+ ...toUnwatch.map((r) => ` ${r.label} \u2014 stop watching (no more commits will update it)`),
20197
+ ...localForSlug > 0 ? [` ${slug} \u2014 stop polling from this machine (${localForSlug} registry entry)`] : [],
20198
+ // Stated up front, not just reported after: a repo-scoped command removing
20199
+ // a machine-wide service is the most surprising thing here. Whether it
20200
+ // actually goes is still decided afterwards, against what really happened.
20201
+ ...daemonInstalled && !keepWatching ? [" the daemon service \u2014 removed only if this leaves it nothing to serve"] : []
20202
+ ];
20203
+ if (!opts.yes) {
20204
+ const interactive = opts.interactive ?? stdinIsInteractive();
20205
+ if (!interactive && !opts.confirm) {
20206
+ error(
20207
+ "archme uninstall: refusing to edit files without confirmation.\nThere is nobody to ask (stdin is not a terminal). Re-run with --yes."
20208
+ );
20209
+ return { exitCode: EX_USAGE3, removed: [], unwatched: [] };
20210
+ }
20211
+ const confirm = opts.confirm ?? (async (lines) => {
20212
+ log(`This will change, in ${repoPath} and on your ArchMe account:
20213
+ `);
20214
+ lines.forEach((l) => log(l));
20215
+ log("");
20216
+ return await promptYesNo("Uninstall ArchitectMe for this repo?");
20217
+ });
20218
+ if (!await confirm(plan)) {
20219
+ log("Cancelled \u2014 nothing was changed.");
20220
+ return { exitCode: EX_OK3, removed: [], unwatched: [] };
20221
+ }
20222
+ }
20223
+ const removed = [];
20224
+ for (const action of actions) {
20225
+ action.apply();
20226
+ removed.push(action.path);
20227
+ log(`Removed: ${action.path} \u2014 ${action.label}`);
20228
+ }
20229
+ for (const dir of PRUNABLE_DIRS) {
20230
+ const path7 = abs(dir);
20231
+ try {
20232
+ if (existsSync3(path7) && readdirSync(path7).length === 0) rmSync(path7, { recursive: true });
20233
+ } catch {
20234
+ }
20235
+ }
20236
+ const unwatched = [];
20237
+ if (toUnwatch.length > 0) {
20238
+ try {
20239
+ await backend.unwatch(toUnwatch.map((r) => r.id));
20240
+ unwatched.push(...toUnwatch.map((r) => r.id));
20241
+ for (const row of toUnwatch) log(`Stopped watching: ${row.label}`);
20242
+ } catch (err2) {
20243
+ watchLookupError = err2 instanceof Error ? err2.message : String(err2);
20244
+ }
20245
+ }
20246
+ let localRemoved = 0;
20247
+ if (slug && localForSlug > 0) {
20248
+ try {
20249
+ localRemoved = await backend.removeLocalRepos(slug);
20250
+ if (localRemoved > 0) log(`Stopped local polling: ${slug}`);
20251
+ } catch {
20252
+ log(`Could not update the local watch registry \u2014 \`archme local-watch --stop ${slug}\` will.`);
20253
+ }
20254
+ }
20255
+ const unwatchFailed = watchLookupError !== null;
20256
+ const remaining = {
20257
+ watchedRepos: allWatched && !unwatchFailed ? allWatched.length - unwatched.length : null,
20258
+ localEntries: allLocal.length - localRemoved
20259
+ };
20260
+ const daemonPlan = decideDaemonRemoval({ installed: daemonInstalled, keepWatching, remaining });
20261
+ if (daemonPlan.action === "remove") {
20262
+ try {
20263
+ await backend.removeDaemon();
20264
+ log(`Removed the daemon service \u2014 ${daemonPlan.reason}.`);
20265
+ } catch (err2) {
20266
+ log(
20267
+ `Could not remove the daemon service (${err2 instanceof Error ? err2.message : String(err2)}).
20268
+ archme daemon uninstall`
20269
+ );
20270
+ }
20271
+ } else {
20272
+ log(`Daemon left in place \u2014 ${daemonPlan.reason}.`);
20273
+ }
20274
+ log(
20275
+ summary({
20276
+ keepWatching,
20277
+ slug,
20278
+ unwatchFailed,
20279
+ watchLookupError,
20280
+ removedConfig: removed.length > 0,
20281
+ checkedAndNotWatched: Boolean(slug) && !unwatchFailed && toUnwatch.length === 0
20282
+ })
20283
+ );
20284
+ return { exitCode: EX_OK3, removed, unwatched };
20285
+ }
20286
+ function watchingUnknownNote(reason) {
20287
+ return `
20288
+ Could not check whether this repo is still being watched (${reason}).
20289
+ If it is, commits will keep updating the architecture. Sign in and re-run to stop it:
20290
+ archme login && archme uninstall`;
20291
+ }
20292
+ function summary(input) {
20293
+ const lines = input.removedConfig ? ["\nArchitectMe agent config removed from this repo."] : ["\nNo ArchitectMe agent config was here to remove."];
20294
+ if (input.keepWatching) {
20295
+ lines.push(
20296
+ "This repo is STILL being watched (--keep-watching), so commits keep updating its architecture."
20297
+ );
20298
+ } else if (input.unwatchFailed && input.watchLookupError) {
20299
+ lines.push(watchingUnknownNote(input.watchLookupError).trimStart());
20300
+ } else if (!input.slug) {
20301
+ lines.push(
20302
+ "No github.com remote here, so there was no watched repo to stop. Check archme.io \u2192 Settings \u2192 Integrations if you expected one."
20303
+ );
20304
+ } else if (input.checkedAndNotWatched) {
20305
+ lines.push(`${input.slug} was not being watched, so no commits were updating it.`);
20306
+ }
20307
+ lines.push(
20308
+ "\nYour architecture on archme.io and this machine's login are untouched:",
20309
+ " archme logout sign this machine out",
20310
+ "\nThe GitHub App installation is org-wide, so it stays. Remove it on github.com if you want it gone."
20311
+ );
20312
+ return lines.join("\n");
20313
+ }
20314
+ async function defaultBackend() {
20315
+ const { createDefaultBackend: createDefaultBackend2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
20316
+ return createDefaultBackend2();
20317
+ }
20318
+ var EX_OK3, EX_USAGE3, PRUNABLE_DIRS;
20319
+ var init_run_uninstall = __esm({
20320
+ "src/uninstall/run-uninstall.ts"() {
20321
+ "use strict";
20322
+ init_prompt();
20323
+ init_edits();
20324
+ init_watching();
20325
+ EX_OK3 = 0;
20326
+ EX_USAGE3 = 78;
20327
+ PRUNABLE_DIRS = [
20328
+ ".claude/skills/architecture",
20329
+ ".claude/skills",
20330
+ ".claude/hooks",
20331
+ ".claude"
20332
+ ];
20333
+ }
20334
+ });
20335
+
20336
+ // src/uninstall/dispatch.ts
20337
+ var dispatch_exports5 = {};
20338
+ __export(dispatch_exports5, {
20339
+ EX_INFRA: () => EX_INFRA3,
20340
+ EX_OK: () => EX_OK4,
20341
+ EX_USAGE: () => EX_USAGE4,
20342
+ dispatchUninstall: () => dispatchUninstall,
20343
+ parseUninstallArgs: () => parseUninstallArgs,
20344
+ printUninstallHelp: () => printUninstallHelp
20345
+ });
20346
+ import { resolve as resolve5 } from "node:path";
20347
+ function parseUninstallArgs(argv) {
20348
+ if (argv.some((a2) => a2 === "--help" || a2 === "-h" || a2 === "help")) return { kind: "help" };
20349
+ let repoPath;
20350
+ let yes = false;
20351
+ let keepWatching = false;
20352
+ for (const arg of argv) {
20353
+ if (arg === "--yes" || arg === "-y") {
20354
+ yes = true;
20355
+ } else if (arg === "--keep-watching") {
20356
+ keepWatching = true;
20357
+ } else if (arg.startsWith("-")) {
20358
+ return { kind: "error", message: `unknown option '${arg}'` };
20359
+ } else if (repoPath === void 0) {
20360
+ repoPath = arg;
20361
+ } else {
20362
+ return { kind: "error", message: `unexpected argument '${arg}'` };
20363
+ }
20364
+ }
20365
+ return { kind: "run", repoPath, yes, keepWatching };
20366
+ }
20367
+ function printUninstallHelp() {
20368
+ console.log(`archme uninstall \u2014 stop ArchitectMe for a repo
20369
+
20370
+ Undoes what \`archme init\` set up, and shows you the whole list before it does:
20371
+
20372
+ 1. Agent config \u2014 the archme MCP server in .mcp.json, the ArchitectMe block in
20373
+ CLAUDE.md, the architecture skill, and the retired search-nudge hook older
20374
+ versions registered in .claude/settings.json.
20375
+ 2. Watching \u2014 stops this repo producing jobs, both via the GitHub App and via
20376
+ local polling. Without this the board keeps updating while looking removed.
20377
+ 3. The daemon \u2014 removed only if it has no other repo left to serve. It is one
20378
+ service per account, so uninstalling one repo never stops the others.
20379
+
20380
+ Your architecture on archme.io and this machine's login survive. The GitHub App
20381
+ installation is org-wide and stays; remove it on github.com if you want it gone.
20382
+
20383
+ Usage:
20384
+ archme uninstall [repo-path] [options]
20385
+
20386
+ repo-path Defaults to the current directory.
20387
+
20388
+ Options:
20389
+ -y, --yes Do not ask for confirmation
20390
+ --keep-watching Remove the agent config only; leave the repo watched
20391
+ and the daemon running
20392
+ -h, --help Show this help`);
20393
+ }
20394
+ async function dispatchUninstall(argv) {
20395
+ const parsed = parseUninstallArgs(argv);
20396
+ if (parsed.kind === "help") {
20397
+ printUninstallHelp();
20398
+ return EX_OK4;
20399
+ }
20400
+ if (parsed.kind === "error") {
20401
+ console.error(`archme uninstall: ${parsed.message}`);
20402
+ printUninstallHelp();
20403
+ return EX_USAGE4;
20404
+ }
20405
+ try {
20406
+ const { runUninstall: runUninstall2 } = await Promise.resolve().then(() => (init_run_uninstall(), run_uninstall_exports));
20407
+ const result = await runUninstall2({
20408
+ repoPath: resolve5(parsed.repoPath ?? process.cwd()),
20409
+ yes: parsed.yes,
20410
+ keepWatching: parsed.keepWatching
20411
+ });
20412
+ return result.exitCode;
20413
+ } catch (err2) {
20414
+ console.error(`archme uninstall: ${err2 instanceof Error ? err2.message : String(err2)}`);
20415
+ return EX_INFRA3;
20416
+ }
20417
+ }
20418
+ var EX_OK4, EX_INFRA3, EX_USAGE4;
20419
+ var init_dispatch5 = __esm({
20420
+ "src/uninstall/dispatch.ts"() {
20421
+ "use strict";
20422
+ EX_OK4 = 0;
20423
+ EX_INFRA3 = 3;
20424
+ EX_USAGE4 = 78;
20425
+ }
20426
+ });
20427
+
19829
20428
  // src/daemon/localWatchDispatch.ts
19830
20429
  var localWatchDispatch_exports = {};
19831
20430
  __export(localWatchDispatch_exports, {
19832
- EX_INFRA: () => EX_INFRA3,
19833
- EX_OK: () => EX_OK3,
19834
- EX_USAGE: () => EX_USAGE3,
20431
+ EX_INFRA: () => EX_INFRA4,
20432
+ EX_OK: () => EX_OK5,
20433
+ EX_USAGE: () => EX_USAGE5,
19835
20434
  dispatchLocalWatch: () => dispatchLocalWatch,
19836
20435
  parseLocalWatchArgs: () => parseLocalWatchArgs,
19837
20436
  printLocalWatchHelp: () => printLocalWatchHelp
@@ -19921,12 +20520,12 @@ async function dispatchLocalWatch(argv) {
19921
20520
  const parsed = parseLocalWatchArgs(argv);
19922
20521
  if (parsed.kind === "help") {
19923
20522
  printLocalWatchHelp();
19924
- return EX_OK3;
20523
+ return EX_OK5;
19925
20524
  }
19926
20525
  if (parsed.kind === "error") {
19927
20526
  console.error(parsed.message);
19928
20527
  printLocalWatchHelp();
19929
- return EX_USAGE3;
20528
+ return EX_USAGE5;
19930
20529
  }
19931
20530
  const mod = await Promise.resolve().then(() => (init_localWatch(), localWatch_exports));
19932
20531
  switch (parsed.kind) {
@@ -19940,13 +20539,13 @@ async function dispatchLocalWatch(argv) {
19940
20539
  return await mod.runLocalWatch(parsed);
19941
20540
  }
19942
20541
  }
19943
- var EX_OK3, EX_INFRA3, EX_USAGE3, PROJECT_SLUG_RE3, REPO_SLUG_RE;
20542
+ var EX_OK5, EX_INFRA4, EX_USAGE5, PROJECT_SLUG_RE3, REPO_SLUG_RE;
19944
20543
  var init_localWatchDispatch = __esm({
19945
20544
  "src/daemon/localWatchDispatch.ts"() {
19946
20545
  "use strict";
19947
- EX_OK3 = 0;
19948
- EX_INFRA3 = 3;
19949
- EX_USAGE3 = 78;
20546
+ EX_OK5 = 0;
20547
+ EX_INFRA4 = 3;
20548
+ EX_USAGE5 = 78;
19950
20549
  PROJECT_SLUG_RE3 = /^[^/\s]+\/[^/\s]+$/;
19951
20550
  REPO_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
19952
20551
  }
@@ -19954,7 +20553,7 @@ var init_localWatchDispatch = __esm({
19954
20553
 
19955
20554
  // src/doctor/collect.ts
19956
20555
  import { readFile as readFile13 } from "node:fs/promises";
19957
- import { join as join23 } from "node:path";
20556
+ import { join as join24 } from "node:path";
19958
20557
  function binaryPresent(name) {
19959
20558
  try {
19960
20559
  requireBinary(name);
@@ -19970,7 +20569,7 @@ async function git3(cwd, args) {
19970
20569
  async function readMcpPin(repoRoot) {
19971
20570
  const empty = { installed: false, pin: { project: null, branch: null } };
19972
20571
  try {
19973
- const raw = await readFile13(join23(repoRoot, ".mcp.json"), "utf8");
20572
+ const raw = await readFile13(join24(repoRoot, ".mcp.json"), "utf8");
19974
20573
  const parsed = JSON.parse(raw);
19975
20574
  const servers = parsed.mcpServers ?? {};
19976
20575
  const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
@@ -20038,17 +20637,17 @@ async function collectFacts(cwd) {
20038
20637
  );
20039
20638
  try {
20040
20639
  const cfg = resolveConfig();
20041
- const authed = await createAuthedClient({
20640
+ const authed2 = await createAuthedClient({
20042
20641
  supabaseUrl: cfg.supabaseUrl,
20043
20642
  supabaseAnonKey: cfg.supabaseAnonKey,
20044
20643
  clientId: cfg.archmeDaemonClientId,
20045
20644
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
20046
20645
  });
20047
- if (!authed.orgId) return facts;
20646
+ if (!authed2.orgId) return facts;
20048
20647
  facts.signedIn = true;
20049
- facts.orgId = authed.orgId;
20050
- facts.identity = authed.identity;
20051
- const supabase = authed.supabase;
20648
+ facts.orgId = authed2.orgId;
20649
+ facts.identity = authed2.identity;
20650
+ const supabase = authed2.supabase;
20052
20651
  const slug = facts.pinnedProject;
20053
20652
  if (slug) {
20054
20653
  const [orgSlug, projectSlug] = slug.split("/");
@@ -20334,18 +20933,18 @@ optional.`;
20334
20933
  });
20335
20934
 
20336
20935
  // src/doctor/dispatch.ts
20337
- var dispatch_exports5 = {};
20338
- __export(dispatch_exports5, {
20339
- EX_INFRA: () => EX_INFRA4,
20340
- EX_OK: () => EX_OK4,
20341
- EX_USAGE: () => EX_USAGE4,
20936
+ var dispatch_exports6 = {};
20937
+ __export(dispatch_exports6, {
20938
+ EX_INFRA: () => EX_INFRA5,
20939
+ EX_OK: () => EX_OK6,
20940
+ EX_USAGE: () => EX_USAGE6,
20342
20941
  dispatchDoctor: () => dispatchDoctor,
20343
20942
  parseDoctorArgs: () => parseDoctorArgs,
20344
20943
  printDoctorHelp: () => printDoctorHelp
20345
20944
  });
20346
20945
  import { mkdtemp as mkdtemp5, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
20347
20946
  import { tmpdir as tmpdir5 } from "node:os";
20348
- import { join as join24 } from "node:path";
20947
+ import { join as join25 } from "node:path";
20349
20948
  function parseDoctorArgs(argv) {
20350
20949
  const args = { plain: false, help: false };
20351
20950
  for (const a2 of argv) {
@@ -20377,11 +20976,11 @@ async function dispatchDoctor(argv) {
20377
20976
  if (!parsed.ok) {
20378
20977
  console.error(parsed.message);
20379
20978
  printDoctorHelp();
20380
- return EX_USAGE4;
20979
+ return EX_USAGE6;
20381
20980
  }
20382
20981
  if (parsed.args.help) {
20383
20982
  printDoctorHelp();
20384
- return EX_OK4;
20983
+ return EX_OK6;
20385
20984
  }
20386
20985
  const facts = await collectFacts(process.cwd());
20387
20986
  const issues = diagnose(facts);
@@ -20397,17 +20996,17 @@ async function dispatchDoctor(argv) {
20397
20996
  console.log("");
20398
20997
  }
20399
20998
  }
20400
- if (parsed.args.plain) return EX_OK4;
20999
+ if (parsed.args.plain) return EX_OK6;
20401
21000
  try {
20402
21001
  requireBinary("claude");
20403
21002
  } catch {
20404
21003
  console.log(
20405
21004
  "Install Claude Code to ask about any of this: npm i -g @anthropic-ai/claude-code"
20406
21005
  );
20407
- return EX_OK4;
21006
+ return EX_OK6;
20408
21007
  }
20409
- const dir = await mkdtemp5(join24(tmpdir5(), "archme-doctor-"));
20410
- const mcpConfigPath = join24(dir, "mcp.json");
21008
+ const dir = await mkdtemp5(join25(tmpdir5(), "archme-doctor-"));
21009
+ const mcpConfigPath = join25(dir, "mcp.json");
20411
21010
  await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
20412
21011
  try {
20413
21012
  const result = await spawnClaude({
@@ -20420,13 +21019,13 @@ async function dispatchDoctor(argv) {
20420
21019
  // is a much larger surface than the problem justifies.
20421
21020
  allowedTools: ["Read", "Glob", "Grep"]
20422
21021
  });
20423
- return result.exitCode === 0 ? EX_OK4 : EX_INFRA4;
21022
+ return result.exitCode === 0 ? EX_OK6 : EX_INFRA5;
20424
21023
  } finally {
20425
21024
  await rm9(dir, { recursive: true, force: true });
20426
21025
  }
20427
21026
  }
20428
- var EX_OK4, EX_INFRA4, EX_USAGE4;
20429
- var init_dispatch5 = __esm({
21027
+ var EX_OK6, EX_INFRA5, EX_USAGE6;
21028
+ var init_dispatch6 = __esm({
20430
21029
  "src/doctor/dispatch.ts"() {
20431
21030
  "use strict";
20432
21031
  init_collect();
@@ -20434,9 +21033,9 @@ var init_dispatch5 = __esm({
20434
21033
  init_prompt2();
20435
21034
  init_spawn_claude();
20436
21035
  init_preflight();
20437
- EX_OK4 = 0;
20438
- EX_INFRA4 = 3;
20439
- EX_USAGE4 = 78;
21036
+ EX_OK6 = 0;
21037
+ EX_INFRA5 = 3;
21038
+ EX_USAGE6 = 78;
20440
21039
  }
20441
21040
  });
20442
21041
 
@@ -20494,6 +21093,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
20494
21093
  "keys",
20495
21094
  "scan",
20496
21095
  "init",
21096
+ "uninstall",
20497
21097
  "local-watch",
20498
21098
  "doctor"
20499
21099
  ]);
@@ -20592,12 +21192,16 @@ async function main(argv) {
20592
21192
  const { dispatchInit: dispatchInit2 } = await Promise.resolve().then(() => (init_dispatch4(), dispatch_exports4));
20593
21193
  return await dispatchInit2(r.args);
20594
21194
  }
21195
+ case "uninstall": {
21196
+ const { dispatchUninstall: dispatchUninstall2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
21197
+ return await dispatchUninstall2(r.args);
21198
+ }
20595
21199
  case "local-watch": {
20596
21200
  const { dispatchLocalWatch: dispatchLocalWatch2 } = await Promise.resolve().then(() => (init_localWatchDispatch(), localWatchDispatch_exports));
20597
21201
  return await dispatchLocalWatch2(r.args);
20598
21202
  }
20599
21203
  case "doctor": {
20600
- const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
21204
+ const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch6(), dispatch_exports6));
20601
21205
  return await dispatchDoctor2(r.args);
20602
21206
  }
20603
21207
  case "logout": {
@@ -20624,6 +21228,8 @@ Other commands:
20624
21228
  archme scan <repo-path> Single-pass scan into an explicit project/branch
20625
21229
  archme local-watch [repo-path] Watch a repo without a GitHub App
20626
21230
  (--list|--stop|--resume)
21231
+ archme uninstall [repo-path] Stop ArchitectMe for a repo: config,
21232
+ watching, and the daemon if it's now idle
20627
21233
  archme doctor Check this machine's setup and explain it
20628
21234
  archme login | logout Authenticate with archme
20629
21235
  archme keys list|create|revoke Manage MCP API keys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Map your codebase into a living architecture diagram: run `archme init` in a repo to scan it, `archme daemon start` to keep it current on every commit.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",