create-vidra-app 0.4.0 → 0.5.1
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/README.md +24 -31
- package/dist/index.js +46 -85
- package/package.json +13 -10
- package/templates/react-vite/README.md +22 -74
- package/templates/react-vite/_gitignore +2 -0
- package/templates/react-vite/package.json +2 -1
- package/templates/react-vite/src/{{projectName}}.Host/MainPage.cs +13 -2
- package/templates/react-vite/src/{{projectName}}.Host/MauiProgram.cs +17 -13
- package/templates/react-vite/src/{{projectName}}.Host/Platforms/MacCatalyst/Program.cs +8 -11
- package/templates/react-vite/src/{{projectName}}.Host/Platforms/Windows/Program.cs +8 -12
- package/templates/react-vite/src/{{projectName}}.Host/{{projectName}}.Host.csproj +19 -4
- package/templates/react-vite/ui/package.json +3 -3
- package/templates/react-vite/ui/src/generated/vidra-access-policy.ts +6 -0
- package/templates/react-vite/ui/src/main.tsx +1 -0
- package/templates/react-vite/ui/vite.config.ts +4 -1
- package/templates/react-vite/vidra.config.ts +33 -0
- package/bin/vidra.mjs +0 -2
- package/dist/cli.js +0 -4320
package/dist/cli.js
DELETED
|
@@ -1,4320 +0,0 @@
|
|
|
1
|
-
// src/commands/build.ts
|
|
2
|
-
import path12 from "path";
|
|
3
|
-
import fs11 from "fs-extra";
|
|
4
|
-
import { execSync as execSync3 } from "child_process";
|
|
5
|
-
|
|
6
|
-
// src/utils.ts
|
|
7
|
-
var parseArgs = (argv) => {
|
|
8
|
-
const args = { _: [] };
|
|
9
|
-
for (let i = 2; i < argv.length; i++) {
|
|
10
|
-
const arg = argv[i];
|
|
11
|
-
if (arg.startsWith("--")) {
|
|
12
|
-
const [key, val] = arg.slice(2).split("=");
|
|
13
|
-
if (val !== void 0) {
|
|
14
|
-
args[key] = val;
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
const next = argv[i + 1];
|
|
18
|
-
if (next !== void 0 && !next.startsWith("--")) {
|
|
19
|
-
args[key] = next;
|
|
20
|
-
i++;
|
|
21
|
-
} else {
|
|
22
|
-
args[key] = true;
|
|
23
|
-
}
|
|
24
|
-
} else {
|
|
25
|
-
args._.push(arg);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return args;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// src/exec.ts
|
|
32
|
-
import { execSync, spawn } from "child_process";
|
|
33
|
-
|
|
34
|
-
// src/theme.ts
|
|
35
|
-
import chalk from "chalk";
|
|
36
|
-
var CLI_VERSION = "0.3.1";
|
|
37
|
-
var STEP_LABEL_WIDTH = 16;
|
|
38
|
-
var lime = chalk.hex("#c8f751");
|
|
39
|
-
var value = chalk.hex("#e8e8ec");
|
|
40
|
-
var dim = chalk.hex("#7a7a86");
|
|
41
|
-
var amber = chalk.hex("#ffcf5c");
|
|
42
|
-
var green = chalk.hex("#86d98f");
|
|
43
|
-
var red = chalk.hex("#ff8585");
|
|
44
|
-
var slate = chalk.hex("#a9b0bd");
|
|
45
|
-
var uiColor = chalk.hex("#6fd3e0");
|
|
46
|
-
var hostColor = chalk.hex("#a48ce8");
|
|
47
|
-
var GLYPH_CHAR = {
|
|
48
|
-
done: "\u2713",
|
|
49
|
-
active: "\u25B8",
|
|
50
|
-
error: "\u2717",
|
|
51
|
-
manual: "?",
|
|
52
|
-
plan: "\u25C6",
|
|
53
|
-
skip: "\u2298"
|
|
54
|
-
};
|
|
55
|
-
var GLYPH_COLOR = {
|
|
56
|
-
done: green,
|
|
57
|
-
active: lime,
|
|
58
|
-
error: red,
|
|
59
|
-
manual: amber,
|
|
60
|
-
plan: slate,
|
|
61
|
-
skip: slate
|
|
62
|
-
};
|
|
63
|
-
var glyph = (name) => GLYPH_COLOR[name](GLYPH_CHAR[name]);
|
|
64
|
-
var TAG_COLOR = {
|
|
65
|
-
ui: uiColor,
|
|
66
|
-
host: hostColor
|
|
67
|
-
};
|
|
68
|
-
var TAG_WIDTH = 6;
|
|
69
|
-
var tag = (name) => TAG_COLOR[name](`[${name}]`.padEnd(TAG_WIDTH));
|
|
70
|
-
var blankTag = () => " ".repeat(TAG_WIDTH);
|
|
71
|
-
var wordmark = () => lime("vidra");
|
|
72
|
-
var header = (sub, context) => {
|
|
73
|
-
const base = ` ${wordmark()} ${value(sub)}`;
|
|
74
|
-
return context ? `${base} ${dim(`\u2014 ${context}`)}` : base;
|
|
75
|
-
};
|
|
76
|
-
var kv = (label, val, width = 9) => ` ${dim(label.padEnd(width))} ${value(val)}`;
|
|
77
|
-
var row = (opts) => {
|
|
78
|
-
const parts = [` ${glyph(opts.glyph)}`];
|
|
79
|
-
if (opts.label !== void 0) {
|
|
80
|
-
const padded = opts.labelWidth ? opts.label.padEnd(opts.labelWidth) : opts.label;
|
|
81
|
-
parts.push(value(padded));
|
|
82
|
-
}
|
|
83
|
-
if (opts.detail) parts.push(opts.detail);
|
|
84
|
-
return parts.join(" ");
|
|
85
|
-
};
|
|
86
|
-
var taggedRow = (glyphName, tagName, text) => ` ${glyph(glyphName)} ${tagName ? tag(tagName) : blankTag()} ${text}`;
|
|
87
|
-
var streamPrefix = (tagName) => ` ${dim("\xB7")} ${tag(tagName)}`;
|
|
88
|
-
var footer = (content) => ` ${content}`;
|
|
89
|
-
var fixLine = (cmd, label = "fix:") => ` ${amber(label)} ${lime(cmd)}`;
|
|
90
|
-
var planBadge = (text = "PLAN ONLY \xB7 BEING BUILT") => amber(`[ ${text} ]`);
|
|
91
|
-
|
|
92
|
-
// src/exec.ts
|
|
93
|
-
var toText = (value3) => {
|
|
94
|
-
if (value3 == null) return "";
|
|
95
|
-
return Buffer.isBuffer(value3) ? value3.toString() : value3;
|
|
96
|
-
};
|
|
97
|
-
var formatProcessError = (error) => {
|
|
98
|
-
const err = error;
|
|
99
|
-
const combined = [toText(err.stderr), toText(err.stdout)].map((s) => s.trim()).filter((s) => s.length > 0).join("\n").trim();
|
|
100
|
-
return combined.length > 0 ? combined : err.message ?? String(error);
|
|
101
|
-
};
|
|
102
|
-
var formatBuildError = (error) => {
|
|
103
|
-
const raw = formatProcessError(error);
|
|
104
|
-
const lines = raw.split(/\r?\n/);
|
|
105
|
-
const errorLines = lines.filter(
|
|
106
|
-
(line) => /(:\s*error\b|\berror\s+[A-Z]{2,}\d+|Build FAILED|MSB\d{4}|NETSDK\d{4})/i.test(
|
|
107
|
-
line
|
|
108
|
-
)
|
|
109
|
-
);
|
|
110
|
-
const picked = errorLines.length > 0 ? errorLines : lines.filter((line) => line.trim().length > 0).slice(-30);
|
|
111
|
-
return picked.join("\n").trim();
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// src/version.ts
|
|
115
|
-
import fs from "fs-extra";
|
|
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 {
|
|
119
|
-
};
|
|
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
|
-
);
|
|
128
|
-
}
|
|
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
|
-
};
|
|
147
|
-
};
|
|
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" };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
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" };
|
|
166
|
-
};
|
|
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
|
-
}
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// src/update-config.ts
|
|
180
|
-
import path2 from "path";
|
|
181
|
-
import fs2 from "fs-extra";
|
|
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 {
|
|
190
|
-
return null;
|
|
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
|
-
}
|
|
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;
|
|
239
|
-
}
|
|
240
|
-
fs2.ensureDirSync(path2.dirname(target));
|
|
241
|
-
fs2.writeFileSync(target, `${JSON.stringify(config, null, 2)}
|
|
242
|
-
`);
|
|
243
|
-
return target;
|
|
244
|
-
};
|
|
245
|
-
|
|
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
|
|
253
|
-
import path3 from "path";
|
|
254
|
-
import os from "os";
|
|
255
|
-
import fs3 from "fs-extra";
|
|
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:";
|
|
391
|
-
var signMacAppBundleIfPossible = (appBundle, options) => {
|
|
392
|
-
if (process.platform !== "darwin") return;
|
|
393
|
-
const purpose = options.purpose ?? "development";
|
|
394
|
-
const { identity, expiredSkipped } = selectMacIdentity(purpose);
|
|
395
|
-
warnAboutExpiredIdentities(expiredSkipped, options);
|
|
396
|
-
const signWith = identity ?? "-";
|
|
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
|
-
}
|
|
415
|
-
try {
|
|
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(" ");
|
|
425
|
-
options.log(
|
|
426
|
-
row({
|
|
427
|
-
glyph: "done",
|
|
428
|
-
label: "codesign",
|
|
429
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
430
|
-
detail
|
|
431
|
-
})
|
|
432
|
-
);
|
|
433
|
-
} catch (error) {
|
|
434
|
-
options.warn(
|
|
435
|
-
row({
|
|
436
|
-
glyph: "manual",
|
|
437
|
-
label: "codesign",
|
|
438
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
439
|
-
detail: dim("could not sign the app bundle; it may fail to launch")
|
|
440
|
-
})
|
|
441
|
-
);
|
|
442
|
-
options.warn(
|
|
443
|
-
footer(
|
|
444
|
-
dim(
|
|
445
|
-
"install Xcode or the Command Line Tools (provides `codesign`), or set VIDRA_MACOS_CODESIGN_KEY."
|
|
446
|
-
)
|
|
447
|
-
)
|
|
448
|
-
);
|
|
449
|
-
options.warn(dim(formatExecError(error)));
|
|
450
|
-
}
|
|
451
|
-
};
|
|
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;
|
|
467
|
-
}
|
|
468
|
-
try {
|
|
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
|
-
})
|
|
479
|
-
);
|
|
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)));
|
|
490
|
-
}
|
|
491
|
-
};
|
|
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) };
|
|
502
|
-
}
|
|
503
|
-
};
|
|
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) };
|
|
517
|
-
}
|
|
518
|
-
};
|
|
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}`
|
|
541
|
-
};
|
|
542
|
-
};
|
|
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) => {
|
|
565
|
-
try {
|
|
566
|
-
const output = execFileSync2("security", args, {
|
|
567
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
568
|
-
encoding: "utf8"
|
|
569
|
-
});
|
|
570
|
-
return output.split(/\r?\n/).map((line) => line.match(/"([^"]+)"/)?.[1] ?? null).filter((v) => v !== null);
|
|
571
|
-
} catch {
|
|
572
|
-
return [];
|
|
573
|
-
}
|
|
574
|
-
};
|
|
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
|
-
});
|
|
582
|
-
};
|
|
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
|
-
);
|
|
612
|
-
}
|
|
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;
|
|
627
|
-
}
|
|
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
|
-
);
|
|
639
|
-
}
|
|
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
|
-
}
|
|
671
|
-
};
|
|
672
|
-
walk(appBundle);
|
|
673
|
-
return found.sort((a, b) => b.split(path4.sep).length - a.split(path4.sep).length);
|
|
674
|
-
};
|
|
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 };
|
|
696
|
-
}
|
|
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);
|
|
707
|
-
}
|
|
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;
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
var verifyWindowsSignature = (target) => {
|
|
766
|
-
const signtool = resolveSignTool();
|
|
767
|
-
if (!signtool) {
|
|
768
|
-
return { ok: false, untrustedRoot: false, output: "signtool.exe not found" };
|
|
769
|
-
}
|
|
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 };
|
|
781
|
-
}
|
|
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 {
|
|
790
|
-
}
|
|
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
|
-
}
|
|
805
|
-
}
|
|
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 };
|
|
832
|
-
}
|
|
833
|
-
return {
|
|
834
|
-
packId,
|
|
835
|
-
packTitle,
|
|
836
|
-
packVersion: opts.version,
|
|
837
|
-
feedUrl: config.feedUrl,
|
|
838
|
-
channel: config.channel ?? null
|
|
839
|
-
};
|
|
840
|
-
};
|
|
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);
|
|
857
|
-
}
|
|
858
|
-
return parts.join(" ");
|
|
859
|
-
};
|
|
860
|
-
var NativeUpdateError = class extends Error {
|
|
861
|
-
constructor(message, detail = "") {
|
|
862
|
-
super(message);
|
|
863
|
-
this.detail = detail;
|
|
864
|
-
}
|
|
865
|
-
detail;
|
|
866
|
-
};
|
|
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
|
-
);
|
|
874
|
-
}
|
|
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 };
|
|
914
|
-
};
|
|
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);
|
|
2270
|
-
};
|
|
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(
|
|
2286
|
-
row({
|
|
2287
|
-
glyph: "error",
|
|
2288
|
-
label: "native update",
|
|
2289
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2290
|
-
detail: dim(error.message)
|
|
2291
|
-
})
|
|
2292
|
-
);
|
|
2293
|
-
if (error.detail) console.error(footer(dim(error.detail)));
|
|
2294
|
-
process.exit(1);
|
|
2295
|
-
}
|
|
2296
|
-
if (outcome.merged === "no-feed") {
|
|
2297
|
-
console.log(
|
|
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
|
-
})
|
|
2322
|
-
);
|
|
2323
|
-
}
|
|
2324
|
-
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
2325
|
-
console.log(
|
|
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
|
-
})
|
|
2334
|
-
);
|
|
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;
|
|
2347
|
-
};
|
|
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 });
|
|
2353
|
-
console.log(
|
|
2354
|
-
row({
|
|
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
|
-
)}`
|
|
2361
|
-
})
|
|
2362
|
-
);
|
|
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
|
-
})
|
|
2374
|
-
);
|
|
2375
|
-
}
|
|
2376
|
-
return zipPath;
|
|
2377
|
-
};
|
|
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(
|
|
2390
|
-
row({
|
|
2391
|
-
glyph: "error",
|
|
2392
|
-
label: packageLabel(target),
|
|
2393
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2394
|
-
detail: dim("packaging failed")
|
|
2395
|
-
})
|
|
2396
|
-
);
|
|
2397
|
-
console.error(dim(formatProcessError(e)));
|
|
2398
|
-
process.exit(1);
|
|
2399
|
-
}
|
|
2400
|
-
const pkgTime = ((Date.now() - start) / 1e3).toFixed(1);
|
|
2401
|
-
const sizeMB = (fs11.statSync(outputPath).size / (1024 * 1024)).toFixed(1);
|
|
2402
|
-
console.log(
|
|
2403
|
-
row({
|
|
2404
|
-
glyph: "done",
|
|
2405
|
-
label: packageLabel(target),
|
|
2406
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2407
|
-
detail: `${value(path12.basename(outputPath))} ${dim(`(${sizeMB} MB, ${pkgTime}s)`)}`
|
|
2408
|
-
})
|
|
2409
|
-
);
|
|
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;
|
|
2435
|
-
}
|
|
2436
|
-
table[i] = c >>> 0;
|
|
2437
|
-
}
|
|
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;
|
|
2446
|
-
};
|
|
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]);
|
|
2501
|
-
};
|
|
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;
|
|
2519
|
-
};
|
|
2520
|
-
|
|
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
|
-
};
|
|
2537
|
-
};
|
|
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")
|
|
2579
|
-
);
|
|
2580
|
-
} catch {
|
|
2581
|
-
return false;
|
|
2582
|
-
}
|
|
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");
|
|
2590
|
-
}
|
|
2591
|
-
const fromEnvironment = process.env.VIDRA_UPDATE_SIGNING_KEY;
|
|
2592
|
-
if (fromEnvironment && fromEnvironment.includes("PRIVATE KEY")) {
|
|
2593
|
-
return fromEnvironment;
|
|
2594
|
-
}
|
|
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"))) {
|
|
2627
|
-
console.error(
|
|
2628
|
-
row({
|
|
2629
|
-
glyph: "error",
|
|
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")
|
|
2633
|
-
})
|
|
2634
|
-
);
|
|
2635
|
-
process.exit(1);
|
|
2636
|
-
}
|
|
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);
|
|
2644
|
-
console.log(
|
|
2645
|
-
row({
|
|
2646
|
-
glyph: "done",
|
|
2647
|
-
label: "pack bundle",
|
|
2648
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2649
|
-
detail: `${value(name)} ${dim(`(${entries.length} files, ${formatBytes(archive.length)})`)}`
|
|
2650
|
-
})
|
|
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);
|
|
2667
|
-
console.log(
|
|
2668
|
-
row({
|
|
2669
|
-
glyph: "done",
|
|
2670
|
-
label: "write feed",
|
|
2671
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2672
|
-
detail: `${value("bundles.json")} ${dim(`(${manifest.bundles.length} entries)`)}`
|
|
2673
|
-
})
|
|
2674
|
-
);
|
|
2675
|
-
stepSignManifest(project, outDir, manifestBytes, options.sign);
|
|
2676
|
-
console.log(
|
|
2677
|
-
row({
|
|
2678
|
-
glyph: "plan",
|
|
2679
|
-
label: "compatible",
|
|
2680
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2681
|
-
detail: `${dim("core")} ${value(fingerprints.core.slice(0, 12))} ${dim("app")} ${value(fingerprints.app.slice(0, 12))}`
|
|
2682
|
-
})
|
|
2683
|
-
);
|
|
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) {
|
|
2703
|
-
console.log(
|
|
2704
|
-
row({
|
|
2705
|
-
glyph: "plan",
|
|
2706
|
-
label: "merge feed",
|
|
2707
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2708
|
-
detail: dim(`no index at ${options.mergeFrom} yet \u2014 publishing the first one`)
|
|
2709
|
-
})
|
|
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"));
|
|
2784
|
-
console.log(
|
|
2785
|
-
row({
|
|
2786
|
-
glyph: "manual",
|
|
2787
|
-
label: "sign feed",
|
|
2788
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2789
|
-
detail: amber(
|
|
2790
|
-
"unsigned \u2014 anyone who can write to your feed host can run code in your app"
|
|
2791
|
-
)
|
|
2792
|
-
})
|
|
2793
|
-
);
|
|
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(
|
|
2825
|
-
row({
|
|
2826
|
-
glyph: "error",
|
|
2827
|
-
label: "build UI",
|
|
2828
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
2829
|
-
detail: dim("vite build failed")
|
|
2830
|
-
})
|
|
2831
|
-
);
|
|
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(
|
|
2918
|
-
row({
|
|
2919
|
-
glyph: "error",
|
|
2920
|
-
label: "write key",
|
|
2921
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
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
|
-
)
|
|
2925
|
-
})
|
|
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
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
return null;
|
|
2997
|
-
};
|
|
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-"));
|
|
3018
|
-
try {
|
|
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();
|
|
3064
|
-
console.error(
|
|
3065
|
-
row({
|
|
3066
|
-
glyph: "error",
|
|
3067
|
-
detail: dim("no artifact given and none found in dist/ \u2014 pass a path")
|
|
3068
|
-
})
|
|
3069
|
-
);
|
|
3070
|
-
process.exit(1);
|
|
3071
|
-
}
|
|
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(", ")}`))
|
|
3086
|
-
);
|
|
3087
|
-
console.log();
|
|
3088
|
-
process.exit(1);
|
|
3089
|
-
}
|
|
3090
|
-
console.log(footer(dim("all checks passed")));
|
|
3091
|
-
console.log();
|
|
3092
|
-
};
|
|
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"];
|
|
3112
|
-
}
|
|
3113
|
-
appBundle = inner;
|
|
3114
|
-
}
|
|
3115
|
-
try {
|
|
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"
|
|
3124
|
-
);
|
|
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(
|
|
3135
|
-
row({
|
|
3136
|
-
glyph: assessment.ok ? "done" : "manual",
|
|
3137
|
-
label: "gatekeeper",
|
|
3138
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
3139
|
-
detail: dim(
|
|
3140
|
-
assessment.ok ? "spctl accepted \u2014 this opens on other Macs" : "spctl rejected \u2014 needs Developer ID + notarization (expected otherwise)"
|
|
3141
|
-
)
|
|
3142
|
-
})
|
|
3143
|
-
);
|
|
3144
|
-
} finally {
|
|
3145
|
-
mounted?.release();
|
|
3146
|
-
}
|
|
3147
|
-
return failures;
|
|
3148
|
-
};
|
|
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
|
-
}
|
|
3163
|
-
try {
|
|
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
|
|
3176
|
-
);
|
|
3177
|
-
if (!sig.ok) failures.push("authenticode");
|
|
3178
|
-
} finally {
|
|
3179
|
-
extracted?.release();
|
|
3180
|
-
}
|
|
3181
|
-
return failures;
|
|
3182
|
-
};
|
|
3183
|
-
var report = (ok, label, detail, output) => {
|
|
3184
|
-
console.log(
|
|
3185
|
-
row({
|
|
3186
|
-
glyph: ok ? "done" : "error",
|
|
3187
|
-
label,
|
|
3188
|
-
labelWidth: STEP_LABEL_WIDTH,
|
|
3189
|
-
detail: dim(detail)
|
|
3190
|
-
})
|
|
3191
|
-
);
|
|
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;
|
|
3201
|
-
};
|
|
3202
|
-
|
|
3203
|
-
// src/commands/dev.ts
|
|
3204
|
-
import path19 from "path";
|
|
3205
|
-
import fs18 from "fs-extra";
|
|
3206
|
-
import { execFileSync as execFileSync9, spawn as spawn2 } from "child_process";
|
|
3207
|
-
import { request } from "http";
|
|
3208
|
-
|
|
3209
|
-
// src/dev-port.ts
|
|
3210
|
-
import { createServer } from "net";
|
|
3211
|
-
import prompts2 from "prompts";
|
|
3212
|
-
var MIN_PORT = 1;
|
|
3213
|
-
var MAX_PORT = 65535;
|
|
3214
|
-
var isValidPort = (port) => Number.isInteger(port) && port >= MIN_PORT && port <= MAX_PORT;
|
|
3215
|
-
var isPortAvailable = (port, hostname) => new Promise((resolve, reject) => {
|
|
3216
|
-
const server = createServer();
|
|
3217
|
-
server.unref();
|
|
3218
|
-
server.once("error", (error) => {
|
|
3219
|
-
if (error.code === "EADDRINUSE" || error.code === "EACCES") {
|
|
3220
|
-
resolve(false);
|
|
3221
|
-
return;
|
|
3222
|
-
}
|
|
3223
|
-
reject(error);
|
|
3224
|
-
});
|
|
3225
|
-
server.listen({ host: hostname, port, exclusive: true }, () => {
|
|
3226
|
-
server.close((error) => {
|
|
3227
|
-
if (error) {
|
|
3228
|
-
reject(error);
|
|
3229
|
-
return;
|
|
3230
|
-
}
|
|
3231
|
-
resolve(true);
|
|
3232
|
-
});
|
|
3233
|
-
});
|
|
3234
|
-
});
|
|
3235
|
-
var findNextAvailablePort = async (occupiedPort, hostname, probe = isPortAvailable) => {
|
|
3236
|
-
for (let port = occupiedPort + 1; port <= MAX_PORT; port++) {
|
|
3237
|
-
if (await probe(port, hostname)) return port;
|
|
3238
|
-
}
|
|
3239
|
-
throw new Error(`No available port found after ${occupiedPort}`);
|
|
3240
|
-
};
|
|
3241
|
-
var defaultPrompt = async (question) => {
|
|
3242
|
-
const result = await prompts2(question, { onCancel: () => false });
|
|
3243
|
-
return { port: result.port };
|
|
3244
|
-
};
|
|
3245
|
-
var selectDevServerUrl = async (configuredUrl, options = {}) => {
|
|
3246
|
-
const url = new URL(configuredUrl);
|
|
3247
|
-
const port = Number(url.port);
|
|
3248
|
-
if (!isValidPort(port)) {
|
|
3249
|
-
throw new Error(`Dev server URL must include a valid port: ${configuredUrl}`);
|
|
3250
|
-
}
|
|
3251
|
-
const probe = options.isPortAvailable ?? isPortAvailable;
|
|
3252
|
-
if (await probe(port, url.hostname)) return configuredUrl;
|
|
3253
|
-
const interactive = options.interactive ?? isInteractive();
|
|
3254
|
-
if (!interactive) {
|
|
3255
|
-
throw new Error(
|
|
3256
|
-
`Port ${port} is already in use and no interactive terminal is available`
|
|
3257
|
-
);
|
|
3258
|
-
}
|
|
3259
|
-
const suggestedPort = await findNextAvailablePort(port, url.hostname, probe);
|
|
3260
|
-
const prompt = options.prompt ?? defaultPrompt;
|
|
3261
|
-
const result = await prompt({
|
|
3262
|
-
type: "number",
|
|
3263
|
-
name: "port",
|
|
3264
|
-
message: `Port ${port} is already in use. Choose another port:`,
|
|
3265
|
-
initial: suggestedPort,
|
|
3266
|
-
validate: async (value3) => {
|
|
3267
|
-
if (!isValidPort(value3)) {
|
|
3268
|
-
return `Enter a port between ${MIN_PORT} and ${MAX_PORT}`;
|
|
3269
|
-
}
|
|
3270
|
-
return await probe(value3, url.hostname) || `Port ${value3} is already in use`;
|
|
3271
|
-
}
|
|
3272
|
-
});
|
|
3273
|
-
const selectedPort = result.port;
|
|
3274
|
-
if (selectedPort === void 0) {
|
|
3275
|
-
throw new Error("Port selection cancelled");
|
|
3276
|
-
}
|
|
3277
|
-
if (!isValidPort(selectedPort) || !await probe(selectedPort, url.hostname)) {
|
|
3278
|
-
throw new Error(`Port ${selectedPort} is not available`);
|
|
3279
|
-
}
|
|
3280
|
-
url.port = String(selectedPort);
|
|
3281
|
-
return url.toString();
|
|
3282
|
-
};
|
|
3283
|
-
|
|
3284
|
-
// src/commands/dev.ts
|
|
3285
|
-
var POLL_INTERVAL_MS = 500;
|
|
3286
|
-
var POLL_TIMEOUT_MS = 3e4;
|
|
3287
|
-
var NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
3288
|
-
var DOTNET_COMMAND = process.platform === "win32" ? "dotnet.exe" : "dotnet";
|
|
3289
|
-
var WATCH_OUTPUT_TAIL_CHARS = 8192;
|
|
3290
|
-
var HOST_TERMINATION_TIMEOUT_MS = 5e3;
|
|
3291
|
-
var TARGETS2 = {
|
|
3292
|
-
macos: {
|
|
3293
|
-
name: "macos",
|
|
3294
|
-
framework: "net10.0-maccatalyst"
|
|
3295
|
-
},
|
|
3296
|
-
windows: {
|
|
3297
|
-
name: "windows",
|
|
3298
|
-
framework: "net10.0-windows10.0.19041.0"
|
|
3299
|
-
}
|
|
3300
|
-
};
|
|
3301
|
-
var devCommand = (argv) => startSession(argv, { vite: true, hotReloadDefault: true });
|
|
3302
|
-
var runCommand = (argv) => startSession(argv, { vite: false, hotReloadDefault: false });
|
|
3303
|
-
var startSession = async (argv, opts) => {
|
|
3304
|
-
const args = parseArgs(["_", "_", ...argv]);
|
|
3305
|
-
const targetName = args["target"] || detectPlatform();
|
|
3306
|
-
const verbose = !!args["verbose"];
|
|
3307
|
-
const hotReload = opts.hotReloadDefault && !args["no-hot-reload"];
|
|
3308
|
-
let viteUrl = process.env.VIDRA_DEV_URL || "http://localhost:5173";
|
|
3309
|
-
const target = TARGETS2[targetName];
|
|
3310
|
-
if (!target) {
|
|
3311
|
-
const supported = Object.keys(TARGETS2).join(", ");
|
|
3312
|
-
console.error(
|
|
3313
|
-
row({
|
|
3314
|
-
glyph: "error",
|
|
3315
|
-
detail: dim(`unsupported target: ${targetName} \u2014 supported: ${supported}`)
|
|
3316
|
-
})
|
|
3317
|
-
);
|
|
3318
|
-
process.exit(1);
|
|
3319
|
-
}
|
|
3320
|
-
ensureTargetMatchesHostOs(target.name);
|
|
3321
|
-
const project = detectProject(process.cwd());
|
|
3322
|
-
if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
|
|
3323
|
-
process.exit(1);
|
|
3324
|
-
}
|
|
3325
|
-
if (opts.vite) {
|
|
3326
|
-
viteUrl = await selectDevServerUrl(viteUrl);
|
|
3327
|
-
}
|
|
3328
|
-
const session = new DevSession(project, target, viteUrl, verbose, {
|
|
3329
|
-
vite: opts.vite,
|
|
3330
|
-
hotReload
|
|
3331
|
-
});
|
|
3332
|
-
await session.run();
|
|
3333
|
-
};
|
|
3334
|
-
var watchStrategyFor = (_targetName) => "delta";
|
|
3335
|
-
var DELTA_CHANNEL_DEAD_WARNING = "further changes won't be applied to this process";
|
|
3336
|
-
var buildDotnetWatchArgs = (opts) => [
|
|
3337
|
-
"watch",
|
|
3338
|
-
"--project",
|
|
3339
|
-
opts.csprojPath,
|
|
3340
|
-
// Never prompt on rude edits (our stdio is piped, so a prompt would hang);
|
|
3341
|
-
// paired with DOTNET_WATCH_RESTART_ON_RUDE_EDIT for older SDKs.
|
|
3342
|
-
"--non-interactive",
|
|
3343
|
-
...opts.verbose ? ["--verbose"] : [],
|
|
3344
|
-
opts.strategy === "rebuild" ? "build" : "run",
|
|
3345
|
-
"-f",
|
|
3346
|
-
opts.framework,
|
|
3347
|
-
"-c",
|
|
3348
|
-
opts.buildConfig,
|
|
3349
|
-
...opts.strategy === "delta" && opts.framework.includes("maccatalyst") ? ["--property:RunWithOpen=false"] : []
|
|
3350
|
-
];
|
|
3351
|
-
var dotnetWatchEnv = (devUrl) => ({
|
|
3352
|
-
VIDRA_DEV_URL: devUrl,
|
|
3353
|
-
DOTNET_WATCH_RESTART_ON_RUDE_EDIT: "1",
|
|
3354
|
-
DOTNET_WATCH_SUPPRESS_EMOJIS: "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"
|
|
3361
|
-
});
|
|
3362
|
-
var buildViteArgs = (devUrl) => [
|
|
3363
|
-
"run",
|
|
3364
|
-
"dev",
|
|
3365
|
-
"--",
|
|
3366
|
-
"--port",
|
|
3367
|
-
new URL(devUrl).port,
|
|
3368
|
-
"--strictPort"
|
|
3369
|
-
];
|
|
3370
|
-
var HOST_READY_SENTINEL = "[vidra] host ready";
|
|
3371
|
-
var classifyWatchLine = (line) => {
|
|
3372
|
-
if (line.toLowerCase().includes(DELTA_CHANNEL_DEAD_WARNING)) {
|
|
3373
|
-
return "deltaChannelDead";
|
|
3374
|
-
}
|
|
3375
|
-
if (/waiting for a file to change/i.test(line)) return "appWaiting";
|
|
3376
|
-
if (/fix the error to continue/i.test(line)) return "buildBlocked";
|
|
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";
|
|
3380
|
-
if (/\bwatch\b/i.test(line) && /\b(?:started|launched)\b/i.test(line)) {
|
|
3381
|
-
return "appStarted";
|
|
3382
|
-
}
|
|
3383
|
-
return null;
|
|
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
|
-
};
|
|
3406
|
-
var DevSession = class {
|
|
3407
|
-
constructor(project, target, viteUrl, verbose, options) {
|
|
3408
|
-
this.project = project;
|
|
3409
|
-
this.target = target;
|
|
3410
|
-
this.viteUrl = viteUrl;
|
|
3411
|
-
this.verbose = verbose;
|
|
3412
|
-
this.vite = options.vite;
|
|
3413
|
-
this.hotReload = options.hotReload;
|
|
3414
|
-
this.strategy = watchStrategyFor(target.name);
|
|
3415
|
-
}
|
|
3416
|
-
project;
|
|
3417
|
-
target;
|
|
3418
|
-
viteUrl;
|
|
3419
|
-
verbose;
|
|
3420
|
-
children = [];
|
|
3421
|
-
buildConfig = process.env.VIDRA_BUILD_CONFIG || "Debug";
|
|
3422
|
-
vite;
|
|
3423
|
-
hotReload;
|
|
3424
|
-
// Not readonly: a session that watches its delta channel die moves itself to
|
|
3425
|
-
// the rebuild loop (see WatchStrategy).
|
|
3426
|
-
strategy;
|
|
3427
|
-
shuttingDown = false;
|
|
3428
|
-
// Watch-mode state: `watchChild` is the `dotnet watch` process (a process
|
|
3429
|
-
// group leader on unix, so shutdown can signal it and the app it launched
|
|
3430
|
-
// together); `watchReady` flips once the app has started at least once.
|
|
3431
|
-
watchChild;
|
|
3432
|
-
watchReady = false;
|
|
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;
|
|
3442
|
-
endSession = () => {
|
|
3443
|
-
};
|
|
3444
|
-
sessionDone = new Promise((resolve) => {
|
|
3445
|
-
this.endSession = resolve;
|
|
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
|
-
}
|
|
3462
|
-
async run() {
|
|
3463
|
-
this.installSignalHandlers();
|
|
3464
|
-
console.log();
|
|
3465
|
-
console.log(header(this.vite ? "dev" : "run", this.target.name));
|
|
3466
|
-
console.log(kv("project", this.project.projectName));
|
|
3467
|
-
console.log(kv("target", this.target.framework));
|
|
3468
|
-
console.log();
|
|
3469
|
-
if (this.vite) {
|
|
3470
|
-
this.startVite();
|
|
3471
|
-
try {
|
|
3472
|
-
await waitForServer(this.viteUrl, POLL_TIMEOUT_MS);
|
|
3473
|
-
} catch (error) {
|
|
3474
|
-
console.error(row({ glyph: "error", detail: dim(error.message) }));
|
|
3475
|
-
this.shutdown(1);
|
|
3476
|
-
}
|
|
3477
|
-
console.log(
|
|
3478
|
-
taggedRow("active", "ui", `${dim("vite ready \u2014")} ${value(this.viteUrl)}`)
|
|
3479
|
-
);
|
|
3480
|
-
} else {
|
|
3481
|
-
console.log(
|
|
3482
|
-
taggedRow(
|
|
3483
|
-
"skip",
|
|
3484
|
-
"ui",
|
|
3485
|
-
`${dim("vite not started \u2014")} ${value("npm run dev:ui")}`
|
|
3486
|
-
)
|
|
3487
|
-
);
|
|
3488
|
-
}
|
|
3489
|
-
this.launchHost();
|
|
3490
|
-
const hostDirLabel = `src/${path19.basename(this.project.hostDir)}`;
|
|
3491
|
-
if (this.vite) {
|
|
3492
|
-
console.log(
|
|
3493
|
-
taggedRow(
|
|
3494
|
-
"active",
|
|
3495
|
-
null,
|
|
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")}`
|
|
3497
|
-
)
|
|
3498
|
-
);
|
|
3499
|
-
console.log();
|
|
3500
|
-
console.log(
|
|
3501
|
-
footer(
|
|
3502
|
-
this.hotReload ? `${dim("watching")} ${value("ui/")} ${dim("\xB7")} ${value(`${hostDirLabel}/`)} ${dim(
|
|
3503
|
-
`\xB7 ${this.csharpLoopSuffix} \xB7 ctrl-c to stop`
|
|
3504
|
-
)}` : `${dim("watching")} ${value("ui/")} ${dim(
|
|
3505
|
-
"\xB7 hot reload on save \xB7 ctrl-c to stop"
|
|
3506
|
-
)}`
|
|
3507
|
-
)
|
|
3508
|
-
);
|
|
3509
|
-
} else {
|
|
3510
|
-
console.log();
|
|
3511
|
-
console.log(
|
|
3512
|
-
footer(dim("host only \xB7 serve the UI separately \xB7 ctrl-c to stop"))
|
|
3513
|
-
);
|
|
3514
|
-
}
|
|
3515
|
-
console.log();
|
|
3516
|
-
await this.sessionDone;
|
|
3517
|
-
}
|
|
3518
|
-
installSignalHandlers() {
|
|
3519
|
-
process.on("SIGINT", () => {
|
|
3520
|
-
console.log("\n" + footer(dim("shutting down\u2026")));
|
|
3521
|
-
this.shutdown(0);
|
|
3522
|
-
});
|
|
3523
|
-
process.on("SIGTERM", () => {
|
|
3524
|
-
this.shutdown(0);
|
|
3525
|
-
});
|
|
3526
|
-
}
|
|
3527
|
-
startVite() {
|
|
3528
|
-
console.log(taggedRow("active", "ui", dim("starting dev server\u2026")));
|
|
3529
|
-
const vite = spawn2(NPM_COMMAND, buildViteArgs(this.viteUrl), {
|
|
3530
|
-
cwd: this.project.uiDir,
|
|
3531
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3532
|
-
shell: process.platform === "win32"
|
|
3533
|
-
});
|
|
3534
|
-
return this.registerChild(vite, "ui", "Vite dev server");
|
|
3535
|
-
}
|
|
3536
|
-
launchHost() {
|
|
3537
|
-
if (this.hotReload) {
|
|
3538
|
-
return this.launchHostWithWatch();
|
|
3539
|
-
}
|
|
3540
|
-
return this.target.name === "macos" ? this.launchMacosHost() : this.launchWindowsHost();
|
|
3541
|
-
}
|
|
3542
|
-
// --- dotnet watch launch (C# hot reload) -----------------------------------
|
|
3543
|
-
launchHostWithWatch() {
|
|
3544
|
-
console.log(
|
|
3545
|
-
taggedRow(
|
|
3546
|
-
"active",
|
|
3547
|
-
"host",
|
|
3548
|
-
`${dim("dotnet watch \u2014 building")} ${value(this.target.framework)} ${dim("\u2026")}`
|
|
3549
|
-
)
|
|
3550
|
-
);
|
|
3551
|
-
const watch = spawn2(
|
|
3552
|
-
DOTNET_COMMAND,
|
|
3553
|
-
buildDotnetWatchArgs({
|
|
3554
|
-
csprojPath: this.project.csprojPath,
|
|
3555
|
-
framework: this.target.framework,
|
|
3556
|
-
buildConfig: this.buildConfig,
|
|
3557
|
-
verbose: this.verbose,
|
|
3558
|
-
strategy: this.strategy
|
|
3559
|
-
}),
|
|
3560
|
-
{
|
|
3561
|
-
cwd: this.project.root,
|
|
3562
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3563
|
-
env: { ...process.env, ...dotnetWatchEnv(this.viteUrl) },
|
|
3564
|
-
// Unix: make dotnet watch its own process-group leader so shutdown can
|
|
3565
|
-
// SIGTERM the group — taking down both watch and the app it launched.
|
|
3566
|
-
// Windows cleanup goes through `taskkill /T` instead.
|
|
3567
|
-
detached: process.platform !== "win32"
|
|
3568
|
-
}
|
|
3569
|
-
);
|
|
3570
|
-
this.watchChild = watch;
|
|
3571
|
-
this.children.push(watch);
|
|
3572
|
-
const notSentinel = (line) => !line.includes(HOST_READY_SENTINEL);
|
|
3573
|
-
prefixStream(watch.stdout, "host", notSentinel);
|
|
3574
|
-
prefixStream(watch.stderr, "host", notSentinel);
|
|
3575
|
-
scanStream(watch.stdout, (line) => this.onWatchLine(line));
|
|
3576
|
-
scanStream(watch.stderr, (line) => this.onWatchLine(line));
|
|
3577
|
-
watch.on("exit", (code, signal) => {
|
|
3578
|
-
if (this.shuttingDown) return;
|
|
3579
|
-
if (this.watchChild !== watch) return;
|
|
3580
|
-
if (this.watchReady) {
|
|
3581
|
-
const failed = code !== null && code !== 0 || signal !== null;
|
|
3582
|
-
if (failed) {
|
|
3583
|
-
console.error(
|
|
3584
|
-
"\n" + row({
|
|
3585
|
-
glyph: "error",
|
|
3586
|
-
detail: dim(
|
|
3587
|
-
`dotnet watch exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
3588
|
-
)
|
|
3589
|
-
})
|
|
3590
|
-
);
|
|
3591
|
-
}
|
|
3592
|
-
this.shutdown(code ?? (signal ? 1 : 0));
|
|
3593
|
-
return;
|
|
3594
|
-
}
|
|
3595
|
-
this.fallBackToClassicLaunch(code, signal);
|
|
3596
|
-
});
|
|
3597
|
-
watch.on("error", (error) => {
|
|
3598
|
-
if (this.shuttingDown) return;
|
|
3599
|
-
console.error(
|
|
3600
|
-
"\n" + row({
|
|
3601
|
-
glyph: "error",
|
|
3602
|
-
detail: dim(`failed to start dotnet watch: ${error.message}`)
|
|
3603
|
-
})
|
|
3604
|
-
);
|
|
3605
|
-
this.shutdown(1);
|
|
3606
|
-
});
|
|
3607
|
-
return watch;
|
|
3608
|
-
}
|
|
3609
|
-
onWatchLine(line) {
|
|
3610
|
-
this.watchOutputTail = (this.watchOutputTail + line + "\n").slice(
|
|
3611
|
-
-WATCH_OUTPUT_TAIL_CHARS
|
|
3612
|
-
);
|
|
3613
|
-
const event = classifyWatchLine(line);
|
|
3614
|
-
if (event === "buildSucceeded" || event === "buildFailed") {
|
|
3615
|
-
this.buildOutcome = event === "buildSucceeded" ? "succeeded" : "failed";
|
|
3616
|
-
this.everBuilt = true;
|
|
3617
|
-
}
|
|
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;
|
|
3627
|
-
}
|
|
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")
|
|
3644
|
-
)
|
|
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;
|
|
3723
|
-
return;
|
|
3724
|
-
}
|
|
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();
|
|
3759
|
-
console.log(
|
|
3760
|
-
taggedRow(
|
|
3761
|
-
"manual",
|
|
3762
|
-
"host",
|
|
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")
|
|
3773
|
-
)
|
|
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();
|
|
3793
|
-
}
|
|
3794
|
-
/**
|
|
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.
|
|
3799
|
-
*/
|
|
3800
|
-
fallBackToClassicLaunch(code, signal) {
|
|
3801
|
-
console.log();
|
|
3802
|
-
console.log(
|
|
3803
|
-
taggedRow(
|
|
3804
|
-
"manual",
|
|
3805
|
-
"host",
|
|
3806
|
-
`${dim("the C# watch loop is unavailable \u2014 dotnet watch exited with")} ${value(
|
|
3807
|
-
signal ? `signal ${signal}` : `code ${code ?? "unknown"}`
|
|
3808
|
-
)}`
|
|
3809
|
-
)
|
|
3810
|
-
);
|
|
3811
|
-
this.fellBackToClassic = true;
|
|
3812
|
-
if (looksLikeMissingWorkload(this.watchOutputTail)) {
|
|
3813
|
-
printWorkloadHint();
|
|
3814
|
-
} else if (looksLikeXcodeTooOld(this.watchOutputTail)) {
|
|
3815
|
-
printXcodeTooOldHint();
|
|
3816
|
-
} else if (looksLikeMissingXcode(this.watchOutputTail)) {
|
|
3817
|
-
printXcodeHint();
|
|
3818
|
-
} else {
|
|
3819
|
-
console.log(
|
|
3820
|
-
footer(
|
|
3821
|
-
dim("nothing in the output explains it \u2014 an incomplete toolchain is the usual cause:")
|
|
3822
|
-
)
|
|
3823
|
-
);
|
|
3824
|
-
console.log(fixLine("dotnet workload update"));
|
|
3825
|
-
if (!this.verbose) {
|
|
3826
|
-
console.log(footer(dim("re-run with --verbose for the full watch log.")));
|
|
3827
|
-
}
|
|
3828
|
-
}
|
|
3829
|
-
console.log(
|
|
3830
|
-
taggedRow(
|
|
3831
|
-
"active",
|
|
3832
|
-
"host",
|
|
3833
|
-
dim("falling back to a classic launch (frontend hot reload still active)\u2026")
|
|
3834
|
-
)
|
|
3835
|
-
);
|
|
3836
|
-
console.log();
|
|
3837
|
-
if (this.target.name === "macos") {
|
|
3838
|
-
this.launchMacosHost();
|
|
3839
|
-
} else {
|
|
3840
|
-
this.launchWindowsHost();
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
// --- classic launch (one build + direct spawn) ------------------------------
|
|
3844
|
-
// Builds the MAUI host as a discrete step (a plain `dotnet build`, never
|
|
3845
|
-
// MSBuild's `-t:Run`) so the per-OS launch paths can spawn the produced
|
|
3846
|
-
// binary directly. `-t:Run` shells out in ways that break for both locally
|
|
3847
|
-
// signed mac apps and unpackaged Windows apps (see the call sites).
|
|
3848
|
-
buildHostSync() {
|
|
3849
|
-
console.log(
|
|
3850
|
-
taggedRow(
|
|
3851
|
-
"active",
|
|
3852
|
-
"host",
|
|
3853
|
-
`${dim("building")} ${value(this.target.framework)} ${dim("\u2026")}`
|
|
3854
|
-
)
|
|
3855
|
-
);
|
|
3856
|
-
try {
|
|
3857
|
-
execFileSync9(
|
|
3858
|
-
DOTNET_COMMAND,
|
|
3859
|
-
[
|
|
3860
|
-
"build",
|
|
3861
|
-
"-c",
|
|
3862
|
-
this.buildConfig,
|
|
3863
|
-
"-f",
|
|
3864
|
-
this.target.framework,
|
|
3865
|
-
this.project.csprojPath
|
|
3866
|
-
],
|
|
3867
|
-
{
|
|
3868
|
-
cwd: this.project.root,
|
|
3869
|
-
stdio: this.verbose ? "inherit" : "pipe"
|
|
3870
|
-
}
|
|
3871
|
-
);
|
|
3872
|
-
} catch (error) {
|
|
3873
|
-
const output = formatBuildError(error);
|
|
3874
|
-
console.error(taggedRow("error", "host", dim("MAUI build failed")));
|
|
3875
|
-
console.error(dim(output));
|
|
3876
|
-
if (looksLikeMissingWorkload(output)) printWorkloadHint();
|
|
3877
|
-
else if (looksLikeMissingXcode(output)) printXcodeHint();
|
|
3878
|
-
if (!this.verbose) {
|
|
3879
|
-
console.error(footer(dim("re-run with --verbose for the full build log.")));
|
|
3880
|
-
}
|
|
3881
|
-
process.exit(1);
|
|
3882
|
-
}
|
|
3883
|
-
}
|
|
3884
|
-
launchMacosHost() {
|
|
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
|
-
};
|
|
3911
|
-
const appBundle = findMacAppBundle(
|
|
3912
|
-
this.project.hostDir,
|
|
3913
|
-
this.target.framework,
|
|
3914
|
-
this.buildConfig
|
|
3915
|
-
);
|
|
3916
|
-
if (!appBundle) return fail2(`could not find .app bundle in ${outputDir}`);
|
|
3917
|
-
signMacAppBundleIfPossible(appBundle, {
|
|
3918
|
-
verbose: this.verbose,
|
|
3919
|
-
log: console.log,
|
|
3920
|
-
warn: console.warn
|
|
3921
|
-
});
|
|
3922
|
-
const binary = findMacExecutable(appBundle);
|
|
3923
|
-
if (!binary) {
|
|
3924
|
-
return fail2(`could not find the app executable in ${appBundle}`);
|
|
3925
|
-
}
|
|
3926
|
-
console.log(
|
|
3927
|
-
taggedRow("active", "host", `${dim("launching")} ${value(path19.basename(appBundle))}${dim("\u2026")}`)
|
|
3928
|
-
);
|
|
3929
|
-
const host = spawn2(binary, [], {
|
|
3930
|
-
cwd: this.project.root,
|
|
3931
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3932
|
-
env: { ...process.env, VIDRA_DEV_URL: this.viteUrl }
|
|
3933
|
-
});
|
|
3934
|
-
return this.registerChild(host, "host", path19.basename(binary));
|
|
3935
|
-
}
|
|
3936
|
-
// Build first, then spawn the produced .exe directly. A single
|
|
3937
|
-
// `dotnet build -t:Run` on an unpackaged MAUI Windows app
|
|
3938
|
-
// (`WindowsPackageType=None`) execs the binary before the WindowsAppSDK
|
|
3939
|
-
// native assets are laid out beside it, so the app can't resolve its deps and
|
|
3940
|
-
// dies with a bare `MSB3073 ... exited with code 3` (ERROR_PATH_NOT_FOUND —
|
|
3941
|
-
// "The system cannot find the path specified"). Building as a discrete step
|
|
3942
|
-
// and then launching the binary is the documented workaround.
|
|
3943
|
-
// See dotnet/maui#13942 and dotnet/maui#5975.
|
|
3944
|
-
launchWindowsHost() {
|
|
3945
|
-
this.buildHostSync();
|
|
3946
|
-
const exe = findWindowsExecutable2(
|
|
3947
|
-
this.project.hostDir,
|
|
3948
|
-
this.project.csprojPath,
|
|
3949
|
-
this.target.framework,
|
|
3950
|
-
this.buildConfig
|
|
3951
|
-
);
|
|
3952
|
-
if (!exe) {
|
|
3953
|
-
console.error(
|
|
3954
|
-
row({
|
|
3955
|
-
glyph: "error",
|
|
3956
|
-
detail: dim(
|
|
3957
|
-
`could not find the host .exe under ${path19.join(this.project.hostDir, "bin", this.buildConfig, this.target.framework)}`
|
|
3958
|
-
)
|
|
3959
|
-
})
|
|
3960
|
-
);
|
|
3961
|
-
process.exit(1);
|
|
3962
|
-
}
|
|
3963
|
-
console.log(
|
|
3964
|
-
taggedRow("active", "host", `${dim("launching")} ${value(path19.basename(exe))}${dim("\u2026")}`)
|
|
3965
|
-
);
|
|
3966
|
-
const host = spawn2(exe, [], {
|
|
3967
|
-
cwd: path19.dirname(exe),
|
|
3968
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3969
|
-
env: { ...process.env, VIDRA_DEV_URL: this.viteUrl }
|
|
3970
|
-
});
|
|
3971
|
-
return this.registerChild(host, "host", path19.basename(exe));
|
|
3972
|
-
}
|
|
3973
|
-
registerChild(child, tag2, label) {
|
|
3974
|
-
this.children.push(child);
|
|
3975
|
-
const include = tag2 === "host" ? (line) => !line.includes(HOST_READY_SENTINEL) : void 0;
|
|
3976
|
-
prefixStream(child.stdout, tag2, include);
|
|
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
|
-
}
|
|
3985
|
-
child.on("exit", (code, signal) => {
|
|
3986
|
-
this.forgetChild(child);
|
|
3987
|
-
if (this.shuttingDown) return;
|
|
3988
|
-
if (tag2 === "ui") {
|
|
3989
|
-
const exitCode = code ?? 1;
|
|
3990
|
-
console.error(
|
|
3991
|
-
"\n" + row({ glyph: "error", detail: dim(`${label} exited with code ${exitCode}`) })
|
|
3992
|
-
);
|
|
3993
|
-
this.shutdown(exitCode);
|
|
3994
|
-
return;
|
|
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
|
-
}
|
|
4008
|
-
const failed = code !== null && code !== 0 || signal !== null;
|
|
4009
|
-
if (failed) {
|
|
4010
|
-
console.error(
|
|
4011
|
-
"\n" + row({
|
|
4012
|
-
glyph: "error",
|
|
4013
|
-
detail: dim(
|
|
4014
|
-
`${label} exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
4015
|
-
)
|
|
4016
|
-
})
|
|
4017
|
-
);
|
|
4018
|
-
if (tag2 === "host" && this.target.name === "macos") {
|
|
4019
|
-
printMacLaunchHint();
|
|
4020
|
-
}
|
|
4021
|
-
}
|
|
4022
|
-
this.shutdown(code ?? (signal ? 1 : 0));
|
|
4023
|
-
});
|
|
4024
|
-
child.on("error", (error) => {
|
|
4025
|
-
if (this.shuttingDown) return;
|
|
4026
|
-
console.error(
|
|
4027
|
-
"\n" + row({ glyph: "error", detail: dim(`failed to start ${label}: ${error.message}`) })
|
|
4028
|
-
);
|
|
4029
|
-
if (tag2 === "host" && this.target.name === "macos") {
|
|
4030
|
-
printMacLaunchHint();
|
|
4031
|
-
}
|
|
4032
|
-
this.shutdown(1);
|
|
4033
|
-
});
|
|
4034
|
-
return child;
|
|
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
|
-
}
|
|
4049
|
-
shutdown(exitCode) {
|
|
4050
|
-
if (this.shuttingDown) return;
|
|
4051
|
-
this.shuttingDown = true;
|
|
4052
|
-
for (const child of [...this.children]) {
|
|
4053
|
-
killChild(child, { processGroup: child === this.watchChild });
|
|
4054
|
-
}
|
|
4055
|
-
this.endSession();
|
|
4056
|
-
process.exit(exitCode);
|
|
4057
|
-
}
|
|
4058
|
-
};
|
|
4059
|
-
var ensureTargetMatchesHostOs = (targetName) => {
|
|
4060
|
-
if (targetName === "macos" && process.platform !== "darwin") {
|
|
4061
|
-
console.error(
|
|
4062
|
-
row({ glyph: "error", detail: dim("the macOS target can only run on macOS") })
|
|
4063
|
-
);
|
|
4064
|
-
process.exit(1);
|
|
4065
|
-
}
|
|
4066
|
-
if (targetName === "windows" && process.platform !== "win32") {
|
|
4067
|
-
console.error(
|
|
4068
|
-
row({ glyph: "error", detail: dim("the Windows target can only run on Windows") })
|
|
4069
|
-
);
|
|
4070
|
-
process.exit(1);
|
|
4071
|
-
}
|
|
4072
|
-
};
|
|
4073
|
-
var prefixStream = (stream, tag2, include = () => true) => {
|
|
4074
|
-
if (!stream) return;
|
|
4075
|
-
const prefix = streamPrefix(tag2);
|
|
4076
|
-
stream.on("data", (chunk) => {
|
|
4077
|
-
const lines = chunk.toString().split("\n");
|
|
4078
|
-
for (const line of lines) {
|
|
4079
|
-
if (line.length > 0 && include(line)) {
|
|
4080
|
-
process.stdout.write(`${prefix} ${line}
|
|
4081
|
-
`);
|
|
4082
|
-
}
|
|
4083
|
-
}
|
|
4084
|
-
});
|
|
4085
|
-
};
|
|
4086
|
-
var scanStream = (stream, onLine) => {
|
|
4087
|
-
if (!stream) return;
|
|
4088
|
-
let pending = "";
|
|
4089
|
-
stream.on("data", (chunk) => {
|
|
4090
|
-
pending += chunk.toString();
|
|
4091
|
-
const lines = pending.split("\n");
|
|
4092
|
-
pending = lines.pop() ?? "";
|
|
4093
|
-
for (const line of lines) onLine(line);
|
|
4094
|
-
});
|
|
4095
|
-
stream.on("end", () => {
|
|
4096
|
-
if (pending) onLine(pending);
|
|
4097
|
-
});
|
|
4098
|
-
};
|
|
4099
|
-
var waitForServer = (url, timeoutMs) => {
|
|
4100
|
-
const { hostname, port, pathname } = new URL(url);
|
|
4101
|
-
const start = Date.now();
|
|
4102
|
-
return new Promise((resolve, reject) => {
|
|
4103
|
-
const poll = () => {
|
|
4104
|
-
if (Date.now() - start > timeoutMs) {
|
|
4105
|
-
reject(
|
|
4106
|
-
new Error(`Timed out waiting for ${url} after ${timeoutMs}ms`)
|
|
4107
|
-
);
|
|
4108
|
-
return;
|
|
4109
|
-
}
|
|
4110
|
-
const req = request(
|
|
4111
|
-
{
|
|
4112
|
-
hostname,
|
|
4113
|
-
port,
|
|
4114
|
-
path: pathname,
|
|
4115
|
-
method: "HEAD",
|
|
4116
|
-
timeout: 1e3
|
|
4117
|
-
},
|
|
4118
|
-
() => resolve()
|
|
4119
|
-
);
|
|
4120
|
-
req.on("error", () => setTimeout(poll, POLL_INTERVAL_MS));
|
|
4121
|
-
req.on("timeout", () => {
|
|
4122
|
-
req.destroy();
|
|
4123
|
-
setTimeout(poll, POLL_INTERVAL_MS);
|
|
4124
|
-
});
|
|
4125
|
-
req.end();
|
|
4126
|
-
};
|
|
4127
|
-
poll();
|
|
4128
|
-
});
|
|
4129
|
-
};
|
|
4130
|
-
var findMacAppBundle = (hostDir, framework, buildConfig) => {
|
|
4131
|
-
const outputDir = path19.join(hostDir, "bin", buildConfig, framework);
|
|
4132
|
-
return findAppBundleRecursive(outputDir);
|
|
4133
|
-
};
|
|
4134
|
-
var findAppBundleRecursive = (dir) => {
|
|
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);
|
|
4138
|
-
if (entry.isDirectory() && entry.name.endsWith(".app")) {
|
|
4139
|
-
return fullPath;
|
|
4140
|
-
}
|
|
4141
|
-
if (entry.isDirectory()) {
|
|
4142
|
-
const nested = findAppBundleRecursive(fullPath);
|
|
4143
|
-
if (nested) return nested;
|
|
4144
|
-
}
|
|
4145
|
-
}
|
|
4146
|
-
return null;
|
|
4147
|
-
};
|
|
4148
|
-
var findMacExecutable = (appBundle) => {
|
|
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 })) {
|
|
4152
|
-
if (entry.isFile()) {
|
|
4153
|
-
return path19.join(macOsDir, entry.name);
|
|
4154
|
-
}
|
|
4155
|
-
}
|
|
4156
|
-
return null;
|
|
4157
|
-
};
|
|
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();
|
|
4162
|
-
return findFileRecursive(outputDir, (name) => name.toLowerCase() === exeName) ?? findFileRecursive(
|
|
4163
|
-
outputDir,
|
|
4164
|
-
(name) => name.toLowerCase().endsWith(".host.exe")
|
|
4165
|
-
) ?? findFileRecursive(outputDir, (name) => name.toLowerCase().endsWith(".exe"));
|
|
4166
|
-
};
|
|
4167
|
-
var findFileRecursive = (dir, match) => {
|
|
4168
|
-
if (!fs18.existsSync(dir)) return null;
|
|
4169
|
-
const entries = fs18.readdirSync(dir, { withFileTypes: true });
|
|
4170
|
-
for (const entry of entries) {
|
|
4171
|
-
if (entry.isFile() && match(entry.name)) {
|
|
4172
|
-
return path19.join(dir, entry.name);
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4175
|
-
for (const entry of entries) {
|
|
4176
|
-
if (entry.isDirectory()) {
|
|
4177
|
-
const found = findFileRecursive(path19.join(dir, entry.name), match);
|
|
4178
|
-
if (found) return found;
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
|
-
return null;
|
|
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
|
-
});
|
|
4197
|
-
var killChild = (child, opts = {}) => {
|
|
4198
|
-
if (!child.pid || child.exitCode !== null) return;
|
|
4199
|
-
if (process.platform === "win32") {
|
|
4200
|
-
try {
|
|
4201
|
-
execFileSync9("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
|
4202
|
-
stdio: "ignore"
|
|
4203
|
-
});
|
|
4204
|
-
} catch {
|
|
4205
|
-
child.kill();
|
|
4206
|
-
}
|
|
4207
|
-
return;
|
|
4208
|
-
}
|
|
4209
|
-
if (opts.processGroup) {
|
|
4210
|
-
try {
|
|
4211
|
-
process.kill(-child.pid, "SIGTERM");
|
|
4212
|
-
return;
|
|
4213
|
-
} catch {
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
child.kill("SIGTERM");
|
|
4217
|
-
};
|
|
4218
|
-
var printMacLaunchHint = () => {
|
|
4219
|
-
console.error();
|
|
4220
|
-
console.error(
|
|
4221
|
-
row({ glyph: "manual", label: "the host built but the app couldn't launch." })
|
|
4222
|
-
);
|
|
4223
|
-
console.error(
|
|
4224
|
-
footer(
|
|
4225
|
-
dim(
|
|
4226
|
-
"on macOS this is usually code signing / Gatekeeper for a locally built app:"
|
|
4227
|
-
)
|
|
4228
|
-
)
|
|
4229
|
-
);
|
|
4230
|
-
console.error(
|
|
4231
|
-
` ${dim("\u2022")} ${dim("install full Xcode, then run")} ${lime("vidra doctor")} ${dim("to verify")}`
|
|
4232
|
-
);
|
|
4233
|
-
console.error(
|
|
4234
|
-
` ${dim("\u2022")} ${dim("approve it once in Finder: right-click the")} ${value(".app")} ${dim("and choose")} ${value("Open")}`
|
|
4235
|
-
);
|
|
4236
|
-
console.error(
|
|
4237
|
-
` ${dim("\u2022")} ${dim("or provide a signing identity via")} ${value("VIDRA_MACOS_CODESIGN_KEY")}`
|
|
4238
|
-
);
|
|
4239
|
-
console.error();
|
|
4240
|
-
};
|
|
4241
|
-
|
|
4242
|
-
// src/cli.ts
|
|
4243
|
-
var printHelp = () => {
|
|
4244
|
-
const cmd = (name, desc) => ` ${value(name.padEnd(10))} ${dim(desc)}`;
|
|
4245
|
-
const ex = (args, comment) => ` ${lime("vidra")} ${value(args.padEnd(22))} ${dim(`# ${comment}`)}`;
|
|
4246
|
-
console.log(`
|
|
4247
|
-
${wordmark()} ${dim(`v${CLI_VERSION}`)}
|
|
4248
|
-
|
|
4249
|
-
${dim("usage")}
|
|
4250
|
-
${lime("vidra")} ${dim("<command> [options]")}
|
|
4251
|
-
|
|
4252
|
-
${dim("commands")}
|
|
4253
|
-
${cmd("dev", "start vite + the native host (UI + C# reload on save)")}
|
|
4254
|
-
${cmd("run", "launch the native host only")}
|
|
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")}
|
|
4259
|
-
${cmd("doctor", "check your environment")}
|
|
4260
|
-
${cmd("help", "show this message")}
|
|
4261
|
-
|
|
4262
|
-
${dim("examples")}
|
|
4263
|
-
${ex("dev --target windows", "run the windows host")}
|
|
4264
|
-
${ex("dev --no-hot-reload", "skip dotnet watch, classic launch")}
|
|
4265
|
-
${ex("build --plan", "preview the build, run nothing")}
|
|
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/")}
|
|
4273
|
-
${ex("doctor", "verify .NET SDK + MAUI workload")}
|
|
4274
|
-
`);
|
|
4275
|
-
};
|
|
4276
|
-
var main = async () => {
|
|
4277
|
-
const args = process.argv.slice(2);
|
|
4278
|
-
const command = args[0];
|
|
4279
|
-
switch (command) {
|
|
4280
|
-
case "dev":
|
|
4281
|
-
await devCommand(args.slice(1));
|
|
4282
|
-
break;
|
|
4283
|
-
case "run":
|
|
4284
|
-
await runCommand(args.slice(1));
|
|
4285
|
-
break;
|
|
4286
|
-
case "build":
|
|
4287
|
-
await buildCommand(args.slice(1));
|
|
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;
|
|
4298
|
-
case "doctor":
|
|
4299
|
-
process.exit(await runDoctor());
|
|
4300
|
-
break;
|
|
4301
|
-
case "help":
|
|
4302
|
-
case "--help":
|
|
4303
|
-
case "-h":
|
|
4304
|
-
case void 0:
|
|
4305
|
-
printHelp();
|
|
4306
|
-
break;
|
|
4307
|
-
case "--version":
|
|
4308
|
-
case "-v":
|
|
4309
|
-
console.log(CLI_VERSION);
|
|
4310
|
-
break;
|
|
4311
|
-
default:
|
|
4312
|
-
console.error(row({ glyph: "error", detail: dim(`unknown command: ${command}`) }));
|
|
4313
|
-
printHelp();
|
|
4314
|
-
process.exit(1);
|
|
4315
|
-
}
|
|
4316
|
-
};
|
|
4317
|
-
main().catch((e) => {
|
|
4318
|
-
console.error(row({ glyph: "error", detail: dim(e.message) }));
|
|
4319
|
-
process.exit(1);
|
|
4320
|
-
});
|