arcane-os 0.16.1 → 0.16.3
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 +21 -0
- package/README.md +1 -1
- package/browser-runtime/pwa-install.mjs +99 -7
- package/docs/reference/pwa.md +35 -7
- package/docs/reference/runtime-components.md +6 -0
- package/docs/reference/sdk-api.md +13 -2
- package/package.json +2 -2
- package/src/workspace.mjs +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.3
|
|
4
|
+
|
|
5
|
+
- Remember browser-reported PWA installation in app-scoped DBOPFS and suppress
|
|
6
|
+
the SDK's floating and inline installation controls on later visits. Capture
|
|
7
|
+
native events immediately while restoring the saved flag; keep rendering,
|
|
8
|
+
component loading and worker registration independent.
|
|
9
|
+
- Preserve confirmed installation across late reads, prompt results and display
|
|
10
|
+
changes. Save installed-app launches as well as `appinstalled`, without
|
|
11
|
+
mistaking prompt acceptance or ordinary fullscreen for installation. Expose
|
|
12
|
+
initial storage readiness and complete persistence errors through the shared
|
|
13
|
+
install owner, and retain confirmed-install writes through owner disposal.
|
|
14
|
+
|
|
15
|
+
## 0.16.2
|
|
16
|
+
|
|
17
|
+
- Scope explicit application selection and workspace resolution to the named
|
|
18
|
+
app before reading its descriptor. An unrelated invalid descriptor no longer
|
|
19
|
+
blocks that operation. Selected descriptors and unscoped discovery retain
|
|
20
|
+
their existing validation. Named resolution reports only the selected app in
|
|
21
|
+
`appIds`; unknown selections report the requested missing identifier without
|
|
22
|
+
validating or listing unrelated apps.
|
|
23
|
+
|
|
3
24
|
## 0.16.1
|
|
4
25
|
|
|
5
26
|
- Remove raw script elements and inline event-handler attributes at the
|
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.16.
|
|
22
|
+
This checkout defines the `0.16.2` 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
|
|
|
@@ -11,7 +11,11 @@ export function getPwaInstall() {
|
|
|
11
11
|
if (sharedOwner && sharedOwner.state.status !== 'disposed') {
|
|
12
12
|
return sharedOwner;
|
|
13
13
|
}
|
|
14
|
-
const owner = {
|
|
14
|
+
const owner = {
|
|
15
|
+
get state() { return snapshot(); },
|
|
16
|
+
get ready() { return ready; },
|
|
17
|
+
subscribe, prompt, dismiss, dispose
|
|
18
|
+
};
|
|
15
19
|
const source = createArcaneEventSource(owner, {
|
|
16
20
|
source: 'arcane.pwa.install', eventTypes: [PWA_INSTALL_STATE_EVENT]
|
|
17
21
|
});
|
|
@@ -20,6 +24,9 @@ export function getPwaInstall() {
|
|
|
20
24
|
'(display-mode: standalone), (display-mode: minimal-ui), '
|
|
21
25
|
+ '(display-mode: fullscreen), (display-mode: window-controls-overlay)'
|
|
22
26
|
);
|
|
27
|
+
const installedDisplayMode = globalThis.matchMedia?.(
|
|
28
|
+
'(display-mode: standalone), (display-mode: minimal-ui), (display-mode: window-controls-overlay)'
|
|
29
|
+
);
|
|
23
30
|
const manifestUrl = globalThis.document?.querySelector('link[rel~="manifest"]')?.href;
|
|
24
31
|
const dismissalKey = `arcane.pwa.install.dismissed:${manifestUrl ?? globalThis.location?.href ?? ''}`;
|
|
25
32
|
let deferredPrompt = null;
|
|
@@ -28,6 +35,11 @@ export function getPwaInstall() {
|
|
|
28
35
|
let status = isRunningAsApp() ? 'running' : 'waiting';
|
|
29
36
|
let outcome = null;
|
|
30
37
|
let error = null;
|
|
38
|
+
let installed = isInstalledApp();
|
|
39
|
+
let storedInstalled = false;
|
|
40
|
+
let storageReady = false;
|
|
41
|
+
let storageError = null;
|
|
42
|
+
let saveTask = null;
|
|
31
43
|
try {
|
|
32
44
|
dismissed = globalThis.sessionStorage?.getItem(dismissalKey) === 'true';
|
|
33
45
|
} catch (storageError) {
|
|
@@ -38,9 +50,17 @@ export function getPwaInstall() {
|
|
|
38
50
|
return displayMode?.matches === true || globalThis.navigator?.standalone === true;
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
function isInstalledApp() {
|
|
54
|
+
// Ordinary browser fullscreen is not evidence of installation.
|
|
55
|
+
return installedDisplayMode?.matches === true || globalThis.navigator?.standalone === true;
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
function snapshot() {
|
|
42
|
-
return {
|
|
43
|
-
|
|
59
|
+
return {
|
|
60
|
+
status,
|
|
61
|
+
available: storageReady && deferredPrompt !== null && !installed && !isRunningAsApp() && !disposed,
|
|
62
|
+
installed, dismissed, outcome, error, storageError
|
|
63
|
+
};
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
function publish(nextStatus, nextError = null) {
|
|
@@ -67,24 +87,92 @@ export function getPwaInstall() {
|
|
|
67
87
|
}
|
|
68
88
|
}
|
|
69
89
|
|
|
90
|
+
async function loadInstallStorage() {
|
|
91
|
+
if (!globalThis.dbopfs) {
|
|
92
|
+
await import('arcane/DBOPFS');
|
|
93
|
+
}
|
|
94
|
+
const storage = globalThis.dbopfs;
|
|
95
|
+
if (!storage) {
|
|
96
|
+
throw new Error('PWA installation state could not open DBOPFS.');
|
|
97
|
+
}
|
|
98
|
+
await storage.readyPromise;
|
|
99
|
+
return storage;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function reportStorageError(failure) {
|
|
103
|
+
storageError = failure;
|
|
104
|
+
console.warn('Arcane PWA installation state could not be persisted or restored:', failure);
|
|
105
|
+
publish(status, error);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function restoreInstallation() {
|
|
109
|
+
try {
|
|
110
|
+
const storage = await storageTask;
|
|
111
|
+
const record = await storage.get('pwa', 'installed.json', true);
|
|
112
|
+
storedInstalled = record?.installed === true;
|
|
113
|
+
if (!disposed && storedInstalled) {
|
|
114
|
+
installed = true;
|
|
115
|
+
deferredPrompt = null;
|
|
116
|
+
}
|
|
117
|
+
} catch (failure) {
|
|
118
|
+
reportStorageError(failure);
|
|
119
|
+
}
|
|
120
|
+
storageReady = true;
|
|
121
|
+
if (!disposed) {
|
|
122
|
+
if (installed) {
|
|
123
|
+
publish(isRunningAsApp() ? 'running' : 'installed', error);
|
|
124
|
+
} else if (deferredPrompt && !isRunningAsApp()) {
|
|
125
|
+
publish('available', error);
|
|
126
|
+
} else {
|
|
127
|
+
publish(status, error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return snapshot();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function saveInstallation() {
|
|
134
|
+
// The initial read avoids rewriting an already remembered installation.
|
|
135
|
+
await ready;
|
|
136
|
+
if (storedInstalled) return;
|
|
137
|
+
const storage = await storageTask;
|
|
138
|
+
await storage.set(
|
|
139
|
+
'pwa',
|
|
140
|
+
'installed.json',
|
|
141
|
+
{installed: true}
|
|
142
|
+
);
|
|
143
|
+
storedInstalled = true;
|
|
144
|
+
storageError = null;
|
|
145
|
+
publish(status, error);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rememberInstallation() {
|
|
149
|
+
installed = true;
|
|
150
|
+
deferredPrompt = null;
|
|
151
|
+
// Retain and observe this durable write even if the page owner detaches.
|
|
152
|
+
saveTask ??= saveInstallation().catch(reportStorageError);
|
|
153
|
+
}
|
|
154
|
+
|
|
70
155
|
function onBeforeInstallPrompt(event) {
|
|
71
|
-
if (disposed || isRunningAsApp() || status === '
|
|
156
|
+
if (disposed || installed || isRunningAsApp() || status === 'accepted') return;
|
|
72
157
|
event.preventDefault();
|
|
73
158
|
deferredPrompt = event;
|
|
74
159
|
outcome = null;
|
|
75
|
-
publish('available');
|
|
160
|
+
publish(storageReady ? 'available' : 'waiting');
|
|
76
161
|
}
|
|
77
162
|
|
|
78
163
|
function onInstalled() {
|
|
79
|
-
|
|
164
|
+
rememberInstallation();
|
|
80
165
|
// This event may precede Android's completion of WebAPK creation.
|
|
81
166
|
publish('installed');
|
|
82
167
|
}
|
|
83
168
|
|
|
84
169
|
function onDisplayModeChange() {
|
|
170
|
+
if (isInstalledApp()) rememberInstallation();
|
|
85
171
|
if (isRunningAsApp()) {
|
|
86
172
|
deferredPrompt = null;
|
|
87
173
|
publish('running');
|
|
174
|
+
} else if (installed) {
|
|
175
|
+
publish('installed');
|
|
88
176
|
} else if (status === 'running') {
|
|
89
177
|
publish('waiting');
|
|
90
178
|
}
|
|
@@ -108,7 +196,7 @@ export function getPwaInstall() {
|
|
|
108
196
|
}
|
|
109
197
|
|
|
110
198
|
function prompt() {
|
|
111
|
-
if (
|
|
199
|
+
if (!snapshot().available) return Promise.resolve(null);
|
|
112
200
|
const event = deferredPrompt;
|
|
113
201
|
deferredPrompt = null;
|
|
114
202
|
publish('prompting');
|
|
@@ -162,8 +250,12 @@ export function getPwaInstall() {
|
|
|
162
250
|
observe(globalThis, 'beforeinstallprompt', onBeforeInstallPrompt);
|
|
163
251
|
observe(globalThis, 'appinstalled', onInstalled);
|
|
164
252
|
observe(displayMode, 'change', onDisplayModeChange);
|
|
253
|
+
observe(installedDisplayMode, 'change', onDisplayModeChange);
|
|
165
254
|
observe(globalThis, 'pagehide', onPageHide);
|
|
166
255
|
sharedOwner = owner;
|
|
256
|
+
const storageTask = loadInstallStorage();
|
|
257
|
+
const ready = restoreInstallation();
|
|
258
|
+
if (installed) rememberInstallation();
|
|
167
259
|
return owner;
|
|
168
260
|
}
|
|
169
261
|
|
package/docs/reference/pwa.md
CHANGED
|
@@ -293,18 +293,29 @@ does not infer installation support from the user-agent string. See
|
|
|
293
293
|
## getPwaInstall()
|
|
294
294
|
|
|
295
295
|
Import `getPwaInstall` and `PWA_INSTALL_STATE_EVENT` from `arcane-os/pwa`.
|
|
296
|
-
`getPwaInstall()` synchronously returns the shared page owner with `state`,
|
|
296
|
+
`getPwaInstall()` synchronously returns the shared page owner with `state`, `ready`,
|
|
297
297
|
`subscribe`, `prompt`, `dismiss` and `dispose`. Call it early when owning a
|
|
298
298
|
separate install entry point so it can capture `beforeinstallprompt` before
|
|
299
299
|
loading the UI. The generated bootstrap already does this through
|
|
300
300
|
`mountPwaInstallPrompt()`.
|
|
301
301
|
|
|
302
|
-
`state` contains `status`, `available`, `dismissed`, `outcome
|
|
302
|
+
`state` contains `status`, `available`, `installed`, `dismissed`, `outcome`,
|
|
303
|
+
`error` and `storageError`.
|
|
303
304
|
Status is `waiting`, `available`, `prompting`, `accepted`, `dismissed`,
|
|
304
305
|
`installed`, `running`, `error` or `disposed`. `available` means a native event
|
|
305
|
-
is retained
|
|
306
|
+
is retained, the initial installation-record read has settled, and installation
|
|
307
|
+
has not been recorded or detected. A dismissed floating suggestion can still
|
|
308
|
+
have `available: true`. `installed` is true after browser-reported installation
|
|
309
|
+
or restoration of that app's saved installation record.
|
|
306
310
|
`outcome` is the browser's `accepted` or `dismissed` choice, or `null` before a
|
|
307
|
-
choice. `error` carries the complete prompt error, or `null
|
|
311
|
+
choice. `error` carries the complete prompt error, or `null`; `storageError`
|
|
312
|
+
carries the complete DBOPFS read or write error, or `null`.
|
|
313
|
+
|
|
314
|
+
`ready` resolves to the state after the initial DBOPFS read settles. Native
|
|
315
|
+
events are captured synchronously while this read runs. Only installation
|
|
316
|
+
availability waits for it; page rendering, component loading and worker
|
|
317
|
+
registration continue independently. A storage failure is logged and published
|
|
318
|
+
as `storageError`; `ready` still resolves and native installation remains usable.
|
|
308
319
|
|
|
309
320
|
`subscribe(listener, {emitCurrent: true, signal} = {})` immediately replays
|
|
310
321
|
state by default and returns an unsubscribe function. Later state travels
|
|
@@ -315,7 +326,8 @@ registration, storage initialization or model readiness.
|
|
|
315
326
|
Call `prompt()` directly from the user's install click, before any asynchronous
|
|
316
327
|
wait. It invokes the browser prompt in the same call stack, consumes the event
|
|
317
328
|
once, and returns a promise for the browser's choice. It resolves to `null`
|
|
318
|
-
when
|
|
329
|
+
when installation is unavailable, including while the saved state is loading
|
|
330
|
+
or after installation is remembered. Failure publishes
|
|
319
331
|
`error` state and rejects. A new native event is required for another prompt.
|
|
320
332
|
|
|
321
333
|
```javascript
|
|
@@ -336,8 +348,24 @@ installButton.addEventListener('click', function requestInstallation() {
|
|
|
336
348
|
|
|
337
349
|
`dismiss()` remembers the session choice without consuming the retained event
|
|
338
350
|
and returns the current state. A browser-native dismissed choice is remembered
|
|
339
|
-
too. `appinstalled` clears the event
|
|
340
|
-
|
|
351
|
+
too. `appinstalled` clears the event, publishes `installed`, and saves
|
|
352
|
+
`{installed: true}` as `pwa/installed.json` through the current app-scoped
|
|
353
|
+
DBOPFS singleton. An installed-app launch in standalone, minimal-ui or
|
|
354
|
+
window-controls-overlay mode, or with `navigator.standalone === true`, also
|
|
355
|
+
records installation. Ordinary fullscreen suppresses the current prompt but
|
|
356
|
+
does not record installation because browsers can enter fullscreen without
|
|
357
|
+
installing an app; see the [display-mode specification](https://drafts.csswg.org/mediaqueries-5/#display-modes).
|
|
358
|
+
|
|
359
|
+
Every new owner reads the saved flag, so both floating and explicit inline
|
|
360
|
+
controls stay hidden on subsequent visits in that browser origin's app scope.
|
|
361
|
+
The SDK never clears this flag or resets it when display mode changes. Prompt
|
|
362
|
+
acceptance and dismissal alone do not write it. The record is independent of
|
|
363
|
+
resource-check history and other application data. A pending read cannot undo
|
|
364
|
+
newly observed installation, and a confirmed installation's pending write is
|
|
365
|
+
retained through owner disposal. A write failure remains observable without
|
|
366
|
+
making the current installed session eligible again.
|
|
367
|
+
|
|
368
|
+
Running in an app display mode publishes `running`. These states
|
|
341
369
|
do not establish offline readiness. On Android, `appinstalled` can arrive
|
|
342
370
|
before WebAPK creation finishes. See the
|
|
343
371
|
[browser lifecycle distinction](https://web.dev/learn/pwa/detection/).
|
|
@@ -1069,6 +1069,12 @@ availability comes from the browser's `beforeinstallprompt` event through
|
|
|
1069
1069
|
`getPwaInstall()`. Without an available event, the suggestion stays hidden;
|
|
1070
1070
|
absence does not identify why installation is unavailable. Installed-app events,
|
|
1071
1071
|
accepted prompts, and an already running installed display mode hide it.
|
|
1072
|
+
The shared SDK owner persists browser-reported installation in app-scoped
|
|
1073
|
+
DBOPFS. Both floating and inline controls stay hidden while that record is
|
|
1074
|
+
loading and remain hidden on later visits when installation is remembered.
|
|
1075
|
+
Acceptance alone does not save installation. The component's `ready` means
|
|
1076
|
+
its methods and subscription are attached; storage completion belongs to the
|
|
1077
|
+
shared owner's `ready` promise and does not delay page rendering.
|
|
1072
1078
|
|
|
1073
1079
|
### Example
|
|
1074
1080
|
|
|
@@ -6231,13 +6231,24 @@ payload and current-state subscription behavior.
|
|
|
6231
6231
|
|
|
6232
6232
|
### Overview
|
|
6233
6233
|
|
|
6234
|
-
`getPwaInstall()` returns one synchronous page owner exposing `state`,
|
|
6234
|
+
`getPwaInstall()` returns one synchronous page owner exposing `state`, `ready`,
|
|
6235
6235
|
`subscribe`, `prompt`, `dismiss` and `dispose`. It captures the browser's
|
|
6236
6236
|
`beforeinstallprompt` event and observes `appinstalled` and app display-mode
|
|
6237
6237
|
changes. A missing event leaves installation availability unknown and the owner
|
|
6238
6238
|
waiting; it does not prove browser incompatibility.
|
|
6239
6239
|
|
|
6240
|
-
State contains `status`, `available`, `dismissed`, `outcome
|
|
6240
|
+
State contains `status`, `available`, `installed`, `dismissed`, `outcome`,
|
|
6241
|
+
`error` and `storageError`. The owner restores `pwa/installed.json` through the
|
|
6242
|
+
app-scoped DBOPFS singleton and saves `{installed: true}` after `appinstalled`
|
|
6243
|
+
or an installed-app launch. Remembered installation suppresses both floating
|
|
6244
|
+
and inline controls on later visits. Acceptance alone and ordinary fullscreen
|
|
6245
|
+
do not record installation.
|
|
6246
|
+
|
|
6247
|
+
`ready` resolves to the state once the initial read settles. Native events are
|
|
6248
|
+
captured immediately, while install availability waits for that read. Rendering
|
|
6249
|
+
and worker registration remain independent. Storage failures are logged and
|
|
6250
|
+
published in `storageError`; they do not reject `ready` or reverse an observed
|
|
6251
|
+
installation. Confirmed-install writes remain owned after disposal.
|
|
6241
6252
|
Subscriptions replay current state by default. `prompt()` must be called
|
|
6242
6253
|
directly within the install click to preserve native user activation. It
|
|
6243
6254
|
consumes the event once and returns the browser choice, resolves to `null` when
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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
|
"scripts": {
|
|
68
68
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
69
69
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
70
|
-
"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:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.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",
|
|
71
71
|
"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",
|
|
72
72
|
"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",
|
|
73
73
|
"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",
|
package/src/workspace.mjs
CHANGED
|
@@ -216,12 +216,17 @@ function classifyRootConfig(config){
|
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
async function discoverAppsInRoot(root,config){
|
|
219
|
+
async function discoverAppsInRoot(root,config,appId){
|
|
220
|
+
if(appId!==undefined&&(!is.string(appId)||!APP_ID_PATTERN.test(appId))){
|
|
221
|
+
fail(`Invalid app id: ${String(appId)}.`,'ARCANE_USAGE');
|
|
222
|
+
}
|
|
220
223
|
const appsRoot=path.join(root,'apps');
|
|
221
224
|
await assertRealDirectory(appsRoot,'Workspace apps root');
|
|
222
225
|
const entries=await readdir(appsRoot,{withFileTypes:true});
|
|
223
226
|
const apps=[];
|
|
224
227
|
for(const entry of entries.sort((left,right)=>ordinal(left.name,right.name))){
|
|
228
|
+
// A named operation validates that app, not unrelated application descriptors.
|
|
229
|
+
if(appId!==undefined&&entry.name!==appId)continue;
|
|
225
230
|
if(!APP_ID_PATTERN.test(entry.name))continue;
|
|
226
231
|
if(entry.isSymbolicLink())fail(`apps/${entry.name} must not be a symbolic link or junction.`);
|
|
227
232
|
if(!entry.isDirectory())continue;
|
|
@@ -274,13 +279,11 @@ export async function inspectWorkspaceProfile(workspaceRoot=process.cwd()){
|
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
export async function selectApp(workspaceRoot=process.cwd(),appId){
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
fail(`Invalid app id: ${String(appId)}.`,'ARCANE_USAGE');
|
|
280
|
-
}
|
|
282
|
+
const profile=await inspectWorkspaceProfile(workspaceRoot);
|
|
283
|
+
const apps=await discoverAppsInRoot(profile.workspaceRoot,profile.config,appId);
|
|
281
284
|
if(appId){
|
|
282
285
|
const selected=apps.find(app=>app.appId===appId);
|
|
283
|
-
if(!selected)fail(`Unknown app "${appId}"
|
|
286
|
+
if(!selected)fail(`Unknown app "${appId}".`);
|
|
284
287
|
return selected;
|
|
285
288
|
}
|
|
286
289
|
if(apps.length===0)fail('No Arcane applications were found under apps/.');
|
|
@@ -294,14 +297,11 @@ export async function resolveWorkspace({workspaceRoot=process.cwd(),appId}={}){
|
|
|
294
297
|
const profile=await inspectWorkspaceProfile(workspaceRoot);
|
|
295
298
|
const canonicalRoot=profile.workspaceRoot;
|
|
296
299
|
const config=profile.config;
|
|
297
|
-
const apps=await discoverAppsInRoot(canonicalRoot,config);
|
|
298
|
-
if(appId!==undefined&&(!is.string(appId)||!APP_ID_PATTERN.test(appId))){
|
|
299
|
-
fail(`Invalid app id: ${String(appId)}.`,'ARCANE_USAGE');
|
|
300
|
-
}
|
|
300
|
+
const apps=await discoverAppsInRoot(canonicalRoot,config,appId);
|
|
301
301
|
let app;
|
|
302
302
|
if(appId){
|
|
303
303
|
app=apps.find(item=>item.appId===appId);
|
|
304
|
-
if(!app)fail(`Unknown app "${appId}"
|
|
304
|
+
if(!app)fail(`Unknown app "${appId}".`);
|
|
305
305
|
}else if(apps.length===1){
|
|
306
306
|
[app]=apps;
|
|
307
307
|
}else if(apps.length===0){
|