@mushi-mushi/cli 0.17.2 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +50 -9
- package/README.md +18 -5
- package/dist/{chunk-S4WQHEGH.js → chunk-7Q44KWGN.js} +77 -51
- package/dist/chunk-JJVWMYSD.js +6 -0
- package/dist/chunk-LDPFTQLE.js +44 -0
- package/dist/chunk-LKJ7KLBC.js +313 -0
- package/dist/cli-shared-FZWLZCCR.js +18 -0
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/endpoint-DGS73AVM.js +15 -0
- package/dist/index.js +507 -546
- package/dist/init.js +5 -5
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-EEQI5AFX.js +0 -6
package/dist/index.js
CHANGED
|
@@ -1,207 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
API_TIMEOUT_MS,
|
|
4
|
+
CONFIG_PATH,
|
|
5
|
+
MUSHI_CLI_VERSION,
|
|
6
|
+
apiCall,
|
|
7
|
+
die,
|
|
8
|
+
fmtDate,
|
|
9
|
+
installSignalHandlers,
|
|
10
|
+
loadConfig,
|
|
11
|
+
pad,
|
|
12
|
+
requireConfig,
|
|
13
|
+
saveConfig
|
|
14
|
+
} from "./chunk-LKJ7KLBC.js";
|
|
15
|
+
import {
|
|
16
|
+
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
17
|
+
assertEndpoint,
|
|
18
|
+
normalizeEndpoint,
|
|
19
|
+
resolveCloudEndpoint
|
|
20
|
+
} from "./chunk-LDPFTQLE.js";
|
|
2
21
|
|
|
3
22
|
// src/index.ts
|
|
4
23
|
import { Command } from "commander";
|
|
5
24
|
|
|
6
|
-
// src/errors.ts
|
|
7
|
-
var EXIT_CODE_MAP = {
|
|
8
|
-
E_AUTH_MISSING: 2,
|
|
9
|
-
E_AUTH_INVALID: 2,
|
|
10
|
-
E_PROJECT_MISSING: 2,
|
|
11
|
-
E_ENDPOINT_INVALID: 2,
|
|
12
|
-
E_INVALID_INPUT: 2,
|
|
13
|
-
E_NOT_INTERACTIVE: 2,
|
|
14
|
-
E_NETWORK: 3,
|
|
15
|
-
E_TIMEOUT: 3,
|
|
16
|
-
E_API_ERROR: 3,
|
|
17
|
-
E_RATE_LIMITED: 3,
|
|
18
|
-
E_FILE_NOT_FOUND: 1,
|
|
19
|
-
E_FILE_PERMISSION: 1,
|
|
20
|
-
E_FRESHNESS_STALE: 1,
|
|
21
|
-
E_INTERRUPTED: 130,
|
|
22
|
-
// 128 + SIGINT (2). POSIX convention.
|
|
23
|
-
E_INTERNAL: 1
|
|
24
|
-
};
|
|
25
|
-
var MushiCliError = class extends Error {
|
|
26
|
-
code;
|
|
27
|
-
hint;
|
|
28
|
-
cause;
|
|
29
|
-
constructor(code, message, hint, cause) {
|
|
30
|
-
super(message);
|
|
31
|
-
this.name = "MushiCliError";
|
|
32
|
-
this.code = code;
|
|
33
|
-
if (hint) this.hint = hint;
|
|
34
|
-
if (cause !== void 0) this.cause = cause;
|
|
35
|
-
}
|
|
36
|
-
/** Exit code POSIX-aware shell scripts should branch on. */
|
|
37
|
-
get exitCode() {
|
|
38
|
-
return EXIT_CODE_MAP[this.code];
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Format for `--json` output. Used by the JSON writer when a CLI
|
|
42
|
-
* subcommand was invoked with `--json` and an error escapes.
|
|
43
|
-
*/
|
|
44
|
-
toJSON() {
|
|
45
|
-
return {
|
|
46
|
-
error: {
|
|
47
|
-
code: this.code,
|
|
48
|
-
message: this.message,
|
|
49
|
-
...this.hint ? { hint: this.hint } : {}
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
// src/signals.ts
|
|
56
|
-
var installed = false;
|
|
57
|
-
var activeController = null;
|
|
58
|
-
function installSignalHandlers() {
|
|
59
|
-
if (installed) return;
|
|
60
|
-
installed = true;
|
|
61
|
-
const abortAndExit = (signal) => {
|
|
62
|
-
activeController?.abort(
|
|
63
|
-
new MushiCliError(
|
|
64
|
-
"E_INTERRUPTED",
|
|
65
|
-
`aborted by ${signal}`,
|
|
66
|
-
"partial state on the server is safe to retry \u2014 re-run the same command"
|
|
67
|
-
)
|
|
68
|
-
);
|
|
69
|
-
const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
|
|
70
|
-
process.nextTick(() => {
|
|
71
|
-
process.exit(code);
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
process.on("SIGINT", () => abortAndExit("SIGINT"));
|
|
75
|
-
process.on("SIGTERM", () => abortAndExit("SIGTERM"));
|
|
76
|
-
}
|
|
77
|
-
function getAbortSignal(external) {
|
|
78
|
-
if (external) return external.signal;
|
|
79
|
-
if (!activeController || activeController.signal.aborted) {
|
|
80
|
-
activeController = new AbortController();
|
|
81
|
-
}
|
|
82
|
-
return activeController.signal;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// src/version.ts
|
|
86
|
-
var MUSHI_CLI_VERSION = true ? "0.17.2" : "0.0.0-dev";
|
|
87
|
-
|
|
88
|
-
// src/config.ts
|
|
89
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
90
|
-
import { homedir } from "os";
|
|
91
|
-
import { dirname, join } from "path";
|
|
92
|
-
var SECURE_FILE_MODE = 384;
|
|
93
|
-
var SECURE_DIR_MODE = 448;
|
|
94
|
-
function resolveXdgConfigPath() {
|
|
95
|
-
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
96
|
-
if (xdg && xdg.length > 0) {
|
|
97
|
-
return join(xdg, "mushi", "config.json");
|
|
98
|
-
}
|
|
99
|
-
if (process.platform === "win32") {
|
|
100
|
-
const appData = process.env["APPDATA"];
|
|
101
|
-
if (appData && appData.length > 0) {
|
|
102
|
-
return join(appData, "mushi", "config.json");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return join(homedir(), ".config", "mushi", "config.json");
|
|
106
|
-
}
|
|
107
|
-
var LEGACY_CONFIG_PATH = join(homedir(), ".mushirc");
|
|
108
|
-
var CONFIG_PATH = resolveXdgConfigPath();
|
|
109
|
-
function loadConfig(path = CONFIG_PATH) {
|
|
110
|
-
let file = {};
|
|
111
|
-
if (existsSync(path)) {
|
|
112
|
-
tightenPermissions(path);
|
|
113
|
-
try {
|
|
114
|
-
file = JSON.parse(readFileSync(path, "utf-8"));
|
|
115
|
-
} catch {
|
|
116
|
-
}
|
|
117
|
-
} else if (path === CONFIG_PATH && existsSync(LEGACY_CONFIG_PATH)) {
|
|
118
|
-
file = migrateLegacyConfig() ?? {};
|
|
119
|
-
}
|
|
120
|
-
const endpointFromEnv = process.env["MUSHI_API_ENDPOINT"] ?? process.env["MUSHI_ENDPOINT"] ?? void 0;
|
|
121
|
-
const fromEnv = {
|
|
122
|
-
...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
|
|
123
|
-
...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
|
|
124
|
-
...endpointFromEnv ? { endpoint: endpointFromEnv } : {}
|
|
125
|
-
};
|
|
126
|
-
return { ...file, ...fromEnv };
|
|
127
|
-
}
|
|
128
|
-
function saveConfig(config, path = CONFIG_PATH) {
|
|
129
|
-
const dir = dirname(path);
|
|
130
|
-
if (!existsSync(dir)) {
|
|
131
|
-
mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
|
|
132
|
-
} else {
|
|
133
|
-
tightenDirPermissions(dir);
|
|
134
|
-
}
|
|
135
|
-
writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
|
|
136
|
-
tightenPermissions(path);
|
|
137
|
-
}
|
|
138
|
-
function migrateLegacyConfig(legacyPath = LEGACY_CONFIG_PATH, destPath = CONFIG_PATH) {
|
|
139
|
-
if (!existsSync(legacyPath)) return null;
|
|
140
|
-
let parsed;
|
|
141
|
-
try {
|
|
142
|
-
parsed = JSON.parse(readFileSync(legacyPath, "utf-8"));
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
const dir = dirname(destPath);
|
|
147
|
-
if (!existsSync(dir)) {
|
|
148
|
-
mkdirSync(dir, { recursive: true, mode: SECURE_DIR_MODE });
|
|
149
|
-
}
|
|
150
|
-
try {
|
|
151
|
-
renameSync(legacyPath, destPath);
|
|
152
|
-
} catch {
|
|
153
|
-
writeFileSync(destPath, JSON.stringify(parsed, null, 2), { mode: SECURE_FILE_MODE });
|
|
154
|
-
try {
|
|
155
|
-
unlinkSync(legacyPath);
|
|
156
|
-
} catch {
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
tightenPermissions(destPath);
|
|
160
|
-
tightenDirPermissions(dir);
|
|
161
|
-
return parsed;
|
|
162
|
-
}
|
|
163
|
-
function tightenPermissions(path) {
|
|
164
|
-
if (process.platform === "win32") return;
|
|
165
|
-
try {
|
|
166
|
-
const current = statSync(path).mode & 511;
|
|
167
|
-
if (current !== SECURE_FILE_MODE) chmodSync(path, SECURE_FILE_MODE);
|
|
168
|
-
} catch {
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function tightenDirPermissions(path) {
|
|
172
|
-
if (process.platform === "win32") return;
|
|
173
|
-
try {
|
|
174
|
-
const current = statSync(path).mode & 511;
|
|
175
|
-
if (current !== SECURE_DIR_MODE) chmodSync(path, SECURE_DIR_MODE);
|
|
176
|
-
} catch {
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
25
|
// src/init.ts
|
|
181
26
|
import * as p from "@clack/prompts";
|
|
182
27
|
import { spawn } from "child_process";
|
|
183
28
|
import { randomUUID } from "crypto";
|
|
184
|
-
import { appendFileSync, existsSync as
|
|
185
|
-
import { join as
|
|
29
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
30
|
+
import { join as join3 } from "path";
|
|
186
31
|
|
|
187
32
|
// src/detect.ts
|
|
188
|
-
import { readFileSync
|
|
189
|
-
import { join
|
|
33
|
+
import { readFileSync, existsSync } from "fs";
|
|
34
|
+
import { join } from "path";
|
|
190
35
|
var FRAMEWORKS = {
|
|
191
36
|
next: {
|
|
192
37
|
id: "next",
|
|
193
38
|
label: "Next.js",
|
|
194
39
|
packageName: "@mushi-mushi/react",
|
|
195
40
|
needsWebPackage: false,
|
|
196
|
-
snippet: (
|
|
41
|
+
snippet: () => `// app/providers.tsx (or pages/_app.tsx for /pages router)
|
|
197
42
|
'use client'
|
|
198
43
|
import { MushiProvider } from '@mushi-mushi/react'
|
|
199
44
|
|
|
200
45
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
201
46
|
return (
|
|
202
47
|
<MushiProvider config={{
|
|
203
|
-
projectId:
|
|
204
|
-
apiKey:
|
|
48
|
+
projectId: process.env.NEXT_PUBLIC_MUSHI_PROJECT_ID!,
|
|
49
|
+
apiKey: process.env.NEXT_PUBLIC_MUSHI_API_KEY!,
|
|
205
50
|
}}>
|
|
206
51
|
{children}
|
|
207
52
|
</MushiProvider>
|
|
@@ -213,13 +58,13 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
213
58
|
label: "React",
|
|
214
59
|
packageName: "@mushi-mushi/react",
|
|
215
60
|
needsWebPackage: false,
|
|
216
|
-
snippet: (
|
|
61
|
+
snippet: () => `// src/main.tsx
|
|
217
62
|
import { MushiProvider } from '@mushi-mushi/react'
|
|
218
63
|
|
|
219
64
|
createRoot(document.getElementById('root')!).render(
|
|
220
65
|
<MushiProvider config={{
|
|
221
|
-
projectId:
|
|
222
|
-
apiKey:
|
|
66
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
67
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
223
68
|
}}>
|
|
224
69
|
<App />
|
|
225
70
|
</MushiProvider>
|
|
@@ -229,57 +74,58 @@ createRoot(document.getElementById('root')!).render(
|
|
|
229
74
|
id: "vue",
|
|
230
75
|
label: "Vue 3",
|
|
231
76
|
packageName: "@mushi-mushi/vue",
|
|
232
|
-
needsWebPackage:
|
|
233
|
-
snippet: (
|
|
77
|
+
needsWebPackage: false,
|
|
78
|
+
snippet: () => `// src/main.ts
|
|
234
79
|
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
235
|
-
import { Mushi } from '@mushi-mushi/web'
|
|
236
80
|
|
|
237
|
-
app.use(MushiPlugin, {
|
|
238
|
-
|
|
81
|
+
app.use(MushiPlugin, {
|
|
82
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
83
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
84
|
+
})`
|
|
239
85
|
},
|
|
240
86
|
nuxt: {
|
|
241
87
|
id: "nuxt",
|
|
242
88
|
label: "Nuxt",
|
|
243
89
|
packageName: "@mushi-mushi/vue",
|
|
244
|
-
needsWebPackage:
|
|
245
|
-
snippet: (
|
|
90
|
+
needsWebPackage: false,
|
|
91
|
+
snippet: () => `// plugins/mushi.client.ts
|
|
246
92
|
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
247
|
-
import { Mushi } from '@mushi-mushi/web'
|
|
248
93
|
|
|
249
94
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
250
95
|
nuxtApp.vueApp.use(MushiPlugin, {
|
|
251
|
-
projectId:
|
|
252
|
-
apiKey:
|
|
96
|
+
projectId: import.meta.env.NUXT_PUBLIC_MUSHI_PROJECT_ID,
|
|
97
|
+
apiKey: import.meta.env.NUXT_PUBLIC_MUSHI_API_KEY,
|
|
253
98
|
})
|
|
254
|
-
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
255
99
|
})`
|
|
256
100
|
},
|
|
257
101
|
svelte: {
|
|
258
102
|
id: "svelte",
|
|
259
103
|
label: "Svelte",
|
|
260
104
|
packageName: "@mushi-mushi/svelte",
|
|
261
|
-
needsWebPackage:
|
|
262
|
-
snippet: (
|
|
105
|
+
needsWebPackage: false,
|
|
106
|
+
snippet: () => `// src/main.ts
|
|
263
107
|
import { initMushi } from '@mushi-mushi/svelte'
|
|
264
|
-
import { Mushi } from '@mushi-mushi/web'
|
|
265
108
|
|
|
266
|
-
initMushi({
|
|
267
|
-
|
|
109
|
+
initMushi({
|
|
110
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
111
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
112
|
+
})`
|
|
268
113
|
},
|
|
269
114
|
sveltekit: {
|
|
270
115
|
id: "sveltekit",
|
|
271
116
|
label: "SvelteKit",
|
|
272
117
|
packageName: "@mushi-mushi/svelte",
|
|
273
|
-
needsWebPackage:
|
|
274
|
-
snippet: (
|
|
118
|
+
needsWebPackage: false,
|
|
119
|
+
snippet: () => `// src/routes/+layout.svelte
|
|
275
120
|
<script>
|
|
276
121
|
import { onMount } from 'svelte'
|
|
277
|
-
import { initMushi } from '@mushi-mushi/svelte'
|
|
278
122
|
|
|
279
123
|
onMount(async () => {
|
|
280
|
-
const {
|
|
281
|
-
initMushi({
|
|
282
|
-
|
|
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
|
+
})
|
|
283
129
|
})
|
|
284
130
|
</script>`
|
|
285
131
|
},
|
|
@@ -287,29 +133,33 @@ Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
|
287
133
|
id: "angular",
|
|
288
134
|
label: "Angular",
|
|
289
135
|
packageName: "@mushi-mushi/angular",
|
|
290
|
-
needsWebPackage:
|
|
291
|
-
snippet: (
|
|
136
|
+
needsWebPackage: false,
|
|
137
|
+
snippet: () => `// src/main.ts
|
|
292
138
|
import { provideMushi } from '@mushi-mushi/angular'
|
|
293
|
-
import { Mushi } from '@mushi-mushi/web'
|
|
294
139
|
|
|
295
140
|
bootstrapApplication(AppComponent, {
|
|
296
141
|
providers: [
|
|
297
|
-
provideMushi({
|
|
142
|
+
provideMushi({
|
|
143
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
144
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
145
|
+
}),
|
|
298
146
|
],
|
|
299
|
-
})
|
|
300
|
-
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
147
|
+
})`
|
|
301
148
|
},
|
|
302
149
|
expo: {
|
|
303
150
|
id: "expo",
|
|
304
151
|
label: "Expo",
|
|
305
152
|
packageName: "@mushi-mushi/react-native",
|
|
306
153
|
needsWebPackage: false,
|
|
307
|
-
snippet: (
|
|
154
|
+
snippet: () => `// App.tsx
|
|
308
155
|
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
309
156
|
|
|
310
157
|
export default function App() {
|
|
311
158
|
return (
|
|
312
|
-
<MushiProvider
|
|
159
|
+
<MushiProvider
|
|
160
|
+
projectId={process.env.EXPO_PUBLIC_MUSHI_PROJECT_ID!}
|
|
161
|
+
apiKey={process.env.EXPO_PUBLIC_MUSHI_API_KEY!}
|
|
162
|
+
>
|
|
313
163
|
<YourApp />
|
|
314
164
|
</MushiProvider>
|
|
315
165
|
)
|
|
@@ -320,12 +170,15 @@ export default function App() {
|
|
|
320
170
|
label: "React Native",
|
|
321
171
|
packageName: "@mushi-mushi/react-native",
|
|
322
172
|
needsWebPackage: false,
|
|
323
|
-
snippet: (
|
|
173
|
+
snippet: () => `// App.tsx
|
|
324
174
|
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
325
175
|
|
|
326
176
|
export default function App() {
|
|
327
177
|
return (
|
|
328
|
-
<MushiProvider
|
|
178
|
+
<MushiProvider
|
|
179
|
+
projectId={process.env.MUSHI_PROJECT_ID!}
|
|
180
|
+
apiKey={process.env.MUSHI_API_KEY!}
|
|
181
|
+
>
|
|
329
182
|
<YourApp />
|
|
330
183
|
</MushiProvider>
|
|
331
184
|
)
|
|
@@ -342,24 +195,27 @@ export default function App() {
|
|
|
342
195
|
* `TypeError: Mushi.init is not a function`. The accompanying admin
|
|
343
196
|
* console snippet (apps/admin/src/lib/sdkSnippets.ts) emits the same
|
|
344
197
|
* `Mushi.configure(...)` shape; both are pinned by tests. */
|
|
345
|
-
snippet: (
|
|
198
|
+
snippet: () => `// src/main.ts \u2014 after install, run \`npx cap sync\`
|
|
346
199
|
import { Mushi } from '@mushi-mushi/capacitor'
|
|
347
200
|
|
|
348
|
-
await Mushi.configure({
|
|
201
|
+
await Mushi.configure({
|
|
202
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
203
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
204
|
+
})`
|
|
349
205
|
},
|
|
350
206
|
express: {
|
|
351
207
|
id: "express",
|
|
352
208
|
label: "Express",
|
|
353
209
|
packageName: "@mushi-mushi/node",
|
|
354
210
|
needsWebPackage: false,
|
|
355
|
-
snippet: (
|
|
211
|
+
snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
356
212
|
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
357
213
|
import { mushiExpressErrorHandler } from '@mushi-mushi/node/express'
|
|
358
214
|
import type { Express } from 'express'
|
|
359
215
|
|
|
360
216
|
export const mushi = new MushiNodeClient({
|
|
361
|
-
projectId:
|
|
362
|
-
apiKey:
|
|
217
|
+
projectId: process.env.MUSHI_PROJECT_ID!,
|
|
218
|
+
apiKey: process.env.MUSHI_API_KEY!,
|
|
363
219
|
environment: process.env.NODE_ENV ?? 'production',
|
|
364
220
|
})
|
|
365
221
|
attachUnhandledHook({ client: mushi })
|
|
@@ -373,14 +229,14 @@ export function attachMushi(app: Express) {
|
|
|
373
229
|
label: "Fastify",
|
|
374
230
|
packageName: "@mushi-mushi/node",
|
|
375
231
|
needsWebPackage: false,
|
|
376
|
-
snippet: (
|
|
232
|
+
snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
377
233
|
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
378
234
|
import { mushiFastifyPlugin } from '@mushi-mushi/node/fastify'
|
|
379
235
|
import Fastify from 'fastify'
|
|
380
236
|
|
|
381
237
|
export const mushi = new MushiNodeClient({
|
|
382
|
-
projectId:
|
|
383
|
-
apiKey:
|
|
238
|
+
projectId: process.env.MUSHI_PROJECT_ID!,
|
|
239
|
+
apiKey: process.env.MUSHI_API_KEY!,
|
|
384
240
|
environment: process.env.NODE_ENV ?? 'production',
|
|
385
241
|
})
|
|
386
242
|
attachUnhandledHook({ client: mushi })
|
|
@@ -393,14 +249,14 @@ mushiFastifyPlugin(app, { client: mushi })`
|
|
|
393
249
|
label: "Hono",
|
|
394
250
|
packageName: "@mushi-mushi/node",
|
|
395
251
|
needsWebPackage: false,
|
|
396
|
-
snippet: (
|
|
252
|
+
snippet: () => `// src/instrument.ts \u2014 load with: node --import ./dist/instrument.js
|
|
397
253
|
import { MushiNodeClient, attachUnhandledHook } from '@mushi-mushi/node'
|
|
398
254
|
import { mushiHonoErrorHandler } from '@mushi-mushi/node/hono'
|
|
399
255
|
import { Hono } from 'hono'
|
|
400
256
|
|
|
401
257
|
export const mushi = new MushiNodeClient({
|
|
402
|
-
projectId:
|
|
403
|
-
apiKey:
|
|
258
|
+
projectId: process.env.MUSHI_PROJECT_ID!,
|
|
259
|
+
apiKey: process.env.MUSHI_API_KEY!,
|
|
404
260
|
environment: process.env.NODE_ENV ?? 'production',
|
|
405
261
|
})
|
|
406
262
|
attachUnhandledHook({ client: mushi })
|
|
@@ -417,17 +273,20 @@ app.onError(
|
|
|
417
273
|
label: "Vanilla JS / unknown",
|
|
418
274
|
packageName: "@mushi-mushi/web",
|
|
419
275
|
needsWebPackage: false,
|
|
420
|
-
snippet: (
|
|
276
|
+
snippet: () => `// Anywhere in your client bundle
|
|
421
277
|
import { Mushi } from '@mushi-mushi/web'
|
|
422
278
|
|
|
423
|
-
Mushi.init({
|
|
279
|
+
Mushi.init({
|
|
280
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
281
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
282
|
+
})`
|
|
424
283
|
}
|
|
425
284
|
};
|
|
426
285
|
function readPackageJson(cwd) {
|
|
427
|
-
const path =
|
|
428
|
-
if (!
|
|
286
|
+
const path = join(cwd, "package.json");
|
|
287
|
+
if (!existsSync(path)) return null;
|
|
429
288
|
try {
|
|
430
|
-
return JSON.parse(
|
|
289
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
431
290
|
} catch {
|
|
432
291
|
return null;
|
|
433
292
|
}
|
|
@@ -448,21 +307,21 @@ function detectFramework(cwd, pkg) {
|
|
|
448
307
|
if (deps.has("express")) return FRAMEWORKS.express;
|
|
449
308
|
if (deps.has("fastify")) return FRAMEWORKS.fastify;
|
|
450
309
|
if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
|
|
451
|
-
if (
|
|
310
|
+
if (existsSync(join(cwd, "next.config.js")) || existsSync(join(cwd, "next.config.ts"))) {
|
|
452
311
|
return FRAMEWORKS.next;
|
|
453
312
|
}
|
|
454
|
-
if (
|
|
313
|
+
if (existsSync(join(cwd, "nuxt.config.ts")) || existsSync(join(cwd, "nuxt.config.js"))) {
|
|
455
314
|
return FRAMEWORKS.nuxt;
|
|
456
315
|
}
|
|
457
|
-
if (
|
|
458
|
-
if (
|
|
316
|
+
if (existsSync(join(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
|
|
317
|
+
if (existsSync(join(cwd, "angular.json"))) return FRAMEWORKS.angular;
|
|
459
318
|
return FRAMEWORKS.vanilla;
|
|
460
319
|
}
|
|
461
320
|
function detectPackageManager(cwd) {
|
|
462
|
-
if (
|
|
463
|
-
if (
|
|
464
|
-
if (
|
|
465
|
-
if (
|
|
321
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
|
|
322
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
323
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
324
|
+
if (existsSync(join(cwd, "package-lock.json"))) return "npm";
|
|
466
325
|
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
467
326
|
if (userAgent.startsWith("bun")) return "bun";
|
|
468
327
|
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
@@ -481,6 +340,18 @@ function envVarsToWrite(apiKey, projectId, framework) {
|
|
|
481
340
|
`MUSHI_API_KEY=${apiKey}`
|
|
482
341
|
].join("\n");
|
|
483
342
|
}
|
|
343
|
+
if (framework.id === "react-native") {
|
|
344
|
+
return [
|
|
345
|
+
`MUSHI_PROJECT_ID=${projectId}`,
|
|
346
|
+
`MUSHI_API_KEY=${apiKey}`
|
|
347
|
+
].join("\n");
|
|
348
|
+
}
|
|
349
|
+
if (framework.id === "expo") {
|
|
350
|
+
return [
|
|
351
|
+
`EXPO_PUBLIC_MUSHI_PROJECT_ID=${projectId}`,
|
|
352
|
+
`EXPO_PUBLIC_MUSHI_API_KEY=${apiKey}`
|
|
353
|
+
].join("\n");
|
|
354
|
+
}
|
|
484
355
|
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
|
|
485
356
|
return [
|
|
486
357
|
`${prefix}MUSHI_PROJECT_ID=${projectId}`,
|
|
@@ -496,41 +367,6 @@ function collectDeps(pkg) {
|
|
|
496
367
|
]);
|
|
497
368
|
}
|
|
498
369
|
|
|
499
|
-
// src/endpoint.ts
|
|
500
|
-
var TEST_REPORT_TIMEOUT_MS = 1e4;
|
|
501
|
-
var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
|
|
502
|
-
var CLOUD_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
|
|
503
|
-
function resolveCloudEndpoint(explicit) {
|
|
504
|
-
if (explicit?.trim()) return explicit.trim();
|
|
505
|
-
const fromEnv = process.env.MUSHI_API_ENDPOINT?.trim();
|
|
506
|
-
if (fromEnv) return fromEnv;
|
|
507
|
-
return CLOUD_API_ENDPOINT;
|
|
508
|
-
}
|
|
509
|
-
function assertEndpoint(url) {
|
|
510
|
-
let parsed;
|
|
511
|
-
try {
|
|
512
|
-
parsed = new URL(url);
|
|
513
|
-
} catch {
|
|
514
|
-
throw new Error(`Invalid endpoint URL: ${url}`);
|
|
515
|
-
}
|
|
516
|
-
const host = parsed.hostname;
|
|
517
|
-
const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local");
|
|
518
|
-
if (parsed.protocol !== "https:" && !isLocal) {
|
|
519
|
-
throw new Error(`Endpoint must use https:// (got ${parsed.protocol}//${host}).`);
|
|
520
|
-
}
|
|
521
|
-
return normalizeEndpoint(parsed.origin + parsed.pathname);
|
|
522
|
-
}
|
|
523
|
-
function normalizeEndpoint(url) {
|
|
524
|
-
if (!url) {
|
|
525
|
-
throw new Error(
|
|
526
|
-
"No API endpoint configured. Run `mushi init` or set MUSHI_API_ENDPOINT. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
let end = url.length;
|
|
530
|
-
while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
|
|
531
|
-
return url.slice(0, end);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
370
|
// src/freshness.ts
|
|
535
371
|
var REGISTRY = "https://registry.npmjs.org";
|
|
536
372
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
@@ -591,8 +427,8 @@ function parse(version) {
|
|
|
591
427
|
}
|
|
592
428
|
|
|
593
429
|
// src/monorepo.ts
|
|
594
|
-
import { existsSync as
|
|
595
|
-
import { dirname
|
|
430
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
431
|
+
import { dirname, join as join2, resolve } from "path";
|
|
596
432
|
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
597
433
|
var FRAMEWORK_DEPS = {
|
|
598
434
|
next: "Next.js",
|
|
@@ -609,9 +445,9 @@ var FRAMEWORK_DEPS = {
|
|
|
609
445
|
function detectWorkspaceHint(cwd) {
|
|
610
446
|
const root = findWorkspaceRoot(cwd);
|
|
611
447
|
if (!root) return null;
|
|
612
|
-
const rootPkg = readPackageJsonSafely(
|
|
448
|
+
const rootPkg = readPackageJsonSafely(join2(root, "package.json"));
|
|
613
449
|
if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
|
|
614
|
-
const source =
|
|
450
|
+
const source = existsSync2(join2(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
|
|
615
451
|
const apps = collectAppsFromGlobs(root);
|
|
616
452
|
if (apps.length === 0) return null;
|
|
617
453
|
return { root, apps, source };
|
|
@@ -620,15 +456,15 @@ function findWorkspaceRoot(start) {
|
|
|
620
456
|
let dir = resolve(start);
|
|
621
457
|
for (let i = 0; i < 8; i++) {
|
|
622
458
|
if (isWorkspaceRoot(dir)) return dir;
|
|
623
|
-
const parent =
|
|
459
|
+
const parent = dirname(dir);
|
|
624
460
|
if (parent === dir) break;
|
|
625
461
|
dir = parent;
|
|
626
462
|
}
|
|
627
463
|
return null;
|
|
628
464
|
}
|
|
629
465
|
function isWorkspaceRoot(dir) {
|
|
630
|
-
if (
|
|
631
|
-
const pkg = readPackageJsonSafely(
|
|
466
|
+
if (existsSync2(join2(dir, "pnpm-workspace.yaml"))) return true;
|
|
467
|
+
const pkg = readPackageJsonSafely(join2(dir, "package.json"));
|
|
632
468
|
if (!pkg) return false;
|
|
633
469
|
return Boolean(pkg.workspaces);
|
|
634
470
|
}
|
|
@@ -636,8 +472,8 @@ function collectAppsFromGlobs(root) {
|
|
|
636
472
|
const results = [];
|
|
637
473
|
for (const glob of WORKSPACE_GLOB_CANDIDATES) {
|
|
638
474
|
const prefix = glob.replace("/*", "");
|
|
639
|
-
const parentDir =
|
|
640
|
-
if (!
|
|
475
|
+
const parentDir = join2(root, prefix);
|
|
476
|
+
if (!existsSync2(parentDir)) continue;
|
|
641
477
|
let entries;
|
|
642
478
|
try {
|
|
643
479
|
entries = readdirSync(parentDir);
|
|
@@ -645,7 +481,7 @@ function collectAppsFromGlobs(root) {
|
|
|
645
481
|
continue;
|
|
646
482
|
}
|
|
647
483
|
for (const entry of entries) {
|
|
648
|
-
const pkgPath =
|
|
484
|
+
const pkgPath = join2(parentDir, entry, "package.json");
|
|
649
485
|
if (!isFileSafe(pkgPath)) continue;
|
|
650
486
|
const pkg = readPackageJsonSafely(pkgPath);
|
|
651
487
|
if (!pkg) continue;
|
|
@@ -663,14 +499,14 @@ function collectAppsFromGlobs(root) {
|
|
|
663
499
|
function readPackageJsonSafely(path) {
|
|
664
500
|
if (!isFileSafe(path)) return null;
|
|
665
501
|
try {
|
|
666
|
-
return JSON.parse(
|
|
502
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
667
503
|
} catch {
|
|
668
504
|
return null;
|
|
669
505
|
}
|
|
670
506
|
}
|
|
671
507
|
function isFileSafe(path) {
|
|
672
508
|
try {
|
|
673
|
-
return
|
|
509
|
+
return existsSync2(path) && statSync(path).isFile();
|
|
674
510
|
} catch {
|
|
675
511
|
return false;
|
|
676
512
|
}
|
|
@@ -690,7 +526,7 @@ function getFrameworkFromPkg(pkg) {
|
|
|
690
526
|
// src/init.ts
|
|
691
527
|
var ENV_FILES = [".env.local", ".env"];
|
|
692
528
|
var PROJECT_ID_PATTERN = /^(?: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;
|
|
693
|
-
var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
|
|
529
|
+
var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
|
|
694
530
|
async function runInit(options = {}) {
|
|
695
531
|
const cwd = options.cwd ?? process.cwd();
|
|
696
532
|
ensureInteractiveOrBailOut(options);
|
|
@@ -850,10 +686,10 @@ function runCommand(pm, packages, cwd) {
|
|
|
850
686
|
});
|
|
851
687
|
}
|
|
852
688
|
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
853
|
-
const target = ENV_FILES.find((f) =>
|
|
854
|
-
const targetPath =
|
|
689
|
+
const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
|
|
690
|
+
const targetPath = join3(cwd, target);
|
|
855
691
|
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
856
|
-
const existing =
|
|
692
|
+
const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
|
|
857
693
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
858
694
|
p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
|
|
859
695
|
return;
|
|
@@ -888,12 +724,12 @@ function escapeRegexChar(ch) {
|
|
|
888
724
|
return /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
|
|
889
725
|
}
|
|
890
726
|
function warnIfMissingFromGitignore(cwd, envFile) {
|
|
891
|
-
const gitignorePath =
|
|
892
|
-
if (!
|
|
727
|
+
const gitignorePath = join3(cwd, ".gitignore");
|
|
728
|
+
if (!existsSync3(gitignorePath)) {
|
|
893
729
|
p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
|
|
894
730
|
return;
|
|
895
731
|
}
|
|
896
|
-
const content =
|
|
732
|
+
const content = readFileSync3(gitignorePath, "utf-8");
|
|
897
733
|
if (!isEnvFileCoveredByGitignore(content, envFile)) {
|
|
898
734
|
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
899
735
|
}
|
|
@@ -902,8 +738,8 @@ function persistCliConfig(apiKey, projectId, endpoint) {
|
|
|
902
738
|
const existing = loadConfig();
|
|
903
739
|
saveConfig({ ...existing, apiKey, projectId, endpoint });
|
|
904
740
|
}
|
|
905
|
-
function printNextSteps(framework,
|
|
906
|
-
p.note(framework.snippet(
|
|
741
|
+
function printNextSteps(framework, _apiKey, _projectId, enableRewards = false) {
|
|
742
|
+
p.note(framework.snippet(), "Add this to your app:");
|
|
907
743
|
if (enableRewards) {
|
|
908
744
|
const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
|
|
909
745
|
import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
@@ -1169,130 +1005,6 @@ function runMigrate(opts = {}) {
|
|
|
1169
1005
|
return { matches };
|
|
1170
1006
|
}
|
|
1171
1007
|
|
|
1172
|
-
// src/cli-shared.ts
|
|
1173
|
-
var API_TIMEOUT_MS = 15e3;
|
|
1174
|
-
async function apiCall(path, config, options = {}) {
|
|
1175
|
-
const endpoint = config.endpoint;
|
|
1176
|
-
if (!endpoint) {
|
|
1177
|
-
return {
|
|
1178
|
-
ok: false,
|
|
1179
|
-
error: {
|
|
1180
|
-
code: "NO_ENDPOINT",
|
|
1181
|
-
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"
|
|
1182
|
-
}
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
const controller = new AbortController();
|
|
1186
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1187
|
-
const signals = [controller.signal, getAbortSignal()];
|
|
1188
|
-
const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
|
|
1189
|
-
try {
|
|
1190
|
-
const res = await fetch(`${endpoint}${path}`, {
|
|
1191
|
-
...options,
|
|
1192
|
-
signal: compositeSignal,
|
|
1193
|
-
headers: {
|
|
1194
|
-
"Content-Type": "application/json",
|
|
1195
|
-
// `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
|
|
1196
|
-
// fallback accepted by some older middleware. Both are sent.
|
|
1197
|
-
"Authorization": `Bearer ${config.apiKey ?? ""}`,
|
|
1198
|
-
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
1199
|
-
"X-Mushi-Project": config.projectId ?? "",
|
|
1200
|
-
"X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
|
|
1201
|
-
...options.headers
|
|
1202
|
-
}
|
|
1203
|
-
});
|
|
1204
|
-
clearTimeout(timer);
|
|
1205
|
-
let body;
|
|
1206
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
1207
|
-
if (contentType.includes("application/json")) {
|
|
1208
|
-
try {
|
|
1209
|
-
body = await res.json();
|
|
1210
|
-
} catch {
|
|
1211
|
-
body = null;
|
|
1212
|
-
}
|
|
1213
|
-
} else {
|
|
1214
|
-
const text2 = await res.text();
|
|
1215
|
-
try {
|
|
1216
|
-
body = JSON.parse(text2);
|
|
1217
|
-
} catch {
|
|
1218
|
-
body = {
|
|
1219
|
-
ok: false,
|
|
1220
|
-
error: {
|
|
1221
|
-
code: `HTTP_${res.status}`,
|
|
1222
|
-
message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
|
|
1223
|
-
}
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
1228
|
-
const b = body;
|
|
1229
|
-
return {
|
|
1230
|
-
ok: false,
|
|
1231
|
-
httpStatus: res.status,
|
|
1232
|
-
error: {
|
|
1233
|
-
code: b["code"] ?? `HTTP_${res.status}`,
|
|
1234
|
-
message: b["message"] ?? `Request failed (${res.status})`
|
|
1235
|
-
}
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
return body;
|
|
1239
|
-
} catch (err) {
|
|
1240
|
-
clearTimeout(timer);
|
|
1241
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
1242
|
-
return {
|
|
1243
|
-
ok: false,
|
|
1244
|
-
error: {
|
|
1245
|
-
code: "TIMEOUT",
|
|
1246
|
-
message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
}
|
|
1250
|
-
return {
|
|
1251
|
-
ok: false,
|
|
1252
|
-
error: {
|
|
1253
|
-
code: "NETWORK_ERROR",
|
|
1254
|
-
message: err instanceof Error ? err.message : String(err)
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
function die(result, exitCode = 1) {
|
|
1260
|
-
const { code, message } = result.error;
|
|
1261
|
-
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
1262
|
-
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
1263
|
-
`);
|
|
1264
|
-
process.exit(exitCode);
|
|
1265
|
-
}
|
|
1266
|
-
function requireConfig(opts = {}) {
|
|
1267
|
-
const config = loadConfig();
|
|
1268
|
-
if (!config.apiKey) {
|
|
1269
|
-
process.stderr.write(
|
|
1270
|
-
"error: API key not configured.\n Run: mushi login --api-key <key> --endpoint <url>\n Or: export MUSHI_API_KEY=<key>\n"
|
|
1271
|
-
);
|
|
1272
|
-
process.exit(2);
|
|
1273
|
-
}
|
|
1274
|
-
if (!config.endpoint) {
|
|
1275
|
-
process.stderr.write(
|
|
1276
|
-
"error: API endpoint not configured.\n Run: mushi login --endpoint https://<ref>.supabase.co/functions/v1/api\n Or: export MUSHI_API_ENDPOINT=<url>\n"
|
|
1277
|
-
);
|
|
1278
|
-
process.exit(2);
|
|
1279
|
-
}
|
|
1280
|
-
if (opts.needsProject && !config.projectId) {
|
|
1281
|
-
process.stderr.write(
|
|
1282
|
-
"error: Project ID not configured.\n Run: mushi login --project-id <uuid>\n Or: export MUSHI_PROJECT_ID=<uuid>\n Find your project ID: https://kensaur.us/mushi-mushi/projects\n"
|
|
1283
|
-
);
|
|
1284
|
-
process.exit(2);
|
|
1285
|
-
}
|
|
1286
|
-
return config;
|
|
1287
|
-
}
|
|
1288
|
-
function fmtDate(iso) {
|
|
1289
|
-
if (!iso) return "\u2014";
|
|
1290
|
-
return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
|
|
1291
|
-
}
|
|
1292
|
-
function pad(s, width) {
|
|
1293
|
-
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
1008
|
// src/commands/account.ts
|
|
1297
1009
|
function registerAccountCommands(program2) {
|
|
1298
1010
|
program2.command("init").description("Set up the Mushi Mushi SDK in this project (auto-detects framework)").option("--project-id <id>", "Skip the prompt \u2014 pass UUID from the Projects page").option("--api-key <key>", "Skip the prompt \u2014 pass the API key (CI only)").option("--framework <id>", "Force a framework (next, react, vue, nuxt, svelte, sveltekit, angular, expo, react-native, capacitor, vanilla)").option("--skip-install", "Print the install command instead of running it").option("-y, --yes", "Accept detected framework without prompting").option("--cwd <path>", "Run the wizard in a different directory").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--skip-test-report", 'Skip the end-of-wizard "send a test report" prompt').action(async (opts) => {
|
|
@@ -1311,17 +1023,78 @@ function registerAccountCommands(program2) {
|
|
|
1311
1023
|
const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
|
|
1312
1024
|
if (matches.length === 0) process.exit(1);
|
|
1313
1025
|
});
|
|
1314
|
-
program2.command("login").description("
|
|
1026
|
+
program2.command("login").description("Authenticate the CLI \u2014 opens browser to get your API key, or pass --api-key to skip").option("--api-key <key>", "Mushi API key (mushi_...) \u2014 skip the browser flow").option("--endpoint <url>", "Supabase edge function URL (defaults to Mushi Cloud)").option("--project-id <id>", "Project UUID \u2014 skip the whoami project-list prompt").option("--no-browser", "Skip opening the browser (print sign-in URL instead)").addHelpText("after", `
|
|
1315
1027
|
Examples:
|
|
1316
|
-
mushi login
|
|
1317
|
-
mushi login --api-key mushi_xxx
|
|
1028
|
+
mushi login # browser-guided: open console \u2192 paste key
|
|
1029
|
+
mushi login --api-key mushi_xxx # non-interactive: save key directly
|
|
1030
|
+
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 { apiCall: apiCall2 } = await import("./cli-shared-FZWLZCCR.js");
|
|
1033
|
+
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
|
|
1034
|
+
const consoleUrl = "https://kensaur.us/mushi-mushi/projects";
|
|
1035
|
+
let apiKey = opts.apiKey;
|
|
1036
|
+
let projectId = opts.projectId;
|
|
1037
|
+
if (!apiKey) {
|
|
1038
|
+
const signInUrl = "https://kensaur.us/mushi-mushi/sign-in";
|
|
1039
|
+
console.log("");
|
|
1040
|
+
console.log(" Mushi login \u2014 browser-guided");
|
|
1041
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1042
|
+
if (opts.browser !== false) {
|
|
1043
|
+
console.log(" Opening the Mushi console in your browser\u2026");
|
|
1044
|
+
try {
|
|
1045
|
+
const { exec } = await import("child_process");
|
|
1046
|
+
const openCmd = process.platform === "win32" ? `start "" "${signInUrl}"` : process.platform === "darwin" ? `open "${signInUrl}"` : `xdg-open "${signInUrl}"`;
|
|
1047
|
+
exec(openCmd);
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
} else {
|
|
1051
|
+
console.log(` Sign in at: ${signInUrl}`);
|
|
1052
|
+
}
|
|
1053
|
+
console.log("");
|
|
1054
|
+
console.log(" After signing in, go to Projects \u2192 API Keys \u2192 New key");
|
|
1055
|
+
console.log(" then paste the key below.");
|
|
1056
|
+
console.log("");
|
|
1057
|
+
const { createInterface } = await import("readline");
|
|
1058
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1059
|
+
const ask = (q) => new Promise((resolve4) => rl.question(q, (a) => resolve4(a.trim())));
|
|
1060
|
+
apiKey = await ask(" API key (mushi_...): ");
|
|
1061
|
+
if (!projectId) {
|
|
1062
|
+
projectId = await ask(` Project ID (uuid, or leave blank to list projects): `);
|
|
1063
|
+
if (!projectId) projectId = void 0;
|
|
1064
|
+
}
|
|
1065
|
+
rl.close();
|
|
1066
|
+
if (!apiKey) {
|
|
1067
|
+
process.stderr.write("\nerror: API key is required.\n");
|
|
1068
|
+
process.exit(2);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1318
1071
|
const config = loadConfig();
|
|
1319
|
-
config.apiKey =
|
|
1320
|
-
|
|
1321
|
-
if (
|
|
1072
|
+
config.apiKey = apiKey;
|
|
1073
|
+
config.endpoint = endpoint;
|
|
1074
|
+
if (projectId) config.projectId = projectId;
|
|
1322
1075
|
saveConfig(config);
|
|
1323
|
-
console.log("
|
|
1324
|
-
console.log("
|
|
1076
|
+
console.log("");
|
|
1077
|
+
console.log(" \u2713 Credentials saved to ~/.config/mushi/config.json");
|
|
1078
|
+
const verifyResult = await apiCall2(
|
|
1079
|
+
"/v1/sync/whoami",
|
|
1080
|
+
{ apiKey, endpoint, projectId }
|
|
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})`);
|
|
1094
|
+
}
|
|
1095
|
+
console.log(` \u2713 Endpoint: ${endpoint}`);
|
|
1096
|
+
console.log("");
|
|
1097
|
+
console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
|
|
1325
1098
|
});
|
|
1326
1099
|
program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1327
1100
|
Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
|
|
@@ -1390,7 +1163,7 @@ Useful after 'mushi login' to confirm credentials are correct.`).action(async (o
|
|
|
1390
1163
|
console.log(`Fixes: ${d.fixes_count} total \xB7 ${d.fixes_merged} merged`);
|
|
1391
1164
|
console.log(`Lessons: ${d.lessons_count} active rules`);
|
|
1392
1165
|
});
|
|
1393
|
-
program2.command("config").description("View or update CLI config (stored in ~/.
|
|
1166
|
+
program2.command("config").description("View or update CLI config (stored in ~/.config/mushi/config.json)").argument("[key]", "Config key to set: apiKey | endpoint | projectId").argument("[value]", "New value").addHelpText("after", `
|
|
1394
1167
|
Keys:
|
|
1395
1168
|
apiKey \u2014 Mushi API key (mushi_...)
|
|
1396
1169
|
endpoint \u2014 Supabase edge function URL
|
|
@@ -1844,7 +1617,7 @@ AI code review context.
|
|
|
1844
1617
|
Typical CI usage:
|
|
1845
1618
|
MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
|
|
1846
1619
|
npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
|
|
1847
|
-
const { writeFile:
|
|
1620
|
+
const { writeFile: writeFile3, mkdir: mkdir2 } = await import("fs/promises");
|
|
1848
1621
|
const nodePath = await import("path");
|
|
1849
1622
|
const config = requireConfig();
|
|
1850
1623
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -1872,7 +1645,7 @@ Typical CI usage:
|
|
|
1872
1645
|
return;
|
|
1873
1646
|
}
|
|
1874
1647
|
await mkdir2(nodePath.dirname(target), { recursive: true });
|
|
1875
|
-
await
|
|
1648
|
+
await writeFile3(target, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
1876
1649
|
if (opts.json) {
|
|
1877
1650
|
console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
|
|
1878
1651
|
} else {
|
|
@@ -1885,14 +1658,14 @@ Typical CI usage:
|
|
|
1885
1658
|
import { createReadStream } from "fs";
|
|
1886
1659
|
import { readFile, readdir } from "fs/promises";
|
|
1887
1660
|
import { createHash } from "crypto";
|
|
1888
|
-
import { join as
|
|
1661
|
+
import { join as join4, relative, basename } from "path";
|
|
1889
1662
|
import * as p2 from "@clack/prompts";
|
|
1890
1663
|
async function findMapFiles(dir) {
|
|
1891
1664
|
const results = [];
|
|
1892
1665
|
async function walk(current) {
|
|
1893
1666
|
const entries = await readdir(current, { withFileTypes: true });
|
|
1894
1667
|
for (const e of entries) {
|
|
1895
|
-
const full =
|
|
1668
|
+
const full = join4(current, e.name);
|
|
1896
1669
|
if (e.isDirectory()) {
|
|
1897
1670
|
await walk(full);
|
|
1898
1671
|
} else if (e.isFile() && (e.name.endsWith(".js.map") || e.name.endsWith(".css.map"))) {
|
|
@@ -2067,7 +1840,7 @@ Examples:
|
|
|
2067
1840
|
mushi index ./src
|
|
2068
1841
|
mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
|
|
2069
1842
|
const config = requireConfig({ needsProject: true });
|
|
2070
|
-
const { readdir: readdir2, readFile:
|
|
1843
|
+
const { readdir: readdir2, readFile: readFile4, stat } = await import("fs/promises");
|
|
2071
1844
|
const nodePath = await import("path");
|
|
2072
1845
|
const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
|
|
2073
1846
|
const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
@@ -2094,7 +1867,7 @@ Examples:
|
|
|
2094
1867
|
`);
|
|
2095
1868
|
continue;
|
|
2096
1869
|
}
|
|
2097
|
-
const source = await
|
|
1870
|
+
const source = await readFile4(file, "utf8");
|
|
2098
1871
|
const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
|
|
2099
1872
|
count++;
|
|
2100
1873
|
bytes += source.length;
|
|
@@ -2141,6 +1914,56 @@ Examples:
|
|
|
2141
1914
|
});
|
|
2142
1915
|
}
|
|
2143
1916
|
|
|
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
|
+
|
|
2144
1967
|
// src/commands/project.ts
|
|
2145
1968
|
function registerProjectCommands(program2) {
|
|
2146
1969
|
const project = program2.command("project").description("Project management");
|
|
@@ -2155,8 +1978,8 @@ Typical first-time flow:
|
|
|
2155
1978
|
# Browser opens \u2192 sign up / magic-link \u2192 come back to terminal
|
|
2156
1979
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2157
1980
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2158
|
-
const { writeFile:
|
|
2159
|
-
const { existsSync:
|
|
1981
|
+
const { writeFile: writeFile3 } = await import("fs/promises");
|
|
1982
|
+
const { existsSync: existsSync4 } = await import("fs");
|
|
2160
1983
|
const nodePath = await import("path");
|
|
2161
1984
|
const endpoint = opts.endpoint ?? loadConfig().endpoint ?? "https://api.mushimushi.dev";
|
|
2162
1985
|
const signUpUrl = "https://kensaur.us/mushi-mushi/sign-up";
|
|
@@ -2203,41 +2026,15 @@ Typical first-time flow:
|
|
|
2203
2026
|
`MUSHI_API_KEY=${apiKey}`,
|
|
2204
2027
|
""
|
|
2205
2028
|
];
|
|
2206
|
-
const envExisting =
|
|
2207
|
-
await
|
|
2029
|
+
const envExisting = existsSync4(envPath);
|
|
2030
|
+
await writeFile3(envPath, envLines.join("\n"), "utf8");
|
|
2208
2031
|
console.log(`
|
|
2209
2032
|
\u2713 ${envExisting ? "Updated" : "Created"} .env.local`);
|
|
2210
|
-
const
|
|
2211
|
-
|
|
2212
|
-
const
|
|
2213
|
-
const
|
|
2214
|
-
|
|
2215
|
-
mushi: {
|
|
2216
|
-
command: "npx",
|
|
2217
|
-
args: ["-y", "@mushi-mushi/mcp@latest"],
|
|
2218
|
-
env: {
|
|
2219
|
-
MUSHI_API_ENDPOINT: endpoint,
|
|
2220
|
-
MUSHI_PROJECT_ID: projectId,
|
|
2221
|
-
MUSHI_API_KEY: apiKey
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
};
|
|
2226
|
-
const mcpExisting = existsSync5(mcpPath);
|
|
2227
|
-
if (mcpExisting) {
|
|
2228
|
-
try {
|
|
2229
|
-
const { readFile: readFile3 } = await import("fs/promises");
|
|
2230
|
-
const raw = JSON.parse(await readFile3(mcpPath, "utf8"));
|
|
2231
|
-
const existing = raw;
|
|
2232
|
-
existing.mcpServers = { ...existing.mcpServers ?? {}, mushi: mcpJson.mcpServers.mushi };
|
|
2233
|
-
await writeFile2(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
2234
|
-
} catch {
|
|
2235
|
-
await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
|
|
2236
|
-
}
|
|
2237
|
-
} else {
|
|
2238
|
-
await writeFile2(mcpPath, JSON.stringify(mcpJson, null, 2) + "\n", "utf8");
|
|
2239
|
-
}
|
|
2240
|
-
console.log(` \u2713 ${mcpExisting ? "Updated" : "Created"} .cursor/mcp.json`);
|
|
2033
|
+
const mcpPath = nodePath.join(cwd, ".cursor", "mcp.json");
|
|
2034
|
+
const serverName = buildMcpServerName({ legacy: true });
|
|
2035
|
+
const serverBlock = buildMcpServerBlock({ endpoint, projectId, apiKey });
|
|
2036
|
+
const { created: mcpCreated } = await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
|
|
2037
|
+
console.log(` \u2713 ${mcpCreated ? "Created" : "Updated"} .cursor/mcp.json`);
|
|
2241
2038
|
console.log("");
|
|
2242
2039
|
console.log(' Done! Restart Cursor and ask: "list mushi tools"');
|
|
2243
2040
|
console.log(" Run `mushi whoami` to verify the connection.");
|
|
@@ -2247,9 +2044,10 @@ Typical first-time flow:
|
|
|
2247
2044
|
|
|
2248
2045
|
// src/commands/setup.ts
|
|
2249
2046
|
function registerSetupCommands(program2) {
|
|
2250
|
-
program2.command("setup").description("Wire Mushi into your IDE with one command").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default:
|
|
2047
|
+
program2.command("setup").description("Wire Mushi into your IDE with one command").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", `
|
|
2251
2048
|
Examples:
|
|
2252
2049
|
mushi setup # wire Cursor (default)
|
|
2050
|
+
mushi setup --all-projects # one server entry per accessible project
|
|
2253
2051
|
mushi setup --ide claude # wire Claude Code
|
|
2254
2052
|
mushi setup --ide cursor --with-rules # also write .cursorrules
|
|
2255
2053
|
|
|
@@ -2259,12 +2057,48 @@ Supported IDEs:
|
|
|
2259
2057
|
continue \u2014 writes .continue/mcp.json
|
|
2260
2058
|
zed \u2014 writes ~/.config/zed/settings.json mcpServers block
|
|
2261
2059
|
|
|
2262
|
-
The command reads credentials from ~/.
|
|
2263
|
-
const { writeFile:
|
|
2264
|
-
const { existsSync:
|
|
2060
|
+
The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
|
|
2061
|
+
const { writeFile: writeFile3, mkdir: mkdir2, readFile: readFile4 } = await import("fs/promises");
|
|
2062
|
+
const { existsSync: existsSync4 } = await import("fs");
|
|
2265
2063
|
const nodePath = await import("path");
|
|
2266
2064
|
const os = await import("os");
|
|
2267
2065
|
const config = requireConfig({ needsProject: true });
|
|
2066
|
+
const redactKeyForDisplay = (text2) => {
|
|
2067
|
+
const key = config.apiKey;
|
|
2068
|
+
if (!key) return text2;
|
|
2069
|
+
const masked = key.length > 12 ? `${key.slice(0, 10)}\u2026${key.slice(-2)}` : "\u2022\u2022\u2022\u2022";
|
|
2070
|
+
return text2.split(key).join(masked);
|
|
2071
|
+
};
|
|
2072
|
+
let slug;
|
|
2073
|
+
if (opts.projectSlug) {
|
|
2074
|
+
slug = opts.projectSlug;
|
|
2075
|
+
} else {
|
|
2076
|
+
try {
|
|
2077
|
+
const res = await fetch(
|
|
2078
|
+
`${config.endpoint?.replace(/\/$/, "")}/v1/admin/mcp/projects`,
|
|
2079
|
+
{
|
|
2080
|
+
headers: {
|
|
2081
|
+
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
2082
|
+
"X-Mushi-Project": config.projectId ?? ""
|
|
2083
|
+
},
|
|
2084
|
+
signal: AbortSignal.timeout(5e3)
|
|
2085
|
+
}
|
|
2086
|
+
);
|
|
2087
|
+
if (res.ok) {
|
|
2088
|
+
const body = await res.json();
|
|
2089
|
+
const project = body?.data?.projects?.find((p3) => p3.id === config.projectId);
|
|
2090
|
+
if (project?.name) {
|
|
2091
|
+
slug = project.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
2092
|
+
} else {
|
|
2093
|
+
slug = config.projectId?.slice(0, 8) ?? "mushi";
|
|
2094
|
+
}
|
|
2095
|
+
} else {
|
|
2096
|
+
slug = config.projectId?.slice(0, 8) ?? "mushi";
|
|
2097
|
+
}
|
|
2098
|
+
} catch {
|
|
2099
|
+
slug = config.projectId?.slice(0, 8) ?? "mushi";
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2268
2102
|
const IDE_CONFIG = {
|
|
2269
2103
|
cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
|
|
2270
2104
|
claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
|
|
@@ -2278,7 +2112,23 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2278
2112
|
process.exit(2);
|
|
2279
2113
|
}
|
|
2280
2114
|
const cwd = process.cwd();
|
|
2281
|
-
|
|
2115
|
+
let allProjectsList = null;
|
|
2116
|
+
if (opts.allProjects && config.endpoint && config.apiKey) {
|
|
2117
|
+
try {
|
|
2118
|
+
const res = await fetch(
|
|
2119
|
+
`${config.endpoint.replace(/\/$/, "")}/v1/admin/mcp/projects`,
|
|
2120
|
+
{
|
|
2121
|
+
headers: { "X-Mushi-Api-Key": config.apiKey, "X-Mushi-Project": config.projectId ?? "" },
|
|
2122
|
+
signal: AbortSignal.timeout(8e3)
|
|
2123
|
+
}
|
|
2124
|
+
);
|
|
2125
|
+
if (res.ok) {
|
|
2126
|
+
const body = await res.json();
|
|
2127
|
+
allProjectsList = body?.data?.projects ?? [];
|
|
2128
|
+
}
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2282
2132
|
const serverName = `mushi-${slug}`;
|
|
2283
2133
|
const mcpServerBlock = {
|
|
2284
2134
|
command: "npx",
|
|
@@ -2292,31 +2142,38 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2292
2142
|
const configDir = ideEntry.dir.startsWith("/") ? ideEntry.dir : nodePath.join(cwd, ideEntry.dir);
|
|
2293
2143
|
const configPath = nodePath.join(configDir, ideEntry.file);
|
|
2294
2144
|
if (ideEntry.format === "mcp-json") {
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
const
|
|
2299
|
-
|
|
2300
|
-
|
|
2145
|
+
if (allProjectsList && allProjectsList.length > 0) {
|
|
2146
|
+
for (const p3 of allProjectsList) {
|
|
2147
|
+
const pServerName = buildMcpServerName({ projectId: p3.id, projectName: p3.name ?? void 0 });
|
|
2148
|
+
const pBlock = buildMcpServerBlock({
|
|
2149
|
+
endpoint: config.endpoint,
|
|
2150
|
+
projectId: p3.id,
|
|
2151
|
+
apiKey: config.apiKey
|
|
2152
|
+
});
|
|
2153
|
+
if (!opts.dryRun) {
|
|
2154
|
+
await writeMcpServerEntry({ configPath, serverName: pServerName, serverBlock: pBlock });
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
if (opts.dryRun) {
|
|
2158
|
+
console.log(`[dry-run] Would add ${allProjectsList.length} mushi-* entries to ${configPath}`);
|
|
2159
|
+
} else {
|
|
2160
|
+
console.log(`\u2713 Added ${allProjectsList.length} mushi-* server entries (${allProjectsList.map((p3) => p3.name ?? p3.id.slice(0, 8)).join(", ")})`);
|
|
2301
2161
|
}
|
|
2302
|
-
}
|
|
2303
|
-
const servers = merged.mcpServers ?? {};
|
|
2304
|
-
servers[serverName] = mcpServerBlock;
|
|
2305
|
-
merged.mcpServers = servers;
|
|
2306
|
-
const output = JSON.stringify(merged, null, 2) + "\n";
|
|
2307
|
-
if (opts.dryRun) {
|
|
2308
|
-
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2309
|
-
console.log(output);
|
|
2310
2162
|
} else {
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2163
|
+
if (opts.dryRun) {
|
|
2164
|
+
const preview = JSON.stringify({ mcpServers: { [serverName]: mcpServerBlock } }, null, 2) + "\n";
|
|
2165
|
+
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2166
|
+
console.log(redactKeyForDisplay(preview));
|
|
2167
|
+
} else {
|
|
2168
|
+
await writeMcpServerEntry({ configPath, serverName, serverBlock: mcpServerBlock });
|
|
2169
|
+
console.log(`\u2713 Written ${configPath}`);
|
|
2170
|
+
}
|
|
2314
2171
|
}
|
|
2315
2172
|
} else if (ideEntry.format === "zed") {
|
|
2316
2173
|
let settings = {};
|
|
2317
|
-
if (
|
|
2174
|
+
if (existsSync4(configPath)) {
|
|
2318
2175
|
try {
|
|
2319
|
-
const raw = await
|
|
2176
|
+
const raw = await readFile4(configPath, "utf8");
|
|
2320
2177
|
settings = JSON.parse(raw);
|
|
2321
2178
|
} catch {
|
|
2322
2179
|
}
|
|
@@ -2338,10 +2195,10 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2338
2195
|
const output = JSON.stringify(settings, null, 2) + "\n";
|
|
2339
2196
|
if (opts.dryRun) {
|
|
2340
2197
|
console.log(`[dry-run] Would write ${configPath}:`);
|
|
2341
|
-
console.log(output);
|
|
2198
|
+
console.log(redactKeyForDisplay(output));
|
|
2342
2199
|
} else {
|
|
2343
2200
|
await mkdir2(configDir, { recursive: true });
|
|
2344
|
-
await
|
|
2201
|
+
await writeFile3(configPath, output, "utf8");
|
|
2345
2202
|
console.log(`\u2713 Written ${configPath}`);
|
|
2346
2203
|
}
|
|
2347
2204
|
}
|
|
@@ -2375,7 +2232,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2375
2232
|
if (opts.dryRun) {
|
|
2376
2233
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2377
2234
|
} else {
|
|
2378
|
-
await
|
|
2235
|
+
await writeFile3(rulesPath, rulesContent, "utf8");
|
|
2379
2236
|
console.log(`\u2713 Written .cursorrules`);
|
|
2380
2237
|
}
|
|
2381
2238
|
} else if (opts.ide === "claude") {
|
|
@@ -2385,7 +2242,7 @@ The command reads credentials from ~/.mushirc (run \`mushi login\` first).`).act
|
|
|
2385
2242
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
2386
2243
|
} else {
|
|
2387
2244
|
await mkdir2(rulesDir, { recursive: true });
|
|
2388
|
-
await
|
|
2245
|
+
await writeFile3(rulesPath, rulesContent, "utf8");
|
|
2389
2246
|
console.log(`\u2713 Written .claude/rules/mushi.md`);
|
|
2390
2247
|
}
|
|
2391
2248
|
}
|
|
@@ -2515,7 +2372,7 @@ function renderNudgeExplainer(phase) {
|
|
|
2515
2372
|
|
|
2516
2373
|
// src/sanitize-config.ts
|
|
2517
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;
|
|
2518
|
-
var API_KEY_RE = /^mushi_[A-Za-z0-9_-]{10,}$/;
|
|
2375
|
+
var API_KEY_RE = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
|
|
2519
2376
|
function sanitizeApiKey(raw) {
|
|
2520
2377
|
const key = raw.replace(/[\r\n\0]/g, "");
|
|
2521
2378
|
if (!API_KEY_RE.test(key)) {
|
|
@@ -2662,11 +2519,11 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
|
2662
2519
|
}
|
|
2663
2520
|
async function checkSdkInstall(cwd) {
|
|
2664
2521
|
try {
|
|
2665
|
-
const { readFile:
|
|
2666
|
-
const { join:
|
|
2522
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
2523
|
+
const { join: join6, resolve: resolve4 } = await import("path");
|
|
2667
2524
|
const root = resolve4(cwd);
|
|
2668
|
-
const pkgPath =
|
|
2669
|
-
const pkg = JSON.parse(await
|
|
2525
|
+
const pkgPath = join6(root, "package.json");
|
|
2526
|
+
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
2670
2527
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2671
2528
|
const sdks = [
|
|
2672
2529
|
"@mushi-mushi/react",
|
|
@@ -2674,11 +2531,11 @@ async function checkSdkInstall(cwd) {
|
|
|
2674
2531
|
"@mushi-mushi/core",
|
|
2675
2532
|
"@mushi-mushi/react-native"
|
|
2676
2533
|
];
|
|
2677
|
-
const
|
|
2534
|
+
const installed = sdks.filter((s) => deps[s]);
|
|
2678
2535
|
return {
|
|
2679
2536
|
name: "SDK installed in this repo",
|
|
2680
|
-
ok:
|
|
2681
|
-
detail:
|
|
2537
|
+
ok: installed.length > 0,
|
|
2538
|
+
detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
|
|
2682
2539
|
};
|
|
2683
2540
|
} catch {
|
|
2684
2541
|
return null;
|
|
@@ -2844,18 +2701,18 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
|
2844
2701
|
async function checkHostAppWiring(cwd) {
|
|
2845
2702
|
const checks = [];
|
|
2846
2703
|
try {
|
|
2847
|
-
const { readFile:
|
|
2848
|
-
const { join:
|
|
2704
|
+
const { readFile: readFile4, access } = await import("fs/promises");
|
|
2705
|
+
const { join: join6, resolve: resolve4 } = await import("path");
|
|
2849
2706
|
const root = resolve4(cwd);
|
|
2850
|
-
const pkgPath =
|
|
2851
|
-
const pkg = JSON.parse(await
|
|
2707
|
+
const pkgPath = join6(root, "package.json");
|
|
2708
|
+
const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
|
|
2852
2709
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2853
2710
|
const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
|
|
2854
2711
|
const envCandidates = [".env.local", ".env"];
|
|
2855
2712
|
let envContent = "";
|
|
2856
2713
|
for (const f of envCandidates) {
|
|
2857
2714
|
try {
|
|
2858
|
-
envContent = await
|
|
2715
|
+
envContent = await readFile4(join6(root, f), "utf8");
|
|
2859
2716
|
break;
|
|
2860
2717
|
} catch {
|
|
2861
2718
|
}
|
|
@@ -2869,7 +2726,7 @@ async function checkHostAppWiring(cwd) {
|
|
|
2869
2726
|
});
|
|
2870
2727
|
let mcpPresent = false;
|
|
2871
2728
|
try {
|
|
2872
|
-
await access(
|
|
2729
|
+
await access(join6(root, ".cursor", "mcp.json"));
|
|
2873
2730
|
mcpPresent = true;
|
|
2874
2731
|
} catch {
|
|
2875
2732
|
}
|
|
@@ -2900,18 +2757,128 @@ async function checkHostAppWiring(cwd) {
|
|
|
2900
2757
|
}
|
|
2901
2758
|
return checks;
|
|
2902
2759
|
}
|
|
2760
|
+
async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
2761
|
+
const checks = [];
|
|
2762
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
2763
|
+
const { join: join6, resolve: resolve4 } = await import("path");
|
|
2764
|
+
const { homedir } = await import("os");
|
|
2765
|
+
const root = resolve4(cwd);
|
|
2766
|
+
const candidates = [
|
|
2767
|
+
join6(root, ".cursor", "mcp.json"),
|
|
2768
|
+
join6(homedir(), ".cursor", "mcp.json")
|
|
2769
|
+
];
|
|
2770
|
+
let mcpPath = null;
|
|
2771
|
+
let mcpRaw = null;
|
|
2772
|
+
for (const candidate of candidates) {
|
|
2773
|
+
try {
|
|
2774
|
+
mcpRaw = await readFile4(candidate, "utf8");
|
|
2775
|
+
mcpPath = candidate;
|
|
2776
|
+
break;
|
|
2777
|
+
} catch {
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
if (!mcpPath || !mcpRaw) {
|
|
2781
|
+
checks.push({
|
|
2782
|
+
name: "[mcp] mcp.json present",
|
|
2783
|
+
ok: false,
|
|
2784
|
+
detail: "No .cursor/mcp.json found in cwd or ~/.cursor/. Run `mushi setup` to create it."
|
|
2785
|
+
});
|
|
2786
|
+
return checks;
|
|
2787
|
+
}
|
|
2788
|
+
checks.push({
|
|
2789
|
+
name: "[mcp] mcp.json present",
|
|
2790
|
+
ok: true,
|
|
2791
|
+
detail: `Found at ${mcpPath}`
|
|
2792
|
+
});
|
|
2793
|
+
let mcpConfig = {};
|
|
2794
|
+
try {
|
|
2795
|
+
mcpConfig = JSON.parse(mcpRaw);
|
|
2796
|
+
} catch {
|
|
2797
|
+
checks.push({ name: "[mcp] mcp.json valid JSON", ok: false, detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`." });
|
|
2798
|
+
return checks;
|
|
2799
|
+
}
|
|
2800
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
2801
|
+
const mushiEntries = Object.entries(servers).filter(([k]) => k === "mushi" || k.startsWith("mushi-"));
|
|
2802
|
+
if (mushiEntries.length === 0) {
|
|
2803
|
+
checks.push({
|
|
2804
|
+
name: "[mcp] mushi server entry",
|
|
2805
|
+
ok: false,
|
|
2806
|
+
detail: "No mushi or mushi-* server found in mcpServers. Run `mushi setup` to add one."
|
|
2807
|
+
});
|
|
2808
|
+
return checks;
|
|
2809
|
+
}
|
|
2810
|
+
checks.push({
|
|
2811
|
+
name: "[mcp] mushi server entry",
|
|
2812
|
+
ok: true,
|
|
2813
|
+
detail: `Found: ${mushiEntries.map(([k]) => k).join(", ")}`
|
|
2814
|
+
});
|
|
2815
|
+
let anyKeyValid = false;
|
|
2816
|
+
let anyEndpointSet = false;
|
|
2817
|
+
for (const [, srv] of mushiEntries) {
|
|
2818
|
+
const s = srv;
|
|
2819
|
+
const env = s.env ?? {};
|
|
2820
|
+
const key = env["MUSHI_API_KEY"] ?? "";
|
|
2821
|
+
const endpoint = env["MUSHI_API_ENDPOINT"] ?? "";
|
|
2822
|
+
if (key.startsWith("mushi_")) anyKeyValid = true;
|
|
2823
|
+
if (endpoint.includes("supabase.co") || endpoint.includes("localhost")) anyEndpointSet = true;
|
|
2824
|
+
}
|
|
2825
|
+
checks.push({
|
|
2826
|
+
name: "[mcp] MUSHI_API_KEY set",
|
|
2827
|
+
ok: anyKeyValid,
|
|
2828
|
+
detail: anyKeyValid ? "At least one mushi server has a valid mushi_* API key" : "No mushi_* API key found in any mushi server env. Re-run `mushi setup` to regenerate."
|
|
2829
|
+
});
|
|
2830
|
+
checks.push({
|
|
2831
|
+
name: "[mcp] MUSHI_API_ENDPOINT set",
|
|
2832
|
+
ok: anyEndpointSet,
|
|
2833
|
+
detail: anyEndpointSet ? "MUSHI_API_ENDPOINT is present and looks valid" : "MUSHI_API_ENDPOINT missing or not a Supabase URL. Re-run `mushi setup`."
|
|
2834
|
+
});
|
|
2835
|
+
if (anyKeyValid && anyEndpointSet && config.apiKey && config.endpoint) {
|
|
2836
|
+
try {
|
|
2837
|
+
const { endpoint, apiKey } = sanitizeCliCredentials(config);
|
|
2838
|
+
const res = await doFetch(`${endpoint}/v1/admin/mcp/account-overview`, {
|
|
2839
|
+
headers: apiKeyHeaders(apiKey, config.projectId),
|
|
2840
|
+
signal: AbortSignal.timeout(6e3)
|
|
2841
|
+
});
|
|
2842
|
+
if (res.ok) {
|
|
2843
|
+
const body = await res.json();
|
|
2844
|
+
const projectCount = body?.data?.total ?? 0;
|
|
2845
|
+
checks.push({
|
|
2846
|
+
name: "[mcp] account-overview reachable",
|
|
2847
|
+
ok: true,
|
|
2848
|
+
detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
|
|
2849
|
+
});
|
|
2850
|
+
} else {
|
|
2851
|
+
checks.push({
|
|
2852
|
+
name: "[mcp] account-overview reachable",
|
|
2853
|
+
ok: false,
|
|
2854
|
+
detail: `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
} catch (err) {
|
|
2858
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2859
|
+
checks.push({
|
|
2860
|
+
name: "[mcp] account-overview reachable",
|
|
2861
|
+
ok: false,
|
|
2862
|
+
detail: `Probe failed: ${msg}. Check MUSHI_API_ENDPOINT and network connectivity.`
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return checks;
|
|
2867
|
+
}
|
|
2903
2868
|
var FIX_HINTS = {
|
|
2904
2869
|
"CLI config file": "Run `mushi connect --endpoint <url> --project-id <uuid> --api-key mushi_xxx` or `mushi config endpoint <url>`.",
|
|
2905
2870
|
"API key configured": "Mint a key in the console (Projects \u2192 API Keys) then `mushi login --api-key mushi_xxx`.",
|
|
2906
2871
|
"Project ID configured": "Copy the project UUID from the console Projects page \u2192 `mushi config projectId <uuid>`.",
|
|
2907
2872
|
"Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
|
|
2908
2873
|
"[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
|
|
2909
|
-
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix."
|
|
2874
|
+
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
|
|
2875
|
+
"[mcp]": "Run `mushi setup` to regenerate .cursor/mcp.json with a fresh API key and endpoint."
|
|
2910
2876
|
};
|
|
2911
2877
|
function fixHintForCheck(name) {
|
|
2912
2878
|
if (FIX_HINTS[name]) return FIX_HINTS[name];
|
|
2913
2879
|
if (name.startsWith("[ingest]")) return FIX_HINTS["[ingest]"];
|
|
2914
2880
|
if (name.startsWith("[server]") || name.startsWith("[preflight]")) return FIX_HINTS["[server]"];
|
|
2881
|
+
if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
|
|
2915
2882
|
return void 0;
|
|
2916
2883
|
}
|
|
2917
2884
|
async function runDoctor(config, options = {}) {
|
|
@@ -2941,6 +2908,10 @@ async function runDoctor(config, options = {}) {
|
|
|
2941
2908
|
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
2942
2909
|
checks.push(...hostChecks);
|
|
2943
2910
|
}
|
|
2911
|
+
if (options.mcp) {
|
|
2912
|
+
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
2913
|
+
checks.push(...mcpChecks);
|
|
2914
|
+
}
|
|
2944
2915
|
return { checks, ready: checks.every((c) => c.ok) };
|
|
2945
2916
|
}
|
|
2946
2917
|
function formatDoctorResult(result) {
|
|
@@ -2987,9 +2958,9 @@ async function planUpgrade(cwd) {
|
|
|
2987
2958
|
...pkg?.dependencies ?? {},
|
|
2988
2959
|
...pkg?.devDependencies ?? {}
|
|
2989
2960
|
};
|
|
2990
|
-
const
|
|
2961
|
+
const installed = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
2991
2962
|
const entries = await Promise.all(
|
|
2992
|
-
|
|
2963
|
+
installed.map(async (name) => {
|
|
2993
2964
|
const current = deps[name] ?? "";
|
|
2994
2965
|
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
2995
2966
|
if (!/^\d/.test(currentCore)) {
|
|
@@ -3061,8 +3032,8 @@ async function runUpgrade(opts = {}) {
|
|
|
3061
3032
|
}
|
|
3062
3033
|
|
|
3063
3034
|
// src/connect.ts
|
|
3064
|
-
import { appendFile,
|
|
3065
|
-
import { join as
|
|
3035
|
+
import { appendFile, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
3036
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
3066
3037
|
function envKeyPresent(content, key) {
|
|
3067
3038
|
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3068
3039
|
return new RegExp(`^${escaped}=`, "m").test(content);
|
|
@@ -3074,7 +3045,7 @@ ${lines.join("\n")}
|
|
|
3074
3045
|
`;
|
|
3075
3046
|
let existing = null;
|
|
3076
3047
|
try {
|
|
3077
|
-
existing = await
|
|
3048
|
+
existing = await readFile3(path, "utf8");
|
|
3078
3049
|
} catch {
|
|
3079
3050
|
}
|
|
3080
3051
|
if (existing !== null) {
|
|
@@ -3089,15 +3060,15 @@ ${needs.join("\n")}
|
|
|
3089
3060
|
`, "utf8");
|
|
3090
3061
|
return true;
|
|
3091
3062
|
}
|
|
3092
|
-
await
|
|
3063
|
+
await writeFile2(path, block, "utf8");
|
|
3093
3064
|
return true;
|
|
3094
3065
|
}
|
|
3095
3066
|
async function ensureMcpJsonGitignored(cwd, messages) {
|
|
3096
|
-
const gitignorePath =
|
|
3067
|
+
const gitignorePath = join5(cwd, ".gitignore");
|
|
3097
3068
|
const patterns = [".cursor/mcp.json", ".cursor/"];
|
|
3098
3069
|
let content = null;
|
|
3099
3070
|
try {
|
|
3100
|
-
content = await
|
|
3071
|
+
content = await readFile3(gitignorePath, "utf8");
|
|
3101
3072
|
} catch {
|
|
3102
3073
|
}
|
|
3103
3074
|
if (content === null) {
|
|
@@ -3128,7 +3099,7 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
3128
3099
|
const pkg = readPackageJson(cwd);
|
|
3129
3100
|
const framework = detectFramework(cwd, pkg);
|
|
3130
3101
|
const lines = envVarsToWrite(opts.apiKey, opts.projectId, framework).split("\n");
|
|
3131
|
-
envPath =
|
|
3102
|
+
envPath = join5(cwd, ".env.local");
|
|
3132
3103
|
const wrote = await mergeEnvFile(envPath, lines);
|
|
3133
3104
|
messages.push(
|
|
3134
3105
|
wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
|
|
@@ -3142,28 +3113,15 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
3142
3113
|
}
|
|
3143
3114
|
let mcpPath = null;
|
|
3144
3115
|
if (opts.wireIde !== false) {
|
|
3145
|
-
const mcpDir =
|
|
3146
|
-
mcpPath =
|
|
3147
|
-
const serverName =
|
|
3148
|
-
const
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
MUSHI_API_KEY: opts.apiKey
|
|
3155
|
-
}
|
|
3156
|
-
};
|
|
3157
|
-
let merged = { mcpServers: {} };
|
|
3158
|
-
try {
|
|
3159
|
-
merged = JSON.parse(await readFile2(mcpPath, "utf8"));
|
|
3160
|
-
} catch {
|
|
3161
|
-
}
|
|
3162
|
-
const servers = merged.mcpServers ?? {};
|
|
3163
|
-
servers[serverName] = mcpServerBlock;
|
|
3164
|
-
merged.mcpServers = servers;
|
|
3165
|
-
await mkdir(mcpDir, { recursive: true });
|
|
3166
|
-
await writeFile(mcpPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
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 });
|
|
3167
3125
|
await ensureMcpJsonGitignored(cwd, messages);
|
|
3168
3126
|
messages.push(`\u2713 Wired ${mcpPath} \u2014 restart Cursor and run "list mushi tools"`);
|
|
3169
3127
|
}
|
|
@@ -3379,12 +3337,15 @@ Examples:
|
|
|
3379
3337
|
).option(
|
|
3380
3338
|
"--host-app",
|
|
3381
3339
|
"Verify host-app wiring: Mushi env vars, Cursor MCP config, Capacitor hybrid SDK notes."
|
|
3340
|
+
).option(
|
|
3341
|
+
"--mcp",
|
|
3342
|
+
"Check Cursor MCP config: verify mushi-* server entry, credentials, and probe account-overview connectivity."
|
|
3382
3343
|
).option(
|
|
3383
3344
|
"--fix",
|
|
3384
3345
|
"Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
|
|
3385
3346
|
).action(async (opts) => {
|
|
3386
3347
|
const config = loadConfig();
|
|
3387
|
-
const doctorOpts = { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories, hostApp: opts.hostApp };
|
|
3348
|
+
const doctorOpts = { cwd: opts.cwd, server: opts.server, ingest: opts.ingest, qaStories: opts.qaStories, hostApp: opts.hostApp, mcp: opts.mcp };
|
|
3388
3349
|
let result = await runDoctor(config, doctorOpts);
|
|
3389
3350
|
if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
|
|
3390
3351
|
const connectResult = await runConnect({
|
|
@@ -3396,7 +3357,7 @@ Examples:
|
|
|
3396
3357
|
wireIde: true
|
|
3397
3358
|
}, config);
|
|
3398
3359
|
for (const msg of connectResult.messages) console.log(msg);
|
|
3399
|
-
result = await runDoctor(config, doctorOpts);
|
|
3360
|
+
result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
|
|
3400
3361
|
}
|
|
3401
3362
|
const { checks } = result;
|
|
3402
3363
|
if (opts.json) {
|