@mushi-mushi/cli 0.18.2 → 0.20.0
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/CONTRIBUTING.md +12 -0
- package/README.md +85 -24
- package/dist/chunk-6LM2KW4S.js +6 -0
- package/dist/chunk-D5IDNBJO.js +750 -0
- package/dist/chunk-XEXYGDQV.js +137 -0
- package/dist/connect-IC7I76Z2.js +297 -0
- package/dist/connect-W2Q6N7K3.js +8 -0
- package/dist/index.js +974 -762
- package/dist/init.js +544 -104
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-A2NHESSJ.js +0 -313
- package/dist/chunk-E4IZPV6D.js +0 -6
- package/dist/cli-shared-XH3XYIS7.js +0 -18
package/dist/index.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
FRAMEWORKS,
|
|
4
|
+
apiKeyHeaders,
|
|
5
|
+
buildMcpServerBlock,
|
|
6
|
+
buildMcpServerName,
|
|
7
|
+
detectFramework,
|
|
8
|
+
detectPackageManager,
|
|
9
|
+
envVarsToWrite,
|
|
10
|
+
fetchIngestSetup,
|
|
11
|
+
installCommand,
|
|
10
12
|
loadConfig,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
readPackageJson,
|
|
14
|
+
runConnect,
|
|
15
|
+
sanitizeApiKey,
|
|
16
|
+
sanitizeCliCredentials,
|
|
17
|
+
sanitizeEndpoint,
|
|
18
|
+
sanitizeProjectId,
|
|
19
|
+
saveConfig,
|
|
20
|
+
writeMcpServerEntry
|
|
21
|
+
} from "./chunk-D5IDNBJO.js";
|
|
15
22
|
import {
|
|
23
|
+
CLOUD_API_ENDPOINT,
|
|
16
24
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
17
25
|
assertEndpoint,
|
|
18
26
|
normalizeEndpoint,
|
|
@@ -22,6 +30,88 @@ import {
|
|
|
22
30
|
// src/index.ts
|
|
23
31
|
import { Command } from "commander";
|
|
24
32
|
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
var EXIT_CODE_MAP = {
|
|
35
|
+
E_AUTH_MISSING: 2,
|
|
36
|
+
E_AUTH_INVALID: 2,
|
|
37
|
+
E_PROJECT_MISSING: 2,
|
|
38
|
+
E_ENDPOINT_INVALID: 2,
|
|
39
|
+
E_INVALID_INPUT: 2,
|
|
40
|
+
E_NOT_INTERACTIVE: 2,
|
|
41
|
+
E_NETWORK: 3,
|
|
42
|
+
E_TIMEOUT: 3,
|
|
43
|
+
E_API_ERROR: 3,
|
|
44
|
+
E_RATE_LIMITED: 3,
|
|
45
|
+
E_FILE_NOT_FOUND: 1,
|
|
46
|
+
E_FILE_PERMISSION: 1,
|
|
47
|
+
E_FRESHNESS_STALE: 1,
|
|
48
|
+
E_INTERRUPTED: 130,
|
|
49
|
+
// 128 + SIGINT (2). POSIX convention.
|
|
50
|
+
E_INTERNAL: 1
|
|
51
|
+
};
|
|
52
|
+
var MushiCliError = class extends Error {
|
|
53
|
+
code;
|
|
54
|
+
hint;
|
|
55
|
+
cause;
|
|
56
|
+
constructor(code, message, hint, cause) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "MushiCliError";
|
|
59
|
+
this.code = code;
|
|
60
|
+
if (hint) this.hint = hint;
|
|
61
|
+
if (cause !== void 0) this.cause = cause;
|
|
62
|
+
}
|
|
63
|
+
/** Exit code POSIX-aware shell scripts should branch on. */
|
|
64
|
+
get exitCode() {
|
|
65
|
+
return EXIT_CODE_MAP[this.code];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Format for `--json` output. Used by the JSON writer when a CLI
|
|
69
|
+
* subcommand was invoked with `--json` and an error escapes.
|
|
70
|
+
*/
|
|
71
|
+
toJSON() {
|
|
72
|
+
return {
|
|
73
|
+
error: {
|
|
74
|
+
code: this.code,
|
|
75
|
+
message: this.message,
|
|
76
|
+
...this.hint ? { hint: this.hint } : {}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/signals.ts
|
|
83
|
+
var installed = false;
|
|
84
|
+
var activeController = null;
|
|
85
|
+
function installSignalHandlers() {
|
|
86
|
+
if (installed) return;
|
|
87
|
+
installed = true;
|
|
88
|
+
const abortAndExit = (signal) => {
|
|
89
|
+
activeController?.abort(
|
|
90
|
+
new MushiCliError(
|
|
91
|
+
"E_INTERRUPTED",
|
|
92
|
+
`aborted by ${signal}`,
|
|
93
|
+
"partial state on the server is safe to retry \u2014 re-run the same command"
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
|
|
97
|
+
process.nextTick(() => {
|
|
98
|
+
process.exit(code);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
process.on("SIGINT", () => abortAndExit("SIGINT"));
|
|
102
|
+
process.on("SIGTERM", () => abortAndExit("SIGTERM"));
|
|
103
|
+
}
|
|
104
|
+
function getAbortSignal(external) {
|
|
105
|
+
if (external) return external.signal;
|
|
106
|
+
if (!activeController || activeController.signal.aborted) {
|
|
107
|
+
activeController = new AbortController();
|
|
108
|
+
}
|
|
109
|
+
return activeController.signal;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/version.ts
|
|
113
|
+
var MUSHI_CLI_VERSION = true ? "0.20.0" : "0.0.0-dev";
|
|
114
|
+
|
|
25
115
|
// src/init.ts
|
|
26
116
|
import * as p from "@clack/prompts";
|
|
27
117
|
import { spawn } from "child_process";
|
|
@@ -29,342 +119,382 @@ import { randomUUID } from "crypto";
|
|
|
29
119
|
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
30
120
|
import { join as join3 } from "path";
|
|
31
121
|
|
|
32
|
-
// src/
|
|
33
|
-
import {
|
|
34
|
-
import { join } from "path";
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
67
|
-
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
68
|
-
}}>
|
|
69
|
-
<App />
|
|
70
|
-
</MushiProvider>
|
|
71
|
-
)`
|
|
72
|
-
},
|
|
73
|
-
vue: {
|
|
74
|
-
id: "vue",
|
|
75
|
-
label: "Vue 3",
|
|
76
|
-
packageName: "@mushi-mushi/vue",
|
|
77
|
-
needsWebPackage: false,
|
|
78
|
-
snippet: () => `// src/main.ts
|
|
79
|
-
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
80
|
-
|
|
81
|
-
app.use(MushiPlugin, {
|
|
82
|
-
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
83
|
-
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
84
|
-
})`
|
|
85
|
-
},
|
|
86
|
-
nuxt: {
|
|
87
|
-
id: "nuxt",
|
|
88
|
-
label: "Nuxt",
|
|
89
|
-
packageName: "@mushi-mushi/vue",
|
|
90
|
-
needsWebPackage: false,
|
|
91
|
-
snippet: () => `// plugins/mushi.client.ts
|
|
92
|
-
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
93
|
-
|
|
94
|
-
export default defineNuxtPlugin((nuxtApp) => {
|
|
95
|
-
nuxtApp.vueApp.use(MushiPlugin, {
|
|
96
|
-
projectId: import.meta.env.NUXT_PUBLIC_MUSHI_PROJECT_ID,
|
|
97
|
-
apiKey: import.meta.env.NUXT_PUBLIC_MUSHI_API_KEY,
|
|
98
|
-
})
|
|
99
|
-
})`
|
|
100
|
-
},
|
|
101
|
-
svelte: {
|
|
102
|
-
id: "svelte",
|
|
103
|
-
label: "Svelte",
|
|
104
|
-
packageName: "@mushi-mushi/svelte",
|
|
105
|
-
needsWebPackage: false,
|
|
106
|
-
snippet: () => `// src/main.ts
|
|
107
|
-
import { initMushi } from '@mushi-mushi/svelte'
|
|
108
|
-
|
|
109
|
-
initMushi({
|
|
110
|
-
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
111
|
-
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
112
|
-
})`
|
|
113
|
-
},
|
|
114
|
-
sveltekit: {
|
|
115
|
-
id: "sveltekit",
|
|
116
|
-
label: "SvelteKit",
|
|
117
|
-
packageName: "@mushi-mushi/svelte",
|
|
118
|
-
needsWebPackage: false,
|
|
119
|
-
snippet: () => `// src/routes/+layout.svelte
|
|
120
|
-
<script>
|
|
121
|
-
import { onMount } from 'svelte'
|
|
122
|
-
|
|
123
|
-
onMount(async () => {
|
|
124
|
-
const { initMushi } = await import('@mushi-mushi/svelte')
|
|
125
|
-
initMushi({
|
|
126
|
-
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
127
|
-
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
</script>`
|
|
131
|
-
},
|
|
132
|
-
angular: {
|
|
133
|
-
id: "angular",
|
|
134
|
-
label: "Angular",
|
|
135
|
-
packageName: "@mushi-mushi/angular",
|
|
136
|
-
needsWebPackage: false,
|
|
137
|
-
snippet: () => `// src/main.ts
|
|
138
|
-
import { provideMushi } from '@mushi-mushi/angular'
|
|
139
|
-
|
|
140
|
-
bootstrapApplication(AppComponent, {
|
|
141
|
-
providers: [
|
|
142
|
-
provideMushi({
|
|
143
|
-
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
144
|
-
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
145
|
-
}),
|
|
146
|
-
],
|
|
147
|
-
})`
|
|
148
|
-
},
|
|
149
|
-
expo: {
|
|
150
|
-
id: "expo",
|
|
151
|
-
label: "Expo",
|
|
152
|
-
packageName: "@mushi-mushi/react-native",
|
|
153
|
-
needsWebPackage: false,
|
|
154
|
-
snippet: () => `// App.tsx
|
|
155
|
-
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
156
|
-
|
|
157
|
-
export default function App() {
|
|
158
|
-
return (
|
|
159
|
-
<MushiProvider
|
|
160
|
-
projectId={process.env.EXPO_PUBLIC_MUSHI_PROJECT_ID!}
|
|
161
|
-
apiKey={process.env.EXPO_PUBLIC_MUSHI_API_KEY!}
|
|
162
|
-
>
|
|
163
|
-
<YourApp />
|
|
164
|
-
</MushiProvider>
|
|
165
|
-
)
|
|
166
|
-
}`
|
|
167
|
-
},
|
|
168
|
-
"react-native": {
|
|
169
|
-
id: "react-native",
|
|
170
|
-
label: "React Native",
|
|
171
|
-
packageName: "@mushi-mushi/react-native",
|
|
172
|
-
needsWebPackage: false,
|
|
173
|
-
snippet: () => `// App.tsx
|
|
174
|
-
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
175
|
-
|
|
176
|
-
export default function App() {
|
|
177
|
-
return (
|
|
178
|
-
<MushiProvider
|
|
179
|
-
projectId={process.env.MUSHI_PROJECT_ID!}
|
|
180
|
-
apiKey={process.env.MUSHI_API_KEY!}
|
|
181
|
-
>
|
|
182
|
-
<YourApp />
|
|
183
|
-
</MushiProvider>
|
|
184
|
-
)
|
|
185
|
-
}`
|
|
186
|
-
},
|
|
187
|
-
capacitor: {
|
|
188
|
-
id: "capacitor",
|
|
189
|
-
label: "Capacitor (Ionic)",
|
|
190
|
-
packageName: "@mushi-mushi/capacitor",
|
|
191
|
-
needsWebPackage: false,
|
|
192
|
-
/* The Capacitor plugin's public API is `Mushi.configure(...)`, not
|
|
193
|
-
* `Mushi.init(...)` — see packages/capacitor/src/definitions.ts. We
|
|
194
|
-
* shipped the wrong call here for two releases and users got a runtime
|
|
195
|
-
* `TypeError: Mushi.init is not a function`. The accompanying admin
|
|
196
|
-
* console snippet (apps/admin/src/lib/sdkSnippets.ts) emits the same
|
|
197
|
-
* `Mushi.configure(...)` shape; both are pinned by tests. */
|
|
198
|
-
snippet: () => `// src/main.ts \u2014 after install, run \`npx cap sync\`
|
|
199
|
-
import { Mushi } from '@mushi-mushi/capacitor'
|
|
200
|
-
|
|
201
|
-
await Mushi.configure({
|
|
202
|
-
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
203
|
-
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
204
|
-
})`
|
|
205
|
-
},
|
|
206
|
-
express: {
|
|
207
|
-
id: "express",
|
|
208
|
-
label: "Express",
|
|
209
|
-
packageName: "@mushi-mushi/node",
|
|
210
|
-
needsWebPackage: false,
|
|
211
|
-
snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
212
|
-
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
213
|
-
import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
|
|
214
|
-
import type { Express } from 'express'
|
|
215
|
-
|
|
216
|
-
export const mushi = new MushiNodeClient({
|
|
217
|
-
projectId: process.env.MUSHI_PROJECT_ID!,
|
|
218
|
-
apiKey: process.env.MUSHI_API_KEY!,
|
|
219
|
-
environment: process.env.NODE_ENV ?? 'production',
|
|
220
|
-
})
|
|
221
|
-
attachUnhandledHook({ client: mushi })
|
|
222
|
-
|
|
223
|
-
export function attachMushi(app: Express) {
|
|
224
|
-
app.use(mushiExpressErrorHandler({ client: mushi }))
|
|
225
|
-
}`
|
|
226
|
-
},
|
|
227
|
-
fastify: {
|
|
228
|
-
id: "fastify",
|
|
229
|
-
label: "Fastify",
|
|
230
|
-
packageName: "@mushi-mushi/node",
|
|
231
|
-
needsWebPackage: false,
|
|
232
|
-
snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
233
|
-
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
234
|
-
import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
|
|
235
|
-
import Fastify from 'fastify'
|
|
236
|
-
|
|
237
|
-
export const mushi = new MushiNodeClient({
|
|
238
|
-
projectId: process.env.MUSHI_PROJECT_ID!,
|
|
239
|
-
apiKey: process.env.MUSHI_API_KEY!,
|
|
240
|
-
environment: process.env.NODE_ENV ?? 'production',
|
|
241
|
-
})
|
|
242
|
-
attachUnhandledHook({ client: mushi })
|
|
243
|
-
|
|
244
|
-
const app = Fastify()
|
|
245
|
-
mushiFastifyPlugin(app, { client: mushi })`
|
|
246
|
-
},
|
|
247
|
-
hono: {
|
|
248
|
-
id: "hono",
|
|
249
|
-
label: "Hono",
|
|
250
|
-
packageName: "@mushi-mushi/node",
|
|
251
|
-
needsWebPackage: false,
|
|
252
|
-
snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
253
|
-
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
254
|
-
import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
|
|
255
|
-
import { Hono } from 'hono'
|
|
256
|
-
|
|
257
|
-
export const mushi = new MushiNodeClient({
|
|
258
|
-
projectId: process.env.MUSHI_PROJECT_ID!,
|
|
259
|
-
apiKey: process.env.MUSHI_API_KEY!,
|
|
260
|
-
environment: process.env.NODE_ENV ?? 'production',
|
|
261
|
-
})
|
|
262
|
-
attachUnhandledHook({ client: mushi })
|
|
263
|
-
|
|
264
|
-
const app = new Hono()
|
|
265
|
-
app.onError(
|
|
266
|
-
mushiHonoErrorHandler({ client: mushi }, (err, c) =>
|
|
267
|
-
c.text('Internal Server Error', 500),
|
|
268
|
-
),
|
|
269
|
-
)`
|
|
270
|
-
},
|
|
271
|
-
vanilla: {
|
|
272
|
-
id: "vanilla",
|
|
273
|
-
label: "Vanilla JS / unknown",
|
|
274
|
-
packageName: "@mushi-mushi/web",
|
|
275
|
-
needsWebPackage: false,
|
|
276
|
-
snippet: () => `// Anywhere in your client bundle
|
|
277
|
-
import { Mushi } from '@mushi-mushi/web'
|
|
278
|
-
|
|
279
|
-
Mushi.init({
|
|
280
|
-
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
281
|
-
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
282
|
-
})`
|
|
122
|
+
// src/console-url.ts
|
|
123
|
+
import { existsSync, readFileSync } from "fs";
|
|
124
|
+
import { dirname, join } from "path";
|
|
125
|
+
var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
126
|
+
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
127
|
+
var LOCAL_PROBE_MS = 800;
|
|
128
|
+
function normalizeConsoleBase(url) {
|
|
129
|
+
const trimmed = url.trim();
|
|
130
|
+
let end = trimmed.length;
|
|
131
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
|
|
132
|
+
return trimmed.slice(0, end);
|
|
133
|
+
}
|
|
134
|
+
function consoleUrl(base, route) {
|
|
135
|
+
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
136
|
+
return `${normalizeConsoleBase(base)}${normalizedRoute}`;
|
|
137
|
+
}
|
|
138
|
+
function resolveConsoleUrlFromEnv() {
|
|
139
|
+
const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
|
|
140
|
+
return raw ? normalizeConsoleBase(raw) : void 0;
|
|
141
|
+
}
|
|
142
|
+
function isInsideMushiMonorepo(cwd) {
|
|
143
|
+
let dir = cwd;
|
|
144
|
+
for (let depth = 0; depth < 14; depth += 1) {
|
|
145
|
+
const pkgPath = join(dir, "package.json");
|
|
146
|
+
if (existsSync(pkgPath)) {
|
|
147
|
+
try {
|
|
148
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
149
|
+
if (pkg.name === "mushi-mushi") return true;
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const parent = dirname(dir);
|
|
154
|
+
if (parent === dir) break;
|
|
155
|
+
dir = parent;
|
|
283
156
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (!existsSync(path)) return null;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
async function probeLocalAdminConsole() {
|
|
288
160
|
try {
|
|
289
|
-
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
|
|
163
|
+
const res = await fetch(LOCAL_ADMIN_URL, {
|
|
164
|
+
signal: controller.signal,
|
|
165
|
+
method: "HEAD"
|
|
166
|
+
});
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
if (res.ok || res.status === 404 || res.status === 401) {
|
|
169
|
+
return LOCAL_ADMIN_URL;
|
|
170
|
+
}
|
|
290
171
|
} catch {
|
|
291
|
-
return null;
|
|
292
172
|
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function resolveConsoleUrlSync(cwd = process.cwd()) {
|
|
176
|
+
const fromEnv = resolveConsoleUrlFromEnv();
|
|
177
|
+
if (fromEnv) return fromEnv;
|
|
178
|
+
const config = loadConfig();
|
|
179
|
+
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
180
|
+
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
181
|
+
return HOSTED_CONSOLE_BASE;
|
|
182
|
+
}
|
|
183
|
+
async function resolveConsoleUrl(options) {
|
|
184
|
+
const fromEnv = resolveConsoleUrlFromEnv();
|
|
185
|
+
if (fromEnv) return fromEnv;
|
|
186
|
+
const config = loadConfig();
|
|
187
|
+
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
188
|
+
const local = await probeLocalAdminConsole();
|
|
189
|
+
if (local) return local;
|
|
190
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
191
|
+
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
192
|
+
return HOSTED_CONSOLE_BASE;
|
|
193
|
+
}
|
|
194
|
+
function projectIdHint(base) {
|
|
195
|
+
return `Where to find it: ${consoleUrl(base, "/projects")} \u2192 select your project \u2192 click the UUID chip to copy (or copy it from the panel right after you create the project).`;
|
|
196
|
+
}
|
|
197
|
+
function apiKeyHint(base) {
|
|
198
|
+
return `Where to find it: ${consoleUrl(base, "/onboarding?tab=verify")} \u2192 Generate API key (SDK ingest needs report:write \u2014 not Settings \u2192 BYOK keys). Treat it like a password \u2014 env file only, never commit.`;
|
|
199
|
+
}
|
|
200
|
+
function cliSetupDeepLink(base) {
|
|
201
|
+
return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
|
|
202
|
+
}
|
|
203
|
+
function reportsUrl(base, reportId) {
|
|
204
|
+
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
205
|
+
}
|
|
206
|
+
async function openInBrowser(url) {
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = new URL(url);
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const safeUrl = parsed.toString();
|
|
217
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
218
|
+
const [command, args] = process.platform === "win32" ? (
|
|
219
|
+
// Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
|
|
220
|
+
// `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
|
|
221
|
+
// characters like `&` / `|` / `^` could still break out — exactly the
|
|
222
|
+
// injection surface CodeQL flags. rundll32 never invokes a shell: the URL
|
|
223
|
+
// is handed to the default browser as one opaque argument.
|
|
224
|
+
["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
225
|
+
) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
|
|
226
|
+
await new Promise((resolve3) => {
|
|
227
|
+
try {
|
|
228
|
+
const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
|
|
229
|
+
child.on("error", () => resolve3());
|
|
230
|
+
child.on("exit", () => resolve3());
|
|
231
|
+
child.unref();
|
|
232
|
+
} catch {
|
|
233
|
+
resolve3();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/cli-shared.ts
|
|
239
|
+
var API_TIMEOUT_MS = 15e3;
|
|
240
|
+
async function apiCall(path, config, options = {}) {
|
|
241
|
+
const endpoint = config.endpoint;
|
|
242
|
+
if (!endpoint) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
error: {
|
|
246
|
+
code: "NO_ENDPOINT",
|
|
247
|
+
message: "No API endpoint configured.\n Run: mushi login --api-key <key> --endpoint <url> --project-id <id>\n Or: export MUSHI_API_ENDPOINT=https://<project-ref>.supabase.co/functions/v1/api"
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
253
|
+
const signals = [controller.signal, getAbortSignal()];
|
|
254
|
+
const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
257
|
+
...options,
|
|
258
|
+
signal: compositeSignal,
|
|
259
|
+
headers: {
|
|
260
|
+
"Content-Type": "application/json",
|
|
261
|
+
// `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
|
|
262
|
+
// fallback accepted by some older middleware. Both are sent.
|
|
263
|
+
"Authorization": `Bearer ${config.apiKey ?? ""}`,
|
|
264
|
+
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
265
|
+
"X-Mushi-Project": config.projectId ?? "",
|
|
266
|
+
"X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
|
|
267
|
+
...options.headers
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
let body;
|
|
272
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
273
|
+
if (contentType.includes("application/json")) {
|
|
274
|
+
try {
|
|
275
|
+
body = await res.json();
|
|
276
|
+
} catch {
|
|
277
|
+
body = null;
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
const text2 = await res.text();
|
|
281
|
+
try {
|
|
282
|
+
body = JSON.parse(text2);
|
|
283
|
+
} catch {
|
|
284
|
+
body = {
|
|
285
|
+
ok: false,
|
|
286
|
+
error: {
|
|
287
|
+
code: `HTTP_${res.status}`,
|
|
288
|
+
message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
294
|
+
const b = body;
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
httpStatus: res.status,
|
|
298
|
+
error: {
|
|
299
|
+
code: b["code"] ?? `HTTP_${res.status}`,
|
|
300
|
+
message: b["message"] ?? `Request failed (${res.status})`
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return body;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
error: {
|
|
311
|
+
code: "TIMEOUT",
|
|
312
|
+
message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
error: {
|
|
319
|
+
code: "NETWORK_ERROR",
|
|
320
|
+
message: err instanceof Error ? err.message : String(err)
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function die(result, exitCode = 1) {
|
|
326
|
+
const { code, message } = result.error;
|
|
327
|
+
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
328
|
+
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
329
|
+
`);
|
|
330
|
+
process.exit(exitCode);
|
|
293
331
|
}
|
|
294
|
-
function
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (deps.has("react-native")) return FRAMEWORKS["react-native"];
|
|
302
|
-
if (deps.has("@capacitor/core") && deps.has("react")) return FRAMEWORKS.react;
|
|
303
|
-
if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
|
|
304
|
-
if (deps.has("svelte")) return FRAMEWORKS.svelte;
|
|
305
|
-
if (deps.has("vue")) return FRAMEWORKS.vue;
|
|
306
|
-
if (deps.has("react")) return FRAMEWORKS.react;
|
|
307
|
-
if (deps.has("express")) return FRAMEWORKS.express;
|
|
308
|
-
if (deps.has("fastify")) return FRAMEWORKS.fastify;
|
|
309
|
-
if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
|
|
310
|
-
if (existsSync(join(cwd, "next.config.js")) || existsSync(join(cwd, "next.config.ts"))) {
|
|
311
|
-
return FRAMEWORKS.next;
|
|
332
|
+
function requireConfig(opts = {}) {
|
|
333
|
+
const config = loadConfig();
|
|
334
|
+
if (!config.apiKey) {
|
|
335
|
+
process.stderr.write(
|
|
336
|
+
"error: API key not configured.\n Run: mushi login --api-key <key>\n Or: export MUSHI_API_KEY=<key>\n"
|
|
337
|
+
);
|
|
338
|
+
process.exit(2);
|
|
312
339
|
}
|
|
313
|
-
if (
|
|
314
|
-
|
|
340
|
+
if (!config.endpoint) {
|
|
341
|
+
config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
|
|
315
342
|
}
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
343
|
+
if (opts.needsProject && !config.projectId) {
|
|
344
|
+
process.stderr.write(
|
|
345
|
+
"error: Project ID not configured.\n Run: mushi login --project-id <uuid>\n Or: export MUSHI_PROJECT_ID=<uuid>\n Find your project ID: " + consoleUrl(resolveConsoleUrlSync(), "/projects") + "\n"
|
|
346
|
+
);
|
|
347
|
+
process.exit(2);
|
|
348
|
+
}
|
|
349
|
+
return config;
|
|
319
350
|
}
|
|
320
|
-
function
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
324
|
-
if (existsSync(join(cwd, "package-lock.json"))) return "npm";
|
|
325
|
-
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
326
|
-
if (userAgent.startsWith("bun")) return "bun";
|
|
327
|
-
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
328
|
-
if (userAgent.startsWith("yarn")) return "yarn";
|
|
329
|
-
return "npm";
|
|
351
|
+
function fmtDate(iso) {
|
|
352
|
+
if (!iso) return "\u2014";
|
|
353
|
+
return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
|
|
330
354
|
}
|
|
331
|
-
function
|
|
332
|
-
|
|
333
|
-
return `${pm} ${verb} ${packages.join(" ")}`;
|
|
355
|
+
function pad(s, width) {
|
|
356
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
334
357
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
358
|
+
|
|
359
|
+
// src/device-auth.ts
|
|
360
|
+
var DEVICE_FETCH_TIMEOUT_MS = 15e3;
|
|
361
|
+
function trimTrailingSlash(endpoint) {
|
|
362
|
+
let end = endpoint.length;
|
|
363
|
+
while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
|
|
364
|
+
return endpoint.slice(0, end);
|
|
365
|
+
}
|
|
366
|
+
async function deviceFetch(url, init = {}) {
|
|
367
|
+
const controller = new AbortController();
|
|
368
|
+
const timer = setTimeout(() => controller.abort(), DEVICE_FETCH_TIMEOUT_MS);
|
|
369
|
+
try {
|
|
370
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
371
|
+
} finally {
|
|
372
|
+
clearTimeout(timer);
|
|
342
373
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
374
|
+
}
|
|
375
|
+
async function startDeviceAuth(endpoint) {
|
|
376
|
+
const base = trimTrailingSlash(endpoint);
|
|
377
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { "Content-Type": "application/json" }
|
|
380
|
+
});
|
|
381
|
+
const json = await res.json().catch(() => null);
|
|
382
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
383
|
+
throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
|
|
348
384
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
385
|
+
return json.data;
|
|
386
|
+
}
|
|
387
|
+
async function pollDeviceToken(endpoint, deviceCode) {
|
|
388
|
+
const base = trimTrailingSlash(endpoint);
|
|
389
|
+
try {
|
|
390
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/token`, {
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { "Content-Type": "application/json" },
|
|
393
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
394
|
+
});
|
|
395
|
+
const json = await res.json().catch(() => ({}));
|
|
396
|
+
if (res.ok && json.ok && json.data?.cli_token) {
|
|
397
|
+
return { status: "approved", cliToken: json.data.cli_token, userId: json.data.user_id };
|
|
398
|
+
}
|
|
399
|
+
switch (json.error) {
|
|
400
|
+
case "authorization_pending":
|
|
401
|
+
return { status: "pending" };
|
|
402
|
+
case "access_denied":
|
|
403
|
+
return { status: "denied" };
|
|
404
|
+
case "expired_token":
|
|
405
|
+
return { status: "expired" };
|
|
406
|
+
default:
|
|
407
|
+
return {
|
|
408
|
+
status: "error",
|
|
409
|
+
message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
|
|
410
|
+
// 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
|
|
411
|
+
// rejection (already claimed / invalid_grant) that won't fix itself.
|
|
412
|
+
retryable: res.status >= 500
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
|
|
354
417
|
}
|
|
355
|
-
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
|
|
356
|
-
return [
|
|
357
|
-
`${prefix}MUSHI_PROJECT_ID=${projectId}`,
|
|
358
|
-
`${prefix}MUSHI_API_KEY=${apiKey}`
|
|
359
|
-
].join("\n");
|
|
360
418
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
419
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
420
|
+
async function waitForCliToken(endpoint, session, opts = {}) {
|
|
421
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
422
|
+
const now = opts.now ?? Date.now;
|
|
423
|
+
const intervalMs = (session.interval || 5) * 1e3;
|
|
424
|
+
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
425
|
+
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
426
|
+
let consecutiveErrors = 0;
|
|
427
|
+
while (now() < deadline) {
|
|
428
|
+
await sleep(intervalMs);
|
|
429
|
+
const outcome = await pollDeviceToken(endpoint, session.device_code);
|
|
430
|
+
switch (outcome.status) {
|
|
431
|
+
case "approved":
|
|
432
|
+
return outcome.cliToken;
|
|
433
|
+
case "pending":
|
|
434
|
+
consecutiveErrors = 0;
|
|
435
|
+
opts.onPending?.();
|
|
436
|
+
continue;
|
|
437
|
+
case "denied":
|
|
438
|
+
throw new Error("Login was denied in the browser.");
|
|
439
|
+
case "expired":
|
|
440
|
+
throw new Error("The login code expired. Run sign-in again.");
|
|
441
|
+
case "error":
|
|
442
|
+
if (!outcome.retryable) {
|
|
443
|
+
throw new Error(outcome.message);
|
|
444
|
+
}
|
|
445
|
+
consecutiveErrors += 1;
|
|
446
|
+
if (consecutiveErrors >= maxConsecutiveErrors) {
|
|
447
|
+
throw new Error(outcome.message);
|
|
448
|
+
}
|
|
449
|
+
opts.onTransientError?.(outcome.message, consecutiveErrors);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
throw new Error("Login timed out before approval. Run sign-in again.");
|
|
454
|
+
}
|
|
455
|
+
function cliAuthHeaders(cliToken) {
|
|
456
|
+
return {
|
|
457
|
+
"Content-Type": "application/json",
|
|
458
|
+
Authorization: `Bearer ${cliToken}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
async function listProjects(endpoint, cliToken) {
|
|
462
|
+
const base = trimTrailingSlash(endpoint);
|
|
463
|
+
try {
|
|
464
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
|
|
465
|
+
const json = await res.json().catch(() => null);
|
|
466
|
+
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
async function createProject(endpoint, cliToken, name) {
|
|
472
|
+
const base = trimTrailingSlash(endpoint);
|
|
473
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: cliAuthHeaders(cliToken),
|
|
476
|
+
body: JSON.stringify({ name })
|
|
477
|
+
});
|
|
478
|
+
const json = await res.json().catch(() => null);
|
|
479
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
480
|
+
throw new Error(json?.error?.message ?? `Could not create project (HTTP ${res.status}).`);
|
|
481
|
+
}
|
|
482
|
+
const { id, name: created, slug, apiKey } = json.data;
|
|
483
|
+
return { id, name: created, slug, apiKey: apiKey ?? null };
|
|
484
|
+
}
|
|
485
|
+
async function mintProjectKey(endpoint, cliToken, projectId) {
|
|
486
|
+
const base = trimTrailingSlash(endpoint);
|
|
487
|
+
try {
|
|
488
|
+
const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: cliAuthHeaders(cliToken),
|
|
491
|
+
body: JSON.stringify({})
|
|
492
|
+
});
|
|
493
|
+
const json = await res.json().catch(() => null);
|
|
494
|
+
if (res.ok && json?.ok) return json.data?.key ?? null;
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
368
498
|
}
|
|
369
499
|
|
|
370
500
|
// src/freshness.ts
|
|
@@ -428,7 +558,7 @@ function parse(version) {
|
|
|
428
558
|
|
|
429
559
|
// src/monorepo.ts
|
|
430
560
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
431
|
-
import { dirname, join as join2, resolve } from "path";
|
|
561
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
432
562
|
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
433
563
|
var FRAMEWORK_DEPS = {
|
|
434
564
|
next: "Next.js",
|
|
@@ -456,7 +586,7 @@ function findWorkspaceRoot(start) {
|
|
|
456
586
|
let dir = resolve(start);
|
|
457
587
|
for (let i = 0; i < 8; i++) {
|
|
458
588
|
if (isWorkspaceRoot(dir)) return dir;
|
|
459
|
-
const parent =
|
|
589
|
+
const parent = dirname2(dir);
|
|
460
590
|
if (parent === dir) break;
|
|
461
591
|
dir = parent;
|
|
462
592
|
}
|
|
@@ -547,7 +677,10 @@ async function runInit(options = {}) {
|
|
|
547
677
|
}
|
|
548
678
|
const detected = detectFramework(cwd, pkg);
|
|
549
679
|
const framework = await chooseFramework(detected, options);
|
|
550
|
-
const
|
|
680
|
+
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
681
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
682
|
+
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
683
|
+
await verifyCredentials(credentials, options, consoleBase);
|
|
551
684
|
const pm = detectPackageManager(cwd);
|
|
552
685
|
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
553
686
|
if (!options.skipInstall) {
|
|
@@ -555,12 +688,12 @@ async function runInit(options = {}) {
|
|
|
555
688
|
} else {
|
|
556
689
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
557
690
|
}
|
|
558
|
-
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
559
691
|
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
560
692
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
561
693
|
const enableRewards = await maybeEnableRewards(options);
|
|
562
|
-
printNextSteps(framework,
|
|
563
|
-
await maybeSendTestReport(credentials, { ...options, endpoint });
|
|
694
|
+
printNextSteps(framework, consoleBase, enableRewards);
|
|
695
|
+
await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
|
|
696
|
+
await maybeOfferConnect(credentials, options, consoleBase);
|
|
564
697
|
p.outro("Setup complete. Happy bug squashing \u{1F41B}");
|
|
565
698
|
}
|
|
566
699
|
function ensureInteractiveOrBailOut(options) {
|
|
@@ -601,25 +734,165 @@ async function chooseFramework(detected, options) {
|
|
|
601
734
|
}
|
|
602
735
|
return FRAMEWORKS[confirmed];
|
|
603
736
|
}
|
|
604
|
-
async function
|
|
737
|
+
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
738
|
+
if (options.projectId && options.apiKey) {
|
|
739
|
+
return {
|
|
740
|
+
projectId: sanitizeSecret(options.projectId),
|
|
741
|
+
apiKey: sanitizeSecret(options.apiKey)
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const existing = loadConfig();
|
|
745
|
+
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
746
|
+
const reuse = options.yes ? true : await p.confirm({
|
|
747
|
+
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
748
|
+
initialValue: true
|
|
749
|
+
});
|
|
750
|
+
if (p.isCancel(reuse)) {
|
|
751
|
+
p.cancel("Aborted.");
|
|
752
|
+
process.exit(0);
|
|
753
|
+
}
|
|
754
|
+
if (reuse) {
|
|
755
|
+
return {
|
|
756
|
+
projectId: sanitizeSecret(existing.projectId),
|
|
757
|
+
apiKey: sanitizeSecret(existing.apiKey)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (options.yes) {
|
|
762
|
+
const creds = await runBrowserSignIn(options, endpoint);
|
|
763
|
+
if (creds) return creds;
|
|
764
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
765
|
+
} else {
|
|
766
|
+
const method = await p.select({
|
|
767
|
+
message: "Connect this app to Mushi",
|
|
768
|
+
initialValue: "browser",
|
|
769
|
+
options: [
|
|
770
|
+
{
|
|
771
|
+
value: "browser",
|
|
772
|
+
label: "Sign in with your browser",
|
|
773
|
+
hint: "Recommended \u2014 no copy-paste, creates the project + key for you"
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
value: "manual",
|
|
777
|
+
label: "Paste a Project ID + API key",
|
|
778
|
+
hint: "Self-hosted or expert setup"
|
|
779
|
+
}
|
|
780
|
+
]
|
|
781
|
+
});
|
|
782
|
+
if (p.isCancel(method)) {
|
|
783
|
+
p.cancel("Aborted.");
|
|
784
|
+
process.exit(0);
|
|
785
|
+
}
|
|
786
|
+
if (method === "browser") {
|
|
787
|
+
const creds = await runBrowserSignIn(options, endpoint);
|
|
788
|
+
if (creds) return creds;
|
|
789
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return collectCredentialsManually(options, consoleBase);
|
|
793
|
+
}
|
|
794
|
+
async function runBrowserSignIn(options, endpoint) {
|
|
795
|
+
const startSpin = p.spinner();
|
|
796
|
+
startSpin.start("Starting secure browser sign-in\u2026");
|
|
797
|
+
let session;
|
|
798
|
+
try {
|
|
799
|
+
session = await startDeviceAuth(endpoint);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
startSpin.stop("Could not start browser sign-in.");
|
|
802
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
startSpin.stop("Browser sign-in ready.");
|
|
806
|
+
p.log.step(`Confirmation code: ${session.user_code}`);
|
|
807
|
+
p.log.info(`Opening ${session.verification_uri}`);
|
|
808
|
+
try {
|
|
809
|
+
await openInBrowser(session.verification_uri);
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
p.log.message("Approve the request in your browser to continue. (Ctrl+C to cancel)");
|
|
813
|
+
const waitSpin = p.spinner();
|
|
814
|
+
waitSpin.start("Waiting for you to approve in the browser\u2026");
|
|
815
|
+
let cliToken;
|
|
816
|
+
try {
|
|
817
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
818
|
+
} catch (err) {
|
|
819
|
+
waitSpin.stop("Browser sign-in didn't complete.");
|
|
820
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
waitSpin.stop("Approved.");
|
|
824
|
+
let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
|
|
825
|
+
let apiKey;
|
|
826
|
+
if (!projectId) {
|
|
827
|
+
let projects = [];
|
|
828
|
+
try {
|
|
829
|
+
projects = await listProjects(endpoint, cliToken);
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
const NEW = "__new__";
|
|
833
|
+
const choice = await p.select({
|
|
834
|
+
message: "Choose a project",
|
|
835
|
+
initialValue: projects[0]?.id ?? NEW,
|
|
836
|
+
options: [
|
|
837
|
+
...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
|
|
838
|
+
{ value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
|
|
839
|
+
]
|
|
840
|
+
});
|
|
841
|
+
if (p.isCancel(choice)) {
|
|
842
|
+
p.cancel("Aborted.");
|
|
843
|
+
process.exit(0);
|
|
844
|
+
}
|
|
845
|
+
if (choice === NEW) {
|
|
846
|
+
const name = await p.text({
|
|
847
|
+
message: "Project name",
|
|
848
|
+
placeholder: "My app",
|
|
849
|
+
validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
|
|
850
|
+
});
|
|
851
|
+
if (p.isCancel(name)) {
|
|
852
|
+
p.cancel("Aborted.");
|
|
853
|
+
process.exit(0);
|
|
854
|
+
}
|
|
855
|
+
const createSpin = p.spinner();
|
|
856
|
+
createSpin.start(`Creating "${name.trim()}"\u2026`);
|
|
857
|
+
try {
|
|
858
|
+
const created = await createProject(endpoint, cliToken, name.trim());
|
|
859
|
+
projectId = created.id;
|
|
860
|
+
apiKey = created.apiKey ?? void 0;
|
|
861
|
+
createSpin.stop(`Created project "${created.name}".`);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
createSpin.stop("Could not create the project.");
|
|
864
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
projectId = choice;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (projectId && !apiKey) {
|
|
872
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
|
|
873
|
+
}
|
|
874
|
+
if (!projectId || !apiKey) return null;
|
|
875
|
+
return { apiKey, projectId };
|
|
876
|
+
}
|
|
877
|
+
async function collectCredentialsManually(options, consoleBase) {
|
|
605
878
|
const existing = loadConfig();
|
|
606
879
|
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
607
880
|
message: "Project ID",
|
|
608
881
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
609
|
-
hint:
|
|
610
|
-
validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID
|
|
882
|
+
hint: projectIdHint(consoleBase),
|
|
883
|
+
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."
|
|
611
884
|
});
|
|
612
885
|
const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
|
|
613
886
|
message: "API key",
|
|
614
887
|
placeholder: "mushi_xxxxxxxxxxxx",
|
|
615
|
-
hint:
|
|
888
|
+
hint: apiKeyHint(consoleBase),
|
|
616
889
|
validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
|
|
617
890
|
});
|
|
618
891
|
const projectId = sanitizeSecret(rawProjectId);
|
|
619
892
|
const apiKey = sanitizeSecret(rawApiKey);
|
|
620
893
|
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
621
894
|
throw new Error(
|
|
622
|
-
`Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from
|
|
895
|
+
`Invalid project ID. Expected a UUID (e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4) or the proj_* prefixed form. Got: ${redact(projectId)} \u2014 copy it from ${projectIdHint(consoleBase)}`
|
|
623
896
|
);
|
|
624
897
|
}
|
|
625
898
|
if (!API_KEY_PATTERN.test(apiKey)) {
|
|
@@ -629,6 +902,23 @@ async function collectCredentials(options) {
|
|
|
629
902
|
}
|
|
630
903
|
return { projectId, apiKey };
|
|
631
904
|
}
|
|
905
|
+
async function verifyCredentials(credentials, options, consoleBase) {
|
|
906
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
907
|
+
const spinner3 = p.spinner();
|
|
908
|
+
spinner3.start("Verifying credentials\u2026");
|
|
909
|
+
const result = await apiCall(
|
|
910
|
+
"/v1/sync/whoami",
|
|
911
|
+
{ apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
|
|
912
|
+
);
|
|
913
|
+
if (!result.ok) {
|
|
914
|
+
spinner3.stop("Credentials could not be verified.");
|
|
915
|
+
p.log.error(result.error?.message ?? "Authentication failed.");
|
|
916
|
+
p.log.info(`Double-check values in the console:
|
|
917
|
+
${cliSetupDeepLink(consoleBase)}`);
|
|
918
|
+
throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
|
|
919
|
+
}
|
|
920
|
+
spinner3.stop(`Connected to ${result.data.project_name}`);
|
|
921
|
+
}
|
|
632
922
|
function sanitizeSecret(raw) {
|
|
633
923
|
return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[\r\n\0]/g, "");
|
|
634
924
|
}
|
|
@@ -671,7 +961,7 @@ async function installPackages(pm, packages, cwd) {
|
|
|
671
961
|
function runCommand(pm, packages, cwd) {
|
|
672
962
|
const verb = pm === "npm" ? "install" : "add";
|
|
673
963
|
const command = process.platform === "win32" ? `${pm}.cmd` : pm;
|
|
674
|
-
return new Promise((
|
|
964
|
+
return new Promise((resolve3, reject) => {
|
|
675
965
|
const child = spawn(command, [verb, ...packages], {
|
|
676
966
|
stdio: "inherit",
|
|
677
967
|
shell: false,
|
|
@@ -680,7 +970,7 @@ function runCommand(pm, packages, cwd) {
|
|
|
680
970
|
});
|
|
681
971
|
child.on("error", reject);
|
|
682
972
|
child.on("exit", (code) => {
|
|
683
|
-
if (code === 0)
|
|
973
|
+
if (code === 0) resolve3();
|
|
684
974
|
else reject(new Error(`${pm} exited with code ${code ?? "null"}`));
|
|
685
975
|
});
|
|
686
976
|
});
|
|
@@ -738,7 +1028,7 @@ function persistCliConfig(apiKey, projectId, endpoint) {
|
|
|
738
1028
|
const existing = loadConfig();
|
|
739
1029
|
saveConfig({ ...existing, apiKey, projectId, endpoint });
|
|
740
1030
|
}
|
|
741
|
-
function printNextSteps(framework,
|
|
1031
|
+
function printNextSteps(framework, consoleBase, enableRewards = false) {
|
|
742
1032
|
p.note(framework.snippet(), "Add this to your app:");
|
|
743
1033
|
if (enableRewards) {
|
|
744
1034
|
const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
|
|
@@ -749,13 +1039,44 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
749
1039
|
// import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
750
1040
|
// <MushiRewardsBadge showPoints />`;
|
|
751
1041
|
p.note(badgeSnippet, "Rewards badge snippet:");
|
|
752
|
-
p.log.info(
|
|
1042
|
+
p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
|
|
753
1043
|
p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
|
|
754
1044
|
}
|
|
755
1045
|
p.log.message("Verify the install:");
|
|
756
1046
|
p.log.message(" \u2022 Start your dev server");
|
|
757
1047
|
p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
|
|
758
|
-
p.log.message(
|
|
1048
|
+
p.log.message(` \u2022 Submit a test report \u2014 it should appear at ${reportsUrl(consoleBase)}`);
|
|
1049
|
+
p.log.info(
|
|
1050
|
+
"Tip: `mushi connect --write-env --wire-ide --wait` also wires Cursor MCP and waits for SDK heartbeat."
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
1054
|
+
if (options.yes) return;
|
|
1055
|
+
const answer = await p.confirm({
|
|
1056
|
+
message: "Run `mushi connect --write-env --wire-ide --wait` now? (SDK env + Cursor MCP + heartbeat check)",
|
|
1057
|
+
initialValue: false
|
|
1058
|
+
});
|
|
1059
|
+
if (p.isCancel(answer) || !answer) return;
|
|
1060
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1061
|
+
try {
|
|
1062
|
+
const { runConnect: runConnect2 } = await import("./connect-W2Q6N7K3.js");
|
|
1063
|
+
await runConnect2({
|
|
1064
|
+
apiKey: credentials.apiKey,
|
|
1065
|
+
projectId: credentials.projectId,
|
|
1066
|
+
endpoint,
|
|
1067
|
+
cwd: options.cwd,
|
|
1068
|
+
writeEnv: true,
|
|
1069
|
+
wireIde: true,
|
|
1070
|
+
wait: true
|
|
1071
|
+
});
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
p.log.warn(
|
|
1074
|
+
err instanceof Error ? err.message : String(err)
|
|
1075
|
+
);
|
|
1076
|
+
p.log.info(
|
|
1077
|
+
"You can run manually: mushi connect --write-env --wire-ide --wait"
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
759
1080
|
}
|
|
760
1081
|
async function maybeEnableRewards(options) {
|
|
761
1082
|
if (options.yes) return false;
|
|
@@ -826,8 +1147,8 @@ async function maybeSendTestReport(credentials, options) {
|
|
|
826
1147
|
reportId = body.data?.reportId;
|
|
827
1148
|
} catch {
|
|
828
1149
|
}
|
|
829
|
-
const
|
|
830
|
-
p.log.success(`View it at
|
|
1150
|
+
const consoleBase = options.consoleBase ?? resolveConsoleUrlSync(options.cwd);
|
|
1151
|
+
p.log.success(`View it at ${reportsUrl(consoleBase, reportId)}`);
|
|
831
1152
|
} catch (err) {
|
|
832
1153
|
const aborted = err instanceof Error && err.name === "AbortError";
|
|
833
1154
|
spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
|
|
@@ -1023,78 +1344,189 @@ function registerAccountCommands(program2) {
|
|
|
1023
1344
|
const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
|
|
1024
1345
|
if (matches.length === 0) process.exit(1);
|
|
1025
1346
|
});
|
|
1026
|
-
program2.command("login").description("Authenticate the CLI
|
|
1347
|
+
program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").addHelpText("after", `
|
|
1027
1348
|
Examples:
|
|
1028
|
-
mushi login
|
|
1029
|
-
mushi login --api-key mushi_xxx
|
|
1349
|
+
mushi login # browser-guided (recommended)
|
|
1350
|
+
mushi login --api-key mushi_xxx # non-interactive / CI
|
|
1030
1351
|
mushi login --api-key mushi_xxx --project-id <uuid>`).action(async (opts) => {
|
|
1031
|
-
const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
|
|
1032
|
-
const
|
|
1033
|
-
const
|
|
1034
|
-
const consoleUrl = "https://kensaur.us/mushi-mushi/projects";
|
|
1352
|
+
const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
|
|
1353
|
+
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
|
|
1354
|
+
const consoleBase = await resolveConsoleUrl();
|
|
1035
1355
|
let apiKey = opts.apiKey;
|
|
1036
1356
|
let projectId = opts.projectId;
|
|
1037
|
-
if (
|
|
1038
|
-
const
|
|
1357
|
+
if (apiKey) {
|
|
1358
|
+
const config2 = loadConfig();
|
|
1359
|
+
config2.apiKey = apiKey;
|
|
1360
|
+
config2.endpoint = endpoint;
|
|
1361
|
+
if (projectId) config2.projectId = projectId;
|
|
1362
|
+
config2.consoleUrl = consoleBase;
|
|
1363
|
+
saveConfig(config2);
|
|
1039
1364
|
console.log("");
|
|
1040
|
-
console.log("
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1365
|
+
console.log(" \u2713 Credentials saved.");
|
|
1366
|
+
const verifyResult = await apiCall(
|
|
1367
|
+
"/v1/sync/whoami",
|
|
1368
|
+
{ apiKey, endpoint, projectId }
|
|
1369
|
+
);
|
|
1370
|
+
if (!verifyResult.ok) {
|
|
1371
|
+
console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
|
|
1372
|
+
console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const d = verifyResult.data;
|
|
1376
|
+
if (d.project_name) {
|
|
1377
|
+
if (!projectId && d.project_id) {
|
|
1378
|
+
config2.projectId = d.project_id;
|
|
1379
|
+
saveConfig(config2);
|
|
1049
1380
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1381
|
+
console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
|
|
1382
|
+
}
|
|
1383
|
+
console.log(` \u2713 Endpoint: ${endpoint}`);
|
|
1384
|
+
console.log("");
|
|
1385
|
+
console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
console.log("");
|
|
1389
|
+
console.log(" Mushi login");
|
|
1390
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1391
|
+
let deviceData;
|
|
1392
|
+
try {
|
|
1393
|
+
deviceData = await startDeviceAuth(endpoint);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
process.stderr.write(`
|
|
1396
|
+
error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
|
|
1397
|
+
`);
|
|
1398
|
+
process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
|
|
1399
|
+
`);
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
const verifyUrl = deviceData.verification_uri;
|
|
1403
|
+
console.log("");
|
|
1404
|
+
console.log(` Your confirmation code: ${deviceData.user_code}`);
|
|
1405
|
+
console.log("");
|
|
1406
|
+
if (opts.browser !== false) {
|
|
1407
|
+
console.log(" Opening the Mushi console in your browser\u2026");
|
|
1408
|
+
try {
|
|
1409
|
+
await openInBrowser(verifyUrl);
|
|
1410
|
+
} catch {
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
console.log(` If the browser didn't open: ${verifyUrl}`);
|
|
1414
|
+
console.log("");
|
|
1415
|
+
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
1416
|
+
let cliToken = null;
|
|
1417
|
+
const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
|
|
1418
|
+
const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
|
|
1419
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
1420
|
+
let consecutiveErrors = 0;
|
|
1421
|
+
while (Date.now() < deadline) {
|
|
1422
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1423
|
+
const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
|
|
1424
|
+
if (outcome.status === "approved") {
|
|
1425
|
+
cliToken = outcome.cliToken;
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
if (outcome.status === "pending") {
|
|
1429
|
+
consecutiveErrors = 0;
|
|
1430
|
+
process.stdout.write(".");
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (outcome.status === "error") {
|
|
1434
|
+
if (outcome.retryable) {
|
|
1435
|
+
consecutiveErrors += 1;
|
|
1436
|
+
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) {
|
|
1437
|
+
process.stdout.write("\xB7");
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
console.log("");
|
|
1442
|
+
process.stderr.write(`
|
|
1443
|
+
error: ${outcome.message}
|
|
1444
|
+
`);
|
|
1445
|
+
process.exit(1);
|
|
1052
1446
|
}
|
|
1053
1447
|
console.log("");
|
|
1054
|
-
|
|
1055
|
-
|
|
1448
|
+
if (outcome.status === "denied") {
|
|
1449
|
+
process.stderr.write("\nerror: Login denied in the browser.\n");
|
|
1450
|
+
} else if (outcome.status === "expired") {
|
|
1451
|
+
process.stderr.write("\nerror: Login code expired. Run mushi login again.\n");
|
|
1452
|
+
}
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1455
|
+
if (!cliToken) {
|
|
1056
1456
|
console.log("");
|
|
1457
|
+
process.stderr.write("\nerror: Login timed out. Run mushi login again.\n");
|
|
1458
|
+
process.exit(1);
|
|
1459
|
+
}
|
|
1460
|
+
console.log("");
|
|
1461
|
+
console.log(" \u2713 Approved!");
|
|
1462
|
+
const projectsList = await listProjects(endpoint, cliToken);
|
|
1463
|
+
let chosenProjectId = projectId;
|
|
1464
|
+
let chosenProjectName;
|
|
1465
|
+
if (!chosenProjectId) {
|
|
1057
1466
|
const { createInterface } = await import("readline");
|
|
1058
1467
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1059
|
-
const ask = (q) => new Promise((
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1468
|
+
const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
|
|
1469
|
+
if (projectsList.length > 0) {
|
|
1470
|
+
console.log("");
|
|
1471
|
+
console.log(" Your projects:");
|
|
1472
|
+
projectsList.forEach((p3, i) => {
|
|
1473
|
+
console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
|
|
1474
|
+
});
|
|
1475
|
+
console.log(` ${projectsList.length + 1}. Create a new project`);
|
|
1476
|
+
console.log("");
|
|
1477
|
+
const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
|
|
1478
|
+
const num = parseInt(choice, 10);
|
|
1479
|
+
if (num >= 1 && num <= projectsList.length) {
|
|
1480
|
+
const picked = projectsList[num - 1];
|
|
1481
|
+
chosenProjectId = picked.id;
|
|
1482
|
+
chosenProjectName = picked.name;
|
|
1483
|
+
}
|
|
1064
1484
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1485
|
+
if (!chosenProjectId) {
|
|
1486
|
+
console.log("");
|
|
1487
|
+
const newName = await ask(" Project name: ");
|
|
1488
|
+
rl.close();
|
|
1489
|
+
if (!newName.trim()) {
|
|
1490
|
+
process.stderr.write("\nerror: Project name is required.\n");
|
|
1491
|
+
process.exit(2);
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
const created = await createProject(endpoint, cliToken, newName.trim());
|
|
1495
|
+
chosenProjectId = created.id;
|
|
1496
|
+
chosenProjectName = created.name;
|
|
1497
|
+
apiKey = created.apiKey ?? void 0;
|
|
1498
|
+
console.log(` \u2713 Created project "${chosenProjectName}"`);
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
process.stderr.write(`
|
|
1501
|
+
error: Could not create project: ${err instanceof Error ? err.message : String(err)}
|
|
1502
|
+
`);
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
}
|
|
1505
|
+
} else {
|
|
1506
|
+
rl.close();
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (!apiKey && chosenProjectId) {
|
|
1510
|
+
try {
|
|
1511
|
+
apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
|
|
1512
|
+
} catch {
|
|
1069
1513
|
}
|
|
1070
1514
|
}
|
|
1071
1515
|
const config = loadConfig();
|
|
1072
|
-
config.apiKey = apiKey;
|
|
1073
1516
|
config.endpoint = endpoint;
|
|
1074
|
-
|
|
1517
|
+
config.consoleUrl = consoleBase;
|
|
1518
|
+
if (chosenProjectId) config.projectId = chosenProjectId;
|
|
1519
|
+
if (apiKey) config.apiKey = apiKey;
|
|
1075
1520
|
saveConfig(config);
|
|
1076
1521
|
console.log("");
|
|
1077
|
-
console.log(
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (!verifyResult.ok) {
|
|
1083
|
-
console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
|
|
1084
|
-
console.warn(` Check your key at: ${consoleUrl}`);
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
const d = verifyResult.data;
|
|
1088
|
-
if (!projectId && d.project_id) {
|
|
1089
|
-
config.projectId = d.project_id;
|
|
1090
|
-
saveConfig(config);
|
|
1091
|
-
console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
|
|
1092
|
-
} else if (d.project_name) {
|
|
1093
|
-
console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
|
|
1522
|
+
if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
|
|
1523
|
+
if (apiKey) {
|
|
1524
|
+
console.log(` \u2713 SDK key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
1525
|
+
} else {
|
|
1526
|
+
console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
|
|
1094
1527
|
}
|
|
1095
|
-
console.log(` \u2713 Endpoint: ${endpoint}`);
|
|
1096
1528
|
console.log("");
|
|
1097
|
-
console.log(" Run 'mushi
|
|
1529
|
+
console.log(" Run 'mushi init' to set up the SDK in this project.");
|
|
1098
1530
|
});
|
|
1099
1531
|
program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1100
1532
|
Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
|
|
@@ -1617,7 +2049,7 @@ AI code review context.
|
|
|
1617
2049
|
Typical CI usage:
|
|
1618
2050
|
MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
|
|
1619
2051
|
npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
|
|
1620
|
-
const { writeFile
|
|
2052
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
1621
2053
|
const nodePath = await import("path");
|
|
1622
2054
|
const config = requireConfig();
|
|
1623
2055
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -1644,8 +2076,8 @@ Typical CI usage:
|
|
|
1644
2076
|
console.log(JSON.stringify(output, null, 2));
|
|
1645
2077
|
return;
|
|
1646
2078
|
}
|
|
1647
|
-
await
|
|
1648
|
-
await
|
|
2079
|
+
await mkdir(nodePath.dirname(target), { recursive: true });
|
|
2080
|
+
await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
1649
2081
|
if (opts.json) {
|
|
1650
2082
|
console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
|
|
1651
2083
|
} else {
|
|
@@ -1677,11 +2109,11 @@ async function findMapFiles(dir) {
|
|
|
1677
2109
|
return results;
|
|
1678
2110
|
}
|
|
1679
2111
|
function fileHash(path) {
|
|
1680
|
-
return new Promise((
|
|
2112
|
+
return new Promise((resolve3, reject) => {
|
|
1681
2113
|
const hash = createHash("sha256");
|
|
1682
2114
|
const stream = createReadStream(path);
|
|
1683
2115
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
1684
|
-
stream.on("end", () =>
|
|
2116
|
+
stream.on("end", () => resolve3(hash.digest("hex")));
|
|
1685
2117
|
stream.on("error", reject);
|
|
1686
2118
|
});
|
|
1687
2119
|
}
|
|
@@ -1828,7 +2260,7 @@ function registerDiagnosticsCommands(program2) {
|
|
|
1828
2260
|
console.log(`\u2713 Test report submitted`);
|
|
1829
2261
|
console.log(` ID: ${d.reportId}`);
|
|
1830
2262
|
console.log(` Status: ${d.status}`);
|
|
1831
|
-
console.log(` View:
|
|
2263
|
+
console.log(` View: ${reportsUrl(resolveConsoleUrlSync(), d.reportId)}`);
|
|
1832
2264
|
}
|
|
1833
2265
|
});
|
|
1834
2266
|
program2.command("index <path>").description("Walk a local repo and upload code chunks to the Mushi RAG indexer").option("--language <lang>", "Limit to one language: ts, tsx, js, py, go, rs").option("--dry-run", "Show what would be uploaded without sending").option("--json", "Machine-readable summary: { files, bytes }").addHelpText("after", `
|
|
@@ -1840,7 +2272,7 @@ Examples:
|
|
|
1840
2272
|
mushi index ./src
|
|
1841
2273
|
mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
|
|
1842
2274
|
const config = requireConfig({ needsProject: true });
|
|
1843
|
-
const { readdir: readdir2, readFile:
|
|
2275
|
+
const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
|
|
1844
2276
|
const nodePath = await import("path");
|
|
1845
2277
|
const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
|
|
1846
2278
|
const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
@@ -1867,7 +2299,7 @@ Examples:
|
|
|
1867
2299
|
`);
|
|
1868
2300
|
continue;
|
|
1869
2301
|
}
|
|
1870
|
-
const source = await
|
|
2302
|
+
const source = await readFile2(file, "utf8");
|
|
1871
2303
|
const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
|
|
1872
2304
|
count++;
|
|
1873
2305
|
bytes += source.length;
|
|
@@ -1914,108 +2346,127 @@ Examples:
|
|
|
1914
2346
|
});
|
|
1915
2347
|
}
|
|
1916
2348
|
|
|
1917
|
-
// src/mcp-config.ts
|
|
1918
|
-
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1919
|
-
import { dirname as dirname2 } from "path";
|
|
1920
|
-
var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
|
|
1921
|
-
async function writeMcpServerEntry(opts) {
|
|
1922
|
-
const { configPath, serverName, serverBlock, dryRun = false } = opts;
|
|
1923
|
-
let merged = { mcpServers: {} };
|
|
1924
|
-
let created = false;
|
|
1925
|
-
try {
|
|
1926
|
-
const existing = await readFile2(configPath, "utf8");
|
|
1927
|
-
try {
|
|
1928
|
-
merged = JSON.parse(existing);
|
|
1929
|
-
} catch {
|
|
1930
|
-
merged = { mcpServers: {} };
|
|
1931
|
-
}
|
|
1932
|
-
} catch {
|
|
1933
|
-
created = true;
|
|
1934
|
-
merged = { mcpServers: {} };
|
|
1935
|
-
}
|
|
1936
|
-
const servers = merged.mcpServers ?? {};
|
|
1937
|
-
servers[serverName] = serverBlock;
|
|
1938
|
-
merged.mcpServers = servers;
|
|
1939
|
-
const output = JSON.stringify(merged, null, 2) + "\n";
|
|
1940
|
-
if (!dryRun) {
|
|
1941
|
-
await mkdir(dirname2(configPath), { recursive: true });
|
|
1942
|
-
await writeFile(configPath, output, "utf8");
|
|
1943
|
-
}
|
|
1944
|
-
return { created, path: configPath };
|
|
1945
|
-
}
|
|
1946
|
-
function buildMcpServerBlock(opts) {
|
|
1947
|
-
return {
|
|
1948
|
-
command: "npx",
|
|
1949
|
-
args: ["-y", "@mushi-mushi/mcp@latest"],
|
|
1950
|
-
env: {
|
|
1951
|
-
MUSHI_API_ENDPOINT: opts.endpoint,
|
|
1952
|
-
MUSHI_PROJECT_ID: opts.projectId,
|
|
1953
|
-
MUSHI_API_KEY: opts.apiKey,
|
|
1954
|
-
MUSHI_FEATURES: DEFAULT_MUSHI_FEATURES
|
|
1955
|
-
}
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
function buildMcpServerName(opts) {
|
|
1959
|
-
if (opts.legacy) return "mushi";
|
|
1960
|
-
if (opts.projectName) {
|
|
1961
|
-
const slug = opts.projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
1962
|
-
return `mushi-${slug}`;
|
|
1963
|
-
}
|
|
1964
|
-
return `mushi-${(opts.projectId ?? "unknown").slice(0, 8)}`;
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
2349
|
// src/commands/project.ts
|
|
1968
2350
|
function registerProjectCommands(program2) {
|
|
1969
2351
|
const project = program2.command("project").description("Project management");
|
|
1970
|
-
project.command("create").description("Create a
|
|
1971
|
-
|
|
1972
|
-
and writes the following to the current directory:
|
|
2352
|
+
project.command("create").description("Create or select a Mushi project via browser sign-in, then write config files").option("--name <name>", "Project name for a new project (skips the prompt)").option("--no-browser", "Print the verification URL instead of opening the browser").option("--endpoint <url>", "Override API endpoint (self-hosted)").addHelpText("after", `
|
|
2353
|
+
Signs you in through the browser (no copy-paste), creates or selects a project,
|
|
2354
|
+
mints a report:write SDK key, and writes the following to the current directory:
|
|
1973
2355
|
.env.local \u2014 MUSHI_API_KEY, MUSHI_PROJECT_ID, MUSHI_API_ENDPOINT
|
|
1974
2356
|
.cursor/mcp.json \u2014 pre-filled mcpServers.mushi block for Cursor
|
|
1975
2357
|
|
|
1976
2358
|
Typical first-time flow:
|
|
1977
2359
|
npx mushi-mushi project create
|
|
1978
|
-
# Browser opens \u2192
|
|
2360
|
+
# Browser opens \u2192 click Approve \u2192 pick or create a project
|
|
1979
2361
|
# CLI writes .env.local and .cursor/mcp.json
|
|
1980
2362
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
1981
|
-
const { writeFile
|
|
2363
|
+
const { writeFile } = await import("fs/promises");
|
|
1982
2364
|
const { existsSync: existsSync4 } = await import("fs");
|
|
1983
2365
|
const nodePath = await import("path");
|
|
1984
|
-
const
|
|
1985
|
-
const
|
|
2366
|
+
const savedConfig = loadConfig();
|
|
2367
|
+
const endpoint = resolveCloudEndpoint(
|
|
2368
|
+
opts.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? savedConfig.endpoint
|
|
2369
|
+
);
|
|
2370
|
+
const consoleBase = await resolveConsoleUrl();
|
|
1986
2371
|
console.log("");
|
|
1987
2372
|
console.log(" Mushi project create");
|
|
1988
2373
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1989
2374
|
console.log("");
|
|
2375
|
+
let session;
|
|
2376
|
+
try {
|
|
2377
|
+
session = await startDeviceAuth(endpoint);
|
|
2378
|
+
} catch (err) {
|
|
2379
|
+
process.stderr.write(`
|
|
2380
|
+
error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
|
|
2381
|
+
`);
|
|
2382
|
+
process.stderr.write(" Fallback: mushi login --api-key <key> --project-id <uuid>\n");
|
|
2383
|
+
process.exit(1);
|
|
2384
|
+
}
|
|
2385
|
+
console.log(` Confirmation code: ${session.user_code}`);
|
|
2386
|
+
console.log("");
|
|
1990
2387
|
if (opts.browser !== false) {
|
|
1991
|
-
console.log("
|
|
2388
|
+
console.log(" Opening the Mushi console in your browser\u2026");
|
|
1992
2389
|
try {
|
|
1993
|
-
|
|
1994
|
-
const openCmd = process.platform === "win32" ? `start "" "${signUpUrl}"` : process.platform === "darwin" ? `open "${signUpUrl}"` : `xdg-open "${signUpUrl}"`;
|
|
1995
|
-
exec(openCmd);
|
|
2390
|
+
await openInBrowser(session.verification_uri);
|
|
1996
2391
|
} catch {
|
|
1997
2392
|
}
|
|
1998
|
-
} else {
|
|
1999
|
-
console.log(` 1. Sign up or log in at: ${signUpUrl}`);
|
|
2000
2393
|
}
|
|
2394
|
+
console.log(` If the browser didn't open: ${session.verification_uri}`);
|
|
2001
2395
|
console.log("");
|
|
2002
|
-
console.log("
|
|
2003
|
-
|
|
2396
|
+
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
2397
|
+
let cliToken;
|
|
2398
|
+
try {
|
|
2399
|
+
cliToken = await waitForCliToken(endpoint, session, {
|
|
2400
|
+
onPending: () => process.stdout.write("."),
|
|
2401
|
+
onTransientError: () => process.stdout.write("\xB7")
|
|
2402
|
+
});
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
console.log("");
|
|
2405
|
+
process.stderr.write(`
|
|
2406
|
+
error: ${err instanceof Error ? err.message : String(err)}
|
|
2407
|
+
`);
|
|
2408
|
+
process.exit(1);
|
|
2409
|
+
}
|
|
2004
2410
|
console.log("");
|
|
2411
|
+
console.log(" \u2713 Approved!");
|
|
2412
|
+
const projectsList = await listProjects(endpoint, cliToken);
|
|
2005
2413
|
const { createInterface } = await import("readline");
|
|
2006
2414
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2007
|
-
const ask = (q) => new Promise((
|
|
2008
|
-
|
|
2009
|
-
|
|
2415
|
+
const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
|
|
2416
|
+
let chosen;
|
|
2417
|
+
let projectId;
|
|
2418
|
+
let projectName;
|
|
2419
|
+
let apiKey;
|
|
2420
|
+
if (!opts.name && projectsList.length > 0) {
|
|
2421
|
+
console.log("");
|
|
2422
|
+
console.log(" Your projects:");
|
|
2423
|
+
projectsList.forEach((pr, i) => {
|
|
2424
|
+
console.log(` ${i + 1}. ${pr.name} (${pr.id})`);
|
|
2425
|
+
});
|
|
2426
|
+
console.log(` ${projectsList.length + 1}. Create a new project`);
|
|
2427
|
+
console.log("");
|
|
2428
|
+
const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
|
|
2429
|
+
const num = parseInt(choice, 10);
|
|
2430
|
+
if (num >= 1 && num <= projectsList.length) {
|
|
2431
|
+
chosen = projectsList[num - 1];
|
|
2432
|
+
projectId = chosen.id;
|
|
2433
|
+
projectName = chosen.name;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (!projectId) {
|
|
2437
|
+
const newName = opts.name?.trim() || await ask(" Project name: ");
|
|
2438
|
+
if (!newName) {
|
|
2439
|
+
rl.close();
|
|
2440
|
+
process.stderr.write("\nerror: Project name is required.\n");
|
|
2441
|
+
process.exit(2);
|
|
2442
|
+
}
|
|
2443
|
+
try {
|
|
2444
|
+
const created = await createProject(endpoint, cliToken, newName);
|
|
2445
|
+
projectId = created.id;
|
|
2446
|
+
projectName = created.name;
|
|
2447
|
+
apiKey = created.apiKey ?? void 0;
|
|
2448
|
+
console.log(` \u2713 Created project "${projectName}"`);
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
rl.close();
|
|
2451
|
+
process.stderr.write(`
|
|
2452
|
+
error: Could not create project: ${err instanceof Error ? err.message : String(err)}
|
|
2453
|
+
`);
|
|
2454
|
+
process.exit(1);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2010
2457
|
rl.close();
|
|
2458
|
+
if (projectId && !apiKey) {
|
|
2459
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
|
|
2460
|
+
}
|
|
2011
2461
|
if (!projectId || !apiKey) {
|
|
2012
|
-
process.stderr.write("\nerror:
|
|
2013
|
-
process.exit(
|
|
2462
|
+
process.stderr.write("\nerror: Could not obtain an SDK key for the project. Run `mushi login` and try again.\n");
|
|
2463
|
+
process.exit(1);
|
|
2014
2464
|
}
|
|
2015
2465
|
const config = loadConfig();
|
|
2016
2466
|
config.apiKey = apiKey;
|
|
2017
2467
|
config.endpoint = endpoint;
|
|
2018
2468
|
config.projectId = projectId;
|
|
2469
|
+
config.consoleUrl = consoleBase;
|
|
2019
2470
|
saveConfig(config);
|
|
2020
2471
|
const cwd = process.cwd();
|
|
2021
2472
|
const envPath = nodePath.join(cwd, ".env.local");
|
|
@@ -2027,7 +2478,7 @@ Typical first-time flow:
|
|
|
2027
2478
|
""
|
|
2028
2479
|
];
|
|
2029
2480
|
const envExisting = existsSync4(envPath);
|
|
2030
|
-
await
|
|
2481
|
+
await writeFile(envPath, envLines.join("\n"), "utf8");
|
|
2031
2482
|
console.log(`
|
|
2032
2483
|
\u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
|
|
2033
2484
|
const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
|
|
@@ -2044,7 +2495,7 @@ Typical first-time flow:
|
|
|
2044
2495
|
|
|
2045
2496
|
// src/commands/setup.ts
|
|
2046
2497
|
function registerSetupCommands(program2) {
|
|
2047
|
-
program2.command("setup").description("Wire
|
|
2498
|
+
program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes").addHelpText("after", `
|
|
2048
2499
|
Examples:
|
|
2049
2500
|
mushi setup # wire Cursor (default)
|
|
2050
2501
|
mushi setup --all-projects # one server entry per accessible project
|
|
@@ -2058,7 +2509,7 @@ Supported IDEs:
|
|
|
2058
2509
|
zed \u2014 writes ~/.config/zed/settings.json mcpServers block
|
|
2059
2510
|
|
|
2060
2511
|
The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
|
|
2061
|
-
const { writeFile
|
|
2512
|
+
const { writeFile, mkdir, readFile: readFile2 } = await import("fs/promises");
|
|
2062
2513
|
const { existsSync: existsSync4 } = await import("fs");
|
|
2063
2514
|
const nodePath = await import("path");
|
|
2064
2515
|
const os = await import("os");
|
|
@@ -2173,7 +2624,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2173
2624
|
let settings = {};
|
|
2174
2625
|
if (existsSync4(configPath)) {
|
|
2175
2626
|
try {
|
|
2176
|
-
const raw = await
|
|
2627
|
+
const raw = await readFile2(configPath, "utf8");
|
|
2177
2628
|
settings = JSON.parse(raw);
|
|
2178
2629
|
} catch {
|
|
2179
2630
|
}
|
|
@@ -2197,8 +2648,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2197
2648
|
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2198
2649
|
console.log(redactKeyForDisplay(output));
|
|
2199
2650
|
} else {
|
|
2200
|
-
await
|
|
2201
|
-
await
|
|
2651
|
+
await mkdir(configDir, { recursive: true });
|
|
2652
|
+
await writeFile(configPath, output, "utf8");
|
|
2202
2653
|
console.log(`\u2713 Written ${configPath}`);
|
|
2203
2654
|
}
|
|
2204
2655
|
}
|
|
@@ -2232,7 +2683,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2232
2683
|
if (opts.dryRun) {
|
|
2233
2684
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2234
2685
|
} else {
|
|
2235
|
-
await
|
|
2686
|
+
await writeFile(rulesPath, rulesContent, "utf8");
|
|
2236
2687
|
console.log(`\u2713 Written .cursorrules`);
|
|
2237
2688
|
}
|
|
2238
2689
|
} else if (opts.ide === "claude") {
|
|
@@ -2241,8 +2692,8 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2241
2692
|
if (opts.dryRun) {
|
|
2242
2693
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2243
2694
|
} else {
|
|
2244
|
-
await
|
|
2245
|
-
await
|
|
2695
|
+
await mkdir(rulesDir, { recursive: true });
|
|
2696
|
+
await writeFile(rulesPath, rulesContent, "utf8");
|
|
2246
2697
|
console.log(`\u2713 Written .claude/rules/mushi.md`);
|
|
2247
2698
|
}
|
|
2248
2699
|
}
|
|
@@ -2370,117 +2821,6 @@ function renderNudgeExplainer(phase) {
|
|
|
2370
2821
|
return lines.join("\n");
|
|
2371
2822
|
}
|
|
2372
2823
|
|
|
2373
|
-
// src/sanitize-config.ts
|
|
2374
|
-
var PROJECT_ID_RE = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
2375
|
-
var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
|
|
2376
|
-
function sanitizeApiKey(raw) {
|
|
2377
|
-
const key = raw.replace(/[\r\n\0]/g, "");
|
|
2378
|
-
if (!API_KEY_RE.test(key)) {
|
|
2379
|
-
throw new Error(
|
|
2380
|
-
"Invalid API key in config \u2014 run `mushi login --api-key <key>` to refresh credentials."
|
|
2381
|
-
);
|
|
2382
|
-
}
|
|
2383
|
-
return key;
|
|
2384
|
-
}
|
|
2385
|
-
function sanitizeProjectId(raw) {
|
|
2386
|
-
const id = raw.trim();
|
|
2387
|
-
if (!PROJECT_ID_RE.test(id)) {
|
|
2388
|
-
throw new Error(
|
|
2389
|
-
"Invalid project ID in config \u2014 expected a UUID or proj_* slug from the admin console."
|
|
2390
|
-
);
|
|
2391
|
-
}
|
|
2392
|
-
return id;
|
|
2393
|
-
}
|
|
2394
|
-
function sanitizeEndpoint(raw) {
|
|
2395
|
-
return assertEndpoint(normalizeEndpoint(raw));
|
|
2396
|
-
}
|
|
2397
|
-
function sanitizeCliCredentials(config) {
|
|
2398
|
-
if (!config.endpoint || !config.apiKey || !config.projectId) {
|
|
2399
|
-
throw new Error("Missing endpoint, apiKey, or projectId");
|
|
2400
|
-
}
|
|
2401
|
-
return {
|
|
2402
|
-
endpoint: sanitizeEndpoint(config.endpoint),
|
|
2403
|
-
apiKey: sanitizeApiKey(config.apiKey),
|
|
2404
|
-
projectId: sanitizeProjectId(config.projectId)
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
function apiKeyHeaders(apiKey, projectId) {
|
|
2408
|
-
const headers = {
|
|
2409
|
-
Authorization: `Bearer ${apiKey}`,
|
|
2410
|
-
"X-Mushi-Api-Key": apiKey
|
|
2411
|
-
};
|
|
2412
|
-
if (projectId) headers["X-Mushi-Project"] = projectId;
|
|
2413
|
-
return headers;
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
// src/heartbeat-wait.ts
|
|
2417
|
-
var IngestSetupHttpError = class extends Error {
|
|
2418
|
-
status;
|
|
2419
|
-
constructor(status) {
|
|
2420
|
-
super(`ingest-setup HTTP ${status}`);
|
|
2421
|
-
this.name = "IngestSetupHttpError";
|
|
2422
|
-
this.status = status;
|
|
2423
|
-
}
|
|
2424
|
-
};
|
|
2425
|
-
var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
|
|
2426
|
-
async function fetchIngestSetup(config, doFetch = globalThis.fetch) {
|
|
2427
|
-
const endpoint = sanitizeEndpoint(config.endpoint);
|
|
2428
|
-
const apiKey = sanitizeApiKey(config.apiKey);
|
|
2429
|
-
const projectId = config.projectId ? sanitizeProjectId(config.projectId) : void 0;
|
|
2430
|
-
const res = await doFetch(`${endpoint}/v1/sync/ingest-setup`, {
|
|
2431
|
-
headers: apiKeyHeaders(apiKey, projectId),
|
|
2432
|
-
signal: AbortSignal.timeout(8e3)
|
|
2433
|
-
});
|
|
2434
|
-
if (!res.ok) {
|
|
2435
|
-
if (NON_RETRYABLE_STATUSES.has(res.status)) throw new IngestSetupHttpError(res.status);
|
|
2436
|
-
return null;
|
|
2437
|
-
}
|
|
2438
|
-
const body = await res.json();
|
|
2439
|
-
return body.ok && body.data ? body.data : null;
|
|
2440
|
-
}
|
|
2441
|
-
async function waitForIngestReady(options) {
|
|
2442
|
-
const doFetch = options.fetch ?? globalThis.fetch;
|
|
2443
|
-
const maxAttempts = options.maxAttempts ?? 40;
|
|
2444
|
-
const intervalMs = options.intervalMs ?? 3e3;
|
|
2445
|
-
let lastPayload = null;
|
|
2446
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2447
|
-
if (options.signal?.aborted) {
|
|
2448
|
-
return { ok: false, payload: lastPayload, attempts: attempt - 1, reason: "aborted" };
|
|
2449
|
-
}
|
|
2450
|
-
try {
|
|
2451
|
-
lastPayload = await fetchIngestSetup(
|
|
2452
|
-
{ endpoint: options.endpoint, apiKey: options.apiKey, projectId: options.projectId },
|
|
2453
|
-
doFetch
|
|
2454
|
-
);
|
|
2455
|
-
if (lastPayload) {
|
|
2456
|
-
options.onPoll?.(lastPayload, attempt);
|
|
2457
|
-
if (lastPayload.ready) {
|
|
2458
|
-
return { ok: true, payload: lastPayload, attempts: attempt, reason: "ready" };
|
|
2459
|
-
}
|
|
2460
|
-
const sdkStep = lastPayload.steps.find((s) => s.id === "sdk_installed");
|
|
2461
|
-
if (sdkStep?.complete) {
|
|
2462
|
-
return { ok: true, payload: lastPayload, attempts: attempt, reason: "heartbeat" };
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
} catch (err) {
|
|
2466
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2467
|
-
if (err instanceof IngestSetupHttpError) {
|
|
2468
|
-
return { ok: false, payload: lastPayload, attempts: attempt, reason: "unauthorized", error: msg };
|
|
2469
|
-
}
|
|
2470
|
-
if (options.signal?.aborted) {
|
|
2471
|
-
return { ok: false, payload: lastPayload, attempts: attempt, reason: "aborted", error: msg };
|
|
2472
|
-
}
|
|
2473
|
-
if (attempt === maxAttempts) {
|
|
2474
|
-
return { ok: false, payload: lastPayload, attempts: attempt, reason: "fetch-error", error: msg };
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
if (attempt < maxAttempts) {
|
|
2478
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
return { ok: false, payload: lastPayload, attempts: maxAttempts, reason: "timeout" };
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
2824
|
// src/doctor.ts
|
|
2485
2825
|
function checkCliConfig(config) {
|
|
2486
2826
|
return [
|
|
@@ -2519,11 +2859,11 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
|
2519
2859
|
}
|
|
2520
2860
|
async function checkSdkInstall(cwd) {
|
|
2521
2861
|
try {
|
|
2522
|
-
const { readFile:
|
|
2523
|
-
const { join:
|
|
2524
|
-
const root =
|
|
2525
|
-
const pkgPath =
|
|
2526
|
-
const pkg = JSON.parse(await
|
|
2862
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
2863
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
2864
|
+
const root = resolve3(cwd);
|
|
2865
|
+
const pkgPath = join5(root, "package.json");
|
|
2866
|
+
const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
2527
2867
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2528
2868
|
const sdks = [
|
|
2529
2869
|
"@mushi-mushi/react",
|
|
@@ -2531,11 +2871,11 @@ async function checkSdkInstall(cwd) {
|
|
|
2531
2871
|
"@mushi-mushi/core",
|
|
2532
2872
|
"@mushi-mushi/react-native"
|
|
2533
2873
|
];
|
|
2534
|
-
const
|
|
2874
|
+
const installed2 = sdks.filter((s) => deps[s]);
|
|
2535
2875
|
return {
|
|
2536
2876
|
name: "SDK installed in this repo",
|
|
2537
|
-
ok:
|
|
2538
|
-
detail:
|
|
2877
|
+
ok: installed2.length > 0,
|
|
2878
|
+
detail: installed2.length > 0 ? installed2.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
|
|
2539
2879
|
};
|
|
2540
2880
|
} catch {
|
|
2541
2881
|
return null;
|
|
@@ -2701,18 +3041,18 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
|
2701
3041
|
async function checkHostAppWiring(cwd) {
|
|
2702
3042
|
const checks = [];
|
|
2703
3043
|
try {
|
|
2704
|
-
const { readFile:
|
|
2705
|
-
const { join:
|
|
2706
|
-
const root =
|
|
2707
|
-
const pkgPath =
|
|
2708
|
-
const pkg = JSON.parse(await
|
|
3044
|
+
const { readFile: readFile2, access } = await import("fs/promises");
|
|
3045
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3046
|
+
const root = resolve3(cwd);
|
|
3047
|
+
const pkgPath = join5(root, "package.json");
|
|
3048
|
+
const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
2709
3049
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2710
3050
|
const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
|
|
2711
3051
|
const envCandidates = [".env.local", ".env"];
|
|
2712
3052
|
let envContent = "";
|
|
2713
3053
|
for (const f of envCandidates) {
|
|
2714
3054
|
try {
|
|
2715
|
-
envContent = await
|
|
3055
|
+
envContent = await readFile2(join5(root, f), "utf8");
|
|
2716
3056
|
break;
|
|
2717
3057
|
} catch {
|
|
2718
3058
|
}
|
|
@@ -2726,7 +3066,7 @@ async function checkHostAppWiring(cwd) {
|
|
|
2726
3066
|
});
|
|
2727
3067
|
let mcpPresent = false;
|
|
2728
3068
|
try {
|
|
2729
|
-
await access(
|
|
3069
|
+
await access(join5(root, ".cursor", "mcp.json"));
|
|
2730
3070
|
mcpPresent = true;
|
|
2731
3071
|
} catch {
|
|
2732
3072
|
}
|
|
@@ -2759,19 +3099,19 @@ async function checkHostAppWiring(cwd) {
|
|
|
2759
3099
|
}
|
|
2760
3100
|
async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
2761
3101
|
const checks = [];
|
|
2762
|
-
const { readFile:
|
|
2763
|
-
const { join:
|
|
3102
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
3103
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
2764
3104
|
const { homedir } = await import("os");
|
|
2765
|
-
const root =
|
|
3105
|
+
const root = resolve3(cwd);
|
|
2766
3106
|
const candidates = [
|
|
2767
|
-
|
|
2768
|
-
|
|
3107
|
+
join5(root, ".cursor", "mcp.json"),
|
|
3108
|
+
join5(homedir(), ".cursor", "mcp.json")
|
|
2769
3109
|
];
|
|
2770
3110
|
let mcpPath = null;
|
|
2771
3111
|
let mcpRaw = null;
|
|
2772
3112
|
for (const candidate of candidates) {
|
|
2773
3113
|
try {
|
|
2774
|
-
mcpRaw = await
|
|
3114
|
+
mcpRaw = await readFile2(candidate, "utf8");
|
|
2775
3115
|
mcpPath = candidate;
|
|
2776
3116
|
break;
|
|
2777
3117
|
} catch {
|
|
@@ -2958,9 +3298,9 @@ async function planUpgrade(cwd) {
|
|
|
2958
3298
|
...pkg?.dependencies ?? {},
|
|
2959
3299
|
...pkg?.devDependencies ?? {}
|
|
2960
3300
|
};
|
|
2961
|
-
const
|
|
3301
|
+
const installed2 = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
2962
3302
|
const entries = await Promise.all(
|
|
2963
|
-
|
|
3303
|
+
installed2.map(async (name) => {
|
|
2964
3304
|
const current = deps[name] ?? "";
|
|
2965
3305
|
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
2966
3306
|
if (!/^\d/.test(currentCore)) {
|
|
@@ -3031,136 +3371,6 @@ async function runUpgrade(opts = {}) {
|
|
|
3031
3371
|
};
|
|
3032
3372
|
}
|
|
3033
3373
|
|
|
3034
|
-
// src/connect.ts
|
|
3035
|
-
import { appendFile, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
3036
|
-
import { join as join5, resolve as resolve3 } from "path";
|
|
3037
|
-
function envKeyPresent(content, key) {
|
|
3038
|
-
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3039
|
-
return new RegExp(`^${escaped}=`, "m").test(content);
|
|
3040
|
-
}
|
|
3041
|
-
async function mergeEnvFile(path, lines) {
|
|
3042
|
-
const block = `
|
|
3043
|
-
# Mushi \u2014 added by mushi connect
|
|
3044
|
-
${lines.join("\n")}
|
|
3045
|
-
`;
|
|
3046
|
-
let existing = null;
|
|
3047
|
-
try {
|
|
3048
|
-
existing = await readFile3(path, "utf8");
|
|
3049
|
-
} catch {
|
|
3050
|
-
}
|
|
3051
|
-
if (existing !== null) {
|
|
3052
|
-
const needs = lines.filter((line) => {
|
|
3053
|
-
const key = line.split("=")[0];
|
|
3054
|
-
return !envKeyPresent(existing, key);
|
|
3055
|
-
});
|
|
3056
|
-
if (needs.length === 0) return false;
|
|
3057
|
-
await appendFile(path, `
|
|
3058
|
-
# Mushi \u2014 added by mushi connect
|
|
3059
|
-
${needs.join("\n")}
|
|
3060
|
-
`, "utf8");
|
|
3061
|
-
return true;
|
|
3062
|
-
}
|
|
3063
|
-
await writeFile2(path, block, "utf8");
|
|
3064
|
-
return true;
|
|
3065
|
-
}
|
|
3066
|
-
async function ensureMcpJsonGitignored(cwd, messages) {
|
|
3067
|
-
const gitignorePath = join5(cwd, ".gitignore");
|
|
3068
|
-
const patterns = [".cursor/mcp.json", ".cursor/"];
|
|
3069
|
-
let content = null;
|
|
3070
|
-
try {
|
|
3071
|
-
content = await readFile3(gitignorePath, "utf8");
|
|
3072
|
-
} catch {
|
|
3073
|
-
}
|
|
3074
|
-
if (content === null) {
|
|
3075
|
-
messages.push(
|
|
3076
|
-
"\u26A0 No .gitignore found \u2014 .cursor/mcp.json contains your API key. Add `.cursor/mcp.json` before committing."
|
|
3077
|
-
);
|
|
3078
|
-
return;
|
|
3079
|
-
}
|
|
3080
|
-
const covered = patterns.some((p3) => content.split("\n").some((line) => line.trim() === p3 || line.trim() === `${p3}/`));
|
|
3081
|
-
if (covered) return;
|
|
3082
|
-
await appendFile(gitignorePath, "\n# Mushi \u2014 keep MCP credentials out of git\n.cursor/mcp.json\n", "utf8");
|
|
3083
|
-
messages.push("\u2713 Added .cursor/mcp.json to .gitignore (contains API key)");
|
|
3084
|
-
}
|
|
3085
|
-
async function runConnect(opts, baseConfig = {}) {
|
|
3086
|
-
const cwd = resolve3(opts.cwd ?? process.cwd());
|
|
3087
|
-
const endpoint = assertEndpoint(opts.endpoint);
|
|
3088
|
-
const messages = [];
|
|
3089
|
-
const config = {
|
|
3090
|
-
...baseConfig,
|
|
3091
|
-
apiKey: opts.apiKey,
|
|
3092
|
-
projectId: opts.projectId,
|
|
3093
|
-
endpoint
|
|
3094
|
-
};
|
|
3095
|
-
saveConfig(config);
|
|
3096
|
-
messages.push(`\u2713 Credentials saved to ${CONFIG_PATH}`);
|
|
3097
|
-
let envPath = null;
|
|
3098
|
-
if (opts.writeEnv !== false) {
|
|
3099
|
-
const pkg = readPackageJson(cwd);
|
|
3100
|
-
const framework = detectFramework(cwd, pkg);
|
|
3101
|
-
const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
|
|
3102
|
-
envPath = join5(cwd, ".env.local");
|
|
3103
|
-
const wrote = await mergeEnvFile(envPath, lines);
|
|
3104
|
-
messages.push(
|
|
3105
|
-
wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
|
|
3106
|
-
);
|
|
3107
|
-
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
3108
|
-
if (deps["@capacitor/core"] && deps["react"]) {
|
|
3109
|
-
messages.push(
|
|
3110
|
-
"\u2139 Capacitor + React detected \u2014 install @mushi-mushi/web (or @mushi-mushi/react) and call initMushi() in main.tsx. Optional @mushi-mushi/capacitor for native shell parity."
|
|
3111
|
-
);
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
let mcpPath = null;
|
|
3115
|
-
if (opts.wireIde !== false) {
|
|
3116
|
-
const mcpDir = join5(cwd, ".cursor");
|
|
3117
|
-
mcpPath = join5(mcpDir, "mcp.json");
|
|
3118
|
-
const serverName = buildMcpServerName({ projectId: opts.projectId });
|
|
3119
|
-
const serverBlock = buildMcpServerBlock({
|
|
3120
|
-
endpoint,
|
|
3121
|
-
projectId: opts.projectId,
|
|
3122
|
-
apiKey: opts.apiKey
|
|
3123
|
-
});
|
|
3124
|
-
await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
|
|
3125
|
-
await ensureMcpJsonGitignored(cwd, messages);
|
|
3126
|
-
messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
|
|
3127
|
-
}
|
|
3128
|
-
let heartbeat = null;
|
|
3129
|
-
if (opts.wait) {
|
|
3130
|
-
const timeoutSec = opts.waitTimeoutSec ?? 120;
|
|
3131
|
-
const maxAttempts = Math.max(1, Math.ceil(timeoutSec * 1e3 / 3e3));
|
|
3132
|
-
messages.push(`\u2026 Waiting for SDK heartbeat (up to ${timeoutSec}s) \u2014 start your dev server with the snippet installed`);
|
|
3133
|
-
heartbeat = await waitForIngestReady({
|
|
3134
|
-
endpoint,
|
|
3135
|
-
apiKey: opts.apiKey,
|
|
3136
|
-
projectId: opts.projectId,
|
|
3137
|
-
maxAttempts,
|
|
3138
|
-
onPoll: (payload, attempt) => {
|
|
3139
|
-
if (!opts.json && attempt % 3 === 0) {
|
|
3140
|
-
const sdk = payload.steps.find((s) => s.id === "sdk_installed");
|
|
3141
|
-
const seen = payload.diagnostic?.last_sdk_seen_at ?? "never";
|
|
3142
|
-
process.stdout.write(` poll ${attempt}: sdk_installed=${sdk?.complete ? "yes" : "no"} last_seen=${seen}
|
|
3143
|
-
`);
|
|
3144
|
-
}
|
|
3145
|
-
}
|
|
3146
|
-
});
|
|
3147
|
-
if (heartbeat.ok) {
|
|
3148
|
-
const label = heartbeat.reason === "heartbeat" ? "SDK heartbeat detected" : "Ingest setup complete";
|
|
3149
|
-
messages.push(`\u2713 ${label} \u2014 ingest pipeline is live`);
|
|
3150
|
-
} else if (heartbeat.reason === "unauthorized") {
|
|
3151
|
-
messages.push(
|
|
3152
|
-
`\u2717 The backend rejected these credentials (${heartbeat.error ?? "auth error"}). Double-check --api-key, --project-id, and --endpoint, then re-run \`mushi connect --wait\`.`
|
|
3153
|
-
);
|
|
3154
|
-
} else {
|
|
3155
|
-
messages.push(
|
|
3156
|
-
`\u2717 No heartbeat before timeout (${heartbeat.reason}). Confirm env vars are in your build, restart the dev server, then re-run \`mushi connect --wait\`.`
|
|
3157
|
-
);
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
const ok = !opts.wait || Boolean(heartbeat?.ok);
|
|
3161
|
-
return { ok, envPath, mcpPath, heartbeat, messages };
|
|
3162
|
-
}
|
|
3163
|
-
|
|
3164
3374
|
// src/commands/fix.ts
|
|
3165
3375
|
function registerFixCommands(program2) {
|
|
3166
3376
|
const fixCmd = program2.command("fix").description("Dispatch an agentic fix for a report");
|
|
@@ -3302,7 +3512,7 @@ Examples:
|
|
|
3302
3512
|
}
|
|
3303
3513
|
if (result.plan.entries.length === 0) process.exit(1);
|
|
3304
3514
|
});
|
|
3305
|
-
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
|
|
3515
|
+
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
|
|
3306
3516
|
Examples:
|
|
3307
3517
|
MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
|
|
3308
3518
|
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
|
|
@@ -3316,8 +3526,10 @@ Examples:
|
|
|
3316
3526
|
projectId: opts.projectId,
|
|
3317
3527
|
endpoint: opts.endpoint,
|
|
3318
3528
|
cwd: opts.cwd,
|
|
3319
|
-
|
|
3320
|
-
|
|
3529
|
+
// Explicit --write-env / --wire-ide force the action on even if --no-env /
|
|
3530
|
+
// --no-ide was also passed; otherwise writing is the default.
|
|
3531
|
+
writeEnv: opts.writeEnv === true ? true : opts.env !== false,
|
|
3532
|
+
wireIde: opts.wireIde === true ? true : opts.ide !== false,
|
|
3321
3533
|
wait: opts.wait,
|
|
3322
3534
|
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3323
3535
|
json: opts.json
|
|
@@ -3987,9 +4199,9 @@ Recent runs for story ${storyId.slice(0, 8)}\u2026:
|
|
|
3987
4199
|
}
|
|
3988
4200
|
}
|
|
3989
4201
|
}
|
|
3990
|
-
const
|
|
4202
|
+
const consoleUrl2 = config.consoleUrl ?? "https://app.mushi.ai";
|
|
3991
4203
|
console.log(`
|
|
3992
|
-
Open in console: ${
|
|
4204
|
+
Open in console: ${consoleUrl2}/qa-coverage?story=${storyId}`);
|
|
3993
4205
|
console.log(` Tip: run 'mushi config consoleUrl <url>' to override (e.g. http://localhost:6464 for local dev)`);
|
|
3994
4206
|
console.log();
|
|
3995
4207
|
});
|
|
@@ -4433,8 +4645,8 @@ ${"\u2500".repeat(60)}`);
|
|
|
4433
4645
|
installSignalHandlers();
|
|
4434
4646
|
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
|
|
4435
4647
|
Environment variables:
|
|
4436
|
-
MUSHI_API_KEY
|
|
4437
|
-
MUSHI_PROJECT_ID Project UUID
|
|
4648
|
+
MUSHI_API_KEY SDK ingest key (report:write scope \u2014 from Onboarding \u2192 Verify in the console)
|
|
4649
|
+
MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
|
|
4438
4650
|
MUSHI_API_ENDPOINT Supabase edge function URL
|
|
4439
4651
|
e.g. https://<ref>.supabase.co/functions/v1/api
|
|
4440
4652
|
|
|
@@ -4444,7 +4656,7 @@ Exit codes:
|
|
|
4444
4656
|
2 configuration error (missing credentials or endpoint)
|
|
4445
4657
|
3 not found (resource does not exist)
|
|
4446
4658
|
|
|
4447
|
-
Console: https://kensaur.us/mushi-mushi/
|
|
4659
|
+
Console: https://kensaur.us/mushi-mushi/admin
|
|
4448
4660
|
Docs: https://github.com/kensaurus/mushi-mushi`);
|
|
4449
4661
|
registerAccountCommands(program);
|
|
4450
4662
|
registerDeployCommands(program);
|