arcane-os 0.56.0 → 0.58.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +6 -2
  3. package/browser-runtime/ai/core-local.mjs +306 -0
  4. package/browser-runtime/ai/core-onnx.mjs +96 -0
  5. package/browser-runtime/ai/onnx-tensors.mjs +85 -0
  6. package/browser-runtime/core/client.mjs +151 -16
  7. package/docs/architecture.md +5 -2
  8. package/docs/platform-targets.md +30 -13
  9. package/docs/reference/README.md +38 -3
  10. package/docs/reference/availability-and-normalization.md +9 -6
  11. package/docs/reference/cli.md +29 -9
  12. package/docs/reference/core-client.md +80 -0
  13. package/docs/reference/core-native-packaging.md +304 -12
  14. package/docs/reference/core-preferences.md +49 -0
  15. package/docs/reference/diarization.md +206 -0
  16. package/docs/reference/git-text-snapshot.md +104 -0
  17. package/docs/reference/inventory/package-api.json +330 -22
  18. package/docs/reference/local-ai.md +331 -0
  19. package/docs/reference/protocols.md +97 -20
  20. package/docs/reference/pwa.md +5 -2
  21. package/docs/reference/runtime-modules.md +6 -5
  22. package/docs/reference/sdk-api.md +611 -63
  23. package/package.json +15 -3
  24. package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
  25. package/schemas/arcane-app.schema.json +55 -1
  26. package/schemas/arcane-package.schema.json +44 -0
  27. package/schemas/native-build-plan.schema.json +27 -1
  28. package/schemas/target-adapter.schema.json +2 -0
  29. package/src/app-descriptor.mjs +18 -2
  30. package/src/cli/main.mjs +20 -4
  31. package/src/constants.mjs +2 -0
  32. package/src/core/host.mjs +23 -0
  33. package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
  34. package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
  35. package/src/core/hosts/macos/ArcaneHost.h +86 -0
  36. package/src/core/hosts/macos/ArcaneHost.m +455 -0
  37. package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
  38. package/src/core/hosts/windows/ArcaneCoreProcess.cs +3 -3
  39. package/src/core/hosts/windows/ArcaneHost.cs +695 -0
  40. package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
  41. package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
  42. package/src/core/services/local-ai.mjs +621 -0
  43. package/src/core/services/packaged-web.mjs +89 -0
  44. package/src/core/services/preferences.mjs +201 -0
  45. package/src/core-classic-source.mjs +2 -2
  46. package/src/dev-server.mjs +9 -5
  47. package/src/diarization/build.mjs +35 -0
  48. package/src/diarization/index.mjs +432 -0
  49. package/src/diarization/native/CMakeLists.txt +9 -0
  50. package/src/diarization/native/main.cpp +269 -0
  51. package/src/diarization/service.mjs +129 -0
  52. package/src/git-text-snapshot.mjs +265 -0
  53. package/src/import-map.mjs +2 -0
  54. package/src/index.mjs +1 -0
  55. package/src/local-ai/archive.mjs +52 -0
  56. package/src/local-ai/config.mjs +62 -0
  57. package/src/local-ai/dev.mjs +390 -0
  58. package/src/local-ai/discover.mjs +71 -0
  59. package/src/local-ai/http.mjs +106 -0
  60. package/src/local-ai/install.mjs +459 -0
  61. package/src/local-ai/onnx-worker.mjs +85 -0
  62. package/src/local-ai/onnx.mjs +368 -0
  63. package/src/local-ai/server.mjs +262 -0
  64. package/src/native/macos-provider.mjs +291 -0
  65. package/src/native/portable-layout.mjs +37 -4
  66. package/src/native/portable-provider.mjs +44 -8
  67. package/src/native/windows-provider.mjs +262 -0
  68. package/src/native-plan.mjs +9 -1
  69. package/src/native-provider-loader.mjs +10 -2
  70. package/src/packager/core.mjs +6 -2
  71. package/src/process.mjs +184 -20
  72. package/src/scaffold.mjs +1 -1
  73. package/src/targets/index.mjs +11 -1
  74. package/src/templates/workspace-template.mjs +6 -5
  75. package/src/toolchain.mjs +33 -5
@@ -0,0 +1,291 @@
1
+ import {createWriteStream} from 'node:fs';
2
+ import {chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {Readable} from 'node:stream';
5
+ import {pipeline} from 'node:stream/promises';
6
+ import Is from 'strong-type';
7
+ import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
8
+ import {runProcess} from '../process.mjs';
9
+ import {createPortableNativeProvider} from './portable-provider.mjs';
10
+ import {outputFile, writeOutput} from './portable-layout.mjs';
11
+
12
+ const is = new Is(false);
13
+ const PROTOCOL = 'arcane-native-builder/1';
14
+ const MANIFEST = 'arcane-native.json';
15
+ const HOST_FILES = ['Arcane', 'runtime/node', 'runtime/NODE-LICENSE', 'LICENSE', 'COMMERCIAL-LICENSE.md', 'NOTICE'];
16
+ const EXECUTABLE = '../MacOS/Arcane';
17
+
18
+ function macosTarget(value) {
19
+ if (!['arm64', 'x64'].includes(value?.architecture)
20
+ || value.target !== `macos-${value.architecture}` || value.platform !== 'macos' || value.format !== 'app') {
21
+ throw new ArcaneError(ERROR_CODES.targetUnavailable, 'The macOS provider assembles macos-arm64 and macos-x64 application bundles.');
22
+ }
23
+ return {...value};
24
+ }
25
+
26
+ function portableRequest(target) {
27
+ return {...target, target: 'portable', format: 'portable'};
28
+ }
29
+
30
+ async function emit(onEvent, event) {
31
+ if (is.function(onEvent)) await onEvent(event);
32
+ }
33
+
34
+ async function hostFilesMissing(directory) {
35
+ const missing = [];
36
+ for (const relative of HOST_FILES) {
37
+ try {
38
+ if (!(await stat(path.join(directory, relative))).isFile()) missing.push(relative);
39
+ } catch (error) {
40
+ if (error.code !== 'ENOENT') throw error;
41
+ missing.push(relative);
42
+ }
43
+ }
44
+ return missing;
45
+ }
46
+
47
+ async function requireHostDirectory(directory) {
48
+ const resolved = await realpath(directory);
49
+ const missing = await hostFilesMissing(resolved);
50
+ if (missing.length) {
51
+ throw new ArcaneError(ERROR_CODES.prerequisiteMissing, 'The selected macOS host assets are incomplete.', {
52
+ details: {hostDirectory: resolved, missing}
53
+ });
54
+ }
55
+ return resolved;
56
+ }
57
+
58
+ async function removeOwnedDirectories(directories, error) {
59
+ const failures = [error];
60
+ for (const directory of directories) {
61
+ try { await rm(directory, {recursive: true, force: true}); }
62
+ catch (cleanupError) { failures.push(cleanupError); }
63
+ }
64
+ if (failures.length > 1) {
65
+ throw new AggregateError(failures, `macOS preparation failed; inspect the owned outputs at ${directories.join(', ')}.`);
66
+ }
67
+ throw error;
68
+ }
69
+
70
+ async function releasedHostDirectory(outputRoot, version, target, {signal, onEvent}) {
71
+ const cache = path.join(outputRoot, '.arcane-native-hosts', version);
72
+ const destination = path.join(cache, target.target);
73
+ try { return await requireHostDirectory(destination); }
74
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
75
+ throwIfAborted(signal);
76
+ await mkdir(cache, {recursive: true});
77
+ const temporary = await mkdtemp(path.join(cache, `preparing-${target.target}-`));
78
+ const archiveName = `arcane-native-macos-${target.architecture}.tar.gz`;
79
+ const archive = path.join(temporary, archiveName);
80
+ const extracted = path.join(temporary, 'host');
81
+ const url = `https://github.com/TheWizardNexus/arcane-os-sdk/releases/download/${version}/${archiveName}`;
82
+ try {
83
+ await emit(onEvent, {type: 'native.host.download.started', target: target.target, version, url});
84
+ throwIfAborted(signal);
85
+ const response = await fetch(url, {signal});
86
+ if (!response.ok) {
87
+ throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The SDK macOS host download returned HTTP ${response.status}.`, {
88
+ details: {url, status: response.status, response: await response.text()}
89
+ });
90
+ }
91
+ if (!response.body) throw new ArcaneError(ERROR_CODES.operationFailed, 'The SDK macOS host download returned no content.', {details: {url}});
92
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(archive, {flags: 'wx'}), {signal});
93
+ throwIfAborted(signal);
94
+ await mkdir(extracted);
95
+ await runProcess('tar', ['-xzf', archive, '-C', extracted], {cwd: temporary, signal, onEvent});
96
+ await requireHostDirectory(extracted);
97
+ throwIfAborted(signal);
98
+ try { await rename(extracted, destination); }
99
+ catch (error) {
100
+ // A concurrent build may have completed this exact SDK/architecture.
101
+ if (!['EEXIST', 'ENOTEMPTY', 'EPERM'].includes(error.code)) throw error;
102
+ try { await requireHostDirectory(destination); }
103
+ catch { throw error; }
104
+ }
105
+ await rm(temporary, {recursive: true, force: true});
106
+ await emit(onEvent, {type: 'native.host.download.completed', target: target.target, version, hostDirectory: destination});
107
+ throwIfAborted(signal);
108
+ return destination;
109
+ } catch (error) {
110
+ return removeOwnedDirectories([temporary], error);
111
+ }
112
+ }
113
+
114
+ function contains(root, candidate) {
115
+ const relative = path.relative(root, candidate);
116
+ return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
117
+ }
118
+
119
+ async function copyHostAssets(source, resources, files, signal, relative = '', ancestors = new Set()) {
120
+ throwIfAborted(signal);
121
+ const canonical = await realpath(source);
122
+ const output = await realpath(path.dirname(resources));
123
+ if (contains(canonical, output) || contains(output, canonical) || ancestors.has(canonical)) {
124
+ throw new ArcaneError(ERROR_CODES.usage, 'macOS host assets overlap their output or contain a directory cycle.', {
125
+ details: {source, outputRoot: resources}
126
+ });
127
+ }
128
+ const parents = new Set([...ancestors, canonical]);
129
+ for (const entry of await readdir(source, {withFileTypes: true})) {
130
+ throwIfAborted(signal);
131
+ const selected = path.join(source, entry.name);
132
+ const destination = relative ? `${relative}/${entry.name}` : entry.name;
133
+ const information = await stat(selected);
134
+ if (information.isDirectory()) {
135
+ await mkdir(outputFile(resources, destination), {recursive: true});
136
+ await copyHostAssets(selected, resources, files, signal, destination, parents);
137
+ } else if (information.isFile()) {
138
+ const content = await readFile(selected, {signal});
139
+ const launcher = destination === 'Arcane';
140
+ const actual = launcher ? path.resolve(resources, EXECUTABLE) : outputFile(resources, destination);
141
+ if (launcher) await writeFile(actual, content, {flag: 'wx', signal});
142
+ else await writeOutput(resources, destination, content, files, signal);
143
+ // These two selected assets are executables even when assembly is
144
+ // performed on a filesystem that cannot represent Unix mode bits.
145
+ const mode = launcher || destination === 'runtime/node' ? (information.mode & 0o777) | 0o111 : information.mode & 0o777;
146
+ await chmod(actual, mode);
147
+ } else {
148
+ throw new ArcaneError(ERROR_CODES.operationFailed, `Cannot copy macOS host asset ${selected}.`);
149
+ }
150
+ }
151
+ }
152
+
153
+ function plistText(value) {
154
+ return String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
155
+ .replaceAll('"', '&quot;').replaceAll("'", '&apos;');
156
+ }
157
+
158
+ function infoPlist(application) {
159
+ const values = {
160
+ CFBundleExecutable: 'Arcane', CFBundleIdentifier: `org.arcane.${application.id}`,
161
+ CFBundleName: application.displayName ?? application.id, CFBundleDisplayName: application.displayName ?? application.id,
162
+ CFBundlePackageType: 'APPL', CFBundleVersion: application.version ?? '1.0.0',
163
+ CFBundleShortVersionString: application.version ?? '1.0.0', LSMinimumSystemVersion: '11.0', NSPrincipalClass: 'NSApplication'
164
+ };
165
+ return [
166
+ '<?xml version="1.0" encoding="UTF-8"?>',
167
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
168
+ '<plist version="1.0"><dict>',
169
+ ...Object.entries(values).map(function property([key, value]) { return ` <key>${key}</key><string>${plistText(value)}</string>`; }),
170
+ '</dict></plist>', ''
171
+ ].join('\n');
172
+ }
173
+
174
+ async function requireBundle(resources) {
175
+ const root = await realpath(resources);
176
+ for (const relative of [...HOST_FILES.filter(function resource(name) { return name !== 'Arcane'; }), EXECUTABLE, '../Info.plist']) {
177
+ if (!(await stat(path.resolve(root, relative))).isFile()) {
178
+ throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The macOS application file is unavailable: ${relative}.`);
179
+ }
180
+ }
181
+ return root;
182
+ }
183
+
184
+ /** Compose complete portable app content with the selected SDK macOS host. */
185
+ export function createMacOSNativeProvider({services, hostDirectory} = {}) {
186
+ const portable = createPortableNativeProvider({services, packagedWeb: true, webKitDocumentLifecycle: true});
187
+ return {
188
+ protocol: PROTOCOL,
189
+ async describe() {
190
+ return {protocol: PROTOCOL, id: 'arcane-sdk-macos', targets: ['macos-arm64', 'macos-x64'], executable: true,
191
+ serviceConfiguration: 'explicit release module factories'};
192
+ },
193
+ async doctor({targetRequest, signal, onEvent} = {}) {
194
+ throwIfAborted(signal);
195
+ const target = macosTarget(targetRequest);
196
+ const core = await portable.doctor({targetRequest: portableRequest(target), signal});
197
+ const missing = hostDirectory === undefined ? [] : await hostFilesMissing(path.resolve(hostDirectory));
198
+ const result = {
199
+ target: target.target, executable: true, ready: core.ready && missing.length === 0,
200
+ sdkRoot: core.sdkRoot, missing: [...core.missing, ...missing],
201
+ hostDirectory: hostDirectory === undefined ? null : path.resolve(hostDirectory),
202
+ hostSource: hostDirectory === undefined ? 'sdk-release' : 'selected-directory',
203
+ downloadRequired: hostDirectory === undefined,
204
+ runtimePrerequisite: 'macOS 11 or newer with AppKit and WebKit, also satisfying the selected Node runtime requirements',
205
+ buildPrerequisite: hostDirectory === undefined ? 'tar' : null
206
+ };
207
+ await emit(onEvent, {type: 'native.doctor.completed', ...result});
208
+ throwIfAborted(signal);
209
+ return result;
210
+ },
211
+ async prepare({targetRequest, signal, onEvent} = {}) {
212
+ throwIfAborted(signal);
213
+ const target = macosTarget(targetRequest);
214
+ const selection = await portable.prepare({targetRequest: portableRequest(target), signal});
215
+ const selected = hostDirectory === undefined ? undefined : await requireHostDirectory(hostDirectory);
216
+ const result = {...selection, target, executable: true, ...(selected ? {hostDirectory: selected} : {})};
217
+ await emit(onEvent, {type: 'native.prepared', target: target.target, sdkVersion: selection.sdkVersion});
218
+ throwIfAborted(signal);
219
+ return result;
220
+ },
221
+ async build(request = {}) {
222
+ const {signal, onEvent} = request;
223
+ throwIfAborted(signal);
224
+ const target = macosTarget(request.targetRequest);
225
+ const selected = hostDirectory ?? request.toolchain?.hostDirectory;
226
+ const selectedHost = selected === undefined ? null : await requireHostDirectory(selected);
227
+ const artifact = await portable.build({
228
+ ...request, targetRequest: portableRequest(target),
229
+ async onEvent(event) {
230
+ if (event.type !== 'native.payload.completed') await emit(onEvent, {...event, target: target.target});
231
+ }
232
+ });
233
+ const portableRoot = artifact.target.rootDir;
234
+ let assembly;
235
+ try {
236
+ const host = selectedHost ?? await releasedHostDirectory(path.resolve(request.outputRoot), artifact.manifest.sdk.version, target, {signal, onEvent});
237
+ throwIfAborted(signal);
238
+ assembly = await mkdtemp(path.join(path.resolve(request.outputRoot), 'arcane-macos-'));
239
+ const plist = outputFile(assembly, `${request.appDescriptor.id}.app/Contents/Info.plist`);
240
+ const contents = path.dirname(plist);
241
+ const root = path.join(contents, 'Resources');
242
+ const bundleRoot = path.dirname(contents);
243
+ await mkdir(path.join(contents, 'MacOS'), {recursive: true});
244
+ await rename(portableRoot, root);
245
+ const files = [...artifact.manifest.files];
246
+ await copyHostAssets(host, root, files, signal);
247
+ await writeFile(plist, infoPlist(request.appDescriptor), {flag: 'wx', signal});
248
+ const manifest = {
249
+ ...artifact.manifest, kind: 'arcane-macos-native', target,
250
+ host: {
251
+ executable: EXECUTABLE, platform: 'macos', architecture: target.architecture, runtime: 'webkit',
252
+ coreExecutable: 'runtime/node', coreEntry: 'runtime/arcane-core.mjs',
253
+ bundleIdentifier: `org.arcane.${request.appDescriptor.id}`, infoPlist: '../Info.plist'
254
+ },
255
+ files
256
+ };
257
+ await writeFile(path.join(root, MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`, {signal});
258
+ throwIfAborted(signal);
259
+ await emit(onEvent, {type: 'native.payload.completed', target: target.target, appId: request.appDescriptor.id, outputRoot: root, bundleRoot});
260
+ throwIfAborted(signal);
261
+ return {...artifact, target: {...target, rootDir: root}, bundleRoot, manifest};
262
+ } catch (error) {
263
+ return removeOwnedDirectories(assembly ? [portableRoot, assembly] : [portableRoot], error);
264
+ }
265
+ },
266
+ async verify({artifact, targetRequest, signal, onEvent} = {}) {
267
+ throwIfAborted(signal);
268
+ const target = macosTarget(targetRequest);
269
+ const result = await portable.verify({artifact, targetRequest: portableRequest(target), signal});
270
+ const root = await requireBundle(result.outputRoot);
271
+ const verified = {...result, target: target.target, executable: true, bundleRoot: path.dirname(path.dirname(root))};
272
+ await emit(onEvent, {type: 'native.verified', target: target.target, outputRoot: root});
273
+ throwIfAborted(signal);
274
+ return verified;
275
+ },
276
+ async run({artifact, targetRequest, signal, onEvent} = {}) {
277
+ throwIfAborted(signal);
278
+ const target = macosTarget(targetRequest);
279
+ if (process.platform !== 'darwin' || process.arch !== target.architecture) {
280
+ throw new ArcaneError(ERROR_CODES.nativeRunUnsupported, `Running this macOS ${target.architecture} artifact requires a matching macOS host.`);
281
+ }
282
+ const root = await requireBundle(artifact?.target?.rootDir);
283
+ return runProcess(path.resolve(root, EXECUTABLE), ['--close-on-stdin-eof'], {
284
+ cwd: root, signal, onEvent, cancellationMode: 'close-input'
285
+ });
286
+ }
287
+ };
288
+ }
289
+
290
+ export const arcaneNativeBuilderProvider = createMacOSNativeProvider();
291
+ export default arcaneNativeBuilderProvider;
@@ -149,20 +149,53 @@ export async function copyCoreRuntime(root, files, signal, {
149
149
  return metadata;
150
150
  }
151
151
 
152
- export function coreEntrySource(application, version, services) {
152
+ export function coreEntrySource(application, version, services, {localAI, runtimes = [], packagedWeb = false} = {}) {
153
153
  const imports = services.map(function serviceImport(service, index) {
154
154
  const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
155
155
  return `import createService${index} from ${JSON.stringify(specifier)};`;
156
156
  });
157
+ // JSON parsing retains authored field names that have object-literal syntax.
157
158
  const definitions = services.map(function serviceDefinition(service, index) {
158
- return ` createService${index}(${JSON.stringify(service.options === undefined ? {} : service.options)})`;
159
+ return ` createService${index}(JSON.parse(${JSON.stringify(JSON.stringify(service.options === undefined ? {} : service.options))}), context)`;
159
160
  });
161
+ if (localAI !== undefined) {
162
+ imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
163
+ definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), context)`);
164
+ }
165
+ if (packagedWeb) {
166
+ imports.push("import {createPackagedWebService} from 'arcane-os/core/packaged-web';");
167
+ definitions.push(" createPackagedWebService({artifactRoot: fileURLToPath(new URL('../', import.meta.url))}, context)");
168
+ }
160
169
  return [
161
- "import {startCoreHost} from 'arcane-os/core/host';",
170
+ "import {fileURLToPath} from 'node:url';",
171
+ "import {readCoreLaunchContext, startCoreHost} from 'arcane-os/core/host';",
162
172
  ...imports,
163
173
  '',
174
+ ...(localAI === undefined ? [] : [
175
+ '// Bundled runtime paths are artifact-root-relative POSIX paths.',
176
+ 'function runtimePath(relative) {',
177
+ " return fileURLToPath(new URL('../' + relative.split('/').map(encodeURIComponent).join('/'), import.meta.url));",
178
+ '}',
179
+ ''
180
+ ]),
181
+ '// Factories share launch context as their second argument; authored options stay first.',
182
+ '// Explicit launch fields override artifact-relative defaults unchanged.',
183
+ 'const context = {',
184
+ " appRoot: fileURLToPath(new URL('../app/', import.meta.url)),",
185
+ ...(localAI === undefined ? [] : [
186
+ ` runtimes: JSON.parse(${JSON.stringify(JSON.stringify(runtimes))}).map(function runtimeLocation(runtime) {`,
187
+ ' const resolved = {...runtime};',
188
+ " for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory']) {",
189
+ ' if (runtime[field] !== undefined) resolved[field] = runtimePath(runtime[field]);',
190
+ ' }',
191
+ ' return resolved;',
192
+ ' }),'
193
+ ]),
194
+ ' ...await readCoreLaunchContext()',
195
+ '};',
196
+ '',
164
197
  'const host = startCoreHost({',
165
- ` application: ${JSON.stringify(application)},`,
198
+ ` application: JSON.parse(${JSON.stringify(JSON.stringify(application))}),`,
166
199
  ` version: ${JSON.stringify(version)},`,
167
200
  ` services: [\n${definitions.join(',\n')}\n ]`,
168
201
  '});',
@@ -61,7 +61,7 @@ function serviceSelection(services, release) {
61
61
  if (!is.string(service?.module) || !release.files.includes(service.module)) {
62
62
  throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The selected release does not contain service module ${String(service?.module)}.`);
63
63
  }
64
- return {module: service.module, options: service.options === undefined ? {} : service.options};
64
+ return {...service, options: service.options === undefined ? {} : service.options};
65
65
  });
66
66
  }
67
67
 
@@ -81,7 +81,7 @@ function eventOwnerPath(release) {
81
81
  }
82
82
 
83
83
  /** Package-owned assembly; never imports an OS checkout or application service. */
84
- export function createPortableNativeProvider({services = []} = {}) {
84
+ export function createPortableNativeProvider({services, packagedWeb = false, webKitDocumentLifecycle = false} = {}) {
85
85
  return {
86
86
  protocol: PROTOCOL,
87
87
  async describe() {
@@ -120,7 +120,8 @@ export function createPortableNativeProvider({services = []} = {}) {
120
120
  const {appDescriptor, release, signal, onEvent} = request;
121
121
  throwIfAborted(signal);
122
122
  const target = portableTarget(request.targetRequest);
123
- const selectedServices = serviceSelection(services, release);
123
+ const localAI = appDescriptor.native?.localAI;
124
+ const selectedServices = serviceSelection(services ?? appDescriptor.native?.services ?? [], release);
124
125
  const eventOwnerModuleURL = eventOwnerPath(release);
125
126
  const output = await outputDirectory(request);
126
127
  throwIfAborted(signal);
@@ -130,7 +131,33 @@ export function createPortableNativeProvider({services = []} = {}) {
130
131
  const files = [];
131
132
  try {
132
133
  await emit(onEvent, {type: 'native.payload.started', appId: appDescriptor.id, outputRoot: root});
133
- await copyRelease({...request, releaseRoot: request.appReleaseRoot}, 'app', root, files, signal);
134
+ async function bundleSelectedRuntimes() {
135
+ if (localAI === undefined) return {runtimes: [], files: []};
136
+ const {bundleLocalAIRuntimes} = await import('../local-ai/install.mjs');
137
+ return bundleLocalAIRuntimes({
138
+ runtimes: localAI.runtimes,
139
+ directory: path.join(output, 'local-ai-runtimes'),
140
+ outputRoot: root,
141
+ platform: target.platform === 'windows' ? 'win32' : target.platform === 'macos' ? 'darwin' : target.platform,
142
+ architecture: target.architecture,
143
+ signal,
144
+ onEvent
145
+ });
146
+ }
147
+ // These outputs have separate directories. Join both owners
148
+ // before cleanup so neither can keep writing a removed assembly.
149
+ const preparation = await Promise.allSettled([
150
+ copyRelease({...request, releaseRoot: request.appReleaseRoot}, 'app', root, files, signal),
151
+ bundleSelectedRuntimes()
152
+ ]);
153
+ const failures = preparation.filter(function failedPreparation(result) { return result.status === 'rejected'; });
154
+ if (failures.length === 1) throw failures[0].reason;
155
+ if (failures.length > 1) {
156
+ throw new AggregateError(failures.map(function preparationError(result) { return result.reason; }),
157
+ 'Portable application and local runtime preparation failed.');
158
+ }
159
+ const bundled = preparation[1].value;
160
+ files.push(...bundled.files);
134
161
  const dependencies = [];
135
162
  for (const [index, dependency] of (request.dependencies ?? []).entries()) {
136
163
  const directory = `dependencies/${index}`;
@@ -155,8 +182,11 @@ export function createPortableNativeProvider({services = []} = {}) {
155
182
  appSdk = {name: metadata.name, version: metadata.version, root: destination};
156
183
  }
157
184
  await writeOutput(root, 'package.json', '{"type":"module"}\n', files, signal);
158
- await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices), files, signal);
159
- await writeOutput(root, 'runtime/arcane-api.js', await createCoreClassicSource({eventOwnerModuleURL}), files, signal);
185
+ await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices,
186
+ {localAI, runtimes: bundled.runtimes, packagedWeb}), files, signal);
187
+ await writeOutput(root, 'runtime/arcane-api.js', await createCoreClassicSource({
188
+ eventOwnerModuleURL, replayRuntimeState: true, webKitDocumentLifecycle
189
+ }), files, signal);
160
190
  const manifest = {
161
191
  schemaVersion: 1,
162
192
  kind: 'arcane-portable-native',
@@ -166,8 +196,14 @@ export function createPortableNativeProvider({services = []} = {}) {
166
196
  target,
167
197
  webRoot: 'app',
168
198
  start: release.manifest?.app?.start ?? './index.html',
169
- core: {entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices},
170
- client: {source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL},
199
+ core: {
200
+ entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
201
+ ...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
202
+ },
203
+ client: {
204
+ source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL, replayRuntimeState: true,
205
+ ...(webKitDocumentLifecycle ? {webKitDocumentLifecycle: true} : {})
206
+ },
171
207
  dependencies,
172
208
  minimumCoreVersion: request.minimumCoreVersion ?? null,
173
209
  files: [...files, MANIFEST]