@mirinjs/cli 0.0.1-alpha.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/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mirinjs/cli",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "CLI for mirin apps: dev, build, init.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/Netko-Labs/mirin#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Netko-Labs/mirin.git",
11
+ "directory": "packages/mirin-cli"
12
+ },
13
+ "bugs": "https://github.com/Netko-Labs/mirin/issues",
14
+ "keywords": [
15
+ "mirin",
16
+ "cli",
17
+ "bun",
18
+ "desktop"
19
+ ],
20
+ "bin": {
21
+ "mirin": "./src/index.ts"
22
+ },
23
+ "files": [
24
+ "src"
25
+ ],
26
+ "engines": {
27
+ "bun": ">=1.2.0"
28
+ },
29
+ "dependencies": {
30
+ "create-mirinjs": "0.0.1-alpha.0"
31
+ },
32
+ "optionalDependencies": {
33
+ "@mirinjs/darwin-arm64": "0.0.1-alpha.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Resolve the native toolchain the CLI needs (core dylib, helper, host entry,
3
+ * CEF framework) in two modes:
4
+ *
5
+ * - **in-repo** (this CLI is running inside the mirin monorepo): build the Rust
6
+ * crates from source and use `vendor/cef`. For contributors.
7
+ * - **installed** (a consumer ran `bun add -d @mirinjs/cli`): use the prebuilt
8
+ * binaries from the per-platform `mirinjs-<os>-<arch>` package, the `host`
9
+ * entry from the `mirin` package, and a CEF framework downloaded once from
10
+ * the matching GitHub Release into `~/.mirinjs/cef/<version>`. No Rust needed.
11
+ *
12
+ * Alpha supports macOS arm64 only.
13
+ */
14
+
15
+ import { $ } from "bun";
16
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
17
+ import { homedir, tmpdir } from "node:os";
18
+ import { dirname, join, resolve } from "node:path";
19
+
20
+ const REPO_SLUG = "Netko-Labs/mirin";
21
+
22
+ const CLI_DIR = import.meta.dir;
23
+ const REPO_ROOT = resolve(CLI_DIR, "..", "..", "..");
24
+ const IN_REPO = existsSync(join(REPO_ROOT, "crates", "mirin-core"));
25
+
26
+ export interface Artifacts {
27
+ /** libmirin_core.dylib — dlopened by the host. */
28
+ coreDylib: string;
29
+ /** mirin-helper — the CEF subprocess binary. */
30
+ helperBin: string;
31
+ /** host entry (TS/JS) compiled with `bun build --compile`. */
32
+ hostEntry: string;
33
+ /** Directory containing "Chromium Embedded Framework.framework". */
34
+ cefPath: string;
35
+ }
36
+
37
+ export function isInRepo(): boolean {
38
+ return IN_REPO;
39
+ }
40
+
41
+ export async function resolveArtifacts(opts: { release: boolean }): Promise<Artifacts> {
42
+ assertSupportedPlatform();
43
+ const cefPath = await ensureCef();
44
+
45
+ if (IN_REPO) {
46
+ const profile = opts.release ? "release" : "debug";
47
+ console.log(`[mirin] building native core + helper (${profile})…`);
48
+ const flags = opts.release ? ["--release"] : [];
49
+ await $`cargo build -p mirin-core -p mirin-helper ${flags}`.cwd(REPO_ROOT);
50
+ const target = join(REPO_ROOT, "target", profile);
51
+ return {
52
+ coreDylib: join(target, "libmirin_core.dylib"),
53
+ helperBin: join(target, "mirin-helper"),
54
+ hostEntry: join(REPO_ROOT, "packages", "mirin", "src", "host.ts"),
55
+ cefPath,
56
+ };
57
+ }
58
+
59
+ const nativeDir = resolveNativeDir();
60
+ return {
61
+ coreDylib: join(nativeDir, "libmirin_core.dylib"),
62
+ helperBin: join(nativeDir, "mirin-helper"),
63
+ hostEntry: resolvePackageFile("mirinjs/host"),
64
+ cefPath,
65
+ };
66
+ }
67
+
68
+ function assertSupportedPlatform(): void {
69
+ if (process.platform !== "darwin" || process.arch !== "arm64") {
70
+ throw new Error(
71
+ `mirin alpha supports macOS arm64 only (got ${process.platform}/${process.arch}).`,
72
+ );
73
+ }
74
+ }
75
+
76
+ function platformTag(): string {
77
+ return `${process.platform}-${process.arch}`; // e.g. "darwin-arm64"
78
+ }
79
+
80
+ function resolveNativeDir(): string {
81
+ const pkg = `@mirinjs/${platformTag()}`;
82
+ try {
83
+ return dirname(resolvePackageFile(`${pkg}/package.json`));
84
+ } catch {
85
+ throw new Error(
86
+ `mirin: prebuilt native package "${pkg}" is not installed. Run \`bun install\` ` +
87
+ `(it is an optional dependency of @mirinjs/cli for your platform).`,
88
+ );
89
+ }
90
+ }
91
+
92
+ function resolvePackageFile(specifier: string): string {
93
+ return Bun.resolveSync(specifier, process.cwd());
94
+ }
95
+
96
+ /** The CLI's own version, used to pick the matching CEF release asset. */
97
+ function cliVersion(): string {
98
+ const pkg = JSON.parse(readFileSync(join(CLI_DIR, "..", "package.json"), "utf8"));
99
+ return pkg.version as string;
100
+ }
101
+
102
+ async function ensureCef(): Promise<string> {
103
+ if (IN_REPO) {
104
+ const vendor = join(REPO_ROOT, "vendor", "cef");
105
+ if (existsSync(join(vendor, "Chromium Embedded Framework.framework"))) return vendor;
106
+ console.error(
107
+ "[mirin] vendor/cef missing — run `bun scripts/fetch-cef.ts` in the monorepo first.",
108
+ );
109
+ return vendor;
110
+ }
111
+
112
+ const version = cliVersion();
113
+ const cacheDir = join(homedir(), ".mirinjs", "cef", `${version}-${platformTag()}`);
114
+ if (existsSync(join(cacheDir, "Chromium Embedded Framework.framework"))) return cacheDir;
115
+
116
+ mkdirSync(cacheDir, { recursive: true });
117
+ const asset = `cef-${platformTag()}.tar.gz`;
118
+ const url = `https://github.com/${REPO_SLUG}/releases/download/v${version}/${asset}`;
119
+ console.log(`[mirin] downloading CEF for ${platformTag()} (one-time, ~hundreds of MB)…`);
120
+
121
+ const res = await fetch(url);
122
+ if (!res.ok) {
123
+ throw new Error(`mirin: failed to download CEF from ${url} (HTTP ${res.status}).`);
124
+ }
125
+ const archive = join(tmpdir(), asset);
126
+ await Bun.write(archive, res);
127
+ await $`tar -xzf ${archive} -C ${cacheDir}`;
128
+ rmSync(archive, { force: true });
129
+ return cacheDir;
130
+ }
package/src/build.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `mirin build` — package a standalone, signed .app (docs/macos-mvp.md).
3
+ *
4
+ * 1. vite build → production UI in dist/
5
+ * 2. resolve native artifacts → core + helper (in-repo build or prebuilt)
6
+ * 3. compile host (minified) → Contents/MacOS/<exe>
7
+ * 4. bundle Worker (minified) → Resources/worker.js
8
+ * 5. assemble + sign the .app → dist/ui, manifest, dylib, helpers, CEF
9
+ *
10
+ * The result runs with no env and no dev server: the host resolves everything
11
+ * from inside the bundle, and webviews load their `app://` URLs served from
12
+ * Contents/Resources by the native scheme handler.
13
+ *
14
+ * Codesign identity: ad-hoc by default; set MIRIN_SIGN_IDENTITY to a Developer
15
+ * ID to produce a distributable, notarizable app.
16
+ */
17
+
18
+ import { $ } from "bun";
19
+ import { mkdirSync, rmSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { buildAppBundle } from "./bundle.ts";
22
+ import { resolveArtifacts } from "./artifacts.ts";
23
+
24
+ export async function build(projectDir = process.cwd()): Promise<number> {
25
+ const outDir = join(projectDir, "build");
26
+ const work = join(projectDir, ".mirin");
27
+ mkdirSync(outDir, { recursive: true });
28
+ mkdirSync(work, { recursive: true });
29
+
30
+ const config = (await import(join(projectDir, "mirin.config.ts"))).default;
31
+ const appName: string = config.name ?? "Mirin App";
32
+ const bundleId: string = config.id ?? "dev.mirin.app";
33
+ const mainEntry = join(projectDir, config.main ?? "main/main.ts");
34
+
35
+ console.log(`[mirin build] ${appName}`);
36
+
37
+ // 1. production UI
38
+ console.log("[mirin build] vite build…");
39
+ await $`bunx vite build`.cwd(projectDir);
40
+
41
+ // 2. native artifacts (release)
42
+ const artifacts = await resolveArtifacts({ release: true });
43
+
44
+ // 3 + 4. host + worker (minified)
45
+ console.log("[mirin build] compiling host + bundling main process…");
46
+ const hostExe = join(work, "host-release");
47
+ const workerJs = join(work, "worker.release.js");
48
+ await $`bun build --compile --minify ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
49
+ await $`bun build ${mainEntry} --target=bun --minify --outfile ${workerJs}`.cwd(projectDir);
50
+
51
+ // 5. assemble + sign
52
+ console.log("[mirin build] assembling .app…");
53
+ rmSync(join(outDir, `${appName}.app`), { recursive: true, force: true });
54
+ const { app } = await buildAppBundle({
55
+ appName,
56
+ bundleId,
57
+ outDir,
58
+ hostExe,
59
+ coreDylib: artifacts.coreDylib,
60
+ helperBin: artifacts.helperBin,
61
+ cefPath: artifacts.cefPath,
62
+ signIdentity: process.env.MIRIN_SIGN_IDENTITY,
63
+ resources: {
64
+ uiDir: join(projectDir, "dist"),
65
+ workerJs,
66
+ manifestJson: JSON.stringify({ windows: config.windows }),
67
+ },
68
+ });
69
+
70
+ console.log(`\n[mirin build] done → ${app}`);
71
+ console.log(` open "${app}"`);
72
+ return 0;
73
+ }
package/src/bundle.ts ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Assemble a macOS .app bundle for a mirin app (docs/architecture.md §1, §5).
3
+ *
4
+ * <App>.app/Contents/
5
+ * MacOS/<exe> compiled Bun host
6
+ * MacOS/libmirin_core.dylib loaded by the host via bun:ffi
7
+ * Frameworks/Chromium Embedded Framework.framework
8
+ * Frameworks/<exe> Helper[ (Type)].app x5 (mirin-helper)
9
+ *
10
+ * Helper bundle names derive from the main executable name — CEF resolves
11
+ * "<exe> Helper (Renderer).app/Contents/MacOS/<exe> Helper (Renderer)".
12
+ */
13
+
14
+ import { $ } from "bun";
15
+ import { cpSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ const FRAMEWORK = "Chromium Embedded Framework.framework";
19
+
20
+ const HELPER_TYPES = [
21
+ { suffix: "", id: "helper" },
22
+ { suffix: " (GPU)", id: "helper.gpu" },
23
+ { suffix: " (Renderer)", id: "helper.renderer" },
24
+ { suffix: " (Plugin)", id: "helper.plugin" },
25
+ { suffix: " (Alerts)", id: "helper.alerts" },
26
+ ];
27
+
28
+ export interface BundleOptions {
29
+ appName: string; // also the executable stem; helpers are "<appName> Helper"
30
+ bundleId: string;
31
+ outDir: string;
32
+ hostExe: string; // compiled Bun host binary
33
+ coreDylib: string; // libmirin_core.dylib
34
+ helperBin: string; // compiled mirin-helper binary
35
+ cefPath: string; // dir containing the CEF framework (vendor/cef)
36
+ /** Codesign identity; "-" (default) is ad-hoc. Set to a Developer ID to ship. */
37
+ signIdentity?: string;
38
+ /** Production-only resources placed under Contents/Resources. */
39
+ resources?: {
40
+ uiDir?: string; // Vite dist/, copied to Resources/ui (served via app://ui)
41
+ workerJs?: string; // bundled main-process Worker entry -> Resources/worker.js
42
+ manifestJson?: string; // serialized manifest -> Resources/mirin.manifest.json
43
+ };
44
+ }
45
+
46
+ type PlistValue = string | boolean | Record<string, string>;
47
+
48
+ function plist(entries: Record<string, PlistValue>): string {
49
+ const render = (v: PlistValue): string => {
50
+ if (typeof v === "boolean") return v ? "<true/>" : "<false/>";
51
+ if (typeof v === "string") return `<string>${v}</string>`;
52
+ const inner = Object.entries(v)
53
+ .map(([k, val]) => `<key>${k}</key><string>${val}</string>`)
54
+ .join("");
55
+ return `<dict>${inner}</dict>`;
56
+ };
57
+ const body = Object.entries(entries)
58
+ .map(([k, v]) => ` <key>${k}</key>\n ${render(v)}`)
59
+ .join("\n");
60
+ return `<?xml version="1.0" encoding="UTF-8"?>
61
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
62
+ <plist version="1.0">
63
+ <dict>
64
+ ${body}
65
+ </dict>
66
+ </plist>
67
+ `;
68
+ }
69
+
70
+ /** Build the .app and return the path to its executable. */
71
+ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string; exe: string }> {
72
+ const { appName, bundleId, cefPath } = opts;
73
+ if (!existsSync(join(cefPath, FRAMEWORK))) {
74
+ throw new Error(`CEF framework not found at ${cefPath} — run: bun scripts/fetch-cef.ts`);
75
+ }
76
+
77
+ const app = join(opts.outDir, `${appName}.app`);
78
+ const contents = join(app, "Contents");
79
+ const macos = join(contents, "MacOS");
80
+ const frameworks = join(contents, "Frameworks");
81
+
82
+ rmSync(app, { recursive: true, force: true });
83
+ mkdirSync(macos, { recursive: true });
84
+ mkdirSync(frameworks, { recursive: true });
85
+ mkdirSync(join(contents, "Resources"), { recursive: true });
86
+
87
+ cpSync(opts.hostExe, join(macos, appName));
88
+ cpSync(opts.coreDylib, join(macos, "libmirin_core.dylib"));
89
+
90
+ writeFileSync(
91
+ join(contents, "Info.plist"),
92
+ plist({
93
+ CFBundleDevelopmentRegion: "en",
94
+ CFBundleDisplayName: appName,
95
+ CFBundleExecutable: appName,
96
+ CFBundleIdentifier: bundleId,
97
+ CFBundleInfoDictionaryVersion: "6.0",
98
+ CFBundleName: appName,
99
+ CFBundlePackageType: "APPL",
100
+ CFBundleShortVersionString: "0.0.1",
101
+ CFBundleVersion: "0.0.1",
102
+ LSMinimumSystemVersion: "13.0",
103
+ NSHighResolutionCapable: true,
104
+ NSSupportsAutomaticGraphicsSwitching: true,
105
+ LSFileQuarantineEnabled: true,
106
+ LSEnvironment: { MallocNanoZone: "0" },
107
+ }),
108
+ );
109
+
110
+ cpSync(join(cefPath, FRAMEWORK), join(frameworks, FRAMEWORK), {
111
+ recursive: true,
112
+ verbatimSymlinks: true,
113
+ });
114
+
115
+ // Production resources: the served UI, the Worker bundle, and the manifest.
116
+ const resources = join(contents, "Resources");
117
+ if (opts.resources?.uiDir) {
118
+ cpSync(opts.resources.uiDir, join(resources, "ui"), { recursive: true });
119
+ }
120
+ if (opts.resources?.workerJs) {
121
+ cpSync(opts.resources.workerJs, join(resources, "worker.js"));
122
+ }
123
+ if (opts.resources?.manifestJson != null) {
124
+ writeFileSync(join(resources, "mirin.manifest.json"), opts.resources.manifestJson);
125
+ }
126
+
127
+ for (const { suffix, id } of HELPER_TYPES) {
128
+ const name = `${appName} Helper${suffix}`;
129
+ const helperApp = join(frameworks, `${name}.app`);
130
+ mkdirSync(join(helperApp, "Contents", "MacOS"), { recursive: true });
131
+ cpSync(opts.helperBin, join(helperApp, "Contents", "MacOS", name));
132
+ writeFileSync(
133
+ join(helperApp, "Contents", "Info.plist"),
134
+ plist({
135
+ CFBundleDevelopmentRegion: "en",
136
+ CFBundleDisplayName: name,
137
+ CFBundleExecutable: name,
138
+ CFBundleIdentifier: `${bundleId}.${id}`,
139
+ CFBundleInfoDictionaryVersion: "6.0",
140
+ CFBundleName: name,
141
+ CFBundlePackageType: "APPL",
142
+ CFBundleShortVersionString: "0.0.1",
143
+ CFBundleVersion: "0.0.1",
144
+ LSMinimumSystemVersion: "13.0",
145
+ LSUIElement: "1",
146
+ NSHighResolutionCapable: true,
147
+ LSEnvironment: { MallocNanoZone: "0" },
148
+ }),
149
+ );
150
+ }
151
+
152
+ // Sign inside-out: framework, helpers, then the outer app. Ad-hoc ("-") by
153
+ // default; pass a Developer ID to produce a distributable, notarizable app.
154
+ const identity = opts.signIdentity ?? "-";
155
+ await $`codesign --force --sign ${identity} ${join(frameworks, FRAMEWORK)}`.quiet();
156
+ for (const { suffix } of HELPER_TYPES) {
157
+ await $`codesign --force --sign ${identity} ${join(frameworks, `${appName} Helper${suffix}.app`)}`.quiet();
158
+ }
159
+ await $`codesign --force --sign ${identity} ${app}`.quiet();
160
+
161
+ return { app, exe: join(macos, appName) };
162
+ }
package/src/dev.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `mirin dev` — the development loop (docs/macos-mvp.md, M4).
3
+ *
4
+ * 1. resolve native artifacts (build in-repo, or prebuilt when installed)
5
+ * 2. compile the Bun host, bundle the user's main-process Worker
6
+ * 3. assemble + ad-hoc-sign the dev .app
7
+ * 4. start the Vite dev server (rolldown-vite) for the UI
8
+ * 5. launch the app pointed at the Vite URL, with RPC injected into the webview
9
+ */
10
+
11
+ import { $ } from "bun";
12
+ import { mkdirSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { buildAppBundle } from "./bundle.ts";
15
+ import { resolveArtifacts } from "./artifacts.ts";
16
+
17
+ const DEV_URL = "http://localhost:5173";
18
+
19
+ export async function dev(projectDir = process.cwd()): Promise<number> {
20
+ const work = join(projectDir, ".mirin");
21
+ mkdirSync(work, { recursive: true });
22
+
23
+ // --- load the manifest ---
24
+ const config = (await import(join(projectDir, "mirin.config.ts"))).default;
25
+ const appName: string = config.name ?? "Mirin App";
26
+ const bundleId: string = config.id ?? "dev.mirin.app";
27
+ const mainEntry = join(projectDir, config.main ?? "main/main.ts");
28
+
29
+ // --- native artifacts ---
30
+ const artifacts = await resolveArtifacts({ release: false });
31
+
32
+ // --- compile the Bun host + bundle the Worker ---
33
+ console.log("[mirin dev] compiling host + bundling main process…");
34
+ const hostExe = join(work, "host");
35
+ const workerJs = join(work, "worker.js");
36
+ await $`bun build --compile ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(projectDir);
37
+ await $`bun build ${mainEntry} --target=bun --outfile ${workerJs}`.cwd(projectDir);
38
+
39
+ // --- assemble the dev .app ---
40
+ console.log("[mirin dev] assembling dev bundle…");
41
+ const { app, exe } = await buildAppBundle({
42
+ appName,
43
+ bundleId,
44
+ outDir: work,
45
+ hostExe,
46
+ coreDylib: artifacts.coreDylib,
47
+ helperBin: artifacts.helperBin,
48
+ cefPath: artifacts.cefPath,
49
+ });
50
+
51
+ // --- start Vite ---
52
+ console.log("[mirin dev] starting Vite dev server…");
53
+ const vite = Bun.spawn(["bunx", "vite", "--clearScreen", "false"], {
54
+ cwd: projectDir,
55
+ stdio: ["ignore", "inherit", "inherit"],
56
+ });
57
+ await waitForUrl(DEV_URL, 15_000);
58
+
59
+ // --- launch the app ---
60
+ console.log(`[mirin dev] launching ${appName}…`);
61
+ const appProc = Bun.spawn([exe], {
62
+ cwd: projectDir,
63
+ env: {
64
+ ...process.env,
65
+ MIRIN_CORE: join(app, "Contents", "MacOS", "libmirin_core.dylib"),
66
+ MIRIN_WORKER: workerJs,
67
+ MIRIN_DEV_URL: DEV_URL,
68
+ MIRIN_MANIFEST_JSON: JSON.stringify({ windows: config.windows }),
69
+ MIRIN_CONFIG_JSON: "{}",
70
+ },
71
+ stdio: ["ignore", "inherit", "inherit"],
72
+ });
73
+
74
+ const cleanup = () => {
75
+ vite.kill();
76
+ appProc.kill();
77
+ };
78
+ process.on("SIGINT", cleanup);
79
+ process.on("SIGTERM", cleanup);
80
+
81
+ const code = await appProc.exited;
82
+ vite.kill();
83
+ return code;
84
+ }
85
+
86
+ async function waitForUrl(url: string, timeoutMs: number): Promise<void> {
87
+ const deadline = Date.now() + timeoutMs;
88
+ while (Date.now() < deadline) {
89
+ try {
90
+ const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
91
+ if (res.ok) return;
92
+ } catch {
93
+ // not up yet
94
+ }
95
+ await Bun.sleep(200);
96
+ }
97
+ throw new Error(`Vite dev server did not start at ${url} within ${timeoutMs}ms`);
98
+ }
package/src/index.ts ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * mirin CLI — `dev`, `build`, `init`.
4
+ */
5
+
6
+ import { resolve } from "node:path";
7
+ import { dev } from "./dev.ts";
8
+ import { build } from "./build.ts";
9
+ import { scaffold } from "create-mirinjs";
10
+
11
+ const [command, arg] = Bun.argv.slice(2);
12
+
13
+ const USAGE = `mirin — build desktop apps with Bun + Chromium
14
+
15
+ Usage:
16
+ mirin dev run the app against the Vite dev server (HMR + typed RPC)
17
+ mirin build package a standalone, signed .app (output: ./build)
18
+ mirin init [dir] scaffold a new app
19
+ `;
20
+
21
+ switch (command) {
22
+ case "dev": {
23
+ process.exit(await dev());
24
+ break;
25
+ }
26
+ case "build": {
27
+ process.exit(await build());
28
+ break;
29
+ }
30
+ case "init": {
31
+ const targetDir = resolve(process.cwd(), arg ?? "my-mirin-app");
32
+ try {
33
+ const name = scaffold(targetDir);
34
+ console.log(`\n✓ Created ${name} in ${arg ?? "my-mirin-app"}`);
35
+ console.log(" bun install && bun run dev");
36
+ process.exit(0);
37
+ } catch (err) {
38
+ console.error(`mirin init: ${err instanceof Error ? err.message : err}`);
39
+ process.exit(1);
40
+ }
41
+ break;
42
+ }
43
+ default:
44
+ console.log(USAGE);
45
+ process.exit(command ? 1 : 0);
46
+ }