craft-native 0.0.92 → 0.0.94

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.
@@ -1,10 +1,14 @@
1
1
  package {{PACKAGE_NAME}}
2
2
 
3
3
  import android.annotation.SuppressLint
4
+ import android.content.Context
4
5
  import android.os.Bundle
5
6
  import android.content.Intent
6
7
  import android.graphics.Bitmap
8
+ import android.net.ConnectivityManager
9
+ import android.net.Network
7
10
  import android.net.Uri
11
+ import android.util.Log
8
12
  import android.webkit.WebResourceRequest
9
13
  import android.webkit.WebResourceError
10
14
  import android.webkit.WebResourceResponse
@@ -19,6 +23,20 @@ import org.json.JSONObject
19
23
  import java.io.FileNotFoundException
20
24
  import java.net.URLConnection
21
25
 
26
+ /** Main-frame failures for which the bundled app is an honest fallback (#254). */
27
+ object CraftLoadFailure {
28
+ fun isUnreachable(errorCode: Int): Boolean = when (errorCode) {
29
+ WebViewClient.ERROR_HOST_LOOKUP,
30
+ WebViewClient.ERROR_CONNECT,
31
+ WebViewClient.ERROR_TIMEOUT,
32
+ // Android uses ERROR_IO for connection interruptions as well as local
33
+ // I/O failures. Keep it here deliberately: it is the closest Android
34
+ // equivalent to iOS's NSURLErrorNetworkConnectionLost.
35
+ WebViewClient.ERROR_IO -> true
36
+ else -> false
37
+ }
38
+ }
39
+
22
40
  class MainActivity : AppCompatActivity() {
23
41
  private lateinit var webView: WebView
24
42
  private lateinit var craftBridge: CraftBridge
@@ -32,6 +50,28 @@ class MainActivity : AppCompatActivity() {
32
50
  .build()
33
51
  }
34
52
  private var loadedBundledFallback = false
53
+ private lateinit var connectivityManager: ConnectivityManager
54
+ @Volatile
55
+ private var isConnected = false
56
+ private var networkMonitoringRegistered = false
57
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
58
+ override fun onAvailable(network: Network) {
59
+ val wasConnected = isConnected
60
+ isConnected = true
61
+ if (!wasConnected && isConnected) {
62
+ runOnUiThread {
63
+ returnFromBundledFallback("the network came back")
64
+ }
65
+ }
66
+ }
67
+
68
+ override fun onLost(network: Network) {
69
+ // A second transport can already be the default by the time the
70
+ // old one reports its loss, so read current state instead of
71
+ // blindly turning the latch off.
72
+ isConnected = connectivityManager.activeNetwork != null
73
+ }
74
+ }
35
75
 
36
76
  @SuppressLint("SetJavaScriptEnabled")
37
77
  override fun onCreate(savedInstanceState: Bundle?) {
@@ -46,21 +86,29 @@ class MainActivity : AppCompatActivity() {
46
86
  craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
47
87
  // The link that launched the app (#215). Queued until the page's bridge
48
88
  // is injected, then dispatched once with `initial: true`, so a page that
49
- // subscribes rather than calling getInitialURL still hears it. A
50
- // relaunch from Recents carries the old intent, and was not opened by it.
51
- // A recreated activity restores what the bridge saved: its intent may
52
- // be a later warm link that onNewIntent set.
89
+ // subscribes rather than calling getInitialURL still hears it.
90
+ //
91
+ // A relaunch from Recents carries the intent the app was first opened
92
+ // with. The person picked the task out of a list, not the link or the
93
+ // shortcut inside it, and neither is news the second time — so both
94
+ // are skipped, not just the link (#229).
95
+ //
96
+ // A recreated activity restores what the bridge saved rather than
97
+ // reading its intent, which onNewIntent may have replaced with a
98
+ // later warm link.
99
+ val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
53
100
  if (savedInstanceState == null) {
54
- val launchLink = intent.data?.toString()
55
- val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
56
- if (launchLink != null && !launchedFromHistory) craftBridge.receiveDeepLink(launchLink)
101
+ if (!launchedFromHistory) {
102
+ intent.data?.toString()?.let(craftBridge::receiveDeepLink)
103
+ handleIncomingShortcut(intent)
104
+ }
57
105
  } else {
58
106
  craftBridge.restoreDeepLinks(savedInstanceState)
59
107
  }
60
- if (savedInstanceState == null) handleIncomingShortcut(intent)
61
108
 
62
109
  setupWebView()
63
110
  loadContent()
111
+ setupNetworkMonitoring()
64
112
  }
65
113
 
66
114
  @SuppressLint("SetJavaScriptEnabled")
@@ -106,7 +154,13 @@ class MainActivity : AppCompatActivity() {
106
154
 
107
155
  override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
108
156
  super.onReceivedError(view, request, error)
109
- if (request?.isForMainFrame == true && hasBundledFallback && !loadedBundledFallback) {
157
+ if (
158
+ request?.isForMainFrame == true &&
159
+ error != null &&
160
+ CraftLoadFailure.isUnreachable(error.errorCode) &&
161
+ hasBundledFallback &&
162
+ !loadedBundledFallback
163
+ ) {
110
164
  loadedBundledFallback = true
111
165
  webView.loadUrl(BUNDLED_APP_URL)
112
166
  }
@@ -119,6 +173,23 @@ class MainActivity : AppCompatActivity() {
119
173
  webView.addJavascriptInterface(craftBridge, "CraftAndroid")
120
174
  }
121
175
 
176
+ private fun setupNetworkMonitoring() {
177
+ if (!hasBundledFallback || devServerUrl == null) return
178
+ connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
179
+ isConnected = connectivityManager.activeNetwork != null
180
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
181
+ networkMonitoringRegistered = true
182
+ }
183
+
184
+ /** Leave the bundled copy for the configured remote origin again. */
185
+ private fun returnFromBundledFallback(reason: String) {
186
+ val remoteUrl = devServerUrl ?: return
187
+ if (!loadedBundledFallback) return
188
+ loadedBundledFallback = false
189
+ Log.i(TAG, "$reason; loading $remoteUrl again instead of the bundled copy")
190
+ webView.loadUrl(remoteUrl)
191
+ }
192
+
122
193
  private fun loadContent() {
123
194
  // Check for dev server URL in config
124
195
  val remoteUrl = devServerUrl
@@ -227,6 +298,7 @@ class MainActivity : AppCompatActivity() {
227
298
  override fun onResume() {
228
299
  super.onResume()
229
300
  webView.onResume()
301
+ returnFromBundledFallback("the app returned to the foreground")
230
302
  }
231
303
 
232
304
  override fun onPause() {
@@ -235,6 +307,10 @@ class MainActivity : AppCompatActivity() {
235
307
  }
236
308
 
237
309
  override fun onDestroy() {
310
+ if (networkMonitoringRegistered) {
311
+ connectivityManager.unregisterNetworkCallback(networkCallback)
312
+ networkMonitoringRegistered = false
313
+ }
238
314
  craftBridge.close()
239
315
  webView.destroy()
240
316
  super.onDestroy()
@@ -256,6 +332,7 @@ class MainActivity : AppCompatActivity() {
256
332
  }
257
333
 
258
334
  companion object {
335
+ private const val TAG = "CraftMainActivity"
259
336
  private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
260
337
  private const val BUNDLED_APP_ORIGIN = "https://appassets.androidplatform.net"
261
338
  private const val BUNDLED_APP_URL = "$BUNDLED_APP_ORIGIN/"
@@ -617,6 +617,22 @@ export interface NotificationOptions {
617
617
  /** Schedule for future (timestamp) */
618
618
  scheduleAt?: number;
619
619
  }
620
+ /** A notification as the bridges read it: `delay` in place of `scheduleAt`. */
621
+ export type BridgeNotification = Omit<NotificationOptions, 'scheduleAt'> & {
622
+ delay?: number;
623
+ };
624
+ /**
625
+ * The bridges' spelling of a notification: `scheduleAt` becomes `delay`.
626
+ *
627
+ * Both bridges schedule from `delay`, milliseconds from now, and iOS also from
628
+ * `timestamp`; neither reads `scheduleAt`, so a reminder meant for an hour
629
+ * from now fired at once (#261). `delay` rather than `timestamp` because
630
+ * Android reads only `delay`, and because an interval is what an instant
631
+ * means, where iOS's calendar trigger would move with the device's time zone.
632
+ * A moment already past sends no delay at all, which both bridges deliver
633
+ * now: a zero delay raises inside UserNotifications on the Swift side.
634
+ */
635
+ export declare function bridgeNotification(options: NotificationOptions, now?: number): BridgeNotification;
620
636
  /**
621
637
  * Local notifications API.
622
638
  *
@@ -693,7 +709,23 @@ export declare const appReview: {
693
709
  export declare const pushNotifications: {
694
710
  register(): Promise<string>;
695
711
  onToken(callback: (token: string) => void): () => void;
712
+ /**
713
+ * A tap on a notification, with the payload it was sent with.
714
+ *
715
+ * Goes through the bridge's replay where there is one, so a tap that
716
+ * launched the app still reaches a page that subscribes once it has
717
+ * hydrated — rather than only one listening before the bridge was ready.
718
+ */
696
719
  onNotification(callback: (data: Record<string, unknown>) => void): () => void;
720
+ /**
721
+ * A notification that arrived while the app was open, with the payload it
722
+ * was sent with — the same shape `onNotification` hands a tap.
723
+ *
724
+ * The banner still shows. This is for a page that wants to react while it
725
+ * is on screen, such as refreshing what the push was about. Live only: an
726
+ * arrival before the page subscribed is not held for it. iOS only for now.
727
+ */
728
+ onReceive(callback: (data: Record<string, unknown>) => void): () => void;
697
729
  };
698
730
  export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts';
699
731
  export interface HealthDataOptions {
@@ -46,6 +46,10 @@ export declare function resolveCraftBinary(explicit?: string): string;
46
46
  * path-matrix behaviour the pantry contract exists to remove. So instead the
47
47
  * spawn is marked, and a CLI that starts up already marked knows it *is* the
48
48
  * thing that was spawned, and says so.
49
+ *
50
+ * Both spawners set it: `runCraftBinary()` in the CLI, and `CraftApp.show()`
51
+ * in the SDK. The SDK one was missing until #236, so every SDK caller on such
52
+ * a machine still met the raw parser error this marker exists to replace.
49
53
  */
50
54
  export declare const CRAFT_CLI_SPAWN_MARKER = "CRAFT_CLI_SPAWNED_FROM";
51
55
  /**
package/dist/cli.js CHANGED
@@ -1736,7 +1736,8 @@ __export(exports_src2, {
1736
1736
  open: () => open2,
1737
1737
  installRuntime: () => installRuntime2,
1738
1738
  init: () => init2,
1739
- build: () => build2
1739
+ build: () => build2,
1740
+ ANDROID_MIN_SDK: () => ANDROID_MIN_SDK
1740
1741
  });
1741
1742
  import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1742
1743
  import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
@@ -1840,6 +1841,9 @@ function validateAndroidConfig(config) {
1840
1841
  throw new Error(`Android ${name} must be a positive integer`);
1841
1842
  }
1842
1843
  }
1844
+ if (Number(config.minSdk) < ANDROID_MIN_SDK) {
1845
+ throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`);
1846
+ }
1843
1847
  if (Number(config.minSdk) > Number(config.targetSdk)) {
1844
1848
  throw new Error("Android minSdk must not exceed targetSdk");
1845
1849
  }
@@ -1977,7 +1981,6 @@ async function init2(options) {
1977
1981
  if (config.enableBackgroundLocation)
1978
1982
  config.enableGeolocation = true;
1979
1983
  if (config.enableHealthConnect) {
1980
- config.minSdk = Math.max(config.minSdk ?? 26, 26);
1981
1984
  config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
1982
1985
  }
1983
1986
  normalizeAndroidNetworkConfig(config);
@@ -2061,7 +2064,7 @@ async function init2(options) {
2061
2064
  const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
2062
2065
  writeFileSync4(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
2063
2066
  const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
2064
- const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk || 24)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
2067
+ const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
2065
2068
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
2066
2069
  writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
2067
2070
  const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
@@ -2359,7 +2362,7 @@ window.__craftRejectPendingPromises = function(message) {
2359
2362
  if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
2360
2363
  });
2361
2364
  };
2362
- `, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, DEFAULT_CONFIG2, RUNTIME_ABIS;
2365
+ `, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, ANDROID_MIN_SDK = 26, DEFAULT_CONFIG2, RUNTIME_ABIS;
2363
2366
  var init_src2 = __esm(() => {
2364
2367
  ({ $: $2 } = globalThis.Bun);
2365
2368
  TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
@@ -2413,7 +2416,7 @@ var init_src2 = __esm(() => {
2413
2416
  enableDeepLinks: false,
2414
2417
  urlSchemes: [],
2415
2418
  trustedOrigins: [],
2416
- minSdk: 26,
2419
+ minSdk: ANDROID_MIN_SDK,
2417
2420
  compileSdk: 36,
2418
2421
  targetSdk: 35
2419
2422
  };
@@ -4530,7 +4533,7 @@ function craftBinaryIsCliShimMessage(spawnedFrom) {
4530
4533
  return [
4531
4534
  `"${spawnedFrom}" on PATH is the Craft CLI, not the native binary.`,
4532
4535
  "",
4533
- "The CLI spawned it expecting the native build, and re-entered itself \u2014",
4536
+ "Craft spawned it expecting the native build and reached this CLI instead \u2014",
4534
4537
  "which is why an option you never typed (--url) came back as unknown.",
4535
4538
  "",
4536
4539
  "Fix it in one of two ways:",
@@ -4584,7 +4587,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
4584
4587
  `);
4585
4588
  }
4586
4589
  // package.json
4587
- var version = "0.0.92";
4590
+ var version = "0.0.94";
4588
4591
 
4589
4592
  // bin/cli.ts
4590
4593
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -5072,10 +5075,16 @@ cli.command("publish", "Publish app to App Store or Play Store").option("--ios",
5072
5075
  }
5073
5076
  });
5074
5077
  cli.command("init <name>", "Initialize a new Craft project").option("--template <type>", "Project template (blank, tabs, drawer, dashboard, desktop, ios, android, all)", { default: "blank" }).option("--bundle-id <id>", "Bundle identifier for mobile").example("craft init MyApp").example("craft init MyApp --template tabs").example("craft init MyApp --template dashboard").example("craft init MyApp --template all").action(async (name, options) => {
5078
+ const template = options?.template || "blank";
5079
+ const templates = ["blank", "tabs", "drawer", "dashboard", "desktop", "ios", "android", "all"];
5080
+ if (!templates.includes(template)) {
5081
+ console.error(`Error: Unknown template "${template}"`);
5082
+ console.error(`Available templates: ${templates.join(", ")}`);
5083
+ process6.exit(1);
5084
+ }
5075
5085
  console.log(`
5076
5086
  \u26A1 Creating new Craft project: ${name}
5077
5087
  `);
5078
- const template = options?.template || "blank";
5079
5088
  const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
5080
5089
  const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
5081
5090
  const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync5, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync5, cpSync: cpSync4 } = await import("fs");
package/dist/index.cjs CHANGED
@@ -218,6 +218,7 @@ __export(exports_src, {
218
218
  createArcSidebar: () => createArcSidebar,
219
219
  createApp: () => createApp,
220
220
  craftUnavailableMessage: () => craftUnavailableMessage,
221
+ craftProcessFailedMessage: () => craftProcessFailedMessage,
221
222
  craftBinaryNotFoundMessage: () => craftBinaryNotFoundMessage,
222
223
  copy: () => copy,
223
224
  components: () => exports_components,
@@ -291,6 +292,7 @@ function resolveCraftBinary(explicit) {
291
292
  }
292
293
  return "craft";
293
294
  }
295
+ var CRAFT_CLI_SPAWN_MARKER = "CRAFT_CLI_SPAWNED_FROM";
294
296
  function craftBinaryNotFoundMessage(triedPath) {
295
297
  return [
296
298
  `Craft native binary not found (tried "${triedPath}").`,
@@ -307,6 +309,11 @@ function craftBinaryNotFoundMessage(triedPath) {
307
309
  ].join(`
308
310
  `);
309
311
  }
312
+ // package.json
313
+ var version = "0.0.94";
314
+
315
+ // src/sdk-version.ts
316
+ var SDK_VERSION = version;
310
317
 
311
318
  // src/package.ts
312
319
  var import_child_process = require("child_process");
@@ -422,13 +429,13 @@ async function packageApp(config) {
422
429
  }
423
430
  async function packageMacOS(config, outDir) {
424
431
  const results = [];
425
- const { name, version, bundleId = `com.myapp.${name.toLowerCase()}` } = config;
432
+ const { name, version: version2, bundleId = `com.myapp.${name.toLowerCase()}` } = config;
426
433
  const opts = config.macos || {};
427
434
  const hardenedRuntime = !opts.appStore;
428
435
  const appBundlePath = import_path.join(outDir, `${name}.app`);
429
436
  const appBundle = createMacOSAppBundle({
430
437
  name,
431
- version,
438
+ version: version2,
432
439
  bundleId,
433
440
  buildNumber: opts.buildNumber,
434
441
  category: opts.category,
@@ -472,7 +479,7 @@ async function packageMacOS(config, outDir) {
472
479
  }
473
480
  results.push({ success: true, platform: "macos", format: "app", outputPath: appBundlePath });
474
481
  if (opts.dmg !== false) {
475
- const outputPath = import_path.join(outDir, `${name}-${version}.dmg`);
482
+ const outputPath = import_path.join(outDir, `${name}-${version2}.dmg`);
476
483
  const dmgResult = await createDMG({ appBundlePath, outputPath, volumeName: name });
477
484
  results.push({
478
485
  success: dmgResult.success,
@@ -485,12 +492,12 @@ async function packageMacOS(config, outDir) {
485
492
  results.push(...await notarizeIfRequested(opts, outputPath, "dmg"));
486
493
  }
487
494
  if (opts.pkg || opts.appStore) {
488
- const outputPath = import_path.join(outDir, `${name}-${version}.pkg`);
495
+ const outputPath = import_path.join(outDir, `${name}-${version2}.pkg`);
489
496
  const pkgResult = await createPKG({
490
497
  appBundlePath,
491
498
  outputPath,
492
499
  identifier: bundleId,
493
- version,
500
+ version: version2,
494
501
  appStore: opts.appStore === true,
495
502
  installerIdentity: opts.installerIdentity
496
503
  });
@@ -536,14 +543,14 @@ async function notarizeIfRequested(opts, artifactPath, format) {
536
543
  }
537
544
  async function packageWindows(config, outDir) {
538
545
  const results = [];
539
- const { name, version } = config;
546
+ const { name, version: version2 } = config;
540
547
  const opts = config.windows || {};
541
548
  if (opts.msi !== false) {
542
549
  const msiResult = await createMSI({
543
550
  name,
544
- version,
551
+ version: version2,
545
552
  binaryPath: config.binaryPath,
546
- outputPath: import_path.join(outDir, `${name}-${version}.msi`),
553
+ outputPath: import_path.join(outDir, `${name}-${version2}.msi`),
547
554
  manufacturer: config.author || "Unknown",
548
555
  architecture: opts.architecture || windowsArchitecture(process.arch),
549
556
  certificatePath: opts.certificatePath,
@@ -560,9 +567,9 @@ async function packageWindows(config, outDir) {
560
567
  if (opts.zip || opts.msi === false) {
561
568
  const zipResult = await createZIP({
562
569
  name,
563
- version,
570
+ version: version2,
564
571
  binaryPath: config.binaryPath,
565
- outputPath: import_path.join(outDir, `${name}-${version}-windows.zip`)
572
+ outputPath: import_path.join(outDir, `${name}-${version2}-windows.zip`)
566
573
  });
567
574
  results.push({
568
575
  success: zipResult.success,
@@ -576,14 +583,14 @@ async function packageWindows(config, outDir) {
576
583
  }
577
584
  async function packageLinux(config, outDir) {
578
585
  const results = [];
579
- const { name, version } = config;
586
+ const { name, version: version2 } = config;
580
587
  const opts = config.linux || {};
581
588
  if (opts.deb !== false) {
582
589
  const debResult = await createDEB({
583
590
  name,
584
- version,
591
+ version: version2,
585
592
  binaryPath: config.binaryPath,
586
- outputPath: import_path.join(outDir, `${name}_${version}_amd64.deb`),
593
+ outputPath: import_path.join(outDir, `${name}_${version2}_amd64.deb`),
587
594
  description: config.description || "",
588
595
  maintainer: config.author || "Unknown",
589
596
  dependencies: opts.debDependencies || ["libgtk-3-0", "libwebkit2gtk-4.1-37"]
@@ -599,9 +606,9 @@ async function packageLinux(config, outDir) {
599
606
  if (opts.rpm) {
600
607
  const rpmResult = await createRPM({
601
608
  name,
602
- version,
609
+ version: version2,
603
610
  binaryPath: config.binaryPath,
604
- outputPath: import_path.join(outDir, `${name}-${version}-1.x86_64.rpm`),
611
+ outputPath: import_path.join(outDir, `${name}-${version2}-1.x86_64.rpm`),
605
612
  description: config.description || "",
606
613
  requires: opts.rpmDependencies || ["gtk3", "webkit2gtk3"]
607
614
  });
@@ -616,9 +623,9 @@ async function packageLinux(config, outDir) {
616
623
  if (opts.appImage) {
617
624
  const appImageResult = await createAppImage({
618
625
  name,
619
- version,
626
+ version: version2,
620
627
  binaryPath: config.binaryPath,
621
- outputPath: import_path.join(outDir, `${name}-${version}-x86_64.AppImage`),
628
+ outputPath: import_path.join(outDir, `${name}-${version2}-x86_64.AppImage`),
622
629
  iconPath: config.iconPath
623
630
  });
624
631
  results.push({
@@ -2809,7 +2816,7 @@ class SystemTray {
2809
2816
  }
2810
2817
  async setIcon(icon) {
2811
2818
  this._options.icon = icon;
2812
- await this._call("setIcon", icon);
2819
+ await this._call("setIcon", JSON.stringify({ icon }));
2813
2820
  }
2814
2821
  async setTooltip(tooltip) {
2815
2822
  this._options.tooltip = tooltip;
@@ -2823,6 +2830,9 @@ class SystemTray {
2823
2830
  if (!processedItem.id && processedItem.label) {
2824
2831
  processedItem.id = `menu_${index}_${Date.now()}`;
2825
2832
  }
2833
+ if (!processedItem.action && processedItem.id && processedItem.type !== "separator") {
2834
+ processedItem.action = processedItem.id;
2835
+ }
2826
2836
  if (processedItem.submenu) {
2827
2837
  processedItem.submenu = processItems(processedItem.submenu);
2828
2838
  }
@@ -2885,9 +2895,9 @@ class SystemTray {
2885
2895
  return new Promise((resolve, reject) => {
2886
2896
  try {
2887
2897
  window.webkit.messageHandlers.craft.postMessage({
2888
- type: "tray",
2889
- action,
2890
- data: typeof data === "string" ? data : JSON.stringify(data)
2898
+ t: "tray",
2899
+ a: action,
2900
+ d: typeof data === "string" ? data : JSON.stringify(data)
2891
2901
  });
2892
2902
  resolve(undefined);
2893
2903
  } catch (error) {
@@ -4167,8 +4177,8 @@ var crypto = {
4167
4177
  if (combined.length < 1 + SALT_LEN + IV_LEN + TAG_LEN) {
4168
4178
  throw new CraftCryptoError("Ciphertext too short to be valid");
4169
4179
  }
4170
- const version = combined[0];
4171
- if (version !== FORMAT_VERSION) {
4180
+ const version2 = combined[0];
4181
+ if (version2 !== FORMAT_VERSION) {
4172
4182
  throw new LegacyCiphertextError;
4173
4183
  }
4174
4184
  const salt = combined.slice(1, 1 + SALT_LEN);
@@ -5133,6 +5143,13 @@ var lifecycle = {
5133
5143
  return () => document.removeEventListener("visibilitychange", handler);
5134
5144
  }
5135
5145
  };
5146
+ function bridgeNotification(options, now = Date.now()) {
5147
+ const { scheduleAt, ...rest } = options;
5148
+ if (scheduleAt === undefined)
5149
+ return rest;
5150
+ const wait = Math.ceil(scheduleAt - now);
5151
+ return wait > 0 ? { ...rest, delay: wait } : rest;
5152
+ }
5136
5153
  var notifications = {
5137
5154
  async show(options) {
5138
5155
  const craft = getCraftMobile();
@@ -5146,7 +5163,7 @@ var notifications = {
5146
5163
  async schedule(options) {
5147
5164
  const craft = getCraftMobile();
5148
5165
  if (typeof window !== "undefined" && craft?.notifications) {
5149
- return craft.notifications.schedule(options);
5166
+ return craft.notifications.schedule(bridgeNotification(options));
5150
5167
  }
5151
5168
  throw new Error("Scheduled notifications not available in web");
5152
5169
  },
@@ -10822,14 +10839,14 @@ async function updaterCommand(args) {
10822
10839
  switch (subcommand) {
10823
10840
  case "check": {
10824
10841
  const url = rest[0];
10825
- const version = rest[1] || "0.0.0";
10842
+ const version2 = rest[1] || "0.0.0";
10826
10843
  if (!url) {
10827
10844
  console.error("Usage: craft updater check <update-url> [current-version]");
10828
10845
  process.exit(1);
10829
10846
  }
10830
10847
  const updater = new AutoUpdater({
10831
10848
  updateUrl: url,
10832
- currentVersion: version,
10849
+ currentVersion: version2,
10833
10850
  appPath: process.cwd(),
10834
10851
  autoDownload: false
10835
10852
  });
@@ -10863,14 +10880,14 @@ ${update.releaseNotes}`);
10863
10880
  break;
10864
10881
  }
10865
10882
  case "generate-manifest": {
10866
- const version = rest[0];
10883
+ const version2 = rest[0];
10867
10884
  const outputPath = rest[1] || "update.json";
10868
- if (!version) {
10885
+ if (!version2) {
10869
10886
  console.error("Usage: craft updater generate-manifest <version> [output]");
10870
10887
  process.exit(1);
10871
10888
  }
10872
10889
  const manifest = {
10873
- version,
10890
+ version: version2,
10874
10891
  releaseDate: new Date().toISOString(),
10875
10892
  platforms: {}
10876
10893
  };
@@ -10902,21 +10919,13 @@ var updater_default = AutoUpdater;
10902
10919
  // src/index.ts
10903
10920
  var execFileAsync2 = import_node_util.promisify(import_node_child_process.execFile);
10904
10921
  var versionMismatchWarned = false;
10905
- function getSdkVersion() {
10906
- try {
10907
- const pkgPath = import_node_path.join("/home/runner/work/craft/craft/packages/typescript/src", "..", "package.json");
10908
- return JSON.parse(import_node_fs2.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
10909
- } catch {
10910
- return "unknown";
10911
- }
10912
- }
10913
10922
  async function probeBinaryVersion(craftPath) {
10914
10923
  if (versionMismatchWarned)
10915
10924
  return;
10916
10925
  try {
10917
10926
  const { stdout } = await execFileAsync2(craftPath, ["--version"], { timeout: 2000 });
10918
10927
  const nativeVersion = parseCraftVersionOutput(stdout);
10919
- const sdkVersion = getSdkVersion();
10928
+ const sdkVersion = SDK_VERSION;
10920
10929
  if (sdkVersion !== "unknown" && nativeVersion && nativeVersion !== sdkVersion) {
10921
10930
  versionMismatchWarned = true;
10922
10931
  console.warn(`⚠️ Craft binary/SDK version mismatch: SDK=${sdkVersion}, native=${nativeVersion}.
@@ -10926,7 +10935,27 @@ async function probeBinaryVersion(craftPath) {
10926
10935
  }
10927
10936
  function parseCraftVersionOutput(output) {
10928
10937
  const firstLine = output.trim().split(/\r?\n/, 1)[0] ?? "";
10929
- return firstLine.replace(/^craft version\s+/i, "").replace(/^v/, "").trim();
10938
+ const withoutName = firstLine.replace(/^craft(?:\s+version)?[\s/]+/i, "").trim();
10939
+ const firstToken = withoutName.split(/\s+/, 1)[0] ?? "";
10940
+ return firstToken.replace(/^v/, "").trim();
10941
+ }
10942
+ var STDERR_TAIL_LIMIT = 4000;
10943
+ function craftProcessFailedMessage(code, stderr, quiet) {
10944
+ const said = stderr.trim();
10945
+ const header = `❌ Craft process exited with code ${code}`;
10946
+ if (said)
10947
+ return `${header}
10948
+
10949
+ ${said}`;
10950
+ const where = quiet ? "It wrote nothing to stderr. Re-run without `quiet` to see its full output." : "Check the console output above for more details.";
10951
+ return `${header}
10952
+
10953
+ ` + `This may indicate:
10954
+ ` + ` • Invalid window configuration
10955
+ ` + ` • Malformed HTML content
10956
+ ` + ` • System resource constraints
10957
+
10958
+ ` + where;
10930
10959
  }
10931
10960
  function detectDevMode() {
10932
10961
  if (typeof process === "undefined")
@@ -10989,8 +11018,14 @@ class CraftApp {
10989
11018
  const args = this.buildArgs();
10990
11019
  const craftPath = await this.findCraftBinary();
10991
11020
  probeBinaryVersion(craftPath);
11021
+ const quiet = this.config.quiet === true;
10992
11022
  this.process = import_node_child_process.spawn(craftPath, args, {
10993
- stdio: this.config.quiet ? "ignore" : "inherit"
11023
+ stdio: quiet ? ["ignore", "ignore", "pipe"] : "inherit",
11024
+ env: { ...process.env, [CRAFT_CLI_SPAWN_MARKER]: craftPath }
11025
+ });
11026
+ let stderrTail = "";
11027
+ this.process.stderr?.on("data", (chunk) => {
11028
+ stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_LIMIT);
10994
11029
  });
10995
11030
  return new Promise((resolve, reject) => {
10996
11031
  this.process?.on("error", (error) => {
@@ -11010,18 +11045,11 @@ class CraftApp {
11010
11045
  ` + `For troubleshooting, visit:
11011
11046
  ` + " https://github.com/home-lang/craft/issues"));
11012
11047
  });
11013
- this.process?.on("exit", (code) => {
11048
+ this.process?.on("close", (code) => {
11014
11049
  if (code === 0 || code === null) {
11015
11050
  resolve();
11016
11051
  } else {
11017
- reject(new Error(`❌ Craft process exited with code ${code}
11018
-
11019
- ` + `This may indicate:
11020
- ` + ` • Invalid window configuration
11021
- ` + ` • Malformed HTML content
11022
- ` + ` • System resource constraints
11023
-
11024
- ` + "Check the console output above for more details."));
11052
+ reject(new Error(craftProcessFailedMessage(code, stderrTail, quiet)));
11025
11053
  }
11026
11054
  });
11027
11055
  });
package/dist/index.d.cts CHANGED
@@ -3,7 +3,27 @@
3
3
  * TypeScript SDK powered by Bun
4
4
  */
5
5
  import type { AppConfig, WindowOptions } from './types.js';
6
+ /**
7
+ * The version a `craft --version` first line reports, whichever form it takes:
8
+ * `craft version 0.0.92`, `craft/0.0.92 darwin-arm64 bun-v1.4.1`,
9
+ * `craft 0.0.92`, `v0.0.92`, or a bare `0.0.92`.
10
+ *
11
+ * The slash form is the CLI's own, and used to survive the strip intact, so
12
+ * the whole line was compared against a semver and never matched — two
13
+ * identical versions reported as drift, on the one path where the real fault
14
+ * was that PATH had resolved to the CLI at all (#236).
15
+ */
6
16
  export declare function parseCraftVersionOutput(output: string): string;
17
+ /**
18
+ * The rejection for a child that exited non-zero.
19
+ *
20
+ * Whatever the child managed to say comes first, because it is the only part
21
+ * that can name the actual fault. `quiet` used to map to `stdio: 'ignore'`,
22
+ * which threw that away and then told the caller to read output that no
23
+ * longer existed — leaving `exited with code 1` as the whole of the error
24
+ * (#236).
25
+ */
26
+ export declare function craftProcessFailedMessage(code: number | null, stderr: string, quiet: boolean): string;
7
27
  export { packageApp, pack, type PackageConfig, type PackageResult } from './package.js';
8
28
  export { craftBinaryNotFoundMessage, resolveCraftBinary } from './binary-resolver.js';
9
29
  export * from './utils/index.js';