arcane-os 0.55.0 → 0.57.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 (56) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +5 -3
  3. package/browser-runtime/ai/core-local.mjs +306 -0
  4. package/browser-runtime/core/client.mjs +75 -14
  5. package/browser-runtime/core/contracts.mjs +2 -1
  6. package/docs/consumer-updates.md +82 -0
  7. package/docs/publishing.md +41 -18
  8. package/docs/reference/README.md +31 -1
  9. package/docs/reference/availability-and-normalization.md +11 -3
  10. package/docs/reference/cli.md +9 -1
  11. package/docs/reference/core-client.md +45 -2
  12. package/docs/reference/core-native-packaging.md +195 -11
  13. package/docs/reference/core-runtime.md +10 -0
  14. package/docs/reference/inventory/package-api.json +160 -20
  15. package/docs/reference/local-ai.md +197 -0
  16. package/docs/reference/protocols.md +91 -14
  17. package/docs/reference/pwa.md +17 -0
  18. package/docs/reference/runtime-modules.md +6 -5
  19. package/docs/reference/sdk-api.md +165 -26
  20. package/package.json +5 -1
  21. package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
  22. package/schemas/arcane-app.schema.json +64 -0
  23. package/schemas/arcane-package.schema.json +54 -0
  24. package/src/app-descriptor.mjs +35 -2
  25. package/src/app-layout.mjs +6 -2
  26. package/src/cli/main.mjs +8 -2
  27. package/src/core/host.mjs +23 -0
  28. package/src/core/hosts/arcane-core-loader.cjs +2 -0
  29. package/src/core/hosts/linux/arcane_core_process.c +568 -0
  30. package/src/core/hosts/linux/arcane_core_process.h +127 -0
  31. package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
  32. package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
  33. package/src/core/hosts/sea-launcher.cjs +17 -0
  34. package/src/core/hosts/windows/ArcaneCoreProcess.cs +416 -0
  35. package/src/core/hosts/windows/ArcaneHost.cs +695 -0
  36. package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
  37. package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
  38. package/src/core/runtime.mjs +15 -3
  39. package/src/core/services/local-ai.mjs +543 -0
  40. package/src/core-classic-source.mjs +2 -2
  41. package/src/dev-server.mjs +7 -0
  42. package/src/import-map.mjs +1 -0
  43. package/src/local-ai/archive.mjs +52 -0
  44. package/src/local-ai/config.mjs +62 -0
  45. package/src/local-ai/dev.mjs +390 -0
  46. package/src/local-ai/discover.mjs +71 -0
  47. package/src/local-ai/http.mjs +106 -0
  48. package/src/local-ai/install.mjs +344 -0
  49. package/src/local-ai/server.mjs +262 -0
  50. package/src/native/portable-layout.mjs +29 -4
  51. package/src/native/portable-provider.mjs +39 -8
  52. package/src/native/windows-provider.mjs +262 -0
  53. package/src/native-provider-loader.mjs +8 -2
  54. package/src/packager/core.mjs +89 -15
  55. package/src/process.mjs +74 -12
  56. package/src/toolchain.mjs +38 -8
package/src/process.mjs CHANGED
@@ -33,8 +33,8 @@ function platformArguments(command,args,platform=process.platform){
33
33
  return [cliPath,...args];
34
34
  }
35
35
 
36
- function appendOutput(current,chunk){
37
- return current+chunk.toString('utf8');
36
+ function appendOutput(current,text){
37
+ return current+text;
38
38
  }
39
39
 
40
40
  async function emitLines(events,type,text){
@@ -45,9 +45,9 @@ async function emitLines(events,type,text){
45
45
  }
46
46
  }
47
47
 
48
- function deliverChunk(stream,events,type,chunk){
48
+ function deliverChunk(stream,events,type,text){
49
49
  stream.pause();
50
- return emitLines(events,type,chunk.toString('utf8')).catch(()=>{
50
+ return emitLines(events,type,text).catch(()=>{
51
51
  // The queue owns and propagates the first callback failure.
52
52
  }).finally(()=>{
53
53
  if(!stream.destroyed){
@@ -124,12 +124,16 @@ export async function runProcess(command,args=[],{
124
124
  heartbeatMs=5000,
125
125
  terminationGraceMs=DEFAULT_TERMINATION_GRACE_MS,
126
126
  allowNonzero=false,
127
- input
127
+ input,
128
+ cancellationMode='terminate-tree'
128
129
  }={}){
129
130
  throwIfAborted(signal);
130
131
  if(!is.array(args)||args.some(argument=>!is.string(argument))){
131
132
  throw new ArcaneError(ERROR_CODES.usage,'Process arguments must be a fixed array of strings.');
132
133
  }
134
+ if(cancellationMode!=='terminate-tree'&&cancellationMode!=='close-input'){
135
+ throw new ArcaneError(ERROR_CODES.usage,'Unknown process cancellation mode.');
136
+ }
133
137
  if(!is.integer(terminationGraceMs)||terminationGraceMs<100||terminationGraceMs>30_000){
134
138
  throw new ArcaneError(
135
139
  ERROR_CODES.usage,
@@ -153,12 +157,15 @@ export async function runProcess(command,args=[],{
153
157
  return new Promise((resolve,reject)=>{
154
158
  let stdout='';
155
159
  let stderr='';
160
+ let childClosed=false;
161
+ let closedResult=null;
156
162
  let settlementStarted=false;
157
163
  let cancellationRequested=false;
158
164
  let cancellationError=null;
159
165
  let terminationRequested=false;
160
166
  let escalation=null;
161
167
  let spawnError=null;
168
+ let inputError=null;
162
169
  let child;
163
170
  let heartbeat=null;
164
171
  const deliveries=new Set();
@@ -184,9 +191,37 @@ export async function runProcess(command,args=[],{
184
191
  clearTimeout(escalation);
185
192
  signal?.removeEventListener('abort',abort);
186
193
  void (async()=>{
194
+ let callbackFailure=null;
187
195
  try{
188
196
  await events.drain();
189
- }catch(callbackFailure){
197
+ }catch(error){
198
+ callbackFailure=error;
199
+ }
200
+ if(cancellationMode==='close-input'&&closedResult){
201
+ const failures=[...new Set([
202
+ callback===reject?value:null,
203
+ callbackFailure,inputError,cancellationError,spawnError
204
+ ].filter(error=>error!==null))];
205
+ if(failures.length>0){
206
+ const primary=failures[0];
207
+ // Owned cancellation/exit errors already carry this exact
208
+ // result. Other errors retain their identity and cause in
209
+ // the complete failure record instead of being rewritten.
210
+ if(failures.length===1&&primary?.details===closedResult){
211
+ reject(primary);
212
+ return;
213
+ }
214
+ const failure=new ArcaneError(
215
+ primary?.code??ERROR_CODES.operationFailed,
216
+ primary?.message??`Could not complete ${command}.`,
217
+ {cause:primary,details:closedResult,exitCode:primary?.exitCode}
218
+ );
219
+ failure.errors=failures;
220
+ reject(failure);
221
+ return;
222
+ }
223
+ }
224
+ if(callbackFailure){
190
225
  reject(callbackFailure);
191
226
  return;
192
227
  }
@@ -195,10 +230,16 @@ export async function runProcess(command,args=[],{
195
230
  };
196
231
 
197
232
  const stopTree=()=>{
198
- if(terminationRequested||settlementStarted){
233
+ if(terminationRequested||settlementStarted||childClosed){
199
234
  return;
200
235
  }
201
236
  terminationRequested=true;
237
+ if(cancellationMode==='close-input'){
238
+ // The selected host owns shutdown after EOF. Keep observing its
239
+ // complete output and exit; accepted durable work may outlive UI.
240
+ child?.stdin.end();
241
+ return;
242
+ }
202
243
  terminateProcessTree(child);
203
244
  escalation=setTimeout(()=>{
204
245
  if(childIsRunning(child)){
@@ -214,7 +255,7 @@ export async function runProcess(command,args=[],{
214
255
  };
215
256
 
216
257
  const abort=()=>{
217
- if(cancellationRequested||settlementStarted){
258
+ if(cancellationRequested||settlementStarted||childClosed){
218
259
  return;
219
260
  }
220
261
  cancellationRequested=true;
@@ -225,7 +266,9 @@ export async function runProcess(command,args=[],{
225
266
  );
226
267
  void events.enqueue({
227
268
  type:'process.cancellation.requested',
228
- message:`Stopping ${command} and its child processes.`,
269
+ message:cancellationMode==='close-input'
270
+ ?`Closing ${command} input and waiting for its owned shutdown.`
271
+ :`Stopping ${command} and its child processes.`,
229
272
  data:{command,pid:child?.pid??null}
230
273
  });
231
274
  stopTree();
@@ -263,6 +306,10 @@ export async function runProcess(command,args=[],{
263
306
  return;
264
307
  }
265
308
 
309
+ // Each readable retains split UTF-8 sequences and flushes at EOF.
310
+ // Capture and events consume the same decoded text.
311
+ child.stdout.setEncoding('utf8');
312
+ child.stderr.setEncoding('utf8');
266
313
  child.stdout.on('data',chunk=>{
267
314
  stdout=appendOutput(stdout,chunk);
268
315
  ownDelivery(child.stdout,'process.stdout',chunk);
@@ -282,6 +329,10 @@ export async function runProcess(command,args=[],{
282
329
  }
283
330
  });
284
331
  child.on('close',(code,terminationSignal)=>{
332
+ // The child lifetime has ended even while its output/event callbacks
333
+ // are still draining. A later abort cannot cancel that completed work.
334
+ childClosed=true;
335
+ signal?.removeEventListener('abort',abort);
285
336
  clearInterval(heartbeat);
286
337
  void (async()=>{
287
338
  await drainDeliveries();
@@ -295,10 +346,11 @@ export async function runProcess(command,args=[],{
295
346
  stdout,
296
347
  stderr
297
348
  };
349
+ closedResult=result;
298
350
  if(terminationRequested){
299
351
  // The direct child has exited. Make one final tree-wide attempt so
300
352
  // a descendant that ignored the graceful request cannot outlive it.
301
- terminateProcessTree(child,{force:true});
353
+ if(cancellationMode!=='close-input')terminateProcessTree(child,{force:true});
302
354
  if(cancellationRequested){
303
355
  await events.enqueue({
304
356
  type:'process.cancelled',
@@ -306,7 +358,8 @@ export async function runProcess(command,args=[],{
306
358
  data:{command,code:result.code,signal:terminationSignal}
307
359
  });
308
360
  }
309
- finish(reject,events.error??cancellationError??new ArcaneError(
361
+ if(cancellationError)cancellationError.details=result;
362
+ finish(reject,events.error??inputError??cancellationError??new ArcaneError(
310
363
  ERROR_CODES.operationFailed,
311
364
  `Stopped ${command} after its event callback failed.`
312
365
  ));
@@ -320,6 +373,12 @@ export async function runProcess(command,args=[],{
320
373
  ));
321
374
  return;
322
375
  }
376
+ if(inputError){
377
+ finish(reject,new ArcaneError(ERROR_CODES.operationFailed,
378
+ `Could not write ${command} input: ${inputError.message}`,
379
+ {cause:inputError,details:result}));
380
+ return;
381
+ }
323
382
  await events.enqueue({
324
383
  type:'process.completed',
325
384
  message:`${command} exited with code ${String(result.code)}.`,
@@ -342,7 +401,10 @@ export async function runProcess(command,args=[],{
342
401
  abort();
343
402
  }
344
403
 
345
- if(input!==undefined){
404
+ if(cancellationMode==='close-input'){
405
+ child.stdin.on('error',error=>{inputError=error;stopTree();});
406
+ if(input!==undefined&&!terminationRequested)child.stdin.write(input);
407
+ }else if(input!==undefined){
346
408
  child.stdin.end(input);
347
409
  }else{
348
410
  child.stdin.end();
package/src/toolchain.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import Is from 'strong-type';
2
2
  import path from 'node:path';
3
- import {resolvePackageOutputRoot} from './app-layout.mjs';
3
+ import {resolveAppRoot,resolvePackageOutputRoot} from './app-layout.mjs';
4
4
  import {readdir,lstat,realpath,mkdir,readFile,writeFile} from 'node:fs/promises';
5
5
  import {createWorkspace,initWorkspace} from './scaffold.mjs';
6
6
  import {
@@ -17,6 +17,8 @@ import {createPwaArtifacts} from './pwa.mjs';
17
17
  import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
18
18
  import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
19
19
  import {refreshAppPackageProjection} from './app-descriptor.mjs';
20
+ import {configureDevelopmentLocalAI} from './local-ai/config.mjs';
21
+ import {createDevelopmentLocalAI} from './local-ai/dev.mjs';
20
22
  import {
21
23
  discoverApps as discoverPackagerApps,
22
24
  inspectApp as inspectPackagedApp,
@@ -566,6 +568,11 @@ export async function developApplication(options = {}) {
566
568
  onEvent: options.onEvent
567
569
  },
568
570
  async function refreshDevelopmentWorkspace(workspaceOperationLease) {
571
+ await configureDevelopmentLocalAI({
572
+ appRoot: resolveAppRoot(profile.workspaceRoot, profile.config, appId),
573
+ runtimes: options.localAI,
574
+ signal: options.signal
575
+ });
569
576
  await refreshAppPackageProjection(
570
577
  {
571
578
  workspaceRoot: profile.workspaceRoot,
@@ -589,7 +596,19 @@ export async function developApplication(options = {}) {
589
596
  return current;
590
597
  }
591
598
  );
592
- const server=await startDevServer({
599
+ const localAIConfig = prepared.descriptor?.native?.localAI;
600
+ const localAI = localAIConfig?.runtimes.length ? createDevelopmentLocalAI({
601
+ config: localAIConfig,
602
+ appRoot: prepared.appRoot,
603
+ directory: path.join(prepared.appRoot, '.arcane', 'local-ai', 'runtimes'),
604
+ application: prepared.descriptor,
605
+ version: prepared.descriptor.version,
606
+ signal: options.signal,
607
+ onEvent: options.onEvent
608
+ }) : undefined;
609
+ let server;
610
+ try {
611
+ server=await startDevServer({
593
612
  workspaceRoot:prepared.workspaceRoot,
594
613
  appId:prepared.appId,
595
614
  mode:'source',
@@ -605,10 +624,19 @@ export async function developApplication(options = {}) {
605
624
  tls:options.tls,
606
625
  certPath:options.certPath,
607
626
  keyPath:options.keyPath,
627
+ localAI,
608
628
  signal:options.signal,
609
629
  onEvent:options.onEvent
610
- });
611
- return {...server,appId:prepared.appId,mode:'source'};
630
+ });
631
+ } catch (error) {
632
+ try {
633
+ await localAI?.close(error);
634
+ } catch (closeError) {
635
+ throw new AggregateError([error, closeError], 'Development startup and local AI shutdown failed.');
636
+ }
637
+ throw error;
638
+ }
639
+ return {...server,appId:prepared.appId,mode:'source',...(localAI ? {localAI} : {})};
612
640
  }
613
641
 
614
642
  export async function packageApplication(options={}){
@@ -840,7 +868,7 @@ export function assertIntegratedNativeToolchain({
840
868
  workspaceMode,workspaceRoot,toolchainRoot,target,providerSource='arcane-checkout'
841
869
  }={}){
842
870
  if(workspaceMode!=='integrated')return;
843
- if(providerSource==='sdk-package'&&target==='portable')return;
871
+ if(providerSource==='sdk-package'&&(target==='portable'||target==='windows-x64'))return;
844
872
  if(!is.string(toolchainRoot)||!sameCanonicalPath(workspaceRoot,toolchainRoot)){
845
873
  throw new ArcaneError(
846
874
  ERROR_CODES.policyDenied,
@@ -996,12 +1024,14 @@ function pathsOverlap(left,right){
996
1024
  async function packageNativeRelease(prepared,app,{
997
1025
  workspaceOperationLease,
998
1026
  selectedSdk,
1027
+ target,
999
1028
  signal,
1000
1029
  onEvent
1001
1030
  }={}){
1002
1031
  const release=await packageApp({
1003
1032
  workspaceRoot:prepared.workspaceRoot,
1004
1033
  appId:app.appId,
1034
+ target,
1005
1035
  browserPwa:false,
1006
1036
  workspaceOperationLease,
1007
1037
  signal,
@@ -1065,7 +1095,7 @@ async function executePairedNativeBuild(options,adapter,{
1065
1095
  const packageOutputs=[];
1066
1096
  for(const app of [prepared.validation.app,...dependencyApps]){
1067
1097
  const releaseRoot=resolvePackageOutputRoot(
1068
- prepared.workspaceRoot,prepared.validation.config,app.manifest
1098
+ prepared.workspaceRoot,prepared.validation.config,app.manifest,{target}
1069
1099
  );
1070
1100
  if(pathsOverlap(releaseRoot,outputRoot)
1071
1101
  ||packageOutputs.some(existing=>pathsOverlap(existing,releaseRoot))){
@@ -1084,13 +1114,13 @@ async function executePairedNativeBuild(options,adapter,{
1084
1114
  descriptorSource:prepared.validation.app.descriptorSource,
1085
1115
  descriptorPath:prepared.validation.app.descriptorPath,
1086
1116
  validation:prepared.validation
1087
- },{workspaceOperationLease,selectedSdk,signal,onEvent});
1117
+ },{workspaceOperationLease,selectedSdk,target,signal,onEvent});
1088
1118
  const dependencyReleases=[];
1089
1119
  for(const dependency of dependencyApps){
1090
1120
  dependencyReleases.push(await packageNativeRelease(
1091
1121
  prepared,
1092
1122
  dependency,
1093
- {workspaceOperationLease,selectedSdk,signal,onEvent}
1123
+ {workspaceOperationLease,selectedSdk,target,signal,onEvent}
1094
1124
  ));
1095
1125
  }
1096
1126
  const toolchain=await adapter.prepare({