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,89 @@
1
+ import {mkdir, readFile, writeFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {startDevServer} from '../../dev-server.mjs';
4
+ import {serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
5
+
6
+ /** Serve one packaged web root inside its existing Core lifetime. */
7
+ export function createPackagedWebService({artifactRoot}, context = {}) {
8
+ let server;
9
+ let closing = false;
10
+ let lifetime;
11
+ return {
12
+ name: 'packaged-web',
13
+ async start(runtime) {
14
+ const manifest = JSON.parse(await readFile(path.join(artifactRoot, 'arcane-native.json'), 'utf8'));
15
+ // The launcher chooses app state independently of its installation.
16
+ // Forward the application's launch file unchanged; the fallback is
17
+ // a separate native argument, not a rewritten application payload.
18
+ const stateFlag = process.argv.indexOf('--arcane-host-state-root');
19
+ const stateRoot = context.stateRoot ?? (stateFlag < 0 ? undefined : process.argv[stateFlag + 1]);
20
+ if (typeof stateRoot !== 'string' || !stateRoot) {
21
+ throw new TypeError('Packaged web serving requires the launcher-selected stateRoot.');
22
+ }
23
+ await mkdir(stateRoot, {recursive: true});
24
+ const originFile = path.join(stateRoot, 'packaged-web-origin.json');
25
+ let port = 0;
26
+ let saved = false;
27
+ try {
28
+ const origin = JSON.parse(await readFile(originFile, 'utf8'));
29
+ port = origin.port;
30
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
31
+ throw new TypeError('The saved packaged origin needs its previously selected listener port.');
32
+ }
33
+ saved = true;
34
+ } catch (error) {
35
+ if (error.code !== 'ENOENT') throw error;
36
+ }
37
+ try {
38
+ server = await startDevServer({
39
+ mode: 'packaged', http: true,
40
+ releaseRoot: path.resolve(artifactRoot, manifest.webRoot),
41
+ host: '127.0.0.1', port
42
+ });
43
+ // Only a first successful listener selects a port. A later bind
44
+ // conflict remains an error instead of moving saved browser data
45
+ // to a different origin. Concurrent first launches cannot replace
46
+ // one another's selection.
47
+ if (!saved) {
48
+ await writeFile(originFile, JSON.stringify({host: '127.0.0.1', port: server.port}) + '\n', {flag: 'wx'});
49
+ }
50
+ const url = new URL(manifest.start, server.origin).href;
51
+ runtime.emit('core.web.ready', {origin: server.origin, url, port: server.port});
52
+ lifetime = server.lifecycle.then(
53
+ function packagedServerEnded() {
54
+ if (!closing) {
55
+ runtime.emit('core.web.failed', serializeCoreError(
56
+ new Error('The packaged application listener closed unexpectedly.')
57
+ ));
58
+ }
59
+ },
60
+ function packagedServerFailed(error) {
61
+ if (!closing) runtime.emit('core.web.failed', serializeCoreError(error));
62
+ throw error;
63
+ }
64
+ );
65
+ // Retain the original rejection for drain while observing it
66
+ // now; the native diagnostic event owns immediate reporting.
67
+ lifetime.catch(function observePackagedServerFailure() {});
68
+ } catch (error) {
69
+ closing = true;
70
+ if (server) {
71
+ try { await server.close(); }
72
+ catch (closeError) {
73
+ throw new AggregateError([error, closeError], 'Packaged application startup and shutdown failed.');
74
+ }
75
+ }
76
+ throw error;
77
+ }
78
+ },
79
+ async drain() {
80
+ closing = true;
81
+ if (!server) return;
82
+ const results = await Promise.allSettled([server.close(), server.closed, lifetime]);
83
+ const failures = [...new Set(results.filter(function rejected(result) {
84
+ return result.status === 'rejected';
85
+ }).map(function reason(result) {return result.reason;}))];
86
+ if (failures.length) throw new AggregateError(failures, 'Packaged application serving shutdown failed.');
87
+ }
88
+ };
89
+ }
@@ -0,0 +1,201 @@
1
+ import {mkdir, mkdtemp, open, readFile, rename, rmdir, unlink} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import Is from 'strong-type';
4
+
5
+ const is = new Is(false);
6
+
7
+ // A selected file has one read/modify/write order within this Core process,
8
+ // including when separate service instances select the same file.
9
+ const fileOperations = new Map();
10
+
11
+ /**
12
+ * Native implementation of the existing Arcane.preferences methods.
13
+ * The application selects its file; namespaces, defaults and preference schemas
14
+ * remain with the caller. Factory construction performs no I/O.
15
+ *
16
+ * Writes have service lifetime. Once accepted, renderer cancellation cannot
17
+ * discard them. drain()/dispose() stop acceptance and await owned operations.
18
+ * Success follows a flushed file replacement, not a promise of filesystem
19
+ * survival across every possible operating-system or hardware failure.
20
+ */
21
+ export function createPreferencesService({file} = {}) {
22
+ if (!is.string(file) || file === '') {
23
+ throw new TypeError('Preferences require an application-selected file path.');
24
+ }
25
+ const filename = path.resolve(file);
26
+ const pending = new Set();
27
+ let closing = null;
28
+
29
+ function accept(operation) {
30
+ if (closing) {
31
+ const error = new Error('The preferences service is closing.');
32
+ error.code = 'CORE_CLOSING';
33
+ return Promise.reject(error);
34
+ }
35
+ const result = orderFileOperation(filename, operation);
36
+ pending.add(result);
37
+ function releaseOperation() { pending.delete(result); }
38
+ result.then(releaseOperation, releaseOperation);
39
+ return result;
40
+ }
41
+
42
+ function list(parameters, {signal} = {}) {
43
+ return accept(async function readKeys() {
44
+ const document = await readPreferences(filename, signal);
45
+ return {keys: Object.keys(document.entries).sort()};
46
+ });
47
+ }
48
+
49
+ function get({key}, {signal} = {}) {
50
+ return accept(async function readValue() {
51
+ requireKey(key);
52
+ const document = await readPreferences(filename, signal);
53
+ const found = Object.hasOwn(document.entries, key);
54
+ return {key, found, value: found ? document.entries[key] : null};
55
+ });
56
+ }
57
+
58
+ function set({key, value}) {
59
+ return accept(async function saveValue() {
60
+ requireKey(key);
61
+ const document = await readPreferences(filename);
62
+ setEntry(document.entries, key, value);
63
+ await writePreferences(filename, document);
64
+ return {key, value};
65
+ });
66
+ }
67
+
68
+ function setMany({entries}) {
69
+ return accept(async function saveBatch() {
70
+ if (!entries || !is.object(entries) || is.array(entries)) {
71
+ throw new TypeError('Preference entries must be a JSON object.');
72
+ }
73
+ const document = await readPreferences(filename);
74
+ const batch = Object.entries(entries);
75
+ for (const [key, value] of batch) setEntry(document.entries, key, value);
76
+ await writePreferences(filename, document);
77
+ return {keys: batch.map(function entryKey([key]) { return key; }).sort(), count: batch.length};
78
+ });
79
+ }
80
+
81
+ function deletePreference({key}) {
82
+ return accept(async function removeValue() {
83
+ requireKey(key);
84
+ const document = await readPreferences(filename);
85
+ const deleted = Object.hasOwn(document.entries, key);
86
+ if (deleted) {
87
+ delete document.entries[key];
88
+ await writePreferences(filename, document);
89
+ }
90
+ return {key, deleted};
91
+ });
92
+ }
93
+
94
+ function drain() {
95
+ if (closing) return closing;
96
+ closing = Promise.allSettled([...pending]).then(function operationsDrained(results) {
97
+ const failures = results.filter(function failed(result) {
98
+ return result.status === 'rejected';
99
+ }).map(function reason(result) { return result.reason; });
100
+ if (failures.length) throw new AggregateError(failures, 'Preference operations failed while draining.');
101
+ });
102
+ return closing;
103
+ }
104
+
105
+ return {
106
+ name: 'preferences',
107
+ methods: {
108
+ 'preferences.list': list,
109
+ 'preferences.get': get,
110
+ 'preferences.set': {lifetime: 'service', handle: set},
111
+ 'preferences.setMany': {lifetime: 'service', handle: setMany},
112
+ 'preferences.delete': {lifetime: 'service', handle: deletePreference}
113
+ },
114
+ drain,
115
+ dispose: drain
116
+ };
117
+ }
118
+
119
+ function orderFileOperation(file, operation) {
120
+ const previous = fileOperations.get(file) ?? Promise.resolve();
121
+ const result = previous.then(operation, operation);
122
+ function releaseQueue() {
123
+ if (fileOperations.get(file) === tail) fileOperations.delete(file);
124
+ }
125
+ // The caller retains the rejecting result. This fulfilled scheduling tail
126
+ // lets a later operation proceed after an earlier reported failure.
127
+ const tail = result.then(releaseQueue, releaseQueue);
128
+ fileOperations.set(file, tail);
129
+ return result;
130
+ }
131
+
132
+ async function readPreferences(file, signal) {
133
+ signal?.throwIfAborted();
134
+ let source;
135
+ try {
136
+ source = await readFile(file, {encoding: 'utf8', signal});
137
+ } catch (error) {
138
+ if (error.code !== 'ENOENT') throw error;
139
+ signal?.throwIfAborted();
140
+ return {schemaVersion: 1, entries: {}};
141
+ }
142
+ const document = JSON.parse(source);
143
+ if (!document || document.schemaVersion !== 1 || !document.entries
144
+ || !is.object(document.entries) || is.array(document.entries)) {
145
+ throw new TypeError(`Preferences at ${file} do not contain the existing schemaVersion 1 entries record.`);
146
+ }
147
+ return document;
148
+ }
149
+
150
+ function requireKey(key) {
151
+ if (!is.string(key)) throw new TypeError('A preference key must be a string.');
152
+ }
153
+
154
+ function setEntry(entries, key, value) {
155
+ // Define an ordinary own data property: "__proto__" is a preference key,
156
+ // not an instruction to change the entries object's prototype.
157
+ Object.defineProperty(entries, key, {value, enumerable: true, writable: true, configurable: true});
158
+ }
159
+
160
+ function jsonValue(key, value) {
161
+ const kind = typeof value;
162
+ if (value === undefined || kind === 'function' || kind === 'symbol'
163
+ || (kind === 'number' && !Number.isFinite(value))) {
164
+ throw new TypeError(`Preference value at ${JSON.stringify(key)} cannot be represented completely in JSON.`);
165
+ }
166
+ return value;
167
+ }
168
+
169
+ async function writePreferences(file, document) {
170
+ // Serialization happens before any filesystem write. The JSON boundary
171
+ // reports unrepresentable values instead of silently omitting them.
172
+ const source = JSON.stringify(document, jsonValue);
173
+ const directory = path.dirname(file);
174
+ await mkdir(directory, {recursive: true});
175
+ const temporaryDirectory = await mkdtemp(path.join(directory, '.arcane-preferences-'));
176
+ const temporary = path.join(temporaryDirectory, 'preferences.json');
177
+ const failures = [];
178
+ let handle = null;
179
+ try {
180
+ handle = await open(temporary, 'w');
181
+ await handle.writeFile(source, 'utf8');
182
+ await handle.sync();
183
+ await handle.close();
184
+ handle = null;
185
+ await rename(temporary, file);
186
+ } catch (error) {
187
+ failures.push(error);
188
+ } finally {
189
+ if (handle) {
190
+ try { await handle.close(); } catch (error) { failures.push(error); }
191
+ }
192
+ try { await unlink(temporary); } catch (error) {
193
+ if (error.code !== 'ENOENT') failures.push(error);
194
+ }
195
+ try { await rmdir(temporaryDirectory); } catch (error) { failures.push(error); }
196
+ }
197
+ if (failures.length === 1) throw failures[0];
198
+ if (failures.length > 1) throw new AggregateError(failures, 'Preference replacement and cleanup failed.');
199
+ }
200
+
201
+ export default createPreferencesService;
@@ -5,7 +5,7 @@ import {readFile} from 'node:fs/promises';
5
5
  * The two owned modules have declaration exports and two explicit imports;
6
6
  * this projection is deliberately not a general JavaScript bundler.
7
7
  */
8
- export async function createCoreClassicSource({eventOwnerModuleURL,replayRuntimeState=false}={}){
8
+ export async function createCoreClassicSource({eventOwnerModuleURL,replayRuntimeState=false,webKitDocumentLifecycle=false}={}){
9
9
  if(typeof eventOwnerModuleURL!=='string'||!eventOwnerModuleURL){
10
10
  throw new TypeError('The classic Core client needs the served SDK event-manager module URL.');
11
11
  }
@@ -37,7 +37,7 @@ ${implementation}
37
37
  },{once:true});
38
38
  })
39
39
  :loadSharedEventOwner();
40
- installCoreClient(global,{eventOwner:arcaneEvents,eventOwnerReady,replayRuntimeState:${replayRuntimeState===true}});
40
+ installCoreClient(global,{eventOwner:arcaneEvents,eventOwnerReady,replayRuntimeState:${replayRuntimeState===true}${webKitDocumentLifecycle===true?',webKitDocumentLifecycle:true':''}});
41
41
  })(globalThis);
42
42
  `;
43
43
  }
@@ -831,7 +831,7 @@ async function resolveDevelopmentTls({workspaceRoot,tls,certPath,keyPath,signal}
831
831
  }catch(error){
832
832
  if(error.code==='ENOENT'){
833
833
  fail(
834
- `Arcane development and packaged previews require HTTPS. `
834
+ `Arcane development and packaged previews default to HTTPS. `
835
835
  +`Provide a certificate at ${certificatePath} and a key at ${privateKeyPath}, `
836
836
  +'or select existing files with --cert and --key.',
837
837
  'ARCANE_DEV_TLS_MISSING'
@@ -854,6 +854,7 @@ async function startOwnedDevServer({
854
854
  tls,
855
855
  certPath,
856
856
  keyPath,
857
+ localAI,
857
858
  signal,
858
859
  sdkRuntimeSourceRoot
859
860
  }={},events,releaseSignal){
@@ -872,12 +873,9 @@ async function startOwnedDevServer({
872
873
  if (!is.boolean(http)) {
873
874
  fail('http must be a boolean.', 'ARCANE_USAGE');
874
875
  }
875
- if (http && mode !== 'source') {
876
- fail('HTTP serving is supported only in source development mode.', 'ARCANE_USAGE');
877
- }
878
876
  if (http && (requestedHttps === true || (tls !== undefined && tls !== null && tls !== false)
879
877
  || certPath !== undefined || keyPath !== undefined || httpPort !== 0)) {
880
- fail('HTTP development cannot combine TLS options or a separate httpPort; select its listener with port.', 'ARCANE_USAGE');
878
+ fail('HTTP serving cannot combine TLS options or a separate httpPort; select its listener with port.', 'ARCANE_USAGE');
881
879
  }
882
880
  const protocol = http ? 'http:' : 'https:';
883
881
  const requestedRuntimeMode=mode==='source'&&sdkRuntimeSourceRoot!==undefined
@@ -1263,6 +1261,7 @@ async function startOwnedDevServer({
1263
1261
  response.end();
1264
1262
  return;
1265
1263
  }
1264
+ if (localAI && await localAI.handler(request, response)) return;
1266
1265
  if(request.method!=='GET'&&request.method!=='HEAD'){
1267
1266
  deny(response,405,'Method not allowed.');
1268
1267
  return;
@@ -1498,6 +1497,7 @@ async function startOwnedDevServer({
1498
1497
  const cleanUrl=`${origin}${routeSet.startPath}`;
1499
1498
  const url=cleanUrl;
1500
1499
  let closeInitiated=false;
1500
+ let localAIClosing=Promise.resolve();
1501
1501
  let lifecycleSettlementStarted=false;
1502
1502
  let operationalError=null;
1503
1503
  let resolveLifecycle;
@@ -1518,6 +1518,8 @@ async function startOwnedDevServer({
1518
1518
  signal?.removeEventListener('abort',abort);
1519
1519
  for (const listener of listeners) listener.removeListener('error', serverFailed);
1520
1520
  try{
1521
+ const [localAIResult]=await Promise.allSettled([localAIClosing]);
1522
+ if(localAIResult.status==='rejected')operationalError??=localAIResult.reason;
1521
1523
  while(requestTasks.size>0){
1522
1524
  await Promise.allSettled([...requestTasks]);
1523
1525
  }
@@ -1556,6 +1558,8 @@ async function startOwnedDevServer({
1556
1558
  }
1557
1559
  if(!closeInitiated){
1558
1560
  closeInitiated=true;
1561
+ localAIClosing=Promise.resolve().then(function closeLocalAI(){return localAI?.close(error);});
1562
+ localAIClosing.catch(function observeLocalAICloseFailure(closeError){operationalError??=closeError;});
1559
1563
  try{
1560
1564
  closeDevelopmentListeners(fileServer, tlsServer).then(
1561
1565
  function developmentServerClosed() {
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {mkdir} from 'node:fs/promises';
4
+ import Is from 'strong-type';
5
+ import {runProcess} from '../process.mjs';
6
+
7
+ const is = new Is(false);
8
+
9
+ /** Build this host's first-party helper against its selected upstream runtime. */
10
+ export async function buildDiarizationHelper({runtime, outputRoot, signal, onEvent} = {}) {
11
+ if (!is.string(runtime?.cmakeDirectory) || !is.string(outputRoot)) {
12
+ throw new TypeError('The selected NeMo runtime cmakeDirectory and helper outputRoot are required.');
13
+ }
14
+ const root = path.resolve(outputRoot);
15
+ const build = path.join(root, 'build');
16
+ const source = fileURLToPath(new URL('./native/', import.meta.url));
17
+ await mkdir(root, {recursive: true});
18
+ const options = {signal, onEvent};
19
+ const configure = await runProcess('cmake', [
20
+ '-S', source,
21
+ '-B', build,
22
+ `-DNeMoSpeech_DIR=${path.resolve(runtime.cmakeDirectory)}`,
23
+ `-DCMAKE_INSTALL_PREFIX=${root}`,
24
+ '-DCMAKE_BUILD_TYPE=Release'
25
+ ], options);
26
+ const compile = await runProcess('cmake', ['--build', build, '--config', 'Release'], options);
27
+ const install = await runProcess('cmake', ['--install', build, '--config', 'Release'], options);
28
+ return {
29
+ platform: process.platform,
30
+ architecture: process.arch,
31
+ root,
32
+ executable: path.join(root, 'bin', process.platform === 'win32' ? 'arcane-diarization.exe' : 'arcane-diarization'),
33
+ diagnostics: {configure, compile, install}
34
+ };
35
+ }