@mirinjs/cli 0.1.0-alpha.2 → 0.1.0-alpha.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +8 -5
  2. package/src/artifacts.ts +165 -26
  3. package/src/build.ts +236 -43
  4. package/src/bundle/linux/app.ts +275 -0
  5. package/src/bundle/linux/index.ts +1 -0
  6. package/src/{bundle.ts → bundle/macos/app.ts} +65 -23
  7. package/src/bundle/macos/index.ts +1 -0
  8. package/src/bundle/shared/cef-locales.ts +91 -0
  9. package/src/{bundle-win.ts → bundle/windows/app.ts} +10 -6
  10. package/src/bundle/windows/index.ts +1 -0
  11. package/src/check.ts +271 -0
  12. package/src/dev.ts +214 -35
  13. package/src/dmg.ts +22 -10
  14. package/src/doctor.ts +227 -0
  15. package/src/extras.ts +52 -13
  16. package/src/icons/macos/appearance.ts +96 -0
  17. package/src/icons/macos/index.ts +1 -0
  18. package/src/icons/sources.ts +25 -0
  19. package/src/icons/windows/icon.ts +255 -0
  20. package/src/icons/windows/index.ts +1 -0
  21. package/src/index.ts +109 -7
  22. package/src/installer-inno.ts +141 -0
  23. package/src/installer-win.ts +17 -11
  24. package/src/package/linux/index.ts +2 -0
  25. package/src/package/linux/lib/defaults.ts +53 -0
  26. package/src/package/linux/lib/tools.ts +45 -0
  27. package/src/package/linux/lib/validation.ts +71 -0
  28. package/src/package/linux/package.ts +293 -0
  29. package/src/package/linux/types.ts +30 -0
  30. package/src/release/installers.ts +148 -0
  31. package/src/release/previous.ts +105 -0
  32. package/src/release.ts +166 -116
  33. package/src/shared/driver.ts +240 -0
  34. package/src/shared/inspector.ts +64 -0
  35. package/src/shared/report.ts +69 -0
  36. package/src/shared/scenario.ts +58 -0
  37. package/src/shared/session.ts +153 -0
  38. package/src/skill.ts +51 -0
  39. package/src/icon-win.ts +0 -81
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirinjs/cli",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.22",
4
4
  "description": "CLI for mirin apps: dev, build, init.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,12 +27,15 @@
27
27
  "bun": ">=1.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "create-mirinjs": "0.1.0-alpha.2",
31
- "mirinjs": "0.1.0-alpha.2"
30
+ "create-mirinjs": "0.1.0-alpha.22",
31
+ "mirinjs": "0.1.0-alpha.22"
32
32
  },
33
33
  "optionalDependencies": {
34
- "@mirinjs/darwin-arm64": "0.1.0-alpha.2",
35
- "@mirinjs/win32-x64": "0.1.0-alpha.2"
34
+ "@mirinjs/darwin-arm64": "0.1.0-alpha.22",
35
+ "@mirinjs/win32-x64": "0.1.0-alpha.22",
36
+ "@mirinjs/win32-arm64": "0.1.0-alpha.22",
37
+ "@mirinjs/linux-arm64": "0.1.0-alpha.22",
38
+ "@mirinjs/linux-x64": "0.1.0-alpha.22"
36
39
  },
37
40
  "publishConfig": {
38
41
  "access": "public"
package/src/artifacts.ts CHANGED
@@ -9,13 +9,23 @@
9
9
  * entry from the `mirin` package, and a CEF framework downloaded once from
10
10
  * the matching GitHub Release into `~/.mirinjs/cef/<version>`. No Rust needed.
11
11
  *
12
- * Alpha supports macOS arm64 only.
12
+ * Alpha supports macOS arm64, Windows x64/arm64, and Linux x64/arm64.
13
13
  */
14
14
 
15
- import { $ } from "bun";
16
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
15
+ import {
16
+ existsSync,
17
+ mkdirSync,
18
+ mkdtempSync,
19
+ readdirSync,
20
+ readFileSync,
21
+ readlinkSync,
22
+ renameSync,
23
+ rmSync,
24
+ } from "node:fs";
17
25
  import { homedir, tmpdir } from "node:os";
18
- import { dirname, join, resolve } from "node:path";
26
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
27
+ import { $ } from "bun";
28
+ import { createReporter, type Reporter } from "./shared/report.ts";
19
29
 
20
30
  const REPO_SLUG = "Netko-Labs/mirin";
21
31
 
@@ -26,6 +36,8 @@ const IN_REPO = existsSync(join(REPO_ROOT, "crates", "mirin-core"));
26
36
  export interface Artifacts {
27
37
  /** libmirin_core.dylib — dlopened by the host. */
28
38
  coreDylib: string;
39
+ /** Standalone updater codec used by `mirin release`. */
40
+ codecBin: string;
29
41
  /** mirin-helper — the CEF subprocess binary. */
30
42
  helperBin: string;
31
43
  /** host entry (TS/JS) compiled with `bun build --compile`. */
@@ -38,18 +50,33 @@ export function isInRepo(): boolean {
38
50
  return IN_REPO;
39
51
  }
40
52
 
41
- export async function resolveArtifacts(opts: { release: boolean }): Promise<Artifacts> {
53
+ /** The in-repo CEF distribution. `present` checks the framework marker, not just
54
+ * the directory: an interrupted `fetch-cef.ts` leaves an empty one behind. */
55
+ export function vendoredCef(): { path: string; present: boolean } {
56
+ const path = join(REPO_ROOT, "vendor", "cef");
57
+ return { path, present: existsSync(join(path, cefMarker())) };
58
+ }
59
+
60
+ export async function resolveArtifacts(opts: {
61
+ release: boolean;
62
+ /** Where build chatter goes. Defaults to human mode, which prints it. */
63
+ reporter?: Reporter;
64
+ }): Promise<Artifacts> {
42
65
  assertSupportedPlatform();
43
- const cefPath = await ensureCef();
66
+ const reporter = opts.reporter ?? createReporter(false);
67
+ const cefPath = await ensureCef(reporter);
44
68
 
45
69
  if (IN_REPO) {
46
70
  const profile = opts.release ? "release" : "debug";
47
- console.log(`[mirin] building native core + helper (${profile})…`);
71
+ reporter.info(`[mirin] building native core + helpers (${profile})…`);
48
72
  const flags = opts.release ? ["--release"] : [];
49
- await $`cargo build -p mirin-core -p mirin-helper ${flags}`.cwd(REPO_ROOT);
73
+ await reporter.build(
74
+ $`cargo build -p mirin-codec -p mirin-core -p mirin-helper ${flags}`.cwd(REPO_ROOT),
75
+ );
50
76
  const target = join(REPO_ROOT, "target", profile);
51
77
  return {
52
78
  coreDylib: join(target, coreFileName()),
79
+ codecBin: join(target, codecFileName()),
53
80
  helperBin: join(target, helperFileName()),
54
81
  hostEntry: join(REPO_ROOT, "packages", "mirin", "src", "host.ts"),
55
82
  cefPath,
@@ -59,6 +86,7 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
59
86
  const nativeDir = resolveNativeDir();
60
87
  return {
61
88
  coreDylib: join(nativeDir, coreFileName()),
89
+ codecBin: join(nativeDir, codecFileName()),
62
90
  helperBin: join(nativeDir, helperFileName()),
63
91
  hostEntry: resolvePackageFile("mirinjs/host"),
64
92
  cefPath,
@@ -67,7 +95,9 @@ export async function resolveArtifacts(opts: { release: boolean }): Promise<Arti
67
95
 
68
96
  /** The native core library file name for the host platform (MSVC has no `lib` prefix). */
69
97
  function coreFileName(): string {
70
- return process.platform === "win32" ? "mirin_core.dll" : "libmirin_core.dylib";
98
+ if (process.platform === "win32") return "mirin_core.dll";
99
+ if (process.platform === "linux") return "libmirin_core.so";
100
+ return "libmirin_core.dylib";
71
101
  }
72
102
 
73
103
  /** The CEF subprocess binary name for the host platform. */
@@ -75,13 +105,19 @@ function helperFileName(): string {
75
105
  return process.platform === "win32" ? "mirin-helper.exe" : "mirin-helper";
76
106
  }
77
107
 
108
+ function codecFileName(): string {
109
+ return process.platform === "win32" ? "mirin-codec.exe" : "mirin-codec";
110
+ }
111
+
78
112
  function assertSupportedPlatform(): void {
79
113
  const supported =
80
114
  (process.platform === "darwin" && process.arch === "arm64") ||
81
- (process.platform === "win32" && process.arch === "x64");
115
+ (process.platform === "win32" && (process.arch === "x64" || process.arch === "arm64")) ||
116
+ (process.platform === "linux" && (process.arch === "x64" || process.arch === "arm64"));
82
117
  if (!supported) {
83
118
  throw new Error(
84
- `mirin alpha supports macOS arm64 and Windows x64 (got ${process.platform}/${process.arch}).`,
119
+ "mirin alpha supports macOS arm64, Windows x64/arm64, and Linux x64/arm64 " +
120
+ `(got ${process.platform}/${process.arch}).`,
85
121
  );
86
122
  }
87
123
  }
@@ -97,7 +133,7 @@ function resolveNativeDir(): string {
97
133
  } catch {
98
134
  throw new Error(
99
135
  `mirin: prebuilt native package "${pkg}" is not installed. Run \`bun install\` ` +
100
- `(it is an optional dependency of @mirinjs/cli for your platform).`,
136
+ "(it is an optional dependency of @mirinjs/cli for your platform).",
101
137
  );
102
138
  }
103
139
  }
@@ -113,41 +149,144 @@ function cliVersion(): string {
113
149
  }
114
150
 
115
151
  /** A file/dir that, if present in a CEF dir, means the distribution is unpacked.
116
- * macOS ships the framework bundle; Windows ships a flat dir with libcef.dll. */
152
+ * macOS ships the framework bundle; Windows ships a flat dir with libcef.dll;
153
+ * Linux ships a flat dir with libcef.so. */
117
154
  function cefMarker(): string {
118
- return process.platform === "win32" ? "libcef.dll" : "Chromium Embedded Framework.framework";
155
+ if (process.platform === "win32") return "libcef.dll";
156
+ if (process.platform === "linux") return "libcef.so";
157
+ return "Chromium Embedded Framework.framework";
119
158
  }
120
159
 
121
- async function ensureCef(): Promise<string> {
160
+ async function ensureCef(reporter: Reporter): Promise<string> {
122
161
  if (IN_REPO) {
123
- const vendor = join(REPO_ROOT, "vendor", "cef");
124
- if (existsSync(join(vendor, cefMarker()))) return vendor;
162
+ const vendor = vendoredCef();
163
+ if (vendor.present) return vendor.path;
125
164
  console.error(
126
165
  "[mirin] vendor/cef missing — run `bun scripts/fetch-cef.ts` in the monorepo first.",
127
166
  );
128
- return vendor;
167
+ return vendor.path;
129
168
  }
130
169
 
131
170
  const version = cliVersion();
132
- const cacheDir = join(homedir(), ".mirinjs", "cef", `${version}-${platformTag()}`);
171
+ const cacheRoot = join(homedir(), ".mirinjs", "cef");
172
+ const cacheDir = join(cacheRoot, `${version}-${platformTag()}`);
133
173
  if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
134
174
 
135
- mkdirSync(cacheDir, { recursive: true });
175
+ mkdirSync(cacheRoot, { recursive: true });
136
176
  const asset = `cef-${platformTag()}.tar.gz`;
137
177
  const url = `https://github.com/${REPO_SLUG}/releases/download/v${version}/${asset}`;
138
- console.log(`[mirin] downloading CEF for ${platformTag()} (one-time, ~hundreds of MB)…`);
178
+ reporter.info(`[mirin] downloading CEF for ${platformTag()} (one-time, ~hundreds of MB)…`);
139
179
 
140
180
  // Download with curl, not `Bun.write(file, await fetch(url))`: the latter
141
181
  // pins a core at 100% CPU and never completes on large gzip responses
142
182
  // (the streaming write path). curl is fast and ubiquitous.
143
- const archive = join(tmpdir(), asset);
183
+ const downloadDir = mkdtempSync(join(tmpdir(), "mirin-cef-download-"));
184
+ const stagingDir = mkdtempSync(join(cacheRoot, ".staging-"));
185
+ const archive = join(downloadDir, asset);
186
+ const checksum = `${archive}.sha256`;
187
+ const checksumUrl = `${url}.sha256`;
188
+ const httpsOnly = "=https";
144
189
  try {
145
- await $`curl -fSL --retry 3 -o ${archive} ${url}`;
146
- await $`tar -xzf ${archive} -C ${cacheDir}`;
190
+ await $`curl --fail --show-error --location --retry 3 --proto ${httpsOnly} --proto-redir ${httpsOnly} -o ${checksum} ${checksumUrl}`;
191
+ await $`curl --fail --show-error --location --retry 3 --proto ${httpsOnly} --proto-redir ${httpsOnly} -o ${archive} ${url}`;
192
+ await verifyFileSha256(archive, readFileSync(checksum, "utf8"));
193
+ const listing = await $`tar -tzf ${archive}`.quiet().text();
194
+ validateArchiveEntries(listing);
195
+ await $`tar -xzf ${archive} -C ${stagingDir}`;
196
+ validateExtractedSymlinks(stagingDir);
197
+ if (!existsSync(join(stagingDir, cefMarker()))) {
198
+ throw new Error(`CEF archive is missing ${cefMarker()}`);
199
+ }
200
+
201
+ // Another process may have populated the cache while this one downloaded.
202
+ if (existsSync(join(cacheDir, cefMarker()))) return cacheDir;
203
+ rmSync(cacheDir, { recursive: true, force: true });
204
+ renameSync(stagingDir, cacheDir);
147
205
  } catch (err) {
148
- rmSync(archive, { force: true });
149
206
  throw new Error(`mirin: failed to download CEF from ${url} (${err}).`);
207
+ } finally {
208
+ rmSync(downloadDir, { recursive: true, force: true });
209
+ rmSync(stagingDir, { recursive: true, force: true });
150
210
  }
151
- rmSync(archive, { force: true });
152
211
  return cacheDir;
153
212
  }
213
+
214
+ /** Reject absolute paths and parent traversal before a downloaded tar is extracted. */
215
+ export function validateArchiveEntries(listing: string): void {
216
+ for (const rawEntry of listing.split(/\r?\n/)) {
217
+ if (rawEntry.length === 0) continue;
218
+ const entry = archivePath(rawEntry);
219
+ if (entry == null) throw new Error(`unsafe CEF archive entry: ${rawEntry}`);
220
+ }
221
+ }
222
+
223
+ /** Verify a downloaded release asset before parsing or extracting it. */
224
+ export async function verifyFileSha256(path: string, checksum: string): Promise<void> {
225
+ const expected = checksum.trim().toLowerCase();
226
+ if (!/^[0-9a-f]{64}$/.test(expected)) {
227
+ throw new Error("invalid CEF SHA-256 checksum");
228
+ }
229
+
230
+ const hasher = new Bun.CryptoHasher("sha256");
231
+ for await (const chunk of Bun.file(path).stream()) {
232
+ hasher.update(chunk);
233
+ }
234
+ const actual = hasher.digest("hex");
235
+ if (actual !== expected) {
236
+ throw new Error(`CEF SHA-256 mismatch (expected ${expected}, got ${actual})`);
237
+ }
238
+ }
239
+
240
+ /** Reject extracted symlinks whose lexical target escapes the staging directory. */
241
+ export function validateExtractedSymlinks(root: string): void {
242
+ const absoluteRoot = resolve(root);
243
+ const pending = [absoluteRoot];
244
+
245
+ while (pending.length > 0) {
246
+ const directory = pending.pop();
247
+ if (!directory) continue;
248
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
249
+ const path = join(directory, entry.name);
250
+ if (entry.isSymbolicLink()) {
251
+ const target = readlinkSync(path);
252
+ const resolvedTarget = resolve(dirname(path), target);
253
+ const fromRoot = relative(absoluteRoot, resolvedTarget);
254
+ if (
255
+ isAbsolute(target) ||
256
+ fromRoot === ".." ||
257
+ fromRoot.startsWith(`..${sep}`) ||
258
+ isAbsolute(fromRoot)
259
+ ) {
260
+ throw new Error(`CEF archive symlink escapes its cache: ${path} -> ${target}`);
261
+ }
262
+ } else if (entry.isDirectory()) {
263
+ pending.push(path);
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ function archivePath(raw: string): string | null {
270
+ let path = raw.replaceAll("\\", "/");
271
+ while (path.startsWith("./")) path = path.slice(2);
272
+ if (path.length === 0 || path === ".") return "";
273
+ if (path.startsWith("/") || /^[A-Za-z]:/.test(path) || hasControlCharacter(path)) {
274
+ return null;
275
+ }
276
+
277
+ const parts: string[] = [];
278
+ for (const part of path.split("/")) {
279
+ if (part.length === 0 || part === ".") continue;
280
+ if (part === "..") return null;
281
+ parts.push(part);
282
+ }
283
+ return parts.length > 0 ? parts.join("/") : "";
284
+ }
285
+
286
+ function hasControlCharacter(value: string): boolean {
287
+ for (let index = 0; index < value.length; index++) {
288
+ const code = value.charCodeAt(index);
289
+ if (code <= 0x1f || code === 0x7f) return true;
290
+ }
291
+ return false;
292
+ }