@mushi-mushi/cli 0.4.0 → 0.5.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 +29 -10
- package/dist/chunk-I3FFGUE5.js +6 -0
- package/dist/{chunk-YZOGONU4.js → chunk-ZZNVMBMG.js} +18 -0
- package/dist/detect.d.ts +3 -1
- package/dist/detect.js +5 -1
- package/dist/index.js +397 -38
- package/dist/init.d.ts +18 -1
- package/dist/init.js +375 -31
- package/dist/version.d.ts +11 -0
- package/dist/version.js +6 -0
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -6,41 +6,52 @@ CLI for Mushi Mushi — set up the SDK in one command, then triage reports and m
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx @mushi-mushi/cli init
|
|
9
|
-
#
|
|
10
|
-
npx mushi-mushi
|
|
9
|
+
# equivalently:
|
|
10
|
+
npx mushi-mushi
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
The wizard:
|
|
14
14
|
|
|
15
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
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.
|
|
17
|
+
3. Detects your package manager (npm / pnpm / yarn / bun) from your lockfile and installs with that — `shell: false`, with Windows `.cmd` shim resolution.
|
|
18
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`.
|
|
19
|
+
5. Warns you if `.env.local` isn't in `.gitignore` (covers `.env*.local`, `*.local`, etc.).
|
|
20
20
|
6. Prints the framework-specific provider snippet to copy-paste.
|
|
21
|
+
7. Offers to **send a real test report** so you see your first classified bug in the console immediately. Opt out via `--skip-test-report`.
|
|
21
22
|
|
|
22
|
-
It never silently overwrites existing env vars or modifies application code.
|
|
23
|
+
It never silently overwrites existing env vars or modifies application code. Pasted credentials are sanitized (stripped of quotes / CR / LF / NUL) and validated against `^proj_[A-Za-z0-9_-]{10,}$` / `^mushi_[A-Za-z0-9_-]{10,}$` before anything is written to disk.
|
|
23
24
|
|
|
24
25
|
### Flags
|
|
25
26
|
|
|
26
27
|
```bash
|
|
27
|
-
mushi init --framework next
|
|
28
|
-
mushi init --project-id proj_xxx --api-key mushi_xxx
|
|
29
|
-
mushi init --skip-install
|
|
30
|
-
mushi init -
|
|
28
|
+
mushi init --framework next # skip framework detection
|
|
29
|
+
mushi init --project-id proj_xxx --api-key mushi_xxx # skip credential prompts
|
|
30
|
+
mushi init --skip-install # print the install command instead
|
|
31
|
+
mushi init --skip-test-report # don't offer to send a test report
|
|
32
|
+
mushi init --cwd apps/web # run in a sub-package of a monorepo
|
|
33
|
+
mushi init --endpoint https://mushi.your-company.com # self-hosted Mushi API
|
|
34
|
+
mushi init -y # accept the detected framework
|
|
31
35
|
```
|
|
32
36
|
|
|
37
|
+
Non-interactive use (CI): pass `--yes --project-id proj_xxx --api-key mushi_xxx` or the wizard exits with a clear error instead of hanging on a prompt.
|
|
38
|
+
|
|
39
|
+
Stale-version hint: the wizard checks the npm registry (2s timeout) and prints a one-line upgrade nudge if a newer stable is published. Opt out with `MUSHI_NO_UPDATE_CHECK=1`.
|
|
40
|
+
|
|
41
|
+
Monorepo awareness: if you run the wizard at a workspace root with no framework dep, it scans `apps/*`, `packages/*`, `examples/*` and tells you which sub-package you probably meant (`mushi init --cwd apps/web`).
|
|
42
|
+
|
|
33
43
|
## Install globally
|
|
34
44
|
|
|
35
45
|
```bash
|
|
36
46
|
npm install -g @mushi-mushi/cli
|
|
37
47
|
mushi --help
|
|
48
|
+
mushi --version
|
|
38
49
|
```
|
|
39
50
|
|
|
40
51
|
## Other commands
|
|
41
52
|
|
|
42
53
|
```bash
|
|
43
|
-
mushi login --api-key mushi_xxx # store credentials in ~/.mushirc
|
|
54
|
+
mushi login --api-key mushi_xxx # store credentials in ~/.mushirc (mode 0o600)
|
|
44
55
|
mushi status # project overview
|
|
45
56
|
mushi reports list # recent reports
|
|
46
57
|
mushi reports show <id> # one report
|
|
@@ -48,8 +59,16 @@ mushi reports triage <id> --status acknowledged --severity high
|
|
|
48
59
|
mushi deploy check # edge-function health probe
|
|
49
60
|
mushi index <path> # walk a local repo and feed RAG
|
|
50
61
|
mushi test # submit a test report end-to-end
|
|
62
|
+
mushi config endpoint https://... # set API endpoint (https:// required outside localhost)
|
|
51
63
|
```
|
|
52
64
|
|
|
65
|
+
## Security notes
|
|
66
|
+
|
|
67
|
+
- `~/.mushirc` is written with mode `0o600` on Unix. Legacy configs with looser permissions are tightened on load.
|
|
68
|
+
- `--endpoint` values are parsed through `new URL()` and required to use `https://` except for `localhost` / `127.0.0.1` / `*.local`.
|
|
69
|
+
- The `--api-key` flag leaks into `ps -ef` — prefer the interactive prompt on shared machines.
|
|
70
|
+
- Full stack traces on error: `DEBUG=mushi mushi init`.
|
|
71
|
+
|
|
53
72
|
## License
|
|
54
73
|
|
|
55
74
|
MIT
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
// src/detect.ts
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
var FRAMEWORK_IDS = [
|
|
5
|
+
"next",
|
|
6
|
+
"react",
|
|
7
|
+
"vue",
|
|
8
|
+
"nuxt",
|
|
9
|
+
"svelte",
|
|
10
|
+
"sveltekit",
|
|
11
|
+
"angular",
|
|
12
|
+
"expo",
|
|
13
|
+
"react-native",
|
|
14
|
+
"capacitor",
|
|
15
|
+
"vanilla"
|
|
16
|
+
];
|
|
17
|
+
function isFrameworkId(value) {
|
|
18
|
+
return typeof value === "string" && FRAMEWORK_IDS.includes(value);
|
|
19
|
+
}
|
|
4
20
|
var FRAMEWORKS = {
|
|
5
21
|
next: {
|
|
6
22
|
id: "next",
|
|
@@ -229,6 +245,8 @@ function collectDeps(pkg) {
|
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
export {
|
|
248
|
+
FRAMEWORK_IDS,
|
|
249
|
+
isFrameworkId,
|
|
232
250
|
FRAMEWORKS,
|
|
233
251
|
readPackageJson,
|
|
234
252
|
detectFramework,
|
package/dist/detect.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ interface PackageJson {
|
|
|
18
18
|
peerDependencies?: Record<string, string>;
|
|
19
19
|
}
|
|
20
20
|
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
21
|
+
declare const FRAMEWORK_IDS: ReadonlyArray<FrameworkId>;
|
|
22
|
+
declare function isFrameworkId(value: unknown): value is FrameworkId;
|
|
21
23
|
declare const FRAMEWORKS: Record<FrameworkId, Framework>;
|
|
22
24
|
declare function readPackageJson(cwd: string): PackageJson | null;
|
|
23
25
|
declare function detectFramework(cwd: string, pkg: PackageJson | null): Framework;
|
|
@@ -25,4 +27,4 @@ declare function detectPackageManager(cwd: string): PackageManager;
|
|
|
25
27
|
declare function installCommand(pm: PackageManager, packages: string[]): string;
|
|
26
28
|
declare function envVarsToWrite(apiKey: string, projectId: string, framework: Framework): string;
|
|
27
29
|
|
|
28
|
-
export { FRAMEWORKS, type Framework, type FrameworkId, type PackageJson, type PackageManager, detectFramework, detectPackageManager, envVarsToWrite, installCommand, readPackageJson };
|
|
30
|
+
export { FRAMEWORKS, FRAMEWORK_IDS, type Framework, type FrameworkId, type PackageJson, type PackageManager, detectFramework, detectPackageManager, envVarsToWrite, installCommand, isFrameworkId, readPackageJson };
|
package/dist/detect.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
FRAMEWORKS,
|
|
3
|
+
FRAMEWORK_IDS,
|
|
3
4
|
detectFramework,
|
|
4
5
|
detectPackageManager,
|
|
5
6
|
envVarsToWrite,
|
|
6
7
|
installCommand,
|
|
8
|
+
isFrameworkId,
|
|
7
9
|
readPackageJson
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-ZZNVMBMG.js";
|
|
9
11
|
export {
|
|
10
12
|
FRAMEWORKS,
|
|
13
|
+
FRAMEWORK_IDS,
|
|
11
14
|
detectFramework,
|
|
12
15
|
detectPackageManager,
|
|
13
16
|
envVarsToWrite,
|
|
14
17
|
installCommand,
|
|
18
|
+
isFrameworkId,
|
|
15
19
|
readPackageJson
|
|
16
20
|
};
|
package/dist/index.js
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
7
|
+
import { chmodSync, readFileSync, statSync, writeFileSync, existsSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
var CONFIG_PATH = join(homedir(), ".mushirc");
|
|
11
|
+
var SECURE_FILE_MODE = 384;
|
|
11
12
|
function loadConfig(path = CONFIG_PATH) {
|
|
12
13
|
if (!existsSync(path)) return {};
|
|
14
|
+
tightenPermissions(path);
|
|
13
15
|
try {
|
|
14
16
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
15
17
|
} catch {
|
|
@@ -17,14 +19,24 @@ function loadConfig(path = CONFIG_PATH) {
|
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
function saveConfig(config, path = CONFIG_PATH) {
|
|
20
|
-
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
22
|
+
writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
|
|
23
|
+
tightenPermissions(path);
|
|
24
|
+
}
|
|
25
|
+
function tightenPermissions(path) {
|
|
26
|
+
if (process.platform === "win32") return;
|
|
27
|
+
try {
|
|
28
|
+
const current = statSync(path).mode & 511;
|
|
29
|
+
if (current !== SECURE_FILE_MODE) chmodSync(path, SECURE_FILE_MODE);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
// src/init.ts
|
|
24
35
|
import * as p from "@clack/prompts";
|
|
25
36
|
import { spawn } from "child_process";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
37
|
+
import { randomUUID } from "crypto";
|
|
38
|
+
import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
39
|
+
import { join as join4 } from "path";
|
|
28
40
|
|
|
29
41
|
// src/detect.ts
|
|
30
42
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
@@ -256,11 +268,198 @@ function collectDeps(pkg) {
|
|
|
256
268
|
]);
|
|
257
269
|
}
|
|
258
270
|
|
|
271
|
+
// src/endpoint.ts
|
|
272
|
+
var DEFAULT_ENDPOINT = "https://api.mushimushi.dev";
|
|
273
|
+
var TEST_REPORT_TIMEOUT_MS = 1e4;
|
|
274
|
+
var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
|
|
275
|
+
function assertEndpoint(url) {
|
|
276
|
+
let parsed;
|
|
277
|
+
try {
|
|
278
|
+
parsed = new URL(url);
|
|
279
|
+
} catch {
|
|
280
|
+
throw new Error(`Invalid endpoint URL: ${url}`);
|
|
281
|
+
}
|
|
282
|
+
const host = parsed.hostname;
|
|
283
|
+
const isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".local");
|
|
284
|
+
if (parsed.protocol !== "https:" && !isLocal) {
|
|
285
|
+
throw new Error(`Endpoint must use https:// (got ${parsed.protocol}//${host}).`);
|
|
286
|
+
}
|
|
287
|
+
return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname);
|
|
288
|
+
}
|
|
289
|
+
function normalizeEndpoint(url) {
|
|
290
|
+
const input = url ?? DEFAULT_ENDPOINT;
|
|
291
|
+
let end = input.length;
|
|
292
|
+
while (end > 0 && input.charCodeAt(end - 1) === 47) end--;
|
|
293
|
+
return input.slice(0, end);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/freshness.ts
|
|
297
|
+
var REGISTRY = "https://registry.npmjs.org";
|
|
298
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
299
|
+
async function checkFreshness(packageName, currentVersion, opts = {}) {
|
|
300
|
+
if (process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
|
|
301
|
+
const registry = opts.registry ?? REGISTRY;
|
|
302
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
303
|
+
const controller = new AbortController();
|
|
304
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
305
|
+
try {
|
|
306
|
+
const res = await fetch(
|
|
307
|
+
`${registry}/${encodeURIComponent(packageName)}/latest`,
|
|
308
|
+
{
|
|
309
|
+
signal: controller.signal,
|
|
310
|
+
headers: { Accept: "application/json" }
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
if (!res.ok) return null;
|
|
314
|
+
const body = await res.json();
|
|
315
|
+
const latest = typeof body.version === "string" ? body.version : null;
|
|
316
|
+
if (!latest) return null;
|
|
317
|
+
return {
|
|
318
|
+
current: currentVersion,
|
|
319
|
+
latest,
|
|
320
|
+
isOutdated: isNewerStableVersion(latest, currentVersion)
|
|
321
|
+
};
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
} finally {
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function isNewerStableVersion(latest, current) {
|
|
329
|
+
const latestCore = stripPreRelease(latest);
|
|
330
|
+
if (hasPreReleaseTag(latest)) return false;
|
|
331
|
+
const [la, lb, lc] = parse(latestCore);
|
|
332
|
+
const [ca, cb, cc] = parse(stripPreRelease(current));
|
|
333
|
+
if (la !== ca) return la > ca;
|
|
334
|
+
if (lb !== cb) return lb > cb;
|
|
335
|
+
return lc > cc;
|
|
336
|
+
}
|
|
337
|
+
function stripPreRelease(version) {
|
|
338
|
+
const idx = version.indexOf("-");
|
|
339
|
+
return idx === -1 ? version : version.slice(0, idx);
|
|
340
|
+
}
|
|
341
|
+
function hasPreReleaseTag(version) {
|
|
342
|
+
return version.includes("-");
|
|
343
|
+
}
|
|
344
|
+
function parse(version) {
|
|
345
|
+
const parts = version.split(".").map((part) => Number(part));
|
|
346
|
+
return [
|
|
347
|
+
Number.isFinite(parts[0]) ? parts[0] : 0,
|
|
348
|
+
Number.isFinite(parts[1]) ? parts[1] : 0,
|
|
349
|
+
Number.isFinite(parts[2]) ? parts[2] : 0
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/monorepo.ts
|
|
354
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "fs";
|
|
355
|
+
import { dirname, join as join3, resolve } from "path";
|
|
356
|
+
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
357
|
+
var FRAMEWORK_DEPS = {
|
|
358
|
+
next: "Next.js",
|
|
359
|
+
nuxt: "Nuxt",
|
|
360
|
+
"@sveltejs/kit": "SvelteKit",
|
|
361
|
+
"@angular/core": "Angular",
|
|
362
|
+
expo: "Expo",
|
|
363
|
+
"react-native": "React Native",
|
|
364
|
+
"@capacitor/core": "Capacitor",
|
|
365
|
+
svelte: "Svelte",
|
|
366
|
+
vue: "Vue",
|
|
367
|
+
react: "React"
|
|
368
|
+
};
|
|
369
|
+
function detectWorkspaceHint(cwd) {
|
|
370
|
+
const root = findWorkspaceRoot(cwd);
|
|
371
|
+
if (!root) return null;
|
|
372
|
+
const rootPkg = readPackageJsonSafely(join3(root, "package.json"));
|
|
373
|
+
if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
|
|
374
|
+
const source = existsSync3(join3(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
|
|
375
|
+
const apps = collectAppsFromGlobs(root);
|
|
376
|
+
if (apps.length === 0) return null;
|
|
377
|
+
return { root, apps, source };
|
|
378
|
+
}
|
|
379
|
+
function findWorkspaceRoot(start) {
|
|
380
|
+
let dir = resolve(start);
|
|
381
|
+
for (let i = 0; i < 8; i++) {
|
|
382
|
+
if (isWorkspaceRoot(dir)) return dir;
|
|
383
|
+
const parent = dirname(dir);
|
|
384
|
+
if (parent === dir) break;
|
|
385
|
+
dir = parent;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
function isWorkspaceRoot(dir) {
|
|
390
|
+
if (existsSync3(join3(dir, "pnpm-workspace.yaml"))) return true;
|
|
391
|
+
const pkg = readPackageJsonSafely(join3(dir, "package.json"));
|
|
392
|
+
if (!pkg) return false;
|
|
393
|
+
return Boolean(pkg.workspaces);
|
|
394
|
+
}
|
|
395
|
+
function collectAppsFromGlobs(root) {
|
|
396
|
+
const results = [];
|
|
397
|
+
for (const glob of WORKSPACE_GLOB_CANDIDATES) {
|
|
398
|
+
const prefix = glob.replace("/*", "");
|
|
399
|
+
const parentDir = join3(root, prefix);
|
|
400
|
+
if (!existsSync3(parentDir)) continue;
|
|
401
|
+
let entries;
|
|
402
|
+
try {
|
|
403
|
+
entries = readdirSync(parentDir);
|
|
404
|
+
} catch {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
const pkgPath = join3(parentDir, entry, "package.json");
|
|
409
|
+
if (!isFileSafe(pkgPath)) continue;
|
|
410
|
+
const pkg = readPackageJsonSafely(pkgPath);
|
|
411
|
+
if (!pkg) continue;
|
|
412
|
+
const framework = getFrameworkFromPkg(pkg);
|
|
413
|
+
if (!framework) continue;
|
|
414
|
+
results.push({
|
|
415
|
+
name: pkg.name ?? `${prefix}/${entry}`,
|
|
416
|
+
relativePath: `${prefix}/${entry}`,
|
|
417
|
+
framework
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return results;
|
|
422
|
+
}
|
|
423
|
+
function readPackageJsonSafely(path) {
|
|
424
|
+
if (!isFileSafe(path)) return null;
|
|
425
|
+
try {
|
|
426
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
427
|
+
} catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function isFileSafe(path) {
|
|
432
|
+
try {
|
|
433
|
+
return existsSync3(path) && statSync2(path).isFile();
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function getFrameworkFromPkg(pkg) {
|
|
439
|
+
const deps = {
|
|
440
|
+
...pkg.dependencies ?? {},
|
|
441
|
+
...pkg.devDependencies ?? {},
|
|
442
|
+
...pkg.peerDependencies ?? {}
|
|
443
|
+
};
|
|
444
|
+
for (const dep of Object.keys(FRAMEWORK_DEPS)) {
|
|
445
|
+
if (dep in deps) return FRAMEWORK_DEPS[dep];
|
|
446
|
+
}
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/version.ts
|
|
451
|
+
var MUSHI_CLI_VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
452
|
+
|
|
259
453
|
// src/init.ts
|
|
260
454
|
var ENV_FILES = [".env.local", ".env"];
|
|
455
|
+
var PROJECT_ID_PATTERN = /^proj_[A-Za-z0-9_-]{10,}$/;
|
|
456
|
+
var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
|
|
261
457
|
async function runInit(options = {}) {
|
|
262
458
|
const cwd = options.cwd ?? process.cwd();
|
|
459
|
+
ensureInteractiveOrBailOut(options);
|
|
263
460
|
p.intro("\u{1F41B} Mushi Mushi setup wizard");
|
|
461
|
+
await printFreshnessHint();
|
|
462
|
+
warnIfWorkspaceRoot(cwd);
|
|
264
463
|
const pkg = readPackageJson(cwd);
|
|
265
464
|
if (!pkg) {
|
|
266
465
|
p.log.warn("No package.json found in this directory.");
|
|
@@ -279,15 +478,28 @@ async function runInit(options = {}) {
|
|
|
279
478
|
const pm = detectPackageManager(cwd);
|
|
280
479
|
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
281
480
|
if (!options.skipInstall) {
|
|
282
|
-
await installPackages(pm, packagesToInstall);
|
|
481
|
+
await installPackages(pm, packagesToInstall, cwd);
|
|
283
482
|
} else {
|
|
284
483
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
285
484
|
}
|
|
286
485
|
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
287
486
|
persistCliConfig(credentials.apiKey, credentials.projectId);
|
|
288
487
|
printNextSteps(framework, credentials.apiKey, credentials.projectId);
|
|
488
|
+
await maybeSendTestReport(credentials, options);
|
|
289
489
|
p.outro("Setup complete. Happy bug squashing \u{1F41B}");
|
|
290
490
|
}
|
|
491
|
+
function ensureInteractiveOrBailOut(options) {
|
|
492
|
+
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
493
|
+
if (isTTY) return;
|
|
494
|
+
const hasAllFlags = Boolean(
|
|
495
|
+
(options.framework || options.yes) && options.projectId && options.apiKey
|
|
496
|
+
);
|
|
497
|
+
if (hasAllFlags) return;
|
|
498
|
+
process.stderr.write(
|
|
499
|
+
"mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id proj_xxx --api-key mushi_xxx\n"
|
|
500
|
+
);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
291
503
|
async function chooseFramework(detected, options) {
|
|
292
504
|
if (options.framework) {
|
|
293
505
|
const explicit = FRAMEWORKS[options.framework];
|
|
@@ -316,23 +528,48 @@ async function chooseFramework(detected, options) {
|
|
|
316
528
|
}
|
|
317
529
|
async function collectCredentials(options) {
|
|
318
530
|
const existing = loadConfig();
|
|
319
|
-
const
|
|
531
|
+
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
320
532
|
message: "Project ID",
|
|
321
533
|
placeholder: "proj_xxxxxxxxxxxx",
|
|
322
|
-
hint: "Find this at https://kensaur.us/mushi-mushi/projects"
|
|
534
|
+
hint: "Find this at https://kensaur.us/mushi-mushi/projects",
|
|
535
|
+
validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected format: proj_ followed by 10+ alphanumeric characters"
|
|
323
536
|
});
|
|
324
|
-
const
|
|
537
|
+
const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
|
|
325
538
|
message: "API key",
|
|
326
539
|
placeholder: "mushi_xxxxxxxxxxxx",
|
|
327
|
-
hint: "Treat this like a password \u2014 it goes in your env file, not in source."
|
|
540
|
+
hint: "Treat this like a password \u2014 it goes in your env file, not in source.",
|
|
541
|
+
validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
|
|
328
542
|
});
|
|
543
|
+
const projectId = sanitizeSecret(rawProjectId);
|
|
544
|
+
const apiKey = sanitizeSecret(rawApiKey);
|
|
545
|
+
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`Invalid project ID. Expected format: proj_[A-Za-z0-9_-]{10,}. Got: ${redact(projectId)}`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (!API_KEY_PATTERN.test(apiKey)) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`Invalid API key. Expected format: mushi_[A-Za-z0-9_-]{10,}. Got: ${redact(apiKey)}`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
329
555
|
return { projectId, apiKey };
|
|
330
556
|
}
|
|
557
|
+
function sanitizeSecret(raw) {
|
|
558
|
+
return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[\r\n\0]/g, "");
|
|
559
|
+
}
|
|
560
|
+
function redact(value) {
|
|
561
|
+
if (value.length <= 8) return "***";
|
|
562
|
+
return `${value.slice(0, 4)}\u2026${value.slice(-2)}`;
|
|
563
|
+
}
|
|
331
564
|
async function promptText(opts) {
|
|
332
565
|
const value = await p.text({
|
|
333
566
|
message: opts.message,
|
|
334
567
|
placeholder: opts.placeholder,
|
|
335
|
-
validate: (v) =>
|
|
568
|
+
validate: (v) => {
|
|
569
|
+
const clean = sanitizeSecret(v);
|
|
570
|
+
if (clean.length === 0) return "Required";
|
|
571
|
+
return opts.validate ? opts.validate(clean) : void 0;
|
|
572
|
+
}
|
|
336
573
|
});
|
|
337
574
|
if (p.isCancel(value)) {
|
|
338
575
|
p.cancel("Aborted.");
|
|
@@ -341,34 +578,40 @@ async function promptText(opts) {
|
|
|
341
578
|
if (opts.hint) p.log.info(opts.hint);
|
|
342
579
|
return value;
|
|
343
580
|
}
|
|
344
|
-
async function installPackages(pm, packages) {
|
|
581
|
+
async function installPackages(pm, packages, cwd) {
|
|
345
582
|
const command = installCommand(pm, packages);
|
|
346
583
|
const spinner2 = p.spinner();
|
|
347
584
|
spinner2.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
|
|
348
585
|
try {
|
|
349
|
-
await runCommand(pm, packages);
|
|
586
|
+
await runCommand(pm, packages, cwd);
|
|
350
587
|
spinner2.stop(`Installed ${packages.join(", ")}`);
|
|
351
588
|
} catch (err) {
|
|
352
589
|
spinner2.stop(`Install failed \u2014 run \`${command}\` manually.`);
|
|
353
|
-
p.log.error(err instanceof Error ? err.message : String(err));
|
|
590
|
+
p.log.error(err instanceof Error ? err.name + ": " + err.message : String(err));
|
|
354
591
|
}
|
|
355
592
|
}
|
|
356
|
-
function runCommand(pm, packages) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
593
|
+
function runCommand(pm, packages, cwd) {
|
|
594
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
595
|
+
const command = process.platform === "win32" ? `${pm}.cmd` : pm;
|
|
596
|
+
return new Promise((resolve2, reject) => {
|
|
597
|
+
const child = spawn(command, [verb, ...packages], {
|
|
598
|
+
stdio: "inherit",
|
|
599
|
+
shell: false,
|
|
600
|
+
cwd,
|
|
601
|
+
env: process.env
|
|
602
|
+
});
|
|
360
603
|
child.on("error", reject);
|
|
361
604
|
child.on("exit", (code) => {
|
|
362
|
-
if (code === 0)
|
|
363
|
-
else reject(new Error(`${pm} exited with code ${code}`));
|
|
605
|
+
if (code === 0) resolve2();
|
|
606
|
+
else reject(new Error(`${pm} exited with code ${code ?? "null"}`));
|
|
364
607
|
});
|
|
365
608
|
});
|
|
366
609
|
}
|
|
367
610
|
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
368
|
-
const target = ENV_FILES.find((f) =>
|
|
369
|
-
const targetPath =
|
|
611
|
+
const target = ENV_FILES.find((f) => existsSync4(join4(cwd, f))) ?? ENV_FILES[0];
|
|
612
|
+
const targetPath = join4(cwd, target);
|
|
370
613
|
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
371
|
-
const existing =
|
|
614
|
+
const existing = existsSync4(targetPath) ? readFileSync4(targetPath, "utf-8") : "";
|
|
372
615
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
373
616
|
p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
|
|
374
617
|
return;
|
|
@@ -378,20 +621,38 @@ function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
|
378
621
|
# Mushi Mushi
|
|
379
622
|
${newVars}
|
|
380
623
|
`);
|
|
381
|
-
if (!existing) {
|
|
382
|
-
writeFileSync2(targetPath, readFileSync3(targetPath, "utf-8"));
|
|
383
|
-
}
|
|
384
624
|
p.log.success(`Wrote env vars to ${target}`);
|
|
385
625
|
warnIfMissingFromGitignore(cwd, target);
|
|
386
626
|
}
|
|
627
|
+
function isEnvFileCoveredByGitignore(gitignoreContent, envFile) {
|
|
628
|
+
const lines = gitignoreContent.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
629
|
+
let covered = false;
|
|
630
|
+
for (const line of lines) {
|
|
631
|
+
if (line.startsWith("!")) {
|
|
632
|
+
if (matchesGitignorePattern(line.slice(1), envFile)) covered = false;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (matchesGitignorePattern(line, envFile)) covered = true;
|
|
636
|
+
}
|
|
637
|
+
return covered;
|
|
638
|
+
}
|
|
639
|
+
function matchesGitignorePattern(pattern, filename) {
|
|
640
|
+
if (pattern.endsWith("/")) return false;
|
|
641
|
+
const normalized = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
642
|
+
const regexSource = normalized.split("").map((ch) => ch === "*" ? "[^/]*" : escapeRegexChar(ch)).join("");
|
|
643
|
+
return new RegExp(`^${regexSource}$`).test(filename);
|
|
644
|
+
}
|
|
645
|
+
function escapeRegexChar(ch) {
|
|
646
|
+
return /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
|
|
647
|
+
}
|
|
387
648
|
function warnIfMissingFromGitignore(cwd, envFile) {
|
|
388
|
-
const gitignorePath =
|
|
389
|
-
if (!
|
|
649
|
+
const gitignorePath = join4(cwd, ".gitignore");
|
|
650
|
+
if (!existsSync4(gitignorePath)) {
|
|
390
651
|
p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
|
|
391
652
|
return;
|
|
392
653
|
}
|
|
393
|
-
const content =
|
|
394
|
-
if (!content
|
|
654
|
+
const content = readFileSync4(gitignorePath, "utf-8");
|
|
655
|
+
if (!isEnvFileCoveredByGitignore(content, envFile)) {
|
|
395
656
|
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
396
657
|
}
|
|
397
658
|
}
|
|
@@ -406,10 +667,105 @@ function printNextSteps(framework, apiKey, projectId) {
|
|
|
406
667
|
p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
|
|
407
668
|
p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
|
|
408
669
|
}
|
|
670
|
+
async function maybeSendTestReport(credentials, options) {
|
|
671
|
+
if (options.sendTestReport === false) return;
|
|
672
|
+
let shouldSend;
|
|
673
|
+
if (options.sendTestReport === true || options.yes) {
|
|
674
|
+
shouldSend = true;
|
|
675
|
+
} else {
|
|
676
|
+
const answer = await p.confirm({
|
|
677
|
+
message: "Send a test report now to verify the pipeline?",
|
|
678
|
+
initialValue: true
|
|
679
|
+
});
|
|
680
|
+
if (p.isCancel(answer)) return;
|
|
681
|
+
shouldSend = answer;
|
|
682
|
+
}
|
|
683
|
+
if (!shouldSend) return;
|
|
684
|
+
const spinner2 = p.spinner();
|
|
685
|
+
spinner2.start("Sending test report\u2026");
|
|
686
|
+
const endpoint = normalizeEndpoint(options.endpoint);
|
|
687
|
+
const controller = new AbortController();
|
|
688
|
+
const timer = setTimeout(() => controller.abort(), TEST_REPORT_FETCH_TIMEOUT_MS);
|
|
689
|
+
try {
|
|
690
|
+
const res = await fetch(`${endpoint}/v1/reports`, {
|
|
691
|
+
method: "POST",
|
|
692
|
+
signal: controller.signal,
|
|
693
|
+
headers: {
|
|
694
|
+
"Content-Type": "application/json",
|
|
695
|
+
"X-Mushi-Api-Key": credentials.apiKey,
|
|
696
|
+
"X-Mushi-Project": credentials.projectId
|
|
697
|
+
},
|
|
698
|
+
body: JSON.stringify({
|
|
699
|
+
projectId: credentials.projectId,
|
|
700
|
+
description: "Test report from the mushi-mushi setup wizard",
|
|
701
|
+
category: "other",
|
|
702
|
+
reporterToken: `wizard-${randomUUID()}`,
|
|
703
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
704
|
+
environment: {
|
|
705
|
+
url: "cli://wizard",
|
|
706
|
+
userAgent: `mushi-wizard/${process.platform}-${process.arch}`,
|
|
707
|
+
platform: process.platform,
|
|
708
|
+
language: "en",
|
|
709
|
+
viewport: { width: 0, height: 0 },
|
|
710
|
+
referrer: "",
|
|
711
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
712
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
});
|
|
716
|
+
if (!res.ok) {
|
|
717
|
+
spinner2.stop(`Test report rejected (HTTP ${res.status}).`);
|
|
718
|
+
p.log.warn(
|
|
719
|
+
res.status === 401 || res.status === 403 ? "Credentials did not authenticate \u2014 double-check the project ID and API key." : "Skipping test report. You can retry with `mushi test`."
|
|
720
|
+
);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
spinner2.stop("Test report sent.");
|
|
724
|
+
p.log.success("View it at https://kensaur.us/mushi-mushi/reports");
|
|
725
|
+
} catch (err) {
|
|
726
|
+
const aborted = err instanceof Error && err.name === "AbortError";
|
|
727
|
+
spinner2.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
|
|
728
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
729
|
+
} finally {
|
|
730
|
+
clearTimeout(timer);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function printFreshnessHint() {
|
|
734
|
+
const result = await checkFreshness("mushi-mushi", MUSHI_CLI_VERSION);
|
|
735
|
+
if (!result || !result.isOutdated) return;
|
|
736
|
+
p.log.info(
|
|
737
|
+
`A newer version of mushi-mushi is available: ${result.current} \u2192 ${result.latest}. Run \`npx mushi-mushi@latest\` to get the freshest wizard.`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
function warnIfWorkspaceRoot(cwd) {
|
|
741
|
+
let hint;
|
|
742
|
+
try {
|
|
743
|
+
hint = detectWorkspaceHint(cwd);
|
|
744
|
+
} catch {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (!hint || hint.apps.length === 0) return;
|
|
748
|
+
const hasFrameworkAtCwd = hint.apps.some(
|
|
749
|
+
(app) => isSameDirectory(cwd, resolveWorkspaceAppPath(hint.root, app.relativePath))
|
|
750
|
+
);
|
|
751
|
+
if (hasFrameworkAtCwd) return;
|
|
752
|
+
const apps = hint.apps.slice(0, 5).map((app) => ` \u2022 ${app.relativePath} (${app.framework})`).join("\n");
|
|
753
|
+
p.log.warn(
|
|
754
|
+
`You appear to be at a workspace root (source: ${hint.source}). Mushi will install into the current directory, which has no framework dep. You probably meant one of these sub-packages:
|
|
755
|
+
${apps}
|
|
756
|
+
Run \`mushi init --cwd <path>\` \u2014 or re-run the wizard from inside that package.`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
function resolveWorkspaceAppPath(root, relativePath) {
|
|
760
|
+
return `${root}/${relativePath}`.replace(/\\/g, "/");
|
|
761
|
+
}
|
|
762
|
+
function isSameDirectory(a, b) {
|
|
763
|
+
return a.replace(/\\/g, "/").replace(/\/+$/, "") === b.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
764
|
+
}
|
|
409
765
|
|
|
410
766
|
// src/index.ts
|
|
411
767
|
async function apiCall(path, config, options = {}) {
|
|
412
|
-
const endpoint = config.endpoint ??
|
|
768
|
+
const endpoint = config.endpoint ?? DEFAULT_ENDPOINT;
|
|
413
769
|
const res = await fetch(`${endpoint}${path}`, {
|
|
414
770
|
...options,
|
|
415
771
|
headers: {
|
|
@@ -422,23 +778,26 @@ async function apiCall(path, config, options = {}) {
|
|
|
422
778
|
});
|
|
423
779
|
return res.json();
|
|
424
780
|
}
|
|
425
|
-
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(
|
|
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) => {
|
|
781
|
+
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION);
|
|
782
|
+
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").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) => {
|
|
427
783
|
await runInit({
|
|
428
784
|
projectId: opts.projectId,
|
|
429
785
|
apiKey: opts.apiKey,
|
|
430
786
|
framework: opts.framework,
|
|
431
787
|
skipInstall: opts.skipInstall,
|
|
432
|
-
yes: opts.yes
|
|
788
|
+
yes: opts.yes,
|
|
789
|
+
cwd: opts.cwd,
|
|
790
|
+
endpoint: opts.endpoint,
|
|
791
|
+
sendTestReport: opts.skipTestReport ? false : void 0
|
|
433
792
|
});
|
|
434
793
|
});
|
|
435
794
|
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) => {
|
|
436
795
|
const config = loadConfig();
|
|
437
796
|
config.apiKey = opts.apiKey;
|
|
438
|
-
if (opts.endpoint) config.endpoint = opts.endpoint;
|
|
797
|
+
if (opts.endpoint) config.endpoint = assertEndpoint(opts.endpoint);
|
|
439
798
|
if (opts.projectId) config.projectId = opts.projectId;
|
|
440
799
|
saveConfig(config);
|
|
441
|
-
console.log("Saved credentials to ~/.mushirc");
|
|
800
|
+
console.log("Saved credentials to ~/.mushirc (mode 0o600)");
|
|
442
801
|
});
|
|
443
802
|
program.command("status").description("Show project stats").action(async () => {
|
|
444
803
|
const config = loadConfig();
|
|
@@ -493,10 +852,10 @@ reports.command("triage <id>").description("Update report status/severity").opti
|
|
|
493
852
|
program.command("config").description("View or update CLI config").argument("[key]", "Config key to set").argument("[value]", "Value").action((key, value) => {
|
|
494
853
|
const config = loadConfig();
|
|
495
854
|
if (key && value) {
|
|
496
|
-
;
|
|
497
|
-
config[key] =
|
|
855
|
+
const safeValue = key === "endpoint" ? assertEndpoint(value) : value;
|
|
856
|
+
config[key] = safeValue;
|
|
498
857
|
saveConfig(config);
|
|
499
|
-
console.log(`Set ${key} = ${
|
|
858
|
+
console.log(`Set ${key} = ${safeValue}`);
|
|
500
859
|
} else {
|
|
501
860
|
console.log(JSON.stringify(config, null, 2));
|
|
502
861
|
}
|
package/dist/init.d.ts
CHANGED
|
@@ -16,7 +16,24 @@ interface InitOptions {
|
|
|
16
16
|
framework?: FrameworkId;
|
|
17
17
|
skipInstall?: boolean;
|
|
18
18
|
yes?: boolean;
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
sendTestReport?: boolean;
|
|
19
21
|
}
|
|
20
22
|
declare function runInit(options?: InitOptions): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Strip whitespace, quotes, and any control characters a user might paste by
|
|
25
|
+
* accident. Prevents env-file injection via newlines in a pasted secret.
|
|
26
|
+
* Exported for test coverage of the env-file-injection defense.
|
|
27
|
+
*/
|
|
28
|
+
declare function sanitizeSecret(raw: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Return true when any line in the user's `.gitignore` actually matches the
|
|
31
|
+
* env file we just wrote. Subtle point: `.env` in gitignore does NOT cover
|
|
32
|
+
* `.env.local` — gitignore matches by filename, not prefix. We build a tiny
|
|
33
|
+
* glob matcher (only `*` as wildcard, gitignore's common case) and test each
|
|
34
|
+
* non-comment line. `!`-prefixed negations are treated as "not covered" to
|
|
35
|
+
* stay on the safe side — better a false warning than a silent leak.
|
|
36
|
+
*/
|
|
37
|
+
declare function isEnvFileCoveredByGitignore(gitignoreContent: string, envFile: string): boolean;
|
|
21
38
|
|
|
22
|
-
export { type InitOptions, runInit };
|
|
39
|
+
export { type InitOptions, isEnvFileCoveredByGitignore, runInit, sanitizeSecret };
|
package/dist/init.js
CHANGED
|
@@ -5,21 +5,27 @@ import {
|
|
|
5
5
|
envVarsToWrite,
|
|
6
6
|
installCommand,
|
|
7
7
|
readPackageJson
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ZZNVMBMG.js";
|
|
9
|
+
import {
|
|
10
|
+
MUSHI_CLI_VERSION
|
|
11
|
+
} from "./chunk-I3FFGUE5.js";
|
|
9
12
|
|
|
10
13
|
// src/init.ts
|
|
11
14
|
import * as p from "@clack/prompts";
|
|
12
15
|
import { spawn } from "child_process";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
18
|
+
import { join as join3 } from "path";
|
|
15
19
|
|
|
16
20
|
// src/config.ts
|
|
17
|
-
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
21
|
+
import { chmodSync, readFileSync, statSync, writeFileSync, existsSync } from "fs";
|
|
18
22
|
import { join } from "path";
|
|
19
23
|
import { homedir } from "os";
|
|
20
24
|
var CONFIG_PATH = join(homedir(), ".mushirc");
|
|
25
|
+
var SECURE_FILE_MODE = 384;
|
|
21
26
|
function loadConfig(path = CONFIG_PATH) {
|
|
22
27
|
if (!existsSync(path)) return {};
|
|
28
|
+
tightenPermissions(path);
|
|
23
29
|
try {
|
|
24
30
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
25
31
|
} catch {
|
|
@@ -27,14 +33,193 @@ function loadConfig(path = CONFIG_PATH) {
|
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
35
|
function saveConfig(config, path = CONFIG_PATH) {
|
|
30
|
-
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
36
|
+
writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
|
|
37
|
+
tightenPermissions(path);
|
|
38
|
+
}
|
|
39
|
+
function tightenPermissions(path) {
|
|
40
|
+
if (process.platform === "win32") return;
|
|
41
|
+
try {
|
|
42
|
+
const current = statSync(path).mode & 511;
|
|
43
|
+
if (current !== SECURE_FILE_MODE) chmodSync(path, SECURE_FILE_MODE);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/endpoint.ts
|
|
49
|
+
var DEFAULT_ENDPOINT = "https://api.mushimushi.dev";
|
|
50
|
+
var TEST_REPORT_TIMEOUT_MS = 1e4;
|
|
51
|
+
var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
|
|
52
|
+
function normalizeEndpoint(url) {
|
|
53
|
+
const input = url ?? DEFAULT_ENDPOINT;
|
|
54
|
+
let end = input.length;
|
|
55
|
+
while (end > 0 && input.charCodeAt(end - 1) === 47) end--;
|
|
56
|
+
return input.slice(0, end);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/freshness.ts
|
|
60
|
+
var REGISTRY = "https://registry.npmjs.org";
|
|
61
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
62
|
+
async function checkFreshness(packageName, currentVersion, opts = {}) {
|
|
63
|
+
if (process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
|
|
64
|
+
const registry = opts.registry ?? REGISTRY;
|
|
65
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(
|
|
70
|
+
`${registry}/${encodeURIComponent(packageName)}/latest`,
|
|
71
|
+
{
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
headers: { Accept: "application/json" }
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
if (!res.ok) return null;
|
|
77
|
+
const body = await res.json();
|
|
78
|
+
const latest = typeof body.version === "string" ? body.version : null;
|
|
79
|
+
if (!latest) return null;
|
|
80
|
+
return {
|
|
81
|
+
current: currentVersion,
|
|
82
|
+
latest,
|
|
83
|
+
isOutdated: isNewerStableVersion(latest, currentVersion)
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isNewerStableVersion(latest, current) {
|
|
92
|
+
const latestCore = stripPreRelease(latest);
|
|
93
|
+
if (hasPreReleaseTag(latest)) return false;
|
|
94
|
+
const [la, lb, lc] = parse(latestCore);
|
|
95
|
+
const [ca, cb, cc] = parse(stripPreRelease(current));
|
|
96
|
+
if (la !== ca) return la > ca;
|
|
97
|
+
if (lb !== cb) return lb > cb;
|
|
98
|
+
return lc > cc;
|
|
99
|
+
}
|
|
100
|
+
function stripPreRelease(version) {
|
|
101
|
+
const idx = version.indexOf("-");
|
|
102
|
+
return idx === -1 ? version : version.slice(0, idx);
|
|
103
|
+
}
|
|
104
|
+
function hasPreReleaseTag(version) {
|
|
105
|
+
return version.includes("-");
|
|
106
|
+
}
|
|
107
|
+
function parse(version) {
|
|
108
|
+
const parts = version.split(".").map((part) => Number(part));
|
|
109
|
+
return [
|
|
110
|
+
Number.isFinite(parts[0]) ? parts[0] : 0,
|
|
111
|
+
Number.isFinite(parts[1]) ? parts[1] : 0,
|
|
112
|
+
Number.isFinite(parts[2]) ? parts[2] : 0
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/monorepo.ts
|
|
117
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
118
|
+
import { dirname, join as join2, resolve } from "path";
|
|
119
|
+
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
120
|
+
var FRAMEWORK_DEPS = {
|
|
121
|
+
next: "Next.js",
|
|
122
|
+
nuxt: "Nuxt",
|
|
123
|
+
"@sveltejs/kit": "SvelteKit",
|
|
124
|
+
"@angular/core": "Angular",
|
|
125
|
+
expo: "Expo",
|
|
126
|
+
"react-native": "React Native",
|
|
127
|
+
"@capacitor/core": "Capacitor",
|
|
128
|
+
svelte: "Svelte",
|
|
129
|
+
vue: "Vue",
|
|
130
|
+
react: "React"
|
|
131
|
+
};
|
|
132
|
+
function detectWorkspaceHint(cwd) {
|
|
133
|
+
const root = findWorkspaceRoot(cwd);
|
|
134
|
+
if (!root) return null;
|
|
135
|
+
const rootPkg = readPackageJsonSafely(join2(root, "package.json"));
|
|
136
|
+
if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
|
|
137
|
+
const source = existsSync2(join2(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
|
|
138
|
+
const apps = collectAppsFromGlobs(root);
|
|
139
|
+
if (apps.length === 0) return null;
|
|
140
|
+
return { root, apps, source };
|
|
141
|
+
}
|
|
142
|
+
function findWorkspaceRoot(start) {
|
|
143
|
+
let dir = resolve(start);
|
|
144
|
+
for (let i = 0; i < 8; i++) {
|
|
145
|
+
if (isWorkspaceRoot(dir)) return dir;
|
|
146
|
+
const parent = dirname(dir);
|
|
147
|
+
if (parent === dir) break;
|
|
148
|
+
dir = parent;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function isWorkspaceRoot(dir) {
|
|
153
|
+
if (existsSync2(join2(dir, "pnpm-workspace.yaml"))) return true;
|
|
154
|
+
const pkg = readPackageJsonSafely(join2(dir, "package.json"));
|
|
155
|
+
if (!pkg) return false;
|
|
156
|
+
return Boolean(pkg.workspaces);
|
|
157
|
+
}
|
|
158
|
+
function collectAppsFromGlobs(root) {
|
|
159
|
+
const results = [];
|
|
160
|
+
for (const glob of WORKSPACE_GLOB_CANDIDATES) {
|
|
161
|
+
const prefix = glob.replace("/*", "");
|
|
162
|
+
const parentDir = join2(root, prefix);
|
|
163
|
+
if (!existsSync2(parentDir)) continue;
|
|
164
|
+
let entries;
|
|
165
|
+
try {
|
|
166
|
+
entries = readdirSync(parentDir);
|
|
167
|
+
} catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const pkgPath = join2(parentDir, entry, "package.json");
|
|
172
|
+
if (!isFileSafe(pkgPath)) continue;
|
|
173
|
+
const pkg = readPackageJsonSafely(pkgPath);
|
|
174
|
+
if (!pkg) continue;
|
|
175
|
+
const framework = getFrameworkFromPkg(pkg);
|
|
176
|
+
if (!framework) continue;
|
|
177
|
+
results.push({
|
|
178
|
+
name: pkg.name ?? `${prefix}/${entry}`,
|
|
179
|
+
relativePath: `${prefix}/${entry}`,
|
|
180
|
+
framework
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
function readPackageJsonSafely(path) {
|
|
187
|
+
if (!isFileSafe(path)) return null;
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function isFileSafe(path) {
|
|
195
|
+
try {
|
|
196
|
+
return existsSync2(path) && statSync2(path).isFile();
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function getFrameworkFromPkg(pkg) {
|
|
202
|
+
const deps = {
|
|
203
|
+
...pkg.dependencies ?? {},
|
|
204
|
+
...pkg.devDependencies ?? {},
|
|
205
|
+
...pkg.peerDependencies ?? {}
|
|
206
|
+
};
|
|
207
|
+
for (const dep of Object.keys(FRAMEWORK_DEPS)) {
|
|
208
|
+
if (dep in deps) return FRAMEWORK_DEPS[dep];
|
|
209
|
+
}
|
|
210
|
+
return void 0;
|
|
31
211
|
}
|
|
32
212
|
|
|
33
213
|
// src/init.ts
|
|
34
214
|
var ENV_FILES = [".env.local", ".env"];
|
|
215
|
+
var PROJECT_ID_PATTERN = /^proj_[A-Za-z0-9_-]{10,}$/;
|
|
216
|
+
var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
|
|
35
217
|
async function runInit(options = {}) {
|
|
36
218
|
const cwd = options.cwd ?? process.cwd();
|
|
219
|
+
ensureInteractiveOrBailOut(options);
|
|
37
220
|
p.intro("\u{1F41B} Mushi Mushi setup wizard");
|
|
221
|
+
await printFreshnessHint();
|
|
222
|
+
warnIfWorkspaceRoot(cwd);
|
|
38
223
|
const pkg = readPackageJson(cwd);
|
|
39
224
|
if (!pkg) {
|
|
40
225
|
p.log.warn("No package.json found in this directory.");
|
|
@@ -53,15 +238,28 @@ async function runInit(options = {}) {
|
|
|
53
238
|
const pm = detectPackageManager(cwd);
|
|
54
239
|
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
55
240
|
if (!options.skipInstall) {
|
|
56
|
-
await installPackages(pm, packagesToInstall);
|
|
241
|
+
await installPackages(pm, packagesToInstall, cwd);
|
|
57
242
|
} else {
|
|
58
243
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
59
244
|
}
|
|
60
245
|
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
61
246
|
persistCliConfig(credentials.apiKey, credentials.projectId);
|
|
62
247
|
printNextSteps(framework, credentials.apiKey, credentials.projectId);
|
|
248
|
+
await maybeSendTestReport(credentials, options);
|
|
63
249
|
p.outro("Setup complete. Happy bug squashing \u{1F41B}");
|
|
64
250
|
}
|
|
251
|
+
function ensureInteractiveOrBailOut(options) {
|
|
252
|
+
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
253
|
+
if (isTTY) return;
|
|
254
|
+
const hasAllFlags = Boolean(
|
|
255
|
+
(options.framework || options.yes) && options.projectId && options.apiKey
|
|
256
|
+
);
|
|
257
|
+
if (hasAllFlags) return;
|
|
258
|
+
process.stderr.write(
|
|
259
|
+
"mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id proj_xxx --api-key mushi_xxx\n"
|
|
260
|
+
);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
65
263
|
async function chooseFramework(detected, options) {
|
|
66
264
|
if (options.framework) {
|
|
67
265
|
const explicit = FRAMEWORKS[options.framework];
|
|
@@ -90,23 +288,48 @@ async function chooseFramework(detected, options) {
|
|
|
90
288
|
}
|
|
91
289
|
async function collectCredentials(options) {
|
|
92
290
|
const existing = loadConfig();
|
|
93
|
-
const
|
|
291
|
+
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
94
292
|
message: "Project ID",
|
|
95
293
|
placeholder: "proj_xxxxxxxxxxxx",
|
|
96
|
-
hint: "Find this at https://kensaur.us/mushi-mushi/projects"
|
|
294
|
+
hint: "Find this at https://kensaur.us/mushi-mushi/projects",
|
|
295
|
+
validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected format: proj_ followed by 10+ alphanumeric characters"
|
|
97
296
|
});
|
|
98
|
-
const
|
|
297
|
+
const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
|
|
99
298
|
message: "API key",
|
|
100
299
|
placeholder: "mushi_xxxxxxxxxxxx",
|
|
101
|
-
hint: "Treat this like a password \u2014 it goes in your env file, not in source."
|
|
300
|
+
hint: "Treat this like a password \u2014 it goes in your env file, not in source.",
|
|
301
|
+
validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
|
|
102
302
|
});
|
|
303
|
+
const projectId = sanitizeSecret(rawProjectId);
|
|
304
|
+
const apiKey = sanitizeSecret(rawApiKey);
|
|
305
|
+
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Invalid project ID. Expected format: proj_[A-Za-z0-9_-]{10,}. Got: ${redact(projectId)}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (!API_KEY_PATTERN.test(apiKey)) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Invalid API key. Expected format: mushi_[A-Za-z0-9_-]{10,}. Got: ${redact(apiKey)}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
103
315
|
return { projectId, apiKey };
|
|
104
316
|
}
|
|
317
|
+
function sanitizeSecret(raw) {
|
|
318
|
+
return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[\r\n\0]/g, "");
|
|
319
|
+
}
|
|
320
|
+
function redact(value) {
|
|
321
|
+
if (value.length <= 8) return "***";
|
|
322
|
+
return `${value.slice(0, 4)}\u2026${value.slice(-2)}`;
|
|
323
|
+
}
|
|
105
324
|
async function promptText(opts) {
|
|
106
325
|
const value = await p.text({
|
|
107
326
|
message: opts.message,
|
|
108
327
|
placeholder: opts.placeholder,
|
|
109
|
-
validate: (v) =>
|
|
328
|
+
validate: (v) => {
|
|
329
|
+
const clean = sanitizeSecret(v);
|
|
330
|
+
if (clean.length === 0) return "Required";
|
|
331
|
+
return opts.validate ? opts.validate(clean) : void 0;
|
|
332
|
+
}
|
|
110
333
|
});
|
|
111
334
|
if (p.isCancel(value)) {
|
|
112
335
|
p.cancel("Aborted.");
|
|
@@ -115,34 +338,40 @@ async function promptText(opts) {
|
|
|
115
338
|
if (opts.hint) p.log.info(opts.hint);
|
|
116
339
|
return value;
|
|
117
340
|
}
|
|
118
|
-
async function installPackages(pm, packages) {
|
|
341
|
+
async function installPackages(pm, packages, cwd) {
|
|
119
342
|
const command = installCommand(pm, packages);
|
|
120
343
|
const spinner2 = p.spinner();
|
|
121
344
|
spinner2.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
|
|
122
345
|
try {
|
|
123
|
-
await runCommand(pm, packages);
|
|
346
|
+
await runCommand(pm, packages, cwd);
|
|
124
347
|
spinner2.stop(`Installed ${packages.join(", ")}`);
|
|
125
348
|
} catch (err) {
|
|
126
349
|
spinner2.stop(`Install failed \u2014 run \`${command}\` manually.`);
|
|
127
|
-
p.log.error(err instanceof Error ? err.message : String(err));
|
|
350
|
+
p.log.error(err instanceof Error ? err.name + ": " + err.message : String(err));
|
|
128
351
|
}
|
|
129
352
|
}
|
|
130
|
-
function runCommand(pm, packages) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
353
|
+
function runCommand(pm, packages, cwd) {
|
|
354
|
+
const verb = pm === "npm" ? "install" : "add";
|
|
355
|
+
const command = process.platform === "win32" ? `${pm}.cmd` : pm;
|
|
356
|
+
return new Promise((resolve2, reject) => {
|
|
357
|
+
const child = spawn(command, [verb, ...packages], {
|
|
358
|
+
stdio: "inherit",
|
|
359
|
+
shell: false,
|
|
360
|
+
cwd,
|
|
361
|
+
env: process.env
|
|
362
|
+
});
|
|
134
363
|
child.on("error", reject);
|
|
135
364
|
child.on("exit", (code) => {
|
|
136
|
-
if (code === 0)
|
|
137
|
-
else reject(new Error(`${pm} exited with code ${code}`));
|
|
365
|
+
if (code === 0) resolve2();
|
|
366
|
+
else reject(new Error(`${pm} exited with code ${code ?? "null"}`));
|
|
138
367
|
});
|
|
139
368
|
});
|
|
140
369
|
}
|
|
141
370
|
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
142
|
-
const target = ENV_FILES.find((f) =>
|
|
143
|
-
const targetPath =
|
|
371
|
+
const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
|
|
372
|
+
const targetPath = join3(cwd, target);
|
|
144
373
|
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
145
|
-
const existing =
|
|
374
|
+
const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
|
|
146
375
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
147
376
|
p.log.warn(`Existing MUSHI_* vars found in ${target} \u2014 leaving them untouched.`);
|
|
148
377
|
return;
|
|
@@ -152,20 +381,38 @@ function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
|
152
381
|
# Mushi Mushi
|
|
153
382
|
${newVars}
|
|
154
383
|
`);
|
|
155
|
-
if (!existing) {
|
|
156
|
-
writeFileSync2(targetPath, readFileSync2(targetPath, "utf-8"));
|
|
157
|
-
}
|
|
158
384
|
p.log.success(`Wrote env vars to ${target}`);
|
|
159
385
|
warnIfMissingFromGitignore(cwd, target);
|
|
160
386
|
}
|
|
387
|
+
function isEnvFileCoveredByGitignore(gitignoreContent, envFile) {
|
|
388
|
+
const lines = gitignoreContent.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
389
|
+
let covered = false;
|
|
390
|
+
for (const line of lines) {
|
|
391
|
+
if (line.startsWith("!")) {
|
|
392
|
+
if (matchesGitignorePattern(line.slice(1), envFile)) covered = false;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (matchesGitignorePattern(line, envFile)) covered = true;
|
|
396
|
+
}
|
|
397
|
+
return covered;
|
|
398
|
+
}
|
|
399
|
+
function matchesGitignorePattern(pattern, filename) {
|
|
400
|
+
if (pattern.endsWith("/")) return false;
|
|
401
|
+
const normalized = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
402
|
+
const regexSource = normalized.split("").map((ch) => ch === "*" ? "[^/]*" : escapeRegexChar(ch)).join("");
|
|
403
|
+
return new RegExp(`^${regexSource}$`).test(filename);
|
|
404
|
+
}
|
|
405
|
+
function escapeRegexChar(ch) {
|
|
406
|
+
return /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
|
|
407
|
+
}
|
|
161
408
|
function warnIfMissingFromGitignore(cwd, envFile) {
|
|
162
|
-
const gitignorePath =
|
|
163
|
-
if (!
|
|
409
|
+
const gitignorePath = join3(cwd, ".gitignore");
|
|
410
|
+
if (!existsSync3(gitignorePath)) {
|
|
164
411
|
p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
|
|
165
412
|
return;
|
|
166
413
|
}
|
|
167
|
-
const content =
|
|
168
|
-
if (!content
|
|
414
|
+
const content = readFileSync3(gitignorePath, "utf-8");
|
|
415
|
+
if (!isEnvFileCoveredByGitignore(content, envFile)) {
|
|
169
416
|
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
170
417
|
}
|
|
171
418
|
}
|
|
@@ -180,6 +427,103 @@ function printNextSteps(framework, apiKey, projectId) {
|
|
|
180
427
|
p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
|
|
181
428
|
p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
|
|
182
429
|
}
|
|
430
|
+
async function maybeSendTestReport(credentials, options) {
|
|
431
|
+
if (options.sendTestReport === false) return;
|
|
432
|
+
let shouldSend;
|
|
433
|
+
if (options.sendTestReport === true || options.yes) {
|
|
434
|
+
shouldSend = true;
|
|
435
|
+
} else {
|
|
436
|
+
const answer = await p.confirm({
|
|
437
|
+
message: "Send a test report now to verify the pipeline?",
|
|
438
|
+
initialValue: true
|
|
439
|
+
});
|
|
440
|
+
if (p.isCancel(answer)) return;
|
|
441
|
+
shouldSend = answer;
|
|
442
|
+
}
|
|
443
|
+
if (!shouldSend) return;
|
|
444
|
+
const spinner2 = p.spinner();
|
|
445
|
+
spinner2.start("Sending test report\u2026");
|
|
446
|
+
const endpoint = normalizeEndpoint(options.endpoint);
|
|
447
|
+
const controller = new AbortController();
|
|
448
|
+
const timer = setTimeout(() => controller.abort(), TEST_REPORT_FETCH_TIMEOUT_MS);
|
|
449
|
+
try {
|
|
450
|
+
const res = await fetch(`${endpoint}/v1/reports`, {
|
|
451
|
+
method: "POST",
|
|
452
|
+
signal: controller.signal,
|
|
453
|
+
headers: {
|
|
454
|
+
"Content-Type": "application/json",
|
|
455
|
+
"X-Mushi-Api-Key": credentials.apiKey,
|
|
456
|
+
"X-Mushi-Project": credentials.projectId
|
|
457
|
+
},
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
projectId: credentials.projectId,
|
|
460
|
+
description: "Test report from the mushi-mushi setup wizard",
|
|
461
|
+
category: "other",
|
|
462
|
+
reporterToken: `wizard-${randomUUID()}`,
|
|
463
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
464
|
+
environment: {
|
|
465
|
+
url: "cli://wizard",
|
|
466
|
+
userAgent: `mushi-wizard/${process.platform}-${process.arch}`,
|
|
467
|
+
platform: process.platform,
|
|
468
|
+
language: "en",
|
|
469
|
+
viewport: { width: 0, height: 0 },
|
|
470
|
+
referrer: "",
|
|
471
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
472
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
});
|
|
476
|
+
if (!res.ok) {
|
|
477
|
+
spinner2.stop(`Test report rejected (HTTP ${res.status}).`);
|
|
478
|
+
p.log.warn(
|
|
479
|
+
res.status === 401 || res.status === 403 ? "Credentials did not authenticate \u2014 double-check the project ID and API key." : "Skipping test report. You can retry with `mushi test`."
|
|
480
|
+
);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
spinner2.stop("Test report sent.");
|
|
484
|
+
p.log.success("View it at https://kensaur.us/mushi-mushi/reports");
|
|
485
|
+
} catch (err) {
|
|
486
|
+
const aborted = err instanceof Error && err.name === "AbortError";
|
|
487
|
+
spinner2.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
|
|
488
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
489
|
+
} finally {
|
|
490
|
+
clearTimeout(timer);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function printFreshnessHint() {
|
|
494
|
+
const result = await checkFreshness("mushi-mushi", MUSHI_CLI_VERSION);
|
|
495
|
+
if (!result || !result.isOutdated) return;
|
|
496
|
+
p.log.info(
|
|
497
|
+
`A newer version of mushi-mushi is available: ${result.current} \u2192 ${result.latest}. Run \`npx mushi-mushi@latest\` to get the freshest wizard.`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
function warnIfWorkspaceRoot(cwd) {
|
|
501
|
+
let hint;
|
|
502
|
+
try {
|
|
503
|
+
hint = detectWorkspaceHint(cwd);
|
|
504
|
+
} catch {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (!hint || hint.apps.length === 0) return;
|
|
508
|
+
const hasFrameworkAtCwd = hint.apps.some(
|
|
509
|
+
(app) => isSameDirectory(cwd, resolveWorkspaceAppPath(hint.root, app.relativePath))
|
|
510
|
+
);
|
|
511
|
+
if (hasFrameworkAtCwd) return;
|
|
512
|
+
const apps = hint.apps.slice(0, 5).map((app) => ` \u2022 ${app.relativePath} (${app.framework})`).join("\n");
|
|
513
|
+
p.log.warn(
|
|
514
|
+
`You appear to be at a workspace root (source: ${hint.source}). Mushi will install into the current directory, which has no framework dep. You probably meant one of these sub-packages:
|
|
515
|
+
${apps}
|
|
516
|
+
Run \`mushi init --cwd <path>\` \u2014 or re-run the wizard from inside that package.`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
function resolveWorkspaceAppPath(root, relativePath) {
|
|
520
|
+
return `${root}/${relativePath}`.replace(/\\/g, "/");
|
|
521
|
+
}
|
|
522
|
+
function isSameDirectory(a, b) {
|
|
523
|
+
return a.replace(/\\/g, "/").replace(/\/+$/, "") === b.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
524
|
+
}
|
|
183
525
|
export {
|
|
184
|
-
|
|
526
|
+
isEnvFileCoveredByGitignore,
|
|
527
|
+
runInit,
|
|
528
|
+
sanitizeSecret
|
|
185
529
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FILE: packages/cli/src/version.ts
|
|
3
|
+
* PURPOSE: Single source of truth for the CLI version at runtime.
|
|
4
|
+
*
|
|
5
|
+
* The bundler (tsup) replaces `__MUSHI_CLI_VERSION__` with the literal from
|
|
6
|
+
* `package.json` at build time. Falling back to `'0.0.0-dev'` keeps
|
|
7
|
+
* `tsc --noEmit` happy during development.
|
|
8
|
+
*/
|
|
9
|
+
declare const MUSHI_CLI_VERSION: string;
|
|
10
|
+
|
|
11
|
+
export { MUSHI_CLI_VERSION };
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
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": {
|
|
@@ -28,8 +28,13 @@
|
|
|
28
28
|
"bugs": {
|
|
29
29
|
"url": "https://github.com/kensaurus/mushi-mushi/issues"
|
|
30
30
|
},
|
|
31
|
+
"funding": {
|
|
32
|
+
"type": "github",
|
|
33
|
+
"url": "https://github.com/sponsors/kensaurus"
|
|
34
|
+
},
|
|
31
35
|
"publishConfig": {
|
|
32
|
-
"access": "public"
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
33
38
|
},
|
|
34
39
|
"files": [
|
|
35
40
|
"dist",
|
|
@@ -78,6 +83,12 @@
|
|
|
78
83
|
"types": "./dist/detect.d.ts",
|
|
79
84
|
"default": "./dist/detect.js"
|
|
80
85
|
}
|
|
86
|
+
},
|
|
87
|
+
"./version": {
|
|
88
|
+
"import": {
|
|
89
|
+
"types": "./dist/version.d.ts",
|
|
90
|
+
"default": "./dist/version.js"
|
|
91
|
+
}
|
|
81
92
|
}
|
|
82
93
|
},
|
|
83
94
|
"type": "module",
|