craft-native 0.0.87 → 0.0.88

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 CHANGED
@@ -314,6 +314,77 @@ import { orbstackStyles, renderOrbStackSidebar, orbstackDemoData } from 'craft-n
314
314
  const html = renderOrbStackSidebar(orbstackDemoData)
315
315
  ```
316
316
 
317
+ ## Auto-Updating
318
+
319
+ `AutoUpdater` fetches a manifest, verifies the download, replaces the app bundle,
320
+ and relaunches.
321
+
322
+ ```ts
323
+ import { AutoUpdater } from 'craft-native'
324
+
325
+ const updater = new AutoUpdater({
326
+ updateUrl: 'https://github.com/you/app/releases/latest/download/update.json',
327
+ currentVersion: '1.0.0',
328
+ appPath: '/Applications/MyApp.app',
329
+ autoDownload: false,
330
+ macos: { teamId: 'ABCDE12345' },
331
+ })
332
+
333
+ updater.on('update-available', info => console.log('available:', info.version))
334
+ updater.on('download-progress', p => console.log(`${p.percent}%`))
335
+
336
+ if (await updater.checkForUpdates()) {
337
+ await updater.downloadUpdate()
338
+ await updater.installUpdate()
339
+ }
340
+ ```
341
+
342
+ ### macOS trust
343
+
344
+ The manifest's SHA-256 proves the download matches what the manifest asked for. It
345
+ says nothing about *who published it* — whoever can rewrite the manifest can rewrite
346
+ the hash with it. So on macOS the staged bundle is also put through the checks
347
+ Gatekeeper runs at launch, before anything is swapped:
348
+
349
+ - `codesign --verify --deep --strict` — the signature covers the bytes on disk
350
+ - `spctl -a -t exec` — Apple has notarized this build
351
+ - `TeamIdentifier` equals `macos.teamId` — *you* published it
352
+
353
+ Pin `teamId`. Without it an updater accepts any notarized bundle, and notarization is
354
+ available to every Apple developer account there is. `@stacksjs/desktop`'s
355
+ `createSelfUpdater` fills it in from the signature on the running copy, which is the
356
+ version most apps want.
357
+
358
+ ### Replacing the bundle
359
+
360
+ The swap is deliberately not `rm -rf app && cp -R new app`:
361
+
362
+ - **`ditto`, not `cp -R`.** `cp` and `fs.cpSync` drop the extended attributes and ACLs
363
+ a code signature is computed over, so a byte-identical copy fails `codesign --verify`.
364
+ - **Two renames, not a delete.** The new bundle lands on the destination volume first,
365
+ then the old one is renamed aside and the new one renamed in. Both are atomic within
366
+ a directory, so there is no window in which an interrupted update leaves the user
367
+ with no app.
368
+ - **Quarantine last.** The flag is cleared only after verification passes — clearing it
369
+ first is how an updater becomes a way to install anything at all.
370
+
371
+ These are exported on their own for installers and CI: `verifyBundleTrust`,
372
+ `readBundleIdentity`, `extractBundle`, `extractBundleFromDmg`, `extractBundleFromZip`,
373
+ `swapBundle`, `dittoBundle`, `clearQuarantine`, `canReplaceBundle`.
374
+
375
+ ### Relaunching
376
+
377
+ `restartApp` opens the bundle and exits. Pass `relaunch` when the updater does not run
378
+ in the process the user launched — an agent behind a webview that calls
379
+ `process.exit(0)` kills a child, leaves the window on a dead server, and relaunches
380
+ nothing.
381
+
382
+ ### Deltas
383
+
384
+ Delta updates are used only when `bspatch` or `xdelta3` is on the machine. Neither
385
+ ships with macOS or a default Linux install, so a manifest that offers a delta falls
386
+ back to the full bundle rather than failing.
387
+
317
388
  ## Examples
318
389
 
319
390
  See the [examples directory](../examples-ts) for more:
package/dist/cli.js CHANGED
@@ -1053,9 +1053,11 @@ __export(exports_src, {
1053
1053
  syncWebAssets: () => syncWebAssets,
1054
1054
  showSimulator: () => showSimulator,
1055
1055
  run: () => run,
1056
+ resolveRuntimeDir: () => resolveRuntimeDir,
1056
1057
  renderWatchEntitlements: () => renderWatchEntitlements,
1057
1058
  renderUsageDescriptions: () => renderUsageDescriptions,
1058
1059
  renderUrlTypes: () => renderUrlTypes,
1060
+ renderRuntimeSettings: () => renderRuntimeSettings,
1059
1061
  renderPrivacyManifest: () => renderPrivacyManifest,
1060
1062
  renderOrientations: () => renderOrientations,
1061
1063
  renderEntitlements: () => renderEntitlements,
@@ -1064,6 +1066,7 @@ __export(exports_src, {
1064
1066
  pickSimulator: () => pickSimulator,
1065
1067
  orderSimulators: () => orderSimulators,
1066
1068
  open: () => open,
1069
+ installRuntime: () => installRuntime,
1067
1070
  init: () => init,
1068
1071
  build: () => build,
1069
1072
  bootSimulator: () => bootSimulator
@@ -1193,6 +1196,14 @@ ${appGroups}</dict>
1193
1196
  </plist>
1194
1197
  `;
1195
1198
  }
1199
+ function renderRuntimeSettings() {
1200
+ return [
1201
+ ' LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: "$(PROJECT_DIR)/Runtime/device"',
1202
+ ' LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: "$(PROJECT_DIR)/Runtime/simulator"',
1203
+ ' OTHER_LDFLAGS: "-lcraft-ios -Wl,-u,_craft_ios_handle_action -Wl,-u,_craft_ios_set_webview ' + '-Wl,-u,_craft_ios_deliver_result -Wl,-u,_craft_ios_deliver_error"'
1204
+ ].join(`
1205
+ `);
1206
+ }
1196
1207
  function renderPrivacyManifest(config) {
1197
1208
  const privacy = config.privacy ?? {};
1198
1209
  const collected = privacy.collectedDataTypes ?? [];
@@ -1296,6 +1307,55 @@ function syncWebAssets(source, output) {
1296
1307
  throw new Error(`Web asset directory must contain index.html: ${source}`);
1297
1308
  }
1298
1309
  }
1310
+ function resolveRuntimeDir(override) {
1311
+ if (override === null)
1312
+ return null;
1313
+ const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
1314
+ if (!dir)
1315
+ return null;
1316
+ if (!existsSync4(dir)) {
1317
+ const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
1318
+ throw new Error(`${source} ${dir}, which does not exist.`);
1319
+ }
1320
+ return dir;
1321
+ }
1322
+ async function installRuntime(output, runtimeDir) {
1323
+ const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
1324
+ const present = archives.filter((a) => existsSync4(join2(runtimeDir, a)));
1325
+ if (present.length === 0) {
1326
+ throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
1327
+ }
1328
+ return { sdk, archives, present };
1329
+ });
1330
+ const dest = join2(output, "Runtime");
1331
+ rmSync2(dest, { recursive: true, force: true });
1332
+ for (const { sdk, archives, present } of resolved) {
1333
+ const sdkDir = join2(dest, sdk);
1334
+ mkdirSync2(sdkDir, { recursive: true });
1335
+ const target = join2(sdkDir, "libcraft-ios.a");
1336
+ if (present.length === 1) {
1337
+ const missing = archives.filter((a) => !present.includes(a));
1338
+ if (missing.length > 0) {
1339
+ console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
1340
+ }
1341
+ cpSync2(join2(runtimeDir, present[0]), target);
1342
+ } else {
1343
+ await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
1344
+ }
1345
+ }
1346
+ return true;
1347
+ }
1348
+ async function refreshRuntime(output, override) {
1349
+ if (!existsSync4(join2(output, "Runtime")))
1350
+ return;
1351
+ const dir = resolveRuntimeDir(override);
1352
+ if (!dir) {
1353
+ console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
1354
+ return;
1355
+ }
1356
+ await installRuntime(output, dir);
1357
+ console.log(" Refreshed the Zig runtime from", dir);
1358
+ }
1299
1359
  async function init(options) {
1300
1360
  const { name, bundleId, teamId, output } = options;
1301
1361
  console.log(`
@@ -1368,7 +1428,14 @@ async function init(options) {
1368
1428
  SWIFT_VERSION: "5.0"
1369
1429
  SKIP_INSTALL: YES`);
1370
1430
  }
1371
- const projectYml = projectYmlTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
1431
+ const runtimeDir = resolveRuntimeDir(options.runtimeDir);
1432
+ const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
1433
+ if (hasRuntime) {
1434
+ console.log(" Linked the Zig runtime from", runtimeDir);
1435
+ } else {
1436
+ rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
1437
+ }
1438
+ const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
1372
1439
  ${nativeDependencies.join(`
1373
1440
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1374
1441
  `));
@@ -1459,6 +1526,7 @@ async function build(options) {
1459
1526
  syncWebAssets(htmlPath, output);
1460
1527
  console.log(` Synced: ${htmlPath} \u2192 dist/`);
1461
1528
  }
1529
+ await refreshRuntime(output, options.runtimeDir);
1462
1530
  if (!generateProject)
1463
1531
  return;
1464
1532
  try {
@@ -1580,7 +1648,7 @@ async function run(options) {
1580
1648
  console.log(" 4. Click Run (\u25B6\uFE0F)");
1581
1649
  }
1582
1650
  }
1583
- var $, TEMPLATES_DIR, DEFAULT_CONFIG;
1651
+ var $, TEMPLATES_DIR, DEFAULT_CONFIG, RUNTIME_ARCHIVES;
1584
1652
  var init_src = __esm(() => {
1585
1653
  ({ $ } = globalThis.Bun);
1586
1654
  TEMPLATES_DIR = join2(dirname(import.meta.dir), "templates");
@@ -1633,6 +1701,10 @@ var init_src = __esm(() => {
1633
1701
  orientations: ["portrait"],
1634
1702
  deviceFamilies: ["iphone", "ipad"]
1635
1703
  };
1704
+ RUNTIME_ARCHIVES = {
1705
+ device: ["libcraft-ios.a"],
1706
+ simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
1707
+ };
1636
1708
  });
1637
1709
 
1638
1710
  // dist/android/src/index.js
@@ -4191,7 +4263,7 @@ function craftBinaryNotFoundMessage(triedPath) {
4191
4263
  `);
4192
4264
  }
4193
4265
  // package.json
4194
- var version = "0.0.87";
4266
+ var version = "0.0.88";
4195
4267
 
4196
4268
  // bin/cli.ts
4197
4269
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];