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
package/src/process.mjs CHANGED
@@ -45,10 +45,30 @@ async function emitLines(events,type,text){
45
45
  }
46
46
  }
47
47
 
48
- function deliverChunk(stream,events,type,text){
48
+ function outputSelected(option,stream){
49
+ return is.boolean(option)?option:option?.[stream]??true;
50
+ }
51
+
52
+ function outputEncodingSelected(option,stream){
53
+ const selected=option!==null&&is.object(option)?option[stream]:option;
54
+ const encoding=selected===undefined?'utf8':selected;
55
+ if(encoding!=='utf8'&&encoding!==null){
56
+ throw new TypeError(`The ${stream} outputEncoding must be 'utf8' or null.`);
57
+ }
58
+ return encoding;
59
+ }
60
+
61
+ function deliverChunk(stream,events,type,chunk,{onOutput,emitOutput,onFailure}){
49
62
  stream.pause();
50
- return emitLines(events,type,text).catch(()=>{
51
- // The queue owns and propagates the first callback failure.
63
+ return (async function deliverOutput(){
64
+ try{
65
+ if(onOutput)await onOutput({stream:type==='process.stdout'?'stdout':'stderr',chunk});
66
+ }catch(error){
67
+ onFailure(error);
68
+ }
69
+ if(emitOutput)await emitLines(events,type,chunk);
70
+ })().catch(()=>{
71
+ // The event queue separately owns and propagates observer failures.
52
72
  }).finally(()=>{
53
73
  if(!stream.destroyed){
54
74
  stream.resume();
@@ -121,15 +141,40 @@ export async function runProcess(command,args=[],{
121
141
  env,
122
142
  signal,
123
143
  onEvent,
144
+ onOutput,
145
+ captureOutput=true,
146
+ emitOutputEvents=true,
147
+ outputEncoding='utf8',
124
148
  heartbeatMs=5000,
125
149
  terminationGraceMs=DEFAULT_TERMINATION_GRACE_MS,
126
150
  allowNonzero=false,
127
- input
151
+ input,
152
+ cancellationMode='terminate-tree'
128
153
  }={}){
129
154
  throwIfAborted(signal);
155
+ if(onOutput!==undefined&&!is.function(onOutput)){
156
+ throw new TypeError('onOutput must be a function.');
157
+ }
158
+ const outputEncodings={
159
+ stdout:outputEncodingSelected(outputEncoding,'stdout'),
160
+ stderr:outputEncodingSelected(outputEncoding,'stderr')
161
+ };
162
+ for(const stream of ['stdout','stderr']){
163
+ if(outputEncodings[stream]===null&&(!onOutput
164
+ ||outputSelected(captureOutput,stream)!==false
165
+ ||outputSelected(emitOutputEvents,stream)!==false)){
166
+ throw new TypeError(`Raw ${stream} requires onOutput, captureOutput:false and emitOutputEvents:false for that stream.`);
167
+ }
168
+ }
169
+ if(!onOutput&&(!outputSelected(captureOutput,'stdout')||!outputSelected(captureOutput,'stderr'))){
170
+ throw new TypeError('Uncaptured process output requires an onOutput consumer.');
171
+ }
130
172
  if(!is.array(args)||args.some(argument=>!is.string(argument))){
131
173
  throw new ArcaneError(ERROR_CODES.usage,'Process arguments must be a fixed array of strings.');
132
174
  }
175
+ if(cancellationMode!=='terminate-tree'&&cancellationMode!=='close-input'){
176
+ throw new ArcaneError(ERROR_CODES.usage,'Unknown process cancellation mode.');
177
+ }
133
178
  if(!is.integer(terminationGraceMs)||terminationGraceMs<100||terminationGraceMs>30_000){
134
179
  throw new ArcaneError(
135
180
  ERROR_CODES.usage,
@@ -151,20 +196,39 @@ export async function runProcess(command,args=[],{
151
196
  throwIfAborted(signal);
152
197
 
153
198
  return new Promise((resolve,reject)=>{
154
- let stdout='';
155
- let stderr='';
199
+ let stdout=outputEncodings.stdout!==null&&outputSelected(captureOutput,'stdout')?'':null;
200
+ let stderr=outputEncodings.stderr!==null&&outputSelected(captureOutput,'stderr')?'':null;
201
+ let childClosed=false;
202
+ let closedResult=null;
156
203
  let settlementStarted=false;
157
204
  let cancellationRequested=false;
158
205
  let cancellationError=null;
159
206
  let terminationRequested=false;
160
207
  let escalation=null;
161
208
  let spawnError=null;
209
+ let inputError=null;
210
+ let inputIterator=null;
211
+ let inputPump=null;
212
+ let inputReturn=null;
213
+ let inputDone=false;
214
+ const outputErrors=[];
162
215
  let child;
163
216
  let heartbeat=null;
164
217
  const deliveries=new Set();
165
218
 
166
219
  const ownDelivery=(stream,type,chunk)=>{
167
- const delivery=deliverChunk(stream,events,type,chunk);
220
+ const name=type==='process.stdout'?'stdout':'stderr';
221
+ const delivery=deliverChunk(stream,events,type,chunk,{
222
+ onOutput,
223
+ emitOutput:outputEncodings[name]!==null&&outputSelected(emitOutputEvents,name),
224
+ onFailure:function outputFailed(error){
225
+ // Preserve complete failed callback input even when the
226
+ // caller elected not to retain successful protocol output.
227
+ outputErrors.push(new ArcaneError(ERROR_CODES.operationFailed,
228
+ `The ${name} output callback failed.`,{cause:error,details:{stream:name,chunk}}));
229
+ stopTree();
230
+ }
231
+ });
168
232
  deliveries.add(delivery);
169
233
  void delivery.then(()=>deliveries.delete(delivery));
170
234
  };
@@ -175,6 +239,26 @@ export async function runProcess(command,args=[],{
175
239
  }
176
240
  };
177
241
 
242
+ function stopInput(){
243
+ if(inputIterator&&!inputDone&&!inputReturn){
244
+ inputReturn=Promise.resolve().then(async function returnInput(){
245
+ await inputIterator.return?.();
246
+ }).catch(function inputReturnFailed(error){
247
+ inputError=inputError&&inputError!==error
248
+ ?new AggregateError([inputError,error],'Process input and its cleanup failed.'):error;
249
+ });
250
+ }
251
+ child?.stdin.end();
252
+ }
253
+
254
+ async function drainInput(){
255
+ stopInput();
256
+ // AsyncIterable producers own cancellation of an outstanding next().
257
+ // Their return() must cooperate so owned cleanup can really finish.
258
+ await inputPump;
259
+ await inputReturn;
260
+ }
261
+
178
262
  const finish=(callback,value)=>{
179
263
  if(settlementStarted){
180
264
  return;
@@ -184,9 +268,38 @@ export async function runProcess(command,args=[],{
184
268
  clearTimeout(escalation);
185
269
  signal?.removeEventListener('abort',abort);
186
270
  void (async()=>{
271
+ await drainInput();
272
+ let callbackFailure=null;
187
273
  try{
188
274
  await events.drain();
189
- }catch(callbackFailure){
275
+ }catch(error){
276
+ callbackFailure=error;
277
+ }
278
+ if((cancellationMode==='close-input'&&closedResult)||inputIterator||onOutput){
279
+ const failures=[...new Set([
280
+ callback===reject?value:null,
281
+ callbackFailure,inputError,cancellationError,spawnError,...outputErrors
282
+ ].filter(error=>error!==null))];
283
+ if(failures.length>0){
284
+ const primary=failures[0];
285
+ // Owned cancellation/exit errors already carry this exact
286
+ // result. Other errors retain their identity and cause in
287
+ // the complete failure record instead of being rewritten.
288
+ if(failures.length===1&&primary?.details===closedResult){
289
+ reject(primary);
290
+ return;
291
+ }
292
+ const failure=new ArcaneError(
293
+ primary?.code??ERROR_CODES.operationFailed,
294
+ primary?.message??`Could not complete ${command}.`,
295
+ {cause:primary,details:closedResult,exitCode:primary?.exitCode}
296
+ );
297
+ failure.errors=failures;
298
+ reject(failure);
299
+ return;
300
+ }
301
+ }
302
+ if(callbackFailure){
190
303
  reject(callbackFailure);
191
304
  return;
192
305
  }
@@ -195,10 +308,16 @@ export async function runProcess(command,args=[],{
195
308
  };
196
309
 
197
310
  const stopTree=()=>{
198
- if(terminationRequested||settlementStarted){
311
+ if(terminationRequested||settlementStarted||childClosed){
199
312
  return;
200
313
  }
201
314
  terminationRequested=true;
315
+ stopInput();
316
+ if(cancellationMode==='close-input'){
317
+ // The selected host owns shutdown after EOF. Keep observing its
318
+ // complete output and exit; accepted durable work may outlive UI.
319
+ return;
320
+ }
202
321
  terminateProcessTree(child);
203
322
  escalation=setTimeout(()=>{
204
323
  if(childIsRunning(child)){
@@ -214,7 +333,7 @@ export async function runProcess(command,args=[],{
214
333
  };
215
334
 
216
335
  const abort=()=>{
217
- if(cancellationRequested||settlementStarted){
336
+ if(cancellationRequested||settlementStarted||childClosed){
218
337
  return;
219
338
  }
220
339
  cancellationRequested=true;
@@ -225,7 +344,9 @@ export async function runProcess(command,args=[],{
225
344
  );
226
345
  void events.enqueue({
227
346
  type:'process.cancellation.requested',
228
- message:`Stopping ${command} and its child processes.`,
347
+ message:cancellationMode==='close-input'
348
+ ?`Closing ${command} input and waiting for its owned shutdown.`
349
+ :`Stopping ${command} and its child processes.`,
229
350
  data:{command,pid:child?.pid??null}
230
351
  });
231
352
  stopTree();
@@ -263,16 +384,16 @@ export async function runProcess(command,args=[],{
263
384
  return;
264
385
  }
265
386
 
266
- // Each readable retains split UTF-8 sequences and flushes at EOF.
267
- // Capture and events consume the same decoded text.
268
- child.stdout.setEncoding('utf8');
269
- child.stderr.setEncoding('utf8');
387
+ // Text streams retain split UTF-8 sequences and flush at EOF. Explicit
388
+ // raw streams bypass decoding and deliver native Buffers only to onOutput.
389
+ if(outputEncodings.stdout!==null)child.stdout.setEncoding(outputEncodings.stdout);
390
+ if(outputEncodings.stderr!==null)child.stderr.setEncoding(outputEncodings.stderr);
270
391
  child.stdout.on('data',chunk=>{
271
- stdout=appendOutput(stdout,chunk);
392
+ if(stdout!==null)stdout=appendOutput(stdout,chunk);
272
393
  ownDelivery(child.stdout,'process.stdout',chunk);
273
394
  });
274
395
  child.stderr.on('data',chunk=>{
275
- stderr=appendOutput(stderr,chunk);
396
+ if(stderr!==null)stderr=appendOutput(stderr,chunk);
276
397
  ownDelivery(child.stderr,'process.stderr',chunk);
277
398
  });
278
399
  child.on('error',error=>{
@@ -286,8 +407,13 @@ export async function runProcess(command,args=[],{
286
407
  }
287
408
  });
288
409
  child.on('close',(code,terminationSignal)=>{
410
+ // The child lifetime has ended even while its output/event callbacks
411
+ // are still draining. A later abort cannot cancel that completed work.
412
+ childClosed=true;
413
+ signal?.removeEventListener('abort',abort);
289
414
  clearInterval(heartbeat);
290
415
  void (async()=>{
416
+ await drainInput();
291
417
  await drainDeliveries();
292
418
  if(settlementStarted)return;
293
419
  const result={
@@ -299,10 +425,11 @@ export async function runProcess(command,args=[],{
299
425
  stdout,
300
426
  stderr
301
427
  };
428
+ closedResult=result;
302
429
  if(terminationRequested){
303
430
  // The direct child has exited. Make one final tree-wide attempt so
304
431
  // a descendant that ignored the graceful request cannot outlive it.
305
- terminateProcessTree(child,{force:true});
432
+ if(cancellationMode!=='close-input')terminateProcessTree(child,{force:true});
306
433
  if(cancellationRequested){
307
434
  await events.enqueue({
308
435
  type:'process.cancelled',
@@ -310,7 +437,8 @@ export async function runProcess(command,args=[],{
310
437
  data:{command,code:result.code,signal:terminationSignal}
311
438
  });
312
439
  }
313
- finish(reject,events.error??cancellationError??new ArcaneError(
440
+ if(cancellationError)cancellationError.details=result;
441
+ finish(reject,events.error??inputError??outputErrors[0]??cancellationError??new ArcaneError(
314
442
  ERROR_CODES.operationFailed,
315
443
  `Stopped ${command} after its event callback failed.`
316
444
  ));
@@ -324,6 +452,16 @@ export async function runProcess(command,args=[],{
324
452
  ));
325
453
  return;
326
454
  }
455
+ if(inputError){
456
+ finish(reject,new ArcaneError(ERROR_CODES.operationFailed,
457
+ `Could not write ${command} input: ${inputError.message}`,
458
+ {cause:inputError,details:result}));
459
+ return;
460
+ }
461
+ if(outputErrors.length){
462
+ finish(reject,outputErrors[0]);
463
+ return;
464
+ }
327
465
  await events.enqueue({
328
466
  type:'process.completed',
329
467
  message:`${command} exited with code ${String(result.code)}.`,
@@ -346,7 +484,33 @@ export async function runProcess(command,args=[],{
346
484
  abort();
347
485
  }
348
486
 
349
- if(input!==undefined){
487
+ if(input&&is.function(input[Symbol.asyncIterator])){
488
+ child.stdin.on('error',error=>{inputError=error;stopTree();});
489
+ inputPump=(async function pumpInput(){
490
+ try{
491
+ inputIterator=input[Symbol.asyncIterator]();
492
+ while(!terminationRequested&&!childClosed){
493
+ const next=await inputIterator.next();
494
+ if(next.done){inputDone=true;break;}
495
+ if(terminationRequested||childClosed)break;
496
+ await new Promise(function writeChunk(resolve,reject){
497
+ child.stdin.write(next.value,function written(error){
498
+ if(error)reject(error);
499
+ else resolve();
500
+ });
501
+ });
502
+ }
503
+ child.stdin.end();
504
+ }catch(error){
505
+ inputError=inputError&&inputError!==error
506
+ ?new AggregateError([inputError,error],'Process input failed.'):error;
507
+ stopTree();
508
+ }
509
+ })();
510
+ }else if(cancellationMode==='close-input'){
511
+ child.stdin.on('error',error=>{inputError=error;stopTree();});
512
+ if(input!==undefined&&!terminationRequested)child.stdin.write(input);
513
+ }else if(input!==undefined){
350
514
  child.stdin.end(input);
351
515
  }else{
352
516
  child.stdin.end();
package/src/scaffold.mjs CHANGED
@@ -44,7 +44,7 @@ function validateInputs(appId,displayName){
44
44
  }
45
45
 
46
46
  function validateScaffoldTarget(target){
47
- const targets=['browser','portable','windows-x64','linux-x64','linux-arm64','android-arm64'];
47
+ const targets=['browser','portable','windows-x64','macos-arm64','macos-x64','linux-x64','linux-arm64','android-arm64'];
48
48
  if(!targets.includes(target)){
49
49
  fail(`Invalid scaffold target: ${String(target)}. Expected one of ${targets.join(', ')}.`,'ARCANE_USAGE');
50
50
  }
@@ -19,7 +19,7 @@ const DEFINITIONS=[
19
19
  },
20
20
  {
21
21
  id:'portable',displayName:'Portable native application',status:'pairing-required',
22
- platforms:['windows','linux'],architectures:['x64','arm64'],formats:['portable'],
22
+ platforms:['windows','linux','macos'],architectures:['x64','arm64'],formats:['portable'],
23
23
  signingModes:['unsigned-local-test'],
24
24
  reason:'Portable app payloads use the SDK package provider by default; explicit provider pairing remains available.'
25
25
  },
@@ -28,6 +28,16 @@ const DEFINITIONS=[
28
28
  platforms:['windows'],architectures:['x64'],formats:['exe'],signingModes:['unsigned-local-test'],
29
29
  reason:'Windows output requires an Arcane OS native provider.'
30
30
  },
31
+ {
32
+ id:'macos-arm64',displayName:'macOS Apple silicon application',status:'pairing-required',
33
+ platforms:['macos'],architectures:['arm64'],formats:['app'],signingModes:['unsigned-local-test'],
34
+ reason:'macOS output uses the SDK provider with the selected architecture host asset.'
35
+ },
36
+ {
37
+ id:'macos-x64',displayName:'macOS Intel application',status:'pairing-required',
38
+ platforms:['macos'],architectures:['x64'],formats:['app'],signingModes:['unsigned-local-test'],
39
+ reason:'macOS output uses the SDK provider with the selected architecture host asset.'
40
+ },
31
41
  {
32
42
  id:'linux-x64',displayName:'Linux x64 executable',status:'pairing-required',
33
43
  platforms:['linux'],architectures:['x64'],formats:['deb'],signingModes:['unsigned-local-test'],
@@ -84,7 +84,7 @@ export function workspaceTemplate({
84
84
  ||(!canonicalSpecifier&&!aliasSpecifier)){
85
85
  throw new Error('Invalid scaffold SDK installation authority.');
86
86
  }
87
- const supportedTargets=['browser','portable','windows-x64','linux-x64','linux-arm64','android-arm64'];
87
+ const supportedTargets=['browser','portable','windows-x64','macos-arm64','macos-x64','linux-x64','linux-arm64','android-arm64'];
88
88
  if(!supportedTargets.includes(target)){
89
89
  throw new Error(`Unsupported scaffold target: ${String(target)}.`);
90
90
  }
@@ -105,17 +105,18 @@ export function workspaceTemplate({
105
105
  const packageInclude=[`${appId}.css`,'index.html'];
106
106
  if(native)packageInclude.push('img/icon.png');
107
107
  packageInclude.push('manifest.json','modules');
108
+ const macosTarget=target==='macos-arm64'||target==='macos-x64';
108
109
  const nativeGuide=native?`
109
110
  ## ${target} native target
110
111
 
111
112
  This scaffold declares both the browser and ${target} targets and includes the
112
113
  required raster application icon.
113
- Pair it with one explicit Arcane OS checkout:
114
+ ${macosTarget?'Use the installed SDK provider and its matching macOS release host asset:':'Pair it with one explicit Arcane OS checkout:'}
114
115
 
115
116
  \`\`\`sh
116
- npm exec -- arcane native-doctor --target ${target} --arcane-root "<path-to-Arcane-OS>"
117
- npm run build -- --arcane-root "<path-to-Arcane-OS>"
118
- ${target==='portable'?'':`npm run run -- --arcane-root "<path-to-Arcane-OS>"\n`}
117
+ npm exec -- arcane native-doctor --target ${target}${macosTarget?'':' --arcane-root "<path-to-Arcane-OS>"'}
118
+ npm run build${macosTarget?'':' -- --arcane-root "<path-to-Arcane-OS>"'}
119
+ ${target==='portable'?'':`npm run run${macosTarget?'':' -- --arcane-root "<path-to-Arcane-OS>"'}\n`}
119
120
  \`\`\`
120
121
 
121
122
  The selected Arcane provider must support this target or the command fails without
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'&&['portable','windows-x64','macos-arm64','macos-x64'].includes(target))return;
844
872
  if(!is.string(toolchainRoot)||!sameCanonicalPath(workspaceRoot,toolchainRoot)){
845
873
  throw new ArcaneError(
846
874
  ERROR_CODES.policyDenied,