@archme/cli 0.4.0 → 0.4.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 (4) hide show
  1. package/LICENSE +64 -0
  2. package/README.md +20 -12
  3. package/dist/cli.js +719 -72
  4. package/package.json +4 -2
package/LICENSE ADDED
@@ -0,0 +1,64 @@
1
+ ArchitectMe CLI License
2
+
3
+ Copyright (c) 2026 ArchitectMe. All rights reserved.
4
+
5
+ This software (the "Software") is proprietary and is licensed, not sold.
6
+
7
+ 1. GRANT OF LICENSE
8
+
9
+ Subject to the terms below, you are granted a non-exclusive,
10
+ non-transferable, revocable license to install and use the Software solely
11
+ to interact with the ArchitectMe service operated at archme.io (the
12
+ "Service"), in connection with an active ArchitectMe account.
13
+
14
+ Use of the Software for any other purpose — including use with any service,
15
+ endpoint, or system other than the Service — is not licensed.
16
+
17
+ 2. RESTRICTIONS
18
+
19
+ You may not:
20
+
21
+ (a) redistribute, sublicense, sell, rent, lease, or otherwise transfer the
22
+ Software or copies of it;
23
+ (b) modify, adapt, translate, or create derivative works of the Software;
24
+ (c) reverse engineer, decompile, or disassemble the Software, or otherwise
25
+ attempt to derive its source code, except to the extent that such
26
+ activity is expressly permitted by applicable law notwithstanding this
27
+ restriction;
28
+ (d) remove, obscure, or alter any proprietary notices contained in the
29
+ Software; or
30
+ (e) use the Software to build, train, or improve a competing product or
31
+ service.
32
+
33
+ 3. RESERVATION OF RIGHTS
34
+
35
+ All right, title, and interest in and to the Software, including all
36
+ intellectual property rights, remain with ArchitectMe. No rights are granted
37
+ except as expressly set out in this license.
38
+
39
+ 4. TERMINATION
40
+
41
+ This license terminates automatically if you breach any of its terms, and
42
+ may be terminated by ArchitectMe at any time. On termination you must cease
43
+ all use of the Software and destroy all copies in your possession.
44
+
45
+ 5. NO WARRANTY
46
+
47
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
48
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
49
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
50
+
51
+ 6. LIMITATION OF LIABILITY
52
+
53
+ IN NO EVENT SHALL ARCHITECTME BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
54
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
55
+ FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER
56
+ DEALINGS IN THE SOFTWARE.
57
+
58
+ 7. THIRD-PARTY COMPONENTS
59
+
60
+ The Software incorporates third-party open-source components, which remain
61
+ subject to their own licenses. Nothing in this license limits your rights
62
+ under those licenses with respect to those components.
63
+
64
+ For licensing enquiries, contact archme.io.
package/README.md CHANGED
@@ -31,29 +31,37 @@ everything else keeps working.
31
31
 
32
32
  ### Developing on the CLI
33
33
 
34
- From a clone, `npm link` puts the local copy on your `PATH`:
34
+ **For iterating on sources, use tsx directly** — it always runs current code:
35
35
 
36
36
  ```bash
37
37
  cd archme-cli
38
38
  npm install
39
+ npx tsx src/bin.ts <subcommand> [args]
40
+ ```
41
+
42
+ `npm link` also works, but note the published `bin` is the *built bundle*
43
+ (`dist/cli.js`), so linking without building leaves a dangling symlink, and a
44
+ stale `dist/` silently shadows your edits. Build first, and rebuild after
45
+ changes:
46
+
47
+ ```bash
48
+ npm install
49
+ npm run build # required — bin points at dist/cli.js
39
50
  npm link
40
51
 
41
- which archme # → <your-npm-prefix>/bin/archme
52
+ which archme # → <your-npm-prefix>/bin/archme
42
53
  ```
43
54
 
44
55
  To uninstall: `cd archme-cli && npm unlink` (or `npm rm -g @archme/cli`).
45
56
 
46
- **One-off / no global install:**
47
-
48
- ```bash
49
- npx tsx archme-cli/src/bin.ts <subcommand> [args]
50
- ```
57
+ `npm run build` uses esbuild to inline the repo's top-level `shared/` modules,
58
+ which do not exist inside the package. `npm run build:check` builds and then
59
+ executes the artifact — that is what CI and `prepublishOnly` run.
51
60
 
52
- The published package runs `dist/cli.js`, built by `npm run build` (esbuild
53
- inlines the repo's top-level `shared/` modules, which do not exist inside the
54
- package). `npm run build:check` builds and then executes the artifact — that is
55
- what CI and `prepublishOnly` run. When iterating on sources, prefer
56
- `npx tsx src/bin.ts`, which never consults `dist/`.
61
+ `bin/archme` (the dev shim, used by an unbuilt clone and by service units when
62
+ `dist/` is absent) runs `src/bin.ts` via tsx. It must point at `bin.ts`, not
63
+ `cli.ts` — `cli.ts` exports `main` without invoking it, so pointing at it makes
64
+ every command a silent exit 0. `src/binShim.test.ts` pins this by executing it.
57
65
 
58
66
  ### Do you need env config?
59
67
 
package/dist/cli.js CHANGED
@@ -1020,15 +1020,15 @@ var require_windows = __commonJS({
1020
1020
  }
1021
1021
  return false;
1022
1022
  }
1023
- function checkStat(stat9, path7, options) {
1024
- if (!stat9.isSymbolicLink() && !stat9.isFile()) {
1023
+ function checkStat(stat10, path7, options) {
1024
+ if (!stat10.isSymbolicLink() && !stat10.isFile()) {
1025
1025
  return false;
1026
1026
  }
1027
1027
  return checkPathExt(path7, options);
1028
1028
  }
1029
1029
  function isexe(path7, options, cb) {
1030
- fs.stat(path7, function(er, stat9) {
1031
- cb(er, er ? false : checkStat(stat9, path7, options));
1030
+ fs.stat(path7, function(er, stat10) {
1031
+ cb(er, er ? false : checkStat(stat10, path7, options));
1032
1032
  });
1033
1033
  }
1034
1034
  function sync(path7, options) {
@@ -1044,20 +1044,20 @@ var require_mode = __commonJS({
1044
1044
  isexe.sync = sync;
1045
1045
  var fs = __require("fs");
1046
1046
  function isexe(path7, options, cb) {
1047
- fs.stat(path7, function(er, stat9) {
1048
- cb(er, er ? false : checkStat(stat9, options));
1047
+ fs.stat(path7, function(er, stat10) {
1048
+ cb(er, er ? false : checkStat(stat10, options));
1049
1049
  });
1050
1050
  }
1051
1051
  function sync(path7, options) {
1052
1052
  return checkStat(fs.statSync(path7), options);
1053
1053
  }
1054
- function checkStat(stat9, options) {
1055
- return stat9.isFile() && checkMode(stat9, options);
1054
+ function checkStat(stat10, options) {
1055
+ return stat10.isFile() && checkMode(stat10, options);
1056
1056
  }
1057
- function checkMode(stat9, options) {
1058
- var mod = stat9.mode;
1059
- var uid = stat9.uid;
1060
- var gid = stat9.gid;
1057
+ function checkMode(stat10, options) {
1058
+ var mod = stat10.mode;
1059
+ var uid = stat10.uid;
1060
+ var gid = stat10.gid;
1061
1061
  var myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid();
1062
1062
  var myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid();
1063
1063
  var u2 = parseInt("100", 8);
@@ -1091,12 +1091,12 @@ var require_isexe = __commonJS({
1091
1091
  if (typeof Promise !== "function") {
1092
1092
  throw new TypeError("callback not provided");
1093
1093
  }
1094
- return new Promise(function(resolve3, reject) {
1094
+ return new Promise(function(resolve4, reject) {
1095
1095
  isexe(path7, options || {}, function(er, is) {
1096
1096
  if (er) {
1097
1097
  reject(er);
1098
1098
  } else {
1099
- resolve3(is);
1099
+ resolve4(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((resolve3, reject) => {
1165
+ const step = (i2) => new Promise((resolve4, reject) => {
1166
1166
  if (i2 === pathEnv.length)
1167
- return opt.all && found.length ? resolve3(found) : reject(getNotFoundError(cmd));
1167
+ return opt.all && found.length ? resolve4(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
- resolve3(subStep(p, i2, 0));
1172
+ resolve4(subStep(p, i2, 0));
1173
1173
  });
1174
- const subStep = (p, i2, ii) => new Promise((resolve3, reject) => {
1174
+ const subStep = (p, i2, ii) => new Promise((resolve4, reject) => {
1175
1175
  if (ii === pathExt.length)
1176
- return resolve3(step(i2 + 1));
1176
+ return resolve4(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 resolve3(p + ext2);
1183
+ return resolve4(p + ext2);
1184
1184
  }
1185
- return resolve3(subStep(p, i2, ii + 1));
1185
+ return resolve4(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((resolve3, reject) => {
2266
- Object.assign(methods, { resolve: resolve3, reject });
2265
+ const promise = new Promise((resolve4, reject) => {
2266
+ Object.assign(methods, { resolve: resolve4, 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 resolve3 = promise.resolve.bind(promise);
7564
- return { resolve: resolve3, promises };
7563
+ const resolve4 = promise.resolve.bind(promise);
7564
+ return { resolve: resolve4, promises };
7565
7565
  };
7566
- waitForConcurrentStreams = async ({ resolve: resolve3, promises }, subprocess) => {
7567
- resolve3();
7566
+ waitForConcurrentStreams = async ({ resolve: resolve4, promises }, subprocess) => {
7567
+ resolve4();
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((resolve3) => setTimeout(resolve3, intervalMs));
8639
+ await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
8640
8640
  const poll = await postJson("/poll", {
8641
8641
  device_code: data.device_code
8642
8642
  });
@@ -8852,7 +8852,7 @@ var init_launchctl = __esm({
8852
8852
  if (r.exitCode === 0 || r.exitCode === 17) return;
8853
8853
  if (r.exitCode === 5) {
8854
8854
  await this.opts.spawner.run("launchctl", ["bootout", SERVICE_TARGET(this.opts.uid)]);
8855
- await new Promise((resolve3) => setTimeout(resolve3, 500));
8855
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
8856
8856
  r = await bootstrap();
8857
8857
  if (r.exitCode === 0 || r.exitCode === 17) return;
8858
8858
  }
@@ -9231,7 +9231,7 @@ async function tailLog(opts = {}) {
9231
9231
  }
9232
9232
  }
9233
9233
  if (!opts.follow) return;
9234
- await new Promise((resolve3, reject) => {
9234
+ await new Promise((resolve4, reject) => {
9235
9235
  let fileWatcher = null;
9236
9236
  let dirWatcher = null;
9237
9237
  const dir = dirname6(path7);
@@ -9295,7 +9295,7 @@ async function tailLog(opts = {}) {
9295
9295
  const handleSigint = () => {
9296
9296
  fileWatcher?.close();
9297
9297
  dirWatcher?.close();
9298
- resolve3();
9298
+ resolve4();
9299
9299
  };
9300
9300
  process.once("SIGINT", handleSigint);
9301
9301
  });
@@ -9425,6 +9425,7 @@ async function writeMcpConfig(opts) {
9425
9425
  project: opts.projectSlug,
9426
9426
  branch: opts.branch
9427
9427
  });
9428
+ if (opts.sourceKind) params.set("source_kind", opts.sourceKind);
9428
9429
  if (opts.source) {
9429
9430
  params.set("source_repo", opts.source.repo);
9430
9431
  params.set("source_after_sha", opts.source.after_sha);
@@ -9506,8 +9507,7 @@ var init_daemonModel = __esm({
9506
9507
  function buildClaudeArgs(opts) {
9507
9508
  const tools = (opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS).join(",");
9508
9509
  const args = [
9509
- "-p",
9510
- opts.prompt,
9510
+ ...opts.interactive ? [opts.prompt] : ["-p", opts.prompt],
9511
9511
  "--mcp-config",
9512
9512
  opts.mcpConfigPath,
9513
9513
  // Load ONLY the server(s) from our --mcp-config file; ignore the repo's
@@ -9524,7 +9524,7 @@ function buildClaudeArgs(opts) {
9524
9524
  "--allowedTools",
9525
9525
  tools
9526
9526
  ];
9527
- if (opts.jsonOutput) {
9527
+ if (opts.jsonOutput && !opts.interactive) {
9528
9528
  args.push("--output-format", "json");
9529
9529
  }
9530
9530
  if (opts.model) {
@@ -9535,9 +9535,20 @@ function buildClaudeArgs(opts) {
9535
9535
  function spawnClaude(opts) {
9536
9536
  const env = scrubPrivilegedEnv(process.env);
9537
9537
  const args = buildClaudeArgs({ ...opts, model: opts.model || DEFAULT_CLAUDE_MODEL });
9538
- if (opts.streamJson) {
9538
+ if (opts.streamJson && !opts.interactive) {
9539
9539
  args.push("--output-format", "stream-json", "--verbose");
9540
9540
  }
9541
+ if (opts.interactive) {
9542
+ return execa("claude", args, {
9543
+ reject: false,
9544
+ // No timeout: the session is only as long as the human wants, and killing
9545
+ // it out from under them mid-answer would be worse than any runaway.
9546
+ cwd: opts.cwd,
9547
+ env,
9548
+ // Hand over the terminal — this is the whole point.
9549
+ stdio: "inherit"
9550
+ });
9551
+ }
9541
9552
  return execa(
9542
9553
  "claude",
9543
9554
  args,
@@ -14086,7 +14097,7 @@ var init_semaphore = __esm({
14086
14097
  acquire() {
14087
14098
  const slot = this.tryAcquire();
14088
14099
  if (slot) return Promise.resolve(slot);
14089
- return new Promise((resolve3) => this.waiters.push(resolve3));
14100
+ return new Promise((resolve4) => this.waiters.push(resolve4));
14090
14101
  }
14091
14102
  makeRelease() {
14092
14103
  let released = false;
@@ -16659,8 +16670,8 @@ async function defaultLoadUptime(pid) {
16659
16670
  if (process.platform === "linux") {
16660
16671
  try {
16661
16672
  const { readFile: readFile12 } = await import("node:fs/promises");
16662
- const stat9 = await readFile12(`/proc/${pid}/stat`, "utf8");
16663
- const parts = stat9.split(") ").pop()?.split(" ") ?? [];
16673
+ const stat10 = await readFile12(`/proc/${pid}/stat`, "utf8");
16674
+ const parts = stat10.split(") ").pop()?.split(" ") ?? [];
16664
16675
  const ticks = Number(parts[19]);
16665
16676
  if (!Number.isFinite(ticks)) return null;
16666
16677
  const { readFile: rf } = await import("node:fs/promises");
@@ -16715,6 +16726,45 @@ var init_dispatch2 = __esm({
16715
16726
  }
16716
16727
  });
16717
16728
 
16729
+ // src/scan/authedClient.ts
16730
+ import { createClient as createClient2 } from "@supabase/supabase-js";
16731
+ async function createAuthedClient(opts) {
16732
+ initBearer({
16733
+ supabaseUrl: opts.supabaseUrl,
16734
+ anonKey: opts.supabaseAnonKey,
16735
+ clientId: opts.clientId,
16736
+ preSpawnMinLifetimeMs: opts.preSpawnMinLifetimeMs
16737
+ });
16738
+ let loaded;
16739
+ try {
16740
+ loaded = await loadStoredAndRefresh();
16741
+ } catch (err) {
16742
+ const msg = err instanceof Error ? err.message : String(err);
16743
+ throw new Error(`${msg}
16744
+ \u2192 run \`archme login\` first`);
16745
+ }
16746
+ const userId = getCurrentUserId();
16747
+ if (!userId) throw new Error("could not resolve user id from OAuth tokens");
16748
+ const supabase = createClient2(opts.supabaseUrl, opts.supabaseAnonKey, {
16749
+ accessToken: async () => getCurrentAccessToken()
16750
+ });
16751
+ return {
16752
+ supabase,
16753
+ userId,
16754
+ orgId: getCurrentOrgId(),
16755
+ identity: `${loaded.user.email ?? "OAuth user"} (${loaded.user.sub ?? "unknown"})`
16756
+ };
16757
+ }
16758
+ var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS;
16759
+ var init_authedClient = __esm({
16760
+ "src/scan/authedClient.ts"() {
16761
+ "use strict";
16762
+ init_bearer();
16763
+ SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
16764
+ SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
16765
+ }
16766
+ });
16767
+
16718
16768
  // src/scan/web-url.ts
16719
16769
  function deriveWebOrigin(apiUrl, onMalformed = "throw") {
16720
16770
  let parsed;
@@ -16995,20 +17045,24 @@ var init_github_remote = __esm({
16995
17045
  });
16996
17046
 
16997
17047
  // src/scan/run-scan.ts
16998
- import { createClient as createClient2 } from "@supabase/supabase-js";
16999
17048
  import { createWriteStream as createWriteStream2 } from "node:fs";
17000
17049
  import { mkdir as mkdir10, readFile as readFile11 } from "node:fs/promises";
17001
17050
  import { basename as basename2, join as join16 } from "node:path";
17002
- function buildBootstrapPrompt(promptVariant) {
17003
- if (!promptVariant) return BOOTSTRAP_PROMPT_BASE;
17051
+ function buildBootstrapPrompt(opts = {}) {
17052
+ const { promptVariant, interactive, briefing } = opts;
17053
+ let base = interactive ? briefing ? `${BOOTSTRAP_INTERACTIVE}
17054
+
17055
+ ${briefing}` : BOOTSTRAP_INTERACTIVE_NO_BRIEFING : BOOTSTRAP_HEADLESS;
17056
+ if (!promptVariant) return base;
17004
17057
  if (!PROMPT_VARIANT_RE.test(promptVariant)) {
17005
17058
  throw new Error(
17006
17059
  `invalid prompt variant id \`${promptVariant}\` (expected YY-MM-DD-HHMM, optionally followed by -<suffix>)`
17007
17060
  );
17008
17061
  }
17009
- return `${BOOTSTRAP_PROMPT_BASE}
17062
+ base += `
17010
17063
 
17011
17064
  Use scan-prompt variant \`${promptVariant}\` \u2014 pass \`variant: "${promptVariant}"\` to \`start_scan\`.`;
17065
+ return base;
17012
17066
  }
17013
17067
  function todayDate() {
17014
17068
  const d = /* @__PURE__ */ new Date();
@@ -17204,26 +17258,14 @@ async function runScan(opts, deps = {}) {
17204
17258
  log(` supabase: ${supabaseUrl}`);
17205
17259
  log(` mcp base: ${mcpUrl}`);
17206
17260
  log(` repo: ${opts.repoPath}`);
17207
- initBearer({
17261
+ const authed = await createAuthedClient({
17208
17262
  supabaseUrl,
17209
- anonKey: supabaseAnon,
17263
+ supabaseAnonKey: supabaseAnon,
17210
17264
  clientId,
17211
17265
  preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
17212
17266
  });
17213
- let loaded;
17214
- try {
17215
- loaded = await loadStoredAndRefresh();
17216
- } catch (err) {
17217
- const msg = err instanceof Error ? err.message : String(err);
17218
- throw new Error(`${msg}
17219
- \u2192 run \`archme login\` first`);
17220
- }
17221
- log(` signed in as ${loaded.user.email ?? "OAuth user"} (${loaded.user.sub ?? "unknown"})`);
17222
- const userId = getCurrentUserId();
17223
- if (!userId) throw new Error("could not resolve user id from OAuth tokens");
17224
- const supabase = createClient2(supabaseUrl, supabaseAnon, {
17225
- accessToken: async () => getCurrentAccessToken()
17226
- });
17267
+ log(` signed in as ${authed.identity}`);
17268
+ const { supabase, userId } = authed;
17227
17269
  let headSha;
17228
17270
  try {
17229
17271
  headSha = await repoHeadSha(opts.repoPath);
@@ -17259,24 +17301,38 @@ async function runScan(opts, deps = {}) {
17259
17301
  baseUrl: mcpUrl,
17260
17302
  projectSlug: `${resolved.orgSlug}/${resolved.projectSlug}`,
17261
17303
  branch: branchName,
17262
- bearer
17263
- // No source headers — developer-driven run, commits stay untagged.
17304
+ bearer,
17305
+ // No source-SHA block (no job id for a local run), but DO tag these as scan
17306
+ // writes: otherwise they are indistinguishable from a manual chat edit, and
17307
+ // the scan-in-progress banner cannot tell a running scan from a human typing.
17308
+ sourceKind: "scan"
17264
17309
  });
17265
17310
  try {
17266
17311
  const model = await resolveModelPin(supabase);
17267
- log(`spawning claude (mcp config at ${configPath}, model ${model}, timeout ${Math.round(SCAN_CLAUDE_TIMEOUT_MS / 6e4)} min)`);
17312
+ const interactive = opts.interactive !== false;
17313
+ log(
17314
+ interactive ? `starting claude (mcp config at ${configPath}, model ${model}) \u2014 this session is interactive; answer its questions as they come` : `spawning claude (mcp config at ${configPath}, model ${model}, timeout ${Math.round(SCAN_CLAUDE_TIMEOUT_MS / 6e4)} min)`
17315
+ );
17268
17316
  const spawn2 = deps.spawn ?? spawnClaude;
17269
17317
  const proc = spawn2({
17270
- prompt: buildBootstrapPrompt(opts.promptVariant),
17318
+ prompt: buildBootstrapPrompt({
17319
+ promptVariant: opts.promptVariant,
17320
+ interactive,
17321
+ briefing: opts.briefing
17322
+ }),
17271
17323
  mcpConfigPath: configPath,
17272
17324
  cwd: opts.repoPath,
17273
- timeoutMs: SCAN_CLAUDE_TIMEOUT_MS,
17325
+ // No timeout interactively — a human is present and can Ctrl-C. Killing a
17326
+ // session mid-answer would be worse than any runaway.
17327
+ timeoutMs: interactive ? void 0 : SCAN_CLAUDE_TIMEOUT_MS,
17274
17328
  allowedTools: SCAN_ALLOWED_TOOLS,
17275
17329
  model,
17330
+ interactive,
17276
17331
  // stream-json: required for the transcript regexes in
17277
17332
  // transcript-signals.ts to see real tool events. Without it, claude
17278
17333
  // -p emits only the model's final text and every signal reports 0.
17279
- streamJson: true
17334
+ // Incompatible with an interactive TTY session.
17335
+ streamJson: !interactive
17280
17336
  });
17281
17337
  const transcriptDir = join16(opts.repoPath, ".archme", "scan");
17282
17338
  const transcriptPath = join16(transcriptDir, "transcript.log");
@@ -17319,8 +17375,8 @@ async function runScan(opts, deps = {}) {
17319
17375
  log(` stories: ${counts.stories}`);
17320
17376
  let signals2 = null;
17321
17377
  try {
17322
- await new Promise((resolve3, reject) => {
17323
- transcriptStream.end((err) => err ? reject(err) : resolve3());
17378
+ await new Promise((resolve4, reject) => {
17379
+ transcriptStream.end((err) => err ? reject(err) : resolve4());
17324
17380
  });
17325
17381
  const reportPath = join16(transcriptDir, "report.json");
17326
17382
  const orphansTxtPath = join16(transcriptDir, "orphans.txt");
@@ -17393,12 +17449,13 @@ async function runScan(opts, deps = {}) {
17393
17449
  });
17394
17450
  }
17395
17451
  }
17396
- var SCAN_CLAUDE_TIMEOUT_MS, SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS, BOOTSTRAP_PROMPT_BASE, PROMPT_VARIANT_RE;
17452
+ var BOOTSTRAP_HEADLESS, BOOTSTRAP_INTERACTIVE, BOOTSTRAP_INTERACTIVE_NO_BRIEFING, PROMPT_VARIANT_RE;
17397
17453
  var init_run_scan = __esm({
17398
17454
  "src/scan/run-scan.ts"() {
17399
17455
  "use strict";
17400
17456
  init_execa();
17401
17457
  init_bearer();
17458
+ init_authedClient();
17402
17459
  init_mcp_config();
17403
17460
  init_spawn_claude();
17404
17461
  init_model_pin();
@@ -17409,9 +17466,11 @@ var init_run_scan = __esm({
17409
17466
  init_transcript_signals();
17410
17467
  init_report();
17411
17468
  init_github_remote();
17412
- SCAN_CLAUDE_TIMEOUT_MS = 45 * 60 * 1e3;
17413
- SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS = SCAN_CLAUDE_TIMEOUT_MS + 5 * 60 * 1e3;
17414
- BOOTSTRAP_PROMPT_BASE = `Run the \`scan\` MCP prompt now and follow it to completion. This is a non-interactive automated run \u2014 skip any steps that pause for user confirmation (e.g. "does this hierarchy look right?"). Proceed with your best-judgement decisions rather than waiting for human input.`;
17469
+ BOOTSTRAP_HEADLESS = `Run the \`scan\` MCP prompt now and follow it to completion. This is a non-interactive automated run \u2014 skip any steps that pause for user confirmation (e.g. "does this hierarchy look right?"). Proceed with your best-judgement decisions rather than waiting for human input.`;
17470
+ BOOTSTRAP_INTERACTIVE = `Run the \`scan\` MCP prompt now and follow it to completion. The user is at an interactive terminal \u2014 ask them when the prompt says to, and wait for their answer.
17471
+
17472
+ Before you call \`start_scan\`, show the user this briefing verbatim and ask them to confirm they want to go ahead. Do not start scanning until they say yes.`;
17473
+ BOOTSTRAP_INTERACTIVE_NO_BRIEFING = `Run the \`scan\` MCP prompt now and follow it to completion. The user is at an interactive terminal \u2014 ask them when the prompt says to, and wait for their answer.`;
17415
17474
  PROMPT_VARIANT_RE = /^\d{2}-\d{2}-\d{2}-\d{4}(-[A-Za-z0-9_-]+)?$/;
17416
17475
  }
17417
17476
  });
@@ -17465,6 +17524,8 @@ async function dispatchScan(args) {
17465
17524
  } else if (arg === "--mcp-url" && next) {
17466
17525
  opts.mcpUrl = next;
17467
17526
  i2++;
17527
+ } else if (arg === "--no-interactive") {
17528
+ opts.interactive = false;
17468
17529
  } else if (arg && arg.startsWith("--")) {
17469
17530
  console.error(`archme scan: unknown flag '${arg}'`);
17470
17531
  printScanHelp();
@@ -17514,6 +17575,7 @@ Usage:
17514
17575
  Flags:
17515
17576
  --project <org/slug> Target project. Defaults to e2e/<basename>.
17516
17577
  --branch <name> Target scan branch. Defaults to <date>-<sha7>-scan.
17578
+ --no-interactive Run headless (no questions). For CI/automation.
17517
17579
  --prompt-variant <id> Scan prompt variant. Defaults to latest.
17518
17580
  --mcp-url <url> MCP base URL. Defaults to $ARCHME_MCP_BASE_URL.
17519
17581
 
@@ -17534,6 +17596,584 @@ var init_dispatch3 = __esm({
17534
17596
  }
17535
17597
  });
17536
17598
 
17599
+ // src/init/repoSize.ts
17600
+ import { stat as stat9 } from "node:fs/promises";
17601
+ import { extname, join as join17 } from "node:path";
17602
+ async function measureRepo(repoPath) {
17603
+ const files = await gitListFiles(repoPath);
17604
+ if (!files) return { sourceFiles: 0, sourceBytes: 0, totalFiles: 0 };
17605
+ let sourceFiles = 0;
17606
+ let sourceBytes = 0;
17607
+ for (const rel of files) {
17608
+ if (!SOURCE_EXTENSIONS.has(extname(rel).toLowerCase())) continue;
17609
+ try {
17610
+ const s = await stat9(join17(repoPath, rel));
17611
+ if (!s.isFile()) continue;
17612
+ sourceFiles++;
17613
+ sourceBytes += s.size;
17614
+ } catch {
17615
+ }
17616
+ }
17617
+ return { sourceFiles, sourceBytes, totalFiles: files.length };
17618
+ }
17619
+ function fullReadTokens(sourceBytes) {
17620
+ return Math.round(sourceBytes / BYTES_PER_TOKEN);
17621
+ }
17622
+ function formatCount(n2) {
17623
+ if (n2 >= 1e6) return `${(n2 / 1e6).toFixed(1)}M`;
17624
+ if (n2 >= 1e3) return `${(n2 / 1e3).toFixed(n2 >= 1e4 ? 0 : 1)}k`;
17625
+ return String(n2);
17626
+ }
17627
+ function formatBytes(bytes) {
17628
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
17629
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
17630
+ return `${bytes} B`;
17631
+ }
17632
+ function expectedDuration(sourceFiles) {
17633
+ if (sourceFiles < 100) return "a few minutes";
17634
+ if (sourceFiles < 1e3) return "5\u201315 minutes";
17635
+ return "15\u201345 minutes";
17636
+ }
17637
+ function formatRepoBriefing(m, repoPath, target) {
17638
+ const lines = [
17639
+ "",
17640
+ "About to scan:",
17641
+ ` ${repoPath}`,
17642
+ ...target ? [` \u2192 project ${target.project}, branch ${target.branch}`] : [],
17643
+ ` ${formatCount(m.sourceFiles)} source files \xB7 ${formatBytes(m.sourceBytes)} (${formatCount(m.totalFiles)} files tracked)`,
17644
+ "",
17645
+ "What this costs:",
17646
+ " The scan runs Claude Code on YOUR machine, against YOUR Claude",
17647
+ " subscription or API key. ArchMe is not billed for it, and you are.",
17648
+ ` Reading every source file once would be ~${formatCount(fullReadTokens(m.sourceBytes))} tokens;`,
17649
+ " a scan reads a fraction of that, and reports its actual usage when done.",
17650
+ ` Expect it to take ${expectedDuration(m.sourceFiles)}.`,
17651
+ ""
17652
+ ];
17653
+ if (m.sourceFiles === 0) {
17654
+ lines.push(
17655
+ " No recognised source files found \u2014 is this the right directory?",
17656
+ ""
17657
+ );
17658
+ }
17659
+ return lines.join("\n");
17660
+ }
17661
+ var BYTES_PER_TOKEN, SOURCE_EXTENSIONS;
17662
+ var init_repoSize = __esm({
17663
+ "src/init/repoSize.ts"() {
17664
+ "use strict";
17665
+ init_repo_walk();
17666
+ BYTES_PER_TOKEN = 4;
17667
+ SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
17668
+ ".ts",
17669
+ ".tsx",
17670
+ ".js",
17671
+ ".jsx",
17672
+ ".mjs",
17673
+ ".cjs",
17674
+ ".py",
17675
+ ".go",
17676
+ ".java",
17677
+ ".rb",
17678
+ ".rs",
17679
+ ".swift",
17680
+ ".kt",
17681
+ ".kts",
17682
+ ".c",
17683
+ ".h",
17684
+ ".cc",
17685
+ ".cpp",
17686
+ ".hpp",
17687
+ ".cs",
17688
+ ".php",
17689
+ ".scala",
17690
+ ".ex",
17691
+ ".exs",
17692
+ ".sql",
17693
+ ".sh",
17694
+ ".bash"
17695
+ ]);
17696
+ }
17697
+ });
17698
+
17699
+ // src/init/watchRepo.ts
17700
+ async function watchRepoForProject(input) {
17701
+ const { supabase, orgId, projectId, repoSlug, branch } = input;
17702
+ if (!repoSlug) {
17703
+ return {
17704
+ watched: false,
17705
+ reason: "no GitHub remote found, so pushes cannot be linked to this project. Add a remote and re-run `archme init` for continuous updates."
17706
+ };
17707
+ }
17708
+ if (!orgId) {
17709
+ return { watched: false, reason: "could not resolve your organization." };
17710
+ }
17711
+ const [owner, name] = repoSlug.split("/");
17712
+ if (!owner || !name) {
17713
+ return { watched: false, reason: `could not parse repo slug \`${repoSlug}\`.` };
17714
+ }
17715
+ const { data: installs, error: installErr } = await supabase.from("org_github_installations").select("installation_id").eq("org_id", orgId).limit(1);
17716
+ if (installErr) {
17717
+ return {
17718
+ watched: false,
17719
+ reason: `could not check GitHub App installations (${installErr.message}).`
17720
+ };
17721
+ }
17722
+ const installationId = installs?.[0]?.installation_id;
17723
+ if (!installationId) {
17724
+ return {
17725
+ watched: false,
17726
+ reason: "the ArchMe GitHub App is not installed on this org yet \u2014 connect it in the app to get automatic updates on every push."
17727
+ };
17728
+ }
17729
+ const { error } = await supabase.rpc("watch_repo", {
17730
+ p_org_id: orgId,
17731
+ p_project_id: projectId,
17732
+ p_repo_owner: owner,
17733
+ p_repo_name: name,
17734
+ p_branch: branch,
17735
+ p_installation_id: installationId
17736
+ });
17737
+ if (error) {
17738
+ return { watched: false, reason: `could not watch the repo (${error.message}).` };
17739
+ }
17740
+ return { watched: true };
17741
+ }
17742
+ var init_watchRepo = __esm({
17743
+ "src/init/watchRepo.ts"() {
17744
+ "use strict";
17745
+ }
17746
+ });
17747
+
17748
+ // src/init/gitContext.ts
17749
+ async function git(cwd, args) {
17750
+ const { stdout } = await execa("git", args, { cwd });
17751
+ return stdout.trim();
17752
+ }
17753
+ async function resolveGitContext(repoPath) {
17754
+ let root;
17755
+ try {
17756
+ root = await git(repoPath, ["rev-parse", "--show-toplevel"]);
17757
+ } catch {
17758
+ throw new Error(
17759
+ `\`${repoPath}\` is not inside a git repository \u2014 run \`archme init\` from a repo, or pass its path`
17760
+ );
17761
+ }
17762
+ try {
17763
+ await git(root, ["rev-parse", "HEAD"]);
17764
+ } catch {
17765
+ throw new Error(
17766
+ `\`${root}\` is a git repository with no commits yet \u2014 make a commit first, then run \`archme init\``
17767
+ );
17768
+ }
17769
+ let branch = "";
17770
+ try {
17771
+ branch = await git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
17772
+ } catch {
17773
+ branch = "";
17774
+ }
17775
+ if (!branch || branch === "HEAD") {
17776
+ try {
17777
+ const ref = await git(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
17778
+ branch = ref.replace(/^refs\/remotes\/origin\//, "");
17779
+ } catch {
17780
+ branch = "main";
17781
+ }
17782
+ }
17783
+ let repoSlug = null;
17784
+ try {
17785
+ const url = await git(root, ["remote", "get-url", "origin"]);
17786
+ repoSlug = url ? parseGitHubRepo(url) : null;
17787
+ } catch {
17788
+ repoSlug = null;
17789
+ }
17790
+ return { root, branch, repoSlug };
17791
+ }
17792
+ var init_gitContext = __esm({
17793
+ "src/init/gitContext.ts"() {
17794
+ "use strict";
17795
+ init_execa();
17796
+ init_github_remote();
17797
+ }
17798
+ });
17799
+
17800
+ // src/init/plan.ts
17801
+ function shouldRunScan(d) {
17802
+ if (d.skipScan) return false;
17803
+ if (d.force) return true;
17804
+ return !d.architectureExists;
17805
+ }
17806
+ function buildProjectMcpUrl(mcpBaseUrl, project, branch) {
17807
+ const url = new URL(mcpBaseUrl);
17808
+ url.searchParams.set("project", project);
17809
+ url.searchParams.set("branch", branch);
17810
+ return url.toString();
17811
+ }
17812
+ function resolveAppOrigin(mcpBaseUrl, override) {
17813
+ if (override) return override.replace(/\/$/, "");
17814
+ return deriveWebOrigin(mcpBaseUrl, "fallback");
17815
+ }
17816
+ function buildInstallAgentUrl(appOrigin, mcpUrl) {
17817
+ const url = new URL("/api/install-agent", appOrigin);
17818
+ url.searchParams.set("mcp_url", mcpUrl);
17819
+ return url.toString();
17820
+ }
17821
+ var init_plan = __esm({
17822
+ "src/init/plan.ts"() {
17823
+ "use strict";
17824
+ init_web_url();
17825
+ }
17826
+ });
17827
+
17828
+ // src/init/run-init.ts
17829
+ import { mkdtemp as mkdtemp4, rm as rm8, writeFile as writeFile11 } from "node:fs/promises";
17830
+ import { tmpdir as tmpdir4 } from "node:os";
17831
+ import { join as join18 } from "node:path";
17832
+ function initScanBranch(defaultBranch) {
17833
+ return normalizeScanBranchName(defaultBranch);
17834
+ }
17835
+ async function defaultFetchInstallScript(url) {
17836
+ const resp = await fetch(url);
17837
+ if (!resp.ok) {
17838
+ throw new Error(`could not fetch agent installer (${resp.status} ${resp.statusText}) from ${url}`);
17839
+ }
17840
+ return await resp.text();
17841
+ }
17842
+ async function defaultRunInstallScript(script, cwd) {
17843
+ const dir = await mkdtemp4(join18(tmpdir4(), "archme-init-"));
17844
+ const scriptPath = join18(dir, "install-agent.sh");
17845
+ try {
17846
+ await writeFile11(scriptPath, script, { mode: 448 });
17847
+ await execa("sh", [scriptPath], { cwd, stdio: "inherit" });
17848
+ } finally {
17849
+ await rm8(dir, { recursive: true, force: true });
17850
+ }
17851
+ }
17852
+ async function runInit(opts, deps = {}) {
17853
+ const log = opts.log ?? ((msg) => console.log(msg));
17854
+ const login = deps.login ?? runLogin;
17855
+ const fetchInstallScript = deps.fetchInstallScript ?? defaultFetchInstallScript;
17856
+ const runInstallScript = deps.runInstallScript ?? defaultRunInstallScript;
17857
+ const scan = deps.scan ?? runScan;
17858
+ const authenticate = deps.authenticate ?? createAuthedClient;
17859
+ const resolveProject = deps.resolveProject ?? resolveOrCreateProject;
17860
+ const resolveOrgSlug = deps.resolveOrgSlug ?? resolveCurrentOrgSlug;
17861
+ const fetchArchitecture = deps.fetchArchitecture ?? fetchArchitectureDetail;
17862
+ const loadCreds = deps.loadCredentials ?? loadAllCredentials;
17863
+ const gitContext = deps.gitContext ?? resolveGitContext;
17864
+ const measure = deps.measure ?? measureRepo;
17865
+ const watchRepo = deps.watchRepo ?? watchRepoForProject;
17866
+ const cfg = resolveConfig();
17867
+ const git2 = await gitContext(opts.repoPath);
17868
+ const repoPath = git2.root;
17869
+ const { credentials } = await loadCreds(cfg.supabaseUrl);
17870
+ const signedIn = credentials.length > 0;
17871
+ if (!signedIn) {
17872
+ log("\u2192 signing you in\u2026");
17873
+ await login();
17874
+ } else {
17875
+ log("\u2713 already signed in");
17876
+ }
17877
+ const authed = await authenticate({
17878
+ supabaseUrl: cfg.supabaseUrl,
17879
+ supabaseAnonKey: cfg.supabaseAnonKey,
17880
+ clientId: cfg.archmeDaemonClientId,
17881
+ preSpawnMinLifetimeMs: SCAN_PRE_SPAWN_TOKEN_MIN_LIFETIME_MS
17882
+ });
17883
+ log(` signed in as ${authed.identity}`);
17884
+ let fullSlug = opts.project;
17885
+ if (!fullSlug) {
17886
+ const orgId = authed.orgId;
17887
+ if (!orgId) {
17888
+ throw new Error(
17889
+ "could not resolve your organization \u2014 run `archme login` again, or pass --project <org>/<slug>"
17890
+ );
17891
+ }
17892
+ const orgSlug = await resolveOrgSlug(authed.supabase, orgId);
17893
+ if (!orgSlug) {
17894
+ throw new Error(
17895
+ `paired org ${orgId} no longer exists \u2014 run \`archme login\` again, or pass --project <org>/<slug>`
17896
+ );
17897
+ }
17898
+ fullSlug = defaultProjectSlug(orgSlug, repoPath);
17899
+ }
17900
+ const project = await resolveProject(
17901
+ authed.supabase,
17902
+ authed.userId,
17903
+ fullSlug
17904
+ );
17905
+ log(
17906
+ project.created ? `\u2713 created project ${fullSlug}` : `\u2713 using existing project ${fullSlug}`
17907
+ );
17908
+ const scanBranch = initScanBranch(project.defaultBranch);
17909
+ const detail = await fetchArchitecture(
17910
+ authed.supabase,
17911
+ project.projectId,
17912
+ scanBranch
17913
+ );
17914
+ const architectureExists = detail.components.length > 0;
17915
+ const ran = signedIn ? [] : ["login"];
17916
+ ran.push("project");
17917
+ const mcpUrl = buildProjectMcpUrl(cfg.archmeMcpBaseUrl, fullSlug, scanBranch);
17918
+ const appOrigin = resolveAppOrigin(cfg.archmeMcpBaseUrl, opts.appOrigin);
17919
+ log("\u2192 installing agent config (.mcp.json, CLAUDE.md, architecture skill)\u2026");
17920
+ const script = await fetchInstallScript(buildInstallAgentUrl(appOrigin, mcpUrl));
17921
+ await runInstallScript(script, repoPath);
17922
+ ran.push("agent-config");
17923
+ const watched = await watchRepo({
17924
+ supabase: authed.supabase,
17925
+ orgId: authed.orgId,
17926
+ projectId: project.projectId,
17927
+ repoSlug: git2.repoSlug,
17928
+ branch: git2.branch,
17929
+ log
17930
+ });
17931
+ if (!watched.watched && watched.reason) {
17932
+ log(` note: ${watched.reason}`);
17933
+ }
17934
+ const webUrl = buildWebUrl({
17935
+ apiUrl: cfg.archmeMcpBaseUrl,
17936
+ orgSlug: project.orgSlug,
17937
+ projectSlug: project.projectSlug,
17938
+ branchName: scanBranch,
17939
+ defaultBranch: project.defaultBranch,
17940
+ onMalformed: "throw"
17941
+ });
17942
+ const wantsScan = shouldRunScan({
17943
+ architectureExists,
17944
+ force: opts.force,
17945
+ skipScan: opts.skipScan
17946
+ });
17947
+ if (!wantsScan) {
17948
+ if (architectureExists && !opts.force) {
17949
+ log(`\u2713 ${fullSlug} already has an architecture \u2014 skipping the scan (use --force to re-scan)`);
17950
+ }
17951
+ log(`
17952
+ Your board: ${webUrl}`);
17953
+ return { exitCode: 0, project: fullSlug, branch: scanBranch, webUrl, ran };
17954
+ }
17955
+ const measurement = await measure(repoPath);
17956
+ const briefing = formatRepoBriefing(measurement, repoPath, {
17957
+ project: fullSlug,
17958
+ branch: scanBranch
17959
+ });
17960
+ log("\u2192 handing over to Claude Code \u2014 it will confirm before scanning");
17961
+ const result = await scan({
17962
+ repoPath,
17963
+ project: fullSlug,
17964
+ // --yes means unattended: run headless and skip the agent's confirmations.
17965
+ interactive: !opts.yes,
17966
+ briefing,
17967
+ // Explicit: without it runScan derives a dated `<date>-<sha7>-scan` branch
17968
+ // that nothing else in this flow knows about.
17969
+ branch: scanBranch,
17970
+ log
17971
+ });
17972
+ ran.push("scan");
17973
+ const finalUrl = result.webUrl || webUrl;
17974
+ if (result.exitCode === 0) {
17975
+ log(`
17976
+ \u2713 Done. Your architecture: ${finalUrl}`);
17977
+ log(" Restart Claude Code in this repo to pick up the archme MCP server.");
17978
+ if (watched.watched) {
17979
+ log("");
17980
+ log("Keeping it up to date \u2014 one more step:");
17981
+ log(" Pushes to this repo now queue an architecture update, but");
17982
+ log(" something has to process that queue, and nothing does yet.");
17983
+ log("");
17984
+ log(" Run the agent yourself, on a machine that stays on:");
17985
+ log("");
17986
+ log(" archme daemon start");
17987
+ log("");
17988
+ log(" Each update counts tokens against your Claude account, the same");
17989
+ log(" way this scan did. Your code is not sent to our servers \u2014 only");
17990
+ log(" the resulting architecture.");
17991
+ log("");
17992
+ log(" Or get in touch at archme.io and we'll run it for you.");
17993
+ } else {
17994
+ log("");
17995
+ log("Not yet set up for automatic updates:");
17996
+ log(` ${watched.reason ?? "the repo could not be linked to this project."}`);
17997
+ log(" Until that is resolved, this architecture will not track new");
17998
+ log(" commits \u2014 re-run `archme init` once it is.");
17999
+ }
18000
+ }
18001
+ return {
18002
+ exitCode: result.exitCode,
18003
+ project: fullSlug,
18004
+ branch: result.branchName || scanBranch,
18005
+ webUrl: finalUrl,
18006
+ ran
18007
+ };
18008
+ }
18009
+ var init_run_init = __esm({
18010
+ "src/init/run-init.ts"() {
18011
+ "use strict";
18012
+ init_execa();
18013
+ init_defaults();
18014
+ init_enroll();
18015
+ init_credentials();
18016
+ init_authedClient();
18017
+ init_run_scan();
18018
+ init_architecture_detail();
18019
+ init_repoSize();
18020
+ init_watchRepo();
18021
+ init_gitContext();
18022
+ init_web_url();
18023
+ init_plan();
18024
+ }
18025
+ });
18026
+
18027
+ // src/init/args.ts
18028
+ function parseInitArgs(argv) {
18029
+ const args = {
18030
+ force: false,
18031
+ skipScan: false,
18032
+ yes: false,
18033
+ noInteractive: false,
18034
+ help: false
18035
+ };
18036
+ const positionals = [];
18037
+ for (let i2 = 0; i2 < argv.length; i2++) {
18038
+ const arg = argv[i2];
18039
+ if (HELP_FLAGS.has(arg)) {
18040
+ args.help = true;
18041
+ return { ok: true, args };
18042
+ }
18043
+ if (BOOLEAN_FLAGS.has(arg)) {
18044
+ if (arg === "--force") args.force = true;
18045
+ else if (arg === "--yes") args.yes = true;
18046
+ else if (arg === "--no-interactive") args.noInteractive = true;
18047
+ else args.skipScan = true;
18048
+ continue;
18049
+ }
18050
+ const eq = arg.indexOf("=");
18051
+ const name = arg.startsWith("--") && eq > 0 ? arg.slice(0, eq) : arg;
18052
+ if (VALUE_FLAGS.has(name)) {
18053
+ let value;
18054
+ if (eq > 0 && arg.startsWith("--")) {
18055
+ value = arg.slice(eq + 1);
18056
+ } else {
18057
+ value = argv[i2 + 1];
18058
+ if (value !== void 0 && value.startsWith("-")) value = void 0;
18059
+ else i2++;
18060
+ }
18061
+ if (!value) return { ok: false, error: `${name} requires a value` };
18062
+ if (name === "--project") {
18063
+ if (!PROJECT_SLUG_RE2.test(value)) {
18064
+ return {
18065
+ ok: false,
18066
+ error: `--project must be <org>/<slug> (got \`${value}\`)`
18067
+ };
18068
+ }
18069
+ args.project = value;
18070
+ } else {
18071
+ args.appOrigin = value;
18072
+ }
18073
+ continue;
18074
+ }
18075
+ if (arg.startsWith("-")) {
18076
+ return { ok: false, error: `unknown option \`${arg}\`` };
18077
+ }
18078
+ positionals.push(arg);
18079
+ }
18080
+ if (positionals.length > 1) {
18081
+ return {
18082
+ ok: false,
18083
+ error: `expected at most one repo path, got ${positionals.length} (${positionals.join(", ")})`
18084
+ };
18085
+ }
18086
+ args.repoPath = positionals[0];
18087
+ return { ok: true, args };
18088
+ }
18089
+ var BOOLEAN_FLAGS, VALUE_FLAGS, HELP_FLAGS, PROJECT_SLUG_RE2;
18090
+ var init_args = __esm({
18091
+ "src/init/args.ts"() {
18092
+ "use strict";
18093
+ BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
18094
+ "--force",
18095
+ "--skip-scan",
18096
+ "--yes",
18097
+ "--no-interactive"
18098
+ ]);
18099
+ VALUE_FLAGS = /* @__PURE__ */ new Set(["--project", "--app-origin"]);
18100
+ HELP_FLAGS = /* @__PURE__ */ new Set(["--help", "-h", "help"]);
18101
+ PROJECT_SLUG_RE2 = /^[^/\s]+\/[^/\s]+$/;
18102
+ }
18103
+ });
18104
+
18105
+ // src/init/dispatch.ts
18106
+ var dispatch_exports4 = {};
18107
+ __export(dispatch_exports4, {
18108
+ EX_INFRA: () => EX_INFRA2,
18109
+ EX_OK: () => EX_OK2,
18110
+ EX_USAGE: () => EX_USAGE2,
18111
+ dispatchInit: () => dispatchInit,
18112
+ printInitHelp: () => printInitHelp
18113
+ });
18114
+ import { resolve as resolve3 } from "node:path";
18115
+ function printInitHelp() {
18116
+ console.log(`archme init \u2014 guided first-time setup for this repo
18117
+
18118
+ Signs you in, creates a project named after the repo, installs agent config
18119
+ (.mcp.json, CLAUDE.md block, architecture skill), then hands over to an
18120
+ interactive Claude Code session that confirms the cost and runs the scan.
18121
+
18122
+ Usage:
18123
+ archme init [repo-path] [options]
18124
+
18125
+ repo-path Defaults to the current directory.
18126
+
18127
+ Options:
18128
+ --project <org/slug> Use a specific project instead of <your-org>/<repo-name>
18129
+ --skip-scan Set up agent config but do not scan
18130
+ --force Re-scan even if the project already has an architecture
18131
+ --yes Run unattended (headless scan, no questions)
18132
+ --no-interactive Same as --yes; for automation
18133
+ --app-origin <url> Origin serving /api/install-agent (local development)
18134
+ -h, --help Show this help
18135
+
18136
+ Requires Claude Code to be installed and authenticated \u2014 the scan runs on your
18137
+ own Claude subscription.`);
18138
+ }
18139
+ async function dispatchInit(argv) {
18140
+ const parsed = parseInitArgs(argv);
18141
+ if (!parsed.ok) {
18142
+ console.error(`archme init: ${parsed.error}`);
18143
+ printInitHelp();
18144
+ return EX_USAGE2;
18145
+ }
18146
+ if (parsed.args.help) {
18147
+ printInitHelp();
18148
+ return EX_OK2;
18149
+ }
18150
+ try {
18151
+ const result = await runInit({
18152
+ repoPath: resolve3(parsed.args.repoPath ?? process.cwd()),
18153
+ project: parsed.args.project,
18154
+ appOrigin: parsed.args.appOrigin,
18155
+ force: parsed.args.force,
18156
+ skipScan: parsed.args.skipScan,
18157
+ yes: parsed.args.yes || parsed.args.noInteractive
18158
+ });
18159
+ return result.exitCode;
18160
+ } catch (err) {
18161
+ console.error(`archme init: ${err instanceof Error ? err.message : String(err)}`);
18162
+ return EX_INFRA2;
18163
+ }
18164
+ }
18165
+ var EX_OK2, EX_INFRA2, EX_USAGE2;
18166
+ var init_dispatch4 = __esm({
18167
+ "src/init/dispatch.ts"() {
18168
+ "use strict";
18169
+ init_run_init();
18170
+ init_args();
18171
+ EX_OK2 = 0;
18172
+ EX_INFRA2 = 3;
18173
+ EX_USAGE2 = 78;
18174
+ }
18175
+ });
18176
+
17537
18177
  // src/auth/logout.ts
17538
18178
  var logout_exports = {};
17539
18179
  __export(logout_exports, {
@@ -17580,7 +18220,7 @@ var init_logout = __esm({
17580
18220
 
17581
18221
  // src/cli.ts
17582
18222
  init_enroll();
17583
- var KNOWN_COMMANDS = /* @__PURE__ */ new Set(["daemon", "analyze", "login", "logout", "keys", "scan"]);
18223
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set(["daemon", "analyze", "login", "logout", "keys", "scan", "init"]);
17584
18224
  function route(argv) {
17585
18225
  if (argv.length === 0) return { name: "help", args: [] };
17586
18226
  const [first, ...rest] = argv;
@@ -17658,6 +18298,10 @@ async function main(argv) {
17658
18298
  const { dispatchScan: dispatchScan2 } = await Promise.resolve().then(() => (init_dispatch3(), dispatch_exports3));
17659
18299
  return await dispatchScan2(r.args);
17660
18300
  }
18301
+ case "init": {
18302
+ const { dispatchInit: dispatchInit2 } = await Promise.resolve().then(() => (init_dispatch4(), dispatch_exports4));
18303
+ return await dispatchInit2(r.args);
18304
+ }
17661
18305
  case "logout": {
17662
18306
  const { runLogout: runLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
17663
18307
  return await runLogout2(r.args);
@@ -17671,6 +18315,8 @@ function printHelp() {
17671
18315
  console.log(`archme \u2014 architecture-aware code analysis and daemon
17672
18316
 
17673
18317
  Usage:
18318
+ archme init [repo-path] Guided first-time setup (login, project,
18319
+ agent config, scan)
17674
18320
  archme daemon <subcommand> Manage the local daemon as a service
17675
18321
  (start|stop|status|logs|restart|enable|
17676
18322
  disable|install|uninstall|run)
@@ -17679,7 +18325,8 @@ Usage:
17679
18325
  archme login | logout Authenticate with archme
17680
18326
  archme keys list|create|revoke Manage MCP API keys
17681
18327
 
17682
- Run 'archme daemon --help', 'archme analyze --help', or 'archme scan --help' for subcommand help.`);
18328
+ Run 'archme init --help', 'archme daemon --help', 'archme analyze --help', or
18329
+ 'archme scan --help' for subcommand help.`);
17683
18330
  }
17684
18331
 
17685
18332
  // src/bin.ts
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@archme/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "archme command-line interface: daemon, analyze, auth, key management",
5
5
  "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
6
7
  "bin": {
7
8
  "archme": "dist/cli.js"
8
9
  },
9
10
  "files": [
10
11
  "dist",
11
12
  "SKILL.md",
12
- "README.md"
13
+ "README.md",
14
+ "LICENSE"
13
15
  ],
14
16
  "engines": {
15
17
  "node": ">=20.0.0"