arcane-os 0.11.3 → 0.13.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.
@@ -39,7 +39,7 @@ for the installed-inventory-derived physical-runtime contract in SDK `0.5.18`.
39
39
  | `arcane-os/speech-playback` | Portable speech preparation, playback state, and injected media adapters. |
40
40
  | `arcane-os/speech-text` | Speech-input formatting cleanup for complete text and streamed chunks. |
41
41
  | `arcane-os/browser-device` | Synchronous mobile or desktop identity hints for application-owned settings. |
42
- | `arcane-os/pwa` | Nonblocking PWA registration, native worker updates and observable lifecycle state. |
42
+ | `arcane-os/pwa` | Nonblocking PWA registration, worker updates, native installation state and a dismissible installation component. |
43
43
  | `arcane-os/ai/browser-wasm` | Caller-selected browser-local Wllama inference, complete DBOPFS model storage, streaming, cancellation, and structural tool-call results. |
44
44
  | `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS provider mechanisms, ordinary upstream assets, materialized/native routing, Workers, and cancellation. |
45
45
  | `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
@@ -129,6 +129,9 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
129
129
  | `getBrowserDeviceClass()` | function | `arcane-os/browser-device` | Browser device settings | Node and Browser; synchronous identity hint with no model, storage, or GPU operation |
130
130
  | `PWA_STATE_EVENT` | constant | `arcane-os/pwa` | Progressive web applications | Browser lifecycle event name; importable without registration |
131
131
  | `registerPwa()` | function | `arcane-os/pwa` | Progressive web applications | Browser service workers; synchronous unsupported state when unavailable |
132
+ | `PWA_INSTALL_STATE_EVENT` | constant | `arcane-os/pwa` | Progressive web applications | Browser installation event name; importable without starting observation |
133
+ | `getPwaInstall()` | function | `arcane-os/pwa` | Progressive web applications | Browser native installation events; waiting state while no prompt is available |
134
+ | `mountPwaInstallPrompt()` | function | `arcane-os/pwa` | Progressive web applications | Browser document and managed HTML import; resolves to null without a document |
132
135
  | `createDbopfsSpeechArtifactStore()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser with ready DBOPFS, Web Locks, Fetch, File/Blob, and object URLs |
133
136
  | `removeBrowserSpeechModelCache()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser CacheStorage; explicit removal of one selected upstream model |
134
137
  | `createNativeBuildPlan()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
@@ -3459,6 +3462,15 @@ async function useselectApp(...arguments_) {
3459
3462
 
3460
3463
  Starts one owned browser development server with exact runtime/app route mappings and a caller-selected bind address.
3461
3464
 
3465
+ HTTPS serving uses the published `node-http-server` module. The SDK
3466
+ selects source routes and supplies generated representations; the module owns
3467
+ static-file conditional GET/HEAD handling and response delivery. The SDK
3468
+ retains modification dates for its generated representations. Unchanged
3469
+ resources can return `304` with no body while changed resources return their
3470
+ complete current representation. PEM-path listeners use module deployment;
3471
+ explicit raw `tls` options retain native HTTPS transport with the same module
3472
+ response and static-file operations.
3473
+
3462
3474
  ### Signature, modes, and result
3463
3475
 
3464
3476
  ```text
@@ -3466,53 +3478,64 @@ async startDevServer(options={})
3466
3478
  ```
3467
3479
 
3468
3480
  Import it from `arcane-os`. Source mode accepts
3469
- `{workspaceRoot=process.cwd(), appId, mode='source', host='127.0.0.1', port=0,
3470
- https=false, certPath, keyPath, tls, signal, onEvent}` and serves one validated
3481
+ `{workspaceRoot=process.cwd(), appId, mode='source', host='127.0.0.1', port=0, httpPort=0,
3482
+ certPath, keyPath, tls, signal, onEvent}` and serves one validated
3471
3483
  workspace application plus its complete SDK or integrated runtime. Packaged mode uses
3472
- `{mode:'packaged', releaseRoot, host, port, signal, onEvent}` and serves the
3473
- complete selected release files. `host` defaults to `127.0.0.1` and accepts an
3484
+ `{mode:'packaged', releaseRoot, workspaceRoot, host, port, httpPort, certPath, keyPath, tls,
3485
+ signal, onEvent}` and serves the complete selected release files.
3486
+ `host` defaults to `127.0.0.1` and accepts an
3474
3487
  explicit network address or hostname. Use `0.0.0.0` for all IPv4 interfaces or
3475
- `::` for the platform's IPv6 wildcard listener; port `0` asks the operating
3476
- system for an available port.
3477
-
3478
- `https:true` reads `.arcane/dev/server-cert.pem` and
3488
+ `::` for the platform's IPv6 wildcard listeners. `port` selects the HTTPS
3489
+ application port, and `httpPort` selects the paired HTTP redirect port. Each
3490
+ defaults to `0`, which asks the operating system for an available port.
3491
+ The SDK request hook returns `308` for HTTP with a `Location` pointing to the
3492
+ actual HTTPS port while preserving the original request path and query. Both listeners
3493
+ use the selected host.
3494
+
3495
+ Every source server and packaged browser preview enforces HTTPS, including
3496
+ localhost. The legacy `https` option is accepted but cannot disable it.
3497
+ Startup reads `.arcane/dev/server-cert.pem` and
3479
3498
  `.arcane/dev/server-key.pem` relative to `workspaceRoot` unless explicit
3480
- `certPath` and `keyPath` are supplied together. The path pair also selects
3481
- HTTPS without `https:true`; relative paths resolve from the workspace. A
3482
- direct `tls` object instead supplies Node HTTPS server options, including
3499
+ `certPath` and `keyPath` are supplied together; relative paths resolve from the
3500
+ workspace. A direct `tls` object instead supplies Node HTTPS server options, including
3483
3501
  `cert` and `key`, without reading certificate files. Keep private material
3484
3502
  server-side. Missing PEM files and Node certificate/key parse errors reject
3485
3503
  startup without falling back to HTTP. Node owns TLS option handling and the
3486
3504
  handshake; browser trust and address matching are evaluated when a client
3487
- connects. The CLI's `--public` selects HTTPS and the wildcard bind;
3505
+ connects. The CLI's `--public` selects the wildcard bind;
3488
3506
  the API's `host` option alone changes only the bind address.
3489
3507
 
3490
- The promise settles after the listener is ready and resolves to
3508
+ The promise settles after both listeners are ready and resolves to
3491
3509
  `{server, protocol, mode, workspaceRoot, appId, host, port, origin, cleanUrl, url,
3492
- networkUrls, close, closed, lifecycle}`. `server` is the raw Node HTTP or HTTPS
3493
- server; `protocol` is `'http:'` or `'https:'`.
3510
+ networkUrls, httpPort, httpOrigin, httpUrl, close, closed, lifecycle}`. `server` is the raw Node HTTPS
3511
+ server; `protocol` is `'https:'`.
3494
3512
  `url` and `cleanUrl` are the same application URL. Wildcard listeners use
3495
3513
  `localhost` in that local URL; `host` retains the actual bound address.
3514
+ `httpPort` is the actual HTTP listener port, `httpOrigin` is its HTTP origin,
3515
+ and `httpUrl` combines that origin with the application start path. These
3516
+ fields identify the redirect endpoint; `origin`, `url`, `cleanUrl`, and
3517
+ `networkUrls` identify HTTPS application endpoints.
3496
3518
  `networkUrls` lists application URLs for applicable non-loopback interface
3497
3519
  addresses discovered once at startup. These URLs are connection candidates,
3498
3520
  not evidence of reachability from another device. The server adds no session
3499
3521
  capability or authentication. In packaged mode, `workspaceRoot` and `appId`
3500
3522
  are `null`.
3501
3523
 
3502
- All returned application URLs use the selected transport's scheme. The server
3503
- reads one PEM pair per startup and does not create certificates or modify trust
3504
- stores. Each client must trust the issuing CA and open an address covered by the
3524
+ PEM-path startup reads the selected pair. The server does not create
3525
+ certificates or modify trust stores. Each client must trust the issuing CA and open an address covered by the
3505
3526
  server certificate. Lifecycle events and CLI summaries exclude TLS options and
3506
3527
  private key contents. See [development HTTPS setup](cli.md#development-https-setup).
3507
3528
 
3508
- Starting the server opens the selected listener and emits awaited,
3509
- backpressured `server.starting` and `server.started` events. Request failures
3510
- emit `server.request.failed`; shutdown emits `server.stopped` after owned
3529
+ Starting the server opens both selected listeners and emits awaited,
3530
+ backpressured `server.starting` and `server.started` events. `server.started`
3531
+ includes `httpPort`, `httpOrigin`, and `httpUrl` alongside the HTTPS endpoint.
3532
+ Request failures emit `server.request.failed`; shutdown emits `server.stopped` after owned
3511
3533
  requests and event delivery drain. Call `await result.close()` in a `finally`
3512
3534
  block, or abort `signal`; `close()` is idempotent and returns the
3513
- same settlement represented by both `closed` and `lifecycle`. A listener error
3514
- or event-callback failure closes the server and rejects its lifecycle. Invalid
3515
- mode/host/port, malformed workspace or release content, an occupied port,
3535
+ same settlement represented by both `closed` and `lifecycle`. Closing the
3536
+ operation closes both listeners. An error from either listener or an
3537
+ event-callback failure closes both and rejects the lifecycle. Invalid
3538
+ mode/host/port/httpPort, malformed workspace or release content, an occupied port,
3516
3539
  or an already-aborted signal rejects startup.
3517
3540
 
3518
3541
  ### Availability and normalization
@@ -3907,9 +3930,16 @@ async function usedescribeTargets(...arguments_) {
3907
3930
  Starts one owned browser development server for the selected application.
3908
3931
 
3909
3932
  `https`, `certPath`, `keyPath`, and `tls` follow the
3910
- [`startDevServer()` TLS contract](#startdevserver), alongside `host` and `port`.
3911
- The operation refreshes the selected app's managed import maps once, then owns
3912
- one source listener and returns its protocol, URLs, and shutdown lifecycle.
3933
+ [`startDevServer()` TLS contract](#startdevserver), alongside `host`, `port`, and
3934
+ `httpPort`. `port` selects HTTPS and `httpPort` selects the HTTP `308` redirect
3935
+ listener; each defaults to an available port.
3936
+ The operation refreshes the selected authored descriptor's `arcane-package.json`
3937
+ projection and managed import maps under one development-refresh lock, then
3938
+ releases that lock before opening the HTTPS source listener and its HTTP
3939
+ redirect listener. It returns both endpoints and their shared shutdown
3940
+ lifecycle. Legacy package-only apps remain unchanged. The operation generates
3941
+ no packaged output; enabled PWA manifests
3942
+ are served directly from the selected source resources.
3913
3943
 
3914
3944
  ### Signature and result
3915
3945
 
@@ -6075,6 +6105,13 @@ registration and returns a synchronous owner with `ready`, `state`, `subscribe`,
6075
6105
  `update` and `dispose`. It does not block page rendering or load models.
6076
6106
  Unsupported environments receive an explicit unsupported state.
6077
6107
 
6108
+ The generated page bootstrap also starts the SDK's background resource check.
6109
+ It stores one `lastChecked` timestamp per app/cache in DBOPFS, advancing it only
6110
+ after the full resource check succeeds, and revalidates on page load after
6111
+ 120 seconds in development or 15 minutes in packaged delivery. Complete
6112
+ responses remain cached without SDK expiration; `304` retains the response
6113
+ and a successful replacement updates it. See the [PWA guide](pwa.md).
6114
+
6078
6115
  ### Example
6079
6116
 
6080
6117
  ```javascript
@@ -6090,6 +6127,99 @@ current-state replay, disposal, offline resource ownership and update behavior.
6090
6127
  The generated bootstrap registers automatically; a manual caller owns a separate
6091
6128
  entry point and must observe its `ready` rejection.
6092
6129
 
6130
+ ## PWA_INSTALL_STATE_EVENT
6131
+
6132
+ ### Overview
6133
+
6134
+ The exact event name `arcane.pwa.install.state` identifies native installation
6135
+ availability and choice state through the existing Arcane event owner. It is
6136
+ separate from service-worker registration state. Importing it starts no
6137
+ observation or installation.
6138
+
6139
+ ### Example
6140
+
6141
+ ```javascript
6142
+ import {PWA_INSTALL_STATE_EVENT} from 'arcane-os/pwa';
6143
+ console.log(PWA_INSTALL_STATE_EVENT);
6144
+ ```
6145
+
6146
+ See the [installation state contract](pwa.md#getpwainstall) for the complete
6147
+ payload and current-state subscription behavior.
6148
+
6149
+ ## getPwaInstall()
6150
+
6151
+ ### Overview
6152
+
6153
+ `getPwaInstall()` returns one synchronous page owner exposing `state`,
6154
+ `subscribe`, `prompt`, `dismiss` and `dispose`. It captures the browser's
6155
+ `beforeinstallprompt` event and observes `appinstalled` and app display-mode
6156
+ changes. A missing event leaves installation availability unknown and the owner
6157
+ waiting; it does not prove browser incompatibility.
6158
+
6159
+ State contains `status`, `available`, `dismissed`, `outcome` and `error`.
6160
+ Subscriptions replay current state by default. `prompt()` must be called
6161
+ directly within the install click to preserve native user activation. It
6162
+ consumes the event once and returns the browser choice, resolves to `null` when
6163
+ unavailable, or rejects with the actual error. `dismiss()` remembers the
6164
+ session's floating-suggestion dismissal without consuming a retained event.
6165
+
6166
+ ### Example
6167
+
6168
+ ```javascript
6169
+ import {getPwaInstall} from 'arcane-os/pwa';
6170
+
6171
+ const install = getPwaInstall();
6172
+ const button = document.querySelector('#install');
6173
+ install.subscribe(function updateInstallButton(state) {
6174
+ button.hidden = !state.available;
6175
+ });
6176
+ button.addEventListener('click', function requestInstallation() {
6177
+ install.prompt().catch(function reportInstallFailure(error) {
6178
+ console.error(error);
6179
+ });
6180
+ });
6181
+ ```
6182
+
6183
+ The [PWA guide](pwa.md#getpwainstall) defines every status, event lifetime,
6184
+ session dismissal, errors and disposal. Disposing this shared owner removes
6185
+ its page-level observation; components normally unsubscribe only their own
6186
+ listener. Installation state does not establish offline readiness or completed
6187
+ Android WebAPK creation.
6188
+
6189
+ ## mountPwaInstallPrompt()
6190
+
6191
+ ### Overview
6192
+
6193
+ `mountPwaInstallPrompt({appName = ''} = {})` starts the shared install observer
6194
+ before asynchronously mounting one initially hidden `pwa-install.html`
6195
+ component. Repeated calls return the same mounting promise. It resolves to the
6196
+ ready `html-import` host, or `null` without a document or when the owner is
6197
+ disposed before mounting. Component-load failure rejects; removal or disposal
6198
+ during loading rejects with `AbortError`. A rejected mount permits another
6199
+ explicit attempt. The first call
6200
+ supplies the initial app name.
6201
+
6202
+ The generated PWA bootstrap calls this automatically with the manifest name.
6203
+ Component mounting and worker registration run independently, and application
6204
+ rendering must not await them. The floating component offers Install and a
6205
+ manual close control; an application can also place the same component inline.
6206
+
6207
+ ### Example
6208
+
6209
+ ```javascript
6210
+ import {mountPwaInstallPrompt} from 'arcane-os/pwa';
6211
+
6212
+ mountPwaInstallPrompt({appName: 'Example Library'}).catch(
6213
+ function reportInstallComponentFailure(error) {
6214
+ console.error(error);
6215
+ }
6216
+ );
6217
+ ```
6218
+
6219
+ See the [mounting contract](pwa.md#mountpwainstallprompt) and
6220
+ [component reference](runtime-components.md#pwa-installhtml). The browser owns
6221
+ native installation eligibility and URL-bar promotion.
6222
+
6093
6223
  ## createBrowserWasmLlmProvider()
6094
6224
 
6095
6225
  ### Overview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.11.3",
3
+ "version": "0.13.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",
@@ -67,7 +67,7 @@
67
67
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
68
68
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
69
69
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
70
- "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
70
+ "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
71
71
  "test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
72
72
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
73
73
  "check": "node tools/check-source.mjs && npm test",
@@ -105,6 +105,7 @@
105
105
  },
106
106
  "dependencies": {
107
107
  "event-pubsub": "6.1.0",
108
+ "node-http-server": "9.1.1",
108
109
  "strong-type": "2.0.1",
109
110
  "vanilla-test": "2.1.3"
110
111
  },
@@ -0,0 +1,216 @@
1
+ <link rel="stylesheet" href="./arcane/css/theme.css?v=1">
2
+ <link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
3
+ <style>
4
+ :host{
5
+ display:block;
6
+ inset-block-start:4.5rem;
7
+ inset-inline-end:1rem;
8
+ max-width:calc(100vw - 2rem);
9
+ min-width:0;
10
+ position:fixed;
11
+ width:22rem;
12
+ z-index:110;
13
+ }
14
+ :host([hidden]){display:none}
15
+ :host([data-presentation="inline"]){
16
+ max-width:100%;
17
+ position:static;
18
+ width:fit-content;
19
+ }
20
+ .panel{
21
+ background:var(--arcane-surface-raised);
22
+ border:1px solid var(--arcane-border);
23
+ border-radius:var(--arcane-radius-medium);
24
+ box-shadow:var(--arcane-shadow-medium);
25
+ box-sizing:border-box;
26
+ color:var(--text-color);
27
+ display:grid;
28
+ gap:var(--arcane-space-2);
29
+ max-height:calc(100vh - 5.5rem);
30
+ max-height:calc(100dvh - 5.5rem);
31
+ overflow:auto;
32
+ padding:var(--arcane-space-3);
33
+ }
34
+ .heading{align-items:start;display:flex;gap:var(--arcane-space-2)}
35
+ .copy{flex:1;min-width:0}
36
+ h2,p{margin:0;overflow-wrap:anywhere;white-space:pre-wrap}
37
+ h2{font-size:1rem;line-height:1.4}
38
+ .description{color:var(--arcane-muted);font-size:.9rem;line-height:1.45}
39
+ .close{flex:none}
40
+ .install{justify-self:start;max-width:100%;overflow-wrap:anywhere;white-space:normal}
41
+ .status{font-size:.9rem;line-height:1.45}
42
+ .status[data-error="true"]{color:var(--arcane-danger)}
43
+ :host([data-presentation="inline"]) .panel{box-shadow:none}
44
+ @media(prefers-reduced-motion:reduce){.install{transition:none}}
45
+ @media(forced-colors:active){.panel,.install,.close{border:1px solid CanvasText}}
46
+ </style>
47
+
48
+ <section class="panel" aria-labelledby="installTitle">
49
+ <div class="heading">
50
+ <div class="copy">
51
+ <h2 id="installTitle">Install this app</h2>
52
+ <p id="description" class="description">Open this app directly from your device.</p>
53
+ </div>
54
+ <button id="close" class="close arcane-close-button" type="button" aria-label="Close installation suggestion">&#x2715;</button>
55
+ </div>
56
+ <button id="install" class="install arcane-button" type="button">Install</button>
57
+ <p id="status" class="status" role="status" aria-live="polite" hidden></p>
58
+ </section>
59
+
60
+ <script type="module">
61
+ const host=this;
62
+ host.hidden=true;
63
+ const [
64
+ {default:Is},
65
+ {getPwaInstall},
66
+ {createArcaneEventSource,projectArcaneDOMEvent},
67
+ {arcaneLogging}
68
+ ]=await Promise.all([
69
+ import('strong-type'),
70
+ import('arcane-os/pwa'),
71
+ import('arcane-os/event-manager'),
72
+ import('arcane-os/logging')
73
+ ]);
74
+ const is=new Is(false);
75
+ const root=host.shadowRoot;
76
+ const title=root.querySelector('#installTitle');
77
+ const description=root.querySelector('#description');
78
+ const installButton=root.querySelector('#install');
79
+ const closeButton=root.querySelector('#close');
80
+ const status=root.querySelector('#status');
81
+ const lifecycleController=new AbortController();
82
+ const events=createArcaneEventSource(host,{
83
+ source:'arcane.component.pwa-install',
84
+ eventTypes:['pwa-install-ready','pwa-install-change','pwa-install-dismissed']
85
+ });
86
+ const owner=getPwaInstall();
87
+ let options={
88
+ appName:host.dataset.appName||'this app',
89
+ installLabel:'Install',
90
+ closeLabel:'Close installation suggestion',
91
+ promptingLabel:'Choose an option in the browser installation prompt.',
92
+ description:'Open this app directly from your device.',
93
+ presentation:host.dataset.presentation||'floating'
94
+ };
95
+ let currentState=owner.state;
96
+ let locallyDismissed=false;
97
+ let pending=false;
98
+ let requestError=null;
99
+ let destroyed=false;
100
+
101
+ host.configure=configure;
102
+ host.install=install;
103
+ host.dismiss=dismiss;
104
+ host.destroy=destroy;
105
+ Object.defineProperty(host,'state',{configurable:true,get:getState});
106
+
107
+ function emit(type,detail){
108
+ if(destroyed)return false;
109
+ const publication=events.dispatch(type,detail);
110
+ return projectArcaneDOMEvent(host,publication.occurrence,{bubbles:true,composed:true});
111
+ }
112
+
113
+ function getState(){
114
+ return {...currentState};
115
+ }
116
+
117
+ function configure(input={}){
118
+ if(destroyed)return false;
119
+ if(input===null||!is.object(input)||is.array(input)){
120
+ throw new TypeError('PWA install configuration must be an object.');
121
+ }
122
+ const next={...options,...input};
123
+ for(const key of ['appName','installLabel','closeLabel','promptingLabel','description']){
124
+ if(!is.string(next[key]))throw new TypeError(`PWA install ${key} must be a string.`);
125
+ }
126
+ if(next.presentation!=='floating'&&next.presentation!=='inline'){
127
+ throw new TypeError('PWA install presentation must be floating or inline.');
128
+ }
129
+ options=next;
130
+ host.dataset.presentation=options.presentation;
131
+ title.textContent=`Install ${options.appName}`;
132
+ description.textContent=options.description;
133
+ installButton.textContent=options.installLabel;
134
+ closeButton.setAttribute('aria-label',options.closeLabel);
135
+ render();
136
+ return {...options};
137
+ }
138
+
139
+ function render(){
140
+ if(destroyed)return;
141
+ const terminal=['accepted','installed','running','disposed'].includes(currentState.status);
142
+ if(terminal)requestError=null;
143
+ const dismissed=locallyDismissed
144
+ ||(options.presentation==='floating'&&currentState.dismissed);
145
+ host.hidden=terminal||dismissed||(!currentState.available&&!pending&&!requestError);
146
+ installButton.disabled=pending||!currentState.available;
147
+ const message=requestError
148
+ ?is.string(requestError.message)?requestError.message:String(requestError)
149
+ :pending?options.promptingLabel:'';
150
+ status.textContent=message;
151
+ status.hidden=!message;
152
+ status.dataset.error=String(Boolean(requestError));
153
+ }
154
+
155
+ function observeInstallState(state){
156
+ if(destroyed)return;
157
+ currentState=state;
158
+ render();
159
+ emit('pwa-install-change',{state:getState(),visible:!host.hidden});
160
+ }
161
+
162
+ function install(){
163
+ if(destroyed||pending||!currentState.available)return Promise.resolve(null);
164
+ locallyDismissed=false;
165
+ requestError=null;
166
+ pending=true;
167
+ render();
168
+ let operation;
169
+ try{
170
+ // Keep the browser prompt inside the initiating click's user activation.
171
+ operation=owner.prompt();
172
+ }catch(error){
173
+ operation=Promise.reject(error);
174
+ }
175
+ return Promise.resolve(operation).catch(function reportPromptError(error){
176
+ if(!destroyed)requestError=error;
177
+ throw error;
178
+ }).finally(function settlePrompt(){
179
+ if(destroyed)return;
180
+ pending=false;
181
+ render();
182
+ });
183
+ }
184
+
185
+ function dismiss(){
186
+ if(destroyed)return false;
187
+ locallyDismissed=true;
188
+ requestError=null;
189
+ if(options.presentation==='floating')owner.dismiss();
190
+ render();
191
+ emit('pwa-install-dismissed',{presentation:options.presentation,state:getState()});
192
+ return true;
193
+ }
194
+
195
+ function destroy(){
196
+ if(destroyed)return false;
197
+ destroyed=true;
198
+ lifecycleController.abort('pwa-install-destroyed');
199
+ events.dispose();
200
+ host.hidden=true;
201
+ host.ready=false;
202
+ return true;
203
+ }
204
+
205
+ installButton.addEventListener('click',function requestInstallation(){
206
+ install().catch(function reportInstallationFailure(error){
207
+ arcaneLogging.error('Unable to open the browser installation prompt:',error);
208
+ });
209
+ },{signal:lifecycleController.signal});
210
+ closeButton.addEventListener('click',dismiss,{signal:lifecycleController.signal});
211
+
212
+ configure();
213
+ owner.subscribe(observeInstallState,{signal:lifecycleController.signal});
214
+ host.ready=true;
215
+ emit('pwa-install-ready',{ready:true,state:getState()});
216
+ </script>
@@ -1,7 +1,8 @@
1
1
  import Is from 'strong-type';
2
2
  import {isDeepStrictEqual} from 'node:util';
3
- import {readFile} from 'node:fs/promises';
3
+ import {readFile, writeFile} from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import {throwIfAborted} from './errors.mjs';
5
6
  import {normalizePwaConfig} from './pwa.mjs';
6
7
  import {
7
8
  ARCANE_MACHINE_BUNDLE_VERSION,
@@ -424,6 +425,47 @@ async function readJsonFile(filePath,label,{optional=false}={}){
424
425
  }
425
426
  }
426
427
 
428
+ export async function refreshAppPackageProjection({workspaceRoot, appId, signal, onEvent}) {
429
+ throwIfAborted(signal);
430
+ const appRoot = path.join(workspaceRoot, 'apps', appId);
431
+ const descriptorPath = path.join(appRoot, APP_DESCRIPTOR_NAME);
432
+ const authored = await readJsonFile(
433
+ descriptorPath,
434
+ `apps/${appId}/${APP_DESCRIPTOR_NAME}`,
435
+ {optional: true}
436
+ );
437
+ if (authored === null) return {updated: false};
438
+
439
+ validateAppDescriptor(
440
+ authored,
441
+ {appId}
442
+ );
443
+ const projection = projectPackageManifest(authored);
444
+ const packagePath = path.join(appRoot, 'arcane-package.json');
445
+ const packageManifest = await readJsonFile(
446
+ packagePath,
447
+ `apps/${appId}/arcane-package.json`
448
+ );
449
+ const packageProjection = packageManifest.pwa === undefined ? packageManifest : {
450
+ ...packageManifest,
451
+ pwa: normalizePwaConfig(packageManifest.pwa)
452
+ };
453
+ if (isDeepStrictEqual(projection, packageProjection)) return {updated: false};
454
+
455
+ // Development updates the generated projection before strict workspace readers run.
456
+ throwIfAborted(signal);
457
+ await writeFile(packagePath, `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
458
+ await onEvent?.(
459
+ {
460
+ type: 'workspace.application.projection.updated',
461
+ workspaceRoot,
462
+ appId,
463
+ path: packagePath
464
+ }
465
+ );
466
+ return {updated: true};
467
+ }
468
+
427
469
  export async function loadAppDescriptor({workspaceRoot,appRoot,appId,packageManifest}){
428
470
  const descriptorPath=path.join(appRoot,APP_DESCRIPTOR_NAME);
429
471
  const authored=await readJsonFile(descriptorPath,`apps/${appId}/${APP_DESCRIPTOR_NAME}`,{optional:true});