create-zenbu-app 0.0.29 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,780 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
2
+ import { r as createLogger, t as buildDesktopApp } from "./desktop-CtHHGkP3.mjs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import fs from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { spawnSync } from "node:child_process";
8
8
  import * as p from "@clack/prompts";
9
- import fsp from "node:fs/promises";
10
- import https from "node:https";
11
- import crypto from "node:crypto";
12
- //#region src/desktop/electron-cache.ts
13
- /**
14
- * Layout under `~/.zenbu/electron/`:
15
- *
16
- * versions/<major>/<exact-version>/Electron.app
17
- * versions/<major>/.current -> "<exact-version>" (text marker)
18
- * .locks/<major>.lock -> flock target during download
19
- * .downloads/<exact>.zip -> intermediate, deleted on success
20
- */
21
- const ROOT = path.join(os.homedir(), ".zenbu", "electron");
22
- async function ensureElectronApp(opts) {
23
- const { log } = opts;
24
- const exact = await resolveExactVersion(opts.versionRange, log);
25
- const major = parseInt(exact.split(".")[0], 10);
26
- if (!Number.isFinite(major)) throw new Error(`failed to parse major from electron version "${exact}"`);
27
- const versionDir = path.join(ROOT, "versions", String(major), exact);
28
- const electronAppPath = path.join(versionDir, "Electron.app");
29
- if (fs.existsSync(path.join(electronAppPath, "Contents", "MacOS"))) {
30
- log.info(`cache hit: ${electronAppPath}`);
31
- return {
32
- electronAppPath,
33
- version: exact,
34
- major
35
- };
36
- }
37
- await fsp.mkdir(path.join(ROOT, ".locks"), { recursive: true });
38
- await fsp.mkdir(path.join(ROOT, ".downloads"), { recursive: true });
39
- await withLock(path.join(ROOT, ".locks", `${major}-${exact}.lock`), async () => {
40
- if (fs.existsSync(path.join(electronAppPath, "Contents", "MacOS"))) {
41
- log.info(`cache hit (post-lock): ${electronAppPath}`);
42
- return;
43
- }
44
- await downloadAndExtract({
45
- version: exact,
46
- versionDir,
47
- log,
48
- downloadBase: opts.downloadBase
49
- });
50
- });
51
- if (!fs.existsSync(path.join(electronAppPath, "Contents", "MacOS"))) throw new Error(`electron cache install failed: ${electronAppPath} missing after extract`);
52
- return {
53
- electronAppPath,
54
- version: exact,
55
- major
56
- };
57
- }
58
- async function downloadAndExtract(opts) {
59
- const arch = process.arch === "arm64" ? "arm64" : "x64";
60
- const filename = `electron-v${opts.version}-darwin-${arch}.zip`;
61
- const url = (opts.downloadBase ?? "https://github.com/electron/electron/releases/download") + `/v${opts.version}/${filename}`;
62
- const zipPath = path.join(ROOT, ".downloads", filename);
63
- opts.log.info(`downloading ${url}`);
64
- await downloadFile(url, zipPath, opts.log);
65
- await fsp.mkdir(opts.versionDir, { recursive: true });
66
- opts.log.info(`extracting -> ${opts.versionDir}`);
67
- const r = spawnSync("ditto", [
68
- "-x",
69
- "-k",
70
- zipPath,
71
- opts.versionDir
72
- ], {
73
- stdio: "pipe",
74
- encoding: "utf8"
75
- });
76
- if (r.status !== 0) throw new Error(`ditto failed (${r.status}): ${r.stderr?.slice(0, 1e3) ?? ""}`);
77
- await fsp.unlink(zipPath).catch(() => {});
78
- }
79
- async function downloadFile(url, dest, log, redirectsLeft = 5) {
80
- const tmp = `${dest}.part`;
81
- if (await new Promise((resolve, reject) => {
82
- https.get(url, (res) => {
83
- const code = res.statusCode ?? 0;
84
- if (code >= 300 && code < 400 && res.headers.location) {
85
- res.resume();
86
- if (redirectsLeft <= 0) {
87
- reject(/* @__PURE__ */ new Error(`download ${url}: too many redirects`));
88
- return;
89
- }
90
- downloadFile(res.headers.location, dest, log, redirectsLeft - 1).then(() => resolve(false), reject);
91
- return;
92
- }
93
- if (code !== 200) {
94
- res.resume();
95
- reject(/* @__PURE__ */ new Error(`download ${url} -> HTTP ${code}`));
96
- return;
97
- }
98
- const total = parseInt(res.headers["content-length"] ?? "0", 10);
99
- let received = 0;
100
- let lastPct = -1;
101
- const file = fs.createWriteStream(tmp);
102
- res.on("data", (chunk) => {
103
- received += chunk.length;
104
- if (total > 0) {
105
- const pct = Math.floor(received / total * 100);
106
- if (pct !== lastPct && pct % 10 === 0) {
107
- log.info(` ${pct}% (${received}/${total})`);
108
- lastPct = pct;
109
- }
110
- }
111
- });
112
- res.pipe(file);
113
- file.on("finish", () => {
114
- file.close((err) => err ? reject(err) : resolve(true));
115
- });
116
- file.on("error", reject);
117
- }).on("error", reject);
118
- })) await fsp.rename(tmp, dest);
119
- }
120
- async function withLock(lockFile, fn) {
121
- const deadline = Date.now() + 5 * 6e4;
122
- while (Date.now() < deadline) try {
123
- const fd = fs.openSync(lockFile, "wx");
124
- try {
125
- return await fn();
126
- } finally {
127
- fs.closeSync(fd);
128
- try {
129
- fs.unlinkSync(lockFile);
130
- } catch {}
131
- }
132
- } catch (err) {
133
- if (err.code !== "EEXIST") throw err;
134
- try {
135
- const stat = fs.statSync(lockFile);
136
- if (Date.now() - stat.mtimeMs > 10 * 6e4) {
137
- fs.unlinkSync(lockFile);
138
- continue;
139
- }
140
- } catch {}
141
- await sleep(500);
142
- }
143
- throw new Error(`timed out waiting for ${lockFile}`);
144
- }
145
- function sleep(ms) {
146
- return new Promise((r) => setTimeout(r, ms));
147
- }
148
- let registryCache = null;
149
- async function fetchRegistry() {
150
- if (registryCache) return registryCache;
151
- registryCache = new Promise((resolve, reject) => {
152
- https.get("https://registry.npmjs.org/electron", { headers: { accept: "application/vnd.npm.install-v1+json" } }, (res) => {
153
- if (res.statusCode !== 200) {
154
- reject(/* @__PURE__ */ new Error(`registry HTTP ${res.statusCode}`));
155
- return;
156
- }
157
- let body = "";
158
- res.setEncoding("utf8");
159
- res.on("data", (c) => body += c);
160
- res.on("end", () => {
161
- try {
162
- resolve(JSON.parse(body));
163
- } catch (err) {
164
- reject(err);
165
- }
166
- });
167
- res.on("error", reject);
168
- }).on("error", reject);
169
- });
170
- return registryCache;
171
- }
172
- /**
173
- * Accepts an exact version, a caret range, or a bare major. Returns the
174
- * latest published electron version that satisfies it. Pure HTTP — no npm
175
- * CLI invocation.
176
- */
177
- async function resolveExactVersion(range, log) {
178
- const trimmed = range.trim();
179
- if (/^\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(trimmed)) return trimmed;
180
- log.info(`resolving electron ${trimmed} from registry`);
181
- const doc = await fetchRegistry();
182
- const all = Object.keys(doc.versions ?? {});
183
- if (/^\d+$/.test(trimmed)) return pickLatestStableMajor(all, parseInt(trimmed, 10), trimmed);
184
- const caret = trimmed.match(/^\^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
185
- if (caret) return pickLatestStableMajor(all, parseInt(caret[1], 10), trimmed);
186
- const tilde = trimmed.match(/^~(\d+)\.(\d+)(?:\.(\d+))?$/);
187
- if (tilde) {
188
- const major = parseInt(tilde[1], 10);
189
- const minor = parseInt(tilde[2], 10);
190
- return pickHighest(all.filter((v) => {
191
- const m = v.match(/^(\d+)\.(\d+)\.(\d+)(?:-|$)/);
192
- if (!m) return false;
193
- return parseInt(m[1], 10) === major && parseInt(m[2], 10) === minor;
194
- }), trimmed);
195
- }
196
- if (trimmed === "latest") {
197
- const latest = doc["dist-tags"]?.latest;
198
- if (!latest) throw new Error("registry has no `latest` dist-tag");
199
- return latest;
200
- }
201
- throw new Error(`unsupported electron version spec: "${trimmed}". Use an exact version, "^MAJOR", "~MAJOR.MINOR", a bare major, or "latest".`);
202
- }
203
- function pickLatestStableMajor(all, major, raw) {
204
- return pickHighest(all.filter((v) => {
205
- const m = v.match(/^(\d+)\.\d+\.\d+$/);
206
- return m !== null && parseInt(m[1], 10) === major;
207
- }), raw);
208
- }
209
- function pickHighest(versions, raw) {
210
- if (versions.length === 0) throw new Error(`no electron version matched "${raw}"`);
211
- const sorted = versions.sort(compareSemver);
212
- return sorted[sorted.length - 1];
213
- }
214
- function compareSemver(a, b) {
215
- const pa = a.split(".").map((n) => parseInt(n, 10));
216
- const pb = b.split(".").map((n) => parseInt(n, 10));
217
- for (let i = 0; i < 3; i++) {
218
- const da = pa[i] ?? 0;
219
- const db = pb[i] ?? 0;
220
- if (da !== db) return da - db;
221
- }
222
- return 0;
223
- }
224
- //#endregion
225
- //#region src/desktop/icon.ts
226
- const ICONSET_SIZES = [
227
- {
228
- name: "icon_16x16.png",
229
- px: 16
230
- },
231
- {
232
- name: "icon_16x16@2x.png",
233
- px: 32
234
- },
235
- {
236
- name: "icon_32x32.png",
237
- px: 32
238
- },
239
- {
240
- name: "icon_32x32@2x.png",
241
- px: 64
242
- },
243
- {
244
- name: "icon_128x128.png",
245
- px: 128
246
- },
247
- {
248
- name: "icon_128x128@2x.png",
249
- px: 256
250
- },
251
- {
252
- name: "icon_256x256.png",
253
- px: 256
254
- },
255
- {
256
- name: "icon_256x256@2x.png",
257
- px: 512
258
- },
259
- {
260
- name: "icon_512x512.png",
261
- px: 512
262
- },
263
- {
264
- name: "icon_512x512@2x.png",
265
- px: 1024
266
- }
267
- ];
268
- async function prepareIcon(opts) {
269
- const { source, dest, log } = opts;
270
- if (!fs.existsSync(source)) throw new Error(`icon source does not exist: ${source}`);
271
- await fsp.mkdir(path.dirname(dest), { recursive: true });
272
- const ext = path.extname(source).toLowerCase();
273
- if (ext === ".icns") {
274
- log.info(`copying icns ${source} -> ${dest}`);
275
- await fsp.copyFile(source, dest);
276
- return;
277
- }
278
- if (ext !== ".png") throw new Error(`unsupported icon format "${ext}". Use .png (square, ideally 1024x1024) or .icns.`);
279
- log.info(`converting png -> icns (${source})`);
280
- const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "czda-iconset-"));
281
- const iconset = path.join(tmp, "icon.iconset");
282
- await fsp.mkdir(iconset, { recursive: true });
283
- for (const { name, px } of ICONSET_SIZES) {
284
- const out = path.join(iconset, name);
285
- const r = spawnSync("sips", [
286
- "-z",
287
- String(px),
288
- String(px),
289
- source,
290
- "--out",
291
- out
292
- ], {
293
- stdio: "pipe",
294
- encoding: "utf8"
295
- });
296
- if (r.status !== 0) throw new Error(`sips failed for ${name} (${r.status}): ${r.stderr?.slice(0, 500) ?? ""}`);
297
- }
298
- const r = spawnSync("iconutil", [
299
- "-c",
300
- "icns",
301
- iconset,
302
- "-o",
303
- dest
304
- ], {
305
- stdio: "pipe",
306
- encoding: "utf8"
307
- });
308
- if (r.status !== 0) throw new Error(`iconutil failed (${r.status}): ${r.stderr?.slice(0, 500) ?? ""}`);
309
- await fsp.rm(tmp, {
310
- recursive: true,
311
- force: true
312
- });
313
- }
314
- //#endregion
315
- //#region src/desktop/bundle.ts
316
- const ENTITLEMENTS = `<?xml version="1.0" encoding="UTF-8"?>
317
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
318
- <plist version="1.0">
319
- <dict>
320
- <key>com.apple.security.cs.allow-jit</key>
321
- <true/>
322
- <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
323
- <true/>
324
- <key>com.apple.security.cs.disable-library-validation</key>
325
- <true/>
326
- </dict>
327
- </plist>
328
- `;
329
- async function synthesizeBundle(opts) {
330
- const { cachedElectronApp, destApp, displayName, slug, version, bundleId = `dev.zenbu.${slug}`, iconSource, packageManager, appsDir, launcherSource, log, dryRun = false, force = false } = opts;
331
- if (!fs.existsSync(cachedElectronApp)) throw new Error(`cached Electron.app missing: ${cachedElectronApp}. Did the cache step fail?`);
332
- if (fs.existsSync(destApp)) {
333
- if (!force) throw new Error(`${destApp} already exists. Re-run with --force to overwrite.`);
334
- if (dryRun) log.info(`[dry-run] would rm -rf ${destApp}`);
335
- else {
336
- log.info(`removing existing ${destApp}`);
337
- await fsp.rm(destApp, {
338
- recursive: true,
339
- force: true
340
- });
341
- }
342
- }
343
- await ensureParentDir(destApp, dryRun, log);
344
- await log.withStep(`clone-copy ${cachedElectronApp} -> ${destApp}`, async () => {
345
- if (dryRun) return;
346
- cloneCopy(cachedElectronApp, destApp, log);
347
- });
348
- const macosDir = path.join(destApp, "Contents", "MacOS");
349
- const oldExe = path.join(macosDir, "Electron");
350
- const newExe = path.join(macosDir, displayName);
351
- await log.withStep(`rename executable -> ${displayName}`, async () => {
352
- if (dryRun) return;
353
- if (fs.existsSync(oldExe)) await fsp.rename(oldExe, newExe);
354
- else if (!fs.existsSync(newExe)) throw new Error(`expected ${oldExe} or ${newExe} to exist after clone-copy`);
355
- });
356
- const infoPlist = path.join(destApp, "Contents", "Info.plist");
357
- await log.withStep(`patch Info.plist`, async () => {
358
- if (dryRun) return;
359
- const ops = [
360
- {
361
- key: "CFBundleName",
362
- type: "string",
363
- value: displayName
364
- },
365
- {
366
- key: "CFBundleDisplayName",
367
- type: "string",
368
- value: displayName
369
- },
370
- {
371
- key: "CFBundleExecutable",
372
- type: "string",
373
- value: displayName
374
- },
375
- {
376
- key: "CFBundleIdentifier",
377
- type: "string",
378
- value: bundleId
379
- },
380
- {
381
- key: "CFBundleIconFile",
382
- type: "string",
383
- value: "icon.icns"
384
- },
385
- {
386
- key: "CFBundleVersion",
387
- type: "string",
388
- value: version
389
- },
390
- {
391
- key: "CFBundleShortVersionString",
392
- type: "string",
393
- value: version
394
- },
395
- {
396
- key: "NSHighResolutionCapable",
397
- type: "bool",
398
- value: true
399
- },
400
- {
401
- key: "LSMinimumSystemVersion",
402
- type: "string",
403
- value: "12.0"
404
- },
405
- {
406
- key: "NSSupportsAutomaticGraphicsSwitching",
407
- type: "bool",
408
- value: true
409
- },
410
- {
411
- key: "NSRequiresAquaSystemAppearance",
412
- type: "bool",
413
- value: false
414
- },
415
- {
416
- key: "NSPrincipalClass",
417
- type: "string",
418
- value: "AtomApplication"
419
- }
420
- ];
421
- for (const op of ops) plutilReplace(infoPlist, op, log);
422
- plutilRemove(infoPlist, ":ElectronAsarIntegrity", log);
423
- });
424
- const resourcesDir = path.join(destApp, "Contents", "Resources");
425
- const iconDest = path.join(resourcesDir, "icon.icns");
426
- await log.withStep(`install icon -> ${iconDest}`, async () => {
427
- if (dryRun) return;
428
- if (iconSource) await prepareIcon({
429
- source: iconSource,
430
- dest: iconDest,
431
- log
432
- });
433
- else {
434
- const fallback = path.join(resourcesDir, "electron.icns");
435
- if (fs.existsSync(fallback)) await fsp.copyFile(fallback, iconDest);
436
- else log.info(`no icon source and no electron.icns fallback; skipping`);
437
- }
438
- });
439
- const bundleAppDir = path.join(resourcesDir, "app");
440
- await log.withStep(`write Resources/app/{package.json,launcher.mjs,app-config.json,host.json}`, async () => {
441
- if (dryRun) return;
442
- await fsp.mkdir(bundleAppDir, { recursive: true });
443
- const bundlePkg = {
444
- name: slug,
445
- version,
446
- main: "launcher.mjs",
447
- type: "module"
448
- };
449
- await fsp.writeFile(path.join(bundleAppDir, "package.json"), JSON.stringify(bundlePkg, null, 2) + "\n");
450
- if (!fs.existsSync(launcherSource)) throw new Error(`launcher source missing: ${launcherSource}`);
451
- await fsp.copyFile(launcherSource, path.join(bundleAppDir, "launcher.mjs"));
452
- const appConfig = {
453
- name: slug,
454
- packageManager,
455
- version,
456
- local: true
457
- };
458
- await fsp.writeFile(path.join(bundleAppDir, "app-config.json"), JSON.stringify(appConfig, null, 2) + "\n");
459
- await fsp.writeFile(path.join(bundleAppDir, "host.json"), JSON.stringify({ version }, null, 2) + "\n");
460
- log.info(`appsDir baked into name = ${slug} (resolves to ${appsDir})`);
461
- });
462
- const entitlementsPath = path.join(os.tmpdir(), `czda-entitlements-${process.pid}-${Date.now()}.plist`);
463
- await log.withStep(`ad-hoc codesign --deep`, async () => {
464
- if (dryRun) return;
465
- await fsp.writeFile(entitlementsPath, ENTITLEMENTS);
466
- try {
467
- const r = spawnSync("codesign", [
468
- "--force",
469
- "--deep",
470
- "--sign",
471
- "-",
472
- "--entitlements",
473
- entitlementsPath,
474
- destApp
475
- ], {
476
- stdio: "pipe",
477
- encoding: "utf8"
478
- });
479
- if (r.status !== 0) throw new Error(`codesign failed (${r.status}): ${r.stderr?.slice(0, 1e3) ?? ""}`);
480
- log.info(r.stderr?.trim() || "codesign ok");
481
- } finally {
482
- await fsp.unlink(entitlementsPath).catch(() => {});
483
- }
484
- });
485
- await log.withStep(`xattr -dr com.apple.quarantine`, async () => {
486
- if (dryRun) return;
487
- spawnSync("xattr", [
488
- "-dr",
489
- "com.apple.quarantine",
490
- destApp
491
- ], { stdio: "ignore" });
492
- });
493
- await log.withStep(`verify (codesign + plutil)`, async () => {
494
- if (dryRun) return;
495
- const cs = spawnSync("codesign", [
496
- "--verify",
497
- "--deep",
498
- "--strict",
499
- destApp
500
- ], {
501
- stdio: "pipe",
502
- encoding: "utf8"
503
- });
504
- if (cs.status !== 0) throw new Error(`codesign verify failed (${cs.status}): ${cs.stderr?.slice(0, 1e3) ?? ""}`);
505
- const pl = spawnSync("plutil", ["-lint", infoPlist], {
506
- stdio: "pipe",
507
- encoding: "utf8"
508
- });
509
- if (pl.status !== 0) throw new Error(`plutil -lint failed (${pl.status}): ${pl.stderr?.slice(0, 500) ?? ""} ${pl.stdout?.slice(0, 500) ?? ""}`);
510
- });
511
- }
512
- function cloneCopy(src, dest, log) {
513
- const r = spawnSync("cp", [
514
- "-c",
515
- "-R",
516
- src,
517
- dest
518
- ], {
519
- stdio: "pipe",
520
- encoding: "utf8"
521
- });
522
- if (r.status === 0) return;
523
- log.info(`cp -c failed, falling back to cp -R: ${r.stderr?.trim() ?? ""}`);
524
- const r2 = spawnSync("cp", [
525
- "-R",
526
- src,
527
- dest
528
- ], {
529
- stdio: "pipe",
530
- encoding: "utf8"
531
- });
532
- if (r2.status !== 0) throw new Error(`cp failed (${r2.status}): ${r2.stderr?.slice(0, 1e3) ?? ""}`);
533
- }
534
- async function ensureParentDir(destApp, dryRun, log) {
535
- const parent = path.dirname(destApp);
536
- if (fs.existsSync(parent)) return;
537
- if (dryRun) {
538
- log.info(`[dry-run] would mkdir -p ${parent}`);
539
- return;
540
- }
541
- await fsp.mkdir(parent, { recursive: true });
542
- }
543
- function plutilReplace(infoPlist, op, log) {
544
- const value = String(op.value);
545
- if (spawnSync("plutil", [
546
- "-replace",
547
- op.key,
548
- `-${op.type}`,
549
- value,
550
- infoPlist
551
- ], {
552
- stdio: "pipe",
553
- encoding: "utf8"
554
- }).status === 0) return;
555
- const insert = spawnSync("plutil", [
556
- "-insert",
557
- op.key,
558
- `-${op.type}`,
559
- value,
560
- infoPlist
561
- ], {
562
- stdio: "pipe",
563
- encoding: "utf8"
564
- });
565
- if (insert.status !== 0) throw new Error(`plutil -insert ${op.key} failed (${insert.status}): ${insert.stderr?.slice(0, 500) ?? ""}`);
566
- log.info(`plutil inserted ${op.key}=${value}`);
567
- }
568
- function plutilRemove(infoPlist, keyPath, log) {
569
- const r = spawnSync("plutil", [
570
- "-remove",
571
- keyPath,
572
- infoPlist
573
- ], {
574
- stdio: "pipe",
575
- encoding: "utf8"
576
- });
577
- if (r.status === 0) {
578
- log.info(`plutil removed ${keyPath}`);
579
- return;
580
- }
581
- log.info(`plutil -remove ${keyPath}: ${r.stderr?.trim() ?? "(no key)"}`);
582
- }
583
- /**
584
- * Resolve the path to `@zenbujs/core/dist/launcher.mjs` from any cwd. We
585
- * use `package.json` as the resolution anchor because that subpath is
586
- * exported (see packages/core/package.json `exports`).
587
- */
588
- function resolveLauncherSource(fromDir) {
589
- const req = createRequire(path.join(fromDir, "noop.js"));
590
- let pkgPath;
591
- try {
592
- pkgPath = req.resolve("@zenbujs/core/package.json");
593
- } catch (err) {
594
- throw new Error(`failed to resolve @zenbujs/core from ${fromDir}: ${err.message}. Make sure create-zenbu-app's dependency on @zenbujs/core is installed.`);
595
- }
596
- const launcher = path.join(path.dirname(pkgPath), "dist", "launcher.mjs");
597
- if (!fs.existsSync(launcher)) throw new Error(`@zenbujs/core launcher not found at ${launcher}. The package may be a stub or unbuilt.`);
598
- return launcher;
599
- }
600
- //#endregion
601
- //#region src/desktop/deps-sig.ts
602
- function lockfileFor(type) {
603
- switch (type) {
604
- case "pnpm": return "pnpm-lock.yaml";
605
- case "npm": return "package-lock.json";
606
- case "yarn": return "yarn.lock";
607
- case "bun": return "bun.lock";
608
- }
609
- }
610
- /**
611
- * Mirror of `packages/core/src/shared/pm-install.ts#depsSignature`, with
612
- * `electronVersion` accepted explicitly because we run from Node (not
613
- * Electron) and `process.versions.electron` is undefined here.
614
- *
615
- * Must stay byte-for-byte equivalent to the launcher's recipe so the
616
- * pre-seeded `<appsDir>/.zenbu/deps-sig` matches what `ensureDepsInstalled`
617
- * recomputes on first launch and the install gate skips.
618
- */
619
- async function depsSignature(opts) {
620
- const hash = crypto.createHash("sha256");
621
- await fileHash(hash, path.join(opts.appsDir, "package.json"));
622
- await fileHash(hash, path.join(opts.appsDir, lockfileFor(opts.pm.type)));
623
- hash.update(`${opts.pm.type}@${opts.pm.version}`);
624
- hash.update("\0");
625
- hash.update(opts.electronVersion);
626
- hash.update("\0");
627
- hash.update(opts.platform ?? process.platform);
628
- hash.update("\0");
629
- hash.update(opts.arch ?? process.arch);
630
- return hash.digest("hex");
631
- }
632
- async function fileHash(hash, filePath) {
633
- hash.update(filePath);
634
- hash.update("\0");
635
- try {
636
- hash.update(await fsp.readFile(filePath));
637
- } catch {}
638
- hash.update("\0");
639
- }
640
- async function writeDepsSig(appsDir, sig) {
641
- const sigPath = path.join(appsDir, ".zenbu", "deps-sig");
642
- await fsp.mkdir(path.dirname(sigPath), { recursive: true });
643
- await fsp.writeFile(sigPath, sig);
644
- }
645
- //#endregion
646
- //#region src/desktop/log.ts
647
- const LOG_DIR = path.join(os.homedir(), ".zenbu", "logs", "create-zenbu-app");
648
- function ts() {
649
- return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
650
- }
651
- function createLogger(opts) {
652
- fs.mkdirSync(LOG_DIR, { recursive: true });
653
- const file = path.join(LOG_DIR, `${ts()}-${opts.slug}.log`);
654
- const stream = fs.createWriteStream(file, { flags: "a" });
655
- stream.write(`=== create-zenbu-app --desktop ${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} slug=${opts.slug} ===\n`);
656
- const writeLine = (prefix, line) => {
657
- const text = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${prefix}${line}\n`;
658
- try {
659
- stream.write(text);
660
- } catch {}
661
- if (opts.verbose) process.stdout.write(text);
662
- };
663
- return {
664
- file,
665
- step(label) {
666
- writeLine("[STEP] ", label);
667
- },
668
- info(line) {
669
- writeLine("", line);
670
- },
671
- error(line) {
672
- const text = `[${(/* @__PURE__ */ new Date()).toISOString()}] [ERR] ${line}\n`;
673
- try {
674
- stream.write(text);
675
- } catch {}
676
- process.stderr.write(text);
677
- },
678
- async withStep(label, fn) {
679
- this.step(label);
680
- const start = Date.now();
681
- try {
682
- const out = await fn();
683
- writeLine("[STEP-OK] ", `${label} (${Date.now() - start}ms)`);
684
- return out;
685
- } catch (err) {
686
- const e = err;
687
- writeLine("[STEP-FAIL] ", `${label} (${Date.now() - start}ms): ${e.stack ?? e.message ?? String(err)}`);
688
- throw err;
689
- }
690
- },
691
- tail(n = 30) {
692
- try {
693
- const all = fs.readFileSync(file, "utf8").split("\n");
694
- return all.slice(Math.max(0, all.length - n - 1)).join("\n");
695
- } catch {
696
- return "";
697
- }
698
- },
699
- close() {
700
- try {
701
- stream.end();
702
- } catch {}
703
- }
704
- };
705
- }
706
- //#endregion
707
- //#region src/desktop/index.ts
708
- /**
709
- * Default destination when `destApp` not provided. We prefer `/Applications`
710
- * but fall back to `~/Applications` when /Applications isn't writable
711
- * (e.g. without admin rights or on locked-down setups).
712
- */
713
- function defaultDestApp(displayName) {
714
- const sysApps = "/Applications";
715
- try {
716
- fs.accessSync(sysApps, fs.constants.W_OK);
717
- return path.join(sysApps, `${displayName}.app`);
718
- } catch {
719
- const userApps = path.join(os.homedir(), "Applications");
720
- return path.join(userApps, `${displayName}.app`);
721
- }
722
- }
723
- async function buildDesktopApp(opts) {
724
- const { displayName, slug, version, electronVersionRange, iconSource, appsDir, packageManager, resolveFrom, log, force = false, dryRun = false, skipDepsSig = false } = opts;
725
- const destApp = opts.destApp ?? defaultDestApp(displayName);
726
- const versionRange = electronVersionRange ?? await readElectronRangeFromAppsDir(appsDir) ?? "latest";
727
- log.info(`displayName=${displayName} slug=${slug} version=${version}`);
728
- log.info(`appsDir=${appsDir}`);
729
- log.info(`destApp=${destApp}`);
730
- log.info(`electronVersionRange=${versionRange}`);
731
- const cache = await log.withStep(`ensure electron cache (${versionRange})`, () => ensureElectronApp({
732
- versionRange,
733
- log
734
- }));
735
- const launcherSource = resolveLauncherSource(resolveFrom);
736
- log.info(`launcher source: ${launcherSource}`);
737
- await synthesizeBundle({
738
- cachedElectronApp: cache.electronAppPath,
739
- destApp,
740
- displayName,
741
- slug,
742
- version,
743
- iconSource,
744
- packageManager,
745
- appsDir,
746
- launcherSource,
747
- log,
748
- dryRun,
749
- force
750
- });
751
- if (!skipDepsSig && !dryRun) await log.withStep(`write <appsDir>/.zenbu/deps-sig`, async () => {
752
- const sig = await depsSignature({
753
- appsDir,
754
- pm: packageManager,
755
- electronVersion: cache.version
756
- });
757
- await writeDepsSig(appsDir, sig);
758
- log.info(`deps-sig=${sig.slice(0, 16)}…`);
759
- });
760
- else if (skipDepsSig) log.info(`skipping deps-sig (--no-install or skipDepsSig=true)`);
761
- return {
762
- destApp,
763
- electronVersion: cache.version,
764
- appsDir,
765
- launchCommand: `open -a "${destApp}"`
766
- };
767
- }
768
- async function readElectronRangeFromAppsDir(appsDir) {
769
- try {
770
- const raw = await fsp.readFile(path.join(appsDir, "package.json"), "utf8");
771
- const pkg = JSON.parse(raw);
772
- return pkg.devDependencies?.electron ?? pkg.dependencies?.electron ?? null;
773
- } catch {
774
- return null;
775
- }
776
- }
777
- //#endregion
778
9
  //#region src/index.ts
779
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
780
11
  const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates");
@@ -866,6 +97,16 @@ function parseDependsOn(raw) {
866
97
  };
867
98
  }
868
99
  const ZENBU_LOCAL_CORE = process.env.ZENBU_LOCAL_CORE;
100
+ const TRIM_AGENTS_MD = process.env.TRIM_AGENTS_MD === "1";
101
+ const AGENTS_MD_MAX_CHARS = 4e4;
102
+ function trimAgentsMdIfRequested(projectDir) {
103
+ if (!TRIM_AGENTS_MD) return;
104
+ const target = path.join(projectDir, "AGENTS.md");
105
+ if (!fs.existsSync(target)) return;
106
+ const original = fs.readFileSync(target, "utf8");
107
+ if (original.length <= AGENTS_MD_MAX_CHARS) return;
108
+ fs.writeFileSync(target, original.slice(0, AGENTS_MD_MAX_CHARS));
109
+ }
869
110
  const CONFIG_OPTIONS = [{
870
111
  id: "tailwind",
871
112
  default: true,
@@ -1321,6 +562,7 @@ async function runDesktopMode() {
1321
562
  const gi = path.join(appsDir, "_gitignore");
1322
563
  if (fs.existsSync(gi)) fs.renameSync(gi, path.join(appsDir, ".gitignore"));
1323
564
  seedPackageManager(appsDir, pm);
565
+ trimAgentsMdIfRequested(appsDir);
1324
566
  if (ZENBU_LOCAL_CORE) {
1325
567
  const corePath = path.resolve(ZENBU_LOCAL_CORE);
1326
568
  rewireToLocalCore(appsDir, corePath, false);
@@ -1429,6 +671,7 @@ async function main() {
1429
671
  const gi = path.join(projectDir, "_gitignore");
1430
672
  if (fs.existsSync(gi)) fs.renameSync(gi, path.join(projectDir, ".gitignore"));
1431
673
  if (!pluginMode) seedPackageManager(projectDir, pm);
674
+ trimAgentsMdIfRequested(projectDir);
1432
675
  if (ZENBU_LOCAL_CORE) {
1433
676
  const corePath = path.resolve(ZENBU_LOCAL_CORE);
1434
677
  rewireToLocalCore(projectDir, corePath, pluginMode);