arcane-os 0.71.2 → 0.72.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 +13 -0
- package/README.md +1 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/core-native-packaging.md +74 -7
- package/docs/reference/core-shared-host.md +43 -4
- package/docs/reference/inventory/package-api.json +3 -3
- package/package.json +1 -1
- package/schemas/arcane-app.schema.json +4 -0
- package/src/app-descriptor.mjs +7 -1
- package/src/core/host.mjs +73 -13
- package/src/core/hosts/macos/ArcaneLauncher.m +14 -3
- package/src/core/hosts/windows/ArcaneLauncher.cs +25 -5
- package/src/native/portable-layout.mjs +9 -1
- package/src/native/portable-provider.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.72.0
|
|
4
|
+
|
|
5
|
+
- Add optional `native.launchContext` defaults for ordinary packaged launch and
|
|
6
|
+
public `resolveNativeLaunchContext()` through `arcane-os/core/host`. Native
|
|
7
|
+
applications and external MCP can select the same app-owned state, workspace,
|
|
8
|
+
shared endpoint and log without reconstructing platform paths.
|
|
9
|
+
- Extend `readCoreLaunchContext()` with explicit defaults and opt-in native
|
|
10
|
+
resolution. Preserve complete explicit launch records, selected workspace
|
|
11
|
+
values, ordinary stdio ownership and the existing no-option entry path.
|
|
12
|
+
- Forward packaged launch defaults through Windows and macOS launchers while
|
|
13
|
+
preserving window, profile, service and shared-host lifecycle behavior. No
|
|
14
|
+
stored-data migration or dependency change is introduced.
|
|
15
|
+
|
|
3
16
|
## 0.71.2
|
|
4
17
|
|
|
5
18
|
- Preserve complete browser Kokoro synthesis input through the selected runtime's
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.72.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
package/docs/reference/README.md
CHANGED
|
@@ -43,6 +43,7 @@ alone does not make bare module names resolve in a browser.
|
|
|
43
43
|
| Use the `arcane` command | [CLI reference](cli.md) |
|
|
44
44
|
| Install an app and cache selected resources offline | [Progressive web applications](pwa.md) |
|
|
45
45
|
| Disable source PWA generation or explicitly retire confirmed generated output | [Source PWA retirement](pwa.md#disable-source-pwa-generation-and-retire-generated-output) |
|
|
46
|
+
| Retire the prior worker after explicitly disabling standalone source PWA | [Disabled-worker browser lifecycle](pwa.md#disabled-worker-retirement-on-the-source-server) |
|
|
46
47
|
| Generate named browser imports or inspect the selected physical runtime | [`arcane import-map`](cli.md#arcane-import-map) and [browser runtime delivery](protocols.md#browser-runtime-delivery) |
|
|
47
48
|
| Connect an application through the shared WebSocket transport | [Shared WebSocket clients](websocket-client.md) |
|
|
48
49
|
| Choose browser, native, cloud, or cross-host behavior | [Availability and normalization](availability-and-normalization.md) |
|
|
@@ -210,6 +210,64 @@ details and [the client contract](core-client.md) for native bridge integration.
|
|
|
210
210
|
|
|
211
211
|
## Launch-time locations
|
|
212
212
|
|
|
213
|
+
An application can select defaults for ordinary double-click launch in its
|
|
214
|
+
descriptor, without a replacement launcher or a required command-line file:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"native": {
|
|
219
|
+
"launchContext": {
|
|
220
|
+
"sharedHost": {},
|
|
221
|
+
"dispatch": {"destination": "Moon Cheese Receiving Dock"}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
This is the `native` fragment of the application's complete descriptor. The
|
|
228
|
+
optional `native.launchContext` object is copied intact to the portable
|
|
229
|
+
manifest and generated Core entry. Its presence explicitly selects native
|
|
230
|
+
location defaults. `sharedHost:{}` opts into the shared Core endpoint and log;
|
|
231
|
+
omitting `sharedHost` retains stdio-owned Core lifetime. Omitting
|
|
232
|
+
`native.launchContext` retains the previous generated-entry behavior entirely.
|
|
233
|
+
Windows and macOS launchers use the same packaged defaults for native state
|
|
234
|
+
selection, so an ordinary executable launch and external MCP can select the
|
|
235
|
+
same app-owned Core.
|
|
236
|
+
|
|
237
|
+
`resolveNativeLaunchContext({appId,context={}})` from `arcane-os/core/host` is
|
|
238
|
+
the public, synchronous location owner for native applications and external MCP.
|
|
239
|
+
It returns a new complete context and fills only omitted fields:
|
|
240
|
+
|
|
241
|
+
| Field | SDK default |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `stateRoot` on Windows | `%LOCALAPPDATA%/Arcane/<appId>`; standard home `AppData/Local` fallback when the environment value is absent |
|
|
244
|
+
| `stateRoot` on macOS | `~/Library/Application Support/Arcane/<appId>` |
|
|
245
|
+
| `stateRoot` on Linux | Absolute `$XDG_DATA_HOME/Arcane/<appId>`, otherwise `~/.local/share/Arcane/<appId>` |
|
|
246
|
+
| `workspaceRoot` | `<resolved stateRoot>/Workspace` |
|
|
247
|
+
| `sharedHost.endpoint`, only when `sharedHost` is selected | Windows named pipe `\\.\pipe\Arcane-` followed by the URL-encoded complete resolved state path; Linux/macOS socket `<resolved stateRoot>/core.sock` |
|
|
248
|
+
| `sharedHost.logFile`, only when `sharedHost` is selected | `<resolved stateRoot>/Core.log` |
|
|
249
|
+
|
|
250
|
+
Windows/macOS roots retain the existing native host locations. Linux is the
|
|
251
|
+
portable Node host convention; an external composing host supplies its actual
|
|
252
|
+
state root when it selects another location. Android and other host adaptations
|
|
253
|
+
must supply `stateRoot` explicitly and own native IPC/process adaptation.
|
|
254
|
+
The resolver never reads saved preferences, creates directories, changes the
|
|
255
|
+
environment, starts services, or migrates existing data. The generated opted-in
|
|
256
|
+
shared entry creates its selected state directory because its socket/log need
|
|
257
|
+
that parent; application services continue to own workspace I/O. Custom
|
|
258
|
+
endpoint/log parent directories retain the existing shared-host requirements.
|
|
259
|
+
|
|
260
|
+
Explicit `stateRoot`, `workspaceRoot`, endpoint, log filename and additional
|
|
261
|
+
fields remain authored values; the resolver derives omitted paths from the
|
|
262
|
+
absolute selected state location. Relative explicit paths retain the caller's
|
|
263
|
+
working-directory meaning, so UI and MCP callers must select the same location.
|
|
264
|
+
No hashing, app-local OS path composition, directory scanning, or new repository
|
|
265
|
+
location rule is involved. `resolveArcaneDataPaths()` continues to own its
|
|
266
|
+
separate `ArcaneData/Repos` contract. A selected native context needs a nonempty
|
|
267
|
+
string `appId` and state directory; a selected `sharedHost` must be an object.
|
|
268
|
+
Malformed resolver inputs report `TypeError`; ordinary OS endpoint requirements
|
|
269
|
+
remain at the IPC operation.
|
|
270
|
+
|
|
213
271
|
For one app-owned Core shared by the native window and a separate MCP process,
|
|
214
272
|
the launch context may explicitly select `sharedHost:{endpoint,logFile}`. The
|
|
215
273
|
generated native child then connects through a stdio bridge, starting an
|
|
@@ -224,27 +282,36 @@ or `null`; the shared host replays that current listener to a newly attached
|
|
|
224
282
|
macOS bridge, without restarting the server or retaining historical events.
|
|
225
283
|
|
|
226
284
|
The generated Core entry accepts `--arcane-launch-config <path>`. The public
|
|
227
|
-
`readCoreLaunchContext({argv=process.argv.slice(2)}={})`
|
|
228
|
-
`arcane-os/core/host` reads that explicitly selected JSON object.
|
|
285
|
+
`readCoreLaunchContext({argv=process.argv.slice(2),defaults={},appId}={})`
|
|
286
|
+
function in `arcane-os/core/host` reads that explicitly selected JSON object.
|
|
287
|
+
Precedence is packaged `defaults`, then the native host's actual state-root
|
|
288
|
+
argument, then every complete top-level field from the explicit JSON object.
|
|
289
|
+
Nested records replace their default field as a whole; they are not deep-merged.
|
|
290
|
+
An `appId` explicitly opts the result into `resolveNativeLaunchContext`; the
|
|
291
|
+
generated entry supplies it only when `native.launchContext` is present.
|
|
292
|
+
A native host
|
|
229
293
|
can separately supply `--arcane-host-state-root <directory>` for the reader's
|
|
230
294
|
`stateRoot` default. Every field in the explicit JSON object takes precedence
|
|
231
295
|
unchanged, including an explicitly present `stateRoot` with a relative, null or
|
|
232
|
-
other application-owned value
|
|
296
|
+
other application-owned value on the ordinary non-resolving reader. With neither
|
|
297
|
+
argument nor defaults/appId the reader still returns `{}`.
|
|
233
298
|
Missing files, missing argument values and malformed JSON report their actual
|
|
234
299
|
errors; the reader does not search for configuration, resolve or validate the
|
|
235
|
-
state directory, rewrite the file or change the process environment.
|
|
300
|
+
state directory, rewrite the file or change the process environment. The
|
|
301
|
+
explicitly opted-in resolver supplies its documented defaults after this merge.
|
|
236
302
|
|
|
237
303
|
The generated entry supplies an artifact-derived `appRoot` and then applies the
|
|
238
304
|
complete explicit launch context. Every service factory receives that same
|
|
239
305
|
object as its second argument, preserving its authored first argument. A
|
|
240
306
|
launcher can supply the user's selected `workspaceRoot` and writable
|
|
241
307
|
`stateRoot` at launch instead of embedding this machine's paths in an app
|
|
242
|
-
package.
|
|
243
|
-
|
|
308
|
+
package. Explicit product selections take precedence over the opted-in SDK
|
|
309
|
+
location defaults. The SDK chooses no saved preference filename or model. JSON values and additional
|
|
244
310
|
application-owned fields remain unchanged, including an app-selected
|
|
245
311
|
`preferencesFile`. The state directory supplies no preference filename and
|
|
246
312
|
triggers no stored-data discovery or migration. Linux and other composing hosts
|
|
247
|
-
may pass the same separate argument; the reader chooses no platform
|
|
313
|
+
may pass the same separate argument; the ordinary reader chooses no platform
|
|
314
|
+
directory until native resolution is explicitly selected.
|
|
248
315
|
|
|
249
316
|
## Selected local-AI runtimes
|
|
250
317
|
|
|
@@ -146,8 +146,46 @@ events may change; applications own those domain methods and state.
|
|
|
146
146
|
|
|
147
147
|
## Native launch and platforms
|
|
148
148
|
|
|
149
|
-
The generated portable Core entry keeps ordinary stdio behavior unless
|
|
150
|
-
|
|
149
|
+
The generated portable Core entry keeps ordinary stdio behavior unless its
|
|
150
|
+
selected launch context contains `sharedHost`. An application can opt in for
|
|
151
|
+
ordinary packaged executable launch with `native.launchContext:{sharedHost:{}}`
|
|
152
|
+
in its complete descriptor. The SDK supplies the app's platform state root,
|
|
153
|
+
workspace, endpoint and diagnostic log through `resolveNativeLaunchContext`.
|
|
154
|
+
The same public owner is available to external MCP:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import {readCoreLaunchContext, connectSharedCoreHost} from 'arcane-os/core/host';
|
|
158
|
+
|
|
159
|
+
const context = await readCoreLaunchContext({
|
|
160
|
+
appId: appDescriptor.id,
|
|
161
|
+
defaults: appDescriptor.native.launchContext
|
|
162
|
+
});
|
|
163
|
+
const connection = await connectSharedCoreHost({
|
|
164
|
+
endpoint: context.sharedHost.endpoint
|
|
165
|
+
});
|
|
166
|
+
// Invoke the application's actual services through connection.client.
|
|
167
|
+
// Disconnect this MCP client without shutting down the application's host.
|
|
168
|
+
await connection.close();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This example connects to a running native/headless host. The existing `start`
|
|
172
|
+
option can start the packaged Core entry explicitly when independent MCP-first
|
|
173
|
+
startup is needed; the entry applies those same packaged defaults. Pass the
|
|
174
|
+
same explicit `--arcane-launch-config` file to both entries for user-selected
|
|
175
|
+
locations. A caller that already owns a complete context can use the pure
|
|
176
|
+
`resolveNativeLaunchContext({appId,context})` directly instead of reading argv.
|
|
177
|
+
For MCP-first startup, prepare the returned `context.stateRoot` with Node's
|
|
178
|
+
`mkdir(context.stateRoot,{recursive:true})` before selecting `start`, because
|
|
179
|
+
the parent's startup diagnostic log opens before the headless child runs.
|
|
180
|
+
This uses the resolved directory directly, without duplicating platform path
|
|
181
|
+
rules. Pass `context.sharedHost.logFile` to that existing `start` record and
|
|
182
|
+
the same launch file to the packaged Core command. Custom endpoint/log parent
|
|
183
|
+
directories remain caller-selected.
|
|
184
|
+
See [launch-time locations](core-native-packaging.md#launch-time-locations) for
|
|
185
|
+
the exact platform defaults, precedence, location override and directory rules.
|
|
186
|
+
No app must reconstruct OS directory rules.
|
|
187
|
+
|
|
188
|
+
An explicit launch file can also select the existing full endpoint contract:
|
|
151
189
|
|
|
152
190
|
```json
|
|
153
191
|
{
|
|
@@ -163,8 +201,9 @@ option, the native Core child is a stdio-to-shared-Core bridge. It connects or
|
|
|
163
201
|
launches the same entry with `--arcane-core-headless`; the headless entry claims
|
|
164
202
|
the endpoint before importing services. Closing the native window ends its
|
|
165
203
|
bridge, while the independent Core host remains available. Service options and
|
|
166
|
-
the complete launch context retain their existing values.
|
|
167
|
-
|
|
204
|
+
the complete launch context retain their existing values. Shared mode remains
|
|
205
|
+
explicit: the new descriptor option selects defaults only for that application,
|
|
206
|
+
and the existing no-option stdio path is unchanged.
|
|
168
207
|
|
|
169
208
|
`startSharedCoreBridge({endpoint,start,input=process.stdin,
|
|
170
209
|
output=process.stdout,signal,onError})` is also public. It returns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sdkVersion": "0.71.
|
|
3
|
+
"sdkVersion": "0.71.1",
|
|
4
4
|
"environment": {
|
|
5
5
|
"runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
|
|
6
6
|
"minimumVersion": "22.23.2 for Node entrypoints",
|
|
@@ -1185,7 +1185,7 @@
|
|
|
1185
1185
|
],
|
|
1186
1186
|
"primaryImport": "arcane-os",
|
|
1187
1187
|
"group": "Headless toolchain operations",
|
|
1188
|
-
"summary": "Starts one owned browser development server for the selected application.",
|
|
1188
|
+
"summary": "Starts one owned browser development server for the selected application, including native worker retirement when standalone source PWA is explicitly disabled and its root worker is absent.",
|
|
1189
1189
|
"availability": "Node; selected operation may produce browser or native output",
|
|
1190
1190
|
"protocol": "Shared headless operation API + arcane-cli-events/1",
|
|
1191
1191
|
"normalization": "SDK-normalized inputs, errors, events, and documented result"
|
|
@@ -2446,7 +2446,7 @@
|
|
|
2446
2446
|
],
|
|
2447
2447
|
"primaryImport": "arcane-os",
|
|
2448
2448
|
"group": "Workspace, doctor, repository, and server",
|
|
2449
|
-
"summary": "Starts an owned source or packaged browser server with complete selected routes, HTTPS by default, explicit HTTP selection, listener readiness and shutdown.",
|
|
2449
|
+
"summary": "Starts an owned source or packaged browser server with complete selected routes, HTTPS by default, explicit HTTP selection, listener readiness and shutdown; explicitly disabled standalone source PWA retires an absent root worker through its native lifecycle.",
|
|
2450
2450
|
"availability": "Node control plane; browser data plane",
|
|
2451
2451
|
"protocol": "Node ESM",
|
|
2452
2452
|
"normalization": "SDK-normalized inputs, errors, events, and documented result"
|
package/package.json
CHANGED
|
@@ -132,6 +132,10 @@
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
"localAI": { "$ref": "#/$defs/localAI" },
|
|
135
|
+
"launchContext": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"description": "Optional complete native launch defaults. Presence opts into SDK per-app native locations; sharedHost:{} explicitly selects the shared endpoint. Explicit launch JSON fields take precedence unchanged."
|
|
138
|
+
},
|
|
135
139
|
"window": {
|
|
136
140
|
"type": "object",
|
|
137
141
|
"additionalProperties": false,
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -254,7 +254,7 @@ function validateNativeWindow(value){
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
function validateNative(value,appId){
|
|
257
|
-
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services','window']),'descriptor.native');
|
|
257
|
+
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services','window','launchContext']),'descriptor.native');
|
|
258
258
|
if(!NATIVE_TYPES.has(value.type))fail('descriptor.native.type is unsupported.');
|
|
259
259
|
const icon=value.icon===null?null:normalizeRelativePath(value.icon,'descriptor.native.icon');
|
|
260
260
|
if(!is.integer(value.order)||value.order<0){
|
|
@@ -265,6 +265,9 @@ function validateNative(value,appId){
|
|
|
265
265
|
});
|
|
266
266
|
const documentCatalog=validateDocumentCatalog(value.documentCatalog);
|
|
267
267
|
const window=validateNativeWindow(value.window);
|
|
268
|
+
if(value.launchContext!==undefined&&!isObject(value.launchContext)){
|
|
269
|
+
fail('descriptor.native.launchContext must be an object.');
|
|
270
|
+
}
|
|
268
271
|
let services;
|
|
269
272
|
if(value.services!==undefined){
|
|
270
273
|
if(!is.array(value.services))fail('descriptor.native.services must be an array.');
|
|
@@ -279,6 +282,7 @@ function validateNative(value,appId){
|
|
|
279
282
|
order:value.order,
|
|
280
283
|
bundledApps,
|
|
281
284
|
...(window===undefined?{}:{window}),
|
|
285
|
+
...(value.launchContext===undefined?{}:{launchContext:value.launchContext}),
|
|
282
286
|
...(services===undefined?{}:{services}),
|
|
283
287
|
...(value.localAI===undefined?{}:{localAI:normalizeLocalAIConfig(value.localAI)}),
|
|
284
288
|
...(documentCatalog?{documentCatalog}:{})
|
|
@@ -416,6 +420,7 @@ export function projectNativeDescriptor(descriptor,{source}={}){
|
|
|
416
420
|
...(value.native.documentCatalog?{documentCatalog:value.native.documentCatalog}:{}),
|
|
417
421
|
...(value.native.services===undefined?{}:{services:value.native.services}),
|
|
418
422
|
...(value.native.window===undefined?{}:{window:value.native.window}),
|
|
423
|
+
...(value.native.launchContext===undefined?{}:{launchContext:value.native.launchContext}),
|
|
419
424
|
...(value.native.localAI===undefined?{}:{localAI:value.native.localAI}),
|
|
420
425
|
include:[...value.package.include]
|
|
421
426
|
};
|
|
@@ -462,6 +467,7 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
462
467
|
bundledApps:[...(native.bundledApps??[])],
|
|
463
468
|
...(native.services===undefined?{}:{services:native.services}),
|
|
464
469
|
...(native.window===undefined?{}:{window:native.window}),
|
|
470
|
+
...(native.launchContext===undefined?{}:{launchContext:native.launchContext}),
|
|
465
471
|
...((packageManifest.localAI??native.localAI)===undefined?{}:{localAI:packageManifest.localAI??native.localAI}),
|
|
466
472
|
...(native.documentCatalog?{documentCatalog:native.documentCatalog}:{})
|
|
467
473
|
},
|
package/src/core/host.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import Is from 'strong-type';
|
|
3
5
|
import {createCoreRuntime} from './runtime.mjs';
|
|
4
6
|
import {startCoreStdio} from './stdio.mjs';
|
|
@@ -7,32 +9,90 @@ export {startSharedCoreHost, connectSharedCoreHost, runSharedCoreHost, startShar
|
|
|
7
9
|
|
|
8
10
|
const is = new Is(false);
|
|
9
11
|
|
|
12
|
+
/** Resolve one app's native locations without creating or migrating data. */
|
|
13
|
+
export function resolveNativeLaunchContext({appId, context = {}} = {}) {
|
|
14
|
+
if (!is.string(appId) || !appId) throw new TypeError('Native launch resolution requires an appId.');
|
|
15
|
+
if (context === null || !is.object(context) || is.array(context)) {
|
|
16
|
+
throw new TypeError('Native launch context must be an object.');
|
|
17
|
+
}
|
|
18
|
+
let stateRoot = context.stateRoot;
|
|
19
|
+
if (stateRoot === undefined) {
|
|
20
|
+
const home = homedir();
|
|
21
|
+
switch (process.platform) {
|
|
22
|
+
case 'win32':
|
|
23
|
+
stateRoot = path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Arcane', appId);
|
|
24
|
+
break;
|
|
25
|
+
case 'darwin':
|
|
26
|
+
stateRoot = path.join(home, 'Library', 'Application Support', 'Arcane', appId);
|
|
27
|
+
break;
|
|
28
|
+
case 'linux': {
|
|
29
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
30
|
+
stateRoot = path.join(xdg && path.isAbsolute(xdg) ? xdg : path.join(home, '.local', 'share'), 'Arcane', appId);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
default:
|
|
34
|
+
throw new TypeError('This native host must supply its persistent stateRoot.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!is.string(stateRoot) || !stateRoot) throw new TypeError('Native stateRoot must be a nonempty directory path.');
|
|
38
|
+
const root = path.resolve(stateRoot);
|
|
39
|
+
const resolved = {
|
|
40
|
+
...context,
|
|
41
|
+
stateRoot,
|
|
42
|
+
workspaceRoot: context.workspaceRoot === undefined ? path.join(root, 'Workspace') : context.workspaceRoot
|
|
43
|
+
};
|
|
44
|
+
if (context.sharedHost !== undefined) {
|
|
45
|
+
if (context.sharedHost === null || !is.object(context.sharedHost) || is.array(context.sharedHost)) {
|
|
46
|
+
throw new TypeError('Native sharedHost must be an object.');
|
|
47
|
+
}
|
|
48
|
+
// Encode the complete selected Windows path as a pipe-name component;
|
|
49
|
+
// Unix IPC uses an ordinary file in that same persistent app directory.
|
|
50
|
+
const endpoint = process.platform === 'win32'
|
|
51
|
+
? '\\\\.\\pipe\\Arcane-' + encodeURIComponent(root)
|
|
52
|
+
: path.join(root, 'core.sock');
|
|
53
|
+
resolved.sharedHost = {
|
|
54
|
+
...context.sharedHost,
|
|
55
|
+
endpoint: context.sharedHost.endpoint === undefined ? endpoint : context.sharedHost.endpoint,
|
|
56
|
+
logFile: context.sharedHost.logFile === undefined ? path.join(root, 'Core.log') : context.sharedHost.logFile
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
|
|
10
62
|
/**
|
|
11
|
-
*
|
|
63
|
+
* Packaged defaults precede the host's actual state-root argument.
|
|
12
64
|
* Every field in an explicitly selected launch JSON object takes precedence
|
|
13
65
|
* unchanged; field meanings belong to the launch owner and individual services.
|
|
66
|
+
* appId explicitly selects native location resolution; ordinary calls keep the
|
|
67
|
+
* existing read-only launch-file contract without choosing any locations.
|
|
14
68
|
*/
|
|
15
|
-
export async function readCoreLaunchContext({argv = process.argv.slice(2)} = {}) {
|
|
69
|
+
export async function readCoreLaunchContext({argv = process.argv.slice(2), defaults = {}, appId} = {}) {
|
|
70
|
+
if (defaults === null || !is.object(defaults) || is.array(defaults)) {
|
|
71
|
+
throw new TypeError('Core launch defaults must be an object.');
|
|
72
|
+
}
|
|
16
73
|
const stateFlag = argv.indexOf('--arcane-host-state-root');
|
|
17
|
-
const
|
|
74
|
+
const context = {...defaults};
|
|
18
75
|
if (stateFlag !== -1) {
|
|
19
76
|
const stateRoot = argv[stateFlag + 1];
|
|
20
77
|
if (stateRoot === undefined || stateRoot === '') {
|
|
21
78
|
throw new TypeError('--arcane-host-state-root requires a directory.');
|
|
22
79
|
}
|
|
23
|
-
|
|
80
|
+
context.stateRoot = stateRoot;
|
|
24
81
|
}
|
|
25
82
|
const flag = argv.indexOf('--arcane-launch-config');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
let selected = context;
|
|
84
|
+
if (flag !== -1) {
|
|
85
|
+
const filename = argv[flag + 1];
|
|
86
|
+
if (filename === undefined || filename === '') {
|
|
87
|
+
throw new TypeError('--arcane-launch-config requires a filename.');
|
|
88
|
+
}
|
|
89
|
+
const explicit = JSON.parse(await readFile(filename, 'utf8'));
|
|
90
|
+
if (explicit === null || !is.object(explicit) || is.array(explicit)) {
|
|
91
|
+
throw new TypeError('Core launch configuration must be a JSON object.');
|
|
92
|
+
}
|
|
93
|
+
selected = {...context, ...explicit};
|
|
34
94
|
}
|
|
35
|
-
return {
|
|
95
|
+
return appId === undefined ? selected : resolveNativeLaunchContext({appId, context: selected});
|
|
36
96
|
}
|
|
37
97
|
|
|
38
98
|
/** Compose app services with the shared dispatcher and draining transport. */
|
|
@@ -231,14 +231,25 @@ typedef NS_ENUM(NSUInteger, ArcaneDiagnosticStream) {
|
|
|
231
231
|
NSString *appID = ArcaneRequiredString(app, @"id", error);
|
|
232
232
|
if (!appID) return NO;
|
|
233
233
|
_title = [app[@"displayName"] isKindOfClass:NSString.class] ? app[@"displayName"] : appID;
|
|
234
|
+
id defaults = manifest[@"launchContext"];
|
|
235
|
+
if (defaults && ![defaults isKindOfClass:NSDictionary.class]) {
|
|
236
|
+
*error = ArcaneLauncherError(@"CORE_LAUNCH_MANIFEST_INVALID", @"The native launchContext must be an object.", defaults);
|
|
237
|
+
return NO;
|
|
238
|
+
}
|
|
239
|
+
NSMutableDictionary *launchContext = defaults ? [defaults mutableCopy] : [NSMutableDictionary dictionary];
|
|
234
240
|
if (_launchFilename) {
|
|
235
|
-
|
|
236
|
-
if (!
|
|
241
|
+
NSDictionary *explicitContext = ArcaneReadRecord([NSURL fileURLWithPath:_launchFilename], error);
|
|
242
|
+
if (!explicitContext) return NO;
|
|
243
|
+
[launchContext addEntriesFromDictionary:explicitContext];
|
|
237
244
|
}
|
|
245
|
+
_launchContext = launchContext;
|
|
246
|
+
// The opted-in Node resolver uses HOME when supplied; retain the previous
|
|
247
|
+
// native default unchanged for applications without launchContext.
|
|
248
|
+
NSString *nativeHome = defaults ? (NSProcessInfo.processInfo.environment[@"HOME"] ?: NSHomeDirectory()) : NSHomeDirectory();
|
|
238
249
|
id selectedState = _launchContext[@"stateRoot"];
|
|
239
250
|
_stateRoot = [selectedState isKindOfClass:NSString.class]
|
|
240
251
|
? ArcaneAbsolutePath(selectedState, _workingDirectory)
|
|
241
|
-
: [[
|
|
252
|
+
: [[nativeHome stringByAppendingPathComponent:@"Library/Application Support/Arcane"] stringByAppendingPathComponent:appID];
|
|
242
253
|
[self scheduleDiagnostics];
|
|
243
254
|
NSString *clientPath = ArcaneRequiredString(client, @"source", error);
|
|
244
255
|
if (!clientPath) return NO;
|
|
@@ -57,13 +57,33 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
57
57
|
Dictionary<string, object> host = manifest.TryGetValue("host", out hostValue) ? hostValue as Dictionary<string, object> : null;
|
|
58
58
|
string icon = host != null && host.TryGetValue("icon", out iconValue) ? iconValue as string : null;
|
|
59
59
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
Dictionary<string, object> launchContext =
|
|
60
|
+
// Native UI and generated Core use the same packaged defaults.
|
|
61
|
+
// Core rereads the explicit file; every explicit field wins.
|
|
62
|
+
Dictionary<string, object> launchContext = manifest.ContainsKey("launchContext")
|
|
63
|
+
? new Dictionary<string, object>(RequiredRecord(manifest, "launchContext"))
|
|
64
|
+
: new Dictionary<string, object>();
|
|
65
|
+
if (launchFilename != null)
|
|
66
|
+
{
|
|
67
|
+
foreach (KeyValuePair<string, object> field in ReadRecord(launchFilename))
|
|
68
|
+
launchContext[field.Key] = field.Value;
|
|
69
|
+
}
|
|
70
|
+
string defaultStateBase = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
71
|
+
if (manifest.ContainsKey("launchContext"))
|
|
72
|
+
{
|
|
73
|
+
// Match the public Node resolver for this opt-in contract,
|
|
74
|
+
// including an explicitly selected process environment.
|
|
75
|
+
defaultStateBase = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
|
76
|
+
if (String.IsNullOrEmpty(defaultStateBase))
|
|
77
|
+
{
|
|
78
|
+
string home = Environment.GetEnvironmentVariable("USERPROFILE");
|
|
79
|
+
if (String.IsNullOrEmpty(home)) home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
80
|
+
defaultStateBase = Path.Combine(home, "AppData", "Local");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
object stateRoot;
|
|
64
|
-
string profileParent = launchContext
|
|
84
|
+
string profileParent = launchContext.TryGetValue("stateRoot", out stateRoot) && stateRoot is string
|
|
65
85
|
? Path.GetFullPath((string)stateRoot)
|
|
66
|
-
: Path.Combine(
|
|
86
|
+
: Path.Combine(defaultStateBase, "Arcane", appId);
|
|
67
87
|
OpenDiagnostics(Path.Combine(profileParent, "Diagnostics"));
|
|
68
88
|
ArcaneHostOptions options = new ArcaneHostOptions
|
|
69
89
|
{
|
|
@@ -151,6 +151,7 @@ export async function copyCoreRuntime(root, files, signal, {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
export function coreEntrySource(application, version, services, {localAI, runtimes = [], packagedWeb = false} = {}) {
|
|
154
|
+
const launchDefaults = application.native?.launchContext;
|
|
154
155
|
const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
|
|
155
156
|
return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
|
|
156
157
|
}) ?? false;
|
|
@@ -199,6 +200,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
199
200
|
"import {fileURLToPath} from 'node:url';",
|
|
200
201
|
"import path from 'node:path';",
|
|
201
202
|
"import {isSea} from 'node:sea';",
|
|
203
|
+
...(launchDefaults === undefined ? [] : ["import {mkdir} from 'node:fs/promises';"]),
|
|
202
204
|
"import {readCoreLaunchContext, startCoreHost, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
|
|
203
205
|
'',
|
|
204
206
|
...(localAI === undefined ? [] : [
|
|
@@ -241,9 +243,15 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
241
243
|
' return resolved;',
|
|
242
244
|
' }),'
|
|
243
245
|
]),
|
|
244
|
-
' ...await readCoreLaunchContext()'
|
|
246
|
+
launchDefaults === undefined ? ' ...await readCoreLaunchContext()'
|
|
247
|
+
: ` ...await readCoreLaunchContext({appId: ${JSON.stringify(application.id)}, defaults: JSON.parse(${JSON.stringify(JSON.stringify(launchDefaults))})})`,
|
|
245
248
|
'};',
|
|
246
249
|
'',
|
|
250
|
+
...(launchDefaults === undefined ? [] : [
|
|
251
|
+
'// The shared endpoint and diagnostic log require their selected state directory.',
|
|
252
|
+
'if (context.sharedHost !== undefined) await mkdir(context.stateRoot, {recursive: true});',
|
|
253
|
+
''
|
|
254
|
+
]),
|
|
247
255
|
...(packagedWeb ? ['let packagedWeb;', ''] : []),
|
|
248
256
|
'async function createServices(register) {',
|
|
249
257
|
` const [${imports.map(function binding(selection) { return selection.binding; }).join(', ')}] = await Promise.all([`,
|
|
@@ -197,6 +197,7 @@ export function createPortableNativeProvider({services, packagedWeb = false, web
|
|
|
197
197
|
webRoot: 'app',
|
|
198
198
|
start: release.manifest?.app?.start ?? './index.html',
|
|
199
199
|
...(appDescriptor.native?.window === undefined ? {} : {window: {...appDescriptor.native.window}}),
|
|
200
|
+
...(appDescriptor.native?.launchContext === undefined ? {} : {launchContext: appDescriptor.native.launchContext}),
|
|
200
201
|
core: {
|
|
201
202
|
entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
|
|
202
203
|
...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
|