@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 +26 -0
- package/dist/chunk-I4UK3WTN.js +6 -0
- package/dist/{chunk-GSQO35YD.js → chunk-M6L22NJU.js} +15 -1
- package/dist/{chunk-WLLFV26V.js → chunk-NZ64LBLU.js} +15 -1
- package/dist/{connect-XFVSH4JZ.js → connect-5VPUYE6H.js} +1 -1
- package/dist/{connect-KX4UGY6Z.js → connect-IWFKKXGZ.js} +2 -2
- package/dist/index.js +196 -18
- package/dist/init.js +69 -17
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-BY5SZYRO.js +0 -6
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
|
|
@@ -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.
|
|
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-
|
|
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,
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
normalizeEndpoint,
|
|
5
5
|
requireUuid,
|
|
6
6
|
saveConfig
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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;
|
|
109
|
-
//
|
|
110
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ??
|
|
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.
|
|
690
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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;
|
|
88
|
-
//
|
|
89
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ??
|
|
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.
|
|
649
|
-
|
|
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-
|
|
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
package/package.json
CHANGED