@mushi-mushi/cli 0.23.0 → 0.23.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.
package/README.md CHANGED
@@ -86,6 +86,20 @@ Monorepo awareness: if you run the wizard at a workspace root with no framework
86
86
 
87
87
  Console URL resolution: `MUSHI_CONSOLE_URL` → saved `consoleUrl` in config → localhost `:6464` probe → mushi-mushi monorepo heuristic → hosted default. Saved on successful `mushi login`.
88
88
 
89
+ ### Browser sign-in reliability
90
+
91
+ Device auth (RFC 8628) uses a stable per-machine `client_id` stored in
92
+ `~/.config/mushi/config.json`. Starting a new login on the same machine supersedes
93
+ any older pending approval for that id.
94
+
95
+ During token polling the CLI retries **429** and **408** responses automatically.
96
+ If saved credentials exist, re-running the wizard validates them with
97
+ `GET /v1/sync/whoami` before reinstalling.
98
+
99
+ The browser approval page waits until the CLI actually claims the token (not
100
+ just when you click Approve). Full flow + troubleshooting:
101
+ [CLI ↔ console loop](https://kensaur.us/mushi-mushi/docs/quickstart/cli-console-loop).
102
+
89
103
  ## Install globally
90
104
 
91
105
  ```bash
@@ -212,6 +226,18 @@ the CLI never points users at a 404. This safety property is unit-pinned in
212
226
  `packages/cli/src/migrate.test.ts` (positive control + negative control +
213
227
  real-catalog regression guard).
214
228
 
229
+ ### `mushi completion`
230
+
231
+ Prints a tab-completion script for bash, zsh, or fish, generated from the
232
+ CLI's live command tree — so it never drifts as commands are added.
233
+
234
+ ```bash
235
+ eval "$(mushi completion bash)" # try it for the current session
236
+ mushi completion bash >> ~/.bashrc # bash: persist across sessions
237
+ mushi completion zsh > "${fpath[1]}/_mushi" # zsh: needs a dir already on $fpath
238
+ mushi completion fish > ~/.config/fish/completions/mushi.fish
239
+ ```
240
+
215
241
  ### `mushi doctor`
216
242
 
217
243
  Checks CLI and SDK health. **Ingest and dispatch (server) checks run by
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var MUSHI_CLI_VERSION = true ? "0.23.1" : "0.0.0-dev";
3
+
4
+ export {
5
+ MUSHI_CLI_VERSION
6
+ };
@@ -10,6 +10,7 @@ import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/p
10
10
  import { join as join4, resolve } from "path";
11
11
 
12
12
  // src/config.ts
13
+ import { randomUUID } from "crypto";
13
14
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
14
15
  import { homedir } from "os";
15
16
  import { dirname, join } from "path";
@@ -50,6 +51,18 @@ function loadConfig(path = CONFIG_PATH) {
50
51
  };
51
52
  return { ...file, ...fromEnv };
52
53
  }
54
+ function ensureClientId(path = CONFIG_PATH) {
55
+ const existing = loadConfig(path);
56
+ if (existing.clientId && /^[A-Za-z0-9_-]{8,64}$/.test(existing.clientId)) {
57
+ return existing.clientId;
58
+ }
59
+ const clientId = `cli_${randomUUID().replace(/-/g, "")}`;
60
+ try {
61
+ saveConfig({ ...existing, clientId }, path);
62
+ } catch {
63
+ }
64
+ return clientId;
65
+ }
53
66
  function saveConfig(config, path = CONFIG_PATH) {
54
67
  const dir = dirname(path);
55
68
  if (!existsSync(dir)) {
@@ -103,7 +116,7 @@ function tightenDirPermissions(path) {
103
116
  }
104
117
 
105
118
  // src/version.ts
106
- var MUSHI_CLI_VERSION = true ? "0.23.0" : "0.0.0-dev";
119
+ var MUSHI_CLI_VERSION = true ? "0.23.1" : "0.0.0-dev";
107
120
 
108
121
  // src/errors.ts
109
122
  var EXIT_CODE_MAP = {
@@ -1214,6 +1227,7 @@ export {
1214
1227
  installSignalHandlers,
1215
1228
  MUSHI_CLI_VERSION,
1216
1229
  loadConfig,
1230
+ ensureClientId,
1217
1231
  saveConfig,
1218
1232
  FRAMEWORKS,
1219
1233
  readPackageJson,
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-BY5SZYRO.js";
3
+ } from "./chunk-I4UK3WTN.js";
4
4
 
5
5
  // src/config.ts
6
+ import { randomUUID } from "crypto";
6
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
7
8
  import { homedir } from "os";
8
9
  import { dirname, join } from "path";
@@ -43,6 +44,18 @@ function loadConfig(path = CONFIG_PATH) {
43
44
  };
44
45
  return { ...file, ...fromEnv };
45
46
  }
47
+ function ensureClientId(path = CONFIG_PATH) {
48
+ const existing = loadConfig(path);
49
+ if (existing.clientId && /^[A-Za-z0-9_-]{8,64}$/.test(existing.clientId)) {
50
+ return existing.clientId;
51
+ }
52
+ const clientId = `cli_${randomUUID().replace(/-/g, "")}`;
53
+ try {
54
+ saveConfig({ ...existing, clientId }, path);
55
+ } catch {
56
+ }
57
+ return clientId;
58
+ }
46
59
  function saveConfig(config, path = CONFIG_PATH) {
47
60
  const dir = dirname(path);
48
61
  if (!existsSync(dir)) {
@@ -405,6 +418,7 @@ async function apiCall(path, config, options = {}) {
405
418
  export {
406
419
  CONFIG_PATH,
407
420
  loadConfig,
421
+ ensureClientId,
408
422
  saveConfig,
409
423
  consoleUrl,
410
424
  resolveConsoleUrlSync,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runConnect
4
- } from "./chunk-GSQO35YD.js";
4
+ } from "./chunk-M6L22NJU.js";
5
5
  import "./chunk-LDPFTQLE.js";
6
6
  export {
7
7
  runConnect
@@ -4,13 +4,13 @@ import {
4
4
  normalizeEndpoint,
5
5
  requireUuid,
6
6
  saveConfig
7
- } from "./chunk-WLLFV26V.js";
7
+ } from "./chunk-NZ64LBLU.js";
8
8
  import {
9
9
  detectFramework,
10
10
  envVarsToWrite,
11
11
  readPackageJson
12
12
  } from "./chunk-AIZRI665.js";
13
- import "./chunk-BY5SZYRO.js";
13
+ import "./chunk-I4UK3WTN.js";
14
14
 
15
15
  // src/connect.ts
16
16
  import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  detectFramework,
13
13
  detectPackageManager,
14
14
  die,
15
+ ensureClientId,
15
16
  envVarsToWrite,
16
17
  fetchIngestSetup,
17
18
  fmtDate,
@@ -36,7 +37,7 @@ import {
36
37
  sanitizeProjectId,
37
38
  saveConfig,
38
39
  writeMcpServerEntry
39
- } from "./chunk-GSQO35YD.js";
40
+ } from "./chunk-M6L22NJU.js";
40
41
  import {
41
42
  TEST_REPORT_FETCH_TIMEOUT_MS,
42
43
  assertEndpoint,
@@ -56,6 +57,7 @@ import { join as join2 } from "path";
56
57
 
57
58
  // src/device-auth.ts
58
59
  var DEVICE_FETCH_TIMEOUT_MS = 15e3;
60
+ var DEFAULT_SLOW_DOWN_MS = 5e3;
59
61
  function trimTrailingSlash(endpoint) {
60
62
  let end = endpoint.length;
61
63
  while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
@@ -70,11 +72,12 @@ async function deviceFetch(url, init = {}) {
70
72
  clearTimeout(timer);
71
73
  }
72
74
  }
73
- async function startDeviceAuth(endpoint) {
75
+ async function startDeviceAuth(endpoint, clientId) {
74
76
  const base = trimTrailingSlash(endpoint);
75
77
  const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
76
78
  method: "POST",
77
- headers: { "Content-Type": "application/json" }
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify(clientId ? { client_id: clientId } : {})
78
81
  });
79
82
  const json = await res.json().catch(() => null);
80
83
  if (!res.ok || !json?.ok || !json.data) {
@@ -97,6 +100,12 @@ async function pollDeviceToken(endpoint, deviceCode) {
97
100
  switch (json.error) {
98
101
  case "authorization_pending":
99
102
  return { status: "pending" };
103
+ case "slow_down": {
104
+ const retryAfterHeader = res.headers?.get?.("Retry-After");
105
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
106
+ const retryAfterMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : DEFAULT_SLOW_DOWN_MS;
107
+ return { status: "slow_down", retryAfterMs };
108
+ }
100
109
  case "access_denied":
101
110
  return { status: "denied" };
102
111
  case "expired_token":
@@ -105,9 +114,11 @@ async function pollDeviceToken(endpoint, deviceCode) {
105
114
  return {
106
115
  status: "error",
107
116
  message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
108
- // 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
109
- // rejection (already claimed / invalid_grant) that won't fix itself.
110
- retryable: res.status >= 500
117
+ // 5xx is a server hiccup the next poll may clear; 408 (gateway
118
+ // timeout) is equally transient. Any other 4xx is a definitive
119
+ // rejection (already claimed / invalid_grant) that won't fix
120
+ // itself. (429/slow_down is handled above as its own outcome.)
121
+ retryable: res.status >= 500 || res.status === 408
111
122
  };
112
123
  }
113
124
  } catch (err) {
@@ -118,7 +129,7 @@ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
118
129
  async function waitForCliToken(endpoint, session, opts = {}) {
119
130
  const sleep = opts.sleep ?? defaultSleep;
120
131
  const now = opts.now ?? Date.now;
121
- const intervalMs = (session.interval || 5) * 1e3;
132
+ let intervalMs = (session.interval || 5) * 1e3;
122
133
  const deadline = now() + (session.expires_in || 600) * 1e3;
123
134
  const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
124
135
  let consecutiveErrors = 0;
@@ -136,6 +147,11 @@ async function waitForCliToken(endpoint, session, opts = {}) {
136
147
  consecutiveErrors = 0;
137
148
  opts.onPending?.();
138
149
  continue;
150
+ case "slow_down":
151
+ consecutiveErrors = 0;
152
+ intervalMs = Math.max(intervalMs, outcome.retryAfterMs);
153
+ opts.onSlowDown?.(outcome.retryAfterMs);
154
+ continue;
139
155
  case "denied":
140
156
  throw new Error("Login was denied in the browser.");
141
157
  case "expired":
@@ -431,7 +447,10 @@ async function runInit(options = {}) {
431
447
  const detected = detectFramework(cwd, pkg);
432
448
  const framework = await chooseFramework(detected, options);
433
449
  const consoleBase = await resolveConsoleUrl({ cwd });
434
- const endpoint = resolveCloudEndpoint(options.endpoint);
450
+ const endpoint = resolveCloudEndpoint(
451
+ options.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? loadConfig().endpoint
452
+ );
453
+ options = { ...options, endpoint };
435
454
  const credentials = await acquireCredentials(options, consoleBase, endpoint);
436
455
  await verifyCredentials(credentials, options, consoleBase);
437
456
  const pm = detectPackageManager(cwd);
@@ -542,14 +561,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
542
561
  p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
543
562
  }
544
563
  }
545
- return collectCredentialsManually(options, consoleBase);
564
+ return collectCredentialsManually(options, consoleBase, endpoint);
546
565
  }
547
566
  async function runBrowserSignIn(options, endpoint, consoleBase) {
548
567
  const startSpin = p.spinner();
549
568
  startSpin.start("Starting secure browser sign-in\u2026");
550
569
  let session;
551
570
  try {
552
- session = await startDeviceAuth(endpoint);
571
+ session = await startDeviceAuth(endpoint, ensureClientId());
553
572
  } catch (err) {
554
573
  startSpin.stop("Could not start browser sign-in.");
555
574
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -647,15 +666,36 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
647
666
  if (!projectId || !apiKey) return null;
648
667
  return { apiKey, projectId };
649
668
  }
650
- async function collectCredentialsManually(options, consoleBase) {
669
+ async function collectCredentialsManually(options, consoleBase, endpoint) {
651
670
  const existing = loadConfig();
652
- const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
671
+ let savedProjectId = existing.projectId;
672
+ let savedApiKey = existing.apiKey;
673
+ if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
674
+ p.log.info(
675
+ `Found saved credentials from a previous sign-in (project ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
676
+ );
677
+ const checkSpin = p.spinner();
678
+ checkSpin.start("Checking saved credentials\u2026");
679
+ const check = await apiCall("/v1/sync/whoami", {
680
+ apiKey: sanitizeSecret(savedApiKey),
681
+ projectId: sanitizeSecret(savedProjectId),
682
+ endpoint
683
+ });
684
+ if (check.ok) {
685
+ checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
686
+ } else {
687
+ checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
688
+ savedProjectId = void 0;
689
+ savedApiKey = void 0;
690
+ }
691
+ }
692
+ const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
653
693
  message: "Project ID",
654
694
  placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
655
695
  hint: projectIdHint(consoleBase),
656
696
  validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID \u2014 copy it from the Projects page or the panel right after you create a project."
657
697
  });
658
- const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
698
+ const rawApiKey = options.apiKey ?? savedApiKey ?? await promptText({
659
699
  message: "API key",
660
700
  placeholder: "mushi_xxxxxxxxxxxx",
661
701
  hint: apiKeyHint(consoleBase),
@@ -686,10 +726,22 @@ async function verifyCredentials(credentials, options, consoleBase) {
686
726
  if (!result.ok) {
687
727
  spinner3.stop("Credentials could not be verified.");
688
728
  p.log.error(result.error?.message ?? "Authentication failed.");
689
- p.log.info(`Double-check values in the console:
690
- ${cliSetupDeepLink(consoleBase)}`);
729
+ p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
730
+ p.log.info(
731
+ [
732
+ "To recover:",
733
+ ' \u2022 Re-run `npx mushi-mushi` and choose "Sign in with your browser", or',
734
+ ` \u2022 Double-check the Project ID / API key in the console: ${cliSetupDeepLink(consoleBase)}`
735
+ ].join("\n")
736
+ );
691
737
  throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
692
738
  }
739
+ if (result.data.project_id && result.data.project_id !== credentials.projectId) {
740
+ p.log.warn(
741
+ `Project ID ${redact(credentials.projectId)} doesn't match this API key's project \u2014 using the key's actual project (${result.data.project_name}) instead.`
742
+ );
743
+ credentials.projectId = result.data.project_id;
744
+ }
693
745
  spinner3.stop(`Connected to ${result.data.project_name}`);
694
746
  }
695
747
  function sanitizeSecret(raw) {
@@ -858,7 +910,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
858
910
  if (p.isCancel(answer) || !answer) return;
859
911
  const endpoint = resolveCloudEndpoint(options.endpoint);
860
912
  try {
861
- const { runConnect: runConnect2 } = await import("./connect-XFVSH4JZ.js");
913
+ const { runConnect: runConnect2 } = await import("./connect-5VPUYE6H.js");
862
914
  await runConnect2({
863
915
  apiKey: credentials.apiKey,
864
916
  projectId: credentials.projectId,
@@ -1201,7 +1253,7 @@ Examples:
1201
1253
  }
1202
1254
  let deviceData;
1203
1255
  try {
1204
- deviceData = await startDeviceAuth(endpoint);
1256
+ deviceData = await startDeviceAuth(endpoint, ensureClientId());
1205
1257
  } catch (err) {
1206
1258
  process.stderr.write(`
1207
1259
  error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
@@ -2208,7 +2260,7 @@ Typical first-time flow:
2208
2260
  console.log("");
2209
2261
  let session;
2210
2262
  try {
2211
- session = await startDeviceAuth(endpoint);
2263
+ session = await startDeviceAuth(endpoint, ensureClientId());
2212
2264
  } catch (err) {
2213
2265
  process.stderr.write(`
2214
2266
  error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
@@ -4789,6 +4841,131 @@ Examples:
4789
4841
  });
4790
4842
  }
4791
4843
 
4844
+ // src/commands/completion-cli.ts
4845
+ import { Argument } from "commander";
4846
+
4847
+ // src/completion.ts
4848
+ var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
4849
+ function isSupportedShell(value) {
4850
+ return SUPPORTED_SHELLS.includes(value);
4851
+ }
4852
+ function buildCommandTree(program2) {
4853
+ return program2.commands.filter((cmd) => cmd.name() !== "help").map((cmd) => ({
4854
+ name: cmd.name(),
4855
+ subcommands: cmd.commands.filter((sub) => sub.name() !== "help").map((sub) => sub.name()).sort()
4856
+ })).sort((a, b) => a.name.localeCompare(b.name));
4857
+ }
4858
+ var COMPLETION_ARG_VALUES = [...SUPPORTED_SHELLS];
4859
+ function subcommandsFor(tree, name) {
4860
+ if (name === "completion") return COMPLETION_ARG_VALUES;
4861
+ return tree.find((n) => n.name === name)?.subcommands ?? [];
4862
+ }
4863
+ function generateBash(tree) {
4864
+ const topLevel = tree.map((n) => n.name).join(" ");
4865
+ const cases = tree.map((n) => {
4866
+ const subs = subcommandsFor(tree, n.name);
4867
+ if (subs.length === 0) return null;
4868
+ return ` ${n.name})
4869
+ COMPREPLY=( $(compgen -W "${subs.join(" ")}" -- "$cur") )
4870
+ ;;`;
4871
+ }).filter((line) => line !== null).join("\n");
4872
+ return `###-begin-mushi-completion-###
4873
+ # mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
4874
+ # Install: mushi completion bash >> ~/.bashrc (then restart your shell)
4875
+ _mushi_completion() {
4876
+ local cur
4877
+ cur="\${COMP_WORDS[COMP_CWORD]}"
4878
+ COMPREPLY=()
4879
+
4880
+ if [ "$COMP_CWORD" -eq 1 ]; then
4881
+ COMPREPLY=( $(compgen -W "${topLevel}" -- "$cur") )
4882
+ return 0
4883
+ fi
4884
+
4885
+ case "\${COMP_WORDS[1]}" in
4886
+ ${cases}
4887
+ *)
4888
+ ;;
4889
+ esac
4890
+ }
4891
+ complete -F _mushi_completion mushi
4892
+ ###-end-mushi-completion-###
4893
+ `;
4894
+ }
4895
+ function generateZsh(tree) {
4896
+ const topLevel = tree.map((n) => n.name).join(" ");
4897
+ const cases = tree.map((n) => {
4898
+ const subs = subcommandsFor(tree, n.name);
4899
+ if (subs.length === 0) return null;
4900
+ return ` ${n.name})
4901
+ local -a subcmds; subcmds=(${subs.join(" ")})
4902
+ _describe '${n.name} subcommand' subcmds
4903
+ ;;`;
4904
+ }).filter((line) => line !== null).join("\n");
4905
+ return `#compdef mushi
4906
+ # mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
4907
+ # Install: mushi completion zsh > "\${fpath[1]}/_mushi" (then restart your shell)
4908
+ _mushi() {
4909
+ local -a top_level
4910
+ top_level=(${topLevel})
4911
+
4912
+ if (( CURRENT == 2 )); then
4913
+ _describe 'command' top_level
4914
+ return
4915
+ fi
4916
+
4917
+ case "\${words[2]}" in
4918
+ ${cases}
4919
+ *)
4920
+ ;;
4921
+ esac
4922
+ }
4923
+
4924
+ _mushi
4925
+ `;
4926
+ }
4927
+ function generateFish(tree) {
4928
+ const topLevel = tree.map((n) => n.name).join(" ");
4929
+ const lines = [
4930
+ "# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi",
4931
+ "# Install: mushi completion fish > ~/.config/fish/completions/mushi.fish",
4932
+ `complete -c mushi -f -n "__fish_use_subcommand" -a "${topLevel}"`
4933
+ ];
4934
+ for (const n of tree) {
4935
+ const subs = subcommandsFor(tree, n.name);
4936
+ if (subs.length === 0) continue;
4937
+ lines.push(`complete -c mushi -f -n "__fish_seen_subcommand_from ${n.name}" -a "${subs.join(" ")}"`);
4938
+ }
4939
+ return lines.join("\n") + "\n";
4940
+ }
4941
+ function generateCompletionScript(shell, tree) {
4942
+ switch (shell) {
4943
+ case "bash":
4944
+ return generateBash(tree);
4945
+ case "zsh":
4946
+ return generateZsh(tree);
4947
+ case "fish":
4948
+ return generateFish(tree);
4949
+ }
4950
+ }
4951
+
4952
+ // src/commands/completion-cli.ts
4953
+ function registerCompletionCommand(program2) {
4954
+ program2.command("completion").description("Print a shell completion script for bash, zsh, or fish").addArgument(new Argument("<shell>", "Target shell").choices([...SUPPORTED_SHELLS])).addHelpText("after", `
4955
+ Examples:
4956
+ eval "$(mushi completion bash)" # try it for the current session
4957
+ mushi completion bash >> ~/.bashrc # bash: persist across sessions
4958
+ mushi completion zsh > "\${fpath[1]}/_mushi" # zsh: needs a dir already on $fpath
4959
+ mushi completion fish > ~/.config/fish/completions/mushi.fish`).action((shell) => {
4960
+ if (!isSupportedShell(shell)) {
4961
+ console.error(`Unsupported shell "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
4962
+ process.exit(2);
4963
+ }
4964
+ const tree = buildCommandTree(program2);
4965
+ console.log(generateCompletionScript(shell, tree));
4966
+ });
4967
+ }
4968
+
4792
4969
  // src/index.ts
4793
4970
  installSignalHandlers();
4794
4971
  var program = new Command().name("mushi").description("Mushi CLI \u2014 set up the SDK, triage a report, fix from your editor").version(MUSHI_CLI_VERSION).addHelpText("after", `
@@ -4827,6 +5004,7 @@ registerQaCommands(program);
4827
5004
  registerAuditCommands(program);
4828
5005
  registerSkillsCommands(program);
4829
5006
  registerBillingCommands(program);
5007
+ registerCompletionCommand(program);
4830
5008
  program.parseAsync().catch((err) => {
4831
5009
  printAndExit(err);
4832
5010
  });
package/dist/init.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  apiKeyHint,
5
5
  cliSetupDeepLink,
6
6
  consoleUrl,
7
+ ensureClientId,
7
8
  loadConfig,
8
9
  normalizeEndpoint,
9
10
  openInBrowser,
@@ -13,7 +14,7 @@ import {
13
14
  resolveConsoleUrl,
14
15
  resolveConsoleUrlSync,
15
16
  saveConfig
16
- } from "./chunk-WLLFV26V.js";
17
+ } from "./chunk-NZ64LBLU.js";
17
18
  import {
18
19
  FRAMEWORKS,
19
20
  detectFramework,
@@ -24,7 +25,7 @@ import {
24
25
  } from "./chunk-AIZRI665.js";
25
26
  import {
26
27
  MUSHI_CLI_VERSION
27
- } from "./chunk-BY5SZYRO.js";
28
+ } from "./chunk-I4UK3WTN.js";
28
29
 
29
30
  // src/init.ts
30
31
  import * as p from "@clack/prompts";
@@ -35,6 +36,7 @@ import { join as join2 } from "path";
35
36
 
36
37
  // src/device-auth.ts
37
38
  var DEVICE_FETCH_TIMEOUT_MS = 15e3;
39
+ var DEFAULT_SLOW_DOWN_MS = 5e3;
38
40
  function trimTrailingSlash(endpoint) {
39
41
  let end = endpoint.length;
40
42
  while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
@@ -49,11 +51,12 @@ async function deviceFetch(url, init = {}) {
49
51
  clearTimeout(timer);
50
52
  }
51
53
  }
52
- async function startDeviceAuth(endpoint) {
54
+ async function startDeviceAuth(endpoint, clientId) {
53
55
  const base = trimTrailingSlash(endpoint);
54
56
  const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
55
57
  method: "POST",
56
- headers: { "Content-Type": "application/json" }
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify(clientId ? { client_id: clientId } : {})
57
60
  });
58
61
  const json = await res.json().catch(() => null);
59
62
  if (!res.ok || !json?.ok || !json.data) {
@@ -76,6 +79,12 @@ async function pollDeviceToken(endpoint, deviceCode) {
76
79
  switch (json.error) {
77
80
  case "authorization_pending":
78
81
  return { status: "pending" };
82
+ case "slow_down": {
83
+ const retryAfterHeader = res.headers?.get?.("Retry-After");
84
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
85
+ const retryAfterMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : DEFAULT_SLOW_DOWN_MS;
86
+ return { status: "slow_down", retryAfterMs };
87
+ }
79
88
  case "access_denied":
80
89
  return { status: "denied" };
81
90
  case "expired_token":
@@ -84,9 +93,11 @@ async function pollDeviceToken(endpoint, deviceCode) {
84
93
  return {
85
94
  status: "error",
86
95
  message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
87
- // 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
88
- // rejection (already claimed / invalid_grant) that won't fix itself.
89
- retryable: res.status >= 500
96
+ // 5xx is a server hiccup the next poll may clear; 408 (gateway
97
+ // timeout) is equally transient. Any other 4xx is a definitive
98
+ // rejection (already claimed / invalid_grant) that won't fix
99
+ // itself. (429/slow_down is handled above as its own outcome.)
100
+ retryable: res.status >= 500 || res.status === 408
90
101
  };
91
102
  }
92
103
  } catch (err) {
@@ -97,7 +108,7 @@ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
97
108
  async function waitForCliToken(endpoint, session, opts = {}) {
98
109
  const sleep = opts.sleep ?? defaultSleep;
99
110
  const now = opts.now ?? Date.now;
100
- const intervalMs = (session.interval || 5) * 1e3;
111
+ let intervalMs = (session.interval || 5) * 1e3;
101
112
  const deadline = now() + (session.expires_in || 600) * 1e3;
102
113
  const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
103
114
  let consecutiveErrors = 0;
@@ -115,6 +126,11 @@ async function waitForCliToken(endpoint, session, opts = {}) {
115
126
  consecutiveErrors = 0;
116
127
  opts.onPending?.();
117
128
  continue;
129
+ case "slow_down":
130
+ consecutiveErrors = 0;
131
+ intervalMs = Math.max(intervalMs, outcome.retryAfterMs);
132
+ opts.onSlowDown?.(outcome.retryAfterMs);
133
+ continue;
118
134
  case "denied":
119
135
  throw new Error("Login was denied in the browser.");
120
136
  case "expired":
@@ -390,7 +406,10 @@ async function runInit(options = {}) {
390
406
  const detected = detectFramework(cwd, pkg);
391
407
  const framework = await chooseFramework(detected, options);
392
408
  const consoleBase = await resolveConsoleUrl({ cwd });
393
- const endpoint = resolveCloudEndpoint(options.endpoint);
409
+ const endpoint = resolveCloudEndpoint(
410
+ options.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? loadConfig().endpoint
411
+ );
412
+ options = { ...options, endpoint };
394
413
  const credentials = await acquireCredentials(options, consoleBase, endpoint);
395
414
  await verifyCredentials(credentials, options, consoleBase);
396
415
  const pm = detectPackageManager(cwd);
@@ -501,14 +520,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
501
520
  p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
502
521
  }
503
522
  }
504
- return collectCredentialsManually(options, consoleBase);
523
+ return collectCredentialsManually(options, consoleBase, endpoint);
505
524
  }
506
525
  async function runBrowserSignIn(options, endpoint, consoleBase) {
507
526
  const startSpin = p.spinner();
508
527
  startSpin.start("Starting secure browser sign-in\u2026");
509
528
  let session;
510
529
  try {
511
- session = await startDeviceAuth(endpoint);
530
+ session = await startDeviceAuth(endpoint, ensureClientId());
512
531
  } catch (err) {
513
532
  startSpin.stop("Could not start browser sign-in.");
514
533
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -606,15 +625,36 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
606
625
  if (!projectId || !apiKey) return null;
607
626
  return { apiKey, projectId };
608
627
  }
609
- async function collectCredentialsManually(options, consoleBase) {
628
+ async function collectCredentialsManually(options, consoleBase, endpoint) {
610
629
  const existing = loadConfig();
611
- const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
630
+ let savedProjectId = existing.projectId;
631
+ let savedApiKey = existing.apiKey;
632
+ if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
633
+ p.log.info(
634
+ `Found saved credentials from a previous sign-in (project ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
635
+ );
636
+ const checkSpin = p.spinner();
637
+ checkSpin.start("Checking saved credentials\u2026");
638
+ const check = await apiCall("/v1/sync/whoami", {
639
+ apiKey: sanitizeSecret(savedApiKey),
640
+ projectId: sanitizeSecret(savedProjectId),
641
+ endpoint
642
+ });
643
+ if (check.ok) {
644
+ checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
645
+ } else {
646
+ checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
647
+ savedProjectId = void 0;
648
+ savedApiKey = void 0;
649
+ }
650
+ }
651
+ const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
612
652
  message: "Project ID",
613
653
  placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
614
654
  hint: projectIdHint(consoleBase),
615
655
  validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID \u2014 copy it from the Projects page or the panel right after you create a project."
616
656
  });
617
- const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
657
+ const rawApiKey = options.apiKey ?? savedApiKey ?? await promptText({
618
658
  message: "API key",
619
659
  placeholder: "mushi_xxxxxxxxxxxx",
620
660
  hint: apiKeyHint(consoleBase),
@@ -645,10 +685,22 @@ async function verifyCredentials(credentials, options, consoleBase) {
645
685
  if (!result.ok) {
646
686
  spinner2.stop("Credentials could not be verified.");
647
687
  p.log.error(result.error?.message ?? "Authentication failed.");
648
- p.log.info(`Double-check values in the console:
649
- ${cliSetupDeepLink(consoleBase)}`);
688
+ p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
689
+ p.log.info(
690
+ [
691
+ "To recover:",
692
+ ' \u2022 Re-run `npx mushi-mushi` and choose "Sign in with your browser", or',
693
+ ` \u2022 Double-check the Project ID / API key in the console: ${cliSetupDeepLink(consoleBase)}`
694
+ ].join("\n")
695
+ );
650
696
  throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
651
697
  }
698
+ if (result.data.project_id && result.data.project_id !== credentials.projectId) {
699
+ p.log.warn(
700
+ `Project ID ${redact(credentials.projectId)} doesn't match this API key's project \u2014 using the key's actual project (${result.data.project_name}) instead.`
701
+ );
702
+ credentials.projectId = result.data.project_id;
703
+ }
652
704
  spinner2.stop(`Connected to ${result.data.project_name}`);
653
705
  }
654
706
  function sanitizeSecret(raw) {
@@ -817,7 +869,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
817
869
  if (p.isCancel(answer) || !answer) return;
818
870
  const endpoint = resolveCloudEndpoint(options.endpoint);
819
871
  try {
820
- const { runConnect } = await import("./connect-KX4UGY6Z.js");
872
+ const { runConnect } = await import("./connect-IWFKKXGZ.js");
821
873
  await runConnect({
822
874
  apiKey: credentials.apiKey,
823
875
  projectId: credentials.projectId,
package/dist/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MUSHI_CLI_VERSION
3
- } from "./chunk-BY5SZYRO.js";
3
+ } from "./chunk-I4UK3WTN.js";
4
4
  export {
5
5
  MUSHI_CLI_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mushi-mushi/cli",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "license": "MIT",
5
5
  "description": "CLI for Mushi Mushi — `mushi init` wizard installs the right SDK for your framework, plus report triage and pipeline health commands",
6
6
  "bin": {
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var MUSHI_CLI_VERSION = true ? "0.23.0" : "0.0.0-dev";
3
-
4
- export {
5
- MUSHI_CLI_VERSION
6
- };