@dormer/cli 0.0.0-stage → 0.1.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.
Files changed (74) hide show
  1. package/README.md +194 -2
  2. package/bin/dormer.mjs +26 -0
  3. package/dist/cli.js +1301 -0
  4. package/dist/index.js +676 -0
  5. package/package.json +59 -4
  6. package/src/args.ts +68 -0
  7. package/src/backend.ts +142 -0
  8. package/src/cli.ts +45 -0
  9. package/src/commands/build.ts +105 -0
  10. package/src/commands/create.ts +132 -0
  11. package/src/commands/dev.ts +128 -0
  12. package/src/commands/doctor.ts +141 -0
  13. package/src/commands/inspect.ts +60 -0
  14. package/src/commands/package.ts +159 -0
  15. package/src/config.ts +151 -0
  16. package/src/host.ts +85 -0
  17. package/src/index.ts +8 -0
  18. package/src/resolve.ts +75 -0
  19. package/src/ui.ts +57 -0
  20. package/templates/react/README.md +34 -0
  21. package/templates/react/_gitignore +5 -0
  22. package/templates/react/dormer.config.json +16 -0
  23. package/templates/react/package.json +29 -0
  24. package/templates/react/renderer/index.html +12 -0
  25. package/templates/react/renderer/src/App.tsx +34 -0
  26. package/templates/react/renderer/src/main.tsx +6 -0
  27. package/templates/react/renderer/src/style.css +8 -0
  28. package/templates/react/renderer/src/vite-env.d.ts +2 -0
  29. package/templates/react/renderer/tsconfig.json +15 -0
  30. package/templates/react/renderer/vite.config.ts +7 -0
  31. package/templates/react/src/main.ts +34 -0
  32. package/templates/react/src/shared.ts +10 -0
  33. package/templates/react/tsconfig.json +15 -0
  34. package/templates/svelte/README.md +34 -0
  35. package/templates/svelte/_gitignore +5 -0
  36. package/templates/svelte/dormer.config.json +16 -0
  37. package/templates/svelte/package.json +27 -0
  38. package/templates/svelte/renderer/index.html +12 -0
  39. package/templates/svelte/renderer/src/App.svelte +32 -0
  40. package/templates/svelte/renderer/src/main.ts +5 -0
  41. package/templates/svelte/renderer/src/style.css +8 -0
  42. package/templates/svelte/renderer/src/vite-env.d.ts +2 -0
  43. package/templates/svelte/renderer/tsconfig.json +15 -0
  44. package/templates/svelte/renderer/vite.config.ts +7 -0
  45. package/templates/svelte/src/main.ts +34 -0
  46. package/templates/svelte/src/shared.ts +10 -0
  47. package/templates/svelte/tsconfig.json +15 -0
  48. package/templates/vanilla/README.md +34 -0
  49. package/templates/vanilla/_gitignore +5 -0
  50. package/templates/vanilla/dormer.config.json +16 -0
  51. package/templates/vanilla/package.json +24 -0
  52. package/templates/vanilla/renderer/index.html +12 -0
  53. package/templates/vanilla/renderer/src/main.ts +31 -0
  54. package/templates/vanilla/renderer/src/style.css +8 -0
  55. package/templates/vanilla/renderer/src/vite-env.d.ts +2 -0
  56. package/templates/vanilla/renderer/tsconfig.json +14 -0
  57. package/templates/vanilla/renderer/vite.config.ts +6 -0
  58. package/templates/vanilla/src/main.ts +34 -0
  59. package/templates/vanilla/src/shared.ts +10 -0
  60. package/templates/vanilla/tsconfig.json +15 -0
  61. package/templates/vue/README.md +34 -0
  62. package/templates/vue/_gitignore +5 -0
  63. package/templates/vue/dormer.config.json +16 -0
  64. package/templates/vue/package.json +27 -0
  65. package/templates/vue/renderer/index.html +12 -0
  66. package/templates/vue/renderer/src/App.vue +36 -0
  67. package/templates/vue/renderer/src/main.ts +5 -0
  68. package/templates/vue/renderer/src/style.css +8 -0
  69. package/templates/vue/renderer/src/vite-env.d.ts +8 -0
  70. package/templates/vue/renderer/tsconfig.json +15 -0
  71. package/templates/vue/renderer/vite.config.ts +7 -0
  72. package/templates/vue/src/main.ts +34 -0
  73. package/templates/vue/src/shared.ts +10 -0
  74. package/templates/vue/tsconfig.json +15 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1301 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/cli.ts
8
+ import path10 from "node:path";
9
+
10
+ // src/ui.ts
11
+ import { inspect } from "node:util";
12
+ var EXIT = {
13
+ ok: 0,
14
+ failure: 1,
15
+ // build failed, host exited non-zero, doctor found a failing check
16
+ missingTool: 2,
17
+ // dormer-host / dormer-bundle not found
18
+ usage: 64,
19
+ // bad arguments
20
+ config: 65,
21
+ // dormer.config.json missing or invalid
22
+ noInput: 66
23
+ // a required input (template, entry, renderer dir) does not exist
24
+ };
25
+ var CliError = class extends Error {
26
+ exitCode;
27
+ hints;
28
+ constructor(message, options = {}) {
29
+ super(message);
30
+ this.name = "CliError";
31
+ this.exitCode = options.exitCode ?? EXIT.failure;
32
+ this.hints = options.hints ?? [];
33
+ }
34
+ };
35
+ var useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && (process.stdout.isTTY || process.env.FORCE_COLOR);
36
+ var paint = (code) => (s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
37
+ var c = { dim: paint(2), bold: paint(1), red: paint(31), green: paint(32), yellow: paint(33), blue: paint(34), cyan: paint(36) };
38
+ var prefix = c.dim("[dormer]");
39
+ var log = {
40
+ info: (msg) => {
41
+ console.log(`${prefix} ${msg}`);
42
+ },
43
+ step: (msg) => {
44
+ console.log(`${prefix} ${c.cyan("\u203A")} ${msg}`);
45
+ },
46
+ ok: (msg) => {
47
+ console.log(`${prefix} ${c.green("\u2713")} ${msg}`);
48
+ },
49
+ warn: (msg) => {
50
+ console.error(`${prefix} ${c.yellow("warning:")} ${msg}`);
51
+ },
52
+ error: (msg) => {
53
+ console.error(`${prefix} ${c.red("error:")} ${msg}`);
54
+ },
55
+ hint: (msg) => {
56
+ console.error(`${prefix} ${c.dim("hint:")} ${msg}`);
57
+ }
58
+ };
59
+ function reportError(err) {
60
+ if (err instanceof CliError) {
61
+ log.error(err.message);
62
+ for (const h of err.hints) log.hint(h);
63
+ return err.exitCode;
64
+ }
65
+ const e = err;
66
+ log.error(e?.message ?? inspect(err));
67
+ if (process.env.DORMER_CLI_DEBUG && e?.stack) console.error(e.stack);
68
+ else log.hint("set DORMER_CLI_DEBUG=1 for a stack trace");
69
+ return EXIT.failure;
70
+ }
71
+ function formatBytes(n) {
72
+ if (n < 1024) return `${n} B`;
73
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`;
74
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
75
+ }
76
+
77
+ // src/args.ts
78
+ var GLOBAL_OPTIONS = { help: { type: "boolean", description: "Show help for this command", alias: "h" } };
79
+ function parseArgs(argv, spec7) {
80
+ const options = { ...GLOBAL_OPTIONS, ...spec7.options };
81
+ const aliases = /* @__PURE__ */ new Map();
82
+ for (const [name, o] of Object.entries(options)) if (o.alias) aliases.set(o.alias, name);
83
+ const flags = {};
84
+ const positionals = [];
85
+ const usage = (msg) => new CliError(msg, { exitCode: EXIT.usage, hints: [`run ${c.bold(`dormer ${spec7.name} --help`)} for usage`] });
86
+ for (let i = 0; i < argv.length; i++) {
87
+ const a = argv[i];
88
+ if (a === "--") {
89
+ positionals.push(...argv.slice(i + 1));
90
+ break;
91
+ }
92
+ if (!a.startsWith("-") || a === "-") {
93
+ positionals.push(a);
94
+ continue;
95
+ }
96
+ let name;
97
+ let inline;
98
+ if (a.startsWith("--")) {
99
+ const eq = a.indexOf("=");
100
+ name = eq === -1 ? a.slice(2) : a.slice(2, eq);
101
+ if (eq !== -1) inline = a.slice(eq + 1);
102
+ } else {
103
+ const short = a.slice(1);
104
+ const full = aliases.get(short);
105
+ if (!full) throw usage(`unknown option ${a}`);
106
+ name = full;
107
+ }
108
+ let negated = false;
109
+ if (!options[name] && name.startsWith("no-") && options[name.slice(3)]?.type === "boolean") {
110
+ name = name.slice(3);
111
+ negated = true;
112
+ }
113
+ const o = options[name];
114
+ if (!o) throw usage(`unknown option --${name}`);
115
+ if (o.type === "boolean") {
116
+ if (inline !== void 0) throw usage(`--${name} does not take a value`);
117
+ flags[name] = !negated;
118
+ continue;
119
+ }
120
+ const value = inline ?? argv[++i];
121
+ if (value === void 0) throw usage(`--${name} requires a value`);
122
+ if (o.values && !o.values.includes(value)) throw usage(`--${name} must be one of: ${o.values.join(", ")} (got "${value}")`);
123
+ if (o.type === "number") {
124
+ const n = Number(value);
125
+ if (!Number.isFinite(n)) throw usage(`--${name} must be a number`);
126
+ flags[name] = n;
127
+ continue;
128
+ }
129
+ if (o.type === "list") {
130
+ flags[name] = flags[name] ?? [];
131
+ flags[name].push(value);
132
+ continue;
133
+ }
134
+ flags[name] = value;
135
+ }
136
+ for (const [name, o] of Object.entries(spec7.options)) if (o.default !== void 0 && flags[name] === void 0) flags[name] = o.default;
137
+ return { positionals, flags };
138
+ }
139
+ function renderHelp(spec7) {
140
+ const lines = [`${c.bold(`dormer ${spec7.name}`)} \u2014 ${spec7.summary}`, "", `Usage: dormer ${spec7.usage}`];
141
+ if (spec7.description) lines.push("", spec7.description.trim());
142
+ const options = { ...spec7.options, ...GLOBAL_OPTIONS };
143
+ const names = Object.entries(options).map(([n, o]) => [`--${n}${o.alias ? `, -${o.alias}` : ""}${o.type === "boolean" ? "" : o.type === "list" ? " <value>\u2026" : " <value>"}`, o]);
144
+ const width = Math.max(...names.map(([n]) => n.length)) + 2;
145
+ lines.push("", "Options:");
146
+ for (const [n, o] of names) {
147
+ const extras = [o.values ? `one of ${o.values.join("|")}` : "", o.default !== void 0 ? `default ${String(o.default)}` : ""].filter(Boolean).join(", ");
148
+ lines.push(` ${n.padEnd(width)}${o.description}${extras ? c.dim(` (${extras})`) : ""}`);
149
+ }
150
+ if (spec7.examples?.length) {
151
+ lines.push("", "Examples:");
152
+ for (const e of spec7.examples) lines.push(` ${e}`);
153
+ }
154
+ return lines.join("\n");
155
+ }
156
+ var flagString = (flags, name) => typeof flags[name] === "string" ? flags[name] : void 0;
157
+ var flagBool = (flags, name) => flags[name] === true;
158
+ var flagNumber = (flags, name) => typeof flags[name] === "number" ? flags[name] : void 0;
159
+ var flagList = (flags, name) => Array.isArray(flags[name]) ? flags[name] : [];
160
+
161
+ // src/host.ts
162
+ import fs2 from "node:fs";
163
+ import path2 from "node:path";
164
+ import { fileURLToPath } from "node:url";
165
+ import { spawn } from "node:child_process";
166
+
167
+ // src/resolve.ts
168
+ import fs from "node:fs";
169
+ import path from "node:path";
170
+ import { pathToFileURL } from "node:url";
171
+ function findPackageDir(fromDir, name) {
172
+ let dir = path.resolve(fromDir);
173
+ for (; ; ) {
174
+ const candidate = path.join(dir, "node_modules", name);
175
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
176
+ const parent = path.dirname(dir);
177
+ if (parent === dir) return void 0;
178
+ dir = parent;
179
+ }
180
+ }
181
+ function readJson(file) {
182
+ return JSON.parse(fs.readFileSync(file, "utf8"));
183
+ }
184
+ function packageEsmEntry(pkgDir) {
185
+ const pkg = readJson(path.join(pkgDir, "package.json"));
186
+ const pick = (v) => {
187
+ if (typeof v === "string") return v;
188
+ if (!v || typeof v !== "object") return void 0;
189
+ const o = v;
190
+ for (const k of ["import", "node", "default"]) if (o[k] !== void 0) {
191
+ const r = pick(o[k]);
192
+ if (r) return r;
193
+ }
194
+ return void 0;
195
+ };
196
+ const fromExports = pkg.exports && typeof pkg.exports === "object" && !Array.isArray(pkg.exports) ? pick(pkg.exports["."] ?? pkg.exports) : pick(pkg.exports);
197
+ const rel = fromExports ?? pkg.module ?? pkg.main ?? "index.js";
198
+ return path.join(pkgDir, rel);
199
+ }
200
+ async function loadVite(root) {
201
+ const dir = findPackageDir(root, "vite");
202
+ if (!dir) throw new CliError(`vite is not installed in ${root}`, { hints: [`run ${c.bold("npm install")} in the app directory (vite is a devDependency of every template)`] });
203
+ const version2 = readJson(path.join(dir, "package.json")).version;
204
+ const vite = await import(pathToFileURL(packageEsmEntry(dir)).href);
205
+ return { vite, version: version2, dir };
206
+ }
207
+ function findViteConfig(rendererDir) {
208
+ for (const n of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
209
+ const f = path.join(rendererDir, n);
210
+ if (fs.existsSync(f)) return f;
211
+ }
212
+ return void 0;
213
+ }
214
+ function findUp(start, marker) {
215
+ let dir = path.resolve(start);
216
+ for (; ; ) {
217
+ if (fs.existsSync(path.join(dir, marker))) return dir;
218
+ const parent = path.dirname(dir);
219
+ if (parent === dir) return void 0;
220
+ dir = parent;
221
+ }
222
+ }
223
+ function which(name) {
224
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
225
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
226
+ for (const ext of exts) {
227
+ const f = path.join(dir, name + ext.toLowerCase());
228
+ try {
229
+ fs.accessSync(f, fs.constants.X_OK);
230
+ if (fs.statSync(f).isFile()) return f;
231
+ } catch {
232
+ }
233
+ }
234
+ }
235
+ return void 0;
236
+ }
237
+
238
+ // src/host.ts
239
+ var CLI_DIR = path2.resolve(path2.dirname(fileURLToPath(import.meta.url)), "..");
240
+ var EXE = process.platform === "win32" ? ".exe" : "";
241
+ function hostPackageName() {
242
+ return `@dormer/host-${process.platform}-${process.arch}`;
243
+ }
244
+ function resolveHostBinary(appRoot) {
245
+ const tried = [];
246
+ const env = process.env.DORMER_HOST_BIN;
247
+ if (env) {
248
+ if (isExecutable(env)) return { path: path2.resolve(env), source: "env", tried };
249
+ throw new CliError(`DORMER_HOST_BIN is set but ${env} is not an executable file`, { exitCode: EXIT.missingTool, hints: ["unset DORMER_HOST_BIN or point it at a dormer-host binary"] });
250
+ }
251
+ const workspaces = /* @__PURE__ */ new Set();
252
+ for (const start of [appRoot, CLI_DIR]) {
253
+ const w = findUp(start, "Cargo.toml");
254
+ if (w) workspaces.add(w);
255
+ }
256
+ for (const w of workspaces) {
257
+ const targetDir = process.env.CARGO_TARGET_DIR ?? path2.join(w, "target");
258
+ for (const profile of ["release", "debug"]) {
259
+ const candidate = path2.join(targetDir, profile, `dormer-host${EXE}`);
260
+ tried.push(candidate);
261
+ if (isExecutable(candidate)) return { path: candidate, source: "cargo-target", tried };
262
+ }
263
+ }
264
+ const pkg = findPackageDir(appRoot, hostPackageName()) ?? findPackageDir(CLI_DIR, hostPackageName());
265
+ const fromPkg = pkg ? path2.join(pkg, "bin", `dormer-host${EXE}`) : path2.join(appRoot, "node_modules", hostPackageName(), "bin", `dormer-host${EXE}`);
266
+ tried.push(fromPkg);
267
+ if (isExecutable(fromPkg)) return { path: fromPkg, source: "package", tried };
268
+ throw new CliError("the native host binary (dormer-host) was not found", {
269
+ exitCode: EXIT.missingTool,
270
+ hints: [
271
+ `looked at:
272
+ ${tried.map((t) => ` ${t}`).join("\n")}`,
273
+ `inside the Dormer repository: ${c.bold("cargo build --release -p dormer-host")}`,
274
+ `or set ${c.bold("DORMER_HOST_BIN=/path/to/dormer-host")}`,
275
+ `(prebuilt binaries will ship as ${hostPackageName()} once published)`
276
+ ]
277
+ });
278
+ }
279
+ function isExecutable(file) {
280
+ try {
281
+ fs2.accessSync(file, fs2.constants.X_OK);
282
+ return fs2.statSync(file).isFile();
283
+ } catch {
284
+ return false;
285
+ }
286
+ }
287
+ function hostArgs(l) {
288
+ const args = ["--app-dir", l.appDir, "--entry", l.entry, "--node", l.node, "--name", l.name];
289
+ if (l.dev) args.push("--dev");
290
+ if (l.devUrl) args.push("--dev-url", l.devUrl);
291
+ for (const v of new Set(l.passEnv ?? [])) args.push("--pass-env", v);
292
+ return args;
293
+ }
294
+ function spawnHost(l) {
295
+ const env = { ...process.env, ...l.env };
296
+ env.RUST_LOG ??= "info";
297
+ return spawn(l.host, hostArgs(l), { stdio: "inherit", env });
298
+ }
299
+
300
+ // src/commands/create.ts
301
+ var create_exports = {};
302
+ __export(create_exports, {
303
+ MONOREPO_PACKAGES: () => MONOREPO_PACKAGES,
304
+ TEMPLATES: () => TEMPLATES,
305
+ copyTemplate: () => copyTemplate,
306
+ identifierFor: () => identifierFor,
307
+ linkMonorepoPackages: () => linkMonorepoPackages,
308
+ monorepoRoot: () => monorepoRoot,
309
+ packageNameFor: () => packageNameFor,
310
+ run: () => run,
311
+ spec: () => spec,
312
+ templatesDir: () => templatesDir
313
+ });
314
+ import fs3 from "node:fs";
315
+ import path3 from "node:path";
316
+ var TEMPLATES = ["vanilla", "react", "vue", "svelte"];
317
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
318
+ var spec = {
319
+ name: "create",
320
+ summary: "Scaffold a new Dormer app from a template",
321
+ usage: "create <dir> --template <name> [--pm npm] [--link] [--force]",
322
+ description: `Copies templates/<name> into <dir>, sets the package name and bundle identifier
323
+ (com.example.<name>) and prints the next steps. Templates depend on the published @dormer/*
324
+ packages; inside the Dormer monorepo pass --link to reference the local packages instead.`,
325
+ options: {
326
+ template: { type: "string", alias: "t", description: "Template to copy", values: TEMPLATES },
327
+ pm: { type: "string", description: "Package manager used in the printed next steps", values: PACKAGE_MANAGERS, default: "npm" },
328
+ link: { type: "boolean", description: "Write file: paths to this monorepo's @dormer/* packages (no publishing needed)" },
329
+ force: { type: "boolean", description: "Allow writing into a non-empty directory" }
330
+ },
331
+ examples: ["dormer create my-app --template react", "dormer create ../scratch/app --template svelte --link"]
332
+ };
333
+ var MONOREPO_PACKAGES = {
334
+ "@dormer/api": "packages/api",
335
+ "@dormer/renderer": "packages/renderer",
336
+ "@dormer/vite-plugin": "packages/vite-plugin",
337
+ "@dormer/cli": "packages/cli",
338
+ // transitive dependencies of @dormer/api, linked too so npm can satisfy them without a registry
339
+ "@dormer/node-runtime": "runtime/node-runtime",
340
+ "@dormer/ipc": "runtime/ipc/ts"
341
+ };
342
+ function templatesDir() {
343
+ for (const d of [path3.join(CLI_DIR, "templates"), path3.resolve(CLI_DIR, "..", "..", "templates")]) {
344
+ if (fs3.existsSync(path3.join(d, "vanilla", "package.json"))) return d;
345
+ }
346
+ throw new CliError("templates directory not found", { exitCode: EXIT.noInput, hints: [`expected ${path3.join(CLI_DIR, "templates")} or ${path3.resolve(CLI_DIR, "..", "..", "templates")}`] });
347
+ }
348
+ function monorepoRoot() {
349
+ const root = path3.resolve(CLI_DIR, "..", "..");
350
+ return fs3.existsSync(path3.join(root, "packages", "api", "package.json")) && fs3.existsSync(path3.join(root, "runtime", "ipc", "ts", "package.json")) ? root : void 0;
351
+ }
352
+ function packageNameFor(dir) {
353
+ const base = path3.basename(path3.resolve(dir)).toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
354
+ return base || "dormer-app";
355
+ }
356
+ function identifierFor(name) {
357
+ const seg = name.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "app";
358
+ return `com.example.${/^[a-z]/.test(seg) ? seg : `app-${seg}`}`;
359
+ }
360
+ var TEXT_EXT = /* @__PURE__ */ new Set([".json", ".md", ".html", ".ts", ".tsx", ".vue", ".svelte", ".js", ".mjs", ".cjs", ".css", ".txt", ".yml", ".yaml"]);
361
+ function copyTemplate(src, dest, vars) {
362
+ const written = [];
363
+ const walk = (from, to) => {
364
+ fs3.mkdirSync(to, { recursive: true });
365
+ for (const entry of fs3.readdirSync(from, { withFileTypes: true })) {
366
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".dormer") continue;
367
+ const target = path3.join(to, entry.name === "_gitignore" ? ".gitignore" : entry.name);
368
+ const source = path3.join(from, entry.name);
369
+ if (entry.isDirectory()) {
370
+ walk(source, target);
371
+ continue;
372
+ }
373
+ if (TEXT_EXT.has(path3.extname(entry.name)) || entry.name === "_gitignore") {
374
+ let text = fs3.readFileSync(source, "utf8");
375
+ for (const [k, v] of Object.entries(vars)) text = text.replaceAll(`{{${k}}}`, v);
376
+ fs3.writeFileSync(target, text);
377
+ } else fs3.copyFileSync(source, target);
378
+ written.push(target);
379
+ }
380
+ };
381
+ walk(src, dest);
382
+ return written;
383
+ }
384
+ function linkMonorepoPackages(pkgJsonPath, repoRoot) {
385
+ const pkg = JSON.parse(fs3.readFileSync(pkgJsonPath, "utf8"));
386
+ const linked = [];
387
+ for (const field of ["dependencies", "devDependencies"]) {
388
+ const deps = pkg[field];
389
+ if (!deps) continue;
390
+ for (const name of Object.keys(deps)) {
391
+ const rel = MONOREPO_PACKAGES[name];
392
+ if (rel) {
393
+ deps[name] = `file:${path3.join(repoRoot, rel)}`;
394
+ linked.push(name);
395
+ }
396
+ }
397
+ }
398
+ pkg.devDependencies ??= {};
399
+ for (const [name, rel] of Object.entries(MONOREPO_PACKAGES)) {
400
+ if (!pkg.dependencies?.[name] && !pkg.devDependencies[name]) {
401
+ pkg.devDependencies[name] = `file:${path3.join(repoRoot, rel)}`;
402
+ linked.push(name);
403
+ }
404
+ }
405
+ fs3.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n");
406
+ return linked;
407
+ }
408
+ async function run(args) {
409
+ const target = args.positionals[0];
410
+ const template = flagString(args.flags, "template");
411
+ if (!target) throw new CliError("missing <dir>", { exitCode: EXIT.usage, hints: [`example: ${c.bold("dormer create my-app --template vanilla")}`] });
412
+ if (!template) throw new CliError("missing --template", { exitCode: EXIT.usage, hints: [`choose one of: ${TEMPLATES.join(", ")}`] });
413
+ const dest = path3.resolve(target);
414
+ const src = path3.join(templatesDir(), template);
415
+ if (!fs3.existsSync(path3.join(src, "package.json"))) throw new CliError(`template "${template}" not found at ${src}`, { exitCode: EXIT.noInput });
416
+ if (fs3.existsSync(dest) && fs3.readdirSync(dest).length > 0 && !flagBool(args.flags, "force")) {
417
+ throw new CliError(`${dest} exists and is not empty`, { exitCode: EXIT.usage, hints: ["choose another directory or pass --force"] });
418
+ }
419
+ const name = packageNameFor(dest);
420
+ const vars = { name, identifier: identifierFor(name), template };
421
+ log.step(`creating ${c.bold(name)} from the ${template} template in ${dest}`);
422
+ const files = copyTemplate(src, dest, vars);
423
+ log.info(`${files.length} files written`);
424
+ if (flagBool(args.flags, "link")) {
425
+ const repo = monorepoRoot();
426
+ if (!repo) throw new CliError("--link requires running the CLI from a checkout of the Dormer monorepo", { exitCode: EXIT.usage, hints: ["without --link the templates reference the published @dormer/* packages"] });
427
+ const linked = linkMonorepoPackages(path3.join(dest, "package.json"), repo);
428
+ log.info(`linked ${linked.length} @dormer packages to ${repo} via file: paths`);
429
+ }
430
+ const pm = flagString(args.flags, "pm") ?? "npm";
431
+ const runCmd = (s) => pm === "npm" ? `npm run ${s}` : `${pm} ${s}`;
432
+ const rel = path3.relative(process.cwd(), dest) || ".";
433
+ console.log(`
434
+ ${c.green("Done.")} Next steps:
435
+
436
+ cd ${rel}
437
+ ${pm} install
438
+ ${runCmd("dev")} ${c.dim("# Vite dev server + native window with live backend rebuilds")}
439
+ ${runCmd("build")} ${c.dim("# dist/renderer + dist/main.js + dist/dormer-app.json")}
440
+ ${runCmd("package")} ${c.dim("# installers via dormer-bundle")}
441
+
442
+ Run ${c.bold("npx dormer doctor")} if the window does not open.
443
+ `);
444
+ return EXIT.ok;
445
+ }
446
+
447
+ // src/commands/dev.ts
448
+ var dev_exports = {};
449
+ __export(dev_exports, {
450
+ findFreePort: () => findFreePort,
451
+ run: () => run2,
452
+ spec: () => spec2
453
+ });
454
+ import net from "node:net";
455
+ import { once } from "node:events";
456
+
457
+ // src/config.ts
458
+ import fs4 from "node:fs";
459
+ import path4 from "node:path";
460
+ var CONFIG_FILE = "dormer.config.json";
461
+ var BUNDLE_TARGETS = ["deb", "rpm", "appimage", "app", "dmg", "nsis", "msi"];
462
+ function findAppRoot(start = process.cwd()) {
463
+ let dir = path4.resolve(start);
464
+ for (; ; ) {
465
+ if (fs4.existsSync(path4.join(dir, CONFIG_FILE))) return dir;
466
+ const parent = path4.dirname(dir);
467
+ if (parent === dir) return void 0;
468
+ dir = parent;
469
+ }
470
+ }
471
+ var IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z0-9-]+)+$/;
472
+ var VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
473
+ function validateConfig(cfg) {
474
+ const problems = [];
475
+ if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) return ["top level must be an object"];
476
+ const o = cfg;
477
+ const str = (k, required) => {
478
+ if (o[k] === void 0) {
479
+ if (required) problems.push(`"${k}" is required`);
480
+ return;
481
+ }
482
+ if (typeof o[k] !== "string" || o[k].length === 0) problems.push(`"${k}" must be a non-empty string`);
483
+ };
484
+ str("name", true);
485
+ str("identifier", true);
486
+ str("version", true);
487
+ str("description", false);
488
+ str("entry", false);
489
+ str("renderer", false);
490
+ if (typeof o.identifier === "string" && !IDENTIFIER_RE.test(o.identifier)) problems.push(`"identifier" must be reverse-DNS like com.example.myapp (got "${o.identifier}")`);
491
+ if (typeof o.version === "string" && !VERSION_RE.test(o.version)) problems.push(`"version" must be semver like 1.2.3 (got "${o.version}")`);
492
+ if (o.windows !== void 0) {
493
+ if (!o.windows || typeof o.windows !== "object" || Array.isArray(o.windows)) problems.push(`"windows" must be an object keyed by window name`);
494
+ else for (const [name, w] of Object.entries(o.windows)) {
495
+ const caps = w?.capabilities;
496
+ if (caps !== void 0 && (!Array.isArray(caps) || caps.some((x) => typeof x !== "string"))) problems.push(`"windows.${name}.capabilities" must be an array of strings`);
497
+ }
498
+ }
499
+ const node = o.node;
500
+ if (node !== void 0) {
501
+ if (!node || typeof node !== "object") problems.push(`"node" must be an object`);
502
+ else {
503
+ if (node.version !== void 0 && (typeof node.version !== "string" || !/^\d+(\.\d+){0,2}$/.test(node.version))) problems.push(`"node.version" must be a version like "24" or "24.4.0"`);
504
+ if (node.binary !== void 0 && typeof node.binary !== "string") problems.push(`"node.binary" must be a path string`);
505
+ }
506
+ }
507
+ const bundle = o.bundle;
508
+ if (bundle?.targets !== void 0) {
509
+ if (!Array.isArray(bundle.targets)) problems.push(`"bundle.targets" must be an array`);
510
+ else for (const t of bundle.targets) if (!BUNDLE_TARGETS.includes(String(t))) problems.push(`"bundle.targets" contains unknown target "${String(t)}" (known: ${BUNDLE_TARGETS.join(", ")})`);
511
+ }
512
+ return problems;
513
+ }
514
+ function loadConfig(root) {
515
+ const file = path4.join(root, CONFIG_FILE);
516
+ if (!fs4.existsSync(file)) {
517
+ throw new CliError(`no ${CONFIG_FILE} found in ${root} (or any parent directory)`, {
518
+ exitCode: EXIT.config,
519
+ hints: [`run this command inside a Dormer app, or create one with ${c.bold("dormer create my-app --template vanilla")}`]
520
+ });
521
+ }
522
+ let parsed;
523
+ try {
524
+ parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
525
+ } catch (e) {
526
+ throw new CliError(`${file} is not valid JSON: ${e.message}`, { exitCode: EXIT.config });
527
+ }
528
+ const problems = validateConfig(parsed);
529
+ if (problems.length) throw new CliError(`${file} is invalid:
530
+ - ${problems.join("\n - ")}`, { exitCode: EXIT.config, hints: ["see packages/cli/README.md for the config schema"] });
531
+ return parsed;
532
+ }
533
+ function resolvePaths(root, config) {
534
+ const distDir = path4.join(root, "dist");
535
+ const devDir = path4.join(root, ".dormer", "dev");
536
+ return {
537
+ root,
538
+ configPath: path4.join(root, CONFIG_FILE),
539
+ entry: path4.resolve(root, config.entry ?? "src/main.ts"),
540
+ rendererDir: path4.resolve(root, config.renderer ?? "renderer"),
541
+ distDir,
542
+ rendererOut: path4.join(distDir, "renderer"),
543
+ backendOut: path4.join(distDir, "main.js"),
544
+ devDir,
545
+ devBackendOut: path4.join(devDir, "main.js")
546
+ };
547
+ }
548
+ function loadApp(cwd = process.cwd()) {
549
+ const root = findAppRoot(cwd) ?? cwd;
550
+ const config = loadConfig(root);
551
+ return { root, config, paths: resolvePaths(root, config) };
552
+ }
553
+
554
+ // src/backend.ts
555
+ import fs5 from "node:fs";
556
+ import path5 from "node:path";
557
+ import { isBuiltin } from "node:module";
558
+ import * as esbuild from "esbuild";
559
+ var NATIVE_DEP_RE = /^(node-gyp-build|node-gyp-build-optional-packages|bindings|prebuild-install|prebuildify|@mapbox\/node-pre-gyp|node-pre-gyp|node-addon-api|cmake-js|nan|napi-build-utils|pkg-prebuilds)$/;
560
+ function packageName(specifier) {
561
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("node:")) return void 0;
562
+ const parts = specifier.split("/");
563
+ return specifier.startsWith("@") ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0 : parts[0];
564
+ }
565
+ function findPackageJson(fromDir, name) {
566
+ let dir = fromDir;
567
+ for (; ; ) {
568
+ const f = path5.join(dir, "node_modules", name, "package.json");
569
+ if (fs5.existsSync(f)) {
570
+ try {
571
+ return JSON.parse(fs5.readFileSync(f, "utf8"));
572
+ } catch {
573
+ return void 0;
574
+ }
575
+ }
576
+ const parent = path5.dirname(dir);
577
+ if (parent === dir) return void 0;
578
+ dir = parent;
579
+ }
580
+ }
581
+ function looksNative(pkg) {
582
+ if (pkg.gypfile || pkg.binary || pkg.napi) return true;
583
+ for (const k of ["dependencies", "optionalDependencies", "peerDependencies"]) {
584
+ const deps = pkg[k];
585
+ if (deps && typeof deps === "object" && Object.keys(deps).some((d) => NATIVE_DEP_RE.test(d))) return true;
586
+ }
587
+ return false;
588
+ }
589
+ function externalsPlugin(externals, explicit) {
590
+ return {
591
+ name: "dormer-externals",
592
+ setup(build2) {
593
+ build2.onResolve({ filter: /\.node$/ }, (args) => ({ path: args.path, external: true }));
594
+ build2.onResolve({ filter: /^[^./]/ }, (args) => {
595
+ if (args.kind === "entry-point") return void 0;
596
+ const name = packageName(args.path);
597
+ if (!name) return void 0;
598
+ if (isBuiltin(name)) return { path: args.path, external: true };
599
+ if (explicit.includes(name)) {
600
+ externals.add(name);
601
+ return { path: args.path, external: true };
602
+ }
603
+ if (name.startsWith("@dormer/")) return void 0;
604
+ const pkg = findPackageJson(args.resolveDir, name);
605
+ if (pkg && looksNative(pkg)) {
606
+ externals.add(name);
607
+ return { path: args.path, external: true };
608
+ }
609
+ return void 0;
610
+ });
611
+ }
612
+ };
613
+ }
614
+ var BANNER = `import { createRequire as __dormerCreateRequire } from "node:module";
615
+ const require = __dormerCreateRequire(import.meta.url);
616
+ `;
617
+ function buildOptions(o, externals) {
618
+ return {
619
+ entryPoints: [o.entry],
620
+ outfile: o.outfile,
621
+ bundle: true,
622
+ platform: "node",
623
+ format: "esm",
624
+ target: "node22",
625
+ conditions: ["node", "import", "module", "default"],
626
+ mainFields: ["module", "main"],
627
+ sourcemap: o.sourcemap === false ? false : true,
628
+ minify: o.minify === true,
629
+ legalComments: "none",
630
+ logLevel: "silent",
631
+ absWorkingDir: o.root,
632
+ banner: { js: BANNER },
633
+ define: { "process.env.DORMER_BUNDLED": '"1"', ...o.define },
634
+ plugins: [externalsPlugin(externals, o.external ?? [])],
635
+ metafile: true
636
+ };
637
+ }
638
+ function formatMessages(messages, kind) {
639
+ return esbuild.formatMessagesSync(messages, { kind, color: Boolean(process.stdout.isTTY) }).join("");
640
+ }
641
+ async function bundleBackend(o) {
642
+ if (!fs5.existsSync(o.entry)) throw new CliError(`backend entry not found: ${o.entry}`, { exitCode: EXIT.noInput, hints: [`set "entry" in dormer.config.json (default src/main.ts)`] });
643
+ const externals = /* @__PURE__ */ new Set();
644
+ fs5.mkdirSync(path5.dirname(o.outfile), { recursive: true });
645
+ let result;
646
+ try {
647
+ result = await esbuild.build(buildOptions(o, externals));
648
+ } catch (e) {
649
+ const err = e;
650
+ if (err.errors?.length) throw new CliError(`backend bundle failed:
651
+ ${formatMessages(err.errors, "error")}`, { hints: [`entry: ${o.entry}`] });
652
+ throw e;
653
+ }
654
+ const bytes = fs5.statSync(o.outfile).size;
655
+ return { outfile: o.outfile, bytes, externals: [...externals].sort(), warnings: result.warnings };
656
+ }
657
+ async function watchBackend(o, onBuild) {
658
+ if (!fs5.existsSync(o.entry)) throw new CliError(`backend entry not found: ${o.entry}`, { exitCode: EXIT.noInput, hints: [`set "entry" in dormer.config.json (default src/main.ts)`] });
659
+ const externals = /* @__PURE__ */ new Set();
660
+ fs5.mkdirSync(path5.dirname(o.outfile), { recursive: true });
661
+ const options = buildOptions(o, externals);
662
+ options.plugins.push({
663
+ name: "dormer-watch-notify",
664
+ setup(build2) {
665
+ build2.onEnd((r) => onBuild({ ok: r.errors.length === 0, errors: r.errors, warnings: r.warnings, externals: [...externals].sort() }));
666
+ }
667
+ });
668
+ const ctx = await esbuild.context(options);
669
+ await ctx.watch();
670
+ return { dispose: () => ctx.dispose() };
671
+ }
672
+ function summarizeExternals(externals) {
673
+ return externals.length ? `${externals.length} native/external package(s) kept out of the bundle: ${c.bold(externals.join(", "))}` : "no external packages (everything bundled)";
674
+ }
675
+
676
+ // src/commands/dev.ts
677
+ var spec2 = {
678
+ name: "dev",
679
+ summary: "Run the app with the Vite dev server, backend rebuilds and the native host",
680
+ usage: "dev [--port <n>] [--host <name>] [--pass-env <VAR>]...",
681
+ description: `Starts Vite for renderer/ (HMR), bundles the backend with esbuild in watch mode to
682
+ .dormer/dev/main.js and launches dormer-host with --dev --dev-url <vite url>. The backend sees
683
+ DORMER_DEV_URL and should call win.loadURL(process.env.DORMER_DEV_URL) (templates do). When the
684
+ backend bundle changes the host is restarted. Ctrl-C or closing the last window stops everything.`,
685
+ options: {
686
+ port: { type: "number", description: "Preferred Vite port (the next free port is used if busy)" },
687
+ host: { type: "string", description: "Host name Vite listens on", default: "localhost" },
688
+ "pass-env": { type: "list", description: "Environment variable to forward into the Node sidecar (repeatable)" }
689
+ },
690
+ examples: ["dormer dev", "dormer dev --port 3000 --pass-env MY_API_KEY"]
691
+ };
692
+ async function portFree(port, host) {
693
+ return new Promise((resolve) => {
694
+ const s = net.createServer();
695
+ s.once("error", () => resolve(false));
696
+ s.listen(port, host, () => s.close(() => resolve(true)));
697
+ });
698
+ }
699
+ async function findFreePort(preferred, host) {
700
+ for (let p = preferred; p < preferred + 50; p++) if (await portFree(p, host)) return p;
701
+ throw new Error(`no free port in ${preferred}-${preferred + 49}`);
702
+ }
703
+ var alive = (p) => Boolean(p && p.exitCode === null && p.signalCode === null);
704
+ async function run2(args) {
705
+ const app = loadApp();
706
+ const { config, paths } = app;
707
+ const host = resolveHostBinary(app.root);
708
+ const hostName = flagString(args.flags, "host") ?? "localhost";
709
+ const port = await findFreePort(flagNumber(args.flags, "port") ?? config.dev?.port ?? 5173, hostName);
710
+ const { vite, version: version2 } = await loadVite(app.root);
711
+ const configFile = findViteConfig(paths.rendererDir);
712
+ if (!configFile) log.warn(`no vite.config.* in ${paths.rendererDir}; add one using @dormer/vite-plugin`);
713
+ const server = await vite.createServer({
714
+ root: paths.rendererDir,
715
+ configFile: configFile ?? false,
716
+ clearScreen: false,
717
+ logLevel: "info",
718
+ server: { port, strictPort: true, host: hostName }
719
+ });
720
+ await server.listen();
721
+ const url = server.resolvedUrls?.local[0] ?? `http://${hostName}:${port}/`;
722
+ log.ok(`vite ${version2} dev server ready at ${c.bold(url)}`);
723
+ const passEnv = /* @__PURE__ */ new Set(["DORMER_DEV_URL", "DORMER_APP_VERSION", ...config.dev?.passEnv ?? [], ...flagList(args.flags, "pass-env")]);
724
+ if (process.env.DORMER_SELFTEST !== void 0) passEnv.add("DORMER_SELFTEST");
725
+ let child;
726
+ let generation = 0;
727
+ let pendingRestart = false;
728
+ let shuttingDown = false;
729
+ let watcher;
730
+ const shutdown = async (code) => {
731
+ if (shuttingDown) await new Promise(() => {
732
+ });
733
+ shuttingDown = true;
734
+ if (alive(child)) {
735
+ child.kill("SIGTERM");
736
+ await Promise.race([once(child, "exit"), new Promise((r) => setTimeout(r, 3e3))]);
737
+ if (alive(child)) child.kill("SIGKILL");
738
+ }
739
+ await watcher?.dispose().catch(() => {
740
+ });
741
+ await server.close().catch(() => {
742
+ });
743
+ process.exit(code);
744
+ };
745
+ const launch = () => {
746
+ const myGeneration = ++generation;
747
+ const proc = spawnHost({
748
+ host: host.path,
749
+ appDir: app.root,
750
+ entry: paths.devBackendOut,
751
+ node: process.execPath,
752
+ name: config.name,
753
+ dev: true,
754
+ devUrl: url,
755
+ passEnv: [...passEnv],
756
+ env: { DORMER_DEV_URL: url, DORMER_APP_VERSION: config.version }
757
+ });
758
+ child = proc;
759
+ log.ok(`host started (pid ${proc.pid}) from ${host.path} [${host.source}]`);
760
+ proc.on("error", (e) => {
761
+ log.error(`failed to start host: ${e.message}`);
762
+ void shutdown(EXIT.failure);
763
+ });
764
+ proc.on("exit", (code, signal) => {
765
+ if (shuttingDown || myGeneration !== generation) return;
766
+ if (pendingRestart) {
767
+ pendingRestart = false;
768
+ launch();
769
+ return;
770
+ }
771
+ log.info(`host exited (${signal ?? `code ${code}`}); stopping dev server`);
772
+ void shutdown(code ?? EXIT.ok);
773
+ });
774
+ };
775
+ const restart = () => {
776
+ if (!alive(child)) {
777
+ launch();
778
+ return;
779
+ }
780
+ pendingRestart = true;
781
+ log.step("backend changed \u2014 restarting host (windows are recreated)");
782
+ const victim = child;
783
+ victim.kill("SIGTERM");
784
+ setTimeout(() => {
785
+ if (alive(victim)) victim.kill("SIGKILL");
786
+ }, 3e3).unref();
787
+ };
788
+ let builds = 0;
789
+ watcher = await watchBackend({ root: app.root, entry: paths.entry, outfile: paths.devBackendOut, sourcemap: true, external: config.backend?.external }, (r) => {
790
+ builds++;
791
+ if (r.warnings.length) console.error(formatMessages(r.warnings, "warning"));
792
+ if (!r.ok) {
793
+ log.error(`backend bundle failed (${builds === 1 ? "fix the error to start the host" : "keeping the previous host running"})
794
+ ${formatMessages(r.errors, "error")}`);
795
+ return;
796
+ }
797
+ if (builds === 1) {
798
+ log.ok(`backend bundled \u2192 ${paths.devBackendOut} (${summarizeExternals(r.externals)})`);
799
+ launch();
800
+ } else restart();
801
+ });
802
+ process.on("SIGINT", () => {
803
+ console.log();
804
+ void shutdown(130);
805
+ });
806
+ process.on("SIGTERM", () => void shutdown(143));
807
+ return new Promise(() => {
808
+ });
809
+ }
810
+
811
+ // src/commands/build.ts
812
+ var build_exports = {};
813
+ __export(build_exports, {
814
+ buildApp: () => buildApp,
815
+ cliVersion: () => cliVersion,
816
+ makeManifest: () => makeManifest,
817
+ run: () => run3,
818
+ spec: () => spec3
819
+ });
820
+ import fs6 from "node:fs";
821
+ import path6 from "node:path";
822
+ var spec3 = {
823
+ name: "build",
824
+ summary: "Build the renderer and the backend into dist/",
825
+ usage: "build [--minify] [--no-sourcemap] [--quiet]",
826
+ description: `Runs \`vite build\` for the renderer (\u2192 dist/renderer), bundles the backend with esbuild
827
+ (\u2192 dist/main.js + .map), copies ${CONFIG_FILE} and writes dist/dormer-app.json. dist/ is owned by
828
+ this command and is recreated on every run.`,
829
+ options: {
830
+ minify: { type: "boolean", description: "Minify the backend bundle" },
831
+ sourcemap: { type: "boolean", description: "Emit dist/main.js.map (use --no-sourcemap to disable)", default: true },
832
+ quiet: { type: "boolean", description: "Only print warnings and errors from Vite" }
833
+ }
834
+ };
835
+ function cliVersion() {
836
+ try {
837
+ return readJson(path6.join(CLI_DIR, "package.json")).version;
838
+ } catch {
839
+ return "0.0.0";
840
+ }
841
+ }
842
+ function makeManifest(config, externals) {
843
+ return {
844
+ manifestVersion: 1,
845
+ name: config.name,
846
+ identifier: config.identifier,
847
+ version: config.version,
848
+ description: config.description,
849
+ entry: "main.js",
850
+ assets: "renderer",
851
+ node: { version: config.node?.version },
852
+ externals,
853
+ windows: config.windows,
854
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
855
+ cli: cliVersion()
856
+ };
857
+ }
858
+ async function buildApp(app, opts = {}) {
859
+ const { config, paths } = app;
860
+ if (!fs6.existsSync(paths.rendererDir)) throw new CliError(`renderer directory not found: ${paths.rendererDir}`, { exitCode: EXIT.noInput, hints: [`set "renderer" in ${CONFIG_FILE} (default "renderer")`] });
861
+ if (!fs6.existsSync(paths.entry)) throw new CliError(`backend entry not found: ${paths.entry}`, { exitCode: EXIT.noInput, hints: [`set "entry" in ${CONFIG_FILE} (default "src/main.ts")`] });
862
+ fs6.rmSync(paths.distDir, { recursive: true, force: true });
863
+ fs6.mkdirSync(paths.distDir, { recursive: true });
864
+ const { vite, version: version2 } = await loadVite(app.root);
865
+ log.step(`building renderer with vite ${version2} \u2192 ${path6.relative(app.root, paths.rendererOut)}`);
866
+ const configFile = findViteConfig(paths.rendererDir);
867
+ if (!configFile) log.warn(`no vite.config.* in ${paths.rendererDir}; add one using @dormer/vite-plugin`);
868
+ await vite.build({
869
+ root: paths.rendererDir,
870
+ configFile: configFile ?? false,
871
+ logLevel: opts.quiet ? "warn" : "info",
872
+ build: { outDir: paths.rendererOut, emptyOutDir: true }
873
+ });
874
+ if (!fs6.existsSync(path6.join(paths.rendererOut, "index.html"))) {
875
+ throw new CliError(`vite build produced no ${path6.join(paths.rendererOut, "index.html")}`, { hints: ["the renderer must have an index.html at its root; check build.outDir is not overridden"] });
876
+ }
877
+ log.step(`bundling backend ${path6.relative(app.root, paths.entry)} \u2192 ${path6.relative(app.root, paths.backendOut)}`);
878
+ const backend = await bundleBackend({ root: app.root, entry: paths.entry, outfile: paths.backendOut, minify: opts.minify, sourcemap: opts.sourcemap !== false, external: config.backend?.external });
879
+ if (backend.warnings.length) console.error(formatMessages(backend.warnings, "warning"));
880
+ log.info(`backend ${formatBytes(backend.bytes)}; ${summarizeExternals(backend.externals)}`);
881
+ fs6.copyFileSync(paths.configPath, path6.join(paths.distDir, CONFIG_FILE));
882
+ const manifest = makeManifest(config, backend.externals);
883
+ const manifestPath = path6.join(paths.distDir, "dormer-app.json");
884
+ fs6.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
885
+ log.ok(`built ${c.bold(`${config.name} ${config.version}`)} \u2192 ${paths.distDir}`);
886
+ return { manifest, backend, manifestPath };
887
+ }
888
+ async function run3(args) {
889
+ const app = loadApp();
890
+ await buildApp(app, { minify: flagBool(args.flags, "minify"), sourcemap: args.flags.sourcemap !== false, quiet: flagBool(args.flags, "quiet") });
891
+ return EXIT.ok;
892
+ }
893
+
894
+ // src/commands/package.ts
895
+ var package_exports = {};
896
+ __export(package_exports, {
897
+ bundlePackageName: () => bundlePackageName,
898
+ defaultTargets: () => defaultTargets,
899
+ findIcuData: () => findIcuData,
900
+ makeBundleSpec: () => makeBundleSpec,
901
+ resolveBundlerBinary: () => resolveBundlerBinary,
902
+ resolveNodeBinary: () => resolveNodeBinary,
903
+ run: () => run4,
904
+ spec: () => spec4
905
+ });
906
+ import fs7 from "node:fs";
907
+ import path7 from "node:path";
908
+ import { spawnSync } from "node:child_process";
909
+ var spec4 = {
910
+ name: "package",
911
+ summary: "Produce installers for the current platform with dormer-bundle",
912
+ usage: "package [--target <t>]... [--out <dir>] [--skip-build] [--dry-run]",
913
+ description: `Builds the app (unless --skip-build), writes dist/bundle.json describing it, then runs
914
+ \`dormer-bundle --config dist/bundle.json --out <dir>\`. The bundler binary is looked up in
915
+ $DORMER_BUNDLE_BIN, the cargo target directory and PATH; when missing, instructions are printed
916
+ and the exit code is 2. --dry-run stops after writing bundle.json.`,
917
+ options: {
918
+ target: { type: "list", alias: "t", description: "Installer format (repeatable)", values: BUNDLE_TARGETS },
919
+ out: { type: "string", description: "Output directory", default: "dist/bundle" },
920
+ "skip-build": { type: "boolean", description: "Reuse the existing dist/ instead of rebuilding" },
921
+ "dry-run": { type: "boolean", description: "Only write dist/bundle.json" }
922
+ }
923
+ };
924
+ function findIcuData(nodeBinary) {
925
+ const dir = path7.dirname(nodeBinary);
926
+ try {
927
+ const f = fs7.readdirSync(dir).find((n) => /^icudt\d+l\.dat$/.test(n));
928
+ return f ? path7.join(dir, f) : void 0;
929
+ } catch {
930
+ return void 0;
931
+ }
932
+ }
933
+ function defaultTargets(platform = process.platform) {
934
+ if (platform === "darwin") return ["app", "dmg"];
935
+ if (platform === "win32") return ["nsis"];
936
+ return ["deb", "appimage"];
937
+ }
938
+ var DEFAULT_LINUX_DEPENDS = ["libwebkit2gtk-4.1-0", "libgtk-3-0"];
939
+ function makeBundleSpec(config, o) {
940
+ const b = config.bundle ?? {};
941
+ let icons = (b.icons ?? []).map((p) => path7.resolve(o.root, p));
942
+ if (icons.length === 0 && fs7.existsSync(path7.join(o.root, "icons"))) {
943
+ icons = fs7.readdirSync(path7.join(o.root, "icons")).filter((f) => /\.(png|icns|ico|svg)$/i.test(f)).map((f) => path7.join(o.root, "icons", f));
944
+ }
945
+ return {
946
+ name: config.name,
947
+ identifier: config.identifier,
948
+ version: config.version,
949
+ description: config.description ?? "",
950
+ appDir: o.appDir,
951
+ entry: "main.js",
952
+ hostBinary: o.hostBinary,
953
+ nodeBinary: o.nodeBinary,
954
+ targets: o.targets,
955
+ icons,
956
+ linux: { depends: b.linux?.depends ?? DEFAULT_LINUX_DEPENDS },
957
+ macos: { entitlements: b.macos?.entitlements ? path7.resolve(o.root, b.macos.entitlements) : null, signingIdentity: b.macos?.signingIdentity ?? null, minimumSystemVersion: b.macos?.minimumSystemVersion ?? "13.0" },
958
+ windows: { signCommand: b.windows?.signCommand ?? null },
959
+ sourceMaps: b.sourceMaps ?? false,
960
+ nodeIcuData: config.node?.icu === "full" ? findIcuData(o.nodeBinary) ?? null : null
961
+ };
962
+ }
963
+ function resolveBundlerBinary(appRoot) {
964
+ const exe = process.platform === "win32" ? "dormer-bundle.exe" : "dormer-bundle";
965
+ const tried = [];
966
+ if (process.env.DORMER_BUNDLE_BIN) {
967
+ tried.push(process.env.DORMER_BUNDLE_BIN);
968
+ if (isExecutable(process.env.DORMER_BUNDLE_BIN)) return { path: process.env.DORMER_BUNDLE_BIN, tried };
969
+ }
970
+ const workspaces = /* @__PURE__ */ new Set();
971
+ for (const start of [appRoot, CLI_DIR]) {
972
+ const ws = findUp(start, "Cargo.toml");
973
+ if (ws) workspaces.add(ws);
974
+ }
975
+ for (const ws of workspaces) {
976
+ for (const profile of ["release", "debug"]) {
977
+ const candidate = path7.join(process.env.CARGO_TARGET_DIR ?? path7.join(ws, "target"), profile, exe);
978
+ tried.push(candidate);
979
+ if (isExecutable(candidate)) return { path: candidate, tried };
980
+ }
981
+ }
982
+ const pkg = findPackageDir(appRoot, bundlePackageName()) ?? findPackageDir(CLI_DIR, bundlePackageName());
983
+ const fromPkg = pkg ? path7.join(pkg, "bin", exe) : path7.join(appRoot, "node_modules", bundlePackageName(), "bin", exe);
984
+ tried.push(fromPkg);
985
+ if (isExecutable(fromPkg)) return { path: fromPkg, tried };
986
+ const onPath = which("dormer-bundle");
987
+ tried.push("$PATH");
988
+ return { path: onPath, tried };
989
+ }
990
+ function bundlePackageName() {
991
+ return `@dormer/bundle-${process.platform}-${process.arch}`;
992
+ }
993
+ function resolveNodeBinary(config) {
994
+ const candidate = config.node?.binary ?? process.env.DORMER_NODE_BIN ?? process.execPath;
995
+ if (!isExecutable(candidate)) throw new CliError(`Node binary not found: ${candidate}`, { exitCode: EXIT.noInput, hints: ['set "node.binary" in dormer.config.json or DORMER_NODE_BIN'] });
996
+ return candidate;
997
+ }
998
+ async function run4(args) {
999
+ const app = loadApp();
1000
+ const { config, paths } = app;
1001
+ if (!flagBool(args.flags, "skip-build")) await buildApp(app, { quiet: true });
1002
+ else if (!fs7.existsSync(path7.join(paths.distDir, "dormer-app.json"))) throw new CliError("--skip-build given but dist/dormer-app.json does not exist", { exitCode: EXIT.noInput, hints: [`run ${c.bold("dormer build")} first`] });
1003
+ const requested = flagList(args.flags, "target");
1004
+ const targets = requested.length ? requested : config.bundle?.targets ?? defaultTargets();
1005
+ const host = resolveHostBinary(app.root);
1006
+ const nodeBinary = resolveNodeBinary(config);
1007
+ const pinned = config.node?.version;
1008
+ if (pinned && pinned.split(".")[0] !== process.versions.node.split(".")[0] && nodeBinary === process.execPath) {
1009
+ log.warn(`dormer.config.json pins Node ${pinned} but the app is packaged with the running Node ${process.versions.node}; set "node.binary" to a Node ${pinned} executable to ship that one`);
1010
+ }
1011
+ const bundle = makeBundleSpec(config, { root: app.root, appDir: paths.distDir, hostBinary: host.path, nodeBinary, targets });
1012
+ const specPath = path7.join(paths.distDir, "bundle.json");
1013
+ fs7.writeFileSync(specPath, JSON.stringify(bundle, null, 2) + "\n");
1014
+ log.ok(`wrote ${specPath} (targets: ${targets.join(", ")})`);
1015
+ if (flagBool(args.flags, "dry-run")) return EXIT.ok;
1016
+ const outDir = path7.resolve(app.root, flagString(args.flags, "out") ?? "dist/bundle");
1017
+ const bundler = resolveBundlerBinary(app.root);
1018
+ if (!bundler.path) {
1019
+ log.error("dormer-bundle (the Rust packaging tool from tools/bundler) was not found");
1020
+ log.hint(`looked at: ${bundler.tried.join(", ")}`);
1021
+ log.hint(`inside the Dormer repository: ${c.bold("cargo build --release -p dormer-bundle")}`);
1022
+ log.hint(`or set ${c.bold("DORMER_BUNDLE_BIN=/path/to/dormer-bundle")}`);
1023
+ log.hint(`the generated ${specPath} can be passed to it manually: dormer-bundle --config ${specPath} --out ${outDir}`);
1024
+ return EXIT.missingTool;
1025
+ }
1026
+ log.step(`running ${bundler.path} --config ${specPath} --out ${outDir}`);
1027
+ fs7.mkdirSync(outDir, { recursive: true });
1028
+ const r = spawnSync(bundler.path, ["--config", specPath, "--out", outDir], { stdio: "inherit" });
1029
+ if (r.error) throw new CliError(`failed to run dormer-bundle: ${r.error.message}`);
1030
+ if (r.status !== 0) {
1031
+ log.error(`dormer-bundle exited with ${r.status ?? r.signal}`);
1032
+ return r.status ?? EXIT.failure;
1033
+ }
1034
+ log.ok(`installers written to ${outDir}`);
1035
+ return EXIT.ok;
1036
+ }
1037
+
1038
+ // src/commands/doctor.ts
1039
+ var doctor_exports = {};
1040
+ __export(doctor_exports, {
1041
+ capabilitySummary: () => capabilitySummary,
1042
+ nodeCheck: () => nodeCheck,
1043
+ run: () => run5,
1044
+ runChecks: () => runChecks,
1045
+ spec: () => spec5
1046
+ });
1047
+ import fs8 from "node:fs";
1048
+ import path8 from "node:path";
1049
+ import { spawnSync as spawnSync2 } from "node:child_process";
1050
+ var spec5 = {
1051
+ name: "doctor",
1052
+ summary: "Check the toolchain, the native host and platform prerequisites",
1053
+ usage: "doctor",
1054
+ description: `Exit code 1 when a check fails (warnings do not fail). Windows and macOS checks are
1055
+ written from documentation and have not been verified on those platforms yet.`,
1056
+ options: {}
1057
+ };
1058
+ function sh(cmd, args) {
1059
+ const r = spawnSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
1060
+ return { ok: !r.error && r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
1061
+ }
1062
+ function nodeCheck(version2 = process.versions.node) {
1063
+ const [major, minor] = version2.split(".").map(Number);
1064
+ if (major < 22) return { name: "Node.js", status: "fail", detail: `v${version2} (22 or newer required)`, hint: "install Node 24 LTS from https://nodejs.org or with your version manager" };
1065
+ if (major === 22 && minor < 18) return { name: "Node.js", status: "warn", detail: `v${version2} (type stripping needs --experimental-strip-types before 22.18; the CLI re-executes itself with it)`, hint: "upgrade to Node 22.18+ or 24 to avoid the re-exec" };
1066
+ return { name: "Node.js", status: "ok", detail: `v${version2}` };
1067
+ }
1068
+ function hostCheck(root) {
1069
+ try {
1070
+ const h = resolveHostBinary(root);
1071
+ return { name: "dormer-host", status: "ok", detail: `${h.path} (${h.source === "env" ? "DORMER_HOST_BIN" : h.source === "cargo-target" ? "cargo target dir" : hostPackageName()})` };
1072
+ } catch (e) {
1073
+ const err = e;
1074
+ return { name: "dormer-host", status: "fail", detail: err.message, hint: err.hints?.slice(1).join("; ") };
1075
+ }
1076
+ }
1077
+ function linuxChecks() {
1078
+ const checks = [];
1079
+ if (sh("pkg-config", ["--exists", "webkit2gtk-4.1"]).ok) checks.push({ name: "WebKitGTK 4.1", status: "ok", detail: `development package present (${sh("pkg-config", ["--modversion", "webkit2gtk-4.1"]).out})` });
1080
+ else {
1081
+ const ld = sh("ldconfig", ["-p"]);
1082
+ if (ld.ok && /libwebkit2gtk-4\.1/.test(ld.out)) checks.push({ name: "WebKitGTK 4.1", status: "ok", detail: "runtime library present (dev headers missing: only needed to build the host)", hint: "to build dormer-host: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev pkg-config" });
1083
+ else checks.push({ name: "WebKitGTK 4.1", status: "fail", detail: "libwebkit2gtk-4.1 not found", hint: "sudo apt install libwebkit2gtk-4.1-0 (runtime) or libwebkit2gtk-4.1-dev (to build the host); Fedora: webkit2gtk4.1" });
1084
+ }
1085
+ const bus = process.env.DBUS_SESSION_BUS_ADDRESS || (process.env.XDG_RUNTIME_DIR && fs8.existsSync(path8.join(process.env.XDG_RUNTIME_DIR, "bus")) ? `unix:path=${path8.join(process.env.XDG_RUNTIME_DIR, "bus")}` : "");
1086
+ checks.push(bus ? { name: "session D-Bus", status: "ok", detail: bus } : { name: "session D-Bus", status: "warn", detail: "no session bus (DBUS_SESSION_BUS_ADDRESS unset, no $XDG_RUNTIME_DIR/bus)", hint: "WebKitGTK and notifications want one: run under dbus-run-session -- <command>" });
1087
+ const display = process.env.WAYLAND_DISPLAY ? `wayland (${process.env.WAYLAND_DISPLAY})` : process.env.DISPLAY ? `x11 (${process.env.DISPLAY})` : "";
1088
+ checks.push(display ? { name: "display", status: "ok", detail: display } : { name: "display", status: "warn", detail: "no DISPLAY or WAYLAND_DISPLAY", hint: 'headless machines: xvfb-run -a -s "-screen 0 1280x800x24" dbus-run-session -- npm run dev' });
1089
+ const nvidia = fs8.existsSync("/proc/driver/nvidia/version") || Boolean(which("nvidia-smi"));
1090
+ if (nvidia) checks.push({ name: "GPU driver", status: "warn", detail: "NVIDIA driver detected", hint: "if windows stay blank or crash in the compositor, set WEBKIT_DISABLE_DMABUF_RENDERER=1" });
1091
+ const headless = ["xvfb-run", "dbus-run-session"].filter((t) => !which(t));
1092
+ checks.push(headless.length ? { name: "headless tools", status: "warn", detail: `missing ${headless.join(", ")} (only needed for headless test runs)`, hint: "sudo apt install xvfb dbus" } : { name: "headless tools", status: "ok", detail: "xvfb-run and dbus-run-session available" });
1093
+ return checks;
1094
+ }
1095
+ function macosChecks() {
1096
+ const clt = sh("xcode-select", ["-p"]);
1097
+ const sw = sh("sw_vers", ["-productVersion"]);
1098
+ const major = Number(sw.out.split(".")[0]);
1099
+ return [
1100
+ clt.ok ? { name: "Xcode command line tools", status: "ok", detail: clt.out } : { name: "Xcode command line tools", status: "fail", detail: "not installed", hint: "run: xcode-select --install" },
1101
+ sw.ok ? major >= 13 ? { name: "macOS", status: "ok", detail: sw.out } : { name: "macOS", status: "warn", detail: `${sw.out} (13+ recommended for WKWebView features Dormer relies on)` } : { name: "macOS", status: "warn", detail: "sw_vers unavailable" }
1102
+ ];
1103
+ }
1104
+ function windowsChecks() {
1105
+ const keys = [
1106
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
1107
+ "HKLM\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
1108
+ "HKCU\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
1109
+ ];
1110
+ for (const k of keys) {
1111
+ const r = sh("reg", ["query", k, "/v", "pv"]);
1112
+ const m = r.ok ? /pv\s+REG_SZ\s+(\S+)/.exec(r.out) : null;
1113
+ if (m && m[1] !== "0.0.0.0") return [{ name: "WebView2 runtime", status: "ok", detail: `version ${m[1]}` }];
1114
+ }
1115
+ return [{ name: "WebView2 runtime", status: "fail", detail: "Evergreen WebView2 runtime not registered", hint: "install it from https://developer.microsoft.com/microsoft-edge/webview2/ (Windows 11 ships it)" }];
1116
+ }
1117
+ function projectChecks(root) {
1118
+ const checks = [];
1119
+ const esb = findPackageDir(CLI_DIR, "esbuild");
1120
+ checks.push(esb ? { name: "esbuild", status: "ok", detail: `v${readJson(path8.join(esb, "package.json")).version}` } : { name: "esbuild", status: "fail", detail: "not resolvable from @dormer/cli", hint: "reinstall @dormer/cli" });
1121
+ if (!root) return checks;
1122
+ const vite = findPackageDir(root, "vite");
1123
+ checks.push(vite ? { name: "vite", status: "ok", detail: `v${readJson(path8.join(vite, "package.json")).version} (${vite})` } : { name: "vite", status: "fail", detail: `not installed in ${root}`, hint: "npm install (vite is a devDependency of every template)" });
1124
+ for (const p of ["@dormer/api", "@dormer/renderer", "@dormer/vite-plugin"]) {
1125
+ const dir = findPackageDir(root, p);
1126
+ checks.push(dir ? { name: p, status: "ok", detail: fs8.realpathSync(dir) } : { name: p, status: "warn", detail: "not installed", hint: `npm install ${p}` });
1127
+ }
1128
+ return checks;
1129
+ }
1130
+ function capabilitySummary(config, root) {
1131
+ const paths = resolvePaths(root, config);
1132
+ const lines = [
1133
+ `${c.bold(config.name)} ${config.version} (${config.identifier})${config.description ? ` \u2014 ${config.description}` : ""}`,
1134
+ `entry ${path8.relative(root, paths.entry)} \xB7 renderer ${path8.relative(root, paths.rendererDir)}/ \xB7 node pin ${config.node?.version ?? c.dim("none")}`
1135
+ ];
1136
+ const windows = Object.entries(config.windows ?? {});
1137
+ if (windows.length === 0) lines.push(`windows: ${c.dim("none declared (renderers get app-local ipc:*/send:* only)")}`);
1138
+ for (const [name, w] of windows) lines.push(`window ${c.bold(name)}: ${(w.capabilities ?? []).length ? w.capabilities.join(", ") : c.dim("no capabilities")}`);
1139
+ if (config.backend?.external?.length) lines.push(`backend externals: ${config.backend.external.join(", ")}`);
1140
+ return lines;
1141
+ }
1142
+ function runChecks(root) {
1143
+ const checks = [nodeCheck(), hostCheck(root ?? process.cwd())];
1144
+ if (process.platform === "linux") checks.push(...linuxChecks());
1145
+ else if (process.platform === "darwin") checks.push(...macosChecks());
1146
+ else if (process.platform === "win32") checks.push(...windowsChecks());
1147
+ checks.push(...projectChecks(root));
1148
+ return checks;
1149
+ }
1150
+ async function run5(_args) {
1151
+ const root = findAppRoot(process.cwd());
1152
+ console.log(`${c.bold("dormer doctor")} \u2014 ${process.platform} ${process.arch}${root ? `, app at ${root}` : ` (no ${CONFIG_FILE} found from ${process.cwd()})`}`);
1153
+ const checks = runChecks(root);
1154
+ const icon = { ok: c.green("\u2713"), warn: c.yellow("!"), fail: c.red("\u2717") };
1155
+ const width = Math.max(...checks.map((k) => k.name.length)) + 2;
1156
+ for (const k of checks) {
1157
+ console.log(` ${icon[k.status]} ${k.name.padEnd(width)}${k.detail}`);
1158
+ if (k.hint && k.status !== "ok") console.log(` ${c.dim("hint:")} ${k.hint}`);
1159
+ }
1160
+ if (root) {
1161
+ console.log(`
1162
+ ${c.bold("App")} (${path8.join(root, CONFIG_FILE)})`);
1163
+ try {
1164
+ for (const l of capabilitySummary(loadConfig(root), root)) console.log(` ${l}`);
1165
+ } catch (e) {
1166
+ console.log(` ${c.red("\u2717")} ${e.message}`);
1167
+ return EXIT.config;
1168
+ }
1169
+ }
1170
+ if (process.platform === "darwin" || process.platform === "win32") console.log(`
1171
+ ${c.dim("note: platform checks on this OS are unverified; please report false results")}`);
1172
+ const fails = checks.filter((k) => k.status === "fail").length;
1173
+ const warns = checks.filter((k) => k.status === "warn").length;
1174
+ console.log(`
1175
+ ${fails ? c.red(`${fails} problem(s)`) : c.green("no problems")}${warns ? `, ${warns} warning(s)` : ""}`);
1176
+ return fails ? EXIT.failure : EXIT.ok;
1177
+ }
1178
+
1179
+ // src/commands/inspect.ts
1180
+ var inspect_exports = {};
1181
+ __export(inspect_exports, {
1182
+ run: () => run6,
1183
+ spec: () => spec6
1184
+ });
1185
+ import fs9 from "node:fs";
1186
+ import path9 from "node:path";
1187
+ var spec6 = {
1188
+ name: "inspect",
1189
+ summary: "Launch the app, capture each window's element tree (and snapshot), print an outline",
1190
+ usage: "inspect [--out <dir>] [--skip-build] [--json] [--timeout <ms>] [--settle <ms>] [--lines <n>]",
1191
+ description: `Runs the built app with DORMER_INSPECT_OUT set. @dormer/api then captures every window once it
1192
+ has loaded: an accessibility-style element tree (roles, names, states, bounds) as JSON and text, and a
1193
+ PNG snapshot where the platform supports it (macOS today). The app exits by itself; the outline is
1194
+ printed here. Files land in .dormer/inspect/ by default.`,
1195
+ options: {
1196
+ out: { type: "string", description: "Output directory (default .dormer/inspect)" },
1197
+ "skip-build": { type: "boolean", description: "Reuse dist/ instead of rebuilding" },
1198
+ json: { type: "boolean", description: "Print inspect.json instead of the outline" },
1199
+ timeout: { type: "number", description: "Give up after this many milliseconds (default 20000)" },
1200
+ settle: { type: "number", description: "Wait this long after page load before capturing (default 700)" },
1201
+ lines: { type: "number", description: "Outline lines to print per window (default 80)" }
1202
+ },
1203
+ examples: ["dormer inspect", "dormer inspect --skip-build --lines 200", "dormer inspect --json | jq .windows[0].nodes"]
1204
+ };
1205
+ async function run6(args) {
1206
+ const app = loadApp();
1207
+ if (!flagBool(args.flags, "skip-build")) await buildApp(app, { quiet: true });
1208
+ const host = resolveHostBinary(app.root);
1209
+ const out = path9.resolve(app.root, flagString(args.flags, "out") ?? path9.join(".dormer", "inspect"));
1210
+ fs9.rmSync(out, { recursive: true, force: true });
1211
+ fs9.mkdirSync(out, { recursive: true });
1212
+ const timeout = flagNumber(args.flags, "timeout") ?? 2e4;
1213
+ const proc = spawnHost({
1214
+ host: host.path,
1215
+ appDir: app.paths.rendererOut,
1216
+ entry: app.paths.backendOut,
1217
+ node: process.execPath,
1218
+ name: app.config.name,
1219
+ passEnv: ["DORMER_INSPECT_OUT", "DORMER_INSPECT_SETTLE_MS", "DORMER_INSPECT_TIMEOUT_MS"],
1220
+ env: { DORMER_INSPECT_OUT: out, DORMER_INSPECT_SETTLE_MS: String(flagNumber(args.flags, "settle") ?? 700), DORMER_INSPECT_TIMEOUT_MS: String(timeout), DORMER_APP_VERSION: app.config.version, RUST_LOG: process.env.RUST_LOG ?? "warn" }
1221
+ });
1222
+ const code = await new Promise((res) => {
1223
+ const t = setTimeout(() => {
1224
+ proc.kill("SIGKILL");
1225
+ res(124);
1226
+ }, timeout + 1e4);
1227
+ proc.on("exit", (c2) => {
1228
+ clearTimeout(t);
1229
+ res(c2 ?? 1);
1230
+ });
1231
+ });
1232
+ const summaryPath = path9.join(out, "inspect.json");
1233
+ if (!fs9.existsSync(summaryPath)) throw new CliError(`no inspection result (host exited with ${code})`, { exitCode: EXIT.failure, hints: ["does the app create a window on ready?", "run with RUST_LOG=info for the host log"] });
1234
+ const summary = JSON.parse(fs9.readFileSync(summaryPath, "utf8"));
1235
+ if (flagBool(args.flags, "json")) {
1236
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
1237
+ return summary.ok ? EXIT.ok : EXIT.failure;
1238
+ }
1239
+ const lines = flagNumber(args.flags, "lines") ?? 80;
1240
+ for (const w of summary.windows) {
1241
+ if (w.error) {
1242
+ log.error(`window ${w.route}: ${w.error}`);
1243
+ continue;
1244
+ }
1245
+ console.log(`${c.bold(`window ${w.route}`)} ${c.dim(w.url)} ${w.nodes} nodes${w.truncated ? " (truncated)" : ""}`);
1246
+ console.log(` snapshot: ${w.png ?? c.dim(`none (${w.pngError ?? "not supported here"})`)}`);
1247
+ console.log(` tree: ${w.outline.replace(/\.outline\.txt$/, ".tree.json")}`);
1248
+ const outline = fs9.readFileSync(w.outline, "utf8").split("\n").filter(Boolean);
1249
+ console.log(outline.slice(0, lines).map((l) => ` ${l}`).join("\n"));
1250
+ if (outline.length > lines) console.log(c.dim(` \u2026 ${outline.length - lines} more lines in ${w.outline}`));
1251
+ }
1252
+ if (summary.timedOut) log.error("timed out before any window could be inspected");
1253
+ return summary.ok ? EXIT.ok : EXIT.failure;
1254
+ }
1255
+
1256
+ // src/cli.ts
1257
+ var COMMANDS = { create: create_exports, dev: dev_exports, build: build_exports, package: package_exports, doctor: doctor_exports, inspect: inspect_exports };
1258
+ function version() {
1259
+ try {
1260
+ return readJson(path10.join(CLI_DIR, "package.json")).version;
1261
+ } catch {
1262
+ return "0.0.0";
1263
+ }
1264
+ }
1265
+ function globalHelp() {
1266
+ const width = Math.max(...Object.keys(COMMANDS).map((n) => n.length)) + 2;
1267
+ return [
1268
+ `${c.bold("dormer")} ${version()} \u2014 build desktop apps with Node.js and the system WebView`,
1269
+ "",
1270
+ "Usage: dormer <command> [options]",
1271
+ "",
1272
+ "Commands:",
1273
+ ...Object.values(COMMANDS).map((cmd) => ` ${cmd.spec.name.padEnd(width)}${cmd.spec.summary}`),
1274
+ "",
1275
+ "Run `dormer <command> --help` for the options of a command.",
1276
+ "Environment: DORMER_HOST_BIN (host binary), DORMER_BUNDLE_BIN (packaging tool), DORMER_CLI_DEBUG=1 (stack traces)."
1277
+ ].join("\n");
1278
+ }
1279
+ async function main(argv) {
1280
+ const [name, ...rest] = argv;
1281
+ if (!name || name === "--help" || name === "-h" || name === "help") {
1282
+ console.log(globalHelp());
1283
+ return EXIT.ok;
1284
+ }
1285
+ if (name === "--version" || name === "-v") {
1286
+ console.log(version());
1287
+ return EXIT.ok;
1288
+ }
1289
+ const cmd = COMMANDS[name];
1290
+ if (!cmd) throw new CliError(`unknown command "${name}"`, { exitCode: EXIT.usage, hints: [`commands: ${Object.keys(COMMANDS).join(", ")}`, "run `dormer --help`"] });
1291
+ const args = parseArgs(rest, cmd.spec);
1292
+ if (args.flags.help) {
1293
+ console.log(renderHelp(cmd.spec));
1294
+ return EXIT.ok;
1295
+ }
1296
+ return cmd.run(args);
1297
+ }
1298
+ main(process.argv.slice(2)).then((code) => process.exit(code), (err) => process.exit(reportError(err)));
1299
+ export {
1300
+ main
1301
+ };