create-vidra-app 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/commands/build.ts
2
- import path5 from "path";
3
- import fs4 from "fs-extra";
2
+ import path12 from "path";
3
+ import fs11 from "fs-extra";
4
4
  import { execSync as execSync3 } from "child_process";
5
5
 
6
6
  // src/utils.ts
@@ -90,9 +90,9 @@ var fixLine = (cmd, label = "fix:") => ` ${amber(label)} ${lime(cmd)}`;
90
90
  var planBadge = (text = "PLAN ONLY \xB7 BEING BUILT") => amber(`[ ${text} ]`);
91
91
 
92
92
  // src/exec.ts
93
- var toText = (value2) => {
94
- if (value2 == null) return "";
95
- return Buffer.isBuffer(value2) ? value2.toString() : value2;
93
+ var toText = (value3) => {
94
+ if (value3 == null) return "";
95
+ return Buffer.isBuffer(value3) ? value3.toString() : value3;
96
96
  };
97
97
  var formatProcessError = (error) => {
98
98
  const err = error;
@@ -111,145 +111,323 @@ var formatBuildError = (error) => {
111
111
  return picked.join("\n").trim();
112
112
  };
113
113
 
114
- // src/project.ts
115
- import path from "path";
114
+ // src/version.ts
116
115
  import fs from "fs-extra";
117
- var detectPlatform = () => {
118
- switch (process.platform) {
119
- case "darwin":
120
- return "macos";
121
- case "win32":
122
- return "windows";
123
- case "linux":
124
- return "linux";
125
- default:
126
- return "macos";
127
- }
116
+ import path from "path";
117
+ var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+[0-9A-Za-z-.]+)?$/;
118
+ var VersionError = class extends Error {
128
119
  };
129
- var detectProject = (cwd) => {
130
- let dir = cwd;
131
- while (true) {
132
- const uiDir = path.join(dir, "ui");
133
- const srcDir = path.join(dir, "src");
134
- const pkgJson = path.join(dir, "package.json");
135
- if (fs.existsSync(pkgJson) && fs.existsSync(uiDir) && fs.existsSync(srcDir)) {
136
- const csproj = findHostCsproj(srcDir);
137
- if (csproj) {
138
- const projectName = path.basename(csproj.dir).replace(/\.Host$/, "");
139
- const displayVersion = readCsprojVersion(csproj.path);
140
- return {
141
- root: dir,
142
- hostDir: csproj.dir,
143
- csprojPath: csproj.path,
144
- projectName,
145
- displayVersion,
146
- uiDir
147
- };
148
- }
149
- }
150
- const parent = path.dirname(dir);
151
- if (parent === dir) break;
152
- dir = parent;
120
+ var buildNumberFor = (major, minor, patch) => major * 1e4 + minor * 100 + patch;
121
+ var SEGMENT_CEILING = 100;
122
+ var parseAppVersion = (raw, source) => {
123
+ const match = SEMVER.exec(raw.trim());
124
+ if (!match) {
125
+ throw new VersionError(
126
+ `${source}: "${raw}" is not a valid version \u2014 expected semver like 1.2.3`
127
+ );
153
128
  }
154
- console.error(
155
- row({
156
- glyph: "error",
157
- detail: dim(
158
- "Could not detect Vidra project. Run this command from your project root."
159
- )
160
- })
161
- );
162
- console.error(
163
- footer(dim("expected: package.json, ui/, src/<Name>.Host/<Name>.Host.csproj"))
164
- );
165
- process.exit(1);
129
+ const [, major, minor, patch] = match.map((v) => v);
130
+ const [maj, min, pat] = [Number(major), Number(minor), Number(patch)];
131
+ const override = process.env.VIDRA_BUILD_NUMBER;
132
+ if (override !== void 0 && !/^\d+$/.test(override)) {
133
+ throw new VersionError(
134
+ `VIDRA_BUILD_NUMBER: "${override}" is not a positive integer`
135
+ );
136
+ }
137
+ if (override === void 0 && (min >= SEGMENT_CEILING || pat >= SEGMENT_CEILING)) {
138
+ throw new VersionError(
139
+ `${source}: "${raw}" \u2014 minor and patch must stay below ${SEGMENT_CEILING} so the build number keeps increasing; set VIDRA_BUILD_NUMBER to override`
140
+ );
141
+ }
142
+ return {
143
+ semver: raw.trim(),
144
+ display: `${maj}.${min}.${pat}`,
145
+ build: override !== void 0 ? Number(override) : buildNumberFor(maj, min, pat)
146
+ };
166
147
  };
167
- var findHostCsproj = (srcDir) => {
168
- if (!fs.existsSync(srcDir)) return null;
169
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
170
- if (entry.isDirectory() && entry.name.endsWith(".Host")) {
171
- const csprojPath = path.join(
172
- srcDir,
173
- entry.name,
174
- `${entry.name}.csproj`
175
- );
176
- if (fs.existsSync(csprojPath)) {
177
- return { dir: path.join(srcDir, entry.name), path: csprojPath };
178
- }
148
+ var resolveAppVersion = (projectRoot, csprojPath) => {
149
+ const pkgPath = path.join(projectRoot, "package.json");
150
+ if (fs.existsSync(pkgPath)) {
151
+ const pkg = fs.readJsonSync(pkgPath, { throws: false });
152
+ if (pkg?.version) {
153
+ return { ...parseAppVersion(pkg.version, "package.json"), source: "package.json" };
179
154
  }
180
155
  }
181
- return null;
156
+ const csproj = fs.readFileSync(csprojPath, "utf-8");
157
+ const fromCsproj = csproj.match(
158
+ /<ApplicationDisplayVersion>([^<]+)<\/ApplicationDisplayVersion>/
159
+ )?.[1];
160
+ if (!fromCsproj) {
161
+ throw new VersionError(
162
+ `no version found \u2014 add a "version" to the app's package.json (npm version patch)`
163
+ );
164
+ }
165
+ return { ...parseAppVersion(fromCsproj, "csproj"), source: "csproj" };
182
166
  };
183
- var readCsprojVersion = (csprojPath) => {
184
- const content = fs.readFileSync(csprojPath, "utf-8");
185
- const match = content.match(
186
- /<ApplicationDisplayVersion>(.*?)<\/ApplicationDisplayVersion>/
187
- );
188
- return match?.[1] ?? "0.1.0";
167
+ var versionPublishArgs = (version) => [
168
+ `-p:ApplicationDisplayVersion=${version.display}`,
169
+ `-p:ApplicationVersion=${version.build}`
170
+ ];
171
+ var resolveAppVersionOrDefault = (projectRoot, csprojPath, fallback = "0.1.0") => {
172
+ try {
173
+ return resolveAppVersion(projectRoot, csprojPath).semver;
174
+ } catch {
175
+ return fallback;
176
+ }
189
177
  };
190
178
 
191
- // src/targets/macos.ts
179
+ // src/update-config.ts
192
180
  import path2 from "path";
193
181
  import fs2 from "fs-extra";
194
- import { execSync as execSync2 } from "child_process";
195
- import os from "os";
196
- var macosTarget = {
197
- name: "macos",
198
- framework: "net10.0-maccatalyst",
199
- extraPublishArgs: "-p:CreatePackage=false",
200
- findBundle(publishDir, _projectName) {
201
- if (!fs2.existsSync(publishDir)) return null;
202
- for (const entry of fs2.readdirSync(publishDir, { withFileTypes: true })) {
203
- if (entry.name.endsWith(".app") && entry.isDirectory()) {
204
- return path2.join(publishDir, entry.name);
205
- }
206
- }
182
+ var UPDATE_CONFIG_FILE = "vidra-updates.json";
183
+ var readUpdateConfig = (projectRoot) => {
184
+ const pkgPath = path2.join(projectRoot, "package.json");
185
+ if (!fs2.existsSync(pkgPath)) return null;
186
+ let pkg;
187
+ try {
188
+ pkg = fs2.readJsonSync(pkgPath);
189
+ } catch {
207
190
  return null;
208
- },
209
- async package(appPath, outputDir, meta) {
210
- const dmgName = `${meta.projectName}-${meta.displayVersion}-macos.dmg`;
211
- const dmgPath = path2.join(outputDir, dmgName);
212
- const volName = meta.projectName;
213
- const staging = fs2.mkdtempSync(path2.join(os.tmpdir(), "vidra-dmg-"));
214
- try {
215
- fs2.copySync(appPath, path2.join(staging, path2.basename(appPath)));
216
- fs2.symlinkSync("/Applications", path2.join(staging, "Applications"));
217
- if (fs2.existsSync(dmgPath)) {
218
- fs2.removeSync(dmgPath);
219
- }
220
- execSync2(
221
- `hdiutil create -volname "${volName}" -srcfolder "${staging}" -ov -format UDZO "${dmgPath}"`,
222
- { stdio: "pipe" }
223
- );
224
- } finally {
225
- fs2.removeSync(staging);
191
+ }
192
+ const updates = pkg?.vidra?.updates;
193
+ if (!updates || typeof updates !== "object") return null;
194
+ const raw = updates;
195
+ const config = {};
196
+ if (typeof raw.feedUrl === "string" && raw.feedUrl.trim().length > 0) {
197
+ config.feedUrl = raw.feedUrl.trim();
198
+ }
199
+ if (typeof raw.channel === "string" && raw.channel.trim().length > 0) {
200
+ config.channel = raw.channel.trim();
201
+ }
202
+ if (typeof raw.enabled === "boolean") {
203
+ config.enabled = raw.enabled;
204
+ }
205
+ const keys = [
206
+ ...typeof raw.publicKey === "string" ? [raw.publicKey] : [],
207
+ ...Array.isArray(raw.publicKeys) ? raw.publicKeys : []
208
+ ].filter((key) => typeof key === "string" && key.trim().length > 0);
209
+ if (keys.length > 0) {
210
+ config.publicKeys = keys.map((key) => key.trim());
211
+ }
212
+ const native = readNativeConfig(raw.native);
213
+ if (native) {
214
+ config.native = native;
215
+ }
216
+ return Object.keys(config).length > 0 ? config : null;
217
+ };
218
+ var readNativeConfig = (raw) => {
219
+ if (!raw || typeof raw !== "object") return null;
220
+ const source = raw;
221
+ const native = {};
222
+ const text = (key) => {
223
+ const value3 = source[key];
224
+ if (typeof value3 === "string" && value3.trim().length > 0) {
225
+ native[key] = value3.trim();
226
226
  }
227
- return dmgPath;
227
+ };
228
+ text("feedUrl");
229
+ text("channel");
230
+ text("packId");
231
+ if (typeof source.enabled === "boolean") native.enabled = source.enabled;
232
+ return Object.keys(native).length > 0 ? native : null;
233
+ };
234
+ var stampUpdateConfig = (hostDir, config) => {
235
+ const target = path2.join(hostDir, "Resources", "Raw", UPDATE_CONFIG_FILE);
236
+ if (!config) {
237
+ fs2.removeSync(target);
238
+ return null;
228
239
  }
240
+ fs2.ensureDirSync(path2.dirname(target));
241
+ fs2.writeFileSync(target, `${JSON.stringify(config, null, 2)}
242
+ `);
243
+ return target;
229
244
  };
230
245
 
231
- // src/signing.ts
246
+ // src/native-update.ts
247
+ import path6 from "path";
248
+ import os2 from "os";
249
+ import fs6 from "fs-extra";
250
+ import { execFileSync as execFileSync4 } from "child_process";
251
+
252
+ // src/velopack.ts
232
253
  import path3 from "path";
254
+ import os from "os";
255
+ import fs3 from "fs-extra";
233
256
  import { execFileSync } from "child_process";
257
+ var ALREADY_RELEASED = /There is a release in channel .*? which is equal or greater to the current version/i;
258
+ var packArgs = (options) => {
259
+ const args = [
260
+ "pack",
261
+ "--packId",
262
+ options.packId,
263
+ "--packVersion",
264
+ options.packVersion,
265
+ "--packDir",
266
+ options.packDir,
267
+ "--mainExe",
268
+ options.mainExe,
269
+ "--outputDir",
270
+ options.outputDir
271
+ ];
272
+ if (options.packTitle) args.push("--packTitle", options.packTitle);
273
+ if (options.channel) args.push("--channel", options.channel);
274
+ if (options.signAppIdentity) args.push("--signAppIdentity", options.signAppIdentity);
275
+ if (options.signEntitlements) args.push("--signEntitlements", options.signEntitlements);
276
+ if (options.keychain) args.push("--keychain", options.keychain);
277
+ if (options.signParams) args.push("--signParams", options.signParams);
278
+ return args;
279
+ };
280
+ var downloadArgs = (options) => {
281
+ const args = ["download", "http", "--url", options.feedUrl, "--outputDir", options.outputDir];
282
+ if (options.channel) args.push("--channel", options.channel);
283
+ return args;
284
+ };
285
+ var resolveVpk = () => {
286
+ const exe = process.platform === "win32" ? "vpk.exe" : "vpk";
287
+ const onPath = which(exe);
288
+ if (onPath) return onPath;
289
+ const home = process.env.DOTNET_CLI_HOME?.trim() || os.homedir();
290
+ const candidate = path3.join(home, ".dotnet", "tools", exe);
291
+ return fs3.existsSync(candidate) ? candidate : null;
292
+ };
293
+ var parseVpkVersion = (helpOutput) => helpOutput.match(/Velopack CLI\s+([\d][\w.+-]*)/i)?.[1] ?? null;
294
+ var vpkVersion = (vpk) => {
295
+ try {
296
+ const output = execFileSync(vpk, ["--help"], {
297
+ encoding: "utf-8",
298
+ stdio: ["ignore", "pipe", "pipe"]
299
+ });
300
+ return parseVpkVersion(output ?? "");
301
+ } catch (error) {
302
+ return parseVpkVersion(combinedOutput(error));
303
+ }
304
+ };
305
+ var isVersionAlreadyReleased = (output) => ALREADY_RELEASED.test(output);
306
+ var runVpk = (vpk, args, options = {}) => {
307
+ try {
308
+ const output = execFileSync(vpk, args, {
309
+ cwd: options.cwd,
310
+ encoding: "utf-8",
311
+ // Never `shell: true`. See the header.
312
+ stdio: options.verbose ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
313
+ env: {
314
+ ...process.env,
315
+ // git-bash's argument mangling again: it rewrites anything starting
316
+ // with `/`, and Velopack's Windows signing arguments all do.
317
+ MSYS2_ARG_CONV_EXCL: "*"
318
+ }
319
+ });
320
+ return { ok: true, output: output ?? "", alreadyReleased: false };
321
+ } catch (error) {
322
+ const output = combinedOutput(error);
323
+ return { ok: false, output, alreadyReleased: isVersionAlreadyReleased(output) };
324
+ }
325
+ };
326
+ var findMacMainExe = (appBundle) => {
327
+ const macos = path3.join(appBundle, "Contents", "MacOS");
328
+ if (!fs3.existsSync(macos)) return null;
329
+ const declaredByPlist = readBundleExecutable(appBundle);
330
+ if (declaredByPlist && fs3.existsSync(path3.join(macos, declaredByPlist))) return declaredByPlist;
331
+ const declared = readMainExeFromSpec(appBundle);
332
+ if (declared && fs3.existsSync(path3.join(macos, declared))) return declared;
333
+ const candidates = fs3.readdirSync(macos, { withFileTypes: true }).filter((e) => e.isFile() && !e.name.startsWith(".") && e.name !== "UpdateMac" && e.name !== "sq.version").map((e) => e.name);
334
+ return candidates[0] ?? null;
335
+ };
336
+ var readBundleExecutable = (appBundle) => {
337
+ const plist = path3.join(appBundle, "Contents", "Info.plist");
338
+ if (!fs3.existsSync(plist)) return null;
339
+ const match = /<key>CFBundleExecutable<\/key>\s*<string>([^<]+)<\/string>/.exec(
340
+ fs3.readFileSync(plist, "utf-8")
341
+ );
342
+ return match?.[1].trim() || null;
343
+ };
344
+ var readMainExeFromSpec = (appBundle) => {
345
+ for (const dir of ["MacOS", "Resources"]) {
346
+ const file = path3.join(appBundle, "Contents", dir, "sq.version");
347
+ if (!fs3.existsSync(file)) continue;
348
+ const match = /<mainExe>([^<]+)<\/mainExe>/.exec(fs3.readFileSync(file, "utf-8"));
349
+ if (match) return match[1].trim();
350
+ }
351
+ return null;
352
+ };
353
+ var findVpkOutputs = (outputDir) => {
354
+ const files = fs3.existsSync(outputDir) ? fs3.readdirSync(outputDir) : [];
355
+ const find = (suffix) => {
356
+ const match = files.find((f) => f.toLowerCase().endsWith(suffix.toLowerCase()));
357
+ return match ? path3.join(outputDir, match) : null;
358
+ };
359
+ return {
360
+ portableZip: find("Portable.zip"),
361
+ setupExe: find("Setup.exe"),
362
+ setupPkg: find("Setup.pkg")
363
+ };
364
+ };
365
+ var combinedOutput = (error) => {
366
+ const err = error;
367
+ const text = (v) => v == null ? "" : Buffer.isBuffer(v) ? v.toString() : v;
368
+ const combined = [text(err.stdout), text(err.stderr)].map((s) => s.trim()).filter(Boolean).join("\n").trim();
369
+ return combined || err.message || String(error);
370
+ };
371
+ var which = (exe) => {
372
+ const probe = process.platform === "win32" ? "where" : "which";
373
+ try {
374
+ const output = execFileSync(probe, [exe], {
375
+ encoding: "utf-8",
376
+ stdio: ["ignore", "pipe", "ignore"]
377
+ });
378
+ const first = output.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
379
+ return first ?? null;
380
+ } catch {
381
+ return null;
382
+ }
383
+ };
384
+
385
+ // src/signing.ts
386
+ import path4 from "path";
387
+ import fs4 from "fs-extra";
388
+ import { execFileSync as execFileSync2, spawnSync } from "child_process";
389
+ var DEVELOPER_ID_PREFIX = "Developer ID Application:";
390
+ var APPLE_DEVELOPMENT_PREFIX = "Apple Development:";
234
391
  var signMacAppBundleIfPossible = (appBundle, options) => {
235
392
  if (process.platform !== "darwin") return;
236
- const identity = resolveMacCodeSigningIdentity();
393
+ const purpose = options.purpose ?? "development";
394
+ const { identity, expiredSkipped } = selectMacIdentity(purpose);
395
+ warnAboutExpiredIdentities(expiredSkipped, options);
237
396
  const signWith = identity ?? "-";
238
- const label = path3.basename(appBundle);
397
+ const label = path4.basename(appBundle);
398
+ const entitlements = options.entitlements && fs4.existsSync(options.entitlements) ? options.entitlements : null;
399
+ const hardened = purpose === "distribution" && entitlements !== null;
400
+ if (purpose === "distribution") {
401
+ warnAboutDistributionIdentity(identity, options);
402
+ if (!entitlements) {
403
+ options.warn(
404
+ row({
405
+ glyph: "manual",
406
+ label: "codesign",
407
+ labelWidth: STEP_LABEL_WIDTH,
408
+ detail: dim(
409
+ "no Entitlements.plist found \u2014 signing without the hardened runtime (cannot be notarized)"
410
+ )
411
+ })
412
+ );
413
+ }
414
+ }
239
415
  try {
240
- execFileSync(
241
- "codesign",
242
- ["--force", "--deep", "--sign", signWith, appBundle],
243
- {
244
- stdio: options.verbose ? "inherit" : "pipe"
245
- }
246
- );
416
+ for (const nested of findNestedMachOPayloads(appBundle)) {
417
+ runCodesign(nested, signWith, { hardened, entitlements: null }, options);
418
+ }
419
+ runCodesign(appBundle, signWith, { hardened, entitlements }, options);
420
+ const detail = [
421
+ value(label),
422
+ dim(identity ? `with ${identity}` : "ad-hoc (-)"),
423
+ hardened ? dim("\xB7 hardened runtime") : ""
424
+ ].filter(Boolean).join(" ");
247
425
  options.log(
248
426
  row({
249
427
  glyph: "done",
250
428
  label: "codesign",
251
429
  labelWidth: STEP_LABEL_WIDTH,
252
- detail: identity ? `${value(label)} ${dim(`with ${identity}`)}` : `${value(label)} ${dim("ad-hoc (-)")}`
430
+ detail
253
431
  })
254
432
  );
255
433
  } catch (error) {
@@ -271,768 +449,2761 @@ var signMacAppBundleIfPossible = (appBundle, options) => {
271
449
  options.warn(dim(formatExecError(error)));
272
450
  }
273
451
  };
274
- var resolveMacCodeSigningIdentity = () => {
275
- const override = process.env.VIDRA_MACOS_CODESIGN_KEY?.trim();
276
- if (override) {
277
- return override;
452
+ var signMacDmgIfPossible = (dmgPath, options) => {
453
+ if (process.platform !== "darwin") return;
454
+ const purpose = options.purpose ?? "distribution";
455
+ const { identity, expiredSkipped } = selectMacIdentity(purpose);
456
+ warnAboutExpiredIdentities(expiredSkipped, options);
457
+ if (!identity) {
458
+ options.warn(
459
+ row({
460
+ glyph: "skip",
461
+ label: "codesign dmg",
462
+ labelWidth: STEP_LABEL_WIDTH,
463
+ detail: dim("no Developer ID identity \u2014 leaving the disk image unsigned")
464
+ })
465
+ );
466
+ return;
278
467
  }
279
468
  try {
280
- const output = execFileSync(
281
- "security",
282
- ["find-identity", "-v", "-p", "codesigning"],
283
- {
284
- stdio: ["ignore", "pipe", "pipe"],
285
- encoding: "utf8"
286
- }
469
+ execFileSync2("codesign", ["--force", "--timestamp", "--sign", identity, dmgPath], {
470
+ stdio: options.verbose ? "inherit" : "pipe"
471
+ });
472
+ options.log(
473
+ row({
474
+ glyph: "done",
475
+ label: "codesign dmg",
476
+ labelWidth: STEP_LABEL_WIDTH,
477
+ detail: `${value(path4.basename(dmgPath))} ${dim(`with ${identity}`)}`
478
+ })
287
479
  );
288
- const identities = output.split(/\r?\n/).map((line) => line.match(/"([^"]+)"/)?.[1] ?? null).filter((value2) => value2 !== null);
289
- return identities.find((identity) => identity.startsWith("Apple Development:")) ?? identities.find((identity) => identity.startsWith("Developer ID Application:")) ?? null;
290
- } catch {
291
- return null;
480
+ } catch (error) {
481
+ options.warn(
482
+ row({
483
+ glyph: "manual",
484
+ label: "codesign dmg",
485
+ labelWidth: STEP_LABEL_WIDTH,
486
+ detail: dim("could not sign the disk image")
487
+ })
488
+ );
489
+ options.warn(dim(formatExecError(error)));
292
490
  }
293
491
  };
294
- var formatExecError = (error) => {
295
- const err = error;
296
- if (Buffer.isBuffer(err.stderr)) {
297
- return err.stderr.toString();
298
- }
299
- if (typeof err.stderr === "string") {
300
- return err.stderr;
492
+ var verifyMacSignature = (target) => {
493
+ try {
494
+ const output = execFileSync2(
495
+ "codesign",
496
+ ["--verify", "--strict", "--verbose=2", target],
497
+ { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
498
+ );
499
+ return { ok: true, output: output ?? "" };
500
+ } catch (error) {
501
+ return { ok: false, output: formatExecError(error) };
301
502
  }
302
- return err.message ?? String(error);
303
503
  };
304
-
305
- // src/targets/windows.ts
306
- import path4 from "path";
307
- import os2 from "os";
308
- import fs3 from "fs-extra";
309
- import { execFileSync as execFileSync2 } from "child_process";
310
- var windowsTarget = {
311
- name: "windows",
312
- framework: "net10.0-windows10.0.19041.0",
313
- // `RuntimeIdentifierOverride` (rather than `-r`/`RuntimeIdentifier`) is the
314
- // MAUI-recommended way to set the Windows RID — it sidesteps WindowsAppSDK
315
- // issue #3337, which otherwise pulls in the wrong packaging assets.
316
- extraPublishArgs: "-p:WindowsPackageType=None -p:SelfContained=true -p:WindowsAppSDKSelfContained=true -p:RuntimeIdentifierOverride=win-x64",
317
- findBundle(publishDir, _projectName) {
318
- const preferred = path4.join(publishDir, "win-x64", "publish");
319
- if (dirContainsExe(preferred)) return preferred;
320
- return findDirWithExe(publishDir);
321
- },
322
- async package(publishOutputDir, outputDir, meta) {
323
- const outName = `${meta.projectName}-${meta.displayVersion}-windows.zip`;
324
- const outPath = path4.join(outputDir, outName);
325
- if (fs3.existsSync(outPath)) fs3.removeSync(outPath);
326
- const staging = fs3.mkdtempSync(path4.join(os2.tmpdir(), "vidra-zip-"));
327
- const stagedApp = path4.join(staging, meta.projectName);
328
- try {
329
- fs3.copySync(publishOutputDir, stagedApp);
330
- execFileSync2(
331
- "powershell",
332
- [
333
- "-NoProfile",
334
- "-NonInteractive",
335
- "-Command",
336
- `Compress-Archive -Path "${stagedApp}" -DestinationPath "${outPath}" -Force`
337
- ],
338
- { stdio: "pipe" }
339
- );
340
- } finally {
341
- fs3.removeSync(staging);
342
- }
343
- return outPath;
504
+ var assessGatekeeper = (target) => {
505
+ const type = target.endsWith(".dmg") ? "open" : "execute";
506
+ const args = ["--assess", "--type", type, "--verbose=2"];
507
+ if (type === "open") args.push("--context", "context:primary-signature");
508
+ args.push(target);
509
+ try {
510
+ const output = execFileSync2("spctl", args, {
511
+ stdio: ["ignore", "pipe", "pipe"],
512
+ encoding: "utf8"
513
+ });
514
+ return { ok: true, output: output ?? "" };
515
+ } catch (error) {
516
+ return { ok: false, output: formatExecError(error) };
344
517
  }
345
518
  };
346
- var dirContainsExe = (dir) => fs3.existsSync(dir) && fs3.readdirSync(dir, { withFileTypes: true }).some((e) => e.isFile() && e.name.toLowerCase().endsWith(".exe"));
347
- var findDirWithExe = (root) => {
348
- if (!fs3.existsSync(root)) return null;
349
- let fallback = null;
350
- const walk = (dir) => {
351
- if (dirContainsExe(dir)) {
352
- if (path4.basename(dir) === "publish") return dir;
353
- fallback ??= dir;
354
- }
355
- for (const e of fs3.readdirSync(dir, { withFileTypes: true })) {
356
- if (e.isDirectory()) {
357
- const found = walk(path4.join(dir, e.name));
358
- if (found) return found;
359
- }
360
- }
361
- return null;
519
+ var REQUIRED_MAC_ENTITLEMENTS = [
520
+ "com.apple.security.cs.allow-jit",
521
+ "com.apple.security.cs.allow-unsigned-executable-memory",
522
+ "com.apple.security.cs.disable-library-validation"
523
+ ];
524
+ var inspectMacHardening = (target) => {
525
+ const flags = runCodesignRead(["-d", "--verbose=2", target]);
526
+ const entitlementsXml = runCodesignRead(["-d", "--entitlements", "-", "--xml", target]) || runCodesignRead(["-d", "--entitlements", "-", target]);
527
+ const hardened = /CodeDirectory[^\n]*flags=[^\n]*runtime/.test(flags);
528
+ const entitlements = [...entitlementsXml.matchAll(/<key>([^<]+)<\/key>/g)].map(
529
+ (m) => m[1]
530
+ );
531
+ const missing = REQUIRED_MAC_ENTITLEMENTS.filter(
532
+ (key) => !entitlementsXml.includes(key)
533
+ );
534
+ return {
535
+ hardened,
536
+ entitlements,
537
+ missing,
538
+ ok: hardened && missing.length === 0,
539
+ output: `${flags}
540
+ ${entitlementsXml}`
362
541
  };
363
- return walk(root) ?? fallback;
364
542
  };
365
-
366
- // src/doctor.ts
367
- import { execFileSync as execFileSync3 } from "child_process";
368
- import prompts from "prompts";
369
- var DOTNET = process.platform === "win32" ? "dotnet.exe" : "dotnet";
370
- var MAUI_DOCS = "https://learn.microsoft.com/dotnet/maui/get-started/installation";
371
- var INSTALL_NET_10_FIX = "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download";
372
- var bufToStr = (v) => v == null ? "" : Buffer.isBuffer(v) ? v.toString() : v;
373
- var run = (cmd, args) => {
543
+ var runCodesignRead = (args) => {
544
+ const result = spawnSync("codesign", args, { encoding: "utf8" });
545
+ return `${result.stdout ?? ""}${result.stderr ?? ""}`;
546
+ };
547
+ var selectMacIdentity = (purpose = "development") => {
548
+ const override = process.env.VIDRA_MACOS_CODESIGN_KEY?.trim();
549
+ if (override) return { identity: override, expiredSkipped: [] };
550
+ const all = listCodeSigningIdentities();
551
+ const expired = new Set(listExpiredCodeSigningIdentities(all));
552
+ const usable = all.filter((id) => !expired.has(id));
553
+ const pick = (list) => (purpose === "distribution" ? list.find((id) => id.startsWith(DEVELOPER_ID_PREFIX)) ?? list.find((id) => id.startsWith(APPLE_DEVELOPMENT_PREFIX)) : list.find((id) => id.startsWith(APPLE_DEVELOPMENT_PREFIX)) ?? list.find((id) => id.startsWith(DEVELOPER_ID_PREFIX))) ?? null;
554
+ return {
555
+ identity: pick(usable),
556
+ // Only worth mentioning an expired certificate if it would otherwise have
557
+ // been the one we chose.
558
+ expiredSkipped: pick([...expired]) ? [pick([...expired])] : []
559
+ };
560
+ };
561
+ var resolveMacCodeSigningIdentity = (purpose = "development") => selectMacIdentity(purpose).identity;
562
+ var listCodeSigningIdentities = () => runFindIdentity(["find-identity", "-p", "codesigning"]);
563
+ var listValidCodeSigningIdentities = () => runFindIdentity(["find-identity", "-v", "-p", "codesigning"]);
564
+ var runFindIdentity = (args) => {
374
565
  try {
375
- const stdout = execFileSync3(cmd, args, {
376
- encoding: "utf-8",
377
- stdio: ["ignore", "pipe", "pipe"]
566
+ const output = execFileSync2("security", args, {
567
+ stdio: ["ignore", "pipe", "pipe"],
568
+ encoding: "utf8"
378
569
  });
379
- return { found: true, ok: true, stdout: stdout ?? "", stderr: "" };
380
- } catch (e) {
381
- const err = e;
382
- return {
383
- found: err.code !== "ENOENT",
384
- ok: false,
385
- stdout: bufToStr(err.stdout),
386
- stderr: bufToStr(err.stderr)
387
- };
570
+ return output.split(/\r?\n/).map((line) => line.match(/"([^"]+)"/)?.[1] ?? null).filter((v) => v !== null);
571
+ } catch {
572
+ return [];
388
573
  }
389
574
  };
390
- var splitLines = (text) => text.split(/\r?\n/);
391
- var matchesAny = (text, patterns) => patterns.some((pattern) => pattern.test(text));
392
- var NET_10_VERSION = /^10\./;
393
- var WORKLOAD_SET_VERSION_LINE = /Workload version:\s*([\w.-]+)/i;
394
- var MAUI_WORKLOAD = /\bmaui\b/i;
395
- var COMMAND_LINE_TOOLS_PATH = /CommandLineTools/i;
396
- var hasNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).some((line) => NET_10_VERSION.test(line.trim()));
397
- var newestNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).map((line) => line.trim().split(/\s+/)[0]).filter((version) => NET_10_VERSION.test(version)).sort(
398
- (a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" })
399
- ).pop();
400
- var outputMentionsMaui = (workloadListOutput) => MAUI_WORKLOAD.test(workloadListOutput);
401
- var workloadSetVersion = (workloadListOutput) => workloadListOutput.match(WORKLOAD_SET_VERSION_LINE)?.[1];
402
- var MIN_MACCATALYST_WATCH_WORKLOAD_SET = [10, 0, 203];
403
- var MIN_MACCATALYST_WATCH_SDK = [10, 0, 300];
404
- var meetsMinimumVersion = (version, minimum) => {
405
- const segments = version.split(".").map((s) => Number.parseInt(s, 10));
406
- if (segments.slice(0, minimum.length).some(Number.isNaN)) return false;
407
- for (let i = 0; i < minimum.length; i++) {
408
- const segment = segments[i] ?? 0;
409
- if (segment !== minimum[i]) return segment > minimum[i];
410
- }
411
- return true;
412
- };
413
- var workloadSetSupportsCSharpHotReload = (version) => meetsMinimumVersion(version, MIN_MACCATALYST_WATCH_WORKLOAD_SET);
414
- var sdkSupportsCSharpHotReload = (version) => meetsMinimumVersion(version, MIN_MACCATALYST_WATCH_SDK);
415
- var macCatalystHotReloadBlocker = () => {
416
- const sdkRes = run(DOTNET, ["--version"]);
417
- if (!sdkRes.found) return null;
418
- const sdkVersion = sdkRes.stdout.trim();
419
- if (sdkVersion && !sdkSupportsCSharpHotReload(sdkVersion)) {
420
- return {
421
- reason: `C# hot reload needs the .NET SDK 10.0.300+ \u2014 found ${sdkVersion}`,
422
- fix: INSTALL_NET_10_FIX
423
- };
424
- }
425
- const res = run(DOTNET, ["workload", "list"]);
426
- if (!res.found) return null;
427
- const version = workloadSetVersion(res.stdout);
428
- if (!version || workloadSetSupportsCSharpHotReload(version)) return null;
429
- return {
430
- reason: `C# hot reload needs workload set 10.0.203+ \u2014 found ${version}`,
431
- fix: "dotnet workload update"
432
- };
575
+ var listExpiredCodeSigningIdentities = (identities = listCodeSigningIdentities()) => {
576
+ const chainValid = new Set(listValidCodeSigningIdentities());
577
+ return identities.filter((id) => {
578
+ if (chainValid.has(id)) return false;
579
+ const notAfter = certificateNotAfter(id);
580
+ return notAfter !== null && notAfter.getTime() < Date.now();
581
+ });
433
582
  };
434
- var MISSING_WORKLOAD_SIGNATURES = [
435
- /NETSDK1147/i,
436
- /workloads?\s+must\s+be\s+installed/i,
437
- /maui-maccatalyst/i,
438
- /maui-windows/i,
439
- /to\s+install\s+the\s+.*workload/i
440
- ];
441
- var MISSING_XCODE_SIGNATURES = [
442
- /valid\s+Xcode\s+installation\s+was\s+not\s+found/i,
443
- /could\s+not\s+find\s+a\s+valid\s+Xcode\s+app\s+bundle/i,
444
- /macios-missing-xcode/i
445
- ];
446
- var OUTDATED_XCODE_SIGNATURES = [
447
- /error\s+MT0180/i,
448
- /requires\s+the\s+MacCatalyst\s+\S+\s+SDK\s+\(shipped\s+with\s+Xcode/i
449
- ];
450
- var looksLikeMissingWorkload = (output) => matchesAny(output, MISSING_WORKLOAD_SIGNATURES);
451
- var looksLikeMissingXcode = (output) => matchesAny(output, MISSING_XCODE_SIGNATURES);
452
- var looksLikeXcodeTooOld = (output) => matchesAny(output, OUTDATED_XCODE_SIGNATURES);
453
- var checkDotnetSdk = () => {
454
- const name = ".NET SDK";
455
- const res = run(DOTNET, ["--list-sdks"]);
456
- if (!res.found) {
457
- return {
458
- name,
459
- status: "missing",
460
- detail: "`dotnet` was not found on your PATH",
461
- fix: INSTALL_NET_10_FIX
462
- };
583
+ var certificateNotAfter = (identity) => {
584
+ const pem = spawnSync("security", ["find-certificate", "-c", identity, "-p"], {
585
+ encoding: "utf8"
586
+ });
587
+ if (pem.status !== 0 || !pem.stdout) return null;
588
+ const parsed = spawnSync("openssl", ["x509", "-noout", "-enddate"], {
589
+ input: pem.stdout,
590
+ encoding: "utf8"
591
+ });
592
+ const raw = parsed.stdout?.match(/notAfter=(.+)/)?.[1]?.trim();
593
+ if (!raw) return null;
594
+ const date = new Date(raw);
595
+ return Number.isNaN(date.getTime()) ? null : date;
596
+ };
597
+ var hasDeveloperIdIdentity = () => {
598
+ const all = listCodeSigningIdentities();
599
+ const expired = new Set(listExpiredCodeSigningIdentities(all));
600
+ return all.some((id) => id.startsWith(DEVELOPER_ID_PREFIX) && !expired.has(id));
601
+ };
602
+ var warnAboutExpiredIdentities = (expired, options) => {
603
+ for (const identity of expired) {
604
+ options.warn(
605
+ row({
606
+ glyph: "manual",
607
+ label: "codesign",
608
+ labelWidth: STEP_LABEL_WIDTH,
609
+ detail: dim(`skipping expired certificate: ${identity}`)
610
+ })
611
+ );
463
612
  }
464
- if (!res.ok && !res.stdout) {
465
- return { name, status: "unknown", detail: "could not run `dotnet --list-sdks`" };
613
+ };
614
+ var warnAboutDistributionIdentity = (identity, options) => {
615
+ if (identity === null) {
616
+ options.warn(
617
+ row({
618
+ glyph: "manual",
619
+ label: "codesign",
620
+ labelWidth: STEP_LABEL_WIDTH,
621
+ detail: dim(
622
+ "no signing identity \u2014 the app will be ad-hoc signed and Gatekeeper will block it on other Macs"
623
+ )
624
+ })
625
+ );
626
+ return;
466
627
  }
467
- if (hasNet10Sdk(res.stdout)) {
468
- const newest = newestNet10Sdk(res.stdout);
469
- return { name, status: "ok", detail: newest ? `found ${newest}` : "found 10.x" };
628
+ if (!identity.startsWith(DEVELOPER_ID_PREFIX) && !process.env.VIDRA_MACOS_CODESIGN_KEY) {
629
+ options.warn(
630
+ row({
631
+ glyph: "manual",
632
+ label: "codesign",
633
+ labelWidth: STEP_LABEL_WIDTH,
634
+ detail: dim(
635
+ `using ${identity} \u2014 development certificates cannot be notarized; a "Developer ID Application" certificate is required to distribute`
636
+ )
637
+ })
638
+ );
470
639
  }
471
- return {
472
- name,
473
- status: "missing",
474
- detail: "no 10.x SDK installed",
475
- fix: INSTALL_NET_10_FIX
640
+ };
641
+ var runCodesign = (target, identity, opts, options) => {
642
+ const args = ["--force", "--timestamp", "--sign", identity];
643
+ if (opts.hardened) args.push("--options", "runtime");
644
+ if (opts.entitlements) args.push("--entitlements", opts.entitlements);
645
+ args.push(target);
646
+ execFileSync2("codesign", args, {
647
+ stdio: options.verbose ? "inherit" : "pipe"
648
+ });
649
+ };
650
+ var findNestedMachOPayloads = (appBundle) => {
651
+ const found = [];
652
+ const walk = (dir) => {
653
+ let entries;
654
+ try {
655
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
656
+ } catch {
657
+ return;
658
+ }
659
+ for (const entry of entries) {
660
+ const full = path4.join(dir, entry.name);
661
+ if (entry.isDirectory()) {
662
+ if (entry.name.endsWith(".framework")) {
663
+ found.push(full);
664
+ continue;
665
+ }
666
+ walk(full);
667
+ } else if (entry.isFile() && /\.(dylib|so)$/.test(entry.name)) {
668
+ found.push(full);
669
+ }
670
+ }
476
671
  };
672
+ walk(appBundle);
673
+ return found.sort((a, b) => b.split(path4.sep).length - a.split(path4.sep).length);
477
674
  };
478
- var checkMauiWorkload = (workloadList) => {
479
- const name = ".NET MAUI workload";
480
- if (!workloadList) {
481
- return { name, status: "unknown", detail: "requires the .NET SDK first" };
675
+ var formatExecError = (error) => {
676
+ const err = error;
677
+ const stderr = Buffer.isBuffer(err.stderr) ? err.stderr.toString() : err.stderr;
678
+ if (stderr) return stderr;
679
+ const stdout = Buffer.isBuffer(err.stdout) ? err.stdout.toString() : err.stdout;
680
+ if (stdout) return stdout;
681
+ return err.message ?? String(error);
682
+ };
683
+
684
+ // src/windows-signing.ts
685
+ import path5 from "path";
686
+ import fs5 from "fs-extra";
687
+ import { execFileSync as execFileSync3 } from "child_process";
688
+ var DEFAULT_TIMESTAMP_URL = "http://timestamp.digicert.com";
689
+ var resolveWindowsSigningConfig = () => {
690
+ const thumbprint = process.env.VIDRA_WINDOWS_CERT_THUMBPRINT?.trim();
691
+ if (thumbprint) return { mode: "thumbprint", thumbprint };
692
+ const pfxPath = process.env.VIDRA_WINDOWS_CERT_PATH?.trim();
693
+ if (pfxPath) {
694
+ const password = process.env.VIDRA_WINDOWS_CERT_PASSWORD?.trim() ?? null;
695
+ return { mode: "pfx", pfxPath, password };
482
696
  }
483
- if (!workloadList.found) {
484
- return { name, status: "unknown", detail: "could not query workloads" };
697
+ return null;
698
+ };
699
+ var timestampUrl = () => process.env.VIDRA_WINDOWS_TIMESTAMP_URL?.trim() || DEFAULT_TIMESTAMP_URL;
700
+ var buildSignToolArgs = (config, targets) => {
701
+ const args = ["sign", "/fd", "SHA256", "/tr", timestampUrl(), "/td", "SHA256"];
702
+ if (config.mode === "pfx") {
703
+ args.push("/f", config.pfxPath);
704
+ if (config.password) args.push("/p", config.password);
705
+ } else {
706
+ args.push("/sha1", config.thumbprint);
485
707
  }
486
- if (outputMentionsMaui(workloadList.stdout)) {
487
- return { name, status: "ok", detail: "installed" };
708
+ return [...args, ...targets];
709
+ };
710
+ var signWindowsBinariesIfPossible = (targets, options) => {
711
+ if (process.platform !== "win32") return false;
712
+ if (targets.length === 0) return false;
713
+ const config = resolveWindowsSigningConfig();
714
+ if (!config) {
715
+ options.log(
716
+ row({
717
+ glyph: "skip",
718
+ label: "authenticode",
719
+ labelWidth: STEP_LABEL_WIDTH,
720
+ detail: dim(
721
+ "no certificate configured \u2014 shipping unsigned (SmartScreen will warn)"
722
+ )
723
+ })
724
+ );
725
+ return false;
726
+ }
727
+ const signtool = resolveSignTool();
728
+ if (!signtool) {
729
+ options.warn(
730
+ row({
731
+ glyph: "manual",
732
+ label: "authenticode",
733
+ labelWidth: STEP_LABEL_WIDTH,
734
+ detail: dim("signtool.exe not found \u2014 install the Windows SDK")
735
+ })
736
+ );
737
+ return false;
738
+ }
739
+ try {
740
+ execFileSync3(signtool, buildSignToolArgs(config, targets), {
741
+ stdio: options.verbose ? "inherit" : "pipe"
742
+ });
743
+ options.log(
744
+ row({
745
+ glyph: "done",
746
+ label: "authenticode",
747
+ labelWidth: STEP_LABEL_WIDTH,
748
+ detail: `${value(`${targets.length} file(s)`)} ${dim(`timestamped via ${timestampUrl()}`)}`
749
+ })
750
+ );
751
+ return true;
752
+ } catch (error) {
753
+ options.warn(
754
+ row({
755
+ glyph: "manual",
756
+ label: "authenticode",
757
+ labelWidth: STEP_LABEL_WIDTH,
758
+ detail: dim("signing failed; shipping unsigned")
759
+ })
760
+ );
761
+ options.warn(dim(formatExecError(error)));
762
+ return false;
488
763
  }
489
- return {
490
- name,
491
- status: "missing",
492
- detail: "not installed",
493
- fix: "dotnet workload install maui"
494
- };
495
764
  };
496
- var checkCSharpHotReload = (workloadList) => {
497
- const name = "C# hot reload";
498
- if (!workloadList) {
499
- return { name, status: "unknown", detail: "requires the .NET SDK first" };
765
+ var verifyWindowsSignature = (target) => {
766
+ const signtool = resolveSignTool();
767
+ if (!signtool) {
768
+ return { ok: false, untrustedRoot: false, output: "signtool.exe not found" };
500
769
  }
501
- if (process.platform !== "darwin") {
502
- return { name, status: "ok", detail: "dotnet watch supported" };
770
+ try {
771
+ const output = execFileSync3(signtool, ["verify", "/pa", "/v", target], {
772
+ stdio: ["ignore", "pipe", "pipe"],
773
+ encoding: "utf8"
774
+ });
775
+ return { ok: true, untrustedRoot: false, output: output ?? "" };
776
+ } catch (error) {
777
+ const output = formatExecError(error);
778
+ const flat = output.replace(/\s+/g, " ");
779
+ const untrustedRoot = /0x800B0109/i.test(flat) || /terminated in a root certificate which is not trusted/i.test(flat);
780
+ return { ok: untrustedRoot, untrustedRoot, output };
503
781
  }
504
- const sdkVersion = run(DOTNET, ["--version"]).stdout.trim();
505
- if (sdkVersion && !sdkSupportsCSharpHotReload(sdkVersion)) {
506
- return {
507
- name,
508
- status: "unknown",
509
- detail: `SDK ${sdkVersion} \u2014 its dotnet-watch crashes Mac Catalyst apps on launch (needs 10.0.300+) \u2014 vidra dev falls back to a classic launch`,
510
- fix: INSTALL_NET_10_FIX
511
- };
782
+ };
783
+ var resolveSignTool = () => {
784
+ const override = process.env.VIDRA_SIGNTOOL_PATH?.trim();
785
+ if (override) return fs5.existsSync(override) ? override : null;
786
+ try {
787
+ execFileSync3("signtool", ["/?"], { stdio: "ignore" });
788
+ return "signtool";
789
+ } catch {
512
790
  }
513
- const version = workloadSetVersion(workloadList.stdout);
514
- if (!version) {
515
- return {
516
- name,
517
- status: "unknown",
518
- detail: "could not read the workload set version"
519
- };
791
+ const roots = [
792
+ process.env["ProgramFiles(x86)"],
793
+ process.env.ProgramFiles
794
+ ].filter((r) => !!r);
795
+ for (const root of roots) {
796
+ const binDir = path5.join(root, "Windows Kits", "10", "bin");
797
+ if (!fs5.existsSync(binDir)) continue;
798
+ const versions = fs5.readdirSync(binDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().reverse();
799
+ for (const version of versions) {
800
+ for (const arch of ["x64", "x86"]) {
801
+ const candidate = path5.join(binDir, version, arch, "signtool.exe");
802
+ if (fs5.existsSync(candidate)) return candidate;
803
+ }
804
+ }
520
805
  }
521
- if (workloadSetSupportsCSharpHotReload(version)) {
522
- return { name, status: "ok", detail: `workload set ${version}` };
806
+ return null;
807
+ };
808
+ var findPrimaryExecutable = (publishDir, projectName) => {
809
+ if (!fs5.existsSync(publishDir)) return null;
810
+ const exes = fs5.readdirSync(publishDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".exe")).map((e) => e.name);
811
+ const preferred = exes.find((n) => n.toLowerCase() === `${projectName}.host.exe`.toLowerCase()) ?? exes.find((n) => n.toLowerCase().startsWith(projectName.toLowerCase()));
812
+ const chosen = preferred ?? exes[0];
813
+ return chosen ? path5.join(publishDir, chosen) : null;
814
+ };
815
+
816
+ // src/native-update.ts
817
+ var RELEASE_DIR = path6.join("dist", "release");
818
+ var entitlementsCopyName = (packId) => `${packId}.entitlements`;
819
+ var NativeUpdateConfigError = class extends Error {
820
+ };
821
+ var resolveNativeUpdateSettings = (opts) => {
822
+ const config = opts.config ?? {};
823
+ if (config.enabled === false) {
824
+ throw new NativeUpdateConfigError(
825
+ "vidra.updates.native.enabled is false: remove --native-update, or set it to true"
826
+ );
827
+ }
828
+ const packId = config.packId ?? readApplicationId(opts.csprojPath) ?? opts.projectName;
829
+ const packTitle = readApplicationTitle(opts.csprojPath) ?? opts.projectName;
830
+ if (!config.feedUrl) {
831
+ return { packId, packTitle, packVersion: opts.version, feedUrl: null, channel: config.channel ?? null };
523
832
  }
524
833
  return {
525
- name,
526
- status: "unknown",
527
- detail: `workload set ${version} predates Mac Catalyst dotnet-watch support (needs 10.0.203+) \u2014 vidra dev falls back to a classic launch`,
528
- fix: "dotnet workload update"
834
+ packId,
835
+ packTitle,
836
+ packVersion: opts.version,
837
+ feedUrl: config.feedUrl,
838
+ channel: config.channel ?? null
529
839
  };
530
840
  };
531
- var checkXcode = () => {
532
- const name = "Xcode";
533
- const res = run("xcode-select", ["-p"]);
534
- if (!res.found || !res.ok) {
535
- return {
536
- name,
537
- status: "missing",
538
- detail: "not found",
539
- fix: "Install Xcode from the App Store"
540
- };
841
+ var readApplicationId = (csprojPath) => readCsprojProperty(csprojPath, "ApplicationId");
842
+ var readApplicationTitle = (csprojPath) => readCsprojProperty(csprojPath, "ApplicationTitle");
843
+ var readCsprojProperty = (csprojPath, name) => {
844
+ if (!fs6.existsSync(csprojPath)) return null;
845
+ const xml = fs6.readFileSync(csprojPath, "utf-8");
846
+ return new RegExp(`<${name}>([^<]+)</${name}>`).exec(xml)?.[1]?.trim() || null;
847
+ };
848
+ var windowsSignParams = () => {
849
+ const config = resolveWindowsSigningConfig();
850
+ if (!config) return null;
851
+ const parts = ["/fd", "SHA256", "/tr", timestampUrl(), "/td", "SHA256"];
852
+ if (config.mode === "pfx") {
853
+ parts.push("/f", config.pfxPath);
854
+ if (config.password) parts.push("/p", config.password);
855
+ } else {
856
+ parts.push("/sha1", config.thumbprint);
541
857
  }
542
- const devDir = res.stdout.trim();
543
- if (COMMAND_LINE_TOOLS_PATH.test(devDir)) {
544
- return {
545
- name,
546
- status: "missing",
547
- detail: "only Command Line Tools detected (Mac Catalyst needs full Xcode)",
548
- fix: "Install Xcode, then: sudo xcode-select -s /Applications/Xcode.app"
549
- };
858
+ return parts.join(" ");
859
+ };
860
+ var NativeUpdateError = class extends Error {
861
+ constructor(message, detail = "") {
862
+ super(message);
863
+ this.detail = detail;
550
864
  }
551
- return { name, status: "ok", detail: devDir };
865
+ detail;
552
866
  };
553
- var isMauiWorkloadInstalled = () => outputMentionsMaui(run(DOTNET, ["workload", "list"]).stdout);
554
- var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
555
- var collectRequirements = (opts = {}) => {
556
- const dotnet = checkDotnetSdk();
557
- const workloadList = dotnet.status === "ok" ? run(DOTNET, ["workload", "list"]) : null;
558
- const reqs = [
559
- dotnet,
560
- checkMauiWorkload(workloadList),
561
- checkCSharpHotReload(workloadList)
562
- ];
563
- if (opts.includeXcode ?? process.platform === "darwin") {
564
- reqs.push(checkXcode());
867
+ var runNativeUpdate = (opts) => {
868
+ const vpk = resolveVpk();
869
+ if (!vpk) {
870
+ throw new NativeUpdateError(
871
+ "vpk is not installed",
872
+ "dotnet tool install -g vpk"
873
+ );
565
874
  }
566
- return reqs;
875
+ const releaseDir = path6.join(opts.projectRoot, RELEASE_DIR);
876
+ fs6.ensureDirSync(releaseDir);
877
+ const merged = mergeFromLiveFeed(vpk, opts.settings, releaseDir, opts.io);
878
+ const mainExe = opts.target === "macos" ? findMacMainExe(opts.packDir) : findWindowsMainExe(opts.packDir);
879
+ if (!mainExe) {
880
+ throw new NativeUpdateError(
881
+ `could not find the app executable inside ${opts.packDir}`
882
+ );
883
+ }
884
+ const args = packArgs({
885
+ packId: opts.settings.packId,
886
+ packVersion: opts.settings.packVersion,
887
+ packTitle: opts.settings.packTitle,
888
+ packDir: opts.packDir,
889
+ mainExe,
890
+ outputDir: releaseDir,
891
+ channel: opts.settings.channel,
892
+ ...opts.target === "macos" ? {
893
+ // Velopack has to own signing: it adds files to a sealed bundle
894
+ // *after* the build signed it, which breaks the seal. Handing it the
895
+ // same identity and entitlements puts the packed bundle back on the
896
+ // baseline's row: Developer ID authority, hardened runtime, all
897
+ // three JIT entitlements, strict and deep verification passing.
898
+ signAppIdentity: resolveMacCodeSigningIdentity("distribution"),
899
+ signEntitlements: opts.entitlements ? copyEntitlementsForVpk(opts.entitlements, opts.settings.packId) : null,
900
+ keychain: process.env.VIDRA_MACOS_KEYCHAIN?.trim() || null
901
+ } : { signParams: windowsSignParams() }
902
+ });
903
+ const result = runVpk(vpk, args, { cwd: opts.projectRoot, verbose: opts.io.verbose });
904
+ if (!result.ok) {
905
+ if (result.alreadyReleased) {
906
+ throw new NativeUpdateError(
907
+ `${opts.settings.packVersion} is already in the feed, nothing was written`,
908
+ "bump the version in package.json (npm version patch), or drop --native-update to rebuild the installer only"
909
+ );
910
+ }
911
+ throw new NativeUpdateError("vpk pack failed", result.output);
912
+ }
913
+ return { releaseDir, outputs: findVpkOutputs(releaseDir), merged };
567
914
  };
568
- var STATUS_GLYPH = {
569
- ok: "done",
570
- missing: "error",
571
- unknown: "manual"
915
+ var mergeFromLiveFeed = (vpk, settings, releaseDir, io) => {
916
+ if (!settings.feedUrl) return "no-feed";
917
+ const result = runVpk(
918
+ vpk,
919
+ downloadArgs({
920
+ feedUrl: settings.feedUrl,
921
+ outputDir: releaseDir,
922
+ channel: settings.channel
923
+ }),
924
+ { verbose: io.verbose }
925
+ );
926
+ if (result.ok) return "merged";
927
+ io.warn(lastLines(result.output, 4));
928
+ return "empty-feed";
929
+ };
930
+ var copyEntitlementsForVpk = (entitlements, packId) => {
931
+ const dir = fs6.mkdtempSync(path6.join(os2.tmpdir(), "vidra-vpk-"));
932
+ const target = path6.join(dir, entitlementsCopyName(packId));
933
+ fs6.copySync(entitlements, target);
934
+ return target;
935
+ };
936
+ var findWindowsMainExe = (publishDir) => {
937
+ const entries = fs6.readdirSync(publishDir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".exe")).map((e) => e.name);
938
+ return entries.find((e) => e.endsWith(".Host.exe")) ?? entries.find((e) => e !== "createdump.exe") ?? null;
939
+ };
940
+ var extractPackedApp = (portableZip) => {
941
+ const staging = fs6.mkdtempSync(path6.join(os2.tmpdir(), "vidra-packed-"));
942
+ execFileSync4("ditto", ["-x", "-k", portableZip, staging], { stdio: "pipe" });
943
+ const found = findAppBundle(staging);
944
+ if (!found) {
945
+ throw new NativeUpdateError(`no .app inside ${path6.basename(portableZip)}`);
946
+ }
947
+ return found;
948
+ };
949
+ var findAppBundle = (root, depth = 0) => {
950
+ if (depth > 2 || !fs6.existsSync(root)) return null;
951
+ for (const entry of fs6.readdirSync(root, { withFileTypes: true })) {
952
+ if (!entry.isDirectory()) continue;
953
+ const full = path6.join(root, entry.name);
954
+ if (entry.name.endsWith(".app")) return full;
955
+ const nested = findAppBundle(full, depth + 1);
956
+ if (nested) return nested;
957
+ }
958
+ return null;
959
+ };
960
+ var lastLines = (text, count) => text.trim().split(/\r?\n/).slice(-count).join("\n");
961
+
962
+ // src/project.ts
963
+ import path7 from "path";
964
+ import fs7 from "fs-extra";
965
+ var detectPlatform = () => {
966
+ switch (process.platform) {
967
+ case "darwin":
968
+ return "macos";
969
+ case "win32":
970
+ return "windows";
971
+ case "linux":
972
+ return "linux";
973
+ default:
974
+ return "macos";
975
+ }
976
+ };
977
+ var detectProject = (cwd) => {
978
+ const project = tryDetectProject(cwd);
979
+ if (project) return project;
980
+ console.error(
981
+ row({
982
+ glyph: "error",
983
+ detail: dim(
984
+ "Could not detect Vidra project. Run this command from your project root."
985
+ )
986
+ })
987
+ );
988
+ console.error(
989
+ footer(dim("expected: package.json, ui/, src/<Name>.Host/<Name>.Host.csproj"))
990
+ );
991
+ process.exit(1);
992
+ };
993
+ var tryDetectProject = (cwd) => {
994
+ let dir = cwd;
995
+ while (true) {
996
+ const uiDir = path7.join(dir, "ui");
997
+ const srcDir = path7.join(dir, "src");
998
+ const pkgJson = path7.join(dir, "package.json");
999
+ if (fs7.existsSync(pkgJson) && fs7.existsSync(uiDir) && fs7.existsSync(srcDir)) {
1000
+ const csproj = findHostCsproj(srcDir);
1001
+ if (csproj) {
1002
+ const projectName = path7.basename(csproj.dir).replace(/\.Host$/, "");
1003
+ const displayVersion = resolveAppVersionOrDefault(dir, csproj.path);
1004
+ return {
1005
+ root: dir,
1006
+ hostDir: csproj.dir,
1007
+ csprojPath: csproj.path,
1008
+ projectName,
1009
+ displayVersion,
1010
+ uiDir
1011
+ };
1012
+ }
1013
+ }
1014
+ const parent = path7.dirname(dir);
1015
+ if (parent === dir) break;
1016
+ dir = parent;
1017
+ }
1018
+ return null;
1019
+ };
1020
+ var findHostCsproj = (srcDir) => {
1021
+ if (!fs7.existsSync(srcDir)) return null;
1022
+ for (const entry of fs7.readdirSync(srcDir, { withFileTypes: true })) {
1023
+ if (entry.isDirectory() && entry.name.endsWith(".Host")) {
1024
+ const csprojPath = path7.join(
1025
+ srcDir,
1026
+ entry.name,
1027
+ `${entry.name}.csproj`
1028
+ );
1029
+ if (fs7.existsSync(csprojPath)) {
1030
+ return { dir: path7.join(srcDir, entry.name), path: csprojPath };
1031
+ }
1032
+ }
1033
+ }
1034
+ return null;
1035
+ };
1036
+
1037
+ // src/targets/macos.ts
1038
+ import path8 from "path";
1039
+ import fs8 from "fs-extra";
1040
+ import { execSync as execSync2 } from "child_process";
1041
+ import os3 from "os";
1042
+ var macosTarget = {
1043
+ name: "macos",
1044
+ framework: "net10.0-maccatalyst",
1045
+ extraPublishArgs: "-p:CreatePackage=false",
1046
+ findBundle(publishDir, _projectName) {
1047
+ if (!fs8.existsSync(publishDir)) return null;
1048
+ for (const entry of fs8.readdirSync(publishDir, { withFileTypes: true })) {
1049
+ if (entry.name.endsWith(".app") && entry.isDirectory()) {
1050
+ return path8.join(publishDir, entry.name);
1051
+ }
1052
+ }
1053
+ return null;
1054
+ },
1055
+ async package(appPath, outputDir, meta) {
1056
+ const dmgName = `${meta.projectName}-${meta.displayVersion}-macos.dmg`;
1057
+ const dmgPath = path8.join(outputDir, dmgName);
1058
+ const volName = meta.projectName;
1059
+ const staging = fs8.mkdtempSync(path8.join(os3.tmpdir(), "vidra-dmg-"));
1060
+ try {
1061
+ fs8.copySync(appPath, path8.join(staging, path8.basename(appPath)));
1062
+ fs8.symlinkSync("/Applications", path8.join(staging, "Applications"));
1063
+ if (fs8.existsSync(dmgPath)) {
1064
+ fs8.removeSync(dmgPath);
1065
+ }
1066
+ execSync2(
1067
+ `hdiutil create -volname "${volName}" -srcfolder "${staging}" -ov -format UDZO "${dmgPath}"`,
1068
+ { stdio: "pipe" }
1069
+ );
1070
+ } finally {
1071
+ fs8.removeSync(staging);
1072
+ }
1073
+ return dmgPath;
1074
+ }
1075
+ };
1076
+
1077
+ // src/notarize.ts
1078
+ import path9 from "path";
1079
+ import { execFileSync as execFileSync5 } from "child_process";
1080
+ var resolveNotaryCredentials = () => {
1081
+ const profile = process.env.VIDRA_NOTARY_PROFILE?.trim();
1082
+ if (profile) return { mode: "profile", profile };
1083
+ const appleId = process.env.VIDRA_APPLE_ID?.trim();
1084
+ const teamId = process.env.VIDRA_TEAM_ID?.trim();
1085
+ const password = process.env.VIDRA_APP_PASSWORD?.trim();
1086
+ if (appleId && teamId && password) {
1087
+ return { mode: "apple-id", appleId, teamId, password };
1088
+ }
1089
+ return null;
1090
+ };
1091
+ var notaryCredentialArgs = (creds) => creds.mode === "profile" ? ["--keychain-profile", creds.profile] : [
1092
+ "--apple-id",
1093
+ creds.appleId,
1094
+ "--team-id",
1095
+ creds.teamId,
1096
+ "--password",
1097
+ creds.password
1098
+ ];
1099
+ var notarizeAndStaple = (artifactPath, options) => {
1100
+ if (process.platform !== "darwin") {
1101
+ return { status: "skipped", reason: "not macOS" };
1102
+ }
1103
+ const creds = resolveNotaryCredentials();
1104
+ if (!creds) {
1105
+ return {
1106
+ status: "skipped",
1107
+ reason: "no notary credentials (set VIDRA_NOTARY_PROFILE, or VIDRA_APPLE_ID + VIDRA_TEAM_ID + VIDRA_APP_PASSWORD)"
1108
+ };
1109
+ }
1110
+ const label = path9.basename(artifactPath);
1111
+ let submitOutput;
1112
+ try {
1113
+ submitOutput = execFileSync5(
1114
+ "xcrun",
1115
+ ["notarytool", "submit", artifactPath, ...notaryCredentialArgs(creds), "--wait"],
1116
+ { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
1117
+ );
1118
+ } catch (error) {
1119
+ const output = formatExecError(error);
1120
+ reportNotaryFailure(output, creds, options);
1121
+ return { status: "failed", reason: output };
1122
+ }
1123
+ if (options.verbose) options.log(dim(submitOutput.trim()));
1124
+ if (!/status:\s*Accepted/i.test(submitOutput)) {
1125
+ reportNotaryFailure(submitOutput, creds, options);
1126
+ return { status: "failed", reason: "notary service did not accept the submission" };
1127
+ }
1128
+ try {
1129
+ execFileSync5("xcrun", ["stapler", "staple", artifactPath], {
1130
+ stdio: options.verbose ? "inherit" : "pipe"
1131
+ });
1132
+ } catch (error) {
1133
+ const output = formatExecError(error);
1134
+ options.warn(
1135
+ row({
1136
+ glyph: "error",
1137
+ label: "staple",
1138
+ labelWidth: STEP_LABEL_WIDTH,
1139
+ detail: dim("notarized, but the ticket could not be stapled")
1140
+ })
1141
+ );
1142
+ options.warn(dim(output));
1143
+ return { status: "failed", reason: output };
1144
+ }
1145
+ options.log(
1146
+ row({
1147
+ glyph: "done",
1148
+ label: "notarize",
1149
+ labelWidth: STEP_LABEL_WIDTH,
1150
+ detail: `${value(label)} ${dim("accepted \xB7 ticket stapled")}`
1151
+ })
1152
+ );
1153
+ return { status: "ok" };
1154
+ };
1155
+ var reportNotaryFailure = (output, creds, options) => {
1156
+ options.warn(
1157
+ row({
1158
+ glyph: "error",
1159
+ label: "notarize",
1160
+ labelWidth: STEP_LABEL_WIDTH,
1161
+ detail: dim("the notary service rejected this build")
1162
+ })
1163
+ );
1164
+ options.warn(dim(output.trim()));
1165
+ const submissionId = parseSubmissionId(output);
1166
+ if (!submissionId) return;
1167
+ try {
1168
+ const log = execFileSync5(
1169
+ "xcrun",
1170
+ ["notarytool", "log", submissionId, ...notaryCredentialArgs(creds)],
1171
+ { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }
1172
+ );
1173
+ options.warn(footer(dim("notary log:")));
1174
+ options.warn(dim(log.trim()));
1175
+ } catch {
1176
+ options.warn(
1177
+ footer(
1178
+ dim(`fetch the details with: xcrun notarytool log ${submissionId} ...`)
1179
+ )
1180
+ );
1181
+ }
1182
+ };
1183
+ var parseSubmissionId = (output) => output.match(/\bid:\s*([0-9a-f-]{36})/i)?.[1] ?? null;
1184
+
1185
+ // src/targets/windows.ts
1186
+ import path10 from "path";
1187
+ import os4 from "os";
1188
+ import fs9 from "fs-extra";
1189
+ import { execFileSync as execFileSync6 } from "child_process";
1190
+ var windowsTarget = {
1191
+ name: "windows",
1192
+ framework: "net10.0-windows10.0.19041.0",
1193
+ // `RuntimeIdentifierOverride` (rather than `-r`/`RuntimeIdentifier`) is the
1194
+ // MAUI-recommended way to set the Windows RID — it sidesteps WindowsAppSDK
1195
+ // issue #3337, which otherwise pulls in the wrong packaging assets.
1196
+ extraPublishArgs: "-p:WindowsPackageType=None -p:SelfContained=true -p:WindowsAppSDKSelfContained=true -p:RuntimeIdentifierOverride=win-x64",
1197
+ findBundle(publishDir, _projectName) {
1198
+ const preferred = path10.join(publishDir, "win-x64", "publish");
1199
+ if (dirContainsExe(preferred)) return preferred;
1200
+ return findDirWithExe(publishDir);
1201
+ },
1202
+ async package(publishOutputDir, outputDir, meta) {
1203
+ const outName = `${meta.projectName}-${meta.displayVersion}-windows.zip`;
1204
+ const outPath = path10.join(outputDir, outName);
1205
+ if (fs9.existsSync(outPath)) fs9.removeSync(outPath);
1206
+ const staging = fs9.mkdtempSync(path10.join(os4.tmpdir(), "vidra-zip-"));
1207
+ const stagedApp = path10.join(staging, meta.projectName);
1208
+ try {
1209
+ fs9.copySync(publishOutputDir, stagedApp);
1210
+ execFileSync6(
1211
+ "powershell",
1212
+ [
1213
+ "-NoProfile",
1214
+ "-NonInteractive",
1215
+ "-Command",
1216
+ `Compress-Archive -Path "${stagedApp}" -DestinationPath "${outPath}" -Force`
1217
+ ],
1218
+ { stdio: "pipe" }
1219
+ );
1220
+ } finally {
1221
+ fs9.removeSync(staging);
1222
+ }
1223
+ return outPath;
1224
+ }
1225
+ };
1226
+ var dirContainsExe = (dir) => fs9.existsSync(dir) && fs9.readdirSync(dir, { withFileTypes: true }).some((e) => e.isFile() && e.name.toLowerCase().endsWith(".exe"));
1227
+ var findDirWithExe = (root) => {
1228
+ if (!fs9.existsSync(root)) return null;
1229
+ let fallback = null;
1230
+ const walk = (dir) => {
1231
+ if (dirContainsExe(dir)) {
1232
+ if (path10.basename(dir) === "publish") return dir;
1233
+ fallback ??= dir;
1234
+ }
1235
+ for (const e of fs9.readdirSync(dir, { withFileTypes: true })) {
1236
+ if (e.isDirectory()) {
1237
+ const found = walk(path10.join(dir, e.name));
1238
+ if (found) return found;
1239
+ }
1240
+ }
1241
+ return null;
1242
+ };
1243
+ return walk(root) ?? fallback;
1244
+ };
1245
+
1246
+ // src/doctor.ts
1247
+ import { execFileSync as execFileSync7 } from "child_process";
1248
+ import fs10 from "fs";
1249
+ import path11 from "path";
1250
+ import prompts from "prompts";
1251
+ var DOTNET = process.platform === "win32" ? "dotnet.exe" : "dotnet";
1252
+ var MAUI_DOCS = "https://learn.microsoft.com/dotnet/maui/get-started/installation";
1253
+ var INSTALL_NET_10_FIX = "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download";
1254
+ var bufToStr = (v) => v == null ? "" : Buffer.isBuffer(v) ? v.toString() : v;
1255
+ var run = (cmd, args) => {
1256
+ try {
1257
+ const stdout = execFileSync7(cmd, args, {
1258
+ encoding: "utf-8",
1259
+ stdio: ["ignore", "pipe", "pipe"]
1260
+ });
1261
+ return { found: true, ok: true, stdout: stdout ?? "", stderr: "" };
1262
+ } catch (e) {
1263
+ const err = e;
1264
+ return {
1265
+ found: err.code !== "ENOENT",
1266
+ ok: false,
1267
+ stdout: bufToStr(err.stdout),
1268
+ stderr: bufToStr(err.stderr)
1269
+ };
1270
+ }
1271
+ };
1272
+ var splitLines = (text) => text.split(/\r?\n/);
1273
+ var matchesAny = (text, patterns) => patterns.some((pattern) => pattern.test(text));
1274
+ var NET_10_VERSION = /^10\./;
1275
+ var MAUI_WORKLOAD = /\bmaui\b/i;
1276
+ var COMMAND_LINE_TOOLS_PATH = /CommandLineTools/i;
1277
+ var hasNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).some((line) => NET_10_VERSION.test(line.trim()));
1278
+ var newestNet10Sdk = (listSdksOutput) => splitLines(listSdksOutput).map((line) => line.trim().split(/\s+/)[0]).filter((version) => NET_10_VERSION.test(version)).sort(
1279
+ (a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" })
1280
+ ).pop();
1281
+ var outputMentionsMaui = (workloadListOutput) => MAUI_WORKLOAD.test(workloadListOutput);
1282
+ var MISSING_WORKLOAD_SIGNATURES = [
1283
+ /NETSDK1147/i,
1284
+ /workloads?\s+must\s+be\s+installed/i,
1285
+ /maui-maccatalyst/i,
1286
+ /maui-windows/i,
1287
+ /to\s+install\s+the\s+.*workload/i
1288
+ ];
1289
+ var MISSING_XCODE_SIGNATURES = [
1290
+ /valid\s+Xcode\s+installation\s+was\s+not\s+found/i,
1291
+ /could\s+not\s+find\s+a\s+valid\s+Xcode\s+app\s+bundle/i,
1292
+ /macios-missing-xcode/i
1293
+ ];
1294
+ var OUTDATED_XCODE_SIGNATURES = [
1295
+ /error\s+MT0180/i,
1296
+ /requires\s+the\s+MacCatalyst\s+\S+\s+SDK\s+\(shipped\s+with\s+Xcode/i
1297
+ ];
1298
+ var looksLikeMissingWorkload = (output) => matchesAny(output, MISSING_WORKLOAD_SIGNATURES);
1299
+ var looksLikeMissingXcode = (output) => matchesAny(output, MISSING_XCODE_SIGNATURES);
1300
+ var looksLikeXcodeTooOld = (output) => matchesAny(output, OUTDATED_XCODE_SIGNATURES);
1301
+ var checkDotnetSdk = () => {
1302
+ const name = ".NET SDK";
1303
+ const res = run(DOTNET, ["--list-sdks"]);
1304
+ if (!res.found) {
1305
+ return {
1306
+ name,
1307
+ status: "missing",
1308
+ detail: "`dotnet` was not found on your PATH",
1309
+ fix: INSTALL_NET_10_FIX
1310
+ };
1311
+ }
1312
+ if (!res.ok && !res.stdout) {
1313
+ return { name, status: "unknown", detail: "could not run `dotnet --list-sdks`" };
1314
+ }
1315
+ if (hasNet10Sdk(res.stdout)) {
1316
+ const newest = newestNet10Sdk(res.stdout);
1317
+ return { name, status: "ok", detail: newest ? `found ${newest}` : "found 10.x" };
1318
+ }
1319
+ return {
1320
+ name,
1321
+ status: "missing",
1322
+ detail: "no 10.x SDK installed",
1323
+ fix: INSTALL_NET_10_FIX
1324
+ };
1325
+ };
1326
+ var checkMauiWorkload = (workloadList) => {
1327
+ const name = ".NET MAUI workload";
1328
+ if (!workloadList) {
1329
+ return { name, status: "unknown", detail: "requires the .NET SDK first" };
1330
+ }
1331
+ if (!workloadList.found) {
1332
+ return { name, status: "unknown", detail: "could not query workloads" };
1333
+ }
1334
+ if (outputMentionsMaui(workloadList.stdout)) {
1335
+ return { name, status: "ok", detail: "installed" };
1336
+ }
1337
+ return {
1338
+ name,
1339
+ status: "missing",
1340
+ detail: "not installed",
1341
+ fix: "dotnet workload install maui"
1342
+ };
1343
+ };
1344
+ var FIRST_FIXED_MACCATALYST_PACK = [26, 2, 10233];
1345
+ var MACCATALYST_PACK_DIR = /^Microsoft\.MacCatalyst\.Sdk\.net\d+\.\d+_\d+\.\d+$/;
1346
+ var compareVersions = (a, b) => {
1347
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
1348
+ const diff = (a[i] ?? 0) - (b[i] ?? 0);
1349
+ if (diff !== 0) return diff;
1350
+ }
1351
+ return 0;
1352
+ };
1353
+ var versionSegments = (version) => {
1354
+ const segments = version.split(".").map((s) => Number.parseInt(s, 10));
1355
+ return segments.length && !segments.some(Number.isNaN) ? segments : null;
1356
+ };
1357
+ var newestPackVersion = (versions) => {
1358
+ let best;
1359
+ for (const raw of versions) {
1360
+ const segments = versionSegments(raw);
1361
+ if (!segments) continue;
1362
+ if (!best || compareVersions(segments, best.segments) > 0) {
1363
+ best = { raw, segments };
1364
+ }
1365
+ }
1366
+ return best?.raw;
1367
+ };
1368
+ var macCatalystPackIsStale = (version) => {
1369
+ const segments = versionSegments(version);
1370
+ return segments ? compareVersions(segments, FIRST_FIXED_MACCATALYST_PACK) < 0 : false;
1371
+ };
1372
+ var dotnetPacksDir = () => {
1373
+ if (process.env.DOTNET_ROOT) {
1374
+ return path11.join(process.env.DOTNET_ROOT, "packs");
1375
+ }
1376
+ const res = run(DOTNET, ["--list-sdks"]);
1377
+ if (!res.found) return void 0;
1378
+ const sdkDir = res.stdout.trim().split("\n").pop()?.match(/\[(.+)\]\s*$/)?.[1];
1379
+ return sdkDir ? path11.join(path11.dirname(sdkDir), "packs") : void 0;
1380
+ };
1381
+ var installedMacCatalystPackVersion = () => {
1382
+ const packs = dotnetPacksDir();
1383
+ if (!packs) return void 0;
1384
+ try {
1385
+ const versions = fs10.readdirSync(packs, { withFileTypes: true }).filter((e) => e.isDirectory() && MACCATALYST_PACK_DIR.test(e.name)).flatMap((e) => {
1386
+ try {
1387
+ return fs10.readdirSync(path11.join(packs, e.name), { withFileTypes: true }).filter((v) => v.isDirectory()).map((v) => v.name);
1388
+ } catch {
1389
+ return [];
1390
+ }
1391
+ });
1392
+ return newestPackVersion(versions);
1393
+ } catch {
1394
+ return void 0;
1395
+ }
1396
+ };
1397
+ var checkCSharpDevLoop = (workloadList) => {
1398
+ const name = "C# dev loop";
1399
+ if (!workloadList) {
1400
+ return { name, status: "unknown", detail: "requires the .NET SDK first" };
1401
+ }
1402
+ if (process.platform !== "darwin") {
1403
+ return {
1404
+ name,
1405
+ status: "ok",
1406
+ detail: "dotnet watch applies C# edits to the running app"
1407
+ };
1408
+ }
1409
+ const pack = installedMacCatalystPackVersion();
1410
+ if (pack && macCatalystPackIsStale(pack)) {
1411
+ return {
1412
+ name,
1413
+ status: "unknown",
1414
+ detail: `Mac Catalyst pack ${pack} cannot launch the app under dotnet watch \u2014 vidra dev falls back to a classic launch (fixed in 26.2.10233+)`,
1415
+ fix: "dotnet workload update"
1416
+ };
1417
+ }
1418
+ const packNote = pack ? ` (Mac Catalyst pack ${pack})` : "";
1419
+ return {
1420
+ name,
1421
+ status: "ok",
1422
+ detail: `dotnet watch applies C# edits to the running app${packNote} \u2014 vidra dev rebuilds and relaunches if the agent drops mid-session`
1423
+ };
1424
+ };
1425
+ var checkXcode = () => {
1426
+ const name = "Xcode";
1427
+ const res = run("xcode-select", ["-p"]);
1428
+ if (!res.found || !res.ok) {
1429
+ return {
1430
+ name,
1431
+ status: "missing",
1432
+ detail: "not found",
1433
+ fix: "Install Xcode from the App Store"
1434
+ };
1435
+ }
1436
+ const devDir = res.stdout.trim();
1437
+ if (COMMAND_LINE_TOOLS_PATH.test(devDir)) {
1438
+ return {
1439
+ name,
1440
+ status: "missing",
1441
+ detail: "only Command Line Tools detected (Mac Catalyst needs full Xcode)",
1442
+ fix: "Install Xcode, then: sudo xcode-select -s /Applications/Xcode.app"
1443
+ };
1444
+ }
1445
+ return { name, status: "ok", detail: devDir };
1446
+ };
1447
+ var isMauiWorkloadInstalled = () => outputMentionsMaui(run(DOTNET, ["workload", "list"]).stdout);
1448
+ var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
1449
+ var checkMacSigningIdentity = () => {
1450
+ const all = listCodeSigningIdentities();
1451
+ const expired = new Set(listExpiredCodeSigningIdentities(all));
1452
+ const identities = all.filter((id) => !expired.has(id));
1453
+ if (identities.some((id) => id.startsWith("Developer ID Application:"))) {
1454
+ return {
1455
+ name: "macOS signing (distribution)",
1456
+ status: "ok",
1457
+ detail: "Developer ID Application certificate found"
1458
+ };
1459
+ }
1460
+ const expiredDeveloperId = [...expired].find(
1461
+ (id) => id.startsWith("Developer ID Application:")
1462
+ );
1463
+ if (expiredDeveloperId) {
1464
+ return {
1465
+ name: "macOS signing (distribution)",
1466
+ status: "unknown",
1467
+ detail: `Developer ID certificate expired \u2014 ${expiredDeveloperId}`,
1468
+ fix: "Renew it in the Apple Developer portal, then re-download and install it"
1469
+ };
1470
+ }
1471
+ if (identities.some((id) => id.startsWith("Apple Development:"))) {
1472
+ return {
1473
+ name: "macOS signing (distribution)",
1474
+ status: "unknown",
1475
+ detail: "only a development certificate found \u2014 fine for `vidra dev`, cannot be notarized",
1476
+ fix: "Create a Developer ID Application certificate (requires the Apple Developer Program)"
1477
+ };
1478
+ }
1479
+ return {
1480
+ name: "macOS signing (distribution)",
1481
+ status: "unknown",
1482
+ detail: "no code-signing identity \u2014 builds will be ad-hoc signed",
1483
+ fix: "Create a Developer ID Application certificate, or set VIDRA_MACOS_CODESIGN_KEY"
1484
+ };
1485
+ };
1486
+ var checkNotarization = () => {
1487
+ const creds = resolveNotaryCredentials();
1488
+ if (creds) {
1489
+ return {
1490
+ name: "macOS notarization",
1491
+ status: "ok",
1492
+ detail: creds.mode === "profile" ? `keychain profile "${creds.profile}"` : `Apple ID ${creds.appleId} (team ${creds.teamId})`
1493
+ };
1494
+ }
1495
+ return {
1496
+ name: "macOS notarization",
1497
+ status: "unknown",
1498
+ detail: "no credentials \u2014 `vidra build` will skip notarization",
1499
+ fix: "xcrun notarytool store-credentials, then set VIDRA_NOTARY_PROFILE"
1500
+ };
1501
+ };
1502
+ var checkWindowsSigning = () => {
1503
+ const config = resolveWindowsSigningConfig();
1504
+ if (config) {
1505
+ return {
1506
+ name: "Windows signing",
1507
+ status: "ok",
1508
+ detail: config.mode === "pfx" ? `certificate file ${config.pfxPath}` : `store certificate ${config.thumbprint}`
1509
+ };
1510
+ }
1511
+ return {
1512
+ name: "Windows signing",
1513
+ status: "unknown",
1514
+ detail: "no certificate \u2014 builds ship unsigned and SmartScreen will warn",
1515
+ fix: "Set VIDRA_WINDOWS_CERT_PATH (+ VIDRA_WINDOWS_CERT_PASSWORD) or VIDRA_WINDOWS_CERT_THUMBPRINT"
1516
+ };
1517
+ };
1518
+ var checkWebView2Runtime = () => {
1519
+ const key = "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
1520
+ const result = run("reg", ["query", key, "/v", "pv"]);
1521
+ const version = result.stdout.match(/pv\s+REG_SZ\s+([\d.]+)/)?.[1];
1522
+ if (result.ok && version && version !== "0.0.0.0") {
1523
+ return {
1524
+ name: "WebView2 runtime",
1525
+ status: "ok",
1526
+ detail: `found ${version}`
1527
+ };
1528
+ }
1529
+ return {
1530
+ name: "WebView2 runtime",
1531
+ status: "unknown",
1532
+ detail: "not detected \u2014 Vidra apps need it to render on this machine",
1533
+ fix: "https://developer.microsoft.com/microsoft-edge/webview2/"
1534
+ };
1535
+ };
1536
+ var checkVelopack = (required) => {
1537
+ const vpk = resolveVpk();
1538
+ if (vpk) {
1539
+ const version = vpkVersion(vpk);
1540
+ return {
1541
+ name: "Velopack (vpk)",
1542
+ status: "ok",
1543
+ detail: version ? `${version} at ${vpk}` : vpk
1544
+ };
1545
+ }
1546
+ return {
1547
+ name: "Velopack (vpk)",
1548
+ status: required ? "missing" : "unknown",
1549
+ detail: required ? "vidra.updates.native is configured but vpk is not installed, so `vidra build --native-update` cannot run" : "not installed, and only needed for `vidra build --native-update`",
1550
+ fix: "dotnet tool install -g vpk"
1551
+ };
1552
+ };
1553
+ var diagnoseUpdateConfiguration = (input) => {
1554
+ const { config } = input;
1555
+ const mauiProgram = input.mauiProgram === null ? null : stripComments(input.mauiProgram);
1556
+ const usesUpdates = mauiProgram?.includes(".UseVidraUpdates(") ?? false;
1557
+ const usesNative = mauiProgram?.includes(".UseVidraNativeUpdates(") ?? false;
1558
+ const found = [];
1559
+ if (config && !usesUpdates && mauiProgram !== null) {
1560
+ found.push({
1561
+ name: "OTA updates wired up",
1562
+ status: "missing",
1563
+ detail: "package.json configures vidra.updates, but MauiProgram never calls .UseVidraUpdates()",
1564
+ fix: "add .UseVidraUpdates() after .UseVidra() in MauiProgram.cs"
1565
+ });
1566
+ }
1567
+ if (!config && usesUpdates) {
1568
+ found.push({
1569
+ name: "OTA updates configured",
1570
+ status: "missing",
1571
+ detail: ".UseVidraUpdates() is called, but package.json has no vidra.updates block, nothing is ever checked",
1572
+ fix: 'add "vidra": { "updates": { "feedUrl": "https://..." } } to package.json'
1573
+ });
1574
+ }
1575
+ if (input.publishedUnsigned && (config?.publicKeys?.length ?? 0) > 0) {
1576
+ found.push({
1577
+ name: "Feed signature",
1578
+ status: "missing",
1579
+ detail: "publicKeys are configured, so signatures are mandatory, but the published bundles.json has no .sig beside it",
1580
+ fix: "republish with `vidra bundle --sign <key.pem>`"
1581
+ });
1582
+ }
1583
+ if (config?.native) {
1584
+ if (!usesNative && mauiProgram !== null) {
1585
+ found.push({
1586
+ name: "Native updates wired up",
1587
+ status: "missing",
1588
+ detail: "vidra.updates.native is configured, but MauiProgram never calls .UseVidraNativeUpdates()",
1589
+ fix: "add .UseVidraNativeUpdates() after .UseVidra(), and reference Vidra.Updates.Native"
1590
+ });
1591
+ }
1592
+ if (!config.native.feedUrl) {
1593
+ found.push({
1594
+ name: "Native feed",
1595
+ status: "missing",
1596
+ detail: "vidra.updates.native has no feedUrl: releases can be packed, and no installed app will ever find them",
1597
+ fix: 'set "vidra": { "updates": { "native": { "feedUrl": "https://.../" } } }'
1598
+ });
1599
+ }
1600
+ }
1601
+ return found;
1602
+ };
1603
+ var stripComments = (source) => source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
1604
+ var inspectUpdateConfiguration = () => {
1605
+ const project = tryDetectProject(process.cwd());
1606
+ if (!project) return [];
1607
+ const config = readUpdateConfig(project.root);
1608
+ const mauiProgram = readIfPresent(path11.join(project.hostDir, "MauiProgram.cs"));
1609
+ const live = mauiProgram === null ? "" : stripComments(mauiProgram);
1610
+ if (!config && !live.includes("Updates(")) {
1611
+ return [];
1612
+ }
1613
+ const bundles = path11.join(project.root, "dist", "bundles.json");
1614
+ const publishedUnsigned = fs10.existsSync(bundles) && !fs10.existsSync(`${bundles}.sig`);
1615
+ return [
1616
+ ...config?.native || resolveVpk() ? [checkVelopack(!!config?.native)] : [],
1617
+ ...diagnoseUpdateConfiguration({ config, mauiProgram, publishedUnsigned })
1618
+ ];
1619
+ };
1620
+ var readIfPresent = (file) => {
1621
+ try {
1622
+ return fs10.readFileSync(file, "utf-8");
1623
+ } catch {
1624
+ return null;
1625
+ }
1626
+ };
1627
+ var collectRequirements = (opts = {}) => {
1628
+ const dotnet = checkDotnetSdk();
1629
+ const workloadList = dotnet.status === "ok" ? run(DOTNET, ["workload", "list"]) : null;
1630
+ const reqs = [
1631
+ dotnet,
1632
+ checkMauiWorkload(workloadList),
1633
+ checkCSharpDevLoop(workloadList)
1634
+ ];
1635
+ if (opts.includeXcode ?? process.platform === "darwin") {
1636
+ reqs.push(checkXcode());
1637
+ }
1638
+ if (process.platform === "darwin") {
1639
+ reqs.push(checkMacSigningIdentity(), checkNotarization());
1640
+ }
1641
+ if (process.platform === "win32") {
1642
+ reqs.push(checkWindowsSigning(), checkWebView2Runtime());
1643
+ }
1644
+ const updateIssues = inspectUpdateConfiguration();
1645
+ if (updateIssues.length > 0) {
1646
+ reqs.push(...updateIssues);
1647
+ }
1648
+ return reqs;
1649
+ };
1650
+ var STATUS_GLYPH = {
1651
+ ok: "done",
1652
+ missing: "error",
1653
+ unknown: "manual"
1654
+ };
1655
+ var printRequirements = (reqs) => {
1656
+ const labelWidth = Math.max(0, ...reqs.map((r) => r.name.length)) + 2;
1657
+ for (const r of reqs) {
1658
+ console.log(
1659
+ row({
1660
+ glyph: STATUS_GLYPH[r.status],
1661
+ label: r.name,
1662
+ labelWidth,
1663
+ detail: r.detail ? dim(r.detail) : void 0
1664
+ })
1665
+ );
1666
+ if (r.status !== "ok" && r.fix) {
1667
+ console.log(fixLine(r.fix));
1668
+ }
1669
+ }
1670
+ };
1671
+ var runDoctor = async () => {
1672
+ console.log();
1673
+ console.log(` ${lime("vidra")} ${value("doctor")}`);
1674
+ console.log();
1675
+ console.log(footer(dim("checking your environment\u2026")));
1676
+ console.log();
1677
+ const reqs = collectRequirements();
1678
+ printRequirements(reqs);
1679
+ console.log();
1680
+ const missing = reqs.filter((r) => r.status === "missing");
1681
+ if (missing.length === 0) {
1682
+ console.log(
1683
+ footer(
1684
+ `${dim("all checks passed \u2014 you're ready to run")} ${lime(
1685
+ "npm run dev"
1686
+ )}${dim(".")}`
1687
+ )
1688
+ );
1689
+ console.log();
1690
+ return 0;
1691
+ }
1692
+ const n = missing.length;
1693
+ console.log(
1694
+ footer(
1695
+ `${dim(
1696
+ `${n} issue${n === 1 ? "" : "s"} found. apply the ${n === 1 ? "fix" : "fixes"} above, then re-run`
1697
+ )} ${lime("npm run doctor")}${dim(".")}`
1698
+ )
1699
+ );
1700
+ console.log();
1701
+ return 1;
1702
+ };
1703
+ var installWorkload = (csprojPath) => {
1704
+ const args = csprojPath ? ["workload", "restore", csprojPath] : ["workload", "install", "maui"];
1705
+ console.log();
1706
+ console.log(
1707
+ row({
1708
+ glyph: "active",
1709
+ detail: `${dim("running")} ${value(`${DOTNET} ${args.join(" ")}`)}`
1710
+ })
1711
+ );
1712
+ console.log(
1713
+ footer(
1714
+ dim("this can download several hundred MB and take a few minutes.")
1715
+ )
1716
+ );
1717
+ console.log();
1718
+ try {
1719
+ execFileSync7(DOTNET, args, { stdio: "inherit" });
1720
+ return true;
1721
+ } catch {
1722
+ console.error();
1723
+ console.error(row({ glyph: "error", label: "workload install failed" }));
1724
+ console.error(
1725
+ footer(
1726
+ dim(
1727
+ "if this is a permissions error, your SDK is in a system location and needs elevation:"
1728
+ )
1729
+ )
1730
+ );
1731
+ console.error(fixLine("sudo dotnet workload install maui"));
1732
+ console.error();
1733
+ return false;
1734
+ }
1735
+ };
1736
+ var ensureMauiWorkload = async (opts = {}) => {
1737
+ const dotnet = checkDotnetSdk();
1738
+ if (dotnet.status === "missing") {
1739
+ console.log();
1740
+ console.log(
1741
+ row({
1742
+ glyph: "error",
1743
+ label: dotnet.name,
1744
+ detail: dotnet.detail ? dim(dotnet.detail) : void 0
1745
+ })
1746
+ );
1747
+ if (dotnet.fix) {
1748
+ console.log(fixLine(dotnet.fix));
1749
+ }
1750
+ return false;
1751
+ }
1752
+ if (dotnet.status === "unknown") return true;
1753
+ if (isMauiWorkloadInstalled()) return true;
1754
+ console.log();
1755
+ console.log(
1756
+ row({
1757
+ glyph: "error",
1758
+ label: ".NET MAUI workload",
1759
+ detail: dim("required but not installed")
1760
+ })
1761
+ );
1762
+ const interactive = opts.interactive ?? isInteractive();
1763
+ if (interactive) {
1764
+ let install = false;
1765
+ try {
1766
+ const res = await prompts({
1767
+ type: "confirm",
1768
+ name: "install",
1769
+ message: "Install the .NET MAUI workload now?",
1770
+ initial: true
1771
+ });
1772
+ install = Boolean(res.install);
1773
+ } catch {
1774
+ install = false;
1775
+ }
1776
+ if (install) {
1777
+ if (installWorkload(opts.csprojPath) && isMauiWorkloadInstalled()) {
1778
+ console.log(
1779
+ row({
1780
+ glyph: "done",
1781
+ label: ".NET MAUI workload",
1782
+ detail: dim("installed")
1783
+ })
1784
+ );
1785
+ return true;
1786
+ }
1787
+ return false;
1788
+ }
1789
+ }
1790
+ console.log(fixLine("dotnet workload install maui", "run:"));
1791
+ console.log(fixLine(MAUI_DOCS, "docs:"));
1792
+ return false;
1793
+ };
1794
+ var printWorkloadHint = () => {
1795
+ console.error();
1796
+ console.error(
1797
+ row({ glyph: "manual", label: "this looks like a missing .NET MAUI workload." })
1798
+ );
1799
+ console.error(fixLine("dotnet workload install maui"));
1800
+ console.error(fixLine("vidra doctor", "check:"));
1801
+ console.error();
1802
+ };
1803
+ var printXcodeTooOldHint = () => {
1804
+ console.error();
1805
+ console.error(
1806
+ row({
1807
+ glyph: "manual",
1808
+ label: "your Xcode is older than the SDK this MAUI workload set expects."
1809
+ })
1810
+ );
1811
+ console.error(
1812
+ ` ${dim("\u2022")} ${dim("update Xcode (App Store), then")} ${lime("sudo xcodebuild -runFirstLaunch")}`
1813
+ );
1814
+ console.error(
1815
+ ` ${dim("\u2022")} ${dim("or pin the workloads to your Xcode's era:")} ${lime("dotnet workload update --version <set>")}`
1816
+ );
1817
+ console.error(fixLine("vidra doctor", "check:"));
1818
+ console.error();
1819
+ };
1820
+ var printXcodeHint = () => {
1821
+ console.error();
1822
+ console.error(
1823
+ row({
1824
+ glyph: "manual",
1825
+ label: "Mac Catalyst needs the full Xcode app, not just the Command Line Tools."
1826
+ })
1827
+ );
1828
+ console.error(` ${dim("1.")} ${value("install Xcode from the App Store")}`);
1829
+ console.error(
1830
+ ` ${dim("2.")} ${lime(
1831
+ "sudo xcode-select -s /Applications/Xcode.app/Contents/Developer"
1832
+ )}`
1833
+ );
1834
+ console.error(` ${dim("3.")} ${lime("sudo xcodebuild -runFirstLaunch")}`);
1835
+ console.error(fixLine("vidra doctor", "check:"));
1836
+ console.error();
1837
+ };
1838
+
1839
+ // src/commands/build.ts
1840
+ var TARGETS = {
1841
+ macos: macosTarget,
1842
+ windows: windowsTarget
1843
+ };
1844
+ var packageLabel = (target) => target.name === "macos" ? "package DMG" : "package ZIP";
1845
+ var artifactName = (project, target) => `${project.projectName}-${project.displayVersion}-${target.name}.${target.name === "macos" ? "dmg" : "zip"}`;
1846
+ var buildCommand = async (argv) => {
1847
+ const args = parseArgs(["_", "_", ...argv]);
1848
+ const verbose = !!args["verbose"];
1849
+ const plan = !!args["plan"] || !!args["dry-run"];
1850
+ const nativeUpdate = !!args["native-update"];
1851
+ const targetName = args["target"] || detectPlatform();
1852
+ const target = TARGETS[targetName];
1853
+ if (!target) {
1854
+ const supported = Object.keys(TARGETS).join(", ");
1855
+ console.error();
1856
+ console.error(
1857
+ row({
1858
+ glyph: "error",
1859
+ detail: dim(`unsupported target: ${targetName} \u2014 supported: ${supported}`)
1860
+ })
1861
+ );
1862
+ process.exit(1);
1863
+ }
1864
+ const project = detectProject(process.cwd());
1865
+ console.log();
1866
+ console.log(
1867
+ header("build", `${target.name} \xB7 Release${plan ? " \xB7 plan" : ""}`)
1868
+ );
1869
+ console.log(kv("project", project.projectName));
1870
+ console.log(kv("target", target.framework));
1871
+ console.log();
1872
+ const updateConfig = readUpdateConfig(project.root);
1873
+ let nativeSettings = null;
1874
+ if (nativeUpdate) {
1875
+ try {
1876
+ nativeSettings = resolveNativeUpdateSettings({
1877
+ config: updateConfig?.native,
1878
+ csprojPath: project.csprojPath,
1879
+ projectName: project.projectName,
1880
+ version: project.displayVersion
1881
+ });
1882
+ } catch (error) {
1883
+ if (!(error instanceof NativeUpdateConfigError)) throw error;
1884
+ console.error();
1885
+ console.error(row({ glyph: "error", label: "native update", labelWidth: STEP_LABEL_WIDTH, detail: dim(error.message) }));
1886
+ console.error();
1887
+ process.exit(1);
1888
+ }
1889
+ }
1890
+ if (plan) {
1891
+ printBuildPlan(project, target, nativeSettings);
1892
+ console.log();
1893
+ console.log(
1894
+ footer(`${dim("nothing has run. re-run without")} ${lime("--plan")} ${dim("to apply.")}`)
1895
+ );
1896
+ console.log();
1897
+ return;
1898
+ }
1899
+ if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
1900
+ process.exit(1);
1901
+ }
1902
+ stepBuildUi(project, verbose);
1903
+ stepCopyAssets(project);
1904
+ stepStampUpdateConfig(project, updateConfig);
1905
+ const publishDir = stepDotnetPublish(project, target, verbose);
1906
+ const bundlePath = target.findBundle(publishDir, project.projectName);
1907
+ if (!bundlePath) {
1908
+ console.error(
1909
+ row({
1910
+ glyph: "error",
1911
+ detail: dim(`could not find build artifact in ${publishDir}`)
1912
+ })
1913
+ );
1914
+ process.exit(1);
1915
+ }
1916
+ const io = { verbose, log: console.log, warn: console.warn };
1917
+ const entitlements = target.name === "macos" ? entitlementsPath(project) : null;
1918
+ if (target.name === "macos") {
1919
+ signMacAppBundleIfPossible(bundlePath, {
1920
+ ...io,
1921
+ purpose: "distribution",
1922
+ entitlements
1923
+ });
1924
+ reportMacVerification(bundlePath, entitlements);
1925
+ }
1926
+ let signedWindowsExe = null;
1927
+ if (target.name === "windows") {
1928
+ signedWindowsExe = findPrimaryExecutable(bundlePath, project.projectName);
1929
+ if (signedWindowsExe && signWindowsBinariesIfPossible([signedWindowsExe], io)) {
1930
+ const verified = verifyWindowsSignature(signedWindowsExe);
1931
+ console.log(
1932
+ row({
1933
+ glyph: verified.ok ? "done" : "manual",
1934
+ label: "verify sig",
1935
+ labelWidth: STEP_LABEL_WIDTH,
1936
+ detail: verified.untrustedRoot ? dim("signature intact; chain not trusted (expected for a self-signed certificate)") : verified.ok ? dim("authenticode signature verified and trusted") : dim("signature did not verify \u2014 see signtool output")
1937
+ })
1938
+ );
1939
+ }
1940
+ }
1941
+ const packed = nativeSettings ? stepNativeUpdate(project, target, bundlePath, entitlements, nativeSettings, io) : null;
1942
+ const outputPath = packed && target.name === "windows" ? stepPublishVelopackWindowsArtifacts(project, target, packed) : await stepPackage(
1943
+ project,
1944
+ target,
1945
+ packed ? extractPackedApp(packed.outputs.portableZip) : bundlePath
1946
+ );
1947
+ if (target.name === "macos") {
1948
+ signMacDmgIfPossible(outputPath, io);
1949
+ const notarized = notarizeAndStaple(outputPath, io);
1950
+ if (notarized.status === "skipped") {
1951
+ console.log(
1952
+ row({
1953
+ glyph: "skip",
1954
+ label: "notarize",
1955
+ labelWidth: STEP_LABEL_WIDTH,
1956
+ detail: dim(notarized.reason)
1957
+ })
1958
+ );
1959
+ } else if (notarized.status === "failed") {
1960
+ process.exit(1);
1961
+ }
1962
+ reportGatekeeper(outputPath);
1963
+ }
1964
+ console.log();
1965
+ console.log(
1966
+ footer(
1967
+ `${dim("done \u2014")} ${value(path12.relative(project.root, outputPath))}`
1968
+ )
1969
+ );
1970
+ console.log();
1971
+ };
1972
+ var entitlementsPath = (project) => {
1973
+ const candidate = path12.join(project.hostDir, "Entitlements.plist");
1974
+ return fs11.existsSync(candidate) ? candidate : null;
1975
+ };
1976
+ var reportMacVerification = (bundlePath, entitlements) => {
1977
+ const verified = verifyMacSignature(bundlePath);
1978
+ console.log(
1979
+ row({
1980
+ glyph: verified.ok ? "done" : "error",
1981
+ label: "verify sig",
1982
+ labelWidth: STEP_LABEL_WIDTH,
1983
+ detail: verified.ok ? dim("codesign --verify --strict passed") : dim("codesign --verify --strict FAILED")
1984
+ })
1985
+ );
1986
+ if (!verified.ok) console.error(dim(verified.output));
1987
+ if (!entitlements) return;
1988
+ const hardening = inspectMacHardening(bundlePath);
1989
+ console.log(
1990
+ row({
1991
+ glyph: hardening.ok ? "done" : "error",
1992
+ label: "hardening",
1993
+ labelWidth: STEP_LABEL_WIDTH,
1994
+ detail: hardening.ok ? dim("hardened runtime + JIT entitlements embedded") : dim(
1995
+ !hardening.hardened ? "hardened runtime NOT enabled \u2014 this cannot be notarized" : `missing entitlements: ${hardening.missing.join(", ")} \u2014 the app will be killed at launch`
1996
+ )
1997
+ })
1998
+ );
1999
+ if (!hardening.ok) console.error(dim(hardening.output.trim()));
2000
+ };
2001
+ var reportGatekeeper = (artifactPath) => {
2002
+ const assessment = assessGatekeeper(artifactPath);
2003
+ if (assessment.ok) {
2004
+ console.log(
2005
+ row({
2006
+ glyph: "done",
2007
+ label: "gatekeeper",
2008
+ labelWidth: STEP_LABEL_WIDTH,
2009
+ detail: dim("spctl accepted \u2014 this will open on other Macs")
2010
+ })
2011
+ );
2012
+ return;
2013
+ }
2014
+ const missing = !hasDeveloperIdIdentity() ? "needs a Developer ID Application certificate" : !resolveNotaryCredentials() ? "needs notarization (set VIDRA_NOTARY_PROFILE or VIDRA_APPLE_ID/VIDRA_TEAM_ID/VIDRA_APP_PASSWORD)" : "see the spctl output above";
2015
+ console.log(
2016
+ row({
2017
+ glyph: "manual",
2018
+ label: "gatekeeper",
2019
+ labelWidth: STEP_LABEL_WIDTH,
2020
+ detail: dim(`spctl rejected \u2014 ${missing}`)
2021
+ })
2022
+ );
2023
+ };
2024
+ var printBuildPlan = (project, target, nativeSettings) => {
2025
+ console.log(
2026
+ row({
2027
+ glyph: "done",
2028
+ label: "build UI",
2029
+ labelWidth: STEP_LABEL_WIDTH,
2030
+ detail: `${dim("vite \u2192")} ${value("ui/dist")}`
2031
+ })
2032
+ );
2033
+ console.log(
2034
+ row({
2035
+ glyph: "done",
2036
+ label: "copy assets",
2037
+ labelWidth: STEP_LABEL_WIDTH,
2038
+ detail: `${dim("\u2192")} ${value("Resources/Raw/wwwroot")}`
2039
+ })
2040
+ );
2041
+ console.log(
2042
+ row({
2043
+ glyph: "done",
2044
+ label: "publish .NET",
2045
+ labelWidth: STEP_LABEL_WIDTH,
2046
+ detail: `${dim("Release \xB7")} ${value(target.framework)}`
2047
+ })
2048
+ );
2049
+ if (nativeSettings) {
2050
+ const vpk = resolveVpk();
2051
+ console.log(
2052
+ row({
2053
+ glyph: vpk ? "done" : "error",
2054
+ label: "vpk pack",
2055
+ labelWidth: STEP_LABEL_WIDTH,
2056
+ detail: vpk ? `${value(`${nativeSettings.packId} ${nativeSettings.packVersion}`)} ${dim(`\u2192 ${RELEASE_DIR}`)}` : dim("vpk is not installed \u2014 dotnet tool install -g vpk")
2057
+ })
2058
+ );
2059
+ console.log(
2060
+ row({
2061
+ glyph: nativeSettings.feedUrl ? "active" : "manual",
2062
+ label: "merge feed",
2063
+ labelWidth: STEP_LABEL_WIDTH,
2064
+ detail: nativeSettings.feedUrl ? `${dim("vpk download \u2190")} ${value(nativeSettings.feedUrl)}` : dim("no vidra.updates.native.feedUrl \u2014 the release is packed, and no app will find it")
2065
+ })
2066
+ );
2067
+ }
2068
+ if (target.name === "macos") {
2069
+ const developerId = hasDeveloperIdIdentity();
2070
+ console.log(
2071
+ row({
2072
+ glyph: "done",
2073
+ label: "codesign .app",
2074
+ labelWidth: STEP_LABEL_WIDTH,
2075
+ detail: dim(
2076
+ developerId ? "Developer ID Application \xB7 hardened runtime" : "no Developer ID found \u2014 will fall back to development/ad-hoc"
2077
+ )
2078
+ })
2079
+ );
2080
+ console.log(
2081
+ row({
2082
+ glyph: "active",
2083
+ label: "package DMG",
2084
+ labelWidth: STEP_LABEL_WIDTH,
2085
+ detail: `${dim("hdiutil UDZO \u2192")} ${value(artifactName(project, target))}`
2086
+ })
2087
+ );
2088
+ console.log(
2089
+ row({
2090
+ glyph: developerId ? "done" : "skip",
2091
+ label: "codesign dmg",
2092
+ labelWidth: STEP_LABEL_WIDTH,
2093
+ detail: dim(
2094
+ developerId ? "sign the disk image" : "skipped without a Developer ID"
2095
+ )
2096
+ })
2097
+ );
2098
+ const creds = resolveNotaryCredentials();
2099
+ console.log(
2100
+ row({
2101
+ glyph: creds ? "active" : "plan",
2102
+ label: "notarize",
2103
+ labelWidth: STEP_LABEL_WIDTH,
2104
+ detail: creds ? dim("notarytool submit --wait, then staple") : `${planBadge()} ${dim("no credentials configured")}`
2105
+ })
2106
+ );
2107
+ console.log(
2108
+ row({
2109
+ glyph: "active",
2110
+ label: "gatekeeper",
2111
+ labelWidth: STEP_LABEL_WIDTH,
2112
+ detail: dim("spctl --assess")
2113
+ })
2114
+ );
2115
+ } else {
2116
+ const winCert = resolveWindowsSigningConfig();
2117
+ console.log(
2118
+ row({
2119
+ glyph: winCert ? "done" : "skip",
2120
+ label: "authenticode",
2121
+ labelWidth: STEP_LABEL_WIDTH,
2122
+ detail: dim(
2123
+ winCert ? `sign the .exe (${winCert.mode})` : "no certificate configured \u2014 will ship unsigned"
2124
+ )
2125
+ })
2126
+ );
2127
+ console.log(
2128
+ row({
2129
+ glyph: "active",
2130
+ label: "package ZIP",
2131
+ labelWidth: STEP_LABEL_WIDTH,
2132
+ detail: nativeSettings ? `${dim("vpk's portable zip \u2192")} ${value(artifactName(project, target))}` : `${dim("self-contained \u2192")} ${value(artifactName(project, target))}`
2133
+ })
2134
+ );
2135
+ if (nativeSettings) {
2136
+ console.log(
2137
+ row({
2138
+ glyph: "active",
2139
+ label: "installer",
2140
+ labelWidth: STEP_LABEL_WIDTH,
2141
+ detail: `${dim("\u2192")} ${value(`${project.projectName}-${project.displayVersion}-Setup.exe`)}`
2142
+ })
2143
+ );
2144
+ }
2145
+ }
2146
+ };
2147
+ var stepBuildUi = (project, verbose) => {
2148
+ const start = Date.now();
2149
+ try {
2150
+ execSync3("npm run build", {
2151
+ cwd: project.uiDir,
2152
+ stdio: verbose ? "inherit" : "pipe"
2153
+ });
2154
+ } catch (e) {
2155
+ console.error(
2156
+ row({
2157
+ glyph: "error",
2158
+ label: "build UI",
2159
+ labelWidth: STEP_LABEL_WIDTH,
2160
+ detail: dim("vite build failed")
2161
+ })
2162
+ );
2163
+ console.error(dim(formatBuildError(e)));
2164
+ process.exit(1);
2165
+ }
2166
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
2167
+ console.log(
2168
+ row({
2169
+ glyph: "done",
2170
+ label: "build UI",
2171
+ labelWidth: STEP_LABEL_WIDTH,
2172
+ detail: `${dim("vite \u2192")} ${value("ui/dist")} ${dim(`(${elapsed}s)`)}`
2173
+ })
2174
+ );
2175
+ };
2176
+ var stepCopyAssets = (project) => {
2177
+ const viteDist = path12.join(project.uiDir, "dist");
2178
+ if (!fs11.existsSync(viteDist)) {
2179
+ console.error(
2180
+ row({
2181
+ glyph: "error",
2182
+ label: "copy assets",
2183
+ labelWidth: STEP_LABEL_WIDTH,
2184
+ detail: dim("ui/dist not found \u2014 vite build may have failed")
2185
+ })
2186
+ );
2187
+ process.exit(1);
2188
+ }
2189
+ const wwwroot = path12.join(project.hostDir, "Resources", "Raw", "wwwroot");
2190
+ fs11.removeSync(wwwroot);
2191
+ fs11.copySync(viteDist, wwwroot);
2192
+ const fileCount = countFiles(wwwroot);
2193
+ console.log(
2194
+ row({
2195
+ glyph: "done",
2196
+ label: "copy assets",
2197
+ labelWidth: STEP_LABEL_WIDTH,
2198
+ detail: `${dim("\u2192")} ${value("Resources/Raw/wwwroot")} ${dim(`(${fileCount} files)`)}`
2199
+ })
2200
+ );
2201
+ };
2202
+ var stepStampUpdateConfig = (project, config) => {
2203
+ stampUpdateConfig(project.hostDir, config);
2204
+ if (!config) {
2205
+ return;
2206
+ }
2207
+ console.log(
2208
+ row({
2209
+ glyph: "done",
2210
+ label: "stamp updates",
2211
+ labelWidth: STEP_LABEL_WIDTH,
2212
+ detail: `${dim("\u2192")} ${value(`Resources/Raw/${UPDATE_CONFIG_FILE}`)} ${dim(
2213
+ config.enabled === false ? "(disabled)" : `(${config.feedUrl ?? "no feed"})`
2214
+ )}`
2215
+ })
2216
+ );
2217
+ };
2218
+ var countFiles = (dir) => {
2219
+ let count = 0;
2220
+ for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
2221
+ if (entry.isDirectory()) {
2222
+ count += countFiles(path12.join(dir, entry.name));
2223
+ } else {
2224
+ count++;
2225
+ }
2226
+ }
2227
+ return count;
2228
+ };
2229
+ var stepDotnetPublish = (project, target, verbose) => {
2230
+ const start = Date.now();
2231
+ const extraArgs = target.extraPublishArgs ?? "-p:CreatePackage=false";
2232
+ const version = resolveAppVersion(project.root, project.csprojPath);
2233
+ const versionArgs = versionPublishArgs(version).join(" ");
2234
+ try {
2235
+ execSync3(
2236
+ `dotnet publish "${project.csprojPath}" -c Release -f ${target.framework} ${extraArgs} ${versionArgs}`,
2237
+ {
2238
+ cwd: project.root,
2239
+ stdio: verbose ? "inherit" : "pipe"
2240
+ }
2241
+ );
2242
+ } catch (e) {
2243
+ const output = formatBuildError(e);
2244
+ console.error(
2245
+ row({
2246
+ glyph: "error",
2247
+ label: "publish .NET",
2248
+ labelWidth: STEP_LABEL_WIDTH,
2249
+ detail: dim("dotnet publish failed")
2250
+ })
2251
+ );
2252
+ console.error(dim(output));
2253
+ if (looksLikeMissingWorkload(output)) printWorkloadHint();
2254
+ else if (looksLikeMissingXcode(output)) printXcodeHint();
2255
+ if (!verbose) {
2256
+ console.error(footer(dim("re-run with --verbose for the full build log.")));
2257
+ }
2258
+ process.exit(1);
2259
+ }
2260
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
2261
+ console.log(
2262
+ row({
2263
+ glyph: "done",
2264
+ label: "publish .NET",
2265
+ labelWidth: STEP_LABEL_WIDTH,
2266
+ detail: `${dim("Release \xB7")} ${value(target.framework)} ${dim(`(${elapsed}s)`)}`
2267
+ })
2268
+ );
2269
+ return path12.join(project.hostDir, "bin", "Release", target.framework);
572
2270
  };
573
- var printRequirements = (reqs) => {
574
- const labelWidth = Math.max(0, ...reqs.map((r) => r.name.length)) + 2;
575
- for (const r of reqs) {
576
- console.log(
2271
+ var stepNativeUpdate = (project, target, packDir, entitlements, settings, io) => {
2272
+ const start = Date.now();
2273
+ let outcome;
2274
+ try {
2275
+ outcome = runNativeUpdate({
2276
+ projectRoot: project.root,
2277
+ settings,
2278
+ packDir,
2279
+ target: target.name,
2280
+ entitlements,
2281
+ io
2282
+ });
2283
+ } catch (error) {
2284
+ if (!(error instanceof NativeUpdateError)) throw error;
2285
+ console.error(
577
2286
  row({
578
- glyph: STATUS_GLYPH[r.status],
579
- label: r.name,
580
- labelWidth,
581
- detail: r.detail ? dim(r.detail) : void 0
2287
+ glyph: "error",
2288
+ label: "native update",
2289
+ labelWidth: STEP_LABEL_WIDTH,
2290
+ detail: dim(error.message)
582
2291
  })
583
2292
  );
584
- if (r.status !== "ok" && r.fix) {
585
- console.log(fixLine(r.fix));
586
- }
2293
+ if (error.detail) console.error(footer(dim(error.detail)));
2294
+ process.exit(1);
587
2295
  }
588
- };
589
- var runDoctor = async () => {
590
- console.log();
591
- console.log(` ${lime("vidra")} ${value("doctor")}`);
592
- console.log();
593
- console.log(footer(dim("checking your environment\u2026")));
594
- console.log();
595
- const reqs = collectRequirements();
596
- printRequirements(reqs);
597
- console.log();
598
- const missing = reqs.filter((r) => r.status === "missing");
599
- if (missing.length === 0) {
2296
+ if (outcome.merged === "no-feed") {
600
2297
  console.log(
601
- footer(
602
- `${dim("all checks passed \u2014 you're ready to run")} ${lime(
603
- "npm run dev"
604
- )}${dim(".")}`
605
- )
2298
+ row({
2299
+ glyph: "manual",
2300
+ label: "merge feed",
2301
+ labelWidth: STEP_LABEL_WIDTH,
2302
+ detail: dim("no vidra.updates.native.feedUrl: this release is packed but the app will never check for it")
2303
+ })
2304
+ );
2305
+ } else if (outcome.merged === "empty-feed") {
2306
+ console.log(
2307
+ row({
2308
+ glyph: "manual",
2309
+ label: "merge feed",
2310
+ labelWidth: STEP_LABEL_WIDTH,
2311
+ detail: dim("nothing downloaded: first release, or the feed is unreachable (see the warning above)")
2312
+ })
2313
+ );
2314
+ } else {
2315
+ console.log(
2316
+ row({
2317
+ glyph: "done",
2318
+ label: "merge feed",
2319
+ labelWidth: STEP_LABEL_WIDTH,
2320
+ detail: `${dim("vpk download \u2192")} ${value(RELEASE_DIR)}`
2321
+ })
606
2322
  );
607
- console.log();
608
- return 0;
609
2323
  }
610
- const n = missing.length;
2324
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
611
2325
  console.log(
612
- footer(
613
- `${dim(
614
- `${n} issue${n === 1 ? "" : "s"} found. apply the ${n === 1 ? "fix" : "fixes"} above, then re-run`
615
- )} ${lime("npm run doctor")}${dim(".")}`
616
- )
2326
+ row({
2327
+ glyph: "done",
2328
+ label: "vpk pack",
2329
+ labelWidth: STEP_LABEL_WIDTH,
2330
+ detail: `${value(`${settings.packId} ${settings.packVersion}`)} ${dim(
2331
+ `\u2192 ${RELEASE_DIR} (${elapsed}s)`
2332
+ )}`
2333
+ })
617
2334
  );
618
- console.log();
619
- return 1;
2335
+ if (!outcome.outputs.portableZip) {
2336
+ console.error(
2337
+ row({
2338
+ glyph: "error",
2339
+ label: "vpk pack",
2340
+ labelWidth: STEP_LABEL_WIDTH,
2341
+ detail: dim(`vpk wrote no portable archive to ${RELEASE_DIR}`)
2342
+ })
2343
+ );
2344
+ process.exit(1);
2345
+ }
2346
+ return outcome;
620
2347
  };
621
- var installWorkload = (csprojPath) => {
622
- const args = csprojPath ? ["workload", "restore", csprojPath] : ["workload", "install", "maui"];
623
- console.log();
2348
+ var stepPublishVelopackWindowsArtifacts = (project, target, packed) => {
2349
+ const outputDir = path12.join(project.root, "dist");
2350
+ fs11.ensureDirSync(outputDir);
2351
+ const zipPath = path12.join(outputDir, artifactName(project, target));
2352
+ fs11.copySync(packed.outputs.portableZip, zipPath, { overwrite: true });
624
2353
  console.log(
625
2354
  row({
626
- glyph: "active",
627
- detail: `${dim("running")} ${value(`${DOTNET} ${args.join(" ")}`)}`
2355
+ glyph: "done",
2356
+ label: packageLabel(target),
2357
+ labelWidth: STEP_LABEL_WIDTH,
2358
+ detail: `${value(path12.basename(zipPath))} ${dim(
2359
+ `(${(fs11.statSync(zipPath).size / (1024 * 1024)).toFixed(1)} MB, from vpk)`
2360
+ )}`
628
2361
  })
629
2362
  );
630
- console.log(
631
- footer(
632
- dim("this can download several hundred MB and take a few minutes.")
633
- )
634
- );
635
- console.log();
636
- try {
637
- execFileSync3(DOTNET, args, { stdio: "inherit" });
638
- return true;
639
- } catch {
640
- console.error();
641
- console.error(row({ glyph: "error", label: "workload install failed" }));
642
- console.error(
643
- footer(
644
- dim(
645
- "if this is a permissions error, your SDK is in a system location and needs elevation:"
646
- )
647
- )
2363
+ if (packed.outputs.setupExe) {
2364
+ const setupName = `${project.projectName}-${project.displayVersion}-Setup.exe`;
2365
+ const setupPath = path12.join(outputDir, setupName);
2366
+ fs11.copySync(packed.outputs.setupExe, setupPath, { overwrite: true });
2367
+ console.log(
2368
+ row({
2369
+ glyph: "done",
2370
+ label: "installer",
2371
+ labelWidth: STEP_LABEL_WIDTH,
2372
+ detail: `${value(setupName)} ${dim("the recommended download; updates apply in place")}`
2373
+ })
648
2374
  );
649
- console.error(fixLine("sudo dotnet workload install maui"));
650
- console.error();
651
- return false;
652
2375
  }
2376
+ return zipPath;
653
2377
  };
654
- var ensureMauiWorkload = async (opts = {}) => {
655
- const dotnet = checkDotnetSdk();
656
- if (dotnet.status === "missing") {
657
- console.log();
658
- console.log(
2378
+ var stepPackage = async (project, target, bundlePath) => {
2379
+ const outputDir = path12.join(project.root, "dist");
2380
+ fs11.ensureDirSync(outputDir);
2381
+ const start = Date.now();
2382
+ let outputPath;
2383
+ try {
2384
+ outputPath = await target.package(bundlePath, outputDir, {
2385
+ projectName: project.projectName,
2386
+ displayVersion: project.displayVersion
2387
+ });
2388
+ } catch (e) {
2389
+ console.error(
659
2390
  row({
660
2391
  glyph: "error",
661
- label: dotnet.name,
662
- detail: dotnet.detail ? dim(dotnet.detail) : void 0
2392
+ label: packageLabel(target),
2393
+ labelWidth: STEP_LABEL_WIDTH,
2394
+ detail: dim("packaging failed")
663
2395
  })
664
2396
  );
665
- if (dotnet.fix) {
666
- console.log(fixLine(dotnet.fix));
667
- }
668
- return false;
2397
+ console.error(dim(formatProcessError(e)));
2398
+ process.exit(1);
669
2399
  }
670
- if (dotnet.status === "unknown") return true;
671
- if (isMauiWorkloadInstalled()) return true;
672
- console.log();
2400
+ const pkgTime = ((Date.now() - start) / 1e3).toFixed(1);
2401
+ const sizeMB = (fs11.statSync(outputPath).size / (1024 * 1024)).toFixed(1);
673
2402
  console.log(
674
2403
  row({
675
- glyph: "error",
676
- label: ".NET MAUI workload",
677
- detail: dim("required but not installed")
2404
+ glyph: "done",
2405
+ label: packageLabel(target),
2406
+ labelWidth: STEP_LABEL_WIDTH,
2407
+ detail: `${value(path12.basename(outputPath))} ${dim(`(${sizeMB} MB, ${pkgTime}s)`)}`
678
2408
  })
679
2409
  );
680
- const interactive = opts.interactive ?? isInteractive();
681
- if (interactive) {
682
- let install = false;
683
- try {
684
- const res = await prompts({
685
- type: "confirm",
686
- name: "install",
687
- message: "Install the .NET MAUI workload now?",
688
- initial: true
689
- });
690
- install = Boolean(res.install);
691
- } catch {
692
- install = false;
693
- }
694
- if (install) {
695
- if (installWorkload(opts.csprojPath) && isMauiWorkloadInstalled()) {
696
- console.log(
697
- row({
698
- glyph: "done",
699
- label: ".NET MAUI workload",
700
- detail: dim("installed")
701
- })
702
- );
703
- return true;
704
- }
705
- return false;
2410
+ return outputPath;
2411
+ };
2412
+
2413
+ // src/commands/bundle.ts
2414
+ import path15 from "path";
2415
+ import crypto2 from "crypto";
2416
+ import fs14 from "fs-extra";
2417
+ import { execSync as execSync4 } from "child_process";
2418
+
2419
+ // src/zip.ts
2420
+ import fs12 from "fs";
2421
+ import path13 from "path";
2422
+ import zlib from "zlib";
2423
+ var LOCAL_HEADER = 67324752;
2424
+ var CENTRAL_HEADER = 33639248;
2425
+ var END_OF_CENTRAL_DIRECTORY = 101010256;
2426
+ var DEFLATE = 8;
2427
+ var DOS_DATE = 33;
2428
+ var DOS_TIME = 0;
2429
+ var crcTable = (() => {
2430
+ const table = new Uint32Array(256);
2431
+ for (let i = 0; i < 256; i++) {
2432
+ let c = i;
2433
+ for (let bit = 0; bit < 8; bit++) {
2434
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
706
2435
  }
2436
+ table[i] = c >>> 0;
707
2437
  }
708
- console.log(fixLine("dotnet workload install maui", "run:"));
709
- console.log(fixLine(MAUI_DOCS, "docs:"));
710
- return false;
711
- };
712
- var printWorkloadHint = () => {
713
- console.error();
714
- console.error(
715
- row({ glyph: "manual", label: "this looks like a missing .NET MAUI workload." })
716
- );
717
- console.error(fixLine("dotnet workload install maui"));
718
- console.error(fixLine("vidra doctor", "check:"));
719
- console.error();
2438
+ return table;
2439
+ })();
2440
+ var crc32 = (data) => {
2441
+ let c = 4294967295;
2442
+ for (const byte of data) {
2443
+ c = crcTable[(c ^ byte) & 255] ^ c >>> 8;
2444
+ }
2445
+ return (c ^ 4294967295) >>> 0;
720
2446
  };
721
- var printXcodeTooOldHint = () => {
722
- console.error();
723
- console.error(
724
- row({
725
- glyph: "manual",
726
- label: "your Xcode is older than the SDK this MAUI workload set expects."
727
- })
728
- );
729
- console.error(
730
- ` ${dim("\u2022")} ${dim("update Xcode (App Store), then")} ${lime("sudo xcodebuild -runFirstLaunch")}`
731
- );
732
- console.error(
733
- ` ${dim("\u2022")} ${dim("or pin the workloads to your Xcode's era:")} ${lime("dotnet workload update --version <set>")}`
734
- );
735
- console.error(fixLine("vidra doctor", "check:"));
736
- console.error();
2447
+ var createZip = (entries) => {
2448
+ const sorted = [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2449
+ const locals = [];
2450
+ const centrals = [];
2451
+ let offset = 0;
2452
+ for (const entry of sorted) {
2453
+ const name = Buffer.from(entry.name.replace(/\\/g, "/"), "utf8");
2454
+ const deflated = zlib.deflateRawSync(entry.content, { level: 9 });
2455
+ const checksum = crc32(entry.content);
2456
+ const local = Buffer.alloc(30);
2457
+ local.writeUInt32LE(LOCAL_HEADER, 0);
2458
+ local.writeUInt16LE(20, 4);
2459
+ local.writeUInt16LE(0, 6);
2460
+ local.writeUInt16LE(DEFLATE, 8);
2461
+ local.writeUInt16LE(DOS_TIME, 10);
2462
+ local.writeUInt16LE(DOS_DATE, 12);
2463
+ local.writeUInt32LE(checksum, 14);
2464
+ local.writeUInt32LE(deflated.length, 18);
2465
+ local.writeUInt32LE(entry.content.length, 22);
2466
+ local.writeUInt16LE(name.length, 26);
2467
+ local.writeUInt16LE(0, 28);
2468
+ locals.push(local, name, deflated);
2469
+ const central = Buffer.alloc(46);
2470
+ central.writeUInt32LE(CENTRAL_HEADER, 0);
2471
+ central.writeUInt16LE(20, 4);
2472
+ central.writeUInt16LE(20, 6);
2473
+ central.writeUInt16LE(0, 8);
2474
+ central.writeUInt16LE(DEFLATE, 10);
2475
+ central.writeUInt16LE(DOS_TIME, 12);
2476
+ central.writeUInt16LE(DOS_DATE, 14);
2477
+ central.writeUInt32LE(checksum, 16);
2478
+ central.writeUInt32LE(deflated.length, 20);
2479
+ central.writeUInt32LE(entry.content.length, 24);
2480
+ central.writeUInt16LE(name.length, 28);
2481
+ central.writeUInt16LE(0, 30);
2482
+ central.writeUInt16LE(0, 32);
2483
+ central.writeUInt16LE(0, 34);
2484
+ central.writeUInt16LE(0, 36);
2485
+ central.writeUInt32LE(33188 << 16 >>> 0, 38);
2486
+ central.writeUInt32LE(offset, 42);
2487
+ centrals.push(central, name);
2488
+ offset += local.length + name.length + deflated.length;
2489
+ }
2490
+ const centralDirectory = Buffer.concat(centrals);
2491
+ const end = Buffer.alloc(22);
2492
+ end.writeUInt32LE(END_OF_CENTRAL_DIRECTORY, 0);
2493
+ end.writeUInt16LE(0, 4);
2494
+ end.writeUInt16LE(0, 6);
2495
+ end.writeUInt16LE(sorted.length, 8);
2496
+ end.writeUInt16LE(sorted.length, 10);
2497
+ end.writeUInt32LE(centralDirectory.length, 12);
2498
+ end.writeUInt32LE(offset, 16);
2499
+ end.writeUInt16LE(0, 20);
2500
+ return Buffer.concat([...locals, centralDirectory, end]);
737
2501
  };
738
- var printXcodeHint = () => {
739
- console.error();
740
- console.error(
741
- row({
742
- glyph: "manual",
743
- label: "Mac Catalyst needs the full Xcode app, not just the Command Line Tools."
744
- })
745
- );
746
- console.error(` ${dim("1.")} ${value("install Xcode from the App Store")}`);
747
- console.error(
748
- ` ${dim("2.")} ${lime(
749
- "sudo xcode-select -s /Applications/Xcode.app/Contents/Developer"
750
- )}`
751
- );
752
- console.error(` ${dim("3.")} ${lime("sudo xcodebuild -runFirstLaunch")}`);
753
- console.error(fixLine("vidra doctor", "check:"));
754
- console.error();
2502
+ var readDirectoryEntries = (root) => {
2503
+ const entries = [];
2504
+ const walk = (dir, prefix) => {
2505
+ for (const item of fs12.readdirSync(dir, { withFileTypes: true }).sort(
2506
+ (a, b) => a.name < b.name ? -1 : 1
2507
+ )) {
2508
+ const full = path13.join(dir, item.name);
2509
+ const name = prefix ? `${prefix}/${item.name}` : item.name;
2510
+ if (item.isDirectory()) {
2511
+ walk(full, name);
2512
+ } else if (item.isFile()) {
2513
+ entries.push({ name, content: fs12.readFileSync(full) });
2514
+ }
2515
+ }
2516
+ };
2517
+ walk(root, "");
2518
+ return entries;
755
2519
  };
756
2520
 
757
- // src/commands/build.ts
758
- var TARGETS = {
759
- macos: macosTarget,
760
- windows: windowsTarget
2521
+ // src/manifest-signing.ts
2522
+ import crypto from "crypto";
2523
+ import fs13 from "fs";
2524
+ import path14 from "path";
2525
+ var SIGNATURE_ALGORITHM = "ecdsa-p256-sha256";
2526
+ var SIGNATURE_FILE = "bundles.json.sig";
2527
+ var generateKeyPair = () => {
2528
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
2529
+ namedCurve: "prime256v1"
2530
+ });
2531
+ const spki = publicKey.export({ format: "der", type: "spki" });
2532
+ return {
2533
+ privateKeyPem: privateKey.export({ format: "pem", type: "pkcs8" }).toString(),
2534
+ publicKey: spki.toString("base64"),
2535
+ keyId: keyIdOf(spki)
2536
+ };
761
2537
  };
762
- var packageLabel = (target) => target.name === "macos" ? "package DMG" : "package ZIP";
763
- var artifactName = (project, target) => `${project.projectName}-${project.displayVersion}-${target.name}.${target.name === "macos" ? "dmg" : "zip"}`;
764
- var buildCommand = async (argv) => {
765
- const args = parseArgs(["_", "_", ...argv]);
766
- const verbose = !!args["verbose"];
767
- const plan = !!args["plan"] || !!args["dry-run"];
768
- const targetName = args["target"] || detectPlatform();
769
- const target = TARGETS[targetName];
770
- if (!target) {
771
- const supported = Object.keys(TARGETS).join(", ");
772
- console.error();
773
- console.error(
774
- row({
775
- glyph: "error",
776
- detail: dim(`unsupported target: ${targetName} \u2014 supported: ${supported}`)
777
- })
2538
+ var keyIdOf = (spki) => crypto.createHash("sha256").update(spki).digest("hex").slice(0, 8);
2539
+ var publicKeyFor = (privateKeyPem) => {
2540
+ const privateKey = crypto.createPrivateKey(privateKeyPem);
2541
+ const spki = crypto.createPublicKey(privateKey).export({
2542
+ format: "der",
2543
+ type: "spki"
2544
+ });
2545
+ return { publicKey: spki.toString("base64"), keyId: keyIdOf(spki) };
2546
+ };
2547
+ var signManifest = (manifestBytes, privateKeyPem) => {
2548
+ const privateKey = crypto.createPrivateKey(privateKeyPem);
2549
+ if (privateKey.asymmetricKeyType !== "ec") {
2550
+ throw new Error(
2551
+ `the signing key is ${privateKey.asymmetricKeyType ?? "of an unknown type"}; vidra signs with an ECDSA P-256 key \u2014 make one with \`vidra keygen\``
2552
+ );
2553
+ }
2554
+ const curve = privateKey.asymmetricKeyDetails?.namedCurve;
2555
+ if (curve && curve !== "prime256v1") {
2556
+ throw new Error(`the signing key uses curve ${curve}; vidra signs with P-256 (prime256v1)`);
2557
+ }
2558
+ const signature = crypto.sign("sha256", manifestBytes, privateKey);
2559
+ const { keyId } = publicKeyFor(privateKeyPem);
2560
+ return {
2561
+ algorithm: SIGNATURE_ALGORITHM,
2562
+ keyId,
2563
+ signature: signature.toString("base64")
2564
+ };
2565
+ };
2566
+ var verifyManifest = (manifestBytes, document, publicKeyBase64) => {
2567
+ if (document.algorithm !== SIGNATURE_ALGORITHM) return false;
2568
+ try {
2569
+ const publicKey = crypto.createPublicKey({
2570
+ key: Buffer.from(publicKeyBase64, "base64"),
2571
+ format: "der",
2572
+ type: "spki"
2573
+ });
2574
+ return crypto.verify(
2575
+ "sha256",
2576
+ manifestBytes,
2577
+ publicKey,
2578
+ Buffer.from(document.signature, "base64")
778
2579
  );
779
- process.exit(1);
2580
+ } catch {
2581
+ return false;
780
2582
  }
781
- const project = detectProject(process.cwd());
782
- console.log();
783
- console.log(
784
- header("build", `${target.name} \xB7 Release${plan ? " \xB7 plan" : ""}`)
785
- );
786
- console.log(kv("project", project.projectName));
787
- console.log(kv("target", target.framework));
788
- console.log();
789
- if (plan) {
790
- printBuildPlan(project, target);
791
- console.log();
792
- console.log(
793
- footer(`${dim("nothing has run. re-run without")} ${lime("--plan")} ${dim("to apply.")}`)
794
- );
795
- console.log();
796
- return;
2583
+ };
2584
+ var resolveSigningKey = (keyPath) => {
2585
+ if (keyPath) {
2586
+ if (!fs13.existsSync(keyPath)) {
2587
+ throw new Error(`no signing key at ${keyPath}`);
2588
+ }
2589
+ return fs13.readFileSync(keyPath, "utf8");
797
2590
  }
798
- if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
799
- process.exit(1);
2591
+ const fromEnvironment = process.env.VIDRA_UPDATE_SIGNING_KEY;
2592
+ if (fromEnvironment && fromEnvironment.includes("PRIVATE KEY")) {
2593
+ return fromEnvironment;
800
2594
  }
801
- stepBuildUi(project, verbose);
802
- stepCopyAssets(project);
803
- const publishDir = stepDotnetPublish(project, target, verbose);
804
- const bundlePath = target.findBundle(publishDir, project.projectName);
805
- if (!bundlePath) {
2595
+ return null;
2596
+ };
2597
+ var writeSignature = (outDir, document) => {
2598
+ const target = path14.join(outDir, SIGNATURE_FILE);
2599
+ fs13.writeFileSync(target, `${JSON.stringify(document, null, 2)}
2600
+ `);
2601
+ return target;
2602
+ };
2603
+
2604
+ // src/commands/bundle.ts
2605
+ var SCHEMA = 1;
2606
+ var parseBundleOptions = (argv) => {
2607
+ const args = parseArgs(["_", "_", ...argv]);
2608
+ return {
2609
+ out: typeof args.out === "string" ? args.out : "dist",
2610
+ channel: typeof args.channel === "string" ? args.channel : void 0,
2611
+ skipBuild: !!args["skip-build"],
2612
+ sign: typeof args.sign === "string" ? args.sign : void 0,
2613
+ mergeFrom: typeof args["merge-from"] === "string" ? args["merge-from"] : void 0
2614
+ };
2615
+ };
2616
+ var bundleCommand = async (argv) => {
2617
+ const options = parseBundleOptions(argv);
2618
+ const project = detectProject(process.cwd());
2619
+ const outDir = path15.resolve(project.root, options.out);
2620
+ const channel = options.channel;
2621
+ console.log(header("bundle", `${project.projectName} ${project.displayVersion}`));
2622
+ if (!options.skipBuild) {
2623
+ stepBuildUi2(project);
2624
+ }
2625
+ const dist = path15.join(project.uiDir, "dist");
2626
+ if (!fs14.existsSync(path15.join(dist, "index.html"))) {
806
2627
  console.error(
807
2628
  row({
808
2629
  glyph: "error",
809
- detail: dim(`could not find build artifact in ${publishDir}`)
2630
+ label: "read ui/dist",
2631
+ labelWidth: STEP_LABEL_WIDTH,
2632
+ detail: dim("no index.html \u2014 run the UI build first, or drop --skip-build")
810
2633
  })
811
2634
  );
812
2635
  process.exit(1);
813
2636
  }
814
- if (target.name === "macos") {
815
- signMacAppBundleIfPossible(bundlePath, {
816
- verbose,
817
- log: console.log,
818
- warn: console.warn
819
- });
820
- }
821
- const outputPath = await stepPackage(project, target, bundlePath);
822
- console.log();
823
- console.log(
824
- footer(
825
- `${dim("done \u2014")} ${value(path5.relative(project.root, outputPath))}`
826
- )
827
- );
828
- console.log();
829
- };
830
- var printBuildPlan = (project, target) => {
2637
+ const fingerprints = readFingerprints(project);
2638
+ const entries = readDirectoryEntries(dist);
2639
+ const archive = createZip(entries);
2640
+ const sha256 = crypto2.createHash("sha256").update(archive).digest("hex");
2641
+ const name = `bundle-${project.displayVersion}-${sha256.slice(0, 8)}.zip`;
2642
+ fs14.ensureDirSync(outDir);
2643
+ fs14.writeFileSync(path15.join(outDir, name), archive);
831
2644
  console.log(
832
2645
  row({
833
2646
  glyph: "done",
834
- label: "build UI",
2647
+ label: "pack bundle",
835
2648
  labelWidth: STEP_LABEL_WIDTH,
836
- detail: `${dim("vite \u2192")} ${value("ui/dist")}`
2649
+ detail: `${value(name)} ${dim(`(${entries.length} files, ${formatBytes(archive.length)})`)}`
837
2650
  })
838
2651
  );
2652
+ const entry = {
2653
+ version: project.displayVersion,
2654
+ url: name,
2655
+ sha256,
2656
+ size: archive.length,
2657
+ coreFingerprint: fingerprints.core,
2658
+ appFingerprint: fingerprints.app,
2659
+ ...channel ? { channel } : {}
2660
+ };
2661
+ const manifestPath = path15.join(outDir, "bundles.json");
2662
+ const base = await loadBaseManifest(options, outDir, signingKeyFor(options));
2663
+ const manifest = mergeManifest(base, entry);
2664
+ const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}
2665
+ `);
2666
+ fs14.writeFileSync(manifestPath, manifestBytes);
839
2667
  console.log(
840
2668
  row({
841
2669
  glyph: "done",
842
- label: "copy assets",
2670
+ label: "write feed",
843
2671
  labelWidth: STEP_LABEL_WIDTH,
844
- detail: `${dim("\u2192")} ${value("Resources/Raw/wwwroot")}`
2672
+ detail: `${value("bundles.json")} ${dim(`(${manifest.bundles.length} entries)`)}`
845
2673
  })
846
2674
  );
2675
+ stepSignManifest(project, outDir, manifestBytes, options.sign);
847
2676
  console.log(
848
2677
  row({
849
- glyph: "done",
850
- label: "publish .NET",
2678
+ glyph: "plan",
2679
+ label: "compatible",
851
2680
  labelWidth: STEP_LABEL_WIDTH,
852
- detail: `${dim("Release \xB7")} ${value(target.framework)}`
2681
+ detail: `${dim("core")} ${value(fingerprints.core.slice(0, 12))} ${dim("app")} ${value(fingerprints.app.slice(0, 12))}`
853
2682
  })
854
2683
  );
855
- if (target.name === "macos") {
2684
+ console.log(
2685
+ footer(
2686
+ dim(
2687
+ `upload ${value(path15.relative(project.root, outDir) || "dist")}/ to your feed host \u2014 every file, and bundles.json last`
2688
+ )
2689
+ )
2690
+ );
2691
+ };
2692
+ var loadBaseManifest = async (options, outDir, privateKeyPem) => {
2693
+ if (!options.mergeFrom) {
2694
+ return readManifest(path15.join(outDir, "bundles.json"));
2695
+ }
2696
+ let fetched;
2697
+ try {
2698
+ fetched = await fetchFeed(options.mergeFrom);
2699
+ } catch (error) {
2700
+ return fail("merge feed", `could not read ${options.mergeFrom}: ${error.message}`);
2701
+ }
2702
+ if (!fetched) {
856
2703
  console.log(
857
2704
  row({
858
- glyph: "done",
859
- label: "codesign .app",
2705
+ glyph: "plan",
2706
+ label: "merge feed",
860
2707
  labelWidth: STEP_LABEL_WIDTH,
861
- detail: dim("Apple Development, or ad-hoc (-)")
2708
+ detail: dim(`no index at ${options.mergeFrom} yet \u2014 publishing the first one`)
862
2709
  })
863
2710
  );
2711
+ return { schema: SCHEMA, bundles: [] };
2712
+ }
2713
+ if (privateKeyPem) {
2714
+ const { publicKey } = publicKeyFor(privateKeyPem);
2715
+ const ok = fetched.signature !== null && verifyManifest(Buffer.from(fetched.manifest), safeParse(fetched.signature), publicKey);
2716
+ if (!ok) {
2717
+ fail(
2718
+ "merge feed",
2719
+ `the index at ${options.mergeFrom} is not signed by your key \u2014 refusing to merge and re-sign it, which would publish someone else's entries under your signature`
2720
+ );
2721
+ }
2722
+ }
2723
+ const base = parseManifest(fetched.manifest);
2724
+ console.log(
2725
+ row({
2726
+ glyph: "done",
2727
+ label: "merge feed",
2728
+ labelWidth: STEP_LABEL_WIDTH,
2729
+ detail: `${value(options.mergeFrom)} ${dim(`(${base.bundles.length} existing entries)`)}`
2730
+ })
2731
+ );
2732
+ return base;
2733
+ };
2734
+ var fetchFeed = async (source) => {
2735
+ if (/^https?:\/\//i.test(source)) {
2736
+ const response = await fetch(source);
2737
+ if (response.status === 404) return null;
2738
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
2739
+ const manifest = await response.text();
2740
+ const signatureResponse = await fetch(`${source}.sig`);
2741
+ return {
2742
+ manifest,
2743
+ signature: signatureResponse.ok ? await signatureResponse.text() : null
2744
+ };
2745
+ }
2746
+ const manifestPath = fs14.existsSync(source) && fs14.statSync(source).isDirectory() ? path15.join(source, "bundles.json") : source;
2747
+ if (!fs14.existsSync(manifestPath)) return null;
2748
+ const signaturePath = `${manifestPath}.sig`;
2749
+ return {
2750
+ manifest: fs14.readFileSync(manifestPath, "utf8"),
2751
+ signature: fs14.existsSync(signaturePath) ? fs14.readFileSync(signaturePath, "utf8") : null
2752
+ };
2753
+ };
2754
+ var safeParse = (text) => {
2755
+ try {
2756
+ return JSON.parse(text);
2757
+ } catch {
2758
+ return { algorithm: "", keyId: "", signature: "" };
2759
+ }
2760
+ };
2761
+ var signingKeyFor = (options) => {
2762
+ try {
2763
+ return resolveSigningKey(options.sign);
2764
+ } catch {
2765
+ return null;
2766
+ }
2767
+ };
2768
+ var stepSignManifest = (project, outDir, manifestBytes, keyPath) => {
2769
+ const configuredKeys = readUpdateConfig(project.root)?.publicKeys ?? [];
2770
+ let privateKeyPem = null;
2771
+ try {
2772
+ privateKeyPem = resolveSigningKey(keyPath);
2773
+ } catch (error) {
2774
+ fail("sign feed", error.message);
2775
+ }
2776
+ if (!privateKeyPem) {
2777
+ if (configuredKeys.length > 0) {
2778
+ fail(
2779
+ "sign feed",
2780
+ "this app trusts a signing key, so an unsigned feed would be refused by every installed copy \u2014 pass --sign <key.pem> or set VIDRA_UPDATE_SIGNING_KEY"
2781
+ );
2782
+ }
2783
+ fs14.removeSync(path15.join(outDir, "bundles.json.sig"));
864
2784
  console.log(
865
2785
  row({
866
- glyph: "plan",
867
- label: "notarize",
2786
+ glyph: "manual",
2787
+ label: "sign feed",
868
2788
  labelWidth: STEP_LABEL_WIDTH,
869
- detail: planBadge()
2789
+ detail: amber(
2790
+ "unsigned \u2014 anyone who can write to your feed host can run code in your app"
2791
+ )
870
2792
  })
871
2793
  );
872
- console.log(
2794
+ return;
2795
+ }
2796
+ let document;
2797
+ let publicKey;
2798
+ try {
2799
+ document = signManifest(manifestBytes, privateKeyPem);
2800
+ publicKey = publicKeyFor(privateKeyPem).publicKey;
2801
+ } catch (error) {
2802
+ return fail("sign feed", error.message);
2803
+ }
2804
+ writeSignature(outDir, document);
2805
+ if (configuredKeys.length > 0 && !configuredKeys.includes(publicKey)) {
2806
+ fail(
2807
+ "sign feed",
2808
+ `signed with key ${document.keyId}, which is not among the ${configuredKeys.length} key(s) in package.json \u2014 installed apps would reject this feed`
2809
+ );
2810
+ }
2811
+ console.log(
2812
+ row({
2813
+ glyph: "done",
2814
+ label: "sign feed",
2815
+ labelWidth: STEP_LABEL_WIDTH,
2816
+ detail: `${value("bundles.json.sig")} ${dim(`(key ${document.keyId})`)}`
2817
+ })
2818
+ );
2819
+ };
2820
+ var stepBuildUi2 = (project) => {
2821
+ try {
2822
+ execSync4("npm run build", { cwd: project.uiDir, stdio: "pipe" });
2823
+ } catch {
2824
+ console.error(
873
2825
  row({
874
- glyph: "active",
875
- label: "package DMG",
2826
+ glyph: "error",
2827
+ label: "build UI",
876
2828
  labelWidth: STEP_LABEL_WIDTH,
877
- detail: `${dim("hdiutil UDZO \u2192")} ${value(artifactName(project, target))}`
2829
+ detail: dim("vite build failed")
878
2830
  })
879
2831
  );
880
- } else {
881
- console.log(
2832
+ process.exit(1);
2833
+ }
2834
+ console.log(
2835
+ row({
2836
+ glyph: "done",
2837
+ label: "build UI",
2838
+ labelWidth: STEP_LABEL_WIDTH,
2839
+ detail: `${dim("vite \u2192")} ${value("ui/dist")}`
2840
+ })
2841
+ );
2842
+ };
2843
+ var readFingerprints = (project) => {
2844
+ const appManifest = path15.join(project.uiDir, "src", "generated", "manifest.json");
2845
+ const app = readFingerprintFile(appManifest);
2846
+ if (!app) {
2847
+ fail(
2848
+ "read fingerprints",
2849
+ `no app contract fingerprint in ${path15.relative(project.root, appManifest)} \u2014 build the host once to generate it`
2850
+ );
2851
+ }
2852
+ const sdkManifest = path15.join(
2853
+ project.uiDir,
2854
+ "node_modules",
2855
+ "@vidra-dev",
2856
+ "sdk",
2857
+ "dist",
2858
+ "manifest.json"
2859
+ );
2860
+ const core = readFingerprintFile(sdkManifest);
2861
+ if (!core) {
2862
+ fail(
2863
+ "read fingerprints",
2864
+ "no core contract fingerprint from @vidra-dev/sdk \u2014 run npm install in ui/, and make sure the SDK is 0.4.0 or newer"
2865
+ );
2866
+ }
2867
+ return { core, app };
2868
+ };
2869
+ var readFingerprintFile = (file) => {
2870
+ if (!fs14.existsSync(file)) return null;
2871
+ try {
2872
+ const parsed = fs14.readJsonSync(file);
2873
+ return typeof parsed.fingerprint === "string" && parsed.fingerprint.length > 0 ? parsed.fingerprint : null;
2874
+ } catch {
2875
+ return null;
2876
+ }
2877
+ };
2878
+ var readManifest = (file) => {
2879
+ if (!fs14.existsSync(file)) return { schema: SCHEMA, bundles: [] };
2880
+ return parseManifest(fs14.readFileSync(file, "utf8"));
2881
+ };
2882
+ var parseManifest = (text) => {
2883
+ try {
2884
+ const parsed = JSON.parse(text);
2885
+ if (parsed.schema !== SCHEMA || !Array.isArray(parsed.bundles)) {
2886
+ return { schema: SCHEMA, bundles: [] };
2887
+ }
2888
+ return { schema: SCHEMA, bundles: parsed.bundles };
2889
+ } catch {
2890
+ return { schema: SCHEMA, bundles: [] };
2891
+ }
2892
+ };
2893
+ var mergeManifest = (manifest, entry) => {
2894
+ const supersedes = (existing) => existing.version === entry.version && (existing.channel ?? null) === (entry.channel ?? null) && existing.coreFingerprint === entry.coreFingerprint && existing.appFingerprint === entry.appFingerprint;
2895
+ return {
2896
+ schema: SCHEMA,
2897
+ bundles: [...manifest.bundles.filter((e) => !supersedes(e)), entry]
2898
+ };
2899
+ };
2900
+ var formatBytes = (bytes) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2901
+ var fail = (label, detail) => {
2902
+ console.error(row({ glyph: "error", label, labelWidth: STEP_LABEL_WIDTH, detail: dim(detail) }));
2903
+ process.exit(1);
2904
+ };
2905
+
2906
+ // src/commands/keygen.ts
2907
+ import fs15 from "fs";
2908
+ import path16 from "path";
2909
+ var keygenCommand = async (argv) => {
2910
+ const args = parseArgs(["_", "_", ...argv]);
2911
+ const out = path16.resolve(
2912
+ typeof args.out === "string" ? args.out : "vidra-signing-key.pem"
2913
+ );
2914
+ const force = !!args.force;
2915
+ console.log(header("keygen", "update signing key"));
2916
+ if (fs15.existsSync(out) && !force) {
2917
+ console.error(
882
2918
  row({
883
- glyph: "active",
884
- label: "package ZIP",
2919
+ glyph: "error",
2920
+ label: "write key",
885
2921
  labelWidth: STEP_LABEL_WIDTH,
886
- detail: `${dim("self-contained \u2192")} ${value(artifactName(project, target))}`
2922
+ detail: dim(
2923
+ `${path16.basename(out)} already exists \u2014 apps trusting it could not install updates signed by a new key. Pass --force only if you mean to replace it.`
2924
+ )
887
2925
  })
888
2926
  );
2927
+ process.exit(1);
2928
+ }
2929
+ const { privateKeyPem, publicKey, keyId } = generateKeyPair();
2930
+ fs15.writeFileSync(out, privateKeyPem, { mode: 384 });
2931
+ try {
2932
+ fs15.chmodSync(out, 384);
2933
+ } catch {
2934
+ }
2935
+ console.log(
2936
+ row({
2937
+ glyph: "done",
2938
+ label: "private key",
2939
+ labelWidth: STEP_LABEL_WIDTH,
2940
+ detail: `${value(path16.relative(process.cwd(), out) || out)} ${dim(`(key ${keyId})`)}`
2941
+ })
2942
+ );
2943
+ const publicKeyPath = `${out}.pub`;
2944
+ fs15.writeFileSync(publicKeyPath, `${publicKey}
2945
+ `);
2946
+ console.log(
2947
+ row({
2948
+ glyph: "done",
2949
+ label: "public key",
2950
+ labelWidth: STEP_LABEL_WIDTH,
2951
+ detail: `${value(path16.relative(process.cwd(), publicKeyPath) || publicKeyPath)} ${dim("\u2014 add it to your app's package.json:")}`
2952
+ })
2953
+ );
2954
+ console.log();
2955
+ console.log(
2956
+ value(
2957
+ JSON.stringify({ vidra: { updates: { publicKeys: [publicKey] } } }, null, 2).split("\n").map((line) => ` ${line}`).join("\n")
2958
+ )
2959
+ );
2960
+ console.log();
2961
+ console.log(
2962
+ row({
2963
+ glyph: "manual",
2964
+ label: "keep it safe",
2965
+ labelWidth: STEP_LABEL_WIDTH,
2966
+ detail: amber(
2967
+ "this key is the authority to run code in every installed copy of your app"
2968
+ )
2969
+ })
2970
+ );
2971
+ console.log(
2972
+ footer(
2973
+ dim(
2974
+ `never commit it \xB7 back it up \xB7 in CI pass it as ${value("VIDRA_UPDATE_SIGNING_KEY")} and sign with ${value("vidra bundle --sign")}`
2975
+ )
2976
+ )
2977
+ );
2978
+ };
2979
+
2980
+ // src/commands/verify.ts
2981
+ import path18 from "path";
2982
+ import fs17 from "fs-extra";
2983
+
2984
+ // src/artifacts.ts
2985
+ import path17 from "path";
2986
+ import os5 from "os";
2987
+ import fs16 from "fs-extra";
2988
+ import { execFileSync as execFileSync8 } from "child_process";
2989
+ var findAppBundle2 = (dir) => {
2990
+ if (!fs16.existsSync(dir)) return null;
2991
+ for (const entry of fs16.readdirSync(dir, { withFileTypes: true })) {
2992
+ if (entry.name.endsWith(".app") && entry.isDirectory()) {
2993
+ return path17.join(dir, entry.name);
2994
+ }
889
2995
  }
2996
+ return null;
890
2997
  };
891
- var stepBuildUi = (project, verbose) => {
892
- const start = Date.now();
2998
+ var findWindowsExecutable = (dir, projectName) => {
2999
+ if (!fs16.existsSync(dir)) return null;
3000
+ const exes = fs16.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.toLowerCase().endsWith(".exe")).map((e) => e.name);
3001
+ const wanted = projectName?.toLowerCase();
3002
+ const preferred = wanted && exes.find((n) => n.toLowerCase() === `${wanted}.host.exe`) || wanted && exes.find((n) => n.toLowerCase().startsWith(wanted)) || exes.find((n) => n.toLowerCase().endsWith(".host.exe")) || exes.find((n) => n.toLowerCase() !== "createdump.exe");
3003
+ return preferred ? path17.join(dir, preferred) : null;
3004
+ };
3005
+ var findWindowsExecutableRecursive = (root, projectName) => {
3006
+ const direct = findWindowsExecutable(root, projectName);
3007
+ if (direct) return direct;
3008
+ if (!fs16.existsSync(root)) return null;
3009
+ for (const entry of fs16.readdirSync(root, { withFileTypes: true })) {
3010
+ if (!entry.isDirectory()) continue;
3011
+ const found = findWindowsExecutableRecursive(path17.join(root, entry.name), projectName);
3012
+ if (found) return found;
3013
+ }
3014
+ return null;
3015
+ };
3016
+ var extractArchive = (zipPath) => {
3017
+ const dir = fs16.mkdtempSync(path17.join(os5.tmpdir(), "vidra-zip-"));
893
3018
  try {
894
- execSync3("npm run build", {
895
- cwd: project.uiDir,
896
- stdio: verbose ? "inherit" : "pipe"
897
- });
898
- } catch (e) {
3019
+ if (process.platform === "win32") {
3020
+ execFileSync8(
3021
+ "powershell",
3022
+ [
3023
+ "-NoProfile",
3024
+ "-Command",
3025
+ `Expand-Archive -Path "${zipPath}" -DestinationPath "${dir}" -Force`
3026
+ ],
3027
+ { stdio: "pipe" }
3028
+ );
3029
+ } else {
3030
+ execFileSync8("unzip", ["-q", "-o", zipPath, "-d", dir], { stdio: "pipe" });
3031
+ }
3032
+ } catch (error) {
3033
+ fs16.removeSync(dir);
3034
+ throw error;
3035
+ }
3036
+ return { dir, release: () => fs16.removeSync(dir) };
3037
+ };
3038
+ var mountDiskImage = (dmgPath) => {
3039
+ const mountPoint = fs16.mkdtempSync(path17.join(os5.tmpdir(), "vidra-dmg-mount-"));
3040
+ execFileSync8(
3041
+ "hdiutil",
3042
+ ["attach", dmgPath, "-mountpoint", mountPoint, "-nobrowse", "-quiet"],
3043
+ { stdio: "pipe" }
3044
+ );
3045
+ return {
3046
+ mountPoint,
3047
+ release: () => {
3048
+ try {
3049
+ execFileSync8("hdiutil", ["detach", mountPoint, "-quiet"], { stdio: "pipe" });
3050
+ } catch {
3051
+ }
3052
+ fs16.removeSync(mountPoint);
3053
+ }
3054
+ };
3055
+ };
3056
+
3057
+ // src/commands/verify.ts
3058
+ var verifyCommand = async (argv) => {
3059
+ const args = parseArgs(["_", "_", ...argv]);
3060
+ const explicit = args._[0];
3061
+ const artifact = explicit ?? discoverArtifact();
3062
+ if (!artifact) {
3063
+ console.error();
899
3064
  console.error(
900
3065
  row({
901
3066
  glyph: "error",
902
- label: "build UI",
903
- labelWidth: STEP_LABEL_WIDTH,
904
- detail: dim("vite build failed")
3067
+ detail: dim("no artifact given and none found in dist/ \u2014 pass a path")
905
3068
  })
906
3069
  );
907
- console.error(dim(formatBuildError(e)));
908
3070
  process.exit(1);
909
3071
  }
910
- const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
911
- console.log(
912
- row({
913
- glyph: "done",
914
- label: "build UI",
915
- labelWidth: STEP_LABEL_WIDTH,
916
- detail: `${dim("vite \u2192")} ${value("ui/dist")} ${dim(`(${elapsed}s)`)}`
917
- })
918
- );
919
- };
920
- var stepCopyAssets = (project) => {
921
- const viteDist = path5.join(project.uiDir, "dist");
922
- if (!fs4.existsSync(viteDist)) {
923
- console.error(
924
- row({
925
- glyph: "error",
926
- label: "copy assets",
927
- labelWidth: STEP_LABEL_WIDTH,
928
- detail: dim("ui/dist not found \u2014 vite build may have failed")
929
- })
3072
+ if (!fs17.existsSync(artifact)) {
3073
+ console.error();
3074
+ console.error(row({ glyph: "error", detail: dim(`not found: ${artifact}`) }));
3075
+ process.exit(1);
3076
+ }
3077
+ console.log();
3078
+ console.log(header("verify", path18.basename(artifact)));
3079
+ console.log(kv("artifact", artifact));
3080
+ console.log();
3081
+ const failures = artifactKind(artifact) === "macos" ? verifyMacArtifact(artifact) : verifyWindowsArtifact(artifact);
3082
+ console.log();
3083
+ if (failures.length > 0) {
3084
+ console.log(
3085
+ footer(dim(`${failures.length} check(s) failed: ${failures.join(", ")}`))
930
3086
  );
3087
+ console.log();
931
3088
  process.exit(1);
932
3089
  }
933
- const wwwroot = path5.join(project.hostDir, "Resources", "Raw", "wwwroot");
934
- fs4.removeSync(wwwroot);
935
- fs4.copySync(viteDist, wwwroot);
936
- const fileCount = countFiles(wwwroot);
937
- console.log(
938
- row({
939
- glyph: "done",
940
- label: "copy assets",
941
- labelWidth: STEP_LABEL_WIDTH,
942
- detail: `${dim("\u2192")} ${value("Resources/Raw/wwwroot")} ${dim(`(${fileCount} files)`)}`
943
- })
944
- );
3090
+ console.log(footer(dim("all checks passed")));
3091
+ console.log();
945
3092
  };
946
- var countFiles = (dir) => {
947
- let count = 0;
948
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
949
- if (entry.isDirectory()) {
950
- count += countFiles(path5.join(dir, entry.name));
951
- } else {
952
- count++;
3093
+ var artifactKind = (artifact) => {
3094
+ if (artifact.endsWith(".dmg") || artifact.endsWith(".app")) return "macos";
3095
+ return "windows";
3096
+ };
3097
+ var verifyMacArtifact = (artifact) => {
3098
+ const failures = [];
3099
+ let appBundle = artifact;
3100
+ let mounted = null;
3101
+ if (artifact.endsWith(".dmg")) {
3102
+ const dmgSig = verifyMacSignature(artifact);
3103
+ report(dmgSig.ok, "dmg signature", dmgSig.ok ? "signed" : "not signed", dmgSig.output);
3104
+ if (!dmgSig.ok) failures.push("dmg signature");
3105
+ const image = mountDiskImage(artifact);
3106
+ mounted = image;
3107
+ const inner = findAppBundle2(image.mountPoint);
3108
+ if (!inner) {
3109
+ image.release();
3110
+ report(false, "app bundle", "no .app inside the disk image");
3111
+ return [...failures, "app bundle"];
953
3112
  }
3113
+ appBundle = inner;
954
3114
  }
955
- return count;
956
- };
957
- var stepDotnetPublish = (project, target, verbose) => {
958
- const start = Date.now();
959
- const extraArgs = target.extraPublishArgs ?? "-p:CreatePackage=false";
960
3115
  try {
961
- execSync3(
962
- `dotnet publish "${project.csprojPath}" -c Release -f ${target.framework} ${extraArgs}`,
963
- {
964
- cwd: project.root,
965
- stdio: verbose ? "inherit" : "pipe"
966
- }
3116
+ const sig = verifyMacSignature(appBundle);
3117
+ report(sig.ok, "signature", sig.ok ? "codesign --verify --strict passed" : "FAILED", sig.output);
3118
+ if (!sig.ok) failures.push("signature");
3119
+ const hardening = inspectMacHardening(appBundle);
3120
+ report(
3121
+ hardening.hardened,
3122
+ "hardened runtime",
3123
+ hardening.hardened ? "enabled" : "NOT enabled \u2014 cannot be notarized"
967
3124
  );
968
- } catch (e) {
969
- const output = formatBuildError(e);
970
- console.error(
3125
+ if (!hardening.hardened) failures.push("hardened runtime");
3126
+ const entitlementsOk = hardening.missing.length === 0;
3127
+ report(
3128
+ entitlementsOk,
3129
+ "entitlements",
3130
+ entitlementsOk ? `${REQUIRED_MAC_ENTITLEMENTS.length} required present` : `missing: ${hardening.missing.join(", ")} \u2014 the .NET JIT will be killed at launch`
3131
+ );
3132
+ if (!entitlementsOk) failures.push("entitlements");
3133
+ const assessment = assessGatekeeper(artifact);
3134
+ console.log(
971
3135
  row({
972
- glyph: "error",
973
- label: "publish .NET",
3136
+ glyph: assessment.ok ? "done" : "manual",
3137
+ label: "gatekeeper",
974
3138
  labelWidth: STEP_LABEL_WIDTH,
975
- detail: dim("dotnet publish failed")
3139
+ detail: dim(
3140
+ assessment.ok ? "spctl accepted \u2014 this opens on other Macs" : "spctl rejected \u2014 needs Developer ID + notarization (expected otherwise)"
3141
+ )
976
3142
  })
977
3143
  );
978
- console.error(dim(output));
979
- if (looksLikeMissingWorkload(output)) printWorkloadHint();
980
- else if (looksLikeMissingXcode(output)) printXcodeHint();
981
- if (!verbose) {
982
- console.error(footer(dim("re-run with --verbose for the full build log.")));
983
- }
984
- process.exit(1);
3144
+ } finally {
3145
+ mounted?.release();
985
3146
  }
986
- const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
987
- console.log(
988
- row({
989
- glyph: "done",
990
- label: "publish .NET",
991
- labelWidth: STEP_LABEL_WIDTH,
992
- detail: `${dim("Release \xB7")} ${value(target.framework)} ${dim(`(${elapsed}s)`)}`
993
- })
994
- );
995
- return path5.join(project.hostDir, "bin", "Release", target.framework);
3147
+ return failures;
996
3148
  };
997
- var stepPackage = async (project, target, bundlePath) => {
998
- const outputDir = path5.join(project.root, "dist");
999
- fs4.ensureDirSync(outputDir);
1000
- const start = Date.now();
1001
- let outputPath;
3149
+ var verifyWindowsArtifact = (artifact) => {
3150
+ const failures = [];
3151
+ let extracted = null;
3152
+ let searchRoot = artifact;
3153
+ if (artifact.endsWith(".zip")) {
3154
+ try {
3155
+ const archive = extractArchive(artifact);
3156
+ extracted = archive;
3157
+ searchRoot = archive.dir;
3158
+ } catch (error) {
3159
+ report(false, "archive", `could not open ${path18.basename(artifact)}`, String(error));
3160
+ return ["archive"];
3161
+ }
3162
+ }
1002
3163
  try {
1003
- outputPath = await target.package(bundlePath, outputDir, {
1004
- projectName: project.projectName,
1005
- displayVersion: project.displayVersion
1006
- });
1007
- } catch (e) {
1008
- console.error(
1009
- row({
1010
- glyph: "error",
1011
- label: packageLabel(target),
1012
- labelWidth: STEP_LABEL_WIDTH,
1013
- detail: dim("packaging failed")
1014
- })
3164
+ const exe = artifact.endsWith(".exe") ? artifact : findWindowsExecutableRecursive(searchRoot);
3165
+ if (!exe) {
3166
+ report(false, "executable", `no .exe found under ${artifact}`);
3167
+ return ["executable"];
3168
+ }
3169
+ console.log(kv("executable", path18.basename(exe)));
3170
+ const sig = verifyWindowsSignature(exe);
3171
+ report(
3172
+ sig.ok,
3173
+ "authenticode",
3174
+ sig.untrustedRoot ? "signed and intact; chain not trusted (expected for a self-signed certificate)" : sig.ok ? "signature verified and trusted" : "not signed or not verifiable",
3175
+ sig.output
1015
3176
  );
1016
- console.error(dim(formatProcessError(e)));
1017
- process.exit(1);
3177
+ if (!sig.ok) failures.push("authenticode");
3178
+ } finally {
3179
+ extracted?.release();
1018
3180
  }
1019
- const pkgTime = ((Date.now() - start) / 1e3).toFixed(1);
1020
- const sizeMB = (fs4.statSync(outputPath).size / (1024 * 1024)).toFixed(1);
3181
+ return failures;
3182
+ };
3183
+ var report = (ok, label, detail, output) => {
1021
3184
  console.log(
1022
3185
  row({
1023
- glyph: "done",
1024
- label: packageLabel(target),
3186
+ glyph: ok ? "done" : "error",
3187
+ label,
1025
3188
  labelWidth: STEP_LABEL_WIDTH,
1026
- detail: `${value(path5.basename(outputPath))} ${dim(`(${sizeMB} MB, ${pkgTime}s)`)}`
3189
+ detail: dim(detail)
1027
3190
  })
1028
3191
  );
1029
- return outputPath;
3192
+ if (!ok && output) console.error(dim(output.trim()));
3193
+ };
3194
+ var discoverArtifact = () => {
3195
+ const project = detectProject(process.cwd());
3196
+ const dist = path18.join(project.root, "dist");
3197
+ if (!fs17.existsSync(dist)) return null;
3198
+ const wanted = detectPlatform() === "windows" ? ".zip" : ".dmg";
3199
+ const candidates = fs17.readdirSync(dist).filter((f) => f.endsWith(wanted)).map((f) => path18.join(dist, f)).sort((a, b) => fs17.statSync(b).mtimeMs - fs17.statSync(a).mtimeMs);
3200
+ return candidates[0] ?? null;
1030
3201
  };
1031
3202
 
1032
3203
  // src/commands/dev.ts
1033
- import path6 from "path";
1034
- import fs5 from "fs-extra";
1035
- import { execFileSync as execFileSync4, spawn as spawn2 } from "child_process";
3204
+ import path19 from "path";
3205
+ import fs18 from "fs-extra";
3206
+ import { execFileSync as execFileSync9, spawn as spawn2 } from "child_process";
1036
3207
  import { request } from "http";
1037
3208
 
1038
3209
  // src/dev-port.ts
@@ -1092,11 +3263,11 @@ var selectDevServerUrl = async (configuredUrl, options = {}) => {
1092
3263
  name: "port",
1093
3264
  message: `Port ${port} is already in use. Choose another port:`,
1094
3265
  initial: suggestedPort,
1095
- validate: async (value2) => {
1096
- if (!isValidPort(value2)) {
3266
+ validate: async (value3) => {
3267
+ if (!isValidPort(value3)) {
1097
3268
  return `Enter a port between ${MIN_PORT} and ${MAX_PORT}`;
1098
3269
  }
1099
- return await probe(value2, url.hostname) || `Port ${value2} is already in use`;
3270
+ return await probe(value3, url.hostname) || `Port ${value3} is already in use`;
1100
3271
  }
1101
3272
  });
1102
3273
  const selectedPort = result.port;
@@ -1116,6 +3287,7 @@ var POLL_TIMEOUT_MS = 3e4;
1116
3287
  var NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm";
1117
3288
  var DOTNET_COMMAND = process.platform === "win32" ? "dotnet.exe" : "dotnet";
1118
3289
  var WATCH_OUTPUT_TAIL_CHARS = 8192;
3290
+ var HOST_TERMINATION_TIMEOUT_MS = 5e3;
1119
3291
  var TARGETS2 = {
1120
3292
  macos: {
1121
3293
  name: "macos",
@@ -1132,7 +3304,7 @@ var startSession = async (argv, opts) => {
1132
3304
  const args = parseArgs(["_", "_", ...argv]);
1133
3305
  const targetName = args["target"] || detectPlatform();
1134
3306
  const verbose = !!args["verbose"];
1135
- let hotReload = opts.hotReloadDefault && !args["no-hot-reload"];
3307
+ const hotReload = opts.hotReloadDefault && !args["no-hot-reload"];
1136
3308
  let viteUrl = process.env.VIDRA_DEV_URL || "http://localhost:5173";
1137
3309
  const target = TARGETS2[targetName];
1138
3310
  if (!target) {
@@ -1150,20 +3322,6 @@ var startSession = async (argv, opts) => {
1150
3322
  if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
1151
3323
  process.exit(1);
1152
3324
  }
1153
- if (hotReload && target.name === "macos") {
1154
- const blocker = macCatalystHotReloadBlocker();
1155
- if (blocker) {
1156
- hotReload = false;
1157
- console.log();
1158
- console.log(
1159
- row({
1160
- glyph: "manual",
1161
- detail: `${dim(blocker.reason + "; using a classic launch")}`
1162
- })
1163
- );
1164
- console.log(fixLine(blocker.fix));
1165
- }
1166
- }
1167
3325
  if (opts.vite) {
1168
3326
  viteUrl = await selectDevServerUrl(viteUrl);
1169
3327
  }
@@ -1173,6 +3331,8 @@ var startSession = async (argv, opts) => {
1173
3331
  });
1174
3332
  await session.run();
1175
3333
  };
3334
+ var watchStrategyFor = (_targetName) => "delta";
3335
+ var DELTA_CHANNEL_DEAD_WARNING = "further changes won't be applied to this process";
1176
3336
  var buildDotnetWatchArgs = (opts) => [
1177
3337
  "watch",
1178
3338
  "--project",
@@ -1181,21 +3341,23 @@ var buildDotnetWatchArgs = (opts) => [
1181
3341
  // paired with DOTNET_WATCH_RESTART_ON_RUDE_EDIT for older SDKs.
1182
3342
  "--non-interactive",
1183
3343
  ...opts.verbose ? ["--verbose"] : [],
1184
- "run",
3344
+ opts.strategy === "rebuild" ? "build" : "run",
1185
3345
  "-f",
1186
3346
  opts.framework,
1187
3347
  "-c",
1188
3348
  opts.buildConfig,
1189
- // Launch the Mac Catalyst app by exec'ing the binary rather than `open -a`,
1190
- // so the environment (VIDRA_DEV_URL, DOTNET_STARTUP_HOOKS) propagates and
1191
- // stdout streams back. Ignored by targets that don't define the property.
1192
- ...opts.targetName === "macos" ? ["--property:RunWithOpen=false"] : []
3349
+ ...opts.strategy === "delta" && opts.framework.includes("maccatalyst") ? ["--property:RunWithOpen=false"] : []
1193
3350
  ];
1194
3351
  var dotnetWatchEnv = (devUrl) => ({
1195
3352
  VIDRA_DEV_URL: devUrl,
1196
3353
  DOTNET_WATCH_RESTART_ON_RUDE_EDIT: "1",
1197
3354
  DOTNET_WATCH_SUPPRESS_EMOJIS: "1",
1198
- DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER: "1"
3355
+ DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER: "1",
3356
+ // The session reads the loop's state out of watch and MSBuild output
3357
+ // ("Build succeeded.", "Waiting for a file to change"), so the child must
3358
+ // speak the language those patterns are written in. Without this a localized
3359
+ // SDK silently breaks the dev loop for everyone whose machine isn't English.
3360
+ DOTNET_CLI_UI_LANGUAGE: "en"
1199
3361
  });
1200
3362
  var buildViteArgs = (devUrl) => [
1201
3363
  "run",
@@ -1207,14 +3369,40 @@ var buildViteArgs = (devUrl) => [
1207
3369
  ];
1208
3370
  var HOST_READY_SENTINEL = "[vidra] host ready";
1209
3371
  var classifyWatchLine = (line) => {
3372
+ if (line.toLowerCase().includes(DELTA_CHANNEL_DEAD_WARNING)) {
3373
+ return "deltaChannelDead";
3374
+ }
1210
3375
  if (/waiting for a file to change/i.test(line)) return "appWaiting";
1211
3376
  if (/fix the error to continue/i.test(line)) return "buildBlocked";
1212
3377
  if (line.includes(HOST_READY_SENTINEL)) return "appStarted";
3378
+ if (/^\s*build succeeded\b/i.test(line)) return "buildSucceeded";
3379
+ if (/^\s*build failed\b/i.test(line)) return "buildFailed";
1213
3380
  if (/\bwatch\b/i.test(line) && /\b(?:started|launched)\b/i.test(line)) {
1214
3381
  return "appStarted";
1215
3382
  }
1216
3383
  return null;
1217
3384
  };
3385
+ var watchReaction = (event, ctx) => {
3386
+ if (event === "appStarted") {
3387
+ return ctx.hostLaunched ? "none" : "markHostReady";
3388
+ }
3389
+ if (event === "deltaChannelDead") {
3390
+ return ctx.strategy === "delta" ? "switchToRebuild" : "none";
3391
+ }
3392
+ if (ctx.strategy === "rebuild") {
3393
+ if (event === "buildSucceeded") return "launchHost";
3394
+ if (event === "buildFailed") return "reportBuildFailed";
3395
+ if (event === "appWaiting" || event === "buildBlocked") {
3396
+ return ctx.everBuilt || ctx.buildOutcome !== null ? "none" : "reportEarlyExit";
3397
+ }
3398
+ return "none";
3399
+ }
3400
+ if (event === "appWaiting" || event === "buildBlocked") {
3401
+ if (ctx.hostLaunched) return "reportAppIdle";
3402
+ return ctx.buildOutcome === "failed" ? "reportBuildFailed" : "reportEarlyExit";
3403
+ }
3404
+ return "none";
3405
+ };
1218
3406
  var DevSession = class {
1219
3407
  constructor(project, target, viteUrl, verbose, options) {
1220
3408
  this.project = project;
@@ -1223,6 +3411,7 @@ var DevSession = class {
1223
3411
  this.verbose = verbose;
1224
3412
  this.vite = options.vite;
1225
3413
  this.hotReload = options.hotReload;
3414
+ this.strategy = watchStrategyFor(target.name);
1226
3415
  }
1227
3416
  project;
1228
3417
  target;
@@ -1232,6 +3421,9 @@ var DevSession = class {
1232
3421
  buildConfig = process.env.VIDRA_BUILD_CONFIG || "Debug";
1233
3422
  vite;
1234
3423
  hotReload;
3424
+ // Not readonly: a session that watches its delta channel die moves itself to
3425
+ // the rebuild loop (see WatchStrategy).
3426
+ strategy;
1235
3427
  shuttingDown = false;
1236
3428
  // Watch-mode state: `watchChild` is the `dotnet watch` process (a process
1237
3429
  // group leader on unix, so shutdown can signal it and the app it launched
@@ -1239,11 +3431,34 @@ var DevSession = class {
1239
3431
  watchChild;
1240
3432
  watchReady = false;
1241
3433
  watchOutputTail = "";
3434
+ buildOutcome = null;
3435
+ everBuilt = false;
3436
+ // `"rebuild"` state: the app process we own, and a guard so that overlapping
3437
+ // build cycles (save twice in a row) can't spawn two hosts at once.
3438
+ hostChild;
3439
+ relaunching = false;
3440
+ relaunchPending = false;
3441
+ fellBackToClassic = false;
1242
3442
  endSession = () => {
1243
3443
  };
1244
3444
  sessionDone = new Promise((resolve) => {
1245
3445
  this.endSession = resolve;
1246
3446
  });
3447
+ /** How C# edits reach the app, in words the user can trust. */
3448
+ get csharpLoopLabel() {
3449
+ return this.strategy === "rebuild" ? "C# rebuild + relaunch" : "C# hot reload active";
3450
+ }
3451
+ /**
3452
+ * The banner word for the loop as a whole. UI edits always hot reload; the
3453
+ * banner only stops saying so when the C# half of the loop cannot.
3454
+ */
3455
+ get loopHeadline() {
3456
+ return this.hotReload && this.strategy === "rebuild" ? "reload on save active" : "hot reload active";
3457
+ }
3458
+ /** The same, phrased for the footer's "watching …" line. */
3459
+ get csharpLoopSuffix() {
3460
+ return this.strategy === "rebuild" ? "C# rebuild + relaunch on save" : "hot reload on save";
3461
+ }
1247
3462
  async run() {
1248
3463
  this.installSignalHandlers();
1249
3464
  console.log();
@@ -1272,20 +3487,20 @@ var DevSession = class {
1272
3487
  );
1273
3488
  }
1274
3489
  this.launchHost();
1275
- const hostDirLabel = `src/${path6.basename(this.project.hostDir)}`;
3490
+ const hostDirLabel = `src/${path19.basename(this.project.hostDir)}`;
1276
3491
  if (this.vite) {
1277
3492
  console.log(
1278
3493
  taggedRow(
1279
3494
  "active",
1280
3495
  null,
1281
- this.hotReload ? `${lime("hot reload active")} ${dim(`\u2014 edit ui/src or ${hostDirLabel} and save`)}` : `${lime("hot reload active")} ${dim("\u2014 edit ui/src and save")}`
3496
+ this.hotReload ? `${lime(this.loopHeadline)} ${dim(`\u2014 edit ui/src or ${hostDirLabel} and save`)}` : `${lime(this.loopHeadline)} ${dim("\u2014 edit ui/src and save")}`
1282
3497
  )
1283
3498
  );
1284
3499
  console.log();
1285
3500
  console.log(
1286
3501
  footer(
1287
3502
  this.hotReload ? `${dim("watching")} ${value("ui/")} ${dim("\xB7")} ${value(`${hostDirLabel}/`)} ${dim(
1288
- "\xB7 hot reload on save \xB7 ctrl-c to stop"
3503
+ `\xB7 ${this.csharpLoopSuffix} \xB7 ctrl-c to stop`
1289
3504
  )}` : `${dim("watching")} ${value("ui/")} ${dim(
1290
3505
  "\xB7 hot reload on save \xB7 ctrl-c to stop"
1291
3506
  )}`
@@ -1340,7 +3555,7 @@ var DevSession = class {
1340
3555
  framework: this.target.framework,
1341
3556
  buildConfig: this.buildConfig,
1342
3557
  verbose: this.verbose,
1343
- targetName: this.target.name
3558
+ strategy: this.strategy
1344
3559
  }),
1345
3560
  {
1346
3561
  cwd: this.project.root,
@@ -1361,6 +3576,7 @@ var DevSession = class {
1361
3576
  scanStream(watch.stderr, (line) => this.onWatchLine(line));
1362
3577
  watch.on("exit", (code, signal) => {
1363
3578
  if (this.shuttingDown) return;
3579
+ if (this.watchChild !== watch) return;
1364
3580
  if (this.watchReady) {
1365
3581
  const failed = code !== null && code !== 0 || signal !== null;
1366
3582
  if (failed) {
@@ -1395,58 +3611,191 @@ var DevSession = class {
1395
3611
  -WATCH_OUTPUT_TAIL_CHARS
1396
3612
  );
1397
3613
  const event = classifyWatchLine(line);
1398
- if (event === "appStarted" && !this.watchReady) {
1399
- this.watchReady = true;
1400
- console.log(
1401
- taggedRow(
1402
- "done",
1403
- "host",
1404
- `${dim("launched")} ${value(this.project.projectName)} ${dim("\u2014 C# hot reload active")}`
1405
- )
1406
- );
1407
- return;
3614
+ if (event === "buildSucceeded" || event === "buildFailed") {
3615
+ this.buildOutcome = event === "buildSucceeded" ? "succeeded" : "failed";
3616
+ this.everBuilt = true;
1408
3617
  }
1409
- if (event !== "appWaiting" && event !== "buildBlocked" || this.shuttingDown) {
1410
- return;
1411
- }
1412
- if (this.watchReady) {
1413
- console.log(
1414
- taggedRow(
1415
- "manual",
1416
- "host",
1417
- dim("app not running \u2014 save a C# file to relaunch, or ctrl-c to stop")
1418
- )
1419
- );
1420
- return;
3618
+ if (this.shuttingDown) return;
3619
+ const reaction = watchReaction(event, {
3620
+ strategy: this.strategy,
3621
+ hostLaunched: this.watchReady,
3622
+ buildOutcome: this.buildOutcome,
3623
+ everBuilt: this.everBuilt
3624
+ });
3625
+ if (event === "appWaiting" || event === "buildBlocked") {
3626
+ this.buildOutcome = null;
1421
3627
  }
1422
- if (!/Build FAILED/i.test(this.watchOutputTail)) {
1423
- console.log(
1424
- taggedRow(
1425
- "manual",
1426
- "host",
1427
- dim(
1428
- "app exited before it was ready \u2014 save a C# file to relaunch, or ctrl-c to stop"
3628
+ switch (reaction) {
3629
+ case "markHostReady":
3630
+ this.announceHostLaunched(line.includes(HOST_READY_SENTINEL));
3631
+ return;
3632
+ case "launchHost":
3633
+ void this.launchOrRelaunchHost();
3634
+ return;
3635
+ case "switchToRebuild":
3636
+ void this.switchToRebuildLoop();
3637
+ return;
3638
+ case "reportAppIdle":
3639
+ console.log(
3640
+ taggedRow(
3641
+ "manual",
3642
+ "host",
3643
+ dim("app not running \u2014 save a C# file to relaunch, or ctrl-c to stop")
1429
3644
  )
1430
- )
1431
- );
3645
+ );
3646
+ return;
3647
+ case "reportEarlyExit":
3648
+ console.log(
3649
+ taggedRow(
3650
+ "manual",
3651
+ "host",
3652
+ dim(
3653
+ "app exited before it was ready \u2014 save a C# file to relaunch, or ctrl-c to stop"
3654
+ )
3655
+ )
3656
+ );
3657
+ return;
3658
+ case "reportBuildFailed":
3659
+ if (!this.watchReady) {
3660
+ if (looksLikeMissingWorkload(this.watchOutputTail)) printWorkloadHint();
3661
+ else if (looksLikeXcodeTooOld(this.watchOutputTail)) printXcodeTooOldHint();
3662
+ else if (looksLikeMissingXcode(this.watchOutputTail)) printXcodeHint();
3663
+ }
3664
+ console.log(
3665
+ taggedRow(
3666
+ "manual",
3667
+ "host",
3668
+ dim("build failed \u2014 fix the error and save to retry, or ctrl-c to stop")
3669
+ )
3670
+ );
3671
+ return;
3672
+ case "none":
3673
+ return;
3674
+ }
3675
+ }
3676
+ /**
3677
+ * A host we spawned ourselves reached VidraPage. Reported per launch, not
3678
+ * per session: under the "rebuild" loop each save produces a new one, and
3679
+ * "the app is back up" is the thing the developer is waiting to see.
3680
+ */
3681
+ onHostReady() {
3682
+ if (this.shuttingDown) return;
3683
+ this.watchReady = true;
3684
+ const loop = this.hotReload ? ` ${dim(`\xB7 ${this.csharpLoopSuffix}`)}` : "";
3685
+ console.log(
3686
+ taggedRow(
3687
+ "done",
3688
+ "host",
3689
+ `${dim("host ready \u2014")} ${value(this.project.projectName)}${loop}`
3690
+ )
3691
+ );
3692
+ }
3693
+ /**
3694
+ * The app the watcher launched is up. Says "host ready" when the app told us
3695
+ * so itself (the VidraPage sentinel) and "launched" when all we have is the
3696
+ * watcher's own start message, which only means a process was spawned. Both
3697
+ * paths through the session — this one and {@link onHostReady} — report
3698
+ * readiness in the same words, so "the app is up" is one thing to look for
3699
+ * rather than two.
3700
+ */
3701
+ announceHostLaunched(sawSentinel) {
3702
+ if (this.watchReady) return;
3703
+ this.watchReady = true;
3704
+ const loop = dim(`\xB7 ${this.csharpLoopSuffix}`);
3705
+ console.log(
3706
+ taggedRow(
3707
+ "done",
3708
+ "host",
3709
+ sawSentinel ? `${dim("host ready \u2014")} ${value(this.project.projectName)} ${loop}` : `${dim("launched")} ${value(this.project.projectName)} ${dim(`\u2014 ${this.csharpLoopLabel}`)}`
3710
+ )
3711
+ );
3712
+ }
3713
+ /**
3714
+ * "rebuild" strategy: a build just succeeded, so replace the running app with
3715
+ * the one that was built. Serialized, because a burst of saves produces a
3716
+ * burst of successful builds and two hosts must never run at once — the
3717
+ * last request wins.
3718
+ */
3719
+ async launchOrRelaunchHost() {
3720
+ if (this.shuttingDown) return;
3721
+ if (this.relaunching) {
3722
+ this.relaunchPending = true;
1432
3723
  return;
1433
3724
  }
1434
- if (looksLikeMissingWorkload(this.watchOutputTail)) printWorkloadHint();
1435
- else if (looksLikeXcodeTooOld(this.watchOutputTail)) printXcodeTooOldHint();
1436
- else if (looksLikeMissingXcode(this.watchOutputTail)) printXcodeHint();
3725
+ this.relaunching = true;
3726
+ try {
3727
+ do {
3728
+ this.relaunchPending = false;
3729
+ const previous = this.hostChild;
3730
+ if (previous) {
3731
+ this.hostChild = void 0;
3732
+ console.log(taggedRow("active", "host", dim("relaunching\u2026")));
3733
+ killChild(previous);
3734
+ await waitForExit(previous, HOST_TERMINATION_TIMEOUT_MS);
3735
+ }
3736
+ if (this.shuttingDown) return;
3737
+ this.hostChild = this.spawnMacosHost({ fatal: false }) ?? void 0;
3738
+ if (this.hostChild) this.watchReady = true;
3739
+ } while (this.relaunchPending && !this.shuttingDown);
3740
+ } finally {
3741
+ this.relaunching = false;
3742
+ }
3743
+ }
3744
+ /**
3745
+ * The hot reload agent is gone and `dotnet watch` has stopped trying to reach
3746
+ * it. Every later edit would be applied to nothing while the watcher keeps
3747
+ * reporting success, so the session stops asking for deltas and drives the
3748
+ * loop itself: the watcher is restarted as `dotnet watch build`, and each
3749
+ * successful build relaunches the app.
3750
+ *
3751
+ * Restarting the watcher also takes the app down with it (it leads the
3752
+ * process group), which is what makes the first rebuild land on a clean
3753
+ * process instead of one whose agent has already given up.
3754
+ */
3755
+ async switchToRebuildLoop() {
3756
+ if (this.shuttingDown || this.strategy !== "delta") return;
3757
+ this.strategy = "rebuild";
3758
+ console.log();
1437
3759
  console.log(
1438
3760
  taggedRow(
1439
3761
  "manual",
1440
3762
  "host",
1441
- dim("build failed \u2014 fix the error and save to retry, or ctrl-c to stop")
3763
+ dim(
3764
+ "the hot reload agent dropped out \u2014 dotnet watch would keep reporting edits as applied"
3765
+ )
3766
+ )
3767
+ );
3768
+ console.log(
3769
+ taggedRow(
3770
+ "active",
3771
+ "host",
3772
+ dim("switching this session to rebuild + relaunch on save\u2026")
1442
3773
  )
1443
3774
  );
3775
+ console.log(
3776
+ footer(
3777
+ dim(
3778
+ "a known Mac Catalyst flake (dotnet/sdk#55488) \u2014 restarting vidra dev often gets deltas back"
3779
+ )
3780
+ )
3781
+ );
3782
+ console.log();
3783
+ const previous = this.watchChild;
3784
+ this.watchChild = void 0;
3785
+ if (previous) {
3786
+ killChild(previous, { processGroup: true });
3787
+ await waitForExit(previous, HOST_TERMINATION_TIMEOUT_MS);
3788
+ }
3789
+ if (this.shuttingDown) return;
3790
+ this.buildOutcome = null;
3791
+ this.everBuilt = false;
3792
+ this.launchHostWithWatch();
1444
3793
  }
1445
3794
  /**
1446
- * `dotnet watch` exited before the app ever started — typically an SDK or
1447
- * MAUI workload set that predates `dotnet run`/`dotnet watch` support for
1448
- * this target. Explain why, then launch the host the classic way (one
1449
- * build + direct spawn) so the dev session still works, minus C# hot reload.
3795
+ * `dotnet watch` exited before the app ever started — an incomplete SDK or
3796
+ * MAUI workload install, usually. Explain why, then launch the host the
3797
+ * classic way (one build + direct spawn) so the dev session still works,
3798
+ * minus anything that reacts to a C# edit.
1450
3799
  */
1451
3800
  fallBackToClassicLaunch(code, signal) {
1452
3801
  console.log();
@@ -1454,11 +3803,12 @@ var DevSession = class {
1454
3803
  taggedRow(
1455
3804
  "manual",
1456
3805
  "host",
1457
- `${dim("C# hot reload unavailable \u2014 dotnet watch exited with")} ${value(
3806
+ `${dim("the C# watch loop is unavailable \u2014 dotnet watch exited with")} ${value(
1458
3807
  signal ? `signal ${signal}` : `code ${code ?? "unknown"}`
1459
3808
  )}`
1460
3809
  )
1461
3810
  );
3811
+ this.fellBackToClassic = true;
1462
3812
  if (looksLikeMissingWorkload(this.watchOutputTail)) {
1463
3813
  printWorkloadHint();
1464
3814
  } else if (looksLikeXcodeTooOld(this.watchOutputTail)) {
@@ -1468,9 +3818,7 @@ var DevSession = class {
1468
3818
  } else {
1469
3819
  console.log(
1470
3820
  footer(
1471
- dim(
1472
- "C# hot reload needs the .NET 10.0.203+ workload set (Mac Catalyst dotnet-watch support) \u2014 try:"
1473
- )
3821
+ dim("nothing in the output explains it \u2014 an incomplete toolchain is the usual cause:")
1474
3822
  )
1475
3823
  );
1476
3824
  console.log(fixLine("dotnet workload update"));
@@ -1506,7 +3854,7 @@ var DevSession = class {
1506
3854
  )
1507
3855
  );
1508
3856
  try {
1509
- execFileSync4(
3857
+ execFileSync9(
1510
3858
  DOTNET_COMMAND,
1511
3859
  [
1512
3860
  "build",
@@ -1535,22 +3883,37 @@ var DevSession = class {
1535
3883
  }
1536
3884
  launchMacosHost() {
1537
3885
  this.buildHostSync();
3886
+ const host = this.spawnMacosHost({ fatal: true });
3887
+ if (!host) process.exit(1);
3888
+ return host;
3889
+ }
3890
+ /**
3891
+ * Locate the built `.app`, sign it if we can, and spawn its executable.
3892
+ *
3893
+ * Split out from {@link launchMacosHost} because the watch loop reaches this
3894
+ * point with the build already done — and, unlike a one-shot launch, must
3895
+ * survive a bad outcome: under `"rebuild"` the session stays alive so the
3896
+ * next save can put things right, hence `fatal: false` returning null rather
3897
+ * than exiting the process.
3898
+ */
3899
+ spawnMacosHost(opts) {
3900
+ const outputDir = path19.join(
3901
+ this.project.hostDir,
3902
+ "bin",
3903
+ this.buildConfig,
3904
+ this.target.framework
3905
+ );
3906
+ const fail2 = (detail) => {
3907
+ console.error(row({ glyph: "error", detail: dim(detail) }));
3908
+ if (opts.fatal) process.exit(1);
3909
+ return null;
3910
+ };
1538
3911
  const appBundle = findMacAppBundle(
1539
3912
  this.project.hostDir,
1540
3913
  this.target.framework,
1541
3914
  this.buildConfig
1542
3915
  );
1543
- if (!appBundle) {
1544
- console.error(
1545
- row({
1546
- glyph: "error",
1547
- detail: dim(
1548
- `could not find .app bundle in ${path6.join(this.project.hostDir, "bin", this.buildConfig, this.target.framework)}`
1549
- )
1550
- })
1551
- );
1552
- process.exit(1);
1553
- }
3916
+ if (!appBundle) return fail2(`could not find .app bundle in ${outputDir}`);
1554
3917
  signMacAppBundleIfPossible(appBundle, {
1555
3918
  verbose: this.verbose,
1556
3919
  log: console.log,
@@ -1558,23 +3921,17 @@ var DevSession = class {
1558
3921
  });
1559
3922
  const binary = findMacExecutable(appBundle);
1560
3923
  if (!binary) {
1561
- console.error(
1562
- row({
1563
- glyph: "error",
1564
- detail: dim(`could not find the app executable in ${appBundle}`)
1565
- })
1566
- );
1567
- process.exit(1);
3924
+ return fail2(`could not find the app executable in ${appBundle}`);
1568
3925
  }
1569
3926
  console.log(
1570
- taggedRow("done", "host", `${dim("launched")} ${value(path6.basename(appBundle))}`)
3927
+ taggedRow("active", "host", `${dim("launching")} ${value(path19.basename(appBundle))}${dim("\u2026")}`)
1571
3928
  );
1572
3929
  const host = spawn2(binary, [], {
1573
3930
  cwd: this.project.root,
1574
3931
  stdio: ["ignore", "pipe", "pipe"],
1575
3932
  env: { ...process.env, VIDRA_DEV_URL: this.viteUrl }
1576
3933
  });
1577
- return this.registerChild(host, "host", path6.basename(binary));
3934
+ return this.registerChild(host, "host", path19.basename(binary));
1578
3935
  }
1579
3936
  // Build first, then spawn the produced .exe directly. A single
1580
3937
  // `dotnet build -t:Run` on an unpackaged MAUI Windows app
@@ -1586,7 +3943,7 @@ var DevSession = class {
1586
3943
  // See dotnet/maui#13942 and dotnet/maui#5975.
1587
3944
  launchWindowsHost() {
1588
3945
  this.buildHostSync();
1589
- const exe = findWindowsExecutable(
3946
+ const exe = findWindowsExecutable2(
1590
3947
  this.project.hostDir,
1591
3948
  this.project.csprojPath,
1592
3949
  this.target.framework,
@@ -1597,28 +3954,36 @@ var DevSession = class {
1597
3954
  row({
1598
3955
  glyph: "error",
1599
3956
  detail: dim(
1600
- `could not find the host .exe under ${path6.join(this.project.hostDir, "bin", this.buildConfig, this.target.framework)}`
3957
+ `could not find the host .exe under ${path19.join(this.project.hostDir, "bin", this.buildConfig, this.target.framework)}`
1601
3958
  )
1602
3959
  })
1603
3960
  );
1604
3961
  process.exit(1);
1605
3962
  }
1606
3963
  console.log(
1607
- taggedRow("done", "host", `${dim("launched")} ${value(path6.basename(exe))}`)
3964
+ taggedRow("active", "host", `${dim("launching")} ${value(path19.basename(exe))}${dim("\u2026")}`)
1608
3965
  );
1609
3966
  const host = spawn2(exe, [], {
1610
- cwd: path6.dirname(exe),
3967
+ cwd: path19.dirname(exe),
1611
3968
  stdio: ["ignore", "pipe", "pipe"],
1612
3969
  env: { ...process.env, VIDRA_DEV_URL: this.viteUrl }
1613
3970
  });
1614
- return this.registerChild(host, "host", path6.basename(exe));
3971
+ return this.registerChild(host, "host", path19.basename(exe));
1615
3972
  }
1616
3973
  registerChild(child, tag2, label) {
1617
3974
  this.children.push(child);
1618
3975
  const include = tag2 === "host" ? (line) => !line.includes(HOST_READY_SENTINEL) : void 0;
1619
3976
  prefixStream(child.stdout, tag2, include);
1620
3977
  prefixStream(child.stderr, tag2, include);
3978
+ if (tag2 === "host") {
3979
+ const onLine = (line) => {
3980
+ if (line.includes(HOST_READY_SENTINEL)) this.onHostReady();
3981
+ };
3982
+ scanStream(child.stdout, onLine);
3983
+ scanStream(child.stderr, onLine);
3984
+ }
1621
3985
  child.on("exit", (code, signal) => {
3986
+ this.forgetChild(child);
1622
3987
  if (this.shuttingDown) return;
1623
3988
  if (tag2 === "ui") {
1624
3989
  const exitCode = code ?? 1;
@@ -1628,6 +3993,18 @@ var DevSession = class {
1628
3993
  this.shutdown(exitCode);
1629
3994
  return;
1630
3995
  }
3996
+ if (this.ownsHostProcess) {
3997
+ if (child !== this.hostChild) return;
3998
+ this.hostChild = void 0;
3999
+ console.log(
4000
+ taggedRow(
4001
+ "manual",
4002
+ "host",
4003
+ dim("app not running \u2014 save a C# file to relaunch, or ctrl-c to stop")
4004
+ )
4005
+ );
4006
+ return;
4007
+ }
1631
4008
  const failed = code !== null && code !== 0 || signal !== null;
1632
4009
  if (failed) {
1633
4010
  console.error(
@@ -1656,10 +4033,23 @@ var DevSession = class {
1656
4033
  });
1657
4034
  return child;
1658
4035
  }
4036
+ /**
4037
+ * True while the "rebuild" loop is driving the app rather than the watcher.
4038
+ * Goes false once we fall back to a classic launch: there is no watcher left
4039
+ * to relaunch anything, so a host exit ends the session as it always did.
4040
+ */
4041
+ get ownsHostProcess() {
4042
+ return this.hotReload && this.strategy === "rebuild" && !this.fellBackToClassic;
4043
+ }
4044
+ /** Drop an exited child so a long session's relaunches don't accumulate. */
4045
+ forgetChild(child) {
4046
+ const index = this.children.indexOf(child);
4047
+ if (index !== -1) this.children.splice(index, 1);
4048
+ }
1659
4049
  shutdown(exitCode) {
1660
4050
  if (this.shuttingDown) return;
1661
4051
  this.shuttingDown = true;
1662
- for (const child of this.children) {
4052
+ for (const child of [...this.children]) {
1663
4053
  killChild(child, { processGroup: child === this.watchChild });
1664
4054
  }
1665
4055
  this.endSession();
@@ -1738,13 +4128,13 @@ var waitForServer = (url, timeoutMs) => {
1738
4128
  });
1739
4129
  };
1740
4130
  var findMacAppBundle = (hostDir, framework, buildConfig) => {
1741
- const outputDir = path6.join(hostDir, "bin", buildConfig, framework);
4131
+ const outputDir = path19.join(hostDir, "bin", buildConfig, framework);
1742
4132
  return findAppBundleRecursive(outputDir);
1743
4133
  };
1744
4134
  var findAppBundleRecursive = (dir) => {
1745
- if (!fs5.existsSync(dir)) return null;
1746
- for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
1747
- const fullPath = path6.join(dir, entry.name);
4135
+ if (!fs18.existsSync(dir)) return null;
4136
+ for (const entry of fs18.readdirSync(dir, { withFileTypes: true })) {
4137
+ const fullPath = path19.join(dir, entry.name);
1748
4138
  if (entry.isDirectory() && entry.name.endsWith(".app")) {
1749
4139
  return fullPath;
1750
4140
  }
@@ -1756,45 +4146,59 @@ var findAppBundleRecursive = (dir) => {
1756
4146
  return null;
1757
4147
  };
1758
4148
  var findMacExecutable = (appBundle) => {
1759
- const macOsDir = path6.join(appBundle, "Contents", "MacOS");
1760
- if (!fs5.existsSync(macOsDir)) return null;
1761
- for (const entry of fs5.readdirSync(macOsDir, { withFileTypes: true })) {
4149
+ const macOsDir = path19.join(appBundle, "Contents", "MacOS");
4150
+ if (!fs18.existsSync(macOsDir)) return null;
4151
+ for (const entry of fs18.readdirSync(macOsDir, { withFileTypes: true })) {
1762
4152
  if (entry.isFile()) {
1763
- return path6.join(macOsDir, entry.name);
4153
+ return path19.join(macOsDir, entry.name);
1764
4154
  }
1765
4155
  }
1766
4156
  return null;
1767
4157
  };
1768
- var findWindowsExecutable = (hostDir, csprojPath, framework, buildConfig) => {
1769
- const outputDir = path6.join(hostDir, "bin", buildConfig, framework);
1770
- if (!fs5.existsSync(outputDir)) return null;
1771
- const exeName = `${path6.basename(csprojPath, ".csproj")}.exe`.toLowerCase();
4158
+ var findWindowsExecutable2 = (hostDir, csprojPath, framework, buildConfig) => {
4159
+ const outputDir = path19.join(hostDir, "bin", buildConfig, framework);
4160
+ if (!fs18.existsSync(outputDir)) return null;
4161
+ const exeName = `${path19.basename(csprojPath, ".csproj")}.exe`.toLowerCase();
1772
4162
  return findFileRecursive(outputDir, (name) => name.toLowerCase() === exeName) ?? findFileRecursive(
1773
4163
  outputDir,
1774
4164
  (name) => name.toLowerCase().endsWith(".host.exe")
1775
4165
  ) ?? findFileRecursive(outputDir, (name) => name.toLowerCase().endsWith(".exe"));
1776
4166
  };
1777
4167
  var findFileRecursive = (dir, match) => {
1778
- if (!fs5.existsSync(dir)) return null;
1779
- const entries = fs5.readdirSync(dir, { withFileTypes: true });
4168
+ if (!fs18.existsSync(dir)) return null;
4169
+ const entries = fs18.readdirSync(dir, { withFileTypes: true });
1780
4170
  for (const entry of entries) {
1781
4171
  if (entry.isFile() && match(entry.name)) {
1782
- return path6.join(dir, entry.name);
4172
+ return path19.join(dir, entry.name);
1783
4173
  }
1784
4174
  }
1785
4175
  for (const entry of entries) {
1786
4176
  if (entry.isDirectory()) {
1787
- const found = findFileRecursive(path6.join(dir, entry.name), match);
4177
+ const found = findFileRecursive(path19.join(dir, entry.name), match);
1788
4178
  if (found) return found;
1789
4179
  }
1790
4180
  }
1791
4181
  return null;
1792
4182
  };
4183
+ var waitForExit = (child, timeoutMs) => new Promise((resolve) => {
4184
+ if (child.exitCode !== null || child.signalCode !== null) {
4185
+ resolve();
4186
+ return;
4187
+ }
4188
+ const timer = setTimeout(() => {
4189
+ child.kill("SIGKILL");
4190
+ resolve();
4191
+ }, timeoutMs);
4192
+ child.once("exit", () => {
4193
+ clearTimeout(timer);
4194
+ resolve();
4195
+ });
4196
+ });
1793
4197
  var killChild = (child, opts = {}) => {
1794
4198
  if (!child.pid || child.exitCode !== null) return;
1795
4199
  if (process.platform === "win32") {
1796
4200
  try {
1797
- execFileSync4("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
4201
+ execFileSync9("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
1798
4202
  stdio: "ignore"
1799
4203
  });
1800
4204
  } catch {
@@ -1846,9 +4250,12 @@ var printHelp = () => {
1846
4250
  ${lime("vidra")} ${dim("<command> [options]")}
1847
4251
 
1848
4252
  ${dim("commands")}
1849
- ${cmd("dev", "start vite + the native host (UI + C# hot reload)")}
4253
+ ${cmd("dev", "start vite + the native host (UI + C# reload on save)")}
1850
4254
  ${cmd("run", "launch the native host only")}
1851
4255
  ${cmd("build", "build & package for distribution")}
4256
+ ${cmd("bundle", "pack ui/dist as an OTA bundle + update its feed")}
4257
+ ${cmd("keygen", "create the key that signs your update feed")}
4258
+ ${cmd("verify", "check a built artifact is actually shippable")}
1852
4259
  ${cmd("doctor", "check your environment")}
1853
4260
  ${cmd("help", "show this message")}
1854
4261
 
@@ -1857,6 +4264,12 @@ ${ex("dev --target windows", "run the windows host")}
1857
4264
  ${ex("dev --no-hot-reload", "skip dotnet watch, classic launch")}
1858
4265
  ${ex("build --plan", "preview the build, run nothing")}
1859
4266
  ${ex("build --target macos", "build & package a macOS DMG")}
4267
+ ${ex("bundle", "pack an OTA bundle into dist/")}
4268
+ ${ex("bundle --channel beta", "publish it on the beta channel")}
4269
+ ${ex("bundle --sign key.pem", "publish a signed feed")}
4270
+ ${ex("bundle --merge-from URL", "add to the feed you already publish")}
4271
+ ${ex("keygen", "create an update signing key")}
4272
+ ${ex("verify", "check the newest artifact in dist/")}
1860
4273
  ${ex("doctor", "verify .NET SDK + MAUI workload")}
1861
4274
  `);
1862
4275
  };
@@ -1873,6 +4286,15 @@ var main = async () => {
1873
4286
  case "build":
1874
4287
  await buildCommand(args.slice(1));
1875
4288
  break;
4289
+ case "bundle":
4290
+ await bundleCommand(args.slice(1));
4291
+ break;
4292
+ case "keygen":
4293
+ await keygenCommand(args.slice(1));
4294
+ break;
4295
+ case "verify":
4296
+ await verifyCommand(args.slice(1));
4297
+ break;
1876
4298
  case "doctor":
1877
4299
  process.exit(await runDoctor());
1878
4300
  break;