arcane-os 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ - Add `arcane dev --public`, including forwarding through
6
+ `npm run dev -- --app <id> --public`, to bind the selected app's existing
7
+ source server to all IPv4 interfaces. Keep the ordinary localhost default
8
+ and let an explicit `--host` select the bind address.
9
+ - Allow explicit network hosts in the shared development server. Report usable
10
+ local and network application URLs, including `networkUrls` in server results
11
+ and lifecycle events, while preserving selected routes and cancellation.
12
+ - Document LAN use and the browser HTTPS requirement for features that need a
13
+ secure context. No app-specific server, firewall rule, or tunnel is added.
14
+
15
+ ## 0.6.2
16
+
17
+ - Replace browser NPU setup buttons with direct browser-specific flags links
18
+ and retain copy-address controls and the public `open()` method.
19
+ - Add the missing GPU performance flags link, setup instructions, and GPU
20
+ detection control. Chrome uses its own `chrome://` flags address.
21
+ - Show adapter availability and its reported name after one shared detection
22
+ request. Keep fallback and failed detection visible without claiming that
23
+ the browser performance flag is enabled or a model is running on that GPU.
24
+ Detection creates no device, loads no model, and changes no browser settings.
25
+
3
26
  ## 0.6.1
4
27
 
5
28
  - Add a reusable profile component for browser NPU setup and WebNN/WebGPU API
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.6.1` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.7.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -35,12 +35,27 @@ Create one browser application, install its pinned SDK, and start its source
35
35
  server:
36
36
 
37
37
  ```bash
38
- npx arcane-os@0.6.1 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.7.0 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
42
42
  ```
43
43
 
44
+ To open a selected app from another device on the network, append `--public`
45
+ to the same command:
46
+
47
+ ```bash
48
+ npm run dev -- --app hello-speech --public
49
+ ```
50
+
51
+ Replace `hello-speech` with any app in the workspace. The server binds to
52
+ `0.0.0.0` and prints network URLs to open on the other device. Plain `npm run dev`
53
+ keeps its localhost default. An explicit `--host` overrides the public bind
54
+ address, and `--port` selects the port. Network reachability depends on the
55
+ machine's firewall and network; the command does not configure internet hosting
56
+ or HTTPS. Browser features requiring a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts)
57
+ need HTTPS when opened through a LAN address.
58
+
44
59
  Open the URL printed by the server. The generated page owns its import map and
45
60
  Arcane theme; its application module is `apps/hello-speech/modules/App.js`.
46
61
  `arcane/AI` is a managed **browser import**, not an npm-exported Node inference
@@ -363,7 +378,7 @@ uses the same controller for automatic memory extraction.
363
378
  Create a new repository-shaped Arcane application with the exact stable SDK:
364
379
 
365
380
  ```bash
366
- npx arcane-os@0.6.1 new my-app --path ./my-app --target portable --git
381
+ npx arcane-os@0.7.0 new my-app --path ./my-app --target portable --git
367
382
  cd my-app
368
383
  npm install
369
384
  npm run dev
@@ -373,7 +388,7 @@ To enroll an existing repository, install the exact SDK and initialize only
373
388
  missing Arcane files:
374
389
 
375
390
  ```bash
376
- npm install --save-dev --save-exact arcane-os@0.6.1
391
+ npm install --save-dev --save-exact arcane-os@0.7.0
377
392
  npm exec -- arcane init my-app --target portable
378
393
  ```
379
394
 
@@ -389,7 +404,7 @@ npm exec -- arcane-os targets
389
404
  No global SDK install or standalone Arcane CLI is required. The application
390
405
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
391
406
 
392
- Use `npx arcane-os@0.6.1` for the initial bootstrap because it names this npm
407
+ Use `npx arcane-os@0.7.0` for the initial bootstrap because it names this npm
393
408
  package explicitly; bare `npx arcane` outside an installed project could resolve
394
409
  a different package. Both installed commands invoke the same headless toolchain.
395
410
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -409,7 +424,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
409
424
 
410
425
  # From the generated app repository
411
426
  cd ../local-app
412
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.6.1.tgz
427
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.0.tgz
413
428
  npm ci
414
429
  ```
415
430
 
@@ -418,7 +433,7 @@ same location. The lockfile retains the selected package dependency while
418
433
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
419
434
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
420
435
  runner also needs that tarball at the locked path. After publication, replace
421
- the local declaration with the exact `arcane-os@0.6.1` registry package and
436
+ the local declaration with the exact `arcane-os@0.7.0` registry package and
422
437
  commit the regenerated lock.
423
438
 
424
439
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -475,7 +490,7 @@ arcane new <id> [--path <directory>] [--display-name <name>] [--target <target>]
475
490
  arcane init [id] [--workspace <directory>] [--display-name <name>] [--target <target>]
476
491
  arcane doctor [--workspace <directory>] [--arcane-root <directory>]
477
492
  arcane import-map [--workspace <directory>] [--app <id>]
478
- arcane dev [--app <id>] [--host 127.0.0.1] [--port 8000]
493
+ arcane dev [--app <id>] [--public] [--host <address>] [--port 8000]
479
494
  arcane test [--app <id>] [--scope app]
480
495
  arcane test --scope shared --test-file <repo-relative.test.mjs>
481
496
  arcane check [--app <id>] [--scope app] [--skip-tests]
@@ -557,7 +572,7 @@ package installation, or assertions.
557
572
 
558
573
  ## Current target support
559
574
 
560
- Version `0.6.1` exposes one browser target and five explicitly paired
575
+ Version `0.7.0` exposes one browser target and five explicitly paired
561
576
  native development targets: a non-runnable portable directory, a
562
577
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
563
578
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -683,7 +698,7 @@ read-only managed-service assessment where the host supports one. Native apps
683
698
  use an app-scoped Arcane Core and `Arcane.localAI.status()` in this SDK version.
684
699
  Ollama is optional for packaging and non-AI applications.
685
700
 
686
- The loopback development server serves complete selected files and does not
701
+ The development server serves complete selected files and does not
687
702
  apply byte-count or truncation gates. Optional hardening is inactive unless the
688
703
  caller explicitly selects `secure:true`. Required credential protection,
689
704
  malformed-request rejection, and unavoidable browser or operating-system safety
@@ -63,6 +63,7 @@ export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator)
63
63
  ) {
64
64
  browserId = "chrome";
65
65
  name = "Google Chrome";
66
+ highPerformanceGpu = { name, url: "chrome://flags/#force-high-performance-gpu" };
66
67
  } else {
67
68
  browserId = "chromium";
68
69
  name = "your Chromium browser";
@@ -81,3 +82,23 @@ export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator)
81
82
  webgpuAvailable: Boolean(navigatorObject?.gpu),
82
83
  };
83
84
  }
85
+
86
+ // Adapter selection is availability evidence, not the browser flag's state or
87
+ // proof that a model is executing on this GPU. The power preference is a hint.
88
+ export async function detectBrowserGpu(navigatorObject = globalThis.navigator) {
89
+ if (!is.function(navigatorObject?.gpu?.requestAdapter)) {
90
+ return { available: false, reason: "api-unavailable" };
91
+ }
92
+ const adapter = await navigatorObject.gpu.requestAdapter({ powerPreference: "high-performance" });
93
+ if (!adapter) return { available: false, reason: "adapter-unavailable" };
94
+ const info = adapter.info;
95
+ return {
96
+ available: true,
97
+ name: info?.description
98
+ || [info?.vendor, info?.architecture, info?.device].filter(Boolean).join(" ")
99
+ || "WebGPU adapter",
100
+ isFallbackAdapter: is.boolean(info?.isFallbackAdapter)
101
+ ? info.isFallbackAdapter
102
+ : is.boolean(adapter.isFallbackAdapter) ? adapter.isFallbackAdapter : null,
103
+ };
104
+ }
@@ -236,7 +236,7 @@ Packaging writes the complete schema-1 release inventory to
236
236
  `ARCANE_APP_RELEASE.json`. Ordinary development, packaging, serving, and run
237
237
  paths do not count, limit, hash, truncate, tail, clip, or identify content by
238
238
  bytes, and they do not require provenance or verification receipts. The
239
- loopback server reads and returns each complete selected source, runtime, or
239
+ development server reads and returns each complete selected source, runtime, or
240
240
  packaged file. Required credential protection, malformed-input rejection,
241
241
  applicable law, unavoidable protocol rules, and operating-system or browser
242
242
  safety remain in effect.
@@ -279,7 +279,7 @@ paths are withheld from the native provider. The provider copies the complete
279
279
  selected release rather than accepting an unrelated source path. Verification
280
280
  is a separate explicit operation for a selected release artifact.
281
281
 
282
- The SDK `0.6.1` runtime requires Arcane `0.8.12` or newer. Compatibility
282
+ The SDK `0.7.0` runtime requires Arcane `0.8.12` or newer. Compatibility
283
283
  is contractual rather than exact-version pinning: the prepared Core must meet
284
284
  the highest minimum declared by the runtime, selected app, and bundled app
285
285
  dependencies; keep each app's Arcane protocol generation; and provide every
@@ -143,7 +143,7 @@ The synchronized browser payload exposes:
143
143
  ESM modules, classic vendor globals, one worker protocol, and one Node-oriented
144
144
  mail transport;
145
145
  - 14 shared entity modules under `runtime/arcane/entities/`;
146
- - 39 reusable HTML-import components under `runtime/arcane/components/`;
146
+ - 40 reusable HTML-import components under `runtime/arcane/components/`;
147
147
  - seven shared CSS artifacts, images, optional physical-workspace security
148
148
  files where present, and the vendored `strong-type` dependency.
149
149
 
@@ -130,11 +130,12 @@ window.addEventListener('ai-tts-failure', function reportSpeechFailure(event) {
130
130
 
131
131
  Call `speechEvents.abort()` when disposing that interface to remove the listener.
132
132
 
133
- ## Browser NPU setup
133
+ ## Browser NPU and GPU setup
134
134
 
135
135
  Applications can place the shared `browser-ai-setup.html` component in their
136
136
  profile or settings page. It reports whether this page exposes WebNN and
137
- WebGPU, without loading a model or creating an accelerator context:
137
+ WebGPU, then requests one WebGPU adapter to display its availability and reported
138
+ name. It does not load a model or create a GPU device or WebNN context:
138
139
 
139
140
  ```html
140
141
  <html-import
@@ -143,15 +144,31 @@ WebGPU, without loading a model or creating an accelerator context:
143
144
  </html-import>
144
145
  ```
145
146
 
146
- **Set up NPU** uses the same browser-settings approach as the existing
147
- high-performance GPU notice: attempt to open the browser's flags page, then
148
- show instructions including the full address to paste if navigation was
149
- blocked. Chrome uses `chrome://flags/#web-machine-learning-neural-network`;
150
- Edge uses `edge://flags/#web-machine-learning-neural-network`. Unrecognized
151
- browsers receive explicit Chrome and Edge choices instead of an assumed target.
147
+ The **NPU setup** section provides real browser-specific flags links. Chrome uses
148
+ `chrome://flags/#web-machine-learning-neural-network`; Edge uses
149
+ `edge://flags/#web-machine-learning-neural-network`. Unrecognized browsers
150
+ receive explicit Chrome and Edge choices. Full addresses and copy controls
151
+ remain available if the browser blocks internal-page links.
152
152
  The [ONNX Runtime WebNN guide](https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html)
153
153
  documents the **Enables WebNN API** flag and model/operator requirements.
154
154
 
155
+ The **GPU performance** section shows adapter detection and, on desktop Windows
156
+ Chromium browsers, the matching Force High Performance GPU flags link. Identified
157
+ Chrome uses `chrome://flags/#force-high-performance-gpu`; other recognized
158
+ Chromium browsers use their corresponding internal scheme. **Detect GPU** or
159
+ **Refresh browser availability** requests a new availability result; concurrent
160
+ requests share the pending operation. The public `checkGpu()` method provides
161
+ the same promise, while `refresh()` remains a synchronous API-presence and
162
+ settings update. Component readiness does not wait for adapter detection.
163
+
164
+ Adapter selection uses `powerPreference: "high-performance"` as a hint, not proof
165
+ of the selected GPU's performance or the flag's current state. A returned
166
+ adapter establishes availability only; its fallback classification is reported
167
+ when exposed. The component cannot read the browser flag or prove that a model
168
+ is executing on the adapter. See the
169
+ [component contract](../runtime-components.md#browser-ai-setuphtml) for result
170
+ fields, failure handling, and disposal behavior.
171
+
155
172
  This control does not save an execution preference, change browser settings,
156
173
  restart the browser, or report that the NPU is active. A WebNN API presence
157
174
  result is not proof of NPU hardware, a compatible model, or physical execution.
@@ -49,6 +49,7 @@ meaning and cardinality rules:
49
49
  | `--app` | app id | Workspace/app operations except shared scope and `verify-bundle`; also the exact `mail serve` caller id. |
50
50
  | `--arcane-root` | directory | `doctor`, native `build`/`run`, `native-doctor`, `native-prepare` |
51
51
  | `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to `127.0.0.1:8000`; `mail serve` defaults to `127.0.0.1:8025` and admits numeric loopback only. |
52
+ | `--public` | flag | `dev`; binds to `0.0.0.0` unless `--host` explicitly selects another address. |
52
53
  | `--target` | target id | `new`, `init`, native diagnostics, `build`, `run` |
53
54
  | `--format` / `--signing` | target-supported values | Native diagnostics, `build`, `run` |
54
55
  | `--output-root` | directory | Native `build` and `run` |
@@ -307,9 +308,9 @@ npm exec -- arcane upgrade --workspace . --app hello-world
307
308
 
308
309
  ### Overview
309
310
 
310
- Starts one loopback development server for one selected app and maps the exact
311
- workspace/runtime routes. It is a development convenience, not a production
312
- security boundary.
311
+ Starts one development server for one selected app and maps the exact
312
+ workspace/runtime routes. It defaults to localhost; `--public` enables access
313
+ from other devices on the network.
313
314
 
314
315
  For an external workspace, the server exposes the selected projected
315
316
  `arcane/` root, including `arcane/sdk` and `arcane/dependencies`, alongside the
@@ -318,20 +319,35 @@ The explicit live-source SDK mapping remains unchanged and does not replace the
318
319
  installed projection.
319
320
 
320
321
  ```text
321
- arcane dev [--app <id>] [--host 127.0.0.1] [--port 8000]
322
+ arcane dev [--app <id>] [--public] [--host <address>] [--port 8000]
322
323
  ```
323
324
 
324
325
  ### Lifecycle
325
326
 
326
327
  The command reports acceptance before bind/start work, emits the final URL,
327
328
  owns the server until cancellation, and restores failure to the process exit.
328
- The default host is loopback. Exposing another interface is an explicit
329
- development choice and does not add authentication.
329
+ The default host is `127.0.0.1`. `--public` selects `0.0.0.0` (all IPv4
330
+ interfaces); an explicit `--host` takes precedence. The command prints the
331
+ local URL and available network URLs. Use a printed network URL on the other
332
+ device, since `localhost` refers to that device and `0.0.0.0` is a bind address.
333
+ Network URLs come from one interface snapshot at startup and do not establish
334
+ remote reachability through the machine's firewall or network.
335
+
336
+ This option changes the listener address. It does not configure a firewall,
337
+ router forwarding, an internet tunnel, authentication, or HTTPS. Browser
338
+ features that require a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts)
339
+ still require HTTPS when accessed through a LAN address.
330
340
 
331
341
  ### Example
332
342
 
333
343
  ```bash
334
344
  npm exec -- arcane dev --app hello-world --port 8000
345
+
346
+ # Forward the public option through an existing dev script, for any app id.
347
+ npm run dev -- --app hello-world --public
348
+
349
+ # Equivalent direct CLI invocation, with an optional port.
350
+ npm exec -- arcane dev --app hello-world --public --port 8000
335
351
  ```
336
352
 
337
353
  ## `arcane test`
@@ -5,9 +5,9 @@
5
5
  "repository": "https://github.com/TheWizardNexus/arcane-os-sdk.git",
6
6
  "branch": "main",
7
7
  "path": "runtime/arcane/components",
8
- "sdkVersion": "0.5.18"
8
+ "sdkVersion": "0.6.2"
9
9
  },
10
- "componentCount": 39,
10
+ "componentCount": 40,
11
11
  "loader": "/arcane/modules/HTMLImport.js",
12
12
  "artifacts": [
13
13
  {
@@ -76,6 +76,30 @@
76
76
  "transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
77
77
  "normalization": "DOM-normalized; caller/provider results remain external"
78
78
  },
79
+ {
80
+ "file": "runtime/arcane/components/browser-ai-setup.html",
81
+ "name": "browser-ai-setup.html",
82
+ "purpose": "Displays browser API and WebGPU adapter availability with browser-specific NPU and GPU flags links.",
83
+ "methods": [
84
+ "refresh()",
85
+ "checkGpu()",
86
+ "open()",
87
+ "destroy()",
88
+ "ready"
89
+ ],
90
+ "events": [
91
+ "browser-ai-setup-ready"
92
+ ],
93
+ "slots": [],
94
+ "dependencies": [
95
+ "browser-device-settings.mjs",
96
+ "arcane-os/logging",
97
+ "arcane-os/event-manager"
98
+ ],
99
+ "availability": "Browser and supported native WebViews",
100
+ "transport": "HTMLImport + DOM; browser flags links, Clipboard API, WebGPU adapter request, and preserved open() navigation attempt with alert",
101
+ "normalization": "API presence, adapter availability, and setup instructions normalized. Mounting starts one adapter request; checkGpu() coalesces concurrent calls, while refresh() remains synchronous. No GPU device, model, or WebNN context is created. Adapter availability does not establish browser flag state, high-performance selection, or model execution."
102
+ },
79
103
  {
80
104
  "file": "runtime/arcane/components/calculator.html",
81
105
  "name": "calculator.html",
@@ -60,7 +60,7 @@ appropriate.
60
60
  | --- | --- | --- | --- | --- |
61
61
  | [`app-bar.html`](#app-barhtml) | Responsive application navigation, route state, status, and trailing actions. | `setNavigation()`<br>`setActiveRoute()`<br>`setStatus()`<br>`refresh()`<br>`destroy()` | `app-bar-ready` | DOM-normalized |
62
62
  | [`assistant-panel.html`](#assistant-panelhtml) | Reusable assistant drawer, message area, composer, pending/streaming/empty/error state, and actions. | `open()`<br>`close()`<br>`toggle()`<br>`send()`<br>`clear()`<br>`setState()`<br>`focusComposer()`<br>`scrollToEnd()`<br>`destroy()` | `assistant-ready`<br>`assistant-opened`<br>`assistant-closed`<br>`assistant-send`<br>`assistant-clear` | DOM-normalized; caller/provider results remain external |
63
- | [`browser-ai-setup.html`](#browser-ai-setuphtml) | Browser API availability and NPU setup instructions with browser-specific flags actions. | `refresh()`<br>`open()`<br>`destroy()`<br>`ready` | `browser-ai-setup-ready` | API presence only; settings navigation and hardware execution remain browser-owned |
63
+ | [`browser-ai-setup.html`](#browser-ai-setuphtml) | Browser API and WebGPU adapter availability, with browser-specific NPU and GPU flags links. | `refresh()`<br>`checkGpu()`<br>`open()`<br>`destroy()`<br>`ready` | `browser-ai-setup-ready` | API and adapter availability; browser flag state and model execution remain browser-owned |
64
64
  | [`calculator.html`](#calculatorhtml) | Calculator keypad and result/error event surface backed by CalculatorEngine. | `calculate()`<br>`destroy()` | `calculator-ready`<br>`calculation-complete`<br>`calculation-error` | Normalized Calculation/error events |
65
65
  | [`chart.html`](#charthtml) | Accessible uPlot line, area, or point chart with normalized options and rows. | `configure()`<br>`populate()`<br>`setData()`<br>`addData()`<br>`update()`<br>`destroy()` | `chart-ready`<br>`chart-remove` | Options/rows normalized; uPlot rendering is vendor-native |
66
66
  | [`chat.html`](#chathtml) | Shared chat, visible selected-model activation request, file upload, streaming, structural tool settlement, speech, language, availability, and conversation-timebox surface. | `streamMessage()`<br>`setMessageProgress()`<br>`setAIAvailability()`<br>`setInitialSpeechMuted()`<br>`setConversationComplete()`<br>`bindConversationTimebox()`<br>`bindSession()`<br>`submitMessage()`<br>`submitToolResult()`<br>`submitToolResults()`<br>`sendMessage()`<br>`languageChanged()`<br>`requestAIActivation()`<br>`destroy()` | `chat-ready`<br>`chat-session-bound`<br>`chat-session-message`<br>`chat-session-error`<br>`chat-send-message`<br>`chat-send-error`<br>`chat-file-uploaded`<br>`chat-file-upload-error`<br>`chat-language-changed`<br>`chat-language-change-error`<br>`chat-ai-activation-request`<br>`chat-ai-activation-error`<br>`chat-speech-synthesis-error`<br>`conversation-timebox-error` | UI/runtime state, explicit user activation intent, and honest structural-call settlement normalized; AI/storage/media behavior mixed |
@@ -158,15 +158,18 @@ Slots: `title`, `subtitle`, `identity`, `messages/message`, `composer`, `actions
158
158
 
159
159
  ### Overview
160
160
 
161
- Displays WebNN and WebGPU API availability for the current page and explains how
162
- to enable WebNN for NPU use. Its setup action uses the same browser-flags opening
163
- attempt and alert fallback as the existing high-performance GPU notice. Chrome
164
- and Edge receive their own flags addresses; other or unidentified browsers show
165
- both explicit choices.
161
+ Displays WebNN and WebGPU API availability for the current page, detects an
162
+ available WebGPU adapter, and provides browser-specific NPU and GPU flags links.
163
+ The WebNN addresses are real links: Chrome and Edge receive their own addresses;
164
+ other or unidentified browsers show both explicit choices. Desktop Windows
165
+ Chromium browsers also receive the matching Force High Performance GPU link,
166
+ including `chrome://flags/#force-high-performance-gpu` for identified Chrome.
167
+ Copy controls and complete addresses remain available when the browser blocks
168
+ navigation to an internal page.
166
169
 
167
170
  ### Public surface
168
171
 
169
- Methods/properties: `refresh()`, `open(browserId?)`, `destroy()`, `ready`.
172
+ Methods/properties: `refresh()`, `checkGpu()`, `open(browserId?)`, `destroy()`, `ready`.
170
173
 
171
174
  Events: `browser-ai-setup-ready`.
172
175
 
@@ -176,6 +179,24 @@ the current settings record: `browserId`, `name`, `webnnFlagsURL`,
176
179
  after destruction. Availability means that the browser exposes the API to this
177
180
  page; it does not confirm a working adapter, loaded model, or hardware execution.
178
181
 
182
+ `checkGpu()` returns a promise for one adapter-availability record. Concurrent
183
+ calls share the pending promise. The component requests an adapter with the
184
+ `"high-performance"` power-preference hint; it does not create a GPU device or
185
+ load a model. An unavailable result contains `available: false` and a `reason`
186
+ of `"api-unavailable"` or `"adapter-unavailable"`. An available result contains
187
+ `available: true`, the reported adapter `name`, and `isFallbackAdapter` as a
188
+ boolean or `null` when the browser exposes no classification. Adapter selection
189
+ does not prove that a browser flag is enabled, that the fastest GPU was selected,
190
+ or that a model is using it. Failure is displayed and logged, and the promise
191
+ resolves to `false`; destruction also yields `false` and suppresses late UI
192
+ updates.
193
+
194
+ Mounting starts one `checkGpu()` operation. **Detect GPU** repeats it explicitly;
195
+ **Refresh browser availability** calls both `refresh()` and `checkGpu()`. Calling
196
+ `refresh()` directly remains synchronous and does not request an adapter.
197
+ `ready` and `browser-ai-setup-ready` describe component setup, which can complete
198
+ while adapter detection is still pending.
199
+
179
200
  `open()` refreshes the display, attempts to open the detected Chrome or Edge flags
180
201
  page, and presents an alert containing the full address and enable/relaunch
181
202
  instructions. Pass `"chrome"` or `"edge"` to choose explicitly. Invoke it from a
@@ -185,17 +206,19 @@ succeeded. Browsers may block internal-page navigation, so the full addresses
185
206
  also remain visible and copyable in the component.
186
207
 
187
208
  `destroy()` aborts owned listeners, disposes the event source, marks `ready`
188
- false, and suppresses UI updates from a pending clipboard operation. It returns
189
- `true` the first time and `false` thereafter. A BFCache-persisted `pagehide`
190
- preserves the component; nonpersisted `pagehide` destroys it.
209
+ false, and suppresses UI updates from pending clipboard and adapter operations.
210
+ It does not cancel the browser-owned adapter request. It returns `true` the
211
+ first time and `false` thereafter. A BFCache-persisted `pagehide` preserves the
212
+ component; nonpersisted `pagehide` destroys it.
191
213
 
192
214
  ### Availability and normalization
193
215
 
194
- **Browser and supported native WebViews.** API presence and setup instructions
195
- are normalized; browser flags, clipboard support, and device execution remain
196
- platform-owned. Mounting or refreshing creates no model, GPU adapter, or WebNN
197
- context. The component saves no preferences, changes no browser settings, and
198
- does not restart the browser. It cannot report model state owned by another page.
216
+ **Browser and supported native WebViews.** API presence, adapter availability,
217
+ and setup instructions are normalized; browser flag state, clipboard support,
218
+ and model execution remain platform-owned. Mounting requests one WebGPU adapter
219
+ when that API is available. No GPU device, model, or WebNN context is created.
220
+ The component saves no preferences, changes no browser settings, and does not
221
+ restart the browser. It cannot report model state owned by another page.
199
222
 
200
223
  ### Example
201
224
 
@@ -3566,5 +3566,5 @@ console.log(Object.keys(module));
3566
3566
  ## Entity and component continuations
3567
3567
 
3568
3568
  - [Runtime entity modules](runtime-entities.md) explains all 14 modules, and [shared entity contracts](core/arcane-entities.md) owns all 29 exports.
3569
- - [Runtime components](runtime-components.md) owns all 39 HTML-import fragments, methods, slots, and events.
3569
+ - [Runtime components](runtime-components.md) owns all 40 HTML-import fragments, methods, slots, and events.
3570
3570
  - [Arcane Ollama](arcane-ollama.md) expands the raw-versus-normalized behavior of `Ollama.js`.
@@ -3451,7 +3451,7 @@ async function useselectApp(...arguments_) {
3451
3451
 
3452
3452
  ### Overview
3453
3453
 
3454
- Starts one owned browser development server with exact runtime/app route mappings and an unguessable session capability.
3454
+ Starts one owned browser development server with exact runtime/app route mappings and a caller-selected bind address.
3455
3455
 
3456
3456
  ### Signature, modes, and result
3457
3457
 
@@ -3464,19 +3464,23 @@ Import it from `arcane-os`. Source mode accepts
3464
3464
  signal, onEvent}` and serves one validated workspace application plus its
3465
3465
  complete SDK or integrated runtime. Packaged mode uses
3466
3466
  `{mode:'packaged', releaseRoot, host, port, signal, onEvent}` and serves the
3467
- complete selected release files. `host` must be
3468
- numeric loopback `127.0.0.1` or `::1`; port `0` asks the operating system for an
3469
- available port.
3467
+ complete selected release files. `host` defaults to `127.0.0.1` and accepts an
3468
+ explicit network address or hostname. Use `0.0.0.0` for all IPv4 interfaces or
3469
+ `::` for the platform's IPv6 wildcard listener; port `0` asks the operating
3470
+ system for an available port.
3470
3471
 
3471
3472
  The promise settles after the listener is ready and resolves to
3472
- `{server, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url, close,
3473
- closed, lifecycle}`. `server` is the raw Node HTTP server. `url` contains the
3474
- unguessable bootstrap capability that establishes an HttpOnly session
3475
- cookie; do not log, persist, or disclose it. `cleanUrl` contains no capability
3476
- but does not bootstrap a new browser session by itself. In packaged mode,
3477
- `workspaceRoot` and `appId` are `null`.
3478
-
3479
- Starting the server opens a loopback listener and emits awaited,
3473
+ `{server, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url,
3474
+ networkUrls, close, closed, lifecycle}`. `server` is the raw Node HTTP server.
3475
+ `url` and `cleanUrl` are the same application URL. Wildcard listeners use
3476
+ `localhost` in that local URL; `host` retains the actual bound address.
3477
+ `networkUrls` lists application URLs for applicable non-loopback interface
3478
+ addresses discovered once at startup. These URLs are connection candidates,
3479
+ not evidence of reachability from another device. The server adds no session
3480
+ capability or authentication. In packaged mode, `workspaceRoot` and `appId`
3481
+ are `null`.
3482
+
3483
+ Starting the server opens the selected listener and emits awaited,
3480
3484
  backpressured `server.starting` and `server.started` events. Request failures
3481
3485
  emit `server.request.failed`; shutdown emits `server.stopped` after owned
3482
3486
  requests and event delivery drain. Call `await result.close()` in a `finally`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -8,9 +8,10 @@
8
8
  .flag-choice{margin-block:var(--arcane-space-4)}
9
9
  .flag-choice h3{margin-block:var(--arcane-space-2)}
10
10
  .actions{display:flex;flex-wrap:wrap;gap:var(--arcane-space-2)}
11
- p,dt,dd,li,button{overflow-wrap:anywhere}
11
+ p,dt,dd,li,button,a{overflow-wrap:anywhere}
12
12
  button{white-space:normal}
13
- code{display:block;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;margin-block:var(--arcane-space-2)}
13
+ .flag-address{display:block;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;margin-block:var(--arcane-space-2)}
14
+ #gpuAvailability{white-space:normal;overflow-wrap:anywhere;line-height:1.4;max-width:100%;box-sizing:border-box}
14
15
  li{margin-block:var(--arcane-space-2)}
15
16
  </style>
16
17
 
@@ -31,6 +32,21 @@
31
32
  <ol id="setupSteps"></ol>
32
33
  <p>Browser API availability does not confirm that an NPU or a particular model is supported. You choose whether to change the browser setting.</p>
33
34
  <p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">ONNX Runtime WebNN setup documentation</a></p>
35
+ <h3>GPU performance</h3>
36
+ <p><span id="gpuAvailability" class="arcane-badge" role="status" aria-live="polite"></span></p>
37
+ <p id="gpuAdapterName" class="arcane-help"></p>
38
+ <button id="detectGpu" class="arcane-button arcane-button--secondary" type="button">Detect GPU</button>
39
+ <p id="gpuInstructions"></p>
40
+ <div id="gpuFlagAddress">
41
+ <a id="gpuFlagsLink" class="flag-address"></a>
42
+ <button id="copyGpuAddress" class="arcane-button arcane-button--secondary" type="button">Copy GPU flags address</button>
43
+ <ol>
44
+ <li>Open the address above. If the browser blocks the link, copy the address and paste it into the address bar.</li>
45
+ <li>Find “Force High Performance GPU” and choose Enabled.</li>
46
+ <li>Save your work, then completely close and reopen the browser before loading the model again.</li>
47
+ </ol>
48
+ </div>
49
+ <p class="arcane-help">GPU detection reports adapter availability. This page cannot read the flag's current setting or confirm that a model is using that adapter.</p>
34
50
  <p id="copyStatus" class="arcane-help" role="status" aria-live="polite"></p>
35
51
  </div>
36
52
  <footer class="arcane-card__footer">
@@ -46,6 +62,7 @@
46
62
 
47
63
  const {
48
64
  getBrowserDeviceSettings,
65
+ detectBrowserGpu,
49
66
  openBrowserDeviceSettings,
50
67
  WEBNN_BROWSER_SETTINGS
51
68
  }=await import('../sdk/ai/browser-device-settings.mjs');
@@ -58,6 +75,12 @@
58
75
  const lifecycleController=new AbortController();
59
76
  const status=root.querySelector('#setupStatus');
60
77
  const copyStatus=root.querySelector('#copyStatus');
78
+ const gpuAvailability=root.querySelector('#gpuAvailability');
79
+ const gpuAdapterName=root.querySelector('#gpuAdapterName');
80
+ const detectGpuButton=root.querySelector('#detectGpu');
81
+ const gpuFlagsLink=root.querySelector('#gpuFlagsLink');
82
+ const copyGpuButton=root.querySelector('#copyGpuAddress');
83
+ const copyButtons=[copyGpuButton];
61
84
  const choices=[];
62
85
  const setupSteps=[
63
86
  'If the flags page does not open, copy the matching address into your browser\'s address bar and open it.',
@@ -66,9 +89,11 @@
66
89
  'Return to this page and select Refresh browser availability.'
67
90
  ];
68
91
  let copyTask=null;
92
+ let gpuTask=null;
69
93
  let destroyed=false;
70
94
 
71
95
  host.refresh=refresh;
96
+ host.checkGpu=checkGpu;
72
97
  host.open=open;
73
98
  host.destroy=destroy;
74
99
 
@@ -85,18 +110,65 @@
85
110
  ?'Exposed to this page.'
86
111
  :'Unavailable to this page.';
87
112
  root.querySelector('#browserInstructions').textContent=settings.webnnFlagsURL
88
- ?`Set up NPU attempts to open the WebNN flags page in ${settings.name} and shows the full instructions.`
113
+ ?`Open the ${settings.name} WebNN flags link below to set up NPU access.`
89
114
  :'A matching WebNN flags address is not known for this browser. Choose Google Chrome or Microsoft Edge below if that is the browser you are using.';
90
115
  for(const choice of choices){
91
116
  choice.row.hidden=Boolean(settings.webnnFlagsURL)
92
117
  &&choice.target.url!==settings.webnnFlagsURL;
93
- choice.setupButton.textContent=settings.webnnFlagsURL
94
- ?'Set up NPU'
95
- :`Set up NPU in ${choice.target.name}`;
118
+ }
119
+ const gpuTarget=settings.highPerformanceGpu;
120
+ root.querySelector('#gpuFlagAddress').hidden=!gpuTarget;
121
+ root.querySelector('#gpuInstructions').textContent=gpuTarget
122
+ ?`If this computer has multiple GPUs, use the ${gpuTarget.name} flags link to request the high-performance GPU when available.`
123
+ :'The Force High Performance GPU flag is documented here for desktop Windows Chromium browsers. No matching flags link is known for this browser and platform.';
124
+ if(gpuTarget){
125
+ gpuFlagsLink.href=gpuTarget.url;
126
+ gpuFlagsLink.textContent=gpuTarget.url;
127
+ gpuFlagsLink.setAttribute('aria-label',`${gpuTarget.name} Force High Performance GPU flags address: ${gpuTarget.url}`);
128
+ }else{
129
+ gpuFlagsLink.removeAttribute('href');
130
+ gpuFlagsLink.textContent='';
96
131
  }
97
132
  return settings;
98
133
  }
99
134
 
135
+ function checkGpu(){
136
+ if(destroyed)return Promise.resolve(false);
137
+ if(gpuTask)return gpuTask;
138
+ gpuAvailability.textContent='Detecting GPU...';
139
+ gpuAvailability.dataset.status='pending';
140
+ gpuAdapterName.textContent='';
141
+ detectGpuButton.disabled=true;
142
+ gpuTask=detect().finally(function finishGpuDetection(){
143
+ gpuTask=null;
144
+ if(!destroyed)detectGpuButton.disabled=false;
145
+ });
146
+ return gpuTask;
147
+
148
+ async function detect(){
149
+ try{
150
+ const result=await detectBrowserGpu();
151
+ if(destroyed)return false;
152
+ gpuAvailability.textContent=result.available
153
+ ?result.isFallbackAdapter===true
154
+ ?'Fallback WebGPU adapter detected.'
155
+ :'GPU detected — WebGPU adapter available.'
156
+ :'No WebGPU adapter available to this page.';
157
+ gpuAvailability.dataset.status=result.available&&result.isFallbackAdapter!==true
158
+ ?'success':'warning';
159
+ gpuAdapterName.textContent=result.available?result.name:'';
160
+ return result;
161
+ }catch(error){
162
+ if(!destroyed){
163
+ gpuAvailability.textContent='GPU detection failed. Select Detect GPU to try again.';
164
+ gpuAvailability.dataset.status='error';
165
+ arcaneLogging.error('Unable to detect the browser GPU adapter.',error);
166
+ }
167
+ return false;
168
+ }
169
+ }
170
+ }
171
+
100
172
  function open(browserId){
101
173
  if(destroyed)return false;
102
174
  const current=refresh();
@@ -105,7 +177,7 @@
105
177
  return browser.browserId===selectedId;
106
178
  });
107
179
  if(!target){
108
- status.textContent='Choose the Google Chrome or Microsoft Edge setup button that matches your browser.';
180
+ status.textContent='Choose the Google Chrome or Microsoft Edge flags link that matches your browser.';
109
181
  return false;
110
182
  }
111
183
  const instructions=setupSteps.map(function numberSetupStep(step,index){
@@ -122,11 +194,11 @@
122
194
  if(destroyed)return Promise.resolve(false);
123
195
  if(copyTask)return copyTask;
124
196
  copyStatus.textContent='Copying address...';
125
- for(const choice of choices)choice.copyButton.disabled=true;
197
+ for(const button of copyButtons)button.disabled=true;
126
198
  copyTask=writeAddress().finally(function finishAddressCopy(){
127
199
  copyTask=null;
128
200
  if(!destroyed){
129
- for(const choice of choices)choice.copyButton.disabled=false;
201
+ for(const button of copyButtons)button.disabled=false;
130
202
  }
131
203
  });
132
204
  return copyTask;
@@ -140,7 +212,7 @@
140
212
  }catch(error){
141
213
  if(!destroyed){
142
214
  copyStatus.textContent='The address could not be copied automatically. Select the complete address above, copy it manually, and paste it into your browser\'s address bar.';
143
- arcaneLogging.error('Unable to copy the browser WebNN flags address.',error);
215
+ arcaneLogging.error('Unable to copy the browser flags address.',error);
144
216
  }
145
217
  return false;
146
218
  }
@@ -151,32 +223,34 @@
151
223
  for(const target of WEBNN_BROWSER_SETTINGS){
152
224
  const row=document.createElement('section');
153
225
  const heading=document.createElement('h3');
154
- const address=document.createElement('code');
226
+ const address=document.createElement('a');
155
227
  const actions=document.createElement('div');
156
- const setupButton=document.createElement('button');
157
228
  const copyButton=document.createElement('button');
158
229
  row.className='flag-choice';
159
230
  heading.textContent=target.name;
231
+ address.className='flag-address';
232
+ address.href=target.url;
160
233
  address.textContent=target.url;
161
- address.tabIndex=0;
162
- address.setAttribute('aria-label',`${target.name} flags address`);
234
+ address.setAttribute('aria-label',`${target.name} WebNN flags address: ${target.url}`);
163
235
  actions.className='actions';
164
- setupButton.type='button';
165
- setupButton.className='arcane-button';
166
- setupButton.addEventListener('click',function openSelectedBrowserSettings(){
167
- return open(target.browserId);
168
- },{signal:lifecycleController.signal});
169
236
  copyButton.type='button';
170
237
  copyButton.className='arcane-button arcane-button--secondary';
171
238
  copyButton.textContent=`Copy ${target.name} address`;
172
239
  copyButton.addEventListener('click',function copySelectedBrowserAddress(){
173
240
  return copyAddress(target.url);
174
241
  },{signal:lifecycleController.signal});
175
- actions.append(setupButton,copyButton);
242
+ actions.append(copyButton);
176
243
  row.append(heading,address,actions);
177
244
  addressList.append(row);
178
- choices.push({row,target,setupButton,copyButton});
245
+ choices.push({row,target});
246
+ copyButtons.push(copyButton);
179
247
  }
248
+
249
+ copyGpuButton.addEventListener('click',function copyGpuFlagsAddress(){
250
+ const target=getBrowserDeviceSettings().highPerformanceGpu;
251
+ if(target)return copyAddress(target.url);
252
+ },{signal:lifecycleController.signal});
253
+ detectGpuButton.addEventListener('click',checkGpu,{signal:lifecycleController.signal});
180
254
  const stepList=root.querySelector('#setupSteps');
181
255
  for(const step of setupSteps){
182
256
  const item=document.createElement('li');
@@ -187,6 +261,7 @@
187
261
  root.querySelector('#refreshAvailability').addEventListener('click',function refreshBrowserAvailability(){
188
262
  refresh();
189
263
  status.textContent='Browser API availability refreshed.';
264
+ return checkGpu();
190
265
  },{signal:lifecycleController.signal});
191
266
  host.ownerDocument.defaultView.addEventListener('pagehide',function disposeBeforePageUnload(event){
192
267
  if(!event.persisted)destroy();
@@ -204,6 +279,7 @@
204
279
  }
205
280
 
206
281
  refresh();
282
+ checkGpu();
207
283
  host.ready=true;
208
284
  const publication=events.dispatch('browser-ai-setup-ready',{}, {
209
285
  publicDetail:{ready:true}
package/src/cli/main.mjs CHANGED
@@ -35,6 +35,7 @@ const VALUE_OPTIONS=new Set([
35
35
  ]);
36
36
  const FLAG_OPTIONS=new Set([
37
37
  'git',
38
+ 'public',
38
39
  'skip-tests',
39
40
  'dry-run',
40
41
  'require-local-ai',
@@ -60,7 +61,7 @@ Usage:
60
61
  ${CLI_NAME} upgrade [--workspace <directory>] [--app <id>]
61
62
  ${CLI_NAME} doctor [--workspace <directory>] [--arcane-root <directory>]
62
63
  ${CLI_NAME} import-map [--workspace <directory>] [--app <id>]
63
- ${CLI_NAME} dev [--app <id>] [--host 127.0.0.1] [--port 8000] [--sdk-runtime-source <sdk-root>]
64
+ ${CLI_NAME} dev [--app <id>] [--public] [--host <address>] [--port 8000] [--sdk-runtime-source <sdk-root>]
64
65
  ${CLI_NAME} test [--app <id>] [--scope app]
65
66
  ${CLI_NAME} test --scope shared --test-file <repo-relative.test.mjs>
66
67
  ${CLI_NAME} check [--app <id>] [--scope app] [--skip-tests]
@@ -83,6 +84,8 @@ Usage:
83
84
  ${CLI_NAME} mail serve --profile <profile> --from <address> --app <id> --origin <origin> [--allow-to <addresses>] [--app-key-stdin] [--host 127.0.0.1] [--port 8025] [--request-timeout <ms>]
84
85
 
85
86
  Development:
87
+ --public Bind all IPv4 interfaces (0.0.0.0) and print network URLs.
88
+ --host <address> Override the bind address; takes precedence over --public.
86
89
  --sdk-runtime-source <sdk-root> Dev-only live SDK checkout; omitted preserves the workspace runtime mode.
87
90
 
88
91
  Global:
@@ -445,6 +448,9 @@ function operationOptions(command,parsed,cwd){
445
448
  if(values['sdk-runtime-source']!==undefined&&command!=='dev'){
446
449
  usage('--sdk-runtime-source is supported only by dev.');
447
450
  }
451
+ if(flags.has('public')&&command!=='dev'){
452
+ usage('--public is supported only by dev.');
453
+ }
448
454
  if(flags.has('overwrite')&&command!=='bundle'){
449
455
  usage('--overwrite is supported only by bundle.');
450
456
  }
@@ -506,7 +512,7 @@ function operationOptions(command,parsed,cwd){
506
512
  noExtraPositionals(command,positionals);
507
513
  return {
508
514
  ...common,
509
- host:values.host??'127.0.0.1',
515
+ host:values.host??(flags.has('public')?'0.0.0.0':'127.0.0.1'),
510
516
  port:readPort(values.port,8000),
511
517
  ...(values['sdk-runtime-source']===undefined?{}:{
512
518
  sdkRuntimeSourceRoot:path.resolve(cwd,values['sdk-runtime-source'])
@@ -831,6 +837,7 @@ function serverSummary(result){
831
837
  host:result.host,
832
838
  port:result.port,
833
839
  url:result.url,
840
+ ...(result.networkUrls===undefined?{}:{networkUrls:result.networkUrls}),
834
841
  ...(result.callerAuthentication
835
842
  ?{callerAuthentication:result.callerAuthentication}
836
843
  :{}),
@@ -841,7 +848,11 @@ function serverSummary(result){
841
848
  }
842
849
 
843
850
  async function waitForServer(result,signal,reporter){
844
- reporter.emit('server.ready',serverSummary(result),`Development server ready at ${result.url}`);
851
+ const readyMessage=[
852
+ `Development server ready at ${result.url}`,
853
+ ...(result.networkUrls??[]).map(function networkAddress(url){return `Network: ${url}`;})
854
+ ].join('\n');
855
+ reporter.emit('server.ready',serverSummary(result),readyMessage);
845
856
  if(result.lifecycle&&is.function(result.lifecycle.then)){
846
857
  const abort=()=>{
847
858
  void Promise.resolve().then(()=>result.close?.()).catch(()=>{});
@@ -2,6 +2,7 @@ import Is from 'strong-type';
2
2
  import {constants as FS_CONSTANTS} from 'node:fs';
3
3
  import {lstat,open,readFile,realpath} from 'node:fs/promises';
4
4
  import http from 'node:http';
5
+ import os from 'node:os';
5
6
  import path from 'node:path';
6
7
  import {resolveWorkspace} from './workspace.mjs';
7
8
  import {createEventQueue} from './event-queue.mjs';
@@ -489,6 +490,29 @@ function listen(server,{host,port,signal}){
489
490
  });
490
491
  }
491
492
 
493
+ function browserHostname(host){
494
+ return host.includes(':')?`[${host}]`:host;
495
+ }
496
+
497
+ function networkUrlsForAddress(address,startPath){
498
+ if(address.address==='127.0.0.1'||address.address==='::1')return [];
499
+ const allIPv4=address.address==='0.0.0.0';
500
+ const allInterfaces=address.address==='::';
501
+ const urls=new Set();
502
+ for(const entries of Object.values(os.networkInterfaces())){
503
+ for(const entry of entries??[]){
504
+ if(entry.internal)continue;
505
+ // Scoped IPv6 addresses need the receiving device's interface,
506
+ // so they cannot provide a portable browser link for another device.
507
+ if(entry.family==='IPv6'&&entry.scopeid)continue;
508
+ if(allIPv4&&entry.family!=='IPv4')continue;
509
+ if(!allIPv4&&!allInterfaces&&entry.address!==address.address)continue;
510
+ urls.add(`http://${browserHostname(entry.address)}:${address.port}${startPath}`);
511
+ }
512
+ }
513
+ return [...urls];
514
+ }
515
+
492
516
  async function startOwnedDevServer({
493
517
  workspaceRoot=process.cwd(),
494
518
  appId,
@@ -504,8 +528,8 @@ async function startOwnedDevServer({
504
528
  if(sdkRuntimeSourceRoot!==undefined&&mode!=='source'){
505
529
  fail('sdkRuntimeSourceRoot is supported only in source development mode.','ARCANE_USAGE');
506
530
  }
507
- if(host!=='127.0.0.1'&&host!=='::1'){
508
- fail('Development server host must be a numeric loopback address (127.0.0.1 or ::1).','ARCANE_POLICY_DENIED');
531
+ if(!is.string(host)||!host.trim()){
532
+ fail('Development server host must be a nonempty string.','ARCANE_USAGE');
509
533
  }
510
534
  if(!is.integer(port)||port<0||port>65535)fail('port must be an integer from 0 through 65535.','ARCANE_USAGE');
511
535
  const requestedRuntimeMode=mode==='source'&&sdkRuntimeSourceRoot!==undefined
@@ -631,7 +655,9 @@ async function startOwnedDevServer({
631
655
  server.close();
632
656
  fail('Development server did not expose a TCP address.');
633
657
  }
634
- const visibleHost=address.family==='IPv6'?`[${address.address}]`:address.address;
658
+ const visibleHost=address.address==='0.0.0.0'||address.address==='::'
659
+ ?'localhost'
660
+ :browserHostname(address.address);
635
661
  const endpoint=new URL(`http://${visibleHost}:${address.port}`);
636
662
  const origin=endpoint.origin;
637
663
  const cleanUrl=`${origin}${routeSet.startPath}`;
@@ -728,17 +754,20 @@ async function startOwnedDevServer({
728
754
  origin,
729
755
  cleanUrl,
730
756
  url,
757
+ networkUrls:[],
731
758
  close,
732
759
  closed:lifecycle,
733
760
  lifecycle
734
761
  };
735
762
  try{
763
+ result.networkUrls=networkUrlsForAddress(address,routeSet.startPath);
736
764
  await events.send({
737
765
  type:'server.started',
738
766
  mode,
739
767
  host:result.host,
740
768
  port:result.port,
741
769
  url,
770
+ networkUrls:result.networkUrls,
742
771
  appId:result.appId,
743
772
  ...(routeSet.runtime?{
744
773
  runtimeMode:routeSet.runtime.mode,