@mushi-mushi/cli 0.2.0 → 0.4.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/README.md +40 -10
- package/dist/chunk-YZOGONU4.js +238 -0
- package/dist/detect.d.ts +28 -0
- package/dist/detect.js +16 -0
- package/dist/index.js +397 -1
- package/dist/init.d.ts +22 -0
- package/dist/init.js +185 -0
- package/package.json +36 -9
package/README.md
CHANGED
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
# @mushi-mushi/cli
|
|
2
2
|
|
|
3
|
-
CLI
|
|
3
|
+
CLI for Mushi Mushi — set up the SDK in one command, then triage reports and monitor the pipeline from your terminal.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## One-command setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @mushi-mushi/cli init
|
|
9
|
+
# or, equivalently:
|
|
10
|
+
npx mushi-mushi init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The wizard:
|
|
14
|
+
|
|
15
|
+
1. Detects your framework (Next.js, Nuxt, SvelteKit, Angular, Expo, Capacitor, plain React/Vue/Svelte, or vanilla JS) from `package.json` and config files.
|
|
16
|
+
2. Picks the right SDK package (`@mushi-mushi/react`, `@mushi-mushi/vue`, etc.) plus `@mushi-mushi/web` when the framework SDK is API-only.
|
|
17
|
+
3. Detects your package manager (npm / pnpm / yarn / bun) from your lockfile and installs with that.
|
|
18
|
+
4. Writes `MUSHI_PROJECT_ID` and `MUSHI_API_KEY` (with the right framework prefix — `NEXT_PUBLIC_`, `NUXT_PUBLIC_`, `VITE_`) to `.env.local` (or `.env`).
|
|
19
|
+
5. Warns you if `.env.local` isn't in `.gitignore`.
|
|
20
|
+
6. Prints the framework-specific provider snippet to copy-paste.
|
|
21
|
+
|
|
22
|
+
It never silently overwrites existing env vars or modifies application code.
|
|
23
|
+
|
|
24
|
+
### Flags
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
mushi init --framework next # skip framework detection
|
|
28
|
+
mushi init --project-id proj_xxx --api-key mushi_xxx # skip credential prompts
|
|
29
|
+
mushi init --skip-install # print the install command instead of running it
|
|
30
|
+
mushi init -y # accept the detected framework without confirmation
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Install globally
|
|
6
34
|
|
|
7
35
|
```bash
|
|
8
36
|
npm install -g @mushi-mushi/cli
|
|
37
|
+
mushi --help
|
|
9
38
|
```
|
|
10
39
|
|
|
11
|
-
##
|
|
40
|
+
## Other commands
|
|
12
41
|
|
|
13
42
|
```bash
|
|
14
|
-
mushi login
|
|
15
|
-
mushi status
|
|
16
|
-
mushi reports list
|
|
17
|
-
mushi reports show <id>
|
|
18
|
-
mushi reports triage <id> --
|
|
19
|
-
mushi deploy check
|
|
20
|
-
mushi
|
|
43
|
+
mushi login --api-key mushi_xxx # store credentials in ~/.mushirc
|
|
44
|
+
mushi status # project overview
|
|
45
|
+
mushi reports list # recent reports
|
|
46
|
+
mushi reports show <id> # one report
|
|
47
|
+
mushi reports triage <id> --status acknowledged --severity high
|
|
48
|
+
mushi deploy check # edge-function health probe
|
|
49
|
+
mushi index <path> # walk a local repo and feed RAG
|
|
50
|
+
mushi test # submit a test report end-to-end
|
|
21
51
|
```
|
|
22
52
|
|
|
23
53
|
## License
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// src/detect.ts
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
var FRAMEWORKS = {
|
|
5
|
+
next: {
|
|
6
|
+
id: "next",
|
|
7
|
+
label: "Next.js",
|
|
8
|
+
packageName: "@mushi-mushi/react",
|
|
9
|
+
needsWebPackage: false,
|
|
10
|
+
snippet: (apiKey, projectId) => `// app/providers.tsx (or pages/_app.tsx for /pages router)
|
|
11
|
+
'use client'
|
|
12
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
13
|
+
|
|
14
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
15
|
+
return (
|
|
16
|
+
<MushiProvider config={{
|
|
17
|
+
projectId: '${projectId}',
|
|
18
|
+
apiKey: '${apiKey}',
|
|
19
|
+
}}>
|
|
20
|
+
{children}
|
|
21
|
+
</MushiProvider>
|
|
22
|
+
)
|
|
23
|
+
}`
|
|
24
|
+
},
|
|
25
|
+
react: {
|
|
26
|
+
id: "react",
|
|
27
|
+
label: "React",
|
|
28
|
+
packageName: "@mushi-mushi/react",
|
|
29
|
+
needsWebPackage: false,
|
|
30
|
+
snippet: (apiKey, projectId) => `// src/main.tsx
|
|
31
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
32
|
+
|
|
33
|
+
createRoot(document.getElementById('root')!).render(
|
|
34
|
+
<MushiProvider config={{
|
|
35
|
+
projectId: '${projectId}',
|
|
36
|
+
apiKey: '${apiKey}',
|
|
37
|
+
}}>
|
|
38
|
+
<App />
|
|
39
|
+
</MushiProvider>
|
|
40
|
+
)`
|
|
41
|
+
},
|
|
42
|
+
vue: {
|
|
43
|
+
id: "vue",
|
|
44
|
+
label: "Vue 3",
|
|
45
|
+
packageName: "@mushi-mushi/vue",
|
|
46
|
+
needsWebPackage: true,
|
|
47
|
+
snippet: (apiKey, projectId) => `// src/main.ts
|
|
48
|
+
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
49
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
50
|
+
|
|
51
|
+
app.use(MushiPlugin, { projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
52
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
53
|
+
},
|
|
54
|
+
nuxt: {
|
|
55
|
+
id: "nuxt",
|
|
56
|
+
label: "Nuxt",
|
|
57
|
+
packageName: "@mushi-mushi/vue",
|
|
58
|
+
needsWebPackage: true,
|
|
59
|
+
snippet: (apiKey, projectId) => `// plugins/mushi.client.ts
|
|
60
|
+
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
61
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
62
|
+
|
|
63
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
64
|
+
nuxtApp.vueApp.use(MushiPlugin, {
|
|
65
|
+
projectId: '${projectId}',
|
|
66
|
+
apiKey: '${apiKey}',
|
|
67
|
+
})
|
|
68
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
69
|
+
})`
|
|
70
|
+
},
|
|
71
|
+
svelte: {
|
|
72
|
+
id: "svelte",
|
|
73
|
+
label: "Svelte",
|
|
74
|
+
packageName: "@mushi-mushi/svelte",
|
|
75
|
+
needsWebPackage: true,
|
|
76
|
+
snippet: (apiKey, projectId) => `// src/main.ts (or +layout.svelte for SvelteKit)
|
|
77
|
+
import { initMushi } from '@mushi-mushi/svelte'
|
|
78
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
79
|
+
|
|
80
|
+
initMushi({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
81
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
82
|
+
},
|
|
83
|
+
sveltekit: {
|
|
84
|
+
id: "sveltekit",
|
|
85
|
+
label: "SvelteKit",
|
|
86
|
+
packageName: "@mushi-mushi/svelte",
|
|
87
|
+
needsWebPackage: true,
|
|
88
|
+
snippet: (apiKey, projectId) => `// src/routes/+layout.svelte
|
|
89
|
+
<script>
|
|
90
|
+
import { onMount } from 'svelte'
|
|
91
|
+
import { initMushi } from '@mushi-mushi/svelte'
|
|
92
|
+
|
|
93
|
+
onMount(async () => {
|
|
94
|
+
const { Mushi } = await import('@mushi-mushi/web')
|
|
95
|
+
initMushi({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
96
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
97
|
+
})
|
|
98
|
+
</script>`
|
|
99
|
+
},
|
|
100
|
+
angular: {
|
|
101
|
+
id: "angular",
|
|
102
|
+
label: "Angular",
|
|
103
|
+
packageName: "@mushi-mushi/angular",
|
|
104
|
+
needsWebPackage: true,
|
|
105
|
+
snippet: (apiKey, projectId) => `// src/main.ts
|
|
106
|
+
import { provideMushi } from '@mushi-mushi/angular'
|
|
107
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
108
|
+
|
|
109
|
+
bootstrapApplication(AppComponent, {
|
|
110
|
+
providers: [
|
|
111
|
+
provideMushi({ projectId: '${projectId}', apiKey: '${apiKey}' }),
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
115
|
+
},
|
|
116
|
+
expo: {
|
|
117
|
+
id: "expo",
|
|
118
|
+
label: "Expo",
|
|
119
|
+
packageName: "@mushi-mushi/react-native",
|
|
120
|
+
needsWebPackage: false,
|
|
121
|
+
snippet: (apiKey, projectId) => `// App.tsx
|
|
122
|
+
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
123
|
+
|
|
124
|
+
export default function App() {
|
|
125
|
+
return (
|
|
126
|
+
<MushiProvider projectId="${projectId}" apiKey="${apiKey}">
|
|
127
|
+
<YourApp />
|
|
128
|
+
</MushiProvider>
|
|
129
|
+
)
|
|
130
|
+
}`
|
|
131
|
+
},
|
|
132
|
+
"react-native": {
|
|
133
|
+
id: "react-native",
|
|
134
|
+
label: "React Native",
|
|
135
|
+
packageName: "@mushi-mushi/react-native",
|
|
136
|
+
needsWebPackage: false,
|
|
137
|
+
snippet: (apiKey, projectId) => `// App.tsx
|
|
138
|
+
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
139
|
+
|
|
140
|
+
export default function App() {
|
|
141
|
+
return (
|
|
142
|
+
<MushiProvider projectId="${projectId}" apiKey="${apiKey}">
|
|
143
|
+
<YourApp />
|
|
144
|
+
</MushiProvider>
|
|
145
|
+
)
|
|
146
|
+
}`
|
|
147
|
+
},
|
|
148
|
+
capacitor: {
|
|
149
|
+
id: "capacitor",
|
|
150
|
+
label: "Capacitor (Ionic)",
|
|
151
|
+
packageName: "@mushi-mushi/capacitor",
|
|
152
|
+
needsWebPackage: false,
|
|
153
|
+
snippet: (apiKey, projectId) => `// src/main.ts
|
|
154
|
+
import { Mushi } from '@mushi-mushi/capacitor'
|
|
155
|
+
|
|
156
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
157
|
+
},
|
|
158
|
+
vanilla: {
|
|
159
|
+
id: "vanilla",
|
|
160
|
+
label: "Vanilla JS / unknown",
|
|
161
|
+
packageName: "@mushi-mushi/web",
|
|
162
|
+
needsWebPackage: false,
|
|
163
|
+
snippet: (apiKey, projectId) => `// Anywhere in your client bundle
|
|
164
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
165
|
+
|
|
166
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
function readPackageJson(cwd) {
|
|
170
|
+
const path = join(cwd, "package.json");
|
|
171
|
+
if (!existsSync(path)) return null;
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function detectFramework(cwd, pkg) {
|
|
179
|
+
const deps = collectDeps(pkg);
|
|
180
|
+
if (deps.has("next")) return FRAMEWORKS.next;
|
|
181
|
+
if (deps.has("nuxt")) return FRAMEWORKS.nuxt;
|
|
182
|
+
if (deps.has("@sveltejs/kit")) return FRAMEWORKS.sveltekit;
|
|
183
|
+
if (deps.has("@angular/core")) return FRAMEWORKS.angular;
|
|
184
|
+
if (deps.has("expo")) return FRAMEWORKS.expo;
|
|
185
|
+
if (deps.has("react-native")) return FRAMEWORKS["react-native"];
|
|
186
|
+
if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
|
|
187
|
+
if (deps.has("svelte")) return FRAMEWORKS.svelte;
|
|
188
|
+
if (deps.has("vue")) return FRAMEWORKS.vue;
|
|
189
|
+
if (deps.has("react")) return FRAMEWORKS.react;
|
|
190
|
+
if (existsSync(join(cwd, "next.config.js")) || existsSync(join(cwd, "next.config.ts"))) {
|
|
191
|
+
return FRAMEWORKS.next;
|
|
192
|
+
}
|
|
193
|
+
if (existsSync(join(cwd, "nuxt.config.ts")) || existsSync(join(cwd, "nuxt.config.js"))) {
|
|
194
|
+
return FRAMEWORKS.nuxt;
|
|
195
|
+
}
|
|
196
|
+
if (existsSync(join(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
|
|
197
|
+
if (existsSync(join(cwd, "angular.json"))) return FRAMEWORKS.angular;
|
|
198
|
+
return FRAMEWORKS.vanilla;
|
|
199
|
+
}
|
|
200
|
+
function detectPackageManager(cwd) {
|
|
201
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
|
|
202
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
203
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
204
|
+
if (existsSync(join(cwd, "package-lock.json"))) return "npm";
|
|
205
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
206
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
207
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
208
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
209
|
+
return "npm";
|
|
210
|
+
}
|
|
211
|
+
function installCommand(pm, packages) {
|
|
212
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
213
|
+
return `${pm} ${verb} ${packages.join(" ")}`;
|
|
214
|
+
}
|
|
215
|
+
function envVarsToWrite(apiKey, projectId, framework) {
|
|
216
|
+
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
|
|
217
|
+
return [
|
|
218
|
+
`${prefix}MUSHI_PROJECT_ID=${projectId}`,
|
|
219
|
+
`${prefix}MUSHI_API_KEY=${apiKey}`
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
function collectDeps(pkg) {
|
|
223
|
+
if (!pkg) return /* @__PURE__ */ new Set();
|
|
224
|
+
return /* @__PURE__ */ new Set([
|
|
225
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
226
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
227
|
+
...Object.keys(pkg.peerDependencies ?? {})
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
FRAMEWORKS,
|
|
233
|
+
readPackageJson,
|
|
234
|
+
detectFramework,
|
|
235
|
+
detectPackageManager,
|
|
236
|
+
installCommand,
|
|
237
|
+
envVarsToWrite
|
|
238
|
+
};
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FILE: packages/cli/src/detect.ts
|
|
3
|
+
* PURPOSE: Pure detection helpers for framework, package manager, and project state.
|
|
4
|
+
* Kept side-effect-free so the wizard remains unit-testable.
|
|
5
|
+
*/
|
|
6
|
+
type FrameworkId = 'next' | 'react' | 'vue' | 'nuxt' | 'svelte' | 'sveltekit' | 'angular' | 'expo' | 'react-native' | 'capacitor' | 'vanilla';
|
|
7
|
+
interface Framework {
|
|
8
|
+
id: FrameworkId;
|
|
9
|
+
label: string;
|
|
10
|
+
packageName: string;
|
|
11
|
+
needsWebPackage: boolean;
|
|
12
|
+
snippet: (apiKey: string, projectId: string) => string;
|
|
13
|
+
}
|
|
14
|
+
interface PackageJson {
|
|
15
|
+
name?: string;
|
|
16
|
+
dependencies?: Record<string, string>;
|
|
17
|
+
devDependencies?: Record<string, string>;
|
|
18
|
+
peerDependencies?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
21
|
+
declare const FRAMEWORKS: Record<FrameworkId, Framework>;
|
|
22
|
+
declare function readPackageJson(cwd: string): PackageJson | null;
|
|
23
|
+
declare function detectFramework(cwd: string, pkg: PackageJson | null): Framework;
|
|
24
|
+
declare function detectPackageManager(cwd: string): PackageManager;
|
|
25
|
+
declare function installCommand(pm: PackageManager, packages: string[]): string;
|
|
26
|
+
declare function envVarsToWrite(apiKey: string, projectId: string, framework: Framework): string;
|
|
27
|
+
|
|
28
|
+
export { FRAMEWORKS, type Framework, type FrameworkId, type PackageJson, type PackageManager, detectFramework, detectPackageManager, envVarsToWrite, installCommand, readPackageJson };
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FRAMEWORKS,
|
|
3
|
+
detectFramework,
|
|
4
|
+
detectPackageManager,
|
|
5
|
+
envVarsToWrite,
|
|
6
|
+
installCommand,
|
|
7
|
+
readPackageJson
|
|
8
|
+
} from "./chunk-YZOGONU4.js";
|
|
9
|
+
export {
|
|
10
|
+
FRAMEWORKS,
|
|
11
|
+
detectFramework,
|
|
12
|
+
detectPackageManager,
|
|
13
|
+
envVarsToWrite,
|
|
14
|
+
installCommand,
|
|
15
|
+
readPackageJson
|
|
16
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,393 @@ function saveConfig(config, path = CONFIG_PATH) {
|
|
|
20
20
|
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// src/init.ts
|
|
24
|
+
import * as p from "@clack/prompts";
|
|
25
|
+
import { spawn } from "child_process";
|
|
26
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
27
|
+
import { join as join3 } from "path";
|
|
28
|
+
|
|
29
|
+
// src/detect.ts
|
|
30
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
31
|
+
import { join as join2 } from "path";
|
|
32
|
+
var FRAMEWORKS = {
|
|
33
|
+
next: {
|
|
34
|
+
id: "next",
|
|
35
|
+
label: "Next.js",
|
|
36
|
+
packageName: "@mushi-mushi/react",
|
|
37
|
+
needsWebPackage: false,
|
|
38
|
+
snippet: (apiKey, projectId) => `// app/providers.tsx (or pages/_app.tsx for /pages router)
|
|
39
|
+
'use client'
|
|
40
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
41
|
+
|
|
42
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
43
|
+
return (
|
|
44
|
+
<MushiProvider config={{
|
|
45
|
+
projectId: '${projectId}',
|
|
46
|
+
apiKey: '${apiKey}',
|
|
47
|
+
}}>
|
|
48
|
+
{children}
|
|
49
|
+
</MushiProvider>
|
|
50
|
+
)
|
|
51
|
+
}`
|
|
52
|
+
},
|
|
53
|
+
react: {
|
|
54
|
+
id: "react",
|
|
55
|
+
label: "React",
|
|
56
|
+
packageName: "@mushi-mushi/react",
|
|
57
|
+
needsWebPackage: false,
|
|
58
|
+
snippet: (apiKey, projectId) => `// src/main.tsx
|
|
59
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
60
|
+
|
|
61
|
+
createRoot(document.getElementById('root')!).render(
|
|
62
|
+
<MushiProvider config={{
|
|
63
|
+
projectId: '${projectId}',
|
|
64
|
+
apiKey: '${apiKey}',
|
|
65
|
+
}}>
|
|
66
|
+
<App />
|
|
67
|
+
</MushiProvider>
|
|
68
|
+
)`
|
|
69
|
+
},
|
|
70
|
+
vue: {
|
|
71
|
+
id: "vue",
|
|
72
|
+
label: "Vue 3",
|
|
73
|
+
packageName: "@mushi-mushi/vue",
|
|
74
|
+
needsWebPackage: true,
|
|
75
|
+
snippet: (apiKey, projectId) => `// src/main.ts
|
|
76
|
+
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
77
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
78
|
+
|
|
79
|
+
app.use(MushiPlugin, { projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
80
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
81
|
+
},
|
|
82
|
+
nuxt: {
|
|
83
|
+
id: "nuxt",
|
|
84
|
+
label: "Nuxt",
|
|
85
|
+
packageName: "@mushi-mushi/vue",
|
|
86
|
+
needsWebPackage: true,
|
|
87
|
+
snippet: (apiKey, projectId) => `// plugins/mushi.client.ts
|
|
88
|
+
import { MushiPlugin } from '@mushi-mushi/vue'
|
|
89
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
90
|
+
|
|
91
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
92
|
+
nuxtApp.vueApp.use(MushiPlugin, {
|
|
93
|
+
projectId: '${projectId}',
|
|
94
|
+
apiKey: '${apiKey}',
|
|
95
|
+
})
|
|
96
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
97
|
+
})`
|
|
98
|
+
},
|
|
99
|
+
svelte: {
|
|
100
|
+
id: "svelte",
|
|
101
|
+
label: "Svelte",
|
|
102
|
+
packageName: "@mushi-mushi/svelte",
|
|
103
|
+
needsWebPackage: true,
|
|
104
|
+
snippet: (apiKey, projectId) => `// src/main.ts (or +layout.svelte for SvelteKit)
|
|
105
|
+
import { initMushi } from '@mushi-mushi/svelte'
|
|
106
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
107
|
+
|
|
108
|
+
initMushi({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
109
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
110
|
+
},
|
|
111
|
+
sveltekit: {
|
|
112
|
+
id: "sveltekit",
|
|
113
|
+
label: "SvelteKit",
|
|
114
|
+
packageName: "@mushi-mushi/svelte",
|
|
115
|
+
needsWebPackage: true,
|
|
116
|
+
snippet: (apiKey, projectId) => `// src/routes/+layout.svelte
|
|
117
|
+
<script>
|
|
118
|
+
import { onMount } from 'svelte'
|
|
119
|
+
import { initMushi } from '@mushi-mushi/svelte'
|
|
120
|
+
|
|
121
|
+
onMount(async () => {
|
|
122
|
+
const { Mushi } = await import('@mushi-mushi/web')
|
|
123
|
+
initMushi({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
124
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })
|
|
125
|
+
})
|
|
126
|
+
</script>`
|
|
127
|
+
},
|
|
128
|
+
angular: {
|
|
129
|
+
id: "angular",
|
|
130
|
+
label: "Angular",
|
|
131
|
+
packageName: "@mushi-mushi/angular",
|
|
132
|
+
needsWebPackage: true,
|
|
133
|
+
snippet: (apiKey, projectId) => `// src/main.ts
|
|
134
|
+
import { provideMushi } from '@mushi-mushi/angular'
|
|
135
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
136
|
+
|
|
137
|
+
bootstrapApplication(AppComponent, {
|
|
138
|
+
providers: [
|
|
139
|
+
provideMushi({ projectId: '${projectId}', apiKey: '${apiKey}' }),
|
|
140
|
+
],
|
|
141
|
+
})
|
|
142
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
143
|
+
},
|
|
144
|
+
expo: {
|
|
145
|
+
id: "expo",
|
|
146
|
+
label: "Expo",
|
|
147
|
+
packageName: "@mushi-mushi/react-native",
|
|
148
|
+
needsWebPackage: false,
|
|
149
|
+
snippet: (apiKey, projectId) => `// App.tsx
|
|
150
|
+
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
151
|
+
|
|
152
|
+
export default function App() {
|
|
153
|
+
return (
|
|
154
|
+
<MushiProvider projectId="${projectId}" apiKey="${apiKey}">
|
|
155
|
+
<YourApp />
|
|
156
|
+
</MushiProvider>
|
|
157
|
+
)
|
|
158
|
+
}`
|
|
159
|
+
},
|
|
160
|
+
"react-native": {
|
|
161
|
+
id: "react-native",
|
|
162
|
+
label: "React Native",
|
|
163
|
+
packageName: "@mushi-mushi/react-native",
|
|
164
|
+
needsWebPackage: false,
|
|
165
|
+
snippet: (apiKey, projectId) => `// App.tsx
|
|
166
|
+
import { MushiProvider } from '@mushi-mushi/react-native'
|
|
167
|
+
|
|
168
|
+
export default function App() {
|
|
169
|
+
return (
|
|
170
|
+
<MushiProvider projectId="${projectId}" apiKey="${apiKey}">
|
|
171
|
+
<YourApp />
|
|
172
|
+
</MushiProvider>
|
|
173
|
+
)
|
|
174
|
+
}`
|
|
175
|
+
},
|
|
176
|
+
capacitor: {
|
|
177
|
+
id: "capacitor",
|
|
178
|
+
label: "Capacitor (Ionic)",
|
|
179
|
+
packageName: "@mushi-mushi/capacitor",
|
|
180
|
+
needsWebPackage: false,
|
|
181
|
+
snippet: (apiKey, projectId) => `// src/main.ts
|
|
182
|
+
import { Mushi } from '@mushi-mushi/capacitor'
|
|
183
|
+
|
|
184
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
185
|
+
},
|
|
186
|
+
vanilla: {
|
|
187
|
+
id: "vanilla",
|
|
188
|
+
label: "Vanilla JS / unknown",
|
|
189
|
+
packageName: "@mushi-mushi/web",
|
|
190
|
+
needsWebPackage: false,
|
|
191
|
+
snippet: (apiKey, projectId) => `// Anywhere in your client bundle
|
|
192
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
193
|
+
|
|
194
|
+
Mushi.init({ projectId: '${projectId}', apiKey: '${apiKey}' })`
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
function readPackageJson(cwd) {
|
|
198
|
+
const path = join2(cwd, "package.json");
|
|
199
|
+
if (!existsSync2(path)) return null;
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function detectFramework(cwd, pkg) {
|
|
207
|
+
const deps = collectDeps(pkg);
|
|
208
|
+
if (deps.has("next")) return FRAMEWORKS.next;
|
|
209
|
+
if (deps.has("nuxt")) return FRAMEWORKS.nuxt;
|
|
210
|
+
if (deps.has("@sveltejs/kit")) return FRAMEWORKS.sveltekit;
|
|
211
|
+
if (deps.has("@angular/core")) return FRAMEWORKS.angular;
|
|
212
|
+
if (deps.has("expo")) return FRAMEWORKS.expo;
|
|
213
|
+
if (deps.has("react-native")) return FRAMEWORKS["react-native"];
|
|
214
|
+
if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
|
|
215
|
+
if (deps.has("svelte")) return FRAMEWORKS.svelte;
|
|
216
|
+
if (deps.has("vue")) return FRAMEWORKS.vue;
|
|
217
|
+
if (deps.has("react")) return FRAMEWORKS.react;
|
|
218
|
+
if (existsSync2(join2(cwd, "next.config.js")) || existsSync2(join2(cwd, "next.config.ts"))) {
|
|
219
|
+
return FRAMEWORKS.next;
|
|
220
|
+
}
|
|
221
|
+
if (existsSync2(join2(cwd, "nuxt.config.ts")) || existsSync2(join2(cwd, "nuxt.config.js"))) {
|
|
222
|
+
return FRAMEWORKS.nuxt;
|
|
223
|
+
}
|
|
224
|
+
if (existsSync2(join2(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
|
|
225
|
+
if (existsSync2(join2(cwd, "angular.json"))) return FRAMEWORKS.angular;
|
|
226
|
+
return FRAMEWORKS.vanilla;
|
|
227
|
+
}
|
|
228
|
+
function detectPackageManager(cwd) {
|
|
229
|
+
if (existsSync2(join2(cwd, "bun.lockb")) || existsSync2(join2(cwd, "bun.lock"))) return "bun";
|
|
230
|
+
if (existsSync2(join2(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
231
|
+
if (existsSync2(join2(cwd, "yarn.lock"))) return "yarn";
|
|
232
|
+
if (existsSync2(join2(cwd, "package-lock.json"))) return "npm";
|
|
233
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
234
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
235
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
236
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
237
|
+
return "npm";
|
|
238
|
+
}
|
|
239
|
+
function installCommand(pm, packages) {
|
|
240
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
241
|
+
return `${pm} ${verb} ${packages.join(" ")}`;
|
|
242
|
+
}
|
|
243
|
+
function envVarsToWrite(apiKey, projectId, framework) {
|
|
244
|
+
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
|
|
245
|
+
return [
|
|
246
|
+
`${prefix}MUSHI_PROJECT_ID=${projectId}`,
|
|
247
|
+
`${prefix}MUSHI_API_KEY=${apiKey}`
|
|
248
|
+
].join("\n");
|
|
249
|
+
}
|
|
250
|
+
function collectDeps(pkg) {
|
|
251
|
+
if (!pkg) return /* @__PURE__ */ new Set();
|
|
252
|
+
return /* @__PURE__ */ new Set([
|
|
253
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
254
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
255
|
+
...Object.keys(pkg.peerDependencies ?? {})
|
|
256
|
+
]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/init.ts
|
|
260
|
+
var ENV_FILES = [".env.local", ".env"];
|
|
261
|
+
async function runInit(options = {}) {
|
|
262
|
+
const cwd = options.cwd ?? process.cwd();
|
|
263
|
+
p.intro("\u{1F41B} Mushi Mushi setup wizard");
|
|
264
|
+
const pkg = readPackageJson(cwd);
|
|
265
|
+
if (!pkg) {
|
|
266
|
+
p.log.warn("No package.json found in this directory.");
|
|
267
|
+
const cont = await p.confirm({
|
|
268
|
+
message: "Continue anyway? (Mushi will install into the current folder)",
|
|
269
|
+
initialValue: false
|
|
270
|
+
});
|
|
271
|
+
if (p.isCancel(cont) || !cont) {
|
|
272
|
+
p.cancel("Aborted. Run from your project root and try again.");
|
|
273
|
+
process.exit(0);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const detected = detectFramework(cwd, pkg);
|
|
277
|
+
const framework = await chooseFramework(detected, options);
|
|
278
|
+
const credentials = await collectCredentials(options);
|
|
279
|
+
const pm = detectPackageManager(cwd);
|
|
280
|
+
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
281
|
+
if (!options.skipInstall) {
|
|
282
|
+
await installPackages(pm, packagesToInstall);
|
|
283
|
+
} else {
|
|
284
|
+
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
285
|
+
}
|
|
286
|
+
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
287
|
+
persistCliConfig(credentials.apiKey, credentials.projectId);
|
|
288
|
+
printNextSteps(framework, credentials.apiKey, credentials.projectId);
|
|
289
|
+
p.outro("Setup complete. Happy bug squashing \u{1F41B}");
|
|
290
|
+
}
|
|
291
|
+
async function chooseFramework(detected, options) {
|
|
292
|
+
if (options.framework) {
|
|
293
|
+
const explicit = FRAMEWORKS[options.framework];
|
|
294
|
+
if (!explicit) throw new Error(`Unknown framework: ${options.framework}`);
|
|
295
|
+
p.log.step(`Using framework: ${explicit.label} (from --framework)`);
|
|
296
|
+
return explicit;
|
|
297
|
+
}
|
|
298
|
+
if (options.yes) {
|
|
299
|
+
p.log.step(`Detected ${detected.label} \u2192 installing ${detected.packageName}`);
|
|
300
|
+
return detected;
|
|
301
|
+
}
|
|
302
|
+
const confirmed = await p.select({
|
|
303
|
+
message: `Detected ${detected.label}. Use this?`,
|
|
304
|
+
initialValue: detected.id,
|
|
305
|
+
options: Object.values(FRAMEWORKS).map((fw) => ({
|
|
306
|
+
value: fw.id,
|
|
307
|
+
label: `${fw.id === detected.id ? "\u2713 " : " "}${fw.label}`,
|
|
308
|
+
hint: fw.packageName
|
|
309
|
+
}))
|
|
310
|
+
});
|
|
311
|
+
if (p.isCancel(confirmed)) {
|
|
312
|
+
p.cancel("Aborted.");
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
return FRAMEWORKS[confirmed];
|
|
316
|
+
}
|
|
317
|
+
async function collectCredentials(options) {
|
|
318
|
+
const existing = loadConfig();
|
|
319
|
+
const projectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
320
|
+
message: "Project ID",
|
|
321
|
+
placeholder: "proj_xxxxxxxxxxxx",
|
|
322
|
+
hint: "Find this at https://kensaur.us/mushi-mushi/projects"
|
|
323
|
+
});
|
|
324
|
+
const apiKey = options.apiKey ?? existing.apiKey ?? await promptText({
|
|
325
|
+
message: "API key",
|
|
326
|
+
placeholder: "mushi_xxxxxxxxxxxx",
|
|
327
|
+
hint: "Treat this like a password \u2014 it goes in your env file, not in source."
|
|
328
|
+
});
|
|
329
|
+
return { projectId, apiKey };
|
|
330
|
+
}
|
|
331
|
+
async function promptText(opts) {
|
|
332
|
+
const value = await p.text({
|
|
333
|
+
message: opts.message,
|
|
334
|
+
placeholder: opts.placeholder,
|
|
335
|
+
validate: (v) => v.length === 0 ? "Required" : void 0
|
|
336
|
+
});
|
|
337
|
+
if (p.isCancel(value)) {
|
|
338
|
+
p.cancel("Aborted.");
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
if (opts.hint) p.log.info(opts.hint);
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
async function installPackages(pm, packages) {
|
|
345
|
+
const command = installCommand(pm, packages);
|
|
346
|
+
const spinner2 = p.spinner();
|
|
347
|
+
spinner2.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
|
|
348
|
+
try {
|
|
349
|
+
await runCommand(pm, packages);
|
|
350
|
+
spinner2.stop(`Installed ${packages.join(", ")}`);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
spinner2.stop(`Install failed \u2014 run \`${command}\` manually.`);
|
|
353
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function runCommand(pm, packages) {
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
359
|
+
const child = spawn(pm, [verb, ...packages], { stdio: "inherit", shell: true });
|
|
360
|
+
child.on("error", reject);
|
|
361
|
+
child.on("exit", (code) => {
|
|
362
|
+
if (code === 0) resolve();
|
|
363
|
+
else reject(new Error(`${pm} exited with code ${code}`));
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
368
|
+
const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
|
|
369
|
+
const targetPath = join3(cwd, target);
|
|
370
|
+
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
371
|
+
const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
|
|
372
|
+
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
373
|
+
p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
377
|
+
appendFileSync(targetPath, `${prefix}
|
|
378
|
+
# Mushi Mushi
|
|
379
|
+
${newVars}
|
|
380
|
+
`);
|
|
381
|
+
if (!existing) {
|
|
382
|
+
writeFileSync2(targetPath, readFileSync3(targetPath, "utf-8"));
|
|
383
|
+
}
|
|
384
|
+
p.log.success(`Wrote env vars to ${target}`);
|
|
385
|
+
warnIfMissingFromGitignore(cwd, target);
|
|
386
|
+
}
|
|
387
|
+
function warnIfMissingFromGitignore(cwd, envFile) {
|
|
388
|
+
const gitignorePath = join3(cwd, ".gitignore");
|
|
389
|
+
if (!existsSync3(gitignorePath)) {
|
|
390
|
+
p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const content = readFileSync3(gitignorePath, "utf-8");
|
|
394
|
+
if (!content.split("\n").some((line) => line.trim() === envFile || line.trim() === ".env*")) {
|
|
395
|
+
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function persistCliConfig(apiKey, projectId) {
|
|
399
|
+
const existing = loadConfig();
|
|
400
|
+
saveConfig({ ...existing, apiKey, projectId });
|
|
401
|
+
}
|
|
402
|
+
function printNextSteps(framework, apiKey, projectId) {
|
|
403
|
+
p.note(framework.snippet(apiKey, projectId), "Add this to your app:");
|
|
404
|
+
p.log.message("Verify the install:");
|
|
405
|
+
p.log.message(" \u2022 Start your dev server");
|
|
406
|
+
p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
|
|
407
|
+
p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
|
|
408
|
+
}
|
|
409
|
+
|
|
23
410
|
// src/index.ts
|
|
24
411
|
async function apiCall(path, config, options = {}) {
|
|
25
412
|
const endpoint = config.endpoint ?? "https://api.mushimushi.dev";
|
|
@@ -35,7 +422,16 @@ async function apiCall(path, config, options = {}) {
|
|
|
35
422
|
});
|
|
36
423
|
return res.json();
|
|
37
424
|
}
|
|
38
|
-
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 manage bug reports
|
|
425
|
+
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version("0.3.0");
|
|
426
|
+
program.command("init").description("Set up the Mushi Mushi SDK in this project (auto-detects framework)").option("--project-id <id>", "Skip the prompt by passing the project ID").option("--api-key <key>", "Skip the prompt by passing the API key").option("--framework <id>", "Force a framework (next, react, vue, nuxt, svelte, sveltekit, angular, expo, react-native, capacitor, vanilla)").option("--skip-install", "Don't auto-install the SDK package \u2014 print the command instead").option("-y, --yes", "Accept detected framework without prompting").action(async (opts) => {
|
|
427
|
+
await runInit({
|
|
428
|
+
projectId: opts.projectId,
|
|
429
|
+
apiKey: opts.apiKey,
|
|
430
|
+
framework: opts.framework,
|
|
431
|
+
skipInstall: opts.skipInstall,
|
|
432
|
+
yes: opts.yes
|
|
433
|
+
});
|
|
434
|
+
});
|
|
39
435
|
program.command("login").description("Store API key for authentication").requiredOption("--api-key <key>", "API key").option("--endpoint <url>", "API endpoint URL").option("--project-id <id>", "Default project ID").action((opts) => {
|
|
40
436
|
const config = loadConfig();
|
|
41
437
|
config.apiKey = opts.apiKey;
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FrameworkId } from './detect.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FILE: packages/cli/src/init.ts
|
|
5
|
+
* PURPOSE: `mushi init` wizard — detects framework, asks for credentials,
|
|
6
|
+
* installs the right SDK, writes env vars, prints next-step snippet.
|
|
7
|
+
*
|
|
8
|
+
* Modeled on the Sentry / PostHog wizard pattern: one shell command, minimal
|
|
9
|
+
* prompts, transparent about every file it touches.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface InitOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
projectId?: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
framework?: FrameworkId;
|
|
17
|
+
skipInstall?: boolean;
|
|
18
|
+
yes?: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare function runInit(options?: InitOptions): Promise<void>;
|
|
21
|
+
|
|
22
|
+
export { type InitOptions, runInit };
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FRAMEWORKS,
|
|
3
|
+
detectFramework,
|
|
4
|
+
detectPackageManager,
|
|
5
|
+
envVarsToWrite,
|
|
6
|
+
installCommand,
|
|
7
|
+
readPackageJson
|
|
8
|
+
} from "./chunk-YZOGONU4.js";
|
|
9
|
+
|
|
10
|
+
// src/init.ts
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
14
|
+
import { join as join2 } from "path";
|
|
15
|
+
|
|
16
|
+
// src/config.ts
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
var CONFIG_PATH = join(homedir(), ".mushirc");
|
|
21
|
+
function loadConfig(path = CONFIG_PATH) {
|
|
22
|
+
if (!existsSync(path)) return {};
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function saveConfig(config, path = CONFIG_PATH) {
|
|
30
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/init.ts
|
|
34
|
+
var ENV_FILES = [".env.local", ".env"];
|
|
35
|
+
async function runInit(options = {}) {
|
|
36
|
+
const cwd = options.cwd ?? process.cwd();
|
|
37
|
+
p.intro("\u{1F41B} Mushi Mushi setup wizard");
|
|
38
|
+
const pkg = readPackageJson(cwd);
|
|
39
|
+
if (!pkg) {
|
|
40
|
+
p.log.warn("No package.json found in this directory.");
|
|
41
|
+
const cont = await p.confirm({
|
|
42
|
+
message: "Continue anyway? (Mushi will install into the current folder)",
|
|
43
|
+
initialValue: false
|
|
44
|
+
});
|
|
45
|
+
if (p.isCancel(cont) || !cont) {
|
|
46
|
+
p.cancel("Aborted. Run from your project root and try again.");
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const detected = detectFramework(cwd, pkg);
|
|
51
|
+
const framework = await chooseFramework(detected, options);
|
|
52
|
+
const credentials = await collectCredentials(options);
|
|
53
|
+
const pm = detectPackageManager(cwd);
|
|
54
|
+
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
55
|
+
if (!options.skipInstall) {
|
|
56
|
+
await installPackages(pm, packagesToInstall);
|
|
57
|
+
} else {
|
|
58
|
+
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
59
|
+
}
|
|
60
|
+
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
61
|
+
persistCliConfig(credentials.apiKey, credentials.projectId);
|
|
62
|
+
printNextSteps(framework, credentials.apiKey, credentials.projectId);
|
|
63
|
+
p.outro("Setup complete. Happy bug squashing \u{1F41B}");
|
|
64
|
+
}
|
|
65
|
+
async function chooseFramework(detected, options) {
|
|
66
|
+
if (options.framework) {
|
|
67
|
+
const explicit = FRAMEWORKS[options.framework];
|
|
68
|
+
if (!explicit) throw new Error(`Unknown framework: ${options.framework}`);
|
|
69
|
+
p.log.step(`Using framework: ${explicit.label} (from --framework)`);
|
|
70
|
+
return explicit;
|
|
71
|
+
}
|
|
72
|
+
if (options.yes) {
|
|
73
|
+
p.log.step(`Detected ${detected.label} \u2192 installing ${detected.packageName}`);
|
|
74
|
+
return detected;
|
|
75
|
+
}
|
|
76
|
+
const confirmed = await p.select({
|
|
77
|
+
message: `Detected ${detected.label}. Use this?`,
|
|
78
|
+
initialValue: detected.id,
|
|
79
|
+
options: Object.values(FRAMEWORKS).map((fw) => ({
|
|
80
|
+
value: fw.id,
|
|
81
|
+
label: `${fw.id === detected.id ? "\u2713 " : " "}${fw.label}`,
|
|
82
|
+
hint: fw.packageName
|
|
83
|
+
}))
|
|
84
|
+
});
|
|
85
|
+
if (p.isCancel(confirmed)) {
|
|
86
|
+
p.cancel("Aborted.");
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
return FRAMEWORKS[confirmed];
|
|
90
|
+
}
|
|
91
|
+
async function collectCredentials(options) {
|
|
92
|
+
const existing = loadConfig();
|
|
93
|
+
const projectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
94
|
+
message: "Project ID",
|
|
95
|
+
placeholder: "proj_xxxxxxxxxxxx",
|
|
96
|
+
hint: "Find this at https://kensaur.us/mushi-mushi/projects"
|
|
97
|
+
});
|
|
98
|
+
const apiKey = options.apiKey ?? existing.apiKey ?? await promptText({
|
|
99
|
+
message: "API key",
|
|
100
|
+
placeholder: "mushi_xxxxxxxxxxxx",
|
|
101
|
+
hint: "Treat this like a password \u2014 it goes in your env file, not in source."
|
|
102
|
+
});
|
|
103
|
+
return { projectId, apiKey };
|
|
104
|
+
}
|
|
105
|
+
async function promptText(opts) {
|
|
106
|
+
const value = await p.text({
|
|
107
|
+
message: opts.message,
|
|
108
|
+
placeholder: opts.placeholder,
|
|
109
|
+
validate: (v) => v.length === 0 ? "Required" : void 0
|
|
110
|
+
});
|
|
111
|
+
if (p.isCancel(value)) {
|
|
112
|
+
p.cancel("Aborted.");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
if (opts.hint) p.log.info(opts.hint);
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
async function installPackages(pm, packages) {
|
|
119
|
+
const command = installCommand(pm, packages);
|
|
120
|
+
const spinner2 = p.spinner();
|
|
121
|
+
spinner2.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
|
|
122
|
+
try {
|
|
123
|
+
await runCommand(pm, packages);
|
|
124
|
+
spinner2.stop(`Installed ${packages.join(", ")}`);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
spinner2.stop(`Install failed \u2014 run \`${command}\` manually.`);
|
|
127
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function runCommand(pm, packages) {
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
133
|
+
const child = spawn(pm, [verb, ...packages], { stdio: "inherit", shell: true });
|
|
134
|
+
child.on("error", reject);
|
|
135
|
+
child.on("exit", (code) => {
|
|
136
|
+
if (code === 0) resolve();
|
|
137
|
+
else reject(new Error(`${pm} exited with code ${code}`));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
142
|
+
const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
|
|
143
|
+
const targetPath = join2(cwd, target);
|
|
144
|
+
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
145
|
+
const existing = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
|
|
146
|
+
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
147
|
+
p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
151
|
+
appendFileSync(targetPath, `${prefix}
|
|
152
|
+
# Mushi Mushi
|
|
153
|
+
${newVars}
|
|
154
|
+
`);
|
|
155
|
+
if (!existing) {
|
|
156
|
+
writeFileSync2(targetPath, readFileSync2(targetPath, "utf-8"));
|
|
157
|
+
}
|
|
158
|
+
p.log.success(`Wrote env vars to ${target}`);
|
|
159
|
+
warnIfMissingFromGitignore(cwd, target);
|
|
160
|
+
}
|
|
161
|
+
function warnIfMissingFromGitignore(cwd, envFile) {
|
|
162
|
+
const gitignorePath = join2(cwd, ".gitignore");
|
|
163
|
+
if (!existsSync2(gitignorePath)) {
|
|
164
|
+
p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
168
|
+
if (!content.split("\n").some((line) => line.trim() === envFile || line.trim() === ".env*")) {
|
|
169
|
+
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function persistCliConfig(apiKey, projectId) {
|
|
173
|
+
const existing = loadConfig();
|
|
174
|
+
saveConfig({ ...existing, apiKey, projectId });
|
|
175
|
+
}
|
|
176
|
+
function printNextSteps(framework, apiKey, projectId) {
|
|
177
|
+
p.note(framework.snippet(apiKey, projectId), "Add this to your app:");
|
|
178
|
+
p.log.message("Verify the install:");
|
|
179
|
+
p.log.message(" \u2022 Start your dev server");
|
|
180
|
+
p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
|
|
181
|
+
p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
|
|
182
|
+
}
|
|
183
|
+
export {
|
|
184
|
+
runInit
|
|
185
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "CLI
|
|
5
|
+
"description": "CLI for Mushi Mushi — `mushi init` wizard installs the right SDK for your framework, plus report triage and pipeline health commands",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mushi": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
+
"@clack/prompts": "^0.11.0",
|
|
10
11
|
"commander": "^14.0.3"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
@@ -39,17 +40,43 @@
|
|
|
39
40
|
"keywords": [
|
|
40
41
|
"mushi-mushi",
|
|
41
42
|
"bug-reporting",
|
|
42
|
-
"
|
|
43
|
+
"user-feedback",
|
|
44
|
+
"session-replay",
|
|
45
|
+
"screenshot",
|
|
46
|
+
"shake-to-report",
|
|
47
|
+
"sdk",
|
|
48
|
+
"wizard",
|
|
49
|
+
"setup",
|
|
50
|
+
"cli",
|
|
51
|
+
"init",
|
|
52
|
+
"next.js",
|
|
53
|
+
"nextjs",
|
|
54
|
+
"react",
|
|
55
|
+
"vue",
|
|
56
|
+
"nuxt",
|
|
57
|
+
"svelte",
|
|
58
|
+
"sveltekit",
|
|
59
|
+
"angular",
|
|
60
|
+
"react-native",
|
|
61
|
+
"expo",
|
|
62
|
+
"capacitor",
|
|
63
|
+
"sentry-companion",
|
|
64
|
+
"error-tracking"
|
|
43
65
|
],
|
|
44
66
|
"exports": {
|
|
45
67
|
".": {
|
|
68
|
+
"import": "./dist/index.js"
|
|
69
|
+
},
|
|
70
|
+
"./init": {
|
|
46
71
|
"import": {
|
|
47
|
-
"types": "./dist/
|
|
48
|
-
"default": "./dist/
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
"types": "./dist/init.d.ts",
|
|
73
|
+
"default": "./dist/init.js"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"./detect": {
|
|
77
|
+
"import": {
|
|
78
|
+
"types": "./dist/detect.d.ts",
|
|
79
|
+
"default": "./dist/detect.js"
|
|
53
80
|
}
|
|
54
81
|
}
|
|
55
82
|
},
|