@archme/cli 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +26 -0
  2. package/dist/cli.js +706 -98
  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
  });
@@ -8877,7 +8877,7 @@ var init_launchctl = __esm({
8877
8877
  if (r.exitCode === 0 || r.exitCode === 17) return;
8878
8878
  if (r.exitCode === 5) {
8879
8879
  await this.opts.spawner.run("launchctl", ["bootout", SERVICE_TARGET(this.opts.uid)]);
8880
- await new Promise((resolve5) => setTimeout(resolve5, 500));
8880
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
8881
8881
  r = await bootstrap();
8882
8882
  if (r.exitCode === 0 || r.exitCode === 17) return;
8883
8883
  }
@@ -9022,6 +9022,13 @@ var init_systemctl = __esm({
9022
9022
  });
9023
9023
 
9024
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
+ });
9025
9032
  import { userInfo } from "node:os";
9026
9033
  function pickController(platform3 = process.platform, env = process.env, spawner = execaSpawner) {
9027
9034
  const unitPath = resolveUnitPath(platform3, env);
@@ -9256,7 +9263,7 @@ async function tailLog(opts = {}) {
9256
9263
  }
9257
9264
  }
9258
9265
  if (!opts.follow) return;
9259
- await new Promise((resolve5, reject) => {
9266
+ await new Promise((resolve6, reject) => {
9260
9267
  let fileWatcher = null;
9261
9268
  let dirWatcher = null;
9262
9269
  const dir = dirname6(path7);
@@ -9320,7 +9327,7 @@ async function tailLog(opts = {}) {
9320
9327
  const handleSigint = () => {
9321
9328
  fileWatcher?.close();
9322
9329
  dirWatcher?.close();
9323
- resolve5();
9330
+ resolve6();
9324
9331
  };
9325
9332
  process.once("SIGINT", handleSigint);
9326
9333
  });
@@ -12131,21 +12138,32 @@ var init_affected = __esm({
12131
12138
  }
12132
12139
  });
12133
12140
 
12141
+ // ../shared/storyNaming.ts
12142
+ var STORY_NAMING_GUIDANCE;
12143
+ var init_storyNaming = __esm({
12144
+ "../shared/storyNaming.ts"() {
12145
+ STORY_NAMING_GUIDANCE = `Story titles:
12146
+ - **Leaves are actions.** A leaf story is a flow, so title it verb + object, imperative: \`sign up with email\`, \`refund a charge\`, \`retry a failed job\`. Not the noun phrase for the same flow (\`email signup\`, \`charge refunds\`, \`failed job retry\`).
12147
+ - **Groups are areas.** A parent story is a feature area, so title it as a noun phrase naming what its leaves have in common: \`auth\`, \`billing\`, \`job queue\`. Not an action (\`authenticate users\`).
12148
+ - Keep both as short as the hierarchy allows \u2014 the group supplies the context, so a leaf never restates it.
12149
+ - Good: "web app" > "auth" > "sign up with email"
12150
+ - Bad: "new user signs up with email on the web app"
12151
+ - **An existing convention wins.** If a story's siblings already agree on a different form, match them \u2014 cohesion across the tree is the goal, and the rules above are only the default when there is nothing to match. Restyle only titles you are already editing for another reason; a rename lands in someone's diff, so never sweep the tree for form alone.`;
12152
+ }
12153
+ });
12154
+
12134
12155
  // ../shared/scanPrompts.ts
12135
12156
  function buildFormatRefStories(opts) {
12136
12157
  const draftBlock = opts.persistentDrafts ? FORMAT_REF_DRAFT_STORIES_BLOCK : "";
12137
12158
  const uncoveredSubFlowRule = opts.persistentDrafts ? CROSS_REF_UNCOVERED_DRAFT_STUB : CROSS_REF_UNCOVERED_REAL_STORY;
12138
- const hierarchyExample = opts.persistentDrafts ? `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", draft: true, userStories: [{id: "login", title: "Login Flow", draft: true, acceptanceCriteria: ["< 1s response"]}]}]}]` : `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", userStories: [{id: "login", title: "Login Flow", acceptanceCriteria: ["< 1s response"], sequence: "sequenceDiagram\\n ..."}]}]}]`;
12159
+ const hierarchyExample = opts.persistentDrafts ? `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", draft: true, userStories: [{id: "login", title: "log in with email", draft: true, acceptanceCriteria: ["< 1s response"]}]}]}]` : `- Example: storyUpdates: [{add: [{id: "auth", title: "Authentication", description: "User auth flows", userStories: [{id: "login", title: "log in with email", acceptanceCriteria: ["< 1s response"], sequence: "sequenceDiagram\\n ..."}]}]}]`;
12139
12160
  return `Recursive story hierarchy:
12140
12161
  - Stories can contain child stories via userStories: [...] \u2014 parent stories act as feature groups
12141
12162
  - Use top-level stories with description, links, and nested children to organize by feature area
12142
12163
  ${hierarchyExample}
12143
12164
  - Removing a parent story cascades to all descendants
12144
12165
 
12145
- Story titles:
12146
- - Keep titles as short as possible \u2014 rely on the parent group for context.
12147
- - Good: "web app" > "auth" > "sign up with email"
12148
- - Bad: "new user signs up with email on the web app"
12166
+ ${STORY_NAMING_GUIDANCE}
12149
12167
 
12150
12168
  Story description format (markdown, 1-4 sentences \u2014 render target is a narrow side panel):
12151
12169
  - Write for someone who knows the product but isn't reading the code. Say what the system does and what the user/operator experiences at the highest useful altitude, not how it's implemented. A tl;dr, not a rehash.
@@ -12198,14 +12216,15 @@ storyUpdates targeting:
12198
12216
  - add: stories to add or update (merged by ID if story already exists)
12199
12217
  - remove: story IDs to remove
12200
12218
  - Examples:
12201
- - Add at root: storyUpdates: [{add: [{id: "us1", title: "Login", ...}]}]
12202
- - Add under story group: storyUpdates: [{under: "auth", add: [{id: "login", title: "Login Flow", ...}]}]
12219
+ - Add at root: storyUpdates: [{add: [{id: "us1", title: "log in", ...}]}]
12220
+ - Add under story group: storyUpdates: [{under: "auth", add: [{id: "login", title: "log in with email", ...}]}]
12203
12221
  - Remove: storyUpdates: [{remove: ["old_story"]}]
12204
12222
  - Combined: storyUpdates: [{under: "auth", add: [{id: "new_flow", ...}], remove: ["old_flow"]}]`;
12205
12223
  }
12206
12224
  var ANCHOR_RULES, ANCHOR_DISCOVERY, ANCHOR_VERIFICATION, FORMAT_REF_COMPONENTS, FORMAT_REF_CONNECTIONS, FORMAT_REF_DRAFT_STORIES_BLOCK, CROSS_REF_UNCOVERED_DRAFT_STUB, CROSS_REF_UNCOVERED_REAL_STORY, FORMAT_REF_STORIES;
12207
12225
  var init_scanPrompts = __esm({
12208
12226
  "../shared/scanPrompts.ts"() {
12227
+ init_storyNaming();
12209
12228
  ANCHOR_RULES = `Every component must have either \`anchors\` (non-empty array) or \`unanchored: true\`. No bare components.
12210
12229
 
12211
12230
  Pick the anchor kind based on how the component lives in code:
@@ -12630,12 +12649,15 @@ async function getLanguage(lang, cacheRoot = defaultCacheRoot()) {
12630
12649
  return language;
12631
12650
  }
12632
12651
  async function getParser(lang, cacheRoot) {
12652
+ const cached = parserCache.get(lang);
12653
+ if (cached) return cached;
12633
12654
  const language = await getLanguage(lang, cacheRoot);
12634
12655
  const parser = new Parser();
12635
12656
  parser.setLanguage(language);
12657
+ parserCache.set(lang, parser);
12636
12658
  return parser;
12637
12659
  }
12638
- var initialized, languageCache;
12660
+ var initialized, languageCache, parserCache;
12639
12661
  var init_parser = __esm({
12640
12662
  "src/analyze/tree-sitter/parser.ts"() {
12641
12663
  "use strict";
@@ -12643,6 +12665,7 @@ var init_parser = __esm({
12643
12665
  init_grammars();
12644
12666
  initialized = false;
12645
12667
  languageCache = /* @__PURE__ */ new Map();
12668
+ parserCache = /* @__PURE__ */ new Map();
12646
12669
  }
12647
12670
  });
12648
12671
 
@@ -12970,22 +12993,34 @@ var init_extract = __esm({
12970
12993
 
12971
12994
  // src/analyze/tree-sitter/symbols.ts
12972
12995
  import { Query } from "web-tree-sitter";
12996
+ async function getQuery(lang) {
12997
+ const cached = queryCache.get(lang);
12998
+ if (cached) return cached;
12999
+ const query = new Query(await getLanguage(lang), SYMBOL_QUERIES[lang]);
13000
+ queryCache.set(lang, query);
13001
+ return query;
13002
+ }
12973
13003
  async function extractSymbols(lang, source) {
12974
13004
  const parser = await getParser(lang);
12975
13005
  const tree = parser.parse(source);
12976
13006
  if (!tree) {
12977
13007
  return [];
12978
13008
  }
12979
- const language = await getLanguage(lang);
12980
- const query = new Query(language, SYMBOL_QUERIES[lang]);
12981
- return symbolsFromMatches(query.matches(tree.rootNode));
13009
+ try {
13010
+ const query = await getQuery(lang);
13011
+ return symbolsFromMatches(query.matches(tree.rootNode));
13012
+ } finally {
13013
+ tree.delete();
13014
+ }
12982
13015
  }
13016
+ var queryCache;
12983
13017
  var init_symbols = __esm({
12984
13018
  "src/analyze/tree-sitter/symbols.ts"() {
12985
13019
  "use strict";
12986
13020
  init_parser();
12987
13021
  init_queries();
12988
13022
  init_extract();
13023
+ queryCache = /* @__PURE__ */ new Map();
12989
13024
  }
12990
13025
  });
12991
13026
 
@@ -14258,7 +14293,7 @@ var init_semaphore = __esm({
14258
14293
  acquire() {
14259
14294
  const slot = this.tryAcquire();
14260
14295
  if (slot) return Promise.resolve(slot);
14261
- return new Promise((resolve5) => this.waiters.push(resolve5));
14296
+ return new Promise((resolve6) => this.waiters.push(resolve6));
14262
14297
  }
14263
14298
  makeRelease() {
14264
14299
  let released = false;
@@ -16118,12 +16153,12 @@ async function rpcHeartbeat(ctx, jobId, deviceId) {
16118
16153
  );
16119
16154
  if (error) console.error(`[${jobId}] heartbeat error:`, error.message);
16120
16155
  }
16121
- async function rpcComplete(ctx, jobId, deviceId, summary, kind, changed, metrics, logs, advanceMarker = true, affected) {
16156
+ async function rpcComplete(ctx, jobId, deviceId, summary2, kind, changed, metrics, logs, advanceMarker = true, affected) {
16122
16157
  const extra = {
16123
16158
  // `kind` drives lifecycle (squash, marker, retry). `changed` is the
16124
16159
  // decoupled what-changed verdict the Check Run / sticky comment render
16125
16160
  // (migration 083): a structural-only job is kind=applied but changed=false.
16126
- result: { summary, kind, changed },
16161
+ result: { summary: summary2, kind, changed },
16127
16162
  metrics,
16128
16163
  logs,
16129
16164
  advance_marker: advanceMarker
@@ -16657,8 +16692,8 @@ async function runJob(ctx, job, claimDeviceId) {
16657
16692
  if (runResult.isError) {
16658
16693
  throw new Error(`claude reported is_error in its JSON result (no successful completion)`);
16659
16694
  }
16660
- const summary = runResult.summary.trim().slice(0, 4e3);
16661
- if (summary) log(`claude> ${summary.slice(0, 500).replace(/\n/g, " ")}`);
16695
+ const summary2 = runResult.summary.trim().slice(0, 4e3);
16696
+ if (summary2) log(`claude> ${summary2.slice(0, 500).replace(/\n/g, " ")}`);
16662
16697
  metrics.model_served = runResult.modelServed;
16663
16698
  metrics.usage = runResult.usage;
16664
16699
  metrics.cost_usd = runResult.costUsd;
@@ -16685,7 +16720,7 @@ async function runJob(ctx, job, claimDeviceId) {
16685
16720
  throw new Error(outcome.reason);
16686
16721
  }
16687
16722
  const changed = outcome.changed;
16688
- 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, " ")}`);
16723
+ 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, " ")}`);
16689
16724
  const totalMs = Date.now() - startedAt;
16690
16725
  markPhase("complete", { kind: outcome.kind, changed, total_ms: totalMs });
16691
16726
  metrics.total_ms = totalMs;
@@ -16693,7 +16728,7 @@ async function runJob(ctx, job, claimDeviceId) {
16693
16728
  ctx,
16694
16729
  job.id,
16695
16730
  deviceId,
16696
- summary,
16731
+ summary2,
16697
16732
  outcome.kind,
16698
16733
  changed,
16699
16734
  metrics,
@@ -18164,14 +18199,14 @@ async function runScan(opts, deps = {}) {
18164
18199
  log(` supabase: ${supabaseUrl}`);
18165
18200
  log(` mcp base: ${mcpUrl}`);
18166
18201
  log(` repo: ${opts.repoPath}`);
18167
- const authed = await createAuthedClient({
18202
+ const authed2 = await createAuthedClient({
18168
18203
  supabaseUrl,
18169
18204
  supabaseAnonKey: supabaseAnon,
18170
18205
  clientId,
18171
18206
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
18172
18207
  });
18173
- log(` signed in as ${authed.identity}`);
18174
- const { supabase, userId } = authed;
18208
+ log(` signed in as ${authed2.identity}`);
18209
+ const { supabase, userId } = authed2;
18175
18210
  let headSha;
18176
18211
  try {
18177
18212
  headSha = await repoHeadSha(opts.repoPath);
@@ -18270,7 +18305,7 @@ async function runScan(opts, deps = {}) {
18270
18305
  const result = await proc;
18271
18306
  const exitCode = result.exitCode ?? 0;
18272
18307
  const signal = result.signal ?? null;
18273
- const summary = (finalResultText || (result.stdout ? String(result.stdout) : "")).trim().slice(-4e3);
18308
+ const summary2 = (finalResultText || (result.stdout ? String(result.stdout) : "")).trim().slice(-4e3);
18274
18309
  if (signal) {
18275
18310
  log(`claude exited ${exitCode} (signal: ${signal})`);
18276
18311
  } else {
@@ -18281,8 +18316,8 @@ async function runScan(opts, deps = {}) {
18281
18316
  log(` stories: ${counts.stories}`);
18282
18317
  let signals2 = null;
18283
18318
  try {
18284
- await new Promise((resolve5, reject) => {
18285
- transcriptStream.end((err2) => err2 ? reject(err2) : resolve5());
18319
+ await new Promise((resolve6, reject) => {
18320
+ transcriptStream.end((err2) => err2 ? reject(err2) : resolve6());
18286
18321
  });
18287
18322
  const reportPath = join19(transcriptDir, "report.json");
18288
18323
  const orphansTxtPath = join19(transcriptDir, "orphans.txt");
@@ -18348,7 +18383,7 @@ async function runScan(opts, deps = {}) {
18348
18383
  orgSlug: resolved2.orgSlug,
18349
18384
  projectSlug: resolved2.projectSlug,
18350
18385
  webUrl,
18351
- summary
18386
+ summary: summary2
18352
18387
  };
18353
18388
  } finally {
18354
18389
  await cleanupMcp().catch(() => {
@@ -18959,6 +18994,9 @@ var init_githubAppSetup = __esm({
18959
18994
 
18960
18995
  // src/daemon/prompt.ts
18961
18996
  import { createInterface } from "node:readline/promises";
18997
+ function stdinIsInteractive() {
18998
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
18999
+ }
18962
19000
  async function withReadline(fn) {
18963
19001
  const rl = createInterface({ input: process.stdin, output: process.stdout });
18964
19002
  try {
@@ -19241,9 +19279,9 @@ async function runLocalWatch(opts) {
19241
19279
  log(` repo: ${target.repo}`);
19242
19280
  log(` branch: ${target.repoBranch} \u2192 archme branch ${target.archmeBranch}`);
19243
19281
  const cfg = resolveConfig();
19244
- let authed;
19282
+ let authed2;
19245
19283
  try {
19246
- authed = await createAuthedClient({
19284
+ authed2 = await createAuthedClient({
19247
19285
  supabaseUrl: cfg.supabaseUrl,
19248
19286
  supabaseAnonKey: cfg.supabaseAnonKey,
19249
19287
  clientId: cfg.archmeDaemonClientId,
@@ -19253,8 +19291,8 @@ async function runLocalWatch(opts) {
19253
19291
  console.error(`archme local-watch: ${err2 instanceof Error ? err2.message : String(err2)}`);
19254
19292
  return 3;
19255
19293
  }
19256
- const { supabase, userId } = authed;
19257
- const orgId = authed.orgId;
19294
+ const { supabase, userId } = authed2;
19295
+ const orgId = authed2.orgId;
19258
19296
  if (!orgId) {
19259
19297
  console.error("archme local-watch: this login is not paired with an org \u2014 run `archme login`");
19260
19298
  return 3;
@@ -19494,22 +19532,22 @@ async function runInit(opts, deps = {}) {
19494
19532
  } else {
19495
19533
  log("\u2713 already signed in");
19496
19534
  }
19497
- const authed = await authenticate({
19535
+ const authed2 = await authenticate({
19498
19536
  supabaseUrl: cfg.supabaseUrl,
19499
19537
  supabaseAnonKey: cfg.supabaseAnonKey,
19500
19538
  clientId: cfg.archmeDaemonClientId,
19501
19539
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
19502
19540
  });
19503
- log(` signed in as ${authed.identity}`);
19541
+ log(` signed in as ${authed2.identity}`);
19504
19542
  let fullSlug = opts.project;
19505
19543
  if (!fullSlug) {
19506
- const orgId = authed.orgId;
19544
+ const orgId = authed2.orgId;
19507
19545
  if (!orgId) {
19508
19546
  throw new Error(
19509
19547
  "could not resolve your organization \u2014 run `archme login` again, or pass --project <org>/<slug>"
19510
19548
  );
19511
19549
  }
19512
- const orgSlug = await resolveOrgSlug(authed.supabase, orgId);
19550
+ const orgSlug = await resolveOrgSlug(authed2.supabase, orgId);
19513
19551
  if (!orgSlug) {
19514
19552
  throw new Error(
19515
19553
  `paired org ${orgId} no longer exists \u2014 run \`archme login\` again, or pass --project <org>/<slug>`
@@ -19518,8 +19556,8 @@ async function runInit(opts, deps = {}) {
19518
19556
  fullSlug = defaultProjectSlug(orgSlug, repoPath);
19519
19557
  }
19520
19558
  const project = await resolveProject(
19521
- authed.supabase,
19522
- authed.userId,
19559
+ authed2.supabase,
19560
+ authed2.userId,
19523
19561
  fullSlug
19524
19562
  );
19525
19563
  log(
@@ -19527,7 +19565,7 @@ async function runInit(opts, deps = {}) {
19527
19565
  );
19528
19566
  const scanBranch = initScanBranch(project.defaultBranch);
19529
19567
  const detail = await fetchArchitecture(
19530
- authed.supabase,
19568
+ authed2.supabase,
19531
19569
  project.projectId,
19532
19570
  scanBranch
19533
19571
  );
@@ -19541,8 +19579,8 @@ async function runInit(opts, deps = {}) {
19541
19579
  await runInstallScript(script, repoPath);
19542
19580
  ran.push("agent-config");
19543
19581
  const watched = await watchRepo({
19544
- supabase: authed.supabase,
19545
- orgId: authed.orgId,
19582
+ supabase: authed2.supabase,
19583
+ orgId: authed2.orgId,
19546
19584
  projectId: project.projectId,
19547
19585
  repoSlug: git4.repoSlug,
19548
19586
  branch: git4.branch,
@@ -19609,18 +19647,18 @@ Your board: ${webUrl}`);
19609
19647
  startGitHubAppInstall: () => (deps.buildInstallUrl ?? buildInstallUrl)({
19610
19648
  supabaseUrl: cfg.supabaseUrl,
19611
19649
  accessToken: getCurrentAccessToken() ?? "",
19612
- orgId: authed.orgId
19650
+ orgId: authed2.orgId
19613
19651
  }),
19614
19652
  waitForInstallation: () => (deps.waitForInstallation ?? waitForInstallation)({
19615
- hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed.supabase, authed.orgId),
19653
+ hasInstallation: () => (deps.orgHasInstallation ?? orgHasInstallation)(authed2.supabase, authed2.orgId),
19616
19654
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
19617
19655
  now: () => Date.now(),
19618
19656
  log
19619
19657
  }),
19620
19658
  watchRepo: async () => {
19621
19659
  const w = await watchRepo({
19622
- supabase: authed.supabase,
19623
- orgId: authed.orgId,
19660
+ supabase: authed2.supabase,
19661
+ orgId: authed2.orgId,
19624
19662
  projectId: project.projectId,
19625
19663
  repoSlug: git4.repoSlug,
19626
19664
  branch: git4.branch,
@@ -19642,8 +19680,8 @@ Your board: ${webUrl}`);
19642
19680
  });
19643
19681
  if (!target.ok) return { ok: false, reason: target.error };
19644
19682
  const res = await (deps.registerLocalWatch ?? registerLocalWatch)({
19645
- supabase: authed.supabase,
19646
- orgId: authed.orgId,
19683
+ supabase: authed2.supabase,
19684
+ orgId: authed2.orgId,
19647
19685
  projectId: project.projectId,
19648
19686
  repoPath: git4.root,
19649
19687
  originUrl: git4.originUrl,
@@ -19852,12 +19890,575 @@ var init_dispatch4 = __esm({
19852
19890
  }
19853
19891
  });
19854
19892
 
19893
+ // src/uninstall/edits.ts
19894
+ function stripArchmeClaudeBlock(content) {
19895
+ const start = content.indexOf(START_MARKER);
19896
+ if (start === -1) return UNCHANGED;
19897
+ const end = content.indexOf(END_MARKER, start);
19898
+ if (end === -1) return UNCHANGED;
19899
+ const before = content.slice(0, start).replace(/\n+$/, "");
19900
+ const after = content.slice(end + END_MARKER.length).replace(/^\n+/, "");
19901
+ if (!before.trim() && !after.trim()) return { kind: "delete" };
19902
+ if (!before.trim()) return { kind: "rewrite", content: `${after.replace(/\n*$/, "")}
19903
+ ` };
19904
+ if (!after.trim()) return { kind: "rewrite", content: `${before}
19905
+ ` };
19906
+ return { kind: "rewrite", content: `${before}
19907
+
19908
+ ${after.replace(/\n*$/, "")}
19909
+ ` };
19910
+ }
19911
+ function parseObject(raw) {
19912
+ try {
19913
+ const parsed = JSON.parse(raw);
19914
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
19915
+ return parsed;
19916
+ } catch {
19917
+ return null;
19918
+ }
19919
+ }
19920
+ function render(value) {
19921
+ return `${JSON.stringify(value, null, 2)}
19922
+ `;
19923
+ }
19924
+ function stripArchmeMcpServer(content) {
19925
+ const root = parseObject(content);
19926
+ if (!root) return UNCHANGED;
19927
+ const servers = root.mcpServers;
19928
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) return UNCHANGED;
19929
+ const entries = servers;
19930
+ if (!(MCP_SERVER_NAME2 in entries)) return UNCHANGED;
19931
+ delete entries[MCP_SERVER_NAME2];
19932
+ const onlyKey = Object.keys(root).length === 1 && "mcpServers" in root;
19933
+ if (onlyKey && Object.keys(entries).length === 0) return { kind: "delete" };
19934
+ return { kind: "rewrite", content: render(root) };
19935
+ }
19936
+ function isArchmeHook(hook) {
19937
+ const command = hook?.command;
19938
+ return typeof command === "string" && command.includes(NUDGE_HOOK_FILE);
19939
+ }
19940
+ function stripArchmeHooks(content) {
19941
+ const root = parseObject(content);
19942
+ if (!root) return UNCHANGED;
19943
+ const hooks = root.hooks;
19944
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) return UNCHANGED;
19945
+ const byEvent = hooks;
19946
+ const pre = byEvent.PreToolUse;
19947
+ if (!Array.isArray(pre)) return UNCHANGED;
19948
+ let changed = false;
19949
+ const kept = [];
19950
+ for (const entry of pre) {
19951
+ const inner = entry?.hooks;
19952
+ if (!Array.isArray(inner) || !inner.some(isArchmeHook)) {
19953
+ kept.push(entry);
19954
+ continue;
19955
+ }
19956
+ changed = true;
19957
+ const survivors = inner.filter((h2) => !isArchmeHook(h2));
19958
+ if (survivors.length > 0) kept.push({ ...entry, hooks: survivors });
19959
+ }
19960
+ if (!changed) return UNCHANGED;
19961
+ if (kept.length > 0) byEvent.PreToolUse = kept;
19962
+ else delete byEvent.PreToolUse;
19963
+ if (Object.keys(byEvent).length === 0) delete root.hooks;
19964
+ if (Object.keys(root).length === 0) return { kind: "delete" };
19965
+ return { kind: "rewrite", content: render(root) };
19966
+ }
19967
+ var UNCHANGED, START_MARKER, END_MARKER, NUDGE_HOOK_FILE, MCP_SERVER_NAME2;
19968
+ var init_edits = __esm({
19969
+ "src/uninstall/edits.ts"() {
19970
+ "use strict";
19971
+ UNCHANGED = { kind: "unchanged" };
19972
+ START_MARKER = "<!-- ARCHME-START -->";
19973
+ END_MARKER = "<!-- ARCHME-END -->";
19974
+ NUDGE_HOOK_FILE = "archme-search-nudge.mjs";
19975
+ MCP_SERVER_NAME2 = "archme";
19976
+ }
19977
+ });
19978
+
19979
+ // src/uninstall/watching.ts
19980
+ function decideDaemonRemoval(input) {
19981
+ if (!input.installed) {
19982
+ return { action: "keep", reason: "no daemon service is installed on this machine" };
19983
+ }
19984
+ if (input.keepWatching) {
19985
+ return { action: "keep", reason: "--keep-watching: this repo is still being watched" };
19986
+ }
19987
+ if (input.remaining.watchedRepos === null) {
19988
+ return {
19989
+ action: "keep",
19990
+ reason: "could not check what else it serves, so it was left running"
19991
+ };
19992
+ }
19993
+ const { watchedRepos, localEntries } = input.remaining;
19994
+ if (watchedRepos > 0 || localEntries > 0) {
19995
+ const parts = [];
19996
+ if (watchedRepos > 0) parts.push(`${watchedRepos} watched repo${watchedRepos === 1 ? "" : "s"}`);
19997
+ if (localEntries > 0) {
19998
+ parts.push(`${localEntries} locally-polled repo${localEntries === 1 ? "" : "s"}`);
19999
+ }
20000
+ return { action: "keep", reason: `still serving ${parts.join(" and ")}` };
20001
+ }
20002
+ return { action: "remove", reason: "nothing is left for it to do" };
20003
+ }
20004
+ function rowsForSlug(rows, slug) {
20005
+ const wanted = slug.toLowerCase();
20006
+ return rows.filter((r) => r.slug.toLowerCase() === wanted);
20007
+ }
20008
+ var init_watching = __esm({
20009
+ "src/uninstall/watching.ts"() {
20010
+ "use strict";
20011
+ }
20012
+ });
20013
+
20014
+ // src/uninstall/backend.ts
20015
+ var backend_exports = {};
20016
+ __export(backend_exports, {
20017
+ createDefaultBackend: () => createDefaultBackend
20018
+ });
20019
+ function labelFor(row) {
20020
+ const org = row.projects?.organizations?.slug;
20021
+ const project = row.projects?.slug;
20022
+ const where = org && project ? `${org}/${project}` : `${row.repo_owner}/${row.repo_name}`;
20023
+ return `${where} @ ${row.branch}`;
20024
+ }
20025
+ function createDefaultBackend() {
20026
+ let clientPromise = null;
20027
+ const supabase = () => clientPromise ??= authed();
20028
+ return {
20029
+ async gitRoot(dir) {
20030
+ try {
20031
+ const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], { cwd: dir });
20032
+ return stdout.trim() || null;
20033
+ } catch {
20034
+ return null;
20035
+ }
20036
+ },
20037
+ async repoSlug(repoPath) {
20038
+ const origin = await readOriginUrl(repoPath);
20039
+ return origin ? parseGitHubRepo(origin) : null;
20040
+ },
20041
+ async listWatchedRepos() {
20042
+ const { supabase: client } = await supabase();
20043
+ const { data, error } = await client.from("watched_repos").select("id, repo_owner, repo_name, branch, projects(slug, organizations(slug))").is("deleted_at", null);
20044
+ if (error) throw new Error(error.message);
20045
+ return (data ?? []).map((row) => ({
20046
+ id: row.id,
20047
+ slug: `${row.repo_owner}/${row.repo_name}`,
20048
+ label: labelFor(row)
20049
+ }));
20050
+ },
20051
+ async unwatch(ids) {
20052
+ if (ids.length === 0) return;
20053
+ const { supabase: client } = await supabase();
20054
+ const now = (/* @__PURE__ */ new Date()).toISOString();
20055
+ const { error } = await client.from("watched_repos").update({ deleted_at: now, updated_at: now }).in("id", ids);
20056
+ if (error) throw new Error(error.message);
20057
+ },
20058
+ async localRepoSlugs() {
20059
+ return (await loadLocalRepos()).map((e) => slugOf(e));
20060
+ },
20061
+ async removeLocalRepos(slug) {
20062
+ const repos = await loadLocalRepos();
20063
+ const kept = removeLocalReposBySlug(repos, slug);
20064
+ const dropped = repos.length - kept.length;
20065
+ if (dropped > 0) await saveLocalRepos(kept);
20066
+ return dropped;
20067
+ },
20068
+ async daemonInstalled() {
20069
+ const { pickController: pickController2 } = await Promise.resolve().then(() => (init_serviceController(), serviceController_exports));
20070
+ return await pickController2().unitExists();
20071
+ },
20072
+ async removeDaemon() {
20073
+ const { pickController: pickController2, orchestrateStop: orchestrateStop2 } = await Promise.resolve().then(() => (init_serviceController(), serviceController_exports));
20074
+ const controller = pickController2();
20075
+ await orchestrateStop2(controller);
20076
+ await controller.uninstall();
20077
+ }
20078
+ };
20079
+ }
20080
+ async function authed() {
20081
+ const cfg = resolveConfig();
20082
+ return await createAuthedClient({
20083
+ supabaseUrl: cfg.supabaseUrl,
20084
+ supabaseAnonKey: cfg.supabaseAnonKey,
20085
+ clientId: cfg.archmeDaemonClientId,
20086
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
20087
+ });
20088
+ }
20089
+ var init_backend = __esm({
20090
+ "src/uninstall/backend.ts"() {
20091
+ "use strict";
20092
+ init_execa();
20093
+ init_authedClient();
20094
+ init_defaults();
20095
+ init_localGit();
20096
+ init_github_remote();
20097
+ init_localRepos();
20098
+ }
20099
+ });
20100
+
20101
+ // src/uninstall/run-uninstall.ts
20102
+ var run_uninstall_exports = {};
20103
+ __export(run_uninstall_exports, {
20104
+ EX_OK: () => EX_OK3,
20105
+ EX_USAGE: () => EX_USAGE3,
20106
+ runUninstall: () => runUninstall
20107
+ });
20108
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
20109
+ import { join as join23 } from "node:path";
20110
+ function readIfExists(path7) {
20111
+ try {
20112
+ return existsSync3(path7) ? readFileSync5(path7, "utf8") : null;
20113
+ } catch {
20114
+ return null;
20115
+ }
20116
+ }
20117
+ function isParseableObject(raw) {
20118
+ try {
20119
+ const parsed = JSON.parse(raw);
20120
+ return Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed);
20121
+ } catch {
20122
+ return false;
20123
+ }
20124
+ }
20125
+ function applyEdit(absolute, edit) {
20126
+ if (edit.kind === "rewrite") writeFileSync3(absolute, edit.content);
20127
+ else if (edit.kind === "delete") rmSync(absolute, { force: true });
20128
+ }
20129
+ async function runUninstall(opts) {
20130
+ const log = opts.log ?? ((m) => console.log(m));
20131
+ const error = opts.error ?? ((m) => console.error(m));
20132
+ const backend = opts.backend ?? await defaultBackend();
20133
+ const repoPath = await backend.gitRoot(opts.repoPath).catch(() => null) ?? opts.repoPath;
20134
+ if (repoPath !== opts.repoPath) log(`Using the repo root: ${repoPath}`);
20135
+ const abs = (rel) => join23(repoPath, rel);
20136
+ const actions = [];
20137
+ const skipped = [];
20138
+ const mcpRaw = readIfExists(abs(".mcp.json"));
20139
+ if (mcpRaw !== null) {
20140
+ const edit = stripArchmeMcpServer(mcpRaw);
20141
+ if (edit.kind !== "unchanged") {
20142
+ actions.push({
20143
+ path: ".mcp.json",
20144
+ label: edit.kind === "delete" ? `delete (it holds only the ${MCP_SERVER_NAME2} server)` : `remove the ${MCP_SERVER_NAME2} server`,
20145
+ apply: () => applyEdit(abs(".mcp.json"), edit)
20146
+ });
20147
+ } else if (!isParseableObject(mcpRaw)) {
20148
+ skipped.push(
20149
+ `.mcp.json \u2014 could not be parsed as JSON; if it has an ${MCP_SERVER_NAME2} server, remove it by hand`
20150
+ );
20151
+ }
20152
+ }
20153
+ const claudeRaw = readIfExists(abs("CLAUDE.md"));
20154
+ if (claudeRaw !== null) {
20155
+ const edit = stripArchmeClaudeBlock(claudeRaw);
20156
+ if (edit.kind !== "unchanged") {
20157
+ actions.push({
20158
+ path: "CLAUDE.md",
20159
+ label: edit.kind === "delete" ? "delete (it holds only the ArchitectMe block)" : "remove the ArchitectMe block",
20160
+ apply: () => applyEdit(abs("CLAUDE.md"), edit)
20161
+ });
20162
+ } else if (claudeRaw.includes(START_MARKER)) {
20163
+ skipped.push("CLAUDE.md \u2014 the ArchitectMe block has no closing marker; remove it by hand");
20164
+ }
20165
+ }
20166
+ const skillPath = ".claude/skills/architecture/SKILL.md";
20167
+ if (existsSync3(abs(skillPath))) {
20168
+ actions.push({
20169
+ path: skillPath,
20170
+ label: "delete the architecture skill",
20171
+ apply: () => rmSync(abs(skillPath), { force: true })
20172
+ });
20173
+ }
20174
+ const hookPath = `.claude/hooks/${NUDGE_HOOK_FILE}`;
20175
+ if (existsSync3(abs(hookPath))) {
20176
+ actions.push({
20177
+ path: hookPath,
20178
+ label: "delete the retired search-nudge hook",
20179
+ apply: () => rmSync(abs(hookPath), { force: true })
20180
+ });
20181
+ }
20182
+ const settingsRaw = readIfExists(abs(".claude/settings.json"));
20183
+ if (settingsRaw !== null) {
20184
+ const edit = stripArchmeHooks(settingsRaw);
20185
+ if (edit.kind !== "unchanged") {
20186
+ actions.push({
20187
+ path: ".claude/settings.json",
20188
+ label: edit.kind === "delete" ? "delete (it holds only the ArchitectMe hook)" : "unregister the search-nudge hook",
20189
+ apply: () => applyEdit(abs(".claude/settings.json"), edit)
20190
+ });
20191
+ } else if (!isParseableObject(settingsRaw)) {
20192
+ skipped.push(
20193
+ ".claude/settings.json \u2014 could not be parsed as JSON; if it registers the ArchMe hook, remove that entry by hand"
20194
+ );
20195
+ }
20196
+ }
20197
+ for (const note of skipped) log(`Left alone: ${note}`);
20198
+ const keepWatching = opts.keepWatching ?? false;
20199
+ const slug = keepWatching ? null : await backend.repoSlug(repoPath).catch(() => null);
20200
+ let allWatched = null;
20201
+ let watchLookupError = null;
20202
+ if (slug) {
20203
+ try {
20204
+ allWatched = await backend.listWatchedRepos();
20205
+ } catch (err2) {
20206
+ watchLookupError = err2 instanceof Error ? err2.message : String(err2);
20207
+ }
20208
+ }
20209
+ const toUnwatch = allWatched ? rowsForSlug(allWatched, slug) : [];
20210
+ let localForSlug = 0;
20211
+ let allLocal = [];
20212
+ if (slug) {
20213
+ allLocal = await backend.localRepoSlugs().catch(() => []);
20214
+ localForSlug = allLocal.filter((s) => s.toLowerCase() === slug.toLowerCase()).length;
20215
+ }
20216
+ if (actions.length === 0 && toUnwatch.length === 0 && localForSlug === 0) {
20217
+ log(`No ArchitectMe agent config found in ${repoPath}. Nothing to remove.`);
20218
+ if (watchLookupError) log(watchingUnknownNote(watchLookupError));
20219
+ return { exitCode: EX_OK3, removed: [], unwatched: [] };
20220
+ }
20221
+ const daemonInstalled = await backend.daemonInstalled().catch(() => false);
20222
+ const plan = [
20223
+ ...actions.map((a2) => ` ${a2.path} \u2014 ${a2.label}`),
20224
+ ...toUnwatch.map((r) => ` ${r.label} \u2014 stop watching (no more commits will update it)`),
20225
+ ...localForSlug > 0 ? [` ${slug} \u2014 stop polling from this machine (${localForSlug} registry entry)`] : [],
20226
+ // Stated up front, not just reported after: a repo-scoped command removing
20227
+ // a machine-wide service is the most surprising thing here. Whether it
20228
+ // actually goes is still decided afterwards, against what really happened.
20229
+ ...daemonInstalled && !keepWatching ? [" the daemon service \u2014 removed only if this leaves it nothing to serve"] : []
20230
+ ];
20231
+ if (!opts.yes) {
20232
+ const interactive = opts.interactive ?? stdinIsInteractive();
20233
+ if (!interactive && !opts.confirm) {
20234
+ error(
20235
+ "archme uninstall: refusing to edit files without confirmation.\nThere is nobody to ask (stdin is not a terminal). Re-run with --yes."
20236
+ );
20237
+ return { exitCode: EX_USAGE3, removed: [], unwatched: [] };
20238
+ }
20239
+ const confirm = opts.confirm ?? (async (lines) => {
20240
+ log(`This will change, in ${repoPath} and on your ArchMe account:
20241
+ `);
20242
+ lines.forEach((l) => log(l));
20243
+ log("");
20244
+ return await promptYesNo("Uninstall ArchitectMe for this repo?");
20245
+ });
20246
+ if (!await confirm(plan)) {
20247
+ log("Cancelled \u2014 nothing was changed.");
20248
+ return { exitCode: EX_OK3, removed: [], unwatched: [] };
20249
+ }
20250
+ }
20251
+ const removed = [];
20252
+ for (const action of actions) {
20253
+ action.apply();
20254
+ removed.push(action.path);
20255
+ log(`Removed: ${action.path} \u2014 ${action.label}`);
20256
+ }
20257
+ for (const dir of PRUNABLE_DIRS) {
20258
+ const path7 = abs(dir);
20259
+ try {
20260
+ if (existsSync3(path7) && readdirSync(path7).length === 0) rmSync(path7, { recursive: true });
20261
+ } catch {
20262
+ }
20263
+ }
20264
+ const unwatched = [];
20265
+ if (toUnwatch.length > 0) {
20266
+ try {
20267
+ await backend.unwatch(toUnwatch.map((r) => r.id));
20268
+ unwatched.push(...toUnwatch.map((r) => r.id));
20269
+ for (const row of toUnwatch) log(`Stopped watching: ${row.label}`);
20270
+ } catch (err2) {
20271
+ watchLookupError = err2 instanceof Error ? err2.message : String(err2);
20272
+ }
20273
+ }
20274
+ let localRemoved = 0;
20275
+ if (slug && localForSlug > 0) {
20276
+ try {
20277
+ localRemoved = await backend.removeLocalRepos(slug);
20278
+ if (localRemoved > 0) log(`Stopped local polling: ${slug}`);
20279
+ } catch {
20280
+ log(`Could not update the local watch registry \u2014 \`archme local-watch --stop ${slug}\` will.`);
20281
+ }
20282
+ }
20283
+ const unwatchFailed = watchLookupError !== null;
20284
+ const remaining = {
20285
+ watchedRepos: allWatched && !unwatchFailed ? allWatched.length - unwatched.length : null,
20286
+ localEntries: allLocal.length - localRemoved
20287
+ };
20288
+ const daemonPlan = decideDaemonRemoval({ installed: daemonInstalled, keepWatching, remaining });
20289
+ if (daemonPlan.action === "remove") {
20290
+ try {
20291
+ await backend.removeDaemon();
20292
+ log(`Removed the daemon service \u2014 ${daemonPlan.reason}.`);
20293
+ } catch (err2) {
20294
+ log(
20295
+ `Could not remove the daemon service (${err2 instanceof Error ? err2.message : String(err2)}).
20296
+ archme daemon uninstall`
20297
+ );
20298
+ }
20299
+ } else {
20300
+ log(`Daemon left in place \u2014 ${daemonPlan.reason}.`);
20301
+ }
20302
+ log(
20303
+ summary({
20304
+ keepWatching,
20305
+ slug,
20306
+ unwatchFailed,
20307
+ watchLookupError,
20308
+ removedConfig: removed.length > 0,
20309
+ checkedAndNotWatched: Boolean(slug) && !unwatchFailed && toUnwatch.length === 0
20310
+ })
20311
+ );
20312
+ return { exitCode: EX_OK3, removed, unwatched };
20313
+ }
20314
+ function watchingUnknownNote(reason) {
20315
+ return `
20316
+ Could not check whether this repo is still being watched (${reason}).
20317
+ If it is, commits will keep updating the architecture. Sign in and re-run to stop it:
20318
+ archme login && archme uninstall`;
20319
+ }
20320
+ function summary(input) {
20321
+ const lines = input.removedConfig ? ["\nArchitectMe agent config removed from this repo."] : ["\nNo ArchitectMe agent config was here to remove."];
20322
+ if (input.keepWatching) {
20323
+ lines.push(
20324
+ "This repo is STILL being watched (--keep-watching), so commits keep updating its architecture."
20325
+ );
20326
+ } else if (input.unwatchFailed && input.watchLookupError) {
20327
+ lines.push(watchingUnknownNote(input.watchLookupError).trimStart());
20328
+ } else if (!input.slug) {
20329
+ lines.push(
20330
+ "No github.com remote here, so there was no watched repo to stop. Check archme.io \u2192 Settings \u2192 Integrations if you expected one."
20331
+ );
20332
+ } else if (input.checkedAndNotWatched) {
20333
+ lines.push(`${input.slug} was not being watched, so no commits were updating it.`);
20334
+ }
20335
+ lines.push(
20336
+ "\nYour architecture on archme.io and this machine's login are untouched:",
20337
+ " archme logout sign this machine out",
20338
+ "\nThe GitHub App installation is org-wide, so it stays. Remove it on github.com if you want it gone."
20339
+ );
20340
+ return lines.join("\n");
20341
+ }
20342
+ async function defaultBackend() {
20343
+ const { createDefaultBackend: createDefaultBackend2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
20344
+ return createDefaultBackend2();
20345
+ }
20346
+ var EX_OK3, EX_USAGE3, PRUNABLE_DIRS;
20347
+ var init_run_uninstall = __esm({
20348
+ "src/uninstall/run-uninstall.ts"() {
20349
+ "use strict";
20350
+ init_prompt();
20351
+ init_edits();
20352
+ init_watching();
20353
+ EX_OK3 = 0;
20354
+ EX_USAGE3 = 78;
20355
+ PRUNABLE_DIRS = [
20356
+ ".claude/skills/architecture",
20357
+ ".claude/skills",
20358
+ ".claude/hooks",
20359
+ ".claude"
20360
+ ];
20361
+ }
20362
+ });
20363
+
20364
+ // src/uninstall/dispatch.ts
20365
+ var dispatch_exports5 = {};
20366
+ __export(dispatch_exports5, {
20367
+ EX_INFRA: () => EX_INFRA3,
20368
+ EX_OK: () => EX_OK4,
20369
+ EX_USAGE: () => EX_USAGE4,
20370
+ dispatchUninstall: () => dispatchUninstall,
20371
+ parseUninstallArgs: () => parseUninstallArgs,
20372
+ printUninstallHelp: () => printUninstallHelp
20373
+ });
20374
+ import { resolve as resolve5 } from "node:path";
20375
+ function parseUninstallArgs(argv) {
20376
+ if (argv.some((a2) => a2 === "--help" || a2 === "-h" || a2 === "help")) return { kind: "help" };
20377
+ let repoPath;
20378
+ let yes = false;
20379
+ let keepWatching = false;
20380
+ for (const arg of argv) {
20381
+ if (arg === "--yes" || arg === "-y") {
20382
+ yes = true;
20383
+ } else if (arg === "--keep-watching") {
20384
+ keepWatching = true;
20385
+ } else if (arg.startsWith("-")) {
20386
+ return { kind: "error", message: `unknown option '${arg}'` };
20387
+ } else if (repoPath === void 0) {
20388
+ repoPath = arg;
20389
+ } else {
20390
+ return { kind: "error", message: `unexpected argument '${arg}'` };
20391
+ }
20392
+ }
20393
+ return { kind: "run", repoPath, yes, keepWatching };
20394
+ }
20395
+ function printUninstallHelp() {
20396
+ console.log(`archme uninstall \u2014 stop ArchitectMe for a repo
20397
+
20398
+ Undoes what \`archme init\` set up, and shows you the whole list before it does:
20399
+
20400
+ 1. Agent config \u2014 the archme MCP server in .mcp.json, the ArchitectMe block in
20401
+ CLAUDE.md, the architecture skill, and the retired search-nudge hook older
20402
+ versions registered in .claude/settings.json.
20403
+ 2. Watching \u2014 stops this repo producing jobs, both via the GitHub App and via
20404
+ local polling. Without this the board keeps updating while looking removed.
20405
+ 3. The daemon \u2014 removed only if it has no other repo left to serve. It is one
20406
+ service per account, so uninstalling one repo never stops the others.
20407
+
20408
+ Your architecture on archme.io and this machine's login survive. The GitHub App
20409
+ installation is org-wide and stays; remove it on github.com if you want it gone.
20410
+
20411
+ Usage:
20412
+ archme uninstall [repo-path] [options]
20413
+
20414
+ repo-path Defaults to the current directory.
20415
+
20416
+ Options:
20417
+ -y, --yes Do not ask for confirmation
20418
+ --keep-watching Remove the agent config only; leave the repo watched
20419
+ and the daemon running
20420
+ -h, --help Show this help`);
20421
+ }
20422
+ async function dispatchUninstall(argv) {
20423
+ const parsed = parseUninstallArgs(argv);
20424
+ if (parsed.kind === "help") {
20425
+ printUninstallHelp();
20426
+ return EX_OK4;
20427
+ }
20428
+ if (parsed.kind === "error") {
20429
+ console.error(`archme uninstall: ${parsed.message}`);
20430
+ printUninstallHelp();
20431
+ return EX_USAGE4;
20432
+ }
20433
+ try {
20434
+ const { runUninstall: runUninstall2 } = await Promise.resolve().then(() => (init_run_uninstall(), run_uninstall_exports));
20435
+ const result = await runUninstall2({
20436
+ repoPath: resolve5(parsed.repoPath ?? process.cwd()),
20437
+ yes: parsed.yes,
20438
+ keepWatching: parsed.keepWatching
20439
+ });
20440
+ return result.exitCode;
20441
+ } catch (err2) {
20442
+ console.error(`archme uninstall: ${err2 instanceof Error ? err2.message : String(err2)}`);
20443
+ return EX_INFRA3;
20444
+ }
20445
+ }
20446
+ var EX_OK4, EX_INFRA3, EX_USAGE4;
20447
+ var init_dispatch5 = __esm({
20448
+ "src/uninstall/dispatch.ts"() {
20449
+ "use strict";
20450
+ EX_OK4 = 0;
20451
+ EX_INFRA3 = 3;
20452
+ EX_USAGE4 = 78;
20453
+ }
20454
+ });
20455
+
19855
20456
  // src/daemon/localWatchDispatch.ts
19856
20457
  var localWatchDispatch_exports = {};
19857
20458
  __export(localWatchDispatch_exports, {
19858
- EX_INFRA: () => EX_INFRA3,
19859
- EX_OK: () => EX_OK3,
19860
- EX_USAGE: () => EX_USAGE3,
20459
+ EX_INFRA: () => EX_INFRA4,
20460
+ EX_OK: () => EX_OK5,
20461
+ EX_USAGE: () => EX_USAGE5,
19861
20462
  dispatchLocalWatch: () => dispatchLocalWatch,
19862
20463
  parseLocalWatchArgs: () => parseLocalWatchArgs,
19863
20464
  printLocalWatchHelp: () => printLocalWatchHelp
@@ -19947,12 +20548,12 @@ async function dispatchLocalWatch(argv) {
19947
20548
  const parsed = parseLocalWatchArgs(argv);
19948
20549
  if (parsed.kind === "help") {
19949
20550
  printLocalWatchHelp();
19950
- return EX_OK3;
20551
+ return EX_OK5;
19951
20552
  }
19952
20553
  if (parsed.kind === "error") {
19953
20554
  console.error(parsed.message);
19954
20555
  printLocalWatchHelp();
19955
- return EX_USAGE3;
20556
+ return EX_USAGE5;
19956
20557
  }
19957
20558
  const mod = await Promise.resolve().then(() => (init_localWatch(), localWatch_exports));
19958
20559
  switch (parsed.kind) {
@@ -19966,13 +20567,13 @@ async function dispatchLocalWatch(argv) {
19966
20567
  return await mod.runLocalWatch(parsed);
19967
20568
  }
19968
20569
  }
19969
- var EX_OK3, EX_INFRA3, EX_USAGE3, PROJECT_SLUG_RE3, REPO_SLUG_RE;
20570
+ var EX_OK5, EX_INFRA4, EX_USAGE5, PROJECT_SLUG_RE3, REPO_SLUG_RE;
19970
20571
  var init_localWatchDispatch = __esm({
19971
20572
  "src/daemon/localWatchDispatch.ts"() {
19972
20573
  "use strict";
19973
- EX_OK3 = 0;
19974
- EX_INFRA3 = 3;
19975
- EX_USAGE3 = 78;
20574
+ EX_OK5 = 0;
20575
+ EX_INFRA4 = 3;
20576
+ EX_USAGE5 = 78;
19976
20577
  PROJECT_SLUG_RE3 = /^[^/\s]+\/[^/\s]+$/;
19977
20578
  REPO_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
19978
20579
  }
@@ -19980,7 +20581,7 @@ var init_localWatchDispatch = __esm({
19980
20581
 
19981
20582
  // src/doctor/collect.ts
19982
20583
  import { readFile as readFile13 } from "node:fs/promises";
19983
- import { join as join23 } from "node:path";
20584
+ import { join as join24 } from "node:path";
19984
20585
  function binaryPresent(name) {
19985
20586
  try {
19986
20587
  requireBinary(name);
@@ -19996,7 +20597,7 @@ async function git3(cwd, args) {
19996
20597
  async function readMcpPin(repoRoot) {
19997
20598
  const empty = { installed: false, pin: { project: null, branch: null } };
19998
20599
  try {
19999
- const raw = await readFile13(join23(repoRoot, ".mcp.json"), "utf8");
20600
+ const raw = await readFile13(join24(repoRoot, ".mcp.json"), "utf8");
20000
20601
  const parsed = JSON.parse(raw);
20001
20602
  const servers = parsed.mcpServers ?? {};
20002
20603
  const entry = Object.entries(servers).find(([name]) => /archme/i.test(name));
@@ -20064,17 +20665,17 @@ async function collectFacts(cwd) {
20064
20665
  );
20065
20666
  try {
20066
20667
  const cfg = resolveConfig();
20067
- const authed = await createAuthedClient({
20668
+ const authed2 = await createAuthedClient({
20068
20669
  supabaseUrl: cfg.supabaseUrl,
20069
20670
  supabaseAnonKey: cfg.supabaseAnonKey,
20070
20671
  clientId: cfg.archmeDaemonClientId,
20071
20672
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
20072
20673
  });
20073
- if (!authed.orgId) return facts;
20674
+ if (!authed2.orgId) return facts;
20074
20675
  facts.signedIn = true;
20075
- facts.orgId = authed.orgId;
20076
- facts.identity = authed.identity;
20077
- const supabase = authed.supabase;
20676
+ facts.orgId = authed2.orgId;
20677
+ facts.identity = authed2.identity;
20678
+ const supabase = authed2.supabase;
20078
20679
  const slug = facts.pinnedProject;
20079
20680
  if (slug) {
20080
20681
  const [orgSlug, projectSlug] = slug.split("/");
@@ -20360,18 +20961,18 @@ optional.`;
20360
20961
  });
20361
20962
 
20362
20963
  // src/doctor/dispatch.ts
20363
- var dispatch_exports5 = {};
20364
- __export(dispatch_exports5, {
20365
- EX_INFRA: () => EX_INFRA4,
20366
- EX_OK: () => EX_OK4,
20367
- EX_USAGE: () => EX_USAGE4,
20964
+ var dispatch_exports6 = {};
20965
+ __export(dispatch_exports6, {
20966
+ EX_INFRA: () => EX_INFRA5,
20967
+ EX_OK: () => EX_OK6,
20968
+ EX_USAGE: () => EX_USAGE6,
20368
20969
  dispatchDoctor: () => dispatchDoctor,
20369
20970
  parseDoctorArgs: () => parseDoctorArgs,
20370
20971
  printDoctorHelp: () => printDoctorHelp
20371
20972
  });
20372
20973
  import { mkdtemp as mkdtemp5, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
20373
20974
  import { tmpdir as tmpdir5 } from "node:os";
20374
- import { join as join24 } from "node:path";
20975
+ import { join as join25 } from "node:path";
20375
20976
  function parseDoctorArgs(argv) {
20376
20977
  const args = { plain: false, help: false };
20377
20978
  for (const a2 of argv) {
@@ -20403,11 +21004,11 @@ async function dispatchDoctor(argv) {
20403
21004
  if (!parsed.ok) {
20404
21005
  console.error(parsed.message);
20405
21006
  printDoctorHelp();
20406
- return EX_USAGE4;
21007
+ return EX_USAGE6;
20407
21008
  }
20408
21009
  if (parsed.args.help) {
20409
21010
  printDoctorHelp();
20410
- return EX_OK4;
21011
+ return EX_OK6;
20411
21012
  }
20412
21013
  const facts = await collectFacts(process.cwd());
20413
21014
  const issues = diagnose(facts);
@@ -20423,17 +21024,17 @@ async function dispatchDoctor(argv) {
20423
21024
  console.log("");
20424
21025
  }
20425
21026
  }
20426
- if (parsed.args.plain) return EX_OK4;
21027
+ if (parsed.args.plain) return EX_OK6;
20427
21028
  try {
20428
21029
  requireBinary("claude");
20429
21030
  } catch {
20430
21031
  console.log(
20431
21032
  "Install Claude Code to ask about any of this: npm i -g @anthropic-ai/claude-code"
20432
21033
  );
20433
- return EX_OK4;
21034
+ return EX_OK6;
20434
21035
  }
20435
- const dir = await mkdtemp5(join24(tmpdir5(), "archme-doctor-"));
20436
- const mcpConfigPath = join24(dir, "mcp.json");
21036
+ const dir = await mkdtemp5(join25(tmpdir5(), "archme-doctor-"));
21037
+ const mcpConfigPath = join25(dir, "mcp.json");
20437
21038
  await writeFile13(mcpConfigPath, JSON.stringify({ mcpServers: {} }), { mode: 384 });
20438
21039
  try {
20439
21040
  const result = await spawnClaude({
@@ -20446,13 +21047,13 @@ async function dispatchDoctor(argv) {
20446
21047
  // is a much larger surface than the problem justifies.
20447
21048
  allowedTools: ["Read", "Glob", "Grep"]
20448
21049
  });
20449
- return result.exitCode === 0 ? EX_OK4 : EX_INFRA4;
21050
+ return result.exitCode === 0 ? EX_OK6 : EX_INFRA5;
20450
21051
  } finally {
20451
21052
  await rm9(dir, { recursive: true, force: true });
20452
21053
  }
20453
21054
  }
20454
- var EX_OK4, EX_INFRA4, EX_USAGE4;
20455
- var init_dispatch5 = __esm({
21055
+ var EX_OK6, EX_INFRA5, EX_USAGE6;
21056
+ var init_dispatch6 = __esm({
20456
21057
  "src/doctor/dispatch.ts"() {
20457
21058
  "use strict";
20458
21059
  init_collect();
@@ -20460,9 +21061,9 @@ var init_dispatch5 = __esm({
20460
21061
  init_prompt2();
20461
21062
  init_spawn_claude();
20462
21063
  init_preflight();
20463
- EX_OK4 = 0;
20464
- EX_INFRA4 = 3;
20465
- EX_USAGE4 = 78;
21064
+ EX_OK6 = 0;
21065
+ EX_INFRA5 = 3;
21066
+ EX_USAGE6 = 78;
20466
21067
  }
20467
21068
  });
20468
21069
 
@@ -20520,6 +21121,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
20520
21121
  "keys",
20521
21122
  "scan",
20522
21123
  "init",
21124
+ "uninstall",
20523
21125
  "local-watch",
20524
21126
  "doctor"
20525
21127
  ]);
@@ -20618,12 +21220,16 @@ async function main(argv) {
20618
21220
  const { dispatchInit: dispatchInit2 } = await Promise.resolve().then(() => (init_dispatch4(), dispatch_exports4));
20619
21221
  return await dispatchInit2(r.args);
20620
21222
  }
21223
+ case "uninstall": {
21224
+ const { dispatchUninstall: dispatchUninstall2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
21225
+ return await dispatchUninstall2(r.args);
21226
+ }
20621
21227
  case "local-watch": {
20622
21228
  const { dispatchLocalWatch: dispatchLocalWatch2 } = await Promise.resolve().then(() => (init_localWatchDispatch(), localWatchDispatch_exports));
20623
21229
  return await dispatchLocalWatch2(r.args);
20624
21230
  }
20625
21231
  case "doctor": {
20626
- const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch5(), dispatch_exports5));
21232
+ const { dispatchDoctor: dispatchDoctor2 } = await Promise.resolve().then(() => (init_dispatch6(), dispatch_exports6));
20627
21233
  return await dispatchDoctor2(r.args);
20628
21234
  }
20629
21235
  case "logout": {
@@ -20650,6 +21256,8 @@ Other commands:
20650
21256
  archme scan <repo-path> Single-pass scan into an explicit project/branch
20651
21257
  archme local-watch [repo-path] Watch a repo without a GitHub App
20652
21258
  (--list|--stop|--resume)
21259
+ archme uninstall [repo-path] Stop ArchitectMe for a repo: config,
21260
+ watching, and the daemon if it's now idle
20653
21261
  archme doctor Check this machine's setup and explain it
20654
21262
  archme login | logout Authenticate with archme
20655
21263
  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.5",
3
+ "version": "0.5.1",
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",