@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 CHANGED
@@ -1,23 +1,53 @@
1
1
  # @mushi-mushi/cli
2
2
 
3
- CLI tool for Mushi Mushi project management and report triage.
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
- ## Install
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
- ## Usage
40
+ ## Other commands
12
41
 
13
42
  ```bash
14
- mushi login # Authenticate with your API key
15
- mushi status # Project overview
16
- mushi reports list # List recent reports
17
- mushi reports show <id> # View report details
18
- mushi reports triage <id> --priority high --assign @dev
19
- mushi deploy check # Check edge function health
20
- mushi test # Submit a test report to verify pipeline
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
+ };
@@ -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 and pipeline").version("0.0.1");
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.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
- "description": "CLI tool for Mushi Mushi — project management, report triage, pipeline health",
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
- "sdk"
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/index.d.ts",
48
- "default": "./dist/index.js"
49
- },
50
- "require": {
51
- "types": "./dist/index.d.cts",
52
- "default": "./dist/index.cjs"
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
  },