@amdahl/cli 0.3.3 → 0.3.4

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 +129 -51
  2. package/dist/amdahl.js +516 -206
  3. package/package.json +1 -1
package/dist/amdahl.js CHANGED
@@ -315,8 +315,8 @@ var require_directives = __commonJS({
315
315
  if (parts.length < 2)
316
316
  return false;
317
317
  }
318
- const [handle, prefix] = parts;
319
- this.tags[handle] = prefix;
318
+ const [handle, prefix2] = parts;
319
+ this.tags[handle] = prefix2;
320
320
  return true;
321
321
  }
322
322
  case "%YAML": {
@@ -366,10 +366,10 @@ var require_directives = __commonJS({
366
366
  const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
367
367
  if (!suffix)
368
368
  onError(`The ${source} tag has no suffix`);
369
- const prefix = this.tags[handle];
370
- if (prefix) {
369
+ const prefix2 = this.tags[handle];
370
+ if (prefix2) {
371
371
  try {
372
- return prefix + decodeURIComponent(suffix);
372
+ return prefix2 + decodeURIComponent(suffix);
373
373
  } catch (error) {
374
374
  onError(String(error));
375
375
  return null;
@@ -385,9 +385,9 @@ var require_directives = __commonJS({
385
385
  * taking into account current tag prefixes and defaults.
386
386
  */
387
387
  tagString(tag) {
388
- for (const [handle, prefix] of Object.entries(this.tags)) {
389
- if (tag.startsWith(prefix))
390
- return handle + escapeTagName(tag.substring(prefix.length));
388
+ for (const [handle, prefix2] of Object.entries(this.tags)) {
389
+ if (tag.startsWith(prefix2))
390
+ return handle + escapeTagName(tag.substring(prefix2.length));
391
391
  }
392
392
  return tag[0] === "!" ? tag : `!<${tag}>`;
393
393
  }
@@ -404,11 +404,11 @@ var require_directives = __commonJS({
404
404
  tagNames = Object.keys(tags);
405
405
  } else
406
406
  tagNames = [];
407
- for (const [handle, prefix] of tagEntries) {
408
- if (handle === "!!" && prefix === "tag:yaml.org,2002:")
407
+ for (const [handle, prefix2] of tagEntries) {
408
+ if (handle === "!!" && prefix2 === "tag:yaml.org,2002:")
409
409
  continue;
410
- if (!doc || tagNames.some((tn) => tn.startsWith(prefix)))
411
- lines.push(`%TAG ${handle} ${prefix}`);
410
+ if (!doc || tagNames.some((tn) => tn.startsWith(prefix2)))
411
+ lines.push(`%TAG ${handle} ${prefix2}`);
412
412
  }
413
413
  return lines.join("\n");
414
414
  }
@@ -443,14 +443,14 @@ var require_anchors = __commonJS({
443
443
  });
444
444
  return anchors;
445
445
  }
446
- function findNewAnchor(prefix, exclude) {
446
+ function findNewAnchor(prefix2, exclude) {
447
447
  for (let i = 1; true; ++i) {
448
- const name = `${prefix}${i}`;
448
+ const name = `${prefix2}${i}`;
449
449
  if (!exclude.has(name))
450
450
  return name;
451
451
  }
452
452
  }
453
- function createNodeAnchors(doc, prefix) {
453
+ function createNodeAnchors(doc, prefix2) {
454
454
  const aliasObjects = [];
455
455
  const sourceObjects = /* @__PURE__ */ new Map();
456
456
  let prevAnchors = null;
@@ -458,7 +458,7 @@ var require_anchors = __commonJS({
458
458
  onAnchor: (source) => {
459
459
  aliasObjects.push(source);
460
460
  prevAnchors ?? (prevAnchors = anchorNames(doc));
461
- const anchor = findNewAnchor(prefix, prevAnchors);
461
+ const anchor = findNewAnchor(prefix2, prevAnchors);
462
462
  prevAnchors.add(anchor);
463
463
  return anchor;
464
464
  },
@@ -2451,10 +2451,10 @@ var require_int = __commonJS({
2451
2451
  var stringifyNumber = require_stringifyNumber();
2452
2452
  var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
2453
2453
  var intResolve = (str2, offset, radix, { intAsBigInt }) => intAsBigInt ? BigInt(str2) : parseInt(str2.substring(offset), radix);
2454
- function intStringify(node, radix, prefix) {
2454
+ function intStringify(node, radix, prefix2) {
2455
2455
  const { value } = node;
2456
2456
  if (intIdentify(value) && value >= 0)
2457
- return prefix + value.toString(radix);
2457
+ return prefix2 + value.toString(radix);
2458
2458
  return stringifyNumber.stringifyNumber(node);
2459
2459
  }
2460
2460
  var intOct = {
@@ -2916,11 +2916,11 @@ var require_int2 = __commonJS({
2916
2916
  const n = parseInt(str2, radix);
2917
2917
  return sign === "-" ? -1 * n : n;
2918
2918
  }
2919
- function intStringify(node, radix, prefix) {
2919
+ function intStringify(node, radix, prefix2) {
2920
2920
  const { value } = node;
2921
2921
  if (intIdentify(value)) {
2922
2922
  const str2 = value.toString(radix);
2923
- return value < 0 ? "-" + prefix + str2.substr(1) : prefix + str2;
2923
+ return value < 0 ? "-" + prefix2 + str2.substr(1) : prefix2 + str2;
2924
2924
  }
2925
2925
  return stringifyNumber.stringifyNumber(node);
2926
2926
  }
@@ -7365,26 +7365,45 @@ var require_dist = __commonJS({
7365
7365
 
7366
7366
  // src/io.ts
7367
7367
  import { spawn } from "child_process";
7368
+ import { accessSync, constants, statSync } from "fs";
7368
7369
  import { homedir, hostname } from "os";
7369
7370
  import { createInterface } from "readline";
7370
7371
 
7371
7372
  // src/select.ts
7372
7373
  import { emitKeypressEvents } from "readline";
7373
7374
  var HINT = "(Use the arrow keys, then Enter)";
7374
- var MULTI_HINT = "(Use the arrow keys, Space to tick or untick, then Enter)";
7375
+ var MULTI_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter to accept";
7375
7376
  var OFF = "\x1B[0m";
7377
+ var GREY = "\x1B[90m";
7378
+ var GREY_OFF = "\x1B[39m";
7376
7379
  var HIDE_CURSOR = "\x1B[?25l";
7377
7380
  var SHOW_CURSOR = "\x1B[?25h";
7378
7381
  var CLEAR_LINE = "\x1B[2K";
7379
7382
  var CLEAR_DOWN = "\x1B[0J";
7380
- var up = (n) => `\x1B[${n}A`;
7381
- function runMenu(input, output, question, hint, rows, onKey) {
7382
- const draw = () => rows().map((r) => `${CLEAR_LINE}\r${r}
7383
- `).join("") + `${CLEAR_LINE}\r${hint}
7384
- `;
7385
- const height = rows().length;
7383
+ var up = (n) => n > 0 ? `\x1B[${n}A` : "";
7384
+ var ESCAPES = /\u001b\[[0-9;?]*[A-Za-z]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
7385
+ function eraseUp(lines) {
7386
+ return `\r${up(lines)}${CLEAR_DOWN}`;
7387
+ }
7388
+ function screenLines(text2, columns) {
7389
+ const width = [...text2.replace(ESCAPES, "")].length;
7390
+ if (!columns || columns < 1) return 1;
7391
+ return Math.max(1, Math.ceil(width / columns));
7392
+ }
7393
+ function runMenu(input, output, head, rows, onKey) {
7394
+ const { question, note, hint } = head;
7395
+ const width = () => head.columns?.();
7396
+ let headLines = 0;
7397
+ let drawn = 0;
7398
+ const frame = () => {
7399
+ const cols = width();
7400
+ const lines = [...rows(), ...fitHint(hint, cols)];
7401
+ drawn = lines.reduce((n, line) => n + screenLines(line, cols), 0);
7402
+ return lines.map((line) => `${CLEAR_LINE}\r${line}
7403
+ `).join("");
7404
+ };
7386
7405
  const wasRaw = input.isRaw === true;
7387
- return new Promise((resolve, reject) => {
7406
+ return new Promise((resolve2, reject) => {
7388
7407
  let done = false;
7389
7408
  const finish = (outcome) => {
7390
7409
  if (done) return;
@@ -7398,19 +7417,22 @@ function runMenu(input, output, question, hint, rows, onKey) {
7398
7417
  }
7399
7418
  input.pause();
7400
7419
  if ("pick" in outcome) {
7401
- output.write(`${up(height + 2)}\r${CLEAR_DOWN}${question} ${outcome.summary}
7420
+ output.write(`\r${up(headLines + drawn)}${CLEAR_DOWN}${question} ${outcome.summary}
7402
7421
  ${SHOW_CURSOR}`);
7403
- resolve(outcome.pick);
7422
+ resolve2(outcome.pick);
7404
7423
  } else {
7405
- output.write(SHOW_CURSOR);
7424
+ output.write(`\r${up(headLines + drawn)}${CLEAR_DOWN}${question}
7425
+ ${SHOW_CURSOR}`);
7406
7426
  reject(outcome.error);
7407
7427
  }
7408
7428
  };
7409
7429
  const onKeypress = (text2, key) => {
7410
7430
  const name = key?.name ?? text2;
7411
- if (key?.ctrl && (name === "c" || name === "d")) return finish({ error: new PromptCancelled() });
7431
+ if (key?.ctrl && (name === "c" || name === "d") || name === "escape" || name === "q" && !key?.ctrl) {
7432
+ return finish({ error: new PromptCancelled() });
7433
+ }
7412
7434
  const result = onKey(text2, name);
7413
- if (result === "redraw") output.write(`${up(height + 1)}${draw()}`);
7435
+ if (result === "redraw") output.write(`\r${up(drawn)}${CLEAR_DOWN}${frame()}`);
7414
7436
  else if (result) finish(result);
7415
7437
  };
7416
7438
  const onError = (err) => finish({ error: err });
@@ -7418,8 +7440,11 @@ ${SHOW_CURSOR}`);
7418
7440
  try {
7419
7441
  emitKeypressEvents(input);
7420
7442
  input.setRawMode?.(true);
7421
- output.write(`${HIDE_CURSOR}${question}
7422
- ${draw()}`);
7443
+ const cols = width();
7444
+ const top = [question, ...note ? wrapWords(note, cols) : []];
7445
+ headLines = top.reduce((n, line) => n + screenLines(line, cols), 0);
7446
+ output.write(`${HIDE_CURSOR}${top.map((line) => `${line}
7447
+ `).join("")}${frame()}`);
7423
7448
  input.on("keypress", onKeypress);
7424
7449
  input.on("error", onError);
7425
7450
  process.on("SIGINT", onSigint);
@@ -7429,6 +7454,26 @@ ${draw()}`);
7429
7454
  }
7430
7455
  });
7431
7456
  }
7457
+ function fitHint(hint, columns) {
7458
+ if (!columns || [...hint].length <= columns) return [hint];
7459
+ const lines = [];
7460
+ for (const part of hint.split(" \xB7 ")) {
7461
+ const last = lines[lines.length - 1];
7462
+ if (last !== void 0 && [...`${last} \xB7 ${part}`].length <= columns) lines[lines.length - 1] = `${last} \xB7 ${part}`;
7463
+ else lines.push(part);
7464
+ }
7465
+ return lines;
7466
+ }
7467
+ function wrapWords(text2, columns) {
7468
+ if (!columns || [...text2].length <= columns) return [text2];
7469
+ const lines = [];
7470
+ for (const word of text2.split(" ")) {
7471
+ const last = lines[lines.length - 1];
7472
+ if (last !== void 0 && [...`${last} ${word}`].length <= columns) lines[lines.length - 1] = `${last} ${word}`;
7473
+ else lines.push(word);
7474
+ }
7475
+ return lines;
7476
+ }
7432
7477
  function marker(options) {
7433
7478
  return options.color ? options.trueColor ? "\x1B[1;38;2;253;75;3m" : "\x1B[1;38;5;202m" : "\x1B[1m";
7434
7479
  }
@@ -7442,7 +7487,7 @@ function selectMenu(input, output, question, choices, options = {}) {
7442
7487
  let current = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
7443
7488
  const on = marker(options);
7444
7489
  const rows = () => choices.map((c, i) => i === current ? `${on}> ${c}${OFF}` : ` ${c}`);
7445
- return runMenu(input, output, question, HINT, rows, (text2, name) => {
7490
+ return runMenu(input, output, { question, hint: HINT, columns: options.columns }, rows, (text2, name) => {
7446
7491
  if (name === "return" || name === "enter") return { pick: current, summary: choices[current] ?? "" };
7447
7492
  const next = move(current, choices.length, text2, name);
7448
7493
  if (next === null || next === current) return null;
@@ -7451,14 +7496,23 @@ function selectMenu(input, output, question, choices, options = {}) {
7451
7496
  });
7452
7497
  }
7453
7498
  function multiSelectMenu(input, output, question, choices, options = {}) {
7454
- let current = 0;
7455
7499
  const ticked = choices.map((_, i) => options.ticked?.[i] === true);
7456
- const on = marker(options);
7457
- const rows = () => choices.map((c, i) => {
7458
- const row = `[${ticked[i] ? "x" : " "}] ${c}`;
7459
- return i === current ? `${on}> ${row}${OFF}` : ` ${row}`;
7460
- });
7461
- return runMenu(input, output, question, MULTI_HINT, rows, (text2, name) => {
7500
+ let current = Math.max(ticked.indexOf(true), 0);
7501
+ const pointer = `${marker(options)}>${OFF}`;
7502
+ const pad = Math.max(...choices.map((c) => c.length));
7503
+ const label = (i) => {
7504
+ const tag = options.tags?.[i];
7505
+ const name = choices[i] ?? "";
7506
+ if (!tag) return name;
7507
+ const cols = options.columns?.() ?? Infinity;
7508
+ const grey = options.color ? `${GREY}${tag}${GREY_OFF}` : tag;
7509
+ if (6 + pad + 3 + tag.length <= cols) return `${name.padEnd(pad)} ${grey}`;
7510
+ if (6 + name.length + 1 + tag.length <= cols) return `${name} ${grey}`;
7511
+ return name;
7512
+ };
7513
+ const rows = () => choices.map((_, i) => `${i === current ? pointer : " "} [${ticked[i] ? "x" : " "}] ${label(i)}`);
7514
+ const head = { question, note: options.note, hint: options.hint ?? MULTI_HINT, columns: options.columns };
7515
+ return runMenu(input, output, head, rows, (text2, name) => {
7462
7516
  if (name === "return" || name === "enter") {
7463
7517
  const picks = choices.flatMap((_, i) => ticked[i] ? [i] : []);
7464
7518
  return { pick: picks, summary: picks.map((i) => choices[i]).join(", ") || "none" };
@@ -7522,20 +7576,20 @@ var PromptCancelled = class extends Error {
7522
7576
  this.name = "PromptCancelled";
7523
7577
  }
7524
7578
  };
7525
- var runProcess = (command, args, options) => new Promise((resolve) => {
7579
+ var runProcess = (command, args, options) => new Promise((resolve2) => {
7526
7580
  let stdout = "";
7527
7581
  let stderr = "";
7528
7582
  let child;
7529
7583
  try {
7530
7584
  child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
7531
7585
  } catch (err) {
7532
- resolve({ code: -1, stdout: "", stderr: String(err) });
7586
+ resolve2({ code: -1, stdout: "", stderr: String(err) });
7533
7587
  return;
7534
7588
  }
7535
7589
  child.stdout.on("data", (d) => stdout += d.toString("utf8"));
7536
7590
  child.stderr.on("data", (d) => stderr += d.toString("utf8"));
7537
- child.on("error", (err) => resolve({ code: -1, stdout, stderr: String(err) }));
7538
- child.on("close", (code) => resolve({ code: code ?? -1, stdout, stderr }));
7591
+ child.on("error", (err) => resolve2({ code: -1, stdout, stderr: String(err) }));
7592
+ child.on("close", (code) => resolve2({ code: code ?? -1, stdout, stderr }));
7539
7593
  child.stdin.on("error", () => {
7540
7594
  });
7541
7595
  if (options?.input !== void 0) child.stdin.end(options.input);
@@ -7548,7 +7602,7 @@ async function readAllStdin() {
7548
7602
  }
7549
7603
  function promptLine(question) {
7550
7604
  const rl = createInterface({ input: process.stdin, output: process.stderr });
7551
- return new Promise((resolve, reject) => {
7605
+ return new Promise((resolve2, reject) => {
7552
7606
  let settled = false;
7553
7607
  const cancel = () => {
7554
7608
  if (settled) return;
@@ -7562,7 +7616,7 @@ function promptLine(question) {
7562
7616
  rl.question(question, (answer) => {
7563
7617
  settled = true;
7564
7618
  rl.close();
7565
- resolve(answer);
7619
+ resolve2(answer);
7566
7620
  });
7567
7621
  });
7568
7622
  }
@@ -7576,6 +7630,15 @@ function uninterruptible(fn) {
7576
7630
  setImmediate(() => process.off("SIGINT", hold));
7577
7631
  }
7578
7632
  }
7633
+ function isExecutable(path) {
7634
+ try {
7635
+ if (!statSync(path).isFile()) return false;
7636
+ accessSync(path, constants.X_OK);
7637
+ return true;
7638
+ } catch {
7639
+ return false;
7640
+ }
7641
+ }
7579
7642
  function openerFor(platform, url) {
7580
7643
  if (platform === "darwin") return ["open", [url]];
7581
7644
  if (platform === "win32") return ["cmd", ["/c", "start", '""', url]];
@@ -7593,11 +7656,13 @@ function realIo() {
7593
7656
  prompt: promptLine,
7594
7657
  select: (question, choices, options) => selectMenu(process.stdin, process.stderr, question, choices, {
7595
7658
  ...options,
7596
- trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
7659
+ trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? ""),
7660
+ columns: () => process.stderr.columns
7597
7661
  }),
7598
7662
  multiSelect: (question, choices, options) => multiSelectMenu(process.stdin, process.stderr, question, choices, {
7599
7663
  ...options,
7600
- trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
7664
+ trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? ""),
7665
+ columns: () => process.stderr.columns
7601
7666
  }),
7602
7667
  listenForEnter: (onEnter, onCancel) => listenForEnter(process.stdin, onEnter, onCancel),
7603
7668
  uninterruptible,
@@ -7607,6 +7672,9 @@ function realIo() {
7607
7672
  return (await runProcess(cmd, args)).code === 0;
7608
7673
  },
7609
7674
  homedir: homedir(),
7675
+ scriptPath: process.argv[1],
7676
+ columns: () => process.stderr.columns,
7677
+ isExecutable,
7610
7678
  cwd: process.cwd(),
7611
7679
  platform: process.platform,
7612
7680
  hostname: hostname(),
@@ -7620,6 +7688,18 @@ function realIo() {
7620
7688
  // src/args.ts
7621
7689
  import { parseArgs } from "util";
7622
7690
 
7691
+ // src/command-name.ts
7692
+ var prefix = "amdahl";
7693
+ function startedByNpx(io) {
7694
+ return io.env.npm_command === "exec" || io.env.npm_lifecycle_event === "npx" || /[\\/]_npx[\\/]/.test(io.scriptPath ?? "");
7695
+ }
7696
+ function setCommandName(io) {
7697
+ prefix = startedByNpx(io) ? "npx amdahl" : "amdahl";
7698
+ }
7699
+ function amdahlCommand(rest = "") {
7700
+ return rest ? `${prefix} ${rest}` : prefix;
7701
+ }
7702
+
7623
7703
  // src/errors.ts
7624
7704
  var EXIT = {
7625
7705
  ok: 0,
@@ -7656,7 +7736,10 @@ var CliError = class extends Error {
7656
7736
  function usageError(message2, hint) {
7657
7737
  return new CliError("usage", message2, EXIT.usage, hint);
7658
7738
  }
7659
- var NOT_SIGNED_IN = "Not signed in. Run `amdahl login`, or set AMDAHL_KEY.";
7739
+ function notSignedIn() {
7740
+ return `Not signed in. Run \`${amdahlCommand("login")}\`, or set AMDAHL_KEY.`;
7741
+ }
7742
+ var NETWORK_HINT = "Check your internet connection, then run the same command again.";
7660
7743
  var CODE_EXITS = {
7661
7744
  oauth_required: EXIT.forbidden,
7662
7745
  cli_client_required: EXIT.forbidden,
@@ -7694,7 +7777,10 @@ var GLOBAL_OPTIONS = {
7694
7777
  version: { type: "boolean", short: "v" },
7695
7778
  verbose: { type: "boolean" }
7696
7779
  };
7780
+ var SETUP_OPTIONS = { "no-browser": { type: "boolean" } };
7697
7781
  var COMMAND_OPTIONS = {
7782
+ setup: SETUP_OPTIONS,
7783
+ help: {},
7698
7784
  login: {
7699
7785
  workspace: { type: "string" },
7700
7786
  "no-browser": { type: "boolean" },
@@ -7747,12 +7833,12 @@ function parseCommandLine(argv) {
7747
7833
  try {
7748
7834
  loose = parseArgs({ args: argv, options: allOptions(), allowPositionals: true, strict: false });
7749
7835
  } catch (err) {
7750
- throw usageError(err instanceof Error ? err.message : String(err));
7836
+ throw usageError(...parseErrorMessage(err));
7751
7837
  }
7752
7838
  const positionals = loose.positionals;
7753
7839
  const first = positionals[0];
7754
7840
  if (first === void 0) {
7755
- return { command: null, args: [], flags: strictParse(argv, GLOBAL_OPTIONS).values };
7841
+ return { command: null, args: [], flags: strictParse(argv, { ...GLOBAL_OPTIONS, ...SETUP_OPTIONS }).values };
7756
7842
  }
7757
7843
  let words = 1;
7758
7844
  let name = first;
@@ -7771,7 +7857,7 @@ function parseCommandLine(argv) {
7771
7857
  }
7772
7858
  name = ALIASES[name] ?? name;
7773
7859
  const spec = COMMAND_OPTIONS[name];
7774
- if (!spec) throw usageError(`Unknown command \`${positionals.slice(0, words).join(" ")}\`.`);
7860
+ if (!spec) throw usageError(`Unknown command \`${positionals.slice(0, words).join(" ")}\`.`, `Run \`${amdahlCommand("--help")}\` to see the commands.`);
7775
7861
  const strict = strictParse(argv, { ...GLOBAL_OPTIONS, ...spec });
7776
7862
  return { command: name, args: strict.positionals.slice(words), flags: strict.values };
7777
7863
  }
@@ -7783,39 +7869,62 @@ function strictParse(argv, options) {
7783
7869
  values: parsed.values
7784
7870
  };
7785
7871
  } catch (err) {
7786
- throw usageError(err instanceof Error ? err.message : String(err));
7872
+ throw usageError(...parseErrorMessage(err));
7787
7873
  }
7788
7874
  }
7875
+ function parseErrorMessage(err) {
7876
+ const message2 = err instanceof Error ? err.message : String(err);
7877
+ const code = err?.code;
7878
+ const help = `Run \`${amdahlCommand("--help")}\`.`;
7879
+ if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
7880
+ const option = /'(-[^']*)'/.exec(message2)?.[1] ?? message2;
7881
+ return [`Unknown option ${option}.`, help];
7882
+ }
7883
+ if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
7884
+ const option = /'(--?[\w-]+)/.exec(message2)?.[1];
7885
+ if (option && /argument missing/.test(message2)) return [`${option} needs a value.`, help];
7886
+ if (option) return [`${option} takes no value.`, help];
7887
+ }
7888
+ return [message2, help];
7889
+ }
7789
7890
  var USAGE = `Usage: amdahl <command> [options]
7790
7891
 
7791
7892
  What you can do:
7792
- amdahl Set up Claude Code, Cursor or Codex, in a terminal: it asks
7793
- which, signs you in, and runs install for each one
7794
- (--verbose shows the files and profile it used)
7893
+ amdahl Set up Claude Code, Cursor or Codex. In a
7894
+ terminal it asks which and signs you in.
7895
+ Run by an agent or with no terminal, it
7896
+ sets up that agent, or every client it
7897
+ finds, and asks nothing (also: amdahl setup)
7795
7898
  amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
7796
- amdahl status Check that this workspace is set up to optimize
7899
+ amdahl status Check that this workspace can optimize
7797
7900
  amdahl keys create --name ci Make an API key for a server or CI
7798
- amdahl install claude-code Connect your AI client (also codex, cursor)
7901
+ amdahl install claude-code Connect one AI client (also codex, cursor)
7799
7902
  amdahl skills add <publisher>/<skill>
7800
- Add a skill from https://amdahl.ai/skills to this project:
7801
- its SKILL.md, or its whole folder
7802
- For everything Amdahl can do: https://docs.amdahl.ai/skills/amdahl/SKILL.md
7903
+ Add a skill from https://amdahl.ai/skills
7904
+ to this project: its SKILL.md, or its folder
7905
+ Everything Amdahl can do: https://docs.amdahl.ai/skills/amdahl/SKILL.md
7803
7906
 
7804
7907
  Commands:
7805
- login [--workspace <slug>] [--no-browser] [--port <n>] Sign in (alias: auth login)
7806
- logout [--all] Sign out (alias: auth logout)
7807
- whoami [--show-token] Show who you are (alias: auth status)
7808
- auth token Print a fresh access token
7809
- status Check whether this workspace can optimize
7810
- optimize [<file>... | -] [--channel email|linkedin] [--evidence off|workspace]
7811
- [--on-unsupported flag|remove] [--tries] [--context <file.json>]
7812
- keys create --name <name> [--preset read-only|agent|internal|admin] [--expires 30d|90d|365d] [--no-browser]
7908
+ setup [--no-browser] The guided setup (bare amdahl)
7909
+ login [--workspace <slug>] [--no-browser] [--port <n>]
7910
+ Sign in (alias: auth login)
7911
+ logout [--all] Sign out (alias: auth logout)
7912
+ whoami [--show-token] Who you are (alias: auth status)
7913
+ auth token Print a fresh access token
7914
+ status Can this workspace optimize?
7915
+ optimize [<file>... | -] [--channel email|linkedin]
7916
+ [--evidence off|workspace] [--on-unsupported flag|remove]
7917
+ [--tries] [--context <file.json>]
7918
+ keys create --name <name> [--preset read-only|agent|internal|admin]
7919
+ [--expires 30d|90d|365d] [--no-browser]
7813
7920
  keys list
7814
7921
  keys revoke <id|prefix> [--yes] [--no-browser]
7815
7922
  workspace list | workspace use <profile>
7816
7923
  install claude-code|codex|cursor [--print] [--force]
7817
- skills add <publisher>/<skill> [-y] [-g] [--agents] [--force] [--accept-warning] [--site <origin>]
7818
- In a terminal it asks one question: this project (Enter) or every project.
7924
+ skills add <publisher>/<skill> [-y] [-g] [--agents] [--force]
7925
+ [--accept-warning] [--site <origin>]
7926
+ In a terminal it asks one question: this project (Enter) or
7927
+ every project.
7819
7928
  -y add without asking, to this project
7820
7929
  -g every project (~/.claude/skills), not just this one
7821
7930
  --agents also .agents/skills, for Codex, Cursor and other agents
@@ -7825,14 +7934,19 @@ Commands:
7825
7934
  after printing the warning. Without it, a terminal asks
7826
7935
  (Cancel or Install anyway); -y, --json or no terminal
7827
7936
  stops (exit 9)
7937
+ help Show this help
7828
7938
 
7829
7939
  Global options:
7830
- --profile <name> Use a saved profile (default: AMDAHL_PROFILE, then the default profile)
7831
- --api-url <url> API host (default: AMDAHL_API_URL, then the profile, then https://app.amdahl.ai)
7832
- --api-key <key> Use an API key for this command (prefer AMDAHL_KEY: argv is visible to other users)
7940
+ --profile <name> Use a saved profile (default: AMDAHL_PROFILE, then the
7941
+ default profile)
7942
+ --api-url <url> API host (default: AMDAHL_API_URL, then the profile,
7943
+ then https://app.amdahl.ai)
7944
+ --api-key <key> Use an API key for this command (prefer AMDAHL_KEY: argv
7945
+ is visible to other users)
7833
7946
  --json Data on stdout as JSON, messages on stderr
7834
7947
  --no-color No colour (NO_COLOR is honoured too)
7835
- --verbose Say more: files written, the profile, where the sign-in is stored
7948
+ --verbose Say more: files written, the profile, where the sign-in
7949
+ is stored
7836
7950
  -h, --help Show this help
7837
7951
  -v, --version Show the version
7838
7952
  `;
@@ -8031,7 +8145,7 @@ async function send(url, init) {
8031
8145
  return await fetch(url, init);
8032
8146
  } catch (err) {
8033
8147
  const message2 = err instanceof Error ? err.message : String(err);
8034
- throw new CliError("network_error", `Could not reach ${new URL(url).host}: ${message2}`, EXIT.network);
8148
+ throw new CliError("network_error", `Could not reach ${new URL(url).host}: ${message2}`, EXIT.network, NETWORK_HINT);
8035
8149
  }
8036
8150
  }
8037
8151
  async function discover(apiUrl) {
@@ -8142,7 +8256,7 @@ function signInAgainLine(reason, url) {
8142
8256
  function noBusinessMessage(consoleUrl, mayCreate, reason) {
8143
8257
  switch (reason) {
8144
8258
  case void 0:
8145
- return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`amdahl login\` again.` : `You're signed in, but you don't have access yet. Amdahl is in beta: join the waitlist at ${consoleUrl}/new, then run \`amdahl login\` again once you're in.`;
8259
+ return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`${amdahlCommand("login")}\` again.` : `You're signed in, but you don't have access yet. Amdahl is in beta: join the waitlist at ${consoleUrl}/new, then run \`${amdahlCommand("login")}\` again once you're in.`;
8146
8260
  case "domain_has_workspace":
8147
8261
  return `Your company already has an Amdahl workspace. Ask its admins to let you in at ${consoleUrl}, then try again.`;
8148
8262
  case "invite_only":
@@ -8178,8 +8292,8 @@ async function startLoopback(io, input) {
8178
8292
  let refused = null;
8179
8293
  let settle;
8180
8294
  let fail;
8181
- const result = new Promise((resolve, reject) => {
8182
- settle = resolve;
8295
+ const result = new Promise((resolve2, reject) => {
8296
+ settle = resolve2;
8183
8297
  fail = reject;
8184
8298
  });
8185
8299
  result.catch(() => {
@@ -8195,7 +8309,7 @@ async function startLoopback(io, input) {
8195
8309
  res.end(page(body));
8196
8310
  };
8197
8311
  if (url.searchParams.get("state") !== input.state) {
8198
- send2(400, "<p>This sign-in did not match the one your terminal started. Run <code>amdahl login</code> again.</p>");
8312
+ send2(400, `<p>This sign-in did not match the one your terminal started. Run <code>${amdahlCommand("login")}</code> again.</p>`);
8199
8313
  fail(new CliError("state_mismatch", "The sign-in response did not match this login (state mismatch).", EXIT.general));
8200
8314
  return;
8201
8315
  }
@@ -8225,7 +8339,7 @@ async function startLoopback(io, input) {
8225
8339
  settle({ kind: "error", error: "invalid_request", description: "The callback carried no code." });
8226
8340
  }
8227
8341
  });
8228
- await new Promise((resolve, reject) => {
8342
+ await new Promise((resolve2, reject) => {
8229
8343
  server.once(
8230
8344
  "error",
8231
8345
  (err) => reject(
@@ -8237,12 +8351,12 @@ async function startLoopback(io, input) {
8237
8351
  )
8238
8352
  )
8239
8353
  );
8240
- server.listen(input.port, "127.0.0.1", () => resolve());
8354
+ server.listen(input.port, "127.0.0.1", () => resolve2());
8241
8355
  });
8242
8356
  const port = server.address().port;
8243
8357
  const timer = setTimeout(() => {
8244
8358
  fail(
8245
- refused ? new CliError("no_business", noBusinessMessage(input.consoleUrl, false, refused), EXIT.noWorkspace) : new CliError("login_timeout", "Timed out waiting for the browser sign-in.", EXIT.general, "Run `amdahl login` again.")
8359
+ refused ? new CliError("no_business", noBusinessMessage(input.consoleUrl, false, refused), EXIT.noWorkspace) : new CliError("login_timeout", "Timed out waiting for the browser sign-in.", EXIT.general, `Run \`${amdahlCommand("login")}\` again.`)
8246
8360
  );
8247
8361
  }, input.timeoutMs);
8248
8362
  timer.unref?.();
@@ -8269,7 +8383,7 @@ function consoleUrlFor(io, apiUrl) {
8269
8383
  }
8270
8384
 
8271
8385
  // src/secrets.ts
8272
- import { chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, rmSync, statSync } from "fs";
8386
+ import { chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, rmSync, statSync as statSync2 } from "fs";
8273
8387
  import { join as join2 } from "path";
8274
8388
  var KEYCHAIN_SERVICE = "amdahl-cli";
8275
8389
  function keychainAccount(apiUrl, profile) {
@@ -8362,7 +8476,7 @@ function fileStore(io, note) {
8362
8476
  const checkMode = () => {
8363
8477
  if (checked || io.platform === "win32" || !existsSync2(path)) return;
8364
8478
  checked = true;
8365
- if ((statSync(path).mode & 63) === 0) return;
8479
+ if ((statSync2(path).mode & 63) === 0) return;
8366
8480
  try {
8367
8481
  chmodSync2(path, 384);
8368
8482
  } catch {
@@ -8371,7 +8485,7 @@ function fileStore(io, note) {
8371
8485
  return;
8372
8486
  }
8373
8487
  io.stderr(
8374
- `Warning: ${path} was readable by other users on this machine. It is private now; to be safe, run \`amdahl logout\` and sign in again.
8488
+ `Warning: ${path} was readable by other users on this machine. It is private now; to be safe, run \`${amdahlCommand("logout")}\` and sign in again.
8375
8489
  `
8376
8490
  );
8377
8491
  };
@@ -8503,7 +8617,7 @@ async function requireOAuthCredential(ctx) {
8503
8617
  "oauth_required",
8504
8618
  `This command needs your own sign-in, but a credential from ${winner === "flag" ? "--api-key" : winner} takes precedence.`,
8505
8619
  EXIT.forbidden,
8506
- "Unset it and run `amdahl login`."
8620
+ `Unset it and run \`${amdahlCommand("login")}\`.`
8507
8621
  );
8508
8622
  }
8509
8623
  return profileCredential(ctx);
@@ -8511,20 +8625,20 @@ async function requireOAuthCredential(ctx) {
8511
8625
  async function profileCredential(ctx) {
8512
8626
  const { profile, profileName } = ctx;
8513
8627
  if (!profile || !profileName) {
8514
- throw new CliError("not_signed_in", NOT_SIGNED_IN, EXIT.unauthenticated);
8628
+ throw new CliError("not_signed_in", notSignedIn(), EXIT.unauthenticated);
8515
8629
  }
8516
8630
  if (normaliseApiUrl(profile.api_url) !== ctx.apiUrl) {
8517
8631
  throw new CliError(
8518
8632
  "profile_host_mismatch",
8519
8633
  `Profile "${profileName}" is signed in to ${profile.api_url}, not ${ctx.apiUrl}.`,
8520
8634
  EXIT.usage,
8521
- "Drop --api-url / AMDAHL_API_URL, or run `amdahl login` against that host."
8635
+ `Drop --api-url / AMDAHL_API_URL, or run \`${amdahlCommand("login")}\` against that host.`
8522
8636
  );
8523
8637
  }
8524
8638
  const account = keychainAccount(profile.api_url, profileName);
8525
8639
  const store = ctx.secrets();
8526
8640
  const secrets = await store.get(account);
8527
- if (!secrets) throw new CliError("not_signed_in", NOT_SIGNED_IN, EXIT.unauthenticated);
8641
+ if (!secrets) throw new CliError("not_signed_in", notSignedIn(), EXIT.unauthenticated);
8528
8642
  const left = Date.parse(secrets.access_expires_at) - ctx.io.now();
8529
8643
  if (Number.isFinite(left) && left >= 6e4) {
8530
8644
  return { source: "profile", token: secrets.access_token, profile: profileName };
@@ -8536,7 +8650,7 @@ async function profileCredential(ctx) {
8536
8650
  "invalid_grant",
8537
8651
  "Your sign-in has expired.",
8538
8652
  EXIT.unauthenticated,
8539
- "Run `amdahl login` again."
8653
+ `Run \`${amdahlCommand("login")}\` again.`
8540
8654
  );
8541
8655
  }
8542
8656
  await store.set(account, refreshed);
@@ -8557,16 +8671,16 @@ function errorDetails(body) {
8557
8671
  }
8558
8672
  function toCliError(err) {
8559
8673
  if (err instanceof CliError) return err;
8560
- if (err instanceof PromptCancelled) return new CliError("cancelled", "Stopped.", EXIT.cancelled);
8674
+ if (err instanceof PromptCancelled) return new CliError("cancelled", "Stopped. Nothing changed.", EXIT.cancelled);
8561
8675
  if (err instanceof AmdahlApiError) {
8562
8676
  const exit = exitForHttp(err.statusCode, err.errorCode);
8563
8677
  const details = errorDetails(err.responseBody);
8564
8678
  const retry = details.retry_after_seconds;
8565
- const hint = exit === EXIT.unauthenticated ? "Run `amdahl login`, or check AMDAHL_KEY." : typeof retry === "number" ? `Try again in ${retry} seconds.` : void 0;
8679
+ const hint = exit === EXIT.unauthenticated ? `Run \`${amdahlCommand("login")}\`, or check AMDAHL_KEY.` : typeof retry === "number" ? `Try again in ${retry} seconds.` : void 0;
8566
8680
  return new CliError(err.errorCode ?? `http_${err.statusCode}`, err.message, exit, hint);
8567
8681
  }
8568
8682
  const message2 = err instanceof Error ? err.message : String(err);
8569
- return new CliError("network_error", `Could not reach the Amdahl API: ${message2}`, EXIT.network);
8683
+ return new CliError("network_error", `Could not reach the Amdahl API: ${message2}`, EXIT.network, NETWORK_HINT);
8570
8684
  }
8571
8685
  async function call(fn) {
8572
8686
  try {
@@ -8668,13 +8782,15 @@ async function browserSignIn(ctx) {
8668
8782
  `);
8669
8783
  };
8670
8784
  let cancelled;
8785
+ let fallbackLine = null;
8671
8786
  if (flags["no-browser"]) {
8672
8787
  showLink("Open this URL in a browser to sign in:");
8673
8788
  } else {
8674
8789
  out.info("Opening your browser\u2026");
8675
8790
  if (!ctx.nonInteractive && io.stderrIsTTY) {
8676
8791
  const fallback = supportsHyperlinks(io.env, out.color) ? hyperlink("Open the sign-in page", url) : "Press Enter for the link.";
8677
- out.info(`Browser didn't open? ${fallback}`);
8792
+ fallbackLine = `Browser didn't open? ${fallback}`;
8793
+ out.info(fallbackLine);
8678
8794
  cancelled = new Promise((_, reject) => {
8679
8795
  stopKeys = io.listenForEnter(
8680
8796
  () => showLink("Open this link to sign in:"),
@@ -8688,6 +8804,7 @@ async function browserSignIn(ctx) {
8688
8804
  }
8689
8805
  const callback = await (cancelled ? Promise.race([loopback.result, cancelled]) : loopback.result);
8690
8806
  stopKeys();
8807
+ if (fallbackLine && !linkShown && callback.kind === "code") io.stderr(eraseUp(screenLines(fallbackLine, io.columns())));
8691
8808
  linkShown = true;
8692
8809
  if (callback.kind === "no_business") {
8693
8810
  throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate, callback.reason), EXIT.noWorkspace);
@@ -8827,8 +8944,8 @@ async function authToken(ctx) {
8827
8944
  }
8828
8945
 
8829
8946
  // src/commands/install.ts
8830
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
8831
- import { dirname, join as join4 } from "path";
8947
+ import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readlinkSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
8948
+ import { basename, dirname, join as join4, resolve } from "path";
8832
8949
 
8833
8950
  // src/skill.ts
8834
8951
  import { createHash as createHash2 } from "crypto";
@@ -8882,23 +8999,136 @@ async function onPath(ctx, command) {
8882
8999
  return res.code === 0;
8883
9000
  }
8884
9001
  var CLIENT_SIGNS = {
8885
- "claude-code": { command: "claude", dir: ".claude" },
8886
- codex: { command: "codex", dir: ".codex" },
8887
- cursor: { command: "cursor", dir: ".cursor" }
9002
+ "claude-code": { command: "claude", paths: [".claude", ".claude.json"] },
9003
+ codex: { command: "codex", paths: [".codex"] },
9004
+ cursor: { command: "cursor", paths: [".cursor"] }
8888
9005
  };
8889
9006
  async function detectClients(ctx) {
8890
9007
  const found = await Promise.all(
8891
9008
  CLIENTS.map(async (client) => {
8892
- const { command, dir } = CLIENT_SIGNS[client];
8893
- return existsSync3(join4(ctx.io.homedir, dir)) || await onPath(ctx, command);
9009
+ const { command, paths } = CLIENT_SIGNS[client];
9010
+ return paths.some((p) => existsSync3(join4(ctx.io.homedir, p))) || await onPath(ctx, command);
8894
9011
  })
8895
9012
  );
8896
9013
  return Object.fromEntries(CLIENTS.map((client, i) => [client, found[i]]));
8897
9014
  }
8898
- async function claudeHasAmdahl(ctx, mcpUrl) {
8899
- const res = await ctx.io.run("claude", ["mcp", "get", "amdahl"]);
9015
+ function claudeConfigPath(ctx) {
9016
+ return join4(ctx.io.env.CLAUDE_CONFIG_DIR || ctx.io.homedir, ".claude.json");
9017
+ }
9018
+ var routes = /* @__PURE__ */ new WeakMap();
9019
+ function claudeRoute(ctx) {
9020
+ let route = routes.get(ctx);
9021
+ if (!route) {
9022
+ route = findClaude(ctx).then((command) => command ? { kind: "command", command } : { kind: "file", path: claudeConfigPath(ctx) });
9023
+ routes.set(ctx, route);
9024
+ }
9025
+ return route;
9026
+ }
9027
+ async function findClaude(ctx) {
9028
+ const { io } = ctx;
9029
+ if (await onPath(ctx, "claude")) return "claude";
9030
+ const win = io.platform === "win32";
9031
+ const local = [join4(io.homedir, ".local", "bin", win ? "claude.exe" : "claude"), join4(io.homedir, ".claude", "local", win ? "claude.exe" : "claude")];
9032
+ const system = win ? [] : ["/opt/homebrew/bin/claude", "/usr/local/bin/claude"];
9033
+ const found = [...local, ...system].find((path) => io.isExecutable(path));
9034
+ if (found) return found;
9035
+ if (win) return null;
9036
+ const prefix2 = io.env.npm_config_global_prefix || await npmGlobalPrefix(ctx);
9037
+ const npmClaude = prefix2 ? join4(prefix2, "bin", "claude") : null;
9038
+ return npmClaude && io.isExecutable(npmClaude) ? npmClaude : null;
9039
+ }
9040
+ async function npmGlobalPrefix(ctx) {
9041
+ const res = await ctx.io.run("npm", ["prefix", "-g"]);
9042
+ const prefix2 = res.code === 0 ? res.stdout.trim() : "";
9043
+ return prefix2 || null;
9044
+ }
9045
+ async function claudeHasAmdahl(ctx, command, mcpUrl) {
9046
+ const res = await ctx.io.run(command, ["mcp", "get", "amdahl"]);
8900
9047
  return res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`);
8901
9048
  }
9049
+ var CONFIG_HINT = "Fix the file, or move it away, then run the same command again.";
9050
+ function readJsonObject(path) {
9051
+ if (!existsSync3(path)) return { config: {}, existed: false };
9052
+ const raw = readFileSync3(path, "utf8");
9053
+ if (!raw.trim()) return { config: {}, existed: true };
9054
+ let config;
9055
+ try {
9056
+ config = JSON.parse(raw);
9057
+ } catch {
9058
+ throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general, CONFIG_HINT);
9059
+ }
9060
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
9061
+ throw new CliError("invalid_config", `${path} does not hold a JSON object; not changing it.`, EXIT.general, CONFIG_HINT);
9062
+ }
9063
+ return { config, existed: true };
9064
+ }
9065
+ function readMcpConfig(path) {
9066
+ const { config, existed } = readJsonObject(path);
9067
+ const servers = config.mcpServers;
9068
+ if (servers === void 0) return { config, servers: {}, existed };
9069
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
9070
+ throw new CliError("invalid_config", `The "mcpServers" in ${path} is not a JSON object; not changing it.`, EXIT.general, CONFIG_HINT);
9071
+ }
9072
+ return { config, servers, existed };
9073
+ }
9074
+ function claudeFileHasAmdahl(path, mcpUrl) {
9075
+ try {
9076
+ const servers = readMcpConfig(path).servers;
9077
+ return servers.amdahl?.url === mcpUrl;
9078
+ } catch {
9079
+ return false;
9080
+ }
9081
+ }
9082
+ function writeTarget(path) {
9083
+ let target = path;
9084
+ for (let hops = 0; hops < 40; hops++) {
9085
+ let link;
9086
+ try {
9087
+ link = lstatSync(target).isSymbolicLink();
9088
+ } catch {
9089
+ return target;
9090
+ }
9091
+ if (!link) return existsSync3(target) ? realpathSync(target) : target;
9092
+ target = resolve(dirname(target), readlinkSync(target));
9093
+ }
9094
+ throw new CliError("invalid_config", `${path} is a chain of more than 40 symbolic links; not changing it.`, EXIT.general, CONFIG_HINT);
9095
+ }
9096
+ function writeFileAtomic(path, text2) {
9097
+ const target = writeTarget(path);
9098
+ const mode = existsSync3(target) ? statSync3(target).mode & 511 : 384;
9099
+ mkdirSync2(dirname(target), { recursive: true });
9100
+ const tmp = join4(dirname(target), `.${basename(target)}.amdahl-${process.pid}.tmp`);
9101
+ try {
9102
+ writeFileSync2(tmp, text2, { mode });
9103
+ chmodSync3(tmp, mode);
9104
+ renameSync2(tmp, target);
9105
+ } catch (err) {
9106
+ rmSync2(tmp, { force: true });
9107
+ throw err;
9108
+ }
9109
+ }
9110
+ function writeClaudeConfig(ctx, path, mcpUrl, command) {
9111
+ return updateConfig(path, () => {
9112
+ const { config, servers, existed } = readMcpConfig(path);
9113
+ const data = { ok: true, client: "claude-code", action: "unchanged", path, command };
9114
+ if (servers.amdahl?.url === mcpUrl) {
9115
+ return { data, human: `${path} already points Claude Code at ${mcpUrl}; nothing changed.`, notes: [] };
9116
+ }
9117
+ config.mcpServers = { ...servers, amdahl: { type: "http", url: mcpUrl } };
9118
+ const backup = `${writeTarget(path)}.amdahl-backup`;
9119
+ ctx.io.uninterruptible(() => {
9120
+ if (existed) copyFileSync(writeTarget(path), backup);
9121
+ writeFileAtomic(path, `${JSON.stringify(config, null, 2)}
9122
+ `);
9123
+ });
9124
+ const saved = existed ? ` The file as it was is saved at ${backup}.` : "";
9125
+ return {
9126
+ data: { ...data, action: "wrote", ...existed ? { backup } : {} },
9127
+ human: `Added Amdahl to Claude Code for all your projects, in ${path} (\`claude\` was not found).${saved} Run /mcp in Claude Code to sign in. If amdahl is not listed there, restart Claude Code first.`,
9128
+ notes: []
9129
+ };
9130
+ });
9131
+ }
8902
9132
  function updateConfig(path, fn) {
8903
9133
  try {
8904
9134
  return fn();
@@ -8913,7 +9143,11 @@ async function isInstalled(ctx, client) {
8913
9143
  const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
8914
9144
  if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
8915
9145
  const mcpUrl = `${ctx.apiUrl}/mcp`;
8916
- if (client === "claude-code") return await onPath(ctx, "claude") && await claudeHasAmdahl(ctx, mcpUrl);
9146
+ if (client === "claude-code") {
9147
+ if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl)) return true;
9148
+ const route = await claudeRoute(ctx);
9149
+ return route.kind === "command" && await claudeHasAmdahl(ctx, route.command, mcpUrl);
9150
+ }
8917
9151
  if (client === "codex") {
8918
9152
  const path = codexConfigPath(ctx.io.homedir);
8919
9153
  return existsSync3(path) && codexHasAmdahl(readFileSync3(path, "utf8"));
@@ -8971,14 +9205,13 @@ async function installMcp(ctx, client, print) {
8971
9205
  if (client === "claude-code") {
8972
9206
  const args = ["mcp", "add", "--scope", "user", "--transport", "http", "amdahl", mcpUrl];
8973
9207
  const command = `claude ${args.join(" ")}`;
8974
- if (print || !await onPath(ctx, "claude")) {
8975
- const notes = print ? [] : ["`claude` is not on your PATH. Run the command above once it is installed."];
8976
- return { data: { ok: true, client, action: "printed", command }, human: command, notes };
8977
- }
8978
- if (await claudeHasAmdahl(ctx, mcpUrl)) {
9208
+ if (print) return { data: { ok: true, client, action: "printed", command }, human: command, notes: [] };
9209
+ const route = await claudeRoute(ctx);
9210
+ if (route.kind === "file") return writeClaudeConfig(ctx, route.path, mcpUrl, command);
9211
+ if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl) || await claudeHasAmdahl(ctx, route.command, mcpUrl)) {
8979
9212
  return { data: { ok: true, client, action: "unchanged", command }, human: `Claude Code already has Amdahl at ${mcpUrl}; nothing changed.`, notes: [] };
8980
9213
  }
8981
- const res = await io.run("claude", args);
9214
+ const res = await io.run(route.command, args);
8982
9215
  if (res.code !== 0) {
8983
9216
  throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
8984
9217
  }
@@ -8996,9 +9229,8 @@ ${block}`, notes: [] };
8996
9229
  if (codexHasAmdahl(current)) {
8997
9230
  return { data: { ok: true, client, action: "unchanged", path: path2 }, human: `${path2} already has [mcp_servers.amdahl]; nothing changed.`, notes: [] };
8998
9231
  }
8999
- mkdirSync2(dirname(path2), { recursive: true });
9000
9232
  const sep2 = current.length === 0 ? "" : current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
9001
- writeFileSync2(path2, `${current}${sep2}${block}`);
9233
+ writeFileAtomic(path2, `${current}${sep2}${block}`);
9002
9234
  return { data: { ok: true, client, action: "wrote", path: path2 }, human: `Added Amdahl to ${path2}. Run \`codex mcp login amdahl\` to sign in.`, notes: [] };
9003
9235
  });
9004
9236
  }
@@ -9009,28 +9241,33 @@ ${block}`, notes: [] };
9009
9241
  ${JSON.stringify({ mcpServers: { amdahl: entry } }, null, 2)}`, notes: [] };
9010
9242
  }
9011
9243
  return updateConfig(path, () => {
9012
- let config = {};
9013
- if (existsSync3(path)) {
9014
- const raw = readFileSync3(path, "utf8");
9015
- if (raw.trim()) {
9016
- try {
9017
- config = JSON.parse(raw);
9018
- } catch {
9019
- throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general);
9020
- }
9021
- }
9022
- }
9023
- const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
9244
+ const { config, servers } = readMcpConfig(path);
9024
9245
  if (JSON.stringify(servers.amdahl) === JSON.stringify(entry)) {
9025
9246
  return { data: { ok: true, client, action: "unchanged", path }, human: `${path} already points at ${mcpUrl}; nothing changed.`, notes: [] };
9026
9247
  }
9027
9248
  config.mcpServers = { ...servers, amdahl: entry };
9028
- mkdirSync2(dirname(path), { recursive: true });
9029
- writeFileSync2(path, `${JSON.stringify(config, null, 2)}
9249
+ writeFileAtomic(path, `${JSON.stringify(config, null, 2)}
9030
9250
  `);
9031
9251
  return { data: { ok: true, client, action: "wrote", path }, human: `Added Amdahl to ${path}. Cursor asks you to sign in on first use.`, notes: [] };
9032
9252
  });
9033
9253
  }
9254
+ async function preflight(ctx, client) {
9255
+ let path = null;
9256
+ if (client === "cursor") path = cursorConfigPath(ctx.io.homedir);
9257
+ if (client === "claude-code") {
9258
+ const route = await claudeRoute(ctx);
9259
+ if (route.kind === "file") path = route.path;
9260
+ }
9261
+ if (!path) return null;
9262
+ try {
9263
+ readMcpConfig(path);
9264
+ return null;
9265
+ } catch (err) {
9266
+ if (err instanceof CliError) return err;
9267
+ const reason = err instanceof Error ? err.message : String(err);
9268
+ return new CliError("write_failed", `Could not read ${path} (${reason}).`, EXIT.general);
9269
+ }
9270
+ }
9034
9271
 
9035
9272
  // src/keygen.ts
9036
9273
  import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
@@ -9098,7 +9335,7 @@ async function waitForDecision(ctx, client, req) {
9098
9335
  }
9099
9336
  async function keysCreate(ctx) {
9100
9337
  const name = str(ctx.flags.name)?.trim();
9101
- if (!name) throw usageError("--name is required.", 'Example: amdahl keys create --name "CI pipeline"');
9338
+ if (!name) throw usageError("--name is required.", `Example: ${amdahlCommand('keys create --name "CI pipeline"')}`);
9102
9339
  if (name.length > 80) throw usageError("--name must be at most 80 characters.");
9103
9340
  const preset = str(ctx.flags.preset) ?? "read-only";
9104
9341
  const bundle = PRESET_BUNDLES[preset];
@@ -9168,7 +9405,7 @@ function findKey(keys, ref) {
9168
9405
  const matches = keys.filter((k) => ref.length >= 8 && k.key_prefix.startsWith(ref));
9169
9406
  if (matches.length === 1 && matches[0]) return matches[0];
9170
9407
  if (matches.length > 1) throw usageError(`"${ref}" matches ${matches.length} keys. Use the key id.`);
9171
- throw new CliError("key_not_found", `No active key matches "${ref}".`, EXIT.general, "Run `amdahl keys list`.");
9408
+ throw new CliError("key_not_found", `No active key matches "${ref}".`, EXIT.general, `Run \`${amdahlCommand("keys list")}\`.`);
9172
9409
  }
9173
9410
  async function keysRevoke(ctx, args) {
9174
9411
  const ref = args[0];
@@ -9426,7 +9663,7 @@ import { dirname as dirname3, join as join6, relative, sep } from "path";
9426
9663
 
9427
9664
  // src/commands/skills-install.ts
9428
9665
  import { randomBytes as randomBytes3 } from "crypto";
9429
- import { chmodSync as chmodSync3, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
9666
+ import { chmodSync as chmodSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
9430
9667
  import { dirname as dirname2, join as join5 } from "path";
9431
9668
  function skillDirs(targets, name) {
9432
9669
  const dirs = [];
@@ -9453,7 +9690,7 @@ function writeFailure(path, err, written = []) {
9453
9690
  function sameFolder(dir, archive) {
9454
9691
  let top;
9455
9692
  try {
9456
- top = lstatSync(dir);
9693
+ top = lstatSync2(dir);
9457
9694
  } catch (err) {
9458
9695
  if (err.code === "ENOENT") return null;
9459
9696
  throw writeFailure(dir, err);
@@ -9472,7 +9709,7 @@ function sameFolder(dir, archive) {
9472
9709
  const file = want.get(path);
9473
9710
  if (!file) return false;
9474
9711
  const full = join5(dir, path);
9475
- if (process.platform !== "win32" && (lstatSync(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9712
+ if (process.platform !== "win32" && (lstatSync2(full).mode & 73 ? 493 : 420) !== file.mode) return false;
9476
9713
  if (!readFileSync5(full).equals(file.bytes)) return false;
9477
9714
  } else {
9478
9715
  return false;
@@ -9491,7 +9728,7 @@ function refuseLinks(dir, file, written = []) {
9491
9728
  for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
9492
9729
  let stat;
9493
9730
  try {
9494
- stat = lstatSync(part);
9731
+ stat = lstatSync2(part);
9495
9732
  } catch (err) {
9496
9733
  if (err.code === "ENOENT") return;
9497
9734
  throw writeFailure(path, err, written);
@@ -9518,7 +9755,7 @@ function planInstall(dirs, payload) {
9518
9755
  refuseLinks(dir, path);
9519
9756
  let current;
9520
9757
  try {
9521
- current = lstatSync(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
9758
+ current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
9522
9759
  } catch (err) {
9523
9760
  throw writeFailure(path, err);
9524
9761
  }
@@ -9543,7 +9780,7 @@ function stage(target, name, archive, staged) {
9543
9780
  s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
9544
9781
  refuseLinks(target);
9545
9782
  s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
9546
- chmodSync3(s.temp, 493);
9783
+ chmodSync4(s.temp, 493);
9547
9784
  for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
9548
9785
  for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
9549
9786
  }
@@ -9551,15 +9788,15 @@ function rollBack(staged) {
9551
9788
  for (const s of [...staged].reverse()) {
9552
9789
  try {
9553
9790
  if (s.swapped) {
9554
- rmSync2(s.target, { recursive: true, force: true });
9555
- if (s.backup) renameSync2(s.backup, s.target);
9791
+ rmSync3(s.target, { recursive: true, force: true });
9792
+ if (s.backup) renameSync3(s.backup, s.target);
9556
9793
  } else if (s.backup) {
9557
- renameSync2(s.backup, s.target);
9794
+ renameSync3(s.backup, s.target);
9558
9795
  }
9559
9796
  } catch {
9560
9797
  }
9561
- if (s.temp) rmSync2(s.temp, { recursive: true, force: true });
9562
- if (s.createdParent) rmSync2(s.createdParent, { recursive: true, force: true });
9798
+ if (s.temp) rmSync3(s.temp, { recursive: true, force: true });
9799
+ if (s.createdParent) rmSync3(s.createdParent, { recursive: true, force: true });
9563
9800
  }
9564
9801
  }
9565
9802
  function writeFolders(plan, name, archive) {
@@ -9575,9 +9812,9 @@ function writeFolders(plan, name, archive) {
9575
9812
  current = s.target;
9576
9813
  if (plan.find((p) => p.path === s.target)?.action === "replaced") {
9577
9814
  s.backup = `${s.temp}.old`;
9578
- renameSync2(s.target, s.backup);
9815
+ renameSync3(s.target, s.backup);
9579
9816
  }
9580
- renameSync2(s.temp, s.target);
9817
+ renameSync3(s.temp, s.target);
9581
9818
  s.swapped = true;
9582
9819
  }
9583
9820
  } catch (err) {
@@ -9585,7 +9822,7 @@ function writeFolders(plan, name, archive) {
9585
9822
  if (err instanceof CliError) throw err;
9586
9823
  throw writeFailure(current, err);
9587
9824
  }
9588
- for (const s of staged) if (s.backup) rmSync2(s.backup, { recursive: true, force: true });
9825
+ for (const s of staged) if (s.backup) rmSync3(s.backup, { recursive: true, force: true });
9589
9826
  }
9590
9827
  function writeFiles(plan, body) {
9591
9828
  const written = [];
@@ -9597,9 +9834,9 @@ function writeFiles(plan, body) {
9597
9834
  mkdirSync3(dirname2(p.path), { recursive: true });
9598
9835
  refuseLinks(dirname2(p.path), p.path, written);
9599
9836
  writeFileSync3(temp, body, { flag: "wx", mode: 420 });
9600
- renameSync2(temp, p.path);
9837
+ renameSync3(temp, p.path);
9601
9838
  } catch (err) {
9602
- rmSync2(temp, { force: true });
9839
+ rmSync3(temp, { force: true });
9603
9840
  if (err instanceof CliError) throw err;
9604
9841
  throw writeFailure(p.path, err, written);
9605
9842
  }
@@ -9704,16 +9941,51 @@ var SHOWN = [
9704
9941
  { client: "cursor", name: "Cursor" },
9705
9942
  { client: "codex", name: "Codex" }
9706
9943
  ];
9707
- function tryItLine(name, connect = false) {
9708
- const ask2 = 'say "make this email sound less like AI"';
9709
- if (connect && name === "Claude Code") return `Try it: in Claude Code, run /mcp once to connect, then ${ask2}`;
9710
- if (name === "Codex") return `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}`;
9711
- return `Try it: open ${name} and ${ask2}`;
9944
+ var QUESTION = "Where should Amdahl work?";
9945
+ var FOUND_TAG = "found on your machine";
9946
+ var NONE_FOUND = "We didn't find any of these on this computer. Tick the ones you use.";
9947
+ var MENU_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter to install";
9948
+ var ASK = 'say "make this email sound less like AI"';
9949
+ function finishLines(names, inClaudeCode = false) {
9950
+ const [only] = names;
9951
+ if (only === void 0) return [];
9952
+ if (names.length === 1) {
9953
+ const step2 = {
9954
+ "Claude Code": inClaudeCode ? "Before that, restart Claude Code and run /mcp once to connect Amdahl." : "Before that, run /mcp once in Claude Code to connect Amdahl.",
9955
+ Cursor: "Before that, sign in when Cursor asks, the first time you use Amdahl.",
9956
+ Codex: "Before that, run `codex mcp login amdahl` once to connect Amdahl."
9957
+ };
9958
+ return [`Try it: ${ASK} in ${only}.`, step2[only] ?? ""];
9959
+ }
9960
+ const step = {
9961
+ "Claude Code": inClaudeCode ? "restart it, then run /mcp" : "run /mcp",
9962
+ Cursor: "sign in when it asks, on first use",
9963
+ Codex: "run `codex mcp login amdahl`"
9964
+ };
9965
+ return [`Try it: ${ASK} in any of them.`, "Before that, connect each one once:", ...names.map((n) => ` ${n}: ${step[n] ?? ""}`)];
9966
+ }
9967
+ var isSet = (value) => Boolean(value) && value !== "false" && value !== "0";
9968
+ function ciVariable(env) {
9969
+ const named = ["CI", "JENKINS_URL", "TF_BUILD", "CODEBUILD_BUILD_ID", "TEAMCITY_VERSION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"].find((name) => isSet(env[name]));
9970
+ if (named) return named;
9971
+ return isSet(env.BUILD_NUMBER) && isSet(env.JENKINS_HOME) ? "BUILD_NUMBER with JENKINS_HOME" : null;
9712
9972
  }
9713
- function shouldSetUp(io) {
9973
+ function setupMode(io) {
9714
9974
  const { env } = io;
9715
- const ci = Boolean(env.CI) && env.CI !== "false" && env.CI !== "0";
9716
- return io.stdinIsTTY && io.stdoutIsTTY && io.stderrIsTTY && env.AMDAHL_NO_PROMPT !== "1" && !ci && !env.CLAUDECODE && !env.CURSOR_AGENT;
9975
+ const ci = ciVariable(env);
9976
+ if (ci) return { kind: "usage", reason: `${ci} is set` };
9977
+ if (env.AMDAHL_NO_PROMPT === "1") return { kind: "usage", reason: "AMDAHL_NO_PROMPT=1 is set" };
9978
+ if (env.CLAUDECODE || env.CURSOR_AGENT) return { kind: "agent" };
9979
+ return io.stdinIsTTY && io.stdoutIsTTY && io.stderrIsTTY ? { kind: "menu" } : { kind: "agent" };
9980
+ }
9981
+ function installLine() {
9982
+ return `To set up a client, run \`${amdahlCommand("install claude-code")}\` (or cursor, codex).`;
9983
+ }
9984
+ function noSetupLine(reason) {
9985
+ return `Setup did not run: ${reason}.
9986
+ ${installLine()}
9987
+
9988
+ `;
9717
9989
  }
9718
9990
  async function signIn(ctx) {
9719
9991
  let credential;
@@ -9731,7 +10003,32 @@ async function signIn(ctx) {
9731
10003
  return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
9732
10004
  }
9733
10005
  }
9734
- async function setup(ctx) {
10006
+ function reportError(io, err) {
10007
+ io.stderr(`Error: ${err.message}
10008
+ ${err.hint ? `${err.hint}
10009
+ ` : ""}`);
10010
+ }
10011
+ async function installEach(ctx, clients, detail) {
10012
+ let exit = EXIT.ok;
10013
+ const ready = [];
10014
+ for (const { client, name } of clients) {
10015
+ let steps;
10016
+ try {
10017
+ steps = await installClient(ctx, client, false);
10018
+ } catch (raw) {
10019
+ const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
10020
+ reportError(ctx.io, err);
10021
+ if (exit === EXIT.ok) exit = err.exit;
10022
+ continue;
10023
+ }
10024
+ ctx.io.stdout(`${steps.mcp.data.action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`}
10025
+ `);
10026
+ ready.push(name);
10027
+ detail([steps.mcp.human, ...steps.mcp.notes, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
10028
+ }
10029
+ return { ready, exit };
10030
+ }
10031
+ async function setup(ctx, mode) {
9735
10032
  const { io, out } = ctx;
9736
10033
  const verbose = ctx.flags.verbose === true;
9737
10034
  const say = (line) => io.stdout(`${line}
@@ -9739,9 +10036,23 @@ async function setup(ctx) {
9739
10036
  const detail = (text2) => {
9740
10037
  if (verbose && text2) say(text2.replace(/^/gm, " "));
9741
10038
  };
10039
+ const inClaudeCode = Boolean(io.env.CLAUDECODE);
9742
10040
  say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
9743
10041
  const found = await detectClients(ctx);
9744
10042
  const detected = SHOWN.filter((c) => found[c.client]);
10043
+ if (mode === "agent") {
10044
+ const running = SHOWN.filter((c) => c.client === "claude-code" && io.env.CLAUDECODE || c.client === "cursor" && io.env.CURSOR_AGENT);
10045
+ const targets = running.length > 0 ? running : detected;
10046
+ if (targets.length === 0) {
10047
+ io.stderr(`Didn't find Claude Code, Cursor or Codex on this computer, so nothing changed.
10048
+ ${installLine()}
10049
+ `);
10050
+ return EXIT.general;
10051
+ }
10052
+ const { ready, exit: exit2 } = await installEach(ctx, targets, detail);
10053
+ for (const line of finishLines(ready, inClaudeCode)) say(line);
10054
+ return exit2;
10055
+ }
9745
10056
  let who;
9746
10057
  if (detected.length > 0) {
9747
10058
  const installed = await Promise.all(detected.map((c) => isInstalled(ctx, c.client)));
@@ -9755,20 +10066,38 @@ async function setup(ctx) {
9755
10066
  }
9756
10067
  if (unreachable || who?.kind === "in") {
9757
10068
  say(`You're set up in ${joinNames(detected.map((c) => c.name))}.${unreachable ? " (Could not reach Amdahl to check your sign-in.)" : ""}`);
9758
- say(tryItLine(detected[0]?.name ?? ""));
10069
+ for (const line of finishLines(detected.map((c) => c.name), inClaudeCode)) say(line);
9759
10070
  return EXIT.ok;
9760
10071
  }
9761
10072
  }
9762
10073
  }
9763
10074
  const picks = await io.multiSelect(
9764
- "Set up in:",
10075
+ QUESTION,
9765
10076
  SHOWN.map((c) => c.name),
9766
- { ticked: SHOWN.map((c) => found[c.client]), color: out.color }
10077
+ {
10078
+ ticked: SHOWN.map((c) => found[c.client]),
10079
+ tags: SHOWN.map((c) => found[c.client] ? FOUND_TAG : void 0),
10080
+ note: detected.length === 0 ? NONE_FOUND : void 0,
10081
+ hint: MENU_HINT,
10082
+ color: out.color
10083
+ }
9767
10084
  );
9768
10085
  if (picks.length === 0) {
9769
10086
  say("Nothing picked, so nothing changed.");
9770
10087
  return EXIT.ok;
9771
10088
  }
10089
+ let exit = EXIT.ok;
10090
+ const chosen = [];
10091
+ for (const pick of picks.flatMap((i) => SHOWN[i] ?? [])) {
10092
+ const err = await preflight(ctx, pick.client);
10093
+ if (!err) {
10094
+ chosen.push(pick);
10095
+ continue;
10096
+ }
10097
+ reportError(io, err);
10098
+ if (exit === EXIT.ok) exit = err.exit;
10099
+ }
10100
+ if (chosen.length === 0) return exit;
9772
10101
  who ??= await signIn(ctx);
9773
10102
  if (who.kind === "in") {
9774
10103
  say(`\u2713 Signed in \xB7 ${who.workspace}`);
@@ -9779,35 +10108,11 @@ async function setup(ctx) {
9779
10108
  detail(signedInLine(ctx, signedIn));
9780
10109
  } else {
9781
10110
  const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
9782
- out.info(`Amdahl rejected the key in ${name}. Skipping the browser sign-in, because that key would still be used instead. Fix it, or unset it and run \`amdahl login\`.`);
10111
+ out.info(`Amdahl rejected the key in ${name}. Skipping the browser sign-in, because that key would still be used instead. Fix it, or unset it and run \`${amdahlCommand("login")}\`.`);
9783
10112
  }
9784
- let exit = EXIT.ok;
9785
- let ready = null;
9786
- for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
9787
- let steps;
9788
- try {
9789
- steps = await installClient(ctx, client, false);
9790
- } catch (raw) {
9791
- const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
9792
- io.stderr(`Error: ${err.message}
9793
- ${err.hint ? `${err.hint}
9794
- ` : ""}`);
9795
- if (exit === EXIT.ok) exit = err.exit;
9796
- continue;
9797
- }
9798
- const action = steps.mcp.data.action;
9799
- if (action === "printed") {
9800
- say(`! ${name}: \`claude\` is not on your PATH. Once it is, run:
9801
- ${String(steps.mcp.data.command)}`);
9802
- } else {
9803
- say(action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`);
9804
- ready ??= { name, connect: action !== "unchanged" };
9805
- }
9806
- const mcpDetail = action === "printed" ? [] : [steps.mcp.human, ...steps.mcp.notes];
9807
- detail([...mcpDetail, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
9808
- }
9809
- if (ready) say(tryItLine(ready.name, ready.connect));
9810
- return exit;
10113
+ const done = await installEach(ctx, chosen, detail);
10114
+ for (const line of finishLines(done.ready, inClaudeCode)) say(line);
10115
+ return exit === EXIT.ok ? done.exit : exit;
9811
10116
  }
9812
10117
 
9813
10118
  // src/commands/skills.ts
@@ -9935,8 +10240,8 @@ function readTar(tar) {
9935
10240
  throw new ArchiveError("its tar is not ustar");
9936
10241
  }
9937
10242
  const name = field(header, 0, 100);
9938
- const prefix = field(header, 345, 155);
9939
- let path = prefix ? `${prefix}/${name}` : name;
10243
+ const prefix2 = field(header, 345, 155);
10244
+ let path = prefix2 ? `${prefix2}/${name}` : name;
9940
10245
  const type = String.fromCharCode(header[156]);
9941
10246
  const size = octal(header, 124, 12);
9942
10247
  const mode = octal(header, 100, 8);
@@ -10357,9 +10662,9 @@ ${done}`);
10357
10662
 
10358
10663
  // src/commands/workspace-status.ts
10359
10664
  var BLOCKER_FIX = {
10360
- missing_scope: "This credential cannot optimize. Use a Customer agent key, or run `amdahl login` again.",
10361
- role_too_low: "Your role cannot optimize. Ask a workspace admin to raise it.",
10362
- quota_exhausted: "The monthly optimize quota is used up."
10665
+ missing_scope: () => `This credential cannot optimize. Use a Customer agent key, or run \`${amdahlCommand("login")}\` again.`,
10666
+ role_too_low: () => "Your role cannot optimize. Ask a workspace admin to raise it.",
10667
+ quota_exhausted: () => "The monthly optimize quota is used up."
10363
10668
  };
10364
10669
  function describe(status2) {
10365
10670
  const lines = [
@@ -10371,8 +10676,9 @@ function describe(status2) {
10371
10676
  if (opt.quota) {
10372
10677
  lines.push(`Quota: ${opt.quota.used} of ${opt.quota.limit} used, resets ${opt.quota.resets_at}`);
10373
10678
  }
10374
- if (!opt.allowed && opt.blocker && BLOCKER_FIX[opt.blocker]) {
10375
- lines.push(` ${BLOCKER_FIX[opt.blocker]}`);
10679
+ const fix = opt.blocker ? BLOCKER_FIX[opt.blocker] : void 0;
10680
+ if (!opt.allowed && fix) {
10681
+ lines.push(` ${fix()}`);
10376
10682
  }
10377
10683
  const conns = status2.connections;
10378
10684
  if ("omitted" in conns) {
@@ -10401,7 +10707,7 @@ async function workspaceList(ctx) {
10401
10707
  email: p.email,
10402
10708
  api_url: p.api_url
10403
10709
  }));
10404
- const human = profiles.length === 0 ? "No profiles. Run `amdahl login`." : profiles.map(
10710
+ const human = profiles.length === 0 ? `No profiles. Run \`${amdahlCommand("login")}\`.` : profiles.map(
10405
10711
  (p) => `${p.name === config.default_profile ? "*" : " "} ${p.name} ${p.workspace.name} ${p.email ?? ""} ${p.api_url}`
10406
10712
  ).join("\n");
10407
10713
  ctx.out.result({ ok: true, default: config.default_profile, profiles }, human);
@@ -10411,7 +10717,7 @@ async function workspaceUse(ctx, args) {
10411
10717
  const name = args[0];
10412
10718
  if (!name || args.length > 1) throw usageError("Usage: amdahl workspace use <profile>");
10413
10719
  if (!ctx.config.profiles[name]) {
10414
- throw usageError(`No profile named "${name}".`, "Run `amdahl workspace list` to see your profiles.");
10720
+ throw usageError(`No profile named "${name}".`, `Run \`${amdahlCommand("workspace list")}\` to see your profiles.`);
10415
10721
  }
10416
10722
  ctx.config.default_profile = name;
10417
10723
  ctx.saveConfig();
@@ -10420,7 +10726,7 @@ async function workspaceUse(ctx, args) {
10420
10726
  }
10421
10727
 
10422
10728
  // src/version.ts
10423
- var VERSION = "0.3.3";
10729
+ var VERSION = "0.3.4";
10424
10730
 
10425
10731
  // src/main.ts
10426
10732
  var COMMANDS = {
@@ -10438,8 +10744,9 @@ var COMMANDS = {
10438
10744
  install,
10439
10745
  "skills add": skillsAdd
10440
10746
  };
10441
- var NO_ARGS = /* @__PURE__ */ new Set(["login", "logout", "whoami", "auth token", "status", "keys create", "keys list", "workspace list"]);
10747
+ var NO_ARGS = /* @__PURE__ */ new Set(["setup", "login", "logout", "whoami", "auth token", "status", "keys create", "keys list", "workspace list"]);
10442
10748
  async function runCli(argv, io) {
10749
+ setCommandName(io);
10443
10750
  const json = argv.includes("--json");
10444
10751
  let out = createOutput(io, json, argv.includes("--no-color"));
10445
10752
  try {
@@ -10450,18 +10757,21 @@ async function runCli(argv, io) {
10450
10757
  `);
10451
10758
  return EXIT.ok;
10452
10759
  }
10453
- if (parsed.flags.help) {
10760
+ if (parsed.flags.help || parsed.command === "help") {
10454
10761
  io.stdout(USAGE);
10455
10762
  return EXIT.ok;
10456
10763
  }
10457
- if (!parsed.command) {
10458
- if (argv.every((a) => a === "--verbose") && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
10459
- io.stderr(USAGE);
10460
- return EXIT.usage;
10461
- }
10462
- if (NO_ARGS.has(parsed.command) && parsed.args.length > 0) {
10764
+ if (NO_ARGS.has(parsed.command ?? "") && parsed.args.length > 0) {
10463
10765
  throw new CliError("usage", `Unexpected argument: ${parsed.args[0]}`, EXIT.usage);
10464
10766
  }
10767
+ if (!parsed.command || parsed.command === "setup") {
10768
+ const mode = parsed.flags.json ? { kind: "usage", reason: "it has no --json output" } : setupMode(io);
10769
+ if (mode.kind === "usage") {
10770
+ io.stderr(`${noSetupLine(mode.reason)}${USAGE}`);
10771
+ return EXIT.usage;
10772
+ }
10773
+ return await setup(createCtx(io, out, parsed.flags), mode.kind);
10774
+ }
10465
10775
  const handler = COMMANDS[parsed.command];
10466
10776
  if (!handler) throw new CliError("usage", `Unknown command ${parsed.command}`, EXIT.usage);
10467
10777
  const ctx = createCtx(io, out, parsed.flags);