@dormer/cli 0.0.0-stage → 0.1.3

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 +201 -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 +57 -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/index.js ADDED
@@ -0,0 +1,676 @@
1
+ // src/config.ts
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ // src/ui.ts
6
+ var EXIT = {
7
+ ok: 0,
8
+ failure: 1,
9
+ // build failed, host exited non-zero, doctor found a failing check
10
+ missingTool: 2,
11
+ // dormer-host / dormer-bundle not found
12
+ usage: 64,
13
+ // bad arguments
14
+ config: 65,
15
+ // dormer.config.json missing or invalid
16
+ noInput: 66
17
+ // a required input (template, entry, renderer dir) does not exist
18
+ };
19
+ var CliError = class extends Error {
20
+ exitCode;
21
+ hints;
22
+ constructor(message, options = {}) {
23
+ super(message);
24
+ this.name = "CliError";
25
+ this.exitCode = options.exitCode ?? EXIT.failure;
26
+ this.hints = options.hints ?? [];
27
+ }
28
+ };
29
+ var useColor = !process.env.NO_COLOR && process.env.TERM !== "dumb" && (process.stdout.isTTY || process.env.FORCE_COLOR);
30
+ var paint = (code) => (s) => useColor ? `\x1B[${code}m${s}\x1B[0m` : s;
31
+ var c = { dim: paint(2), bold: paint(1), red: paint(31), green: paint(32), yellow: paint(33), blue: paint(34), cyan: paint(36) };
32
+ var prefix = c.dim("[dormer]");
33
+ var log = {
34
+ info: (msg) => {
35
+ console.log(`${prefix} ${msg}`);
36
+ },
37
+ step: (msg) => {
38
+ console.log(`${prefix} ${c.cyan("\u203A")} ${msg}`);
39
+ },
40
+ ok: (msg) => {
41
+ console.log(`${prefix} ${c.green("\u2713")} ${msg}`);
42
+ },
43
+ warn: (msg) => {
44
+ console.error(`${prefix} ${c.yellow("warning:")} ${msg}`);
45
+ },
46
+ error: (msg) => {
47
+ console.error(`${prefix} ${c.red("error:")} ${msg}`);
48
+ },
49
+ hint: (msg) => {
50
+ console.error(`${prefix} ${c.dim("hint:")} ${msg}`);
51
+ }
52
+ };
53
+ function formatBytes(n) {
54
+ if (n < 1024) return `${n} B`;
55
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`;
56
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
57
+ }
58
+
59
+ // src/config.ts
60
+ var CONFIG_FILE = "dormer.config.json";
61
+ var BUNDLE_TARGETS = ["deb", "rpm", "appimage", "app", "dmg", "nsis", "msi"];
62
+ function findAppRoot(start = process.cwd()) {
63
+ let dir = path.resolve(start);
64
+ for (; ; ) {
65
+ if (fs.existsSync(path.join(dir, CONFIG_FILE))) return dir;
66
+ const parent = path.dirname(dir);
67
+ if (parent === dir) return void 0;
68
+ dir = parent;
69
+ }
70
+ }
71
+ var IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z0-9-]+)+$/;
72
+ var VERSION_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
73
+ function validateConfig(cfg) {
74
+ const problems = [];
75
+ if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) return ["top level must be an object"];
76
+ const o = cfg;
77
+ const str = (k, required) => {
78
+ if (o[k] === void 0) {
79
+ if (required) problems.push(`"${k}" is required`);
80
+ return;
81
+ }
82
+ if (typeof o[k] !== "string" || o[k].length === 0) problems.push(`"${k}" must be a non-empty string`);
83
+ };
84
+ str("name", true);
85
+ str("identifier", true);
86
+ str("version", true);
87
+ str("description", false);
88
+ str("entry", false);
89
+ str("renderer", false);
90
+ if (typeof o.identifier === "string" && !IDENTIFIER_RE.test(o.identifier)) problems.push(`"identifier" must be reverse-DNS like com.example.myapp (got "${o.identifier}")`);
91
+ if (typeof o.version === "string" && !VERSION_RE.test(o.version)) problems.push(`"version" must be semver like 1.2.3 (got "${o.version}")`);
92
+ if (o.windows !== void 0) {
93
+ if (!o.windows || typeof o.windows !== "object" || Array.isArray(o.windows)) problems.push(`"windows" must be an object keyed by window name`);
94
+ else for (const [name, w] of Object.entries(o.windows)) {
95
+ const caps = w?.capabilities;
96
+ if (caps !== void 0 && (!Array.isArray(caps) || caps.some((x) => typeof x !== "string"))) problems.push(`"windows.${name}.capabilities" must be an array of strings`);
97
+ }
98
+ }
99
+ const node = o.node;
100
+ if (node !== void 0) {
101
+ if (!node || typeof node !== "object") problems.push(`"node" must be an object`);
102
+ else {
103
+ 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"`);
104
+ if (node.binary !== void 0 && typeof node.binary !== "string") problems.push(`"node.binary" must be a path string`);
105
+ }
106
+ }
107
+ const bundle = o.bundle;
108
+ if (bundle?.targets !== void 0) {
109
+ if (!Array.isArray(bundle.targets)) problems.push(`"bundle.targets" must be an array`);
110
+ 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(", ")})`);
111
+ }
112
+ return problems;
113
+ }
114
+ function loadConfig(root) {
115
+ const file = path.join(root, CONFIG_FILE);
116
+ if (!fs.existsSync(file)) {
117
+ throw new CliError(`no ${CONFIG_FILE} found in ${root} (or any parent directory)`, {
118
+ exitCode: EXIT.config,
119
+ hints: [`run this command inside a Dormer app, or create one with ${c.bold("dormer create my-app --template vanilla")}`]
120
+ });
121
+ }
122
+ let parsed;
123
+ try {
124
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
125
+ } catch (e) {
126
+ throw new CliError(`${file} is not valid JSON: ${e.message}`, { exitCode: EXIT.config });
127
+ }
128
+ const problems = validateConfig(parsed);
129
+ if (problems.length) throw new CliError(`${file} is invalid:
130
+ - ${problems.join("\n - ")}`, { exitCode: EXIT.config, hints: ["see packages/cli/README.md for the config schema"] });
131
+ return parsed;
132
+ }
133
+ function resolvePaths(root, config) {
134
+ const distDir = path.join(root, "dist");
135
+ const devDir = path.join(root, ".dormer", "dev");
136
+ return {
137
+ root,
138
+ configPath: path.join(root, CONFIG_FILE),
139
+ entry: path.resolve(root, config.entry ?? "src/main.ts"),
140
+ rendererDir: path.resolve(root, config.renderer ?? "renderer"),
141
+ distDir,
142
+ rendererOut: path.join(distDir, "renderer"),
143
+ backendOut: path.join(distDir, "main.js"),
144
+ devDir,
145
+ devBackendOut: path.join(devDir, "main.js")
146
+ };
147
+ }
148
+ function loadApp(cwd = process.cwd()) {
149
+ const root = findAppRoot(cwd) ?? cwd;
150
+ const config = loadConfig(root);
151
+ return { root, config, paths: resolvePaths(root, config) };
152
+ }
153
+
154
+ // src/host.ts
155
+ import fs3 from "node:fs";
156
+ import path3 from "node:path";
157
+ import { fileURLToPath } from "node:url";
158
+ import { spawn } from "node:child_process";
159
+
160
+ // src/resolve.ts
161
+ import fs2 from "node:fs";
162
+ import path2 from "node:path";
163
+ import { pathToFileURL } from "node:url";
164
+ function findPackageDir(fromDir, name) {
165
+ let dir = path2.resolve(fromDir);
166
+ for (; ; ) {
167
+ const candidate = path2.join(dir, "node_modules", name);
168
+ if (fs2.existsSync(path2.join(candidate, "package.json"))) return candidate;
169
+ const parent = path2.dirname(dir);
170
+ if (parent === dir) return void 0;
171
+ dir = parent;
172
+ }
173
+ }
174
+ function readJson(file) {
175
+ return JSON.parse(fs2.readFileSync(file, "utf8"));
176
+ }
177
+ function packageEsmEntry(pkgDir) {
178
+ const pkg = readJson(path2.join(pkgDir, "package.json"));
179
+ const pick = (v) => {
180
+ if (typeof v === "string") return v;
181
+ if (!v || typeof v !== "object") return void 0;
182
+ const o = v;
183
+ for (const k of ["import", "node", "default"]) if (o[k] !== void 0) {
184
+ const r = pick(o[k]);
185
+ if (r) return r;
186
+ }
187
+ return void 0;
188
+ };
189
+ const fromExports = pkg.exports && typeof pkg.exports === "object" && !Array.isArray(pkg.exports) ? pick(pkg.exports["."] ?? pkg.exports) : pick(pkg.exports);
190
+ const rel = fromExports ?? pkg.module ?? pkg.main ?? "index.js";
191
+ return path2.join(pkgDir, rel);
192
+ }
193
+ async function loadVite(root) {
194
+ const dir = findPackageDir(root, "vite");
195
+ 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)`] });
196
+ const version = readJson(path2.join(dir, "package.json")).version;
197
+ const vite = await import(pathToFileURL(packageEsmEntry(dir)).href);
198
+ return { vite, version, dir };
199
+ }
200
+ function findViteConfig(rendererDir) {
201
+ for (const n of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
202
+ const f = path2.join(rendererDir, n);
203
+ if (fs2.existsSync(f)) return f;
204
+ }
205
+ return void 0;
206
+ }
207
+ function findUp(start, marker) {
208
+ let dir = path2.resolve(start);
209
+ for (; ; ) {
210
+ if (fs2.existsSync(path2.join(dir, marker))) return dir;
211
+ const parent = path2.dirname(dir);
212
+ if (parent === dir) return void 0;
213
+ dir = parent;
214
+ }
215
+ }
216
+ function which(name) {
217
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
218
+ for (const dir of (process.env.PATH ?? "").split(path2.delimiter)) {
219
+ for (const ext of exts) {
220
+ const f = path2.join(dir, name + ext.toLowerCase());
221
+ try {
222
+ fs2.accessSync(f, fs2.constants.X_OK);
223
+ if (fs2.statSync(f).isFile()) return f;
224
+ } catch {
225
+ }
226
+ }
227
+ }
228
+ return void 0;
229
+ }
230
+
231
+ // src/host.ts
232
+ var CLI_DIR = path3.resolve(path3.dirname(fileURLToPath(import.meta.url)), "..");
233
+ var EXE = process.platform === "win32" ? ".exe" : "";
234
+ function hostPackageName() {
235
+ return `@dormer/host-${process.platform}-${process.arch}`;
236
+ }
237
+ function resolveHostBinary(appRoot) {
238
+ const tried = [];
239
+ const env = process.env.DORMER_HOST_BIN;
240
+ if (env) {
241
+ if (isExecutable(env)) return { path: path3.resolve(env), source: "env", tried };
242
+ 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"] });
243
+ }
244
+ const workspaces = /* @__PURE__ */ new Set();
245
+ for (const start of [appRoot, CLI_DIR]) {
246
+ const w = findUp(start, "Cargo.toml");
247
+ if (w) workspaces.add(w);
248
+ }
249
+ for (const w of workspaces) {
250
+ const targetDir = process.env.CARGO_TARGET_DIR ?? path3.join(w, "target");
251
+ for (const profile of ["release", "debug"]) {
252
+ const candidate = path3.join(targetDir, profile, `dormer-host${EXE}`);
253
+ tried.push(candidate);
254
+ if (isExecutable(candidate)) return { path: candidate, source: "cargo-target", tried };
255
+ }
256
+ }
257
+ const pkg = findPackageDir(appRoot, hostPackageName()) ?? findPackageDir(CLI_DIR, hostPackageName());
258
+ const fromPkg = pkg ? path3.join(pkg, "bin", `dormer-host${EXE}`) : path3.join(appRoot, "node_modules", hostPackageName(), "bin", `dormer-host${EXE}`);
259
+ tried.push(fromPkg);
260
+ if (isExecutable(fromPkg)) return { path: fromPkg, source: "package", tried };
261
+ throw new CliError("the native host binary (dormer-host) was not found", {
262
+ exitCode: EXIT.missingTool,
263
+ hints: [
264
+ `looked at:
265
+ ${tried.map((t) => ` ${t}`).join("\n")}`,
266
+ `inside the Dormer repository: ${c.bold("cargo build --release -p dormer-host")}`,
267
+ `or set ${c.bold("DORMER_HOST_BIN=/path/to/dormer-host")}`,
268
+ `(prebuilt binaries will ship as ${hostPackageName()} once published)`
269
+ ]
270
+ });
271
+ }
272
+ function isExecutable(file) {
273
+ try {
274
+ fs3.accessSync(file, fs3.constants.X_OK);
275
+ return fs3.statSync(file).isFile();
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+ function hostArgs(l) {
281
+ const args = ["--app-dir", l.appDir, "--entry", l.entry, "--node", l.node, "--name", l.name];
282
+ if (l.dev) args.push("--dev");
283
+ if (l.devUrl) args.push("--dev-url", l.devUrl);
284
+ for (const v of new Set(l.passEnv ?? [])) args.push("--pass-env", v);
285
+ return args;
286
+ }
287
+ function spawnHost(l) {
288
+ const env = { ...process.env, ...l.env };
289
+ env.RUST_LOG ??= "info";
290
+ return spawn(l.host, hostArgs(l), { stdio: "inherit", env });
291
+ }
292
+
293
+ // src/backend.ts
294
+ import fs4 from "node:fs";
295
+ import path4 from "node:path";
296
+ import { isBuiltin } from "node:module";
297
+ import * as esbuild from "esbuild";
298
+ 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)$/;
299
+ function packageName(specifier) {
300
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("node:")) return void 0;
301
+ const parts = specifier.split("/");
302
+ return specifier.startsWith("@") ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : void 0 : parts[0];
303
+ }
304
+ function findPackageJson(fromDir, name) {
305
+ let dir = fromDir;
306
+ for (; ; ) {
307
+ const f = path4.join(dir, "node_modules", name, "package.json");
308
+ if (fs4.existsSync(f)) {
309
+ try {
310
+ return JSON.parse(fs4.readFileSync(f, "utf8"));
311
+ } catch {
312
+ return void 0;
313
+ }
314
+ }
315
+ const parent = path4.dirname(dir);
316
+ if (parent === dir) return void 0;
317
+ dir = parent;
318
+ }
319
+ }
320
+ function looksNative(pkg) {
321
+ if (pkg.gypfile || pkg.binary || pkg.napi) return true;
322
+ for (const k of ["dependencies", "optionalDependencies", "peerDependencies"]) {
323
+ const deps = pkg[k];
324
+ if (deps && typeof deps === "object" && Object.keys(deps).some((d) => NATIVE_DEP_RE.test(d))) return true;
325
+ }
326
+ return false;
327
+ }
328
+ function externalsPlugin(externals, explicit) {
329
+ return {
330
+ name: "dormer-externals",
331
+ setup(build2) {
332
+ build2.onResolve({ filter: /\.node$/ }, (args) => ({ path: args.path, external: true }));
333
+ build2.onResolve({ filter: /^[^./]/ }, (args) => {
334
+ if (args.kind === "entry-point") return void 0;
335
+ const name = packageName(args.path);
336
+ if (!name) return void 0;
337
+ if (isBuiltin(name)) return { path: args.path, external: true };
338
+ if (explicit.includes(name)) {
339
+ externals.add(name);
340
+ return { path: args.path, external: true };
341
+ }
342
+ if (name.startsWith("@dormer/")) return void 0;
343
+ const pkg = findPackageJson(args.resolveDir, name);
344
+ if (pkg && looksNative(pkg)) {
345
+ externals.add(name);
346
+ return { path: args.path, external: true };
347
+ }
348
+ return void 0;
349
+ });
350
+ }
351
+ };
352
+ }
353
+ var BANNER = `import { createRequire as __dormerCreateRequire } from "node:module";
354
+ const require = __dormerCreateRequire(import.meta.url);
355
+ `;
356
+ function buildOptions(o, externals) {
357
+ return {
358
+ entryPoints: [o.entry],
359
+ outfile: o.outfile,
360
+ bundle: true,
361
+ platform: "node",
362
+ format: "esm",
363
+ target: "node22",
364
+ conditions: ["node", "import", "module", "default"],
365
+ mainFields: ["module", "main"],
366
+ sourcemap: o.sourcemap === false ? false : true,
367
+ minify: o.minify === true,
368
+ legalComments: "none",
369
+ logLevel: "silent",
370
+ absWorkingDir: o.root,
371
+ banner: { js: BANNER },
372
+ define: { "process.env.DORMER_BUNDLED": '"1"', ...o.define },
373
+ plugins: [externalsPlugin(externals, o.external ?? [])],
374
+ metafile: true
375
+ };
376
+ }
377
+ function formatMessages(messages, kind) {
378
+ return esbuild.formatMessagesSync(messages, { kind, color: Boolean(process.stdout.isTTY) }).join("");
379
+ }
380
+ async function bundleBackend(o) {
381
+ if (!fs4.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)`] });
382
+ const externals = /* @__PURE__ */ new Set();
383
+ fs4.mkdirSync(path4.dirname(o.outfile), { recursive: true });
384
+ let result;
385
+ try {
386
+ result = await esbuild.build(buildOptions(o, externals));
387
+ } catch (e) {
388
+ const err = e;
389
+ if (err.errors?.length) throw new CliError(`backend bundle failed:
390
+ ${formatMessages(err.errors, "error")}`, { hints: [`entry: ${o.entry}`] });
391
+ throw e;
392
+ }
393
+ const bytes = fs4.statSync(o.outfile).size;
394
+ return { outfile: o.outfile, bytes, externals: [...externals].sort(), warnings: result.warnings };
395
+ }
396
+ async function watchBackend(o, onBuild) {
397
+ if (!fs4.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)`] });
398
+ const externals = /* @__PURE__ */ new Set();
399
+ fs4.mkdirSync(path4.dirname(o.outfile), { recursive: true });
400
+ const options = buildOptions(o, externals);
401
+ options.plugins.push({
402
+ name: "dormer-watch-notify",
403
+ setup(build2) {
404
+ build2.onEnd((r) => onBuild({ ok: r.errors.length === 0, errors: r.errors, warnings: r.warnings, externals: [...externals].sort() }));
405
+ }
406
+ });
407
+ const ctx = await esbuild.context(options);
408
+ await ctx.watch();
409
+ return { dispose: () => ctx.dispose() };
410
+ }
411
+ function summarizeExternals(externals) {
412
+ return externals.length ? `${externals.length} native/external package(s) kept out of the bundle: ${c.bold(externals.join(", "))}` : "no external packages (everything bundled)";
413
+ }
414
+
415
+ // src/commands/build.ts
416
+ import fs5 from "node:fs";
417
+ import path5 from "node:path";
418
+ var spec = {
419
+ name: "build",
420
+ summary: "Build the renderer and the backend into dist/",
421
+ usage: "build [--minify] [--no-sourcemap] [--quiet]",
422
+ description: `Runs \`vite build\` for the renderer (\u2192 dist/renderer), bundles the backend with esbuild
423
+ (\u2192 dist/main.js + .map), copies ${CONFIG_FILE} and writes dist/dormer-app.json. dist/ is owned by
424
+ this command and is recreated on every run.`,
425
+ options: {
426
+ minify: { type: "boolean", description: "Minify the backend bundle" },
427
+ sourcemap: { type: "boolean", description: "Emit dist/main.js.map (use --no-sourcemap to disable)", default: true },
428
+ quiet: { type: "boolean", description: "Only print warnings and errors from Vite" }
429
+ }
430
+ };
431
+ function cliVersion() {
432
+ try {
433
+ return readJson(path5.join(CLI_DIR, "package.json")).version;
434
+ } catch {
435
+ return "0.0.0";
436
+ }
437
+ }
438
+ function makeManifest(config, externals) {
439
+ return {
440
+ manifestVersion: 1,
441
+ name: config.name,
442
+ identifier: config.identifier,
443
+ version: config.version,
444
+ description: config.description,
445
+ entry: "main.js",
446
+ assets: "renderer",
447
+ node: { version: config.node?.version },
448
+ externals,
449
+ windows: config.windows,
450
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
451
+ cli: cliVersion()
452
+ };
453
+ }
454
+ async function buildApp(app, opts = {}) {
455
+ const { config, paths } = app;
456
+ if (!fs5.existsSync(paths.rendererDir)) throw new CliError(`renderer directory not found: ${paths.rendererDir}`, { exitCode: EXIT.noInput, hints: [`set "renderer" in ${CONFIG_FILE} (default "renderer")`] });
457
+ if (!fs5.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")`] });
458
+ fs5.rmSync(paths.distDir, { recursive: true, force: true });
459
+ fs5.mkdirSync(paths.distDir, { recursive: true });
460
+ const { vite, version } = await loadVite(app.root);
461
+ log.step(`building renderer with vite ${version} \u2192 ${path5.relative(app.root, paths.rendererOut)}`);
462
+ const configFile = findViteConfig(paths.rendererDir);
463
+ if (!configFile) log.warn(`no vite.config.* in ${paths.rendererDir}; add one using @dormer/vite-plugin`);
464
+ await vite.build({
465
+ root: paths.rendererDir,
466
+ configFile: configFile ?? false,
467
+ logLevel: opts.quiet ? "warn" : "info",
468
+ build: { outDir: paths.rendererOut, emptyOutDir: true }
469
+ });
470
+ if (!fs5.existsSync(path5.join(paths.rendererOut, "index.html"))) {
471
+ throw new CliError(`vite build produced no ${path5.join(paths.rendererOut, "index.html")}`, { hints: ["the renderer must have an index.html at its root; check build.outDir is not overridden"] });
472
+ }
473
+ log.step(`bundling backend ${path5.relative(app.root, paths.entry)} \u2192 ${path5.relative(app.root, paths.backendOut)}`);
474
+ const backend = await bundleBackend({ root: app.root, entry: paths.entry, outfile: paths.backendOut, minify: opts.minify, sourcemap: opts.sourcemap !== false, external: config.backend?.external });
475
+ if (backend.warnings.length) console.error(formatMessages(backend.warnings, "warning"));
476
+ log.info(`backend ${formatBytes(backend.bytes)}; ${summarizeExternals(backend.externals)}`);
477
+ fs5.copyFileSync(paths.configPath, path5.join(paths.distDir, CONFIG_FILE));
478
+ const manifest = makeManifest(config, backend.externals);
479
+ const manifestPath = path5.join(paths.distDir, "dormer-app.json");
480
+ fs5.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
481
+ log.ok(`built ${c.bold(`${config.name} ${config.version}`)} \u2192 ${paths.distDir}`);
482
+ return { manifest, backend, manifestPath };
483
+ }
484
+
485
+ // src/commands/package.ts
486
+ import fs6 from "node:fs";
487
+ import path6 from "node:path";
488
+ function findIcuData(nodeBinary) {
489
+ const dir = path6.dirname(nodeBinary);
490
+ try {
491
+ const f = fs6.readdirSync(dir).find((n) => /^icudt\d+l\.dat$/.test(n));
492
+ return f ? path6.join(dir, f) : void 0;
493
+ } catch {
494
+ return void 0;
495
+ }
496
+ }
497
+ function defaultTargets(platform = process.platform) {
498
+ if (platform === "darwin") return ["app", "dmg"];
499
+ if (platform === "win32") return ["nsis"];
500
+ return ["deb", "appimage"];
501
+ }
502
+ var DEFAULT_LINUX_DEPENDS = ["libwebkit2gtk-4.1-0", "libgtk-3-0"];
503
+ function makeBundleSpec(config, o) {
504
+ const b = config.bundle ?? {};
505
+ let icons = (b.icons ?? []).map((p) => path6.resolve(o.root, p));
506
+ if (icons.length === 0 && fs6.existsSync(path6.join(o.root, "icons"))) {
507
+ icons = fs6.readdirSync(path6.join(o.root, "icons")).filter((f) => /\.(png|icns|ico|svg)$/i.test(f)).map((f) => path6.join(o.root, "icons", f));
508
+ }
509
+ return {
510
+ name: config.name,
511
+ identifier: config.identifier,
512
+ version: config.version,
513
+ description: config.description ?? "",
514
+ appDir: o.appDir,
515
+ entry: "main.js",
516
+ hostBinary: o.hostBinary,
517
+ nodeBinary: o.nodeBinary,
518
+ targets: o.targets,
519
+ icons,
520
+ linux: { depends: b.linux?.depends ?? DEFAULT_LINUX_DEPENDS },
521
+ macos: { entitlements: b.macos?.entitlements ? path6.resolve(o.root, b.macos.entitlements) : null, signingIdentity: b.macos?.signingIdentity ?? null, minimumSystemVersion: b.macos?.minimumSystemVersion ?? "13.0" },
522
+ windows: { signCommand: b.windows?.signCommand ?? null },
523
+ sourceMaps: b.sourceMaps ?? false,
524
+ nodeIcuData: config.node?.icu === "full" ? findIcuData(o.nodeBinary) ?? null : null
525
+ };
526
+ }
527
+ function resolveBundlerBinary(appRoot) {
528
+ const exe = process.platform === "win32" ? "dormer-bundle.exe" : "dormer-bundle";
529
+ const tried = [];
530
+ if (process.env.DORMER_BUNDLE_BIN) {
531
+ tried.push(process.env.DORMER_BUNDLE_BIN);
532
+ if (isExecutable(process.env.DORMER_BUNDLE_BIN)) return { path: process.env.DORMER_BUNDLE_BIN, tried };
533
+ }
534
+ const workspaces = /* @__PURE__ */ new Set();
535
+ for (const start of [appRoot, CLI_DIR]) {
536
+ const ws = findUp(start, "Cargo.toml");
537
+ if (ws) workspaces.add(ws);
538
+ }
539
+ for (const ws of workspaces) {
540
+ for (const profile of ["release", "debug"]) {
541
+ const candidate = path6.join(process.env.CARGO_TARGET_DIR ?? path6.join(ws, "target"), profile, exe);
542
+ tried.push(candidate);
543
+ if (isExecutable(candidate)) return { path: candidate, tried };
544
+ }
545
+ }
546
+ const pkg = findPackageDir(appRoot, bundlePackageName()) ?? findPackageDir(CLI_DIR, bundlePackageName());
547
+ const fromPkg = pkg ? path6.join(pkg, "bin", exe) : path6.join(appRoot, "node_modules", bundlePackageName(), "bin", exe);
548
+ tried.push(fromPkg);
549
+ if (isExecutable(fromPkg)) return { path: fromPkg, tried };
550
+ const onPath = which("dormer-bundle");
551
+ tried.push("$PATH");
552
+ return { path: onPath, tried };
553
+ }
554
+ function bundlePackageName() {
555
+ return `@dormer/bundle-${process.platform}-${process.arch}`;
556
+ }
557
+
558
+ // src/commands/doctor.ts
559
+ import fs7 from "node:fs";
560
+ import path7 from "node:path";
561
+ import { spawnSync } from "node:child_process";
562
+ function sh(cmd, args) {
563
+ const r = spawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
564
+ return { ok: !r.error && r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
565
+ }
566
+ function nodeCheck(version = process.versions.node) {
567
+ const [major, minor] = version.split(".").map(Number);
568
+ if (major < 22) return { name: "Node.js", status: "fail", detail: `v${version} (22 or newer required)`, hint: "install Node 24 LTS from https://nodejs.org or with your version manager" };
569
+ if (major === 22 && minor < 18) return { name: "Node.js", status: "warn", detail: `v${version} (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" };
570
+ return { name: "Node.js", status: "ok", detail: `v${version}` };
571
+ }
572
+ function hostCheck(root) {
573
+ try {
574
+ const h = resolveHostBinary(root);
575
+ return { name: "dormer-host", status: "ok", detail: `${h.path} (${h.source === "env" ? "DORMER_HOST_BIN" : h.source === "cargo-target" ? "cargo target dir" : hostPackageName()})` };
576
+ } catch (e) {
577
+ const err = e;
578
+ return { name: "dormer-host", status: "fail", detail: err.message, hint: err.hints?.slice(1).join("; ") };
579
+ }
580
+ }
581
+ function linuxChecks() {
582
+ const checks = [];
583
+ 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})` });
584
+ else {
585
+ const ld = sh("ldconfig", ["-p"]);
586
+ 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" });
587
+ 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" });
588
+ }
589
+ const bus = process.env.DBUS_SESSION_BUS_ADDRESS || (process.env.XDG_RUNTIME_DIR && fs7.existsSync(path7.join(process.env.XDG_RUNTIME_DIR, "bus")) ? `unix:path=${path7.join(process.env.XDG_RUNTIME_DIR, "bus")}` : "");
590
+ 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>" });
591
+ const display = process.env.WAYLAND_DISPLAY ? `wayland (${process.env.WAYLAND_DISPLAY})` : process.env.DISPLAY ? `x11 (${process.env.DISPLAY})` : "";
592
+ 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' });
593
+ const nvidia = fs7.existsSync("/proc/driver/nvidia/version") || Boolean(which("nvidia-smi"));
594
+ 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" });
595
+ const headless = ["xvfb-run", "dbus-run-session"].filter((t) => !which(t));
596
+ 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" });
597
+ return checks;
598
+ }
599
+ function macosChecks() {
600
+ const clt = sh("xcode-select", ["-p"]);
601
+ const sw = sh("sw_vers", ["-productVersion"]);
602
+ const major = Number(sw.out.split(".")[0]);
603
+ return [
604
+ 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" },
605
+ 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" }
606
+ ];
607
+ }
608
+ function windowsChecks() {
609
+ const keys = [
610
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
611
+ "HKLM\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
612
+ "HKCU\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
613
+ ];
614
+ for (const k of keys) {
615
+ const r = sh("reg", ["query", k, "/v", "pv"]);
616
+ const m = r.ok ? /pv\s+REG_SZ\s+(\S+)/.exec(r.out) : null;
617
+ if (m && m[1] !== "0.0.0.0") return [{ name: "WebView2 runtime", status: "ok", detail: `version ${m[1]}` }];
618
+ }
619
+ 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)" }];
620
+ }
621
+ function projectChecks(root) {
622
+ const checks = [];
623
+ const esb = findPackageDir(CLI_DIR, "esbuild");
624
+ checks.push(esb ? { name: "esbuild", status: "ok", detail: `v${readJson(path7.join(esb, "package.json")).version}` } : { name: "esbuild", status: "fail", detail: "not resolvable from @dormer/cli", hint: "reinstall @dormer/cli" });
625
+ if (!root) return checks;
626
+ const vite = findPackageDir(root, "vite");
627
+ checks.push(vite ? { name: "vite", status: "ok", detail: `v${readJson(path7.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)" });
628
+ for (const p of ["@dormer/api", "@dormer/renderer", "@dormer/vite-plugin"]) {
629
+ const dir = findPackageDir(root, p);
630
+ checks.push(dir ? { name: p, status: "ok", detail: fs7.realpathSync(dir) } : { name: p, status: "warn", detail: "not installed", hint: `npm install ${p}` });
631
+ }
632
+ return checks;
633
+ }
634
+ function capabilitySummary(config, root) {
635
+ const paths = resolvePaths(root, config);
636
+ const lines = [
637
+ `${c.bold(config.name)} ${config.version} (${config.identifier})${config.description ? ` \u2014 ${config.description}` : ""}`,
638
+ `entry ${path7.relative(root, paths.entry)} \xB7 renderer ${path7.relative(root, paths.rendererDir)}/ \xB7 node pin ${config.node?.version ?? c.dim("none")}`
639
+ ];
640
+ const windows = Object.entries(config.windows ?? {});
641
+ if (windows.length === 0) lines.push(`windows: ${c.dim("none declared (renderers get app-local ipc:*/send:* only)")}`);
642
+ for (const [name, w] of windows) lines.push(`window ${c.bold(name)}: ${(w.capabilities ?? []).length ? w.capabilities.join(", ") : c.dim("no capabilities")}`);
643
+ if (config.backend?.external?.length) lines.push(`backend externals: ${config.backend.external.join(", ")}`);
644
+ return lines;
645
+ }
646
+ function runChecks(root) {
647
+ const checks = [nodeCheck(), hostCheck(root ?? process.cwd())];
648
+ if (process.platform === "linux") checks.push(...linuxChecks());
649
+ else if (process.platform === "darwin") checks.push(...macosChecks());
650
+ else if (process.platform === "win32") checks.push(...windowsChecks());
651
+ checks.push(...projectChecks(root));
652
+ return checks;
653
+ }
654
+ export {
655
+ BUNDLE_TARGETS,
656
+ CliError,
657
+ EXIT,
658
+ buildApp,
659
+ bundleBackend,
660
+ capabilitySummary,
661
+ defaultTargets,
662
+ findAppRoot,
663
+ hostArgs,
664
+ hostPackageName,
665
+ loadApp,
666
+ loadConfig,
667
+ makeBundleSpec,
668
+ makeManifest,
669
+ resolveBundlerBinary,
670
+ resolveHostBinary,
671
+ resolvePaths,
672
+ runChecks,
673
+ spawnHost,
674
+ validateConfig,
675
+ watchBackend
676
+ };