arcane-os 0.57.0 → 0.59.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 (60) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -2
  3. package/browser-runtime/ai/core-onnx.mjs +96 -0
  4. package/browser-runtime/ai/onnx-tensors.mjs +85 -0
  5. package/browser-runtime/core/client.mjs +152 -17
  6. package/docs/architecture.md +5 -2
  7. package/docs/platform-targets.md +30 -13
  8. package/docs/reference/README.md +22 -3
  9. package/docs/reference/availability-and-normalization.md +14 -7
  10. package/docs/reference/cli.md +22 -10
  11. package/docs/reference/core-client.md +87 -0
  12. package/docs/reference/core-native-packaging.md +129 -9
  13. package/docs/reference/core-preferences.md +49 -0
  14. package/docs/reference/diarization.md +206 -0
  15. package/docs/reference/git-text-snapshot.md +104 -0
  16. package/docs/reference/inventory/package-api.json +205 -23
  17. package/docs/reference/local-ai.md +145 -11
  18. package/docs/reference/protocols.md +28 -19
  19. package/docs/reference/pwa.md +74 -4
  20. package/docs/reference/sdk-api.md +589 -39
  21. package/package.json +11 -3
  22. package/schemas/arcane-app.schema.json +3 -3
  23. package/schemas/arcane-package.schema.json +2 -2
  24. package/schemas/native-build-plan.schema.json +27 -1
  25. package/schemas/target-adapter.schema.json +2 -0
  26. package/src/app-descriptor.mjs +1 -1
  27. package/src/cli/main.mjs +14 -4
  28. package/src/constants.mjs +2 -0
  29. package/src/core/host.mjs +14 -5
  30. package/src/core/hosts/macos/ArcaneHost.h +86 -0
  31. package/src/core/hosts/macos/ArcaneHost.m +455 -0
  32. package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
  33. package/src/core/hosts/windows/ArcaneLauncher.cs +4 -1
  34. package/src/core/services/local-ai.mjs +82 -4
  35. package/src/core/services/packaged-web.mjs +89 -0
  36. package/src/core/services/preferences.mjs +201 -0
  37. package/src/core-classic-source.mjs +2 -2
  38. package/src/dev-server.mjs +2 -5
  39. package/src/diarization/build.mjs +35 -0
  40. package/src/diarization/index.mjs +432 -0
  41. package/src/diarization/native/CMakeLists.txt +9 -0
  42. package/src/diarization/native/main.cpp +269 -0
  43. package/src/diarization/service.mjs +129 -0
  44. package/src/git-text-snapshot.mjs +265 -0
  45. package/src/import-map.mjs +9 -4
  46. package/src/index.mjs +1 -0
  47. package/src/local-ai/config.mjs +1 -1
  48. package/src/local-ai/install.mjs +134 -19
  49. package/src/local-ai/onnx-worker.mjs +85 -0
  50. package/src/local-ai/onnx.mjs +368 -0
  51. package/src/native/macos-provider.mjs +291 -0
  52. package/src/native/portable-layout.mjs +10 -2
  53. package/src/native/portable-provider.mjs +10 -5
  54. package/src/native-plan.mjs +9 -1
  55. package/src/native-provider-loader.mjs +3 -1
  56. package/src/process.mjs +123 -17
  57. package/src/scaffold.mjs +1 -1
  58. package/src/targets/index.mjs +11 -1
  59. package/src/templates/workspace-template.mjs +6 -5
  60. package/src/toolchain.mjs +42 -13
@@ -149,7 +149,7 @@ export async function copyCoreRuntime(root, files, signal, {
149
149
  return metadata;
150
150
  }
151
151
 
152
- export function coreEntrySource(application, version, services, {localAI, runtimes = []} = {}) {
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)};`;
@@ -162,6 +162,10 @@ export function coreEntrySource(application, version, services, {localAI, runtim
162
162
  imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
163
163
  definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), context)`);
164
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
+ }
165
169
  return [
166
170
  "import {fileURLToPath} from 'node:url';",
167
171
  "import {readCoreLaunchContext, startCoreHost} from 'arcane-os/core/host';",
@@ -180,7 +184,11 @@ export function coreEntrySource(application, version, services, {localAI, runtim
180
184
  " appRoot: fileURLToPath(new URL('../app/', import.meta.url)),",
181
185
  ...(localAI === undefined ? [] : [
182
186
  ` runtimes: JSON.parse(${JSON.stringify(JSON.stringify(runtimes))}).map(function runtimeLocation(runtime) {`,
183
- ' return {...runtime, root: runtimePath(runtime.root), executable: runtimePath(runtime.executable)};',
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;',
184
192
  ' }),'
185
193
  ]),
186
194
  ' ...await readCoreLaunchContext()',
@@ -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() {
@@ -138,7 +138,7 @@ export function createPortableNativeProvider({services} = {}) {
138
138
  runtimes: localAI.runtimes,
139
139
  directory: path.join(output, 'local-ai-runtimes'),
140
140
  outputRoot: root,
141
- platform: target.platform === 'windows' ? 'win32' : target.platform,
141
+ platform: target.platform === 'windows' ? 'win32' : target.platform === 'macos' ? 'darwin' : target.platform,
142
142
  architecture: target.architecture,
143
143
  signal,
144
144
  onEvent
@@ -183,8 +183,10 @@ export function createPortableNativeProvider({services} = {}) {
183
183
  }
184
184
  await writeOutput(root, 'package.json', '{"type":"module"}\n', files, signal);
185
185
  await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices,
186
- {localAI, runtimes: bundled.runtimes}), files, signal);
187
- await writeOutput(root, 'runtime/arcane-api.js', await createCoreClassicSource({eventOwnerModuleURL, replayRuntimeState: true}), files, signal);
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);
188
190
  const manifest = {
189
191
  schemaVersion: 1,
190
192
  kind: 'arcane-portable-native',
@@ -198,7 +200,10 @@ export function createPortableNativeProvider({services} = {}) {
198
200
  entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
199
201
  ...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
200
202
  },
201
- client: {source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL, replayRuntimeState: true},
203
+ client: {
204
+ source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL, replayRuntimeState: true,
205
+ ...(webKitDocumentLifecycle ? {webKitDocumentLifecycle: true} : {})
206
+ },
202
207
  dependencies,
203
208
  minimumCoreVersion: request.minimumCoreVersion ?? null,
204
209
  files: [...files, MANIFEST]
@@ -12,7 +12,7 @@ export const NATIVE_BUILDER_PROTOCOL='arcane-native-builder/1';
12
12
 
13
13
  const NATIVE_TARGETS={
14
14
  portable:{
15
- platforms:new Set(['windows','linux']),
15
+ platforms:new Set(['windows','linux','macos']),
16
16
  architectures:new Set(['x64','arm64']),
17
17
  format:'portable',
18
18
  signingMode:'unsigned-local-test',
@@ -22,6 +22,14 @@ const NATIVE_TARGETS={
22
22
  platforms:new Set(['windows']),architectures:new Set(['x64']),format:'exe',
23
23
  signingMode:'unsigned-local-test',signingProfileId:null
24
24
  },
25
+ 'macos-arm64':{
26
+ platforms:new Set(['macos']),architectures:new Set(['arm64']),format:'app',
27
+ signingMode:'unsigned-local-test',signingProfileId:null
28
+ },
29
+ 'macos-x64':{
30
+ platforms:new Set(['macos']),architectures:new Set(['x64']),format:'app',
31
+ signingMode:'unsigned-local-test',signingProfileId:null
32
+ },
25
33
  'linux-x64':{
26
34
  platforms:new Set(['linux']),architectures:new Set(['x64']),format:'deb',
27
35
  signingMode:'unsigned-local-test',signingProfileId:null
@@ -24,7 +24,9 @@ const FIXED_NATIVE_PROVIDER_PATHS={
24
24
 
25
25
  const SDK_NATIVE_PROVIDER_PATHS={
26
26
  portable:'./native/portable-provider.mjs',
27
- 'windows-x64':'./native/windows-provider.mjs'
27
+ 'windows-x64':'./native/windows-provider.mjs',
28
+ 'macos-arm64':'./native/macos-provider.mjs',
29
+ 'macos-x64':'./native/macos-provider.mjs'
28
30
  };
29
31
 
30
32
  export const ARCANE_NATIVE_PROVIDER_PATHS={
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,6 +141,10 @@ 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,
@@ -128,6 +152,23 @@ export async function runProcess(command,args=[],{
128
152
  cancellationMode='terminate-tree'
129
153
  }={}){
130
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
+ }
131
172
  if(!is.array(args)||args.some(argument=>!is.string(argument))){
132
173
  throw new ArcaneError(ERROR_CODES.usage,'Process arguments must be a fixed array of strings.');
133
174
  }
@@ -155,8 +196,8 @@ export async function runProcess(command,args=[],{
155
196
  throwIfAborted(signal);
156
197
 
157
198
  return new Promise((resolve,reject)=>{
158
- let stdout='';
159
- let stderr='';
199
+ let stdout=outputEncodings.stdout!==null&&outputSelected(captureOutput,'stdout')?'':null;
200
+ let stderr=outputEncodings.stderr!==null&&outputSelected(captureOutput,'stderr')?'':null;
160
201
  let childClosed=false;
161
202
  let closedResult=null;
162
203
  let settlementStarted=false;
@@ -166,12 +207,28 @@ export async function runProcess(command,args=[],{
166
207
  let escalation=null;
167
208
  let spawnError=null;
168
209
  let inputError=null;
210
+ let inputIterator=null;
211
+ let inputPump=null;
212
+ let inputReturn=null;
213
+ let inputDone=false;
214
+ const outputErrors=[];
169
215
  let child;
170
216
  let heartbeat=null;
171
217
  const deliveries=new Set();
172
218
 
173
219
  const ownDelivery=(stream,type,chunk)=>{
174
- 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
+ });
175
232
  deliveries.add(delivery);
176
233
  void delivery.then(()=>deliveries.delete(delivery));
177
234
  };
@@ -182,6 +239,26 @@ export async function runProcess(command,args=[],{
182
239
  }
183
240
  };
184
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
+
185
262
  const finish=(callback,value)=>{
186
263
  if(settlementStarted){
187
264
  return;
@@ -191,16 +268,17 @@ export async function runProcess(command,args=[],{
191
268
  clearTimeout(escalation);
192
269
  signal?.removeEventListener('abort',abort);
193
270
  void (async()=>{
271
+ await drainInput();
194
272
  let callbackFailure=null;
195
273
  try{
196
274
  await events.drain();
197
275
  }catch(error){
198
276
  callbackFailure=error;
199
277
  }
200
- if(cancellationMode==='close-input'&&closedResult){
278
+ if((cancellationMode==='close-input'&&closedResult)||inputIterator||onOutput){
201
279
  const failures=[...new Set([
202
280
  callback===reject?value:null,
203
- callbackFailure,inputError,cancellationError,spawnError
281
+ callbackFailure,inputError,cancellationError,spawnError,...outputErrors
204
282
  ].filter(error=>error!==null))];
205
283
  if(failures.length>0){
206
284
  const primary=failures[0];
@@ -234,10 +312,10 @@ export async function runProcess(command,args=[],{
234
312
  return;
235
313
  }
236
314
  terminationRequested=true;
315
+ stopInput();
237
316
  if(cancellationMode==='close-input'){
238
317
  // The selected host owns shutdown after EOF. Keep observing its
239
318
  // complete output and exit; accepted durable work may outlive UI.
240
- child?.stdin.end();
241
319
  return;
242
320
  }
243
321
  terminateProcessTree(child);
@@ -306,16 +384,16 @@ export async function runProcess(command,args=[],{
306
384
  return;
307
385
  }
308
386
 
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');
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);
313
391
  child.stdout.on('data',chunk=>{
314
- stdout=appendOutput(stdout,chunk);
392
+ if(stdout!==null)stdout=appendOutput(stdout,chunk);
315
393
  ownDelivery(child.stdout,'process.stdout',chunk);
316
394
  });
317
395
  child.stderr.on('data',chunk=>{
318
- stderr=appendOutput(stderr,chunk);
396
+ if(stderr!==null)stderr=appendOutput(stderr,chunk);
319
397
  ownDelivery(child.stderr,'process.stderr',chunk);
320
398
  });
321
399
  child.on('error',error=>{
@@ -335,6 +413,7 @@ export async function runProcess(command,args=[],{
335
413
  signal?.removeEventListener('abort',abort);
336
414
  clearInterval(heartbeat);
337
415
  void (async()=>{
416
+ await drainInput();
338
417
  await drainDeliveries();
339
418
  if(settlementStarted)return;
340
419
  const result={
@@ -359,7 +438,7 @@ export async function runProcess(command,args=[],{
359
438
  });
360
439
  }
361
440
  if(cancellationError)cancellationError.details=result;
362
- finish(reject,events.error??inputError??cancellationError??new ArcaneError(
441
+ finish(reject,events.error??inputError??outputErrors[0]??cancellationError??new ArcaneError(
363
442
  ERROR_CODES.operationFailed,
364
443
  `Stopped ${command} after its event callback failed.`
365
444
  ));
@@ -379,6 +458,10 @@ export async function runProcess(command,args=[],{
379
458
  {cause:inputError,details:result}));
380
459
  return;
381
460
  }
461
+ if(outputErrors.length){
462
+ finish(reject,outputErrors[0]);
463
+ return;
464
+ }
382
465
  await events.enqueue({
383
466
  type:'process.completed',
384
467
  message:`${command} exited with code ${String(result.code)}.`,
@@ -401,7 +484,30 @@ export async function runProcess(command,args=[],{
401
484
  abort();
402
485
  }
403
486
 
404
- if(cancellationMode==='close-input'){
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'){
405
511
  child.stdin.on('error',error=>{inputError=error;stopTree();});
406
512
  if(input!==undefined&&!terminationRequested)child.stdin.write(input);
407
513
  }else if(input!==undefined){
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,7 +1,7 @@
1
1
  import Is from 'strong-type';
2
2
  import path from 'node:path';
3
3
  import {resolveAppRoot,resolvePackageOutputRoot} from './app-layout.mjs';
4
- import {readdir,lstat,realpath,mkdir,readFile,writeFile} from 'node:fs/promises';
4
+ import {readdir,lstat,realpath,mkdir,readFile,writeFile,unlink} from 'node:fs/promises';
5
5
  import {createWorkspace,initWorkspace} from './scaffold.mjs';
6
6
  import {
7
7
  discoverApps as discoverWorkspaceApps,
@@ -12,8 +12,14 @@ import {
12
12
  } from './workspace.mjs';
13
13
  import {loadArcaneIntegratedProvider} from './integrated-provider-loader.mjs';
14
14
  import {collectSourcePwaAssets,startDevServer} from './dev-server.mjs';
15
- import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
16
- import {createPwaArtifacts} from './pwa.mjs';
15
+ import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext,removePwaEntryReferences} from './import-map.mjs';
16
+ import {
17
+ createPwaArtifacts,
18
+ PWA_MANIFEST_NAME,
19
+ PWA_OFFLINE_MANIFEST_NAME,
20
+ PWA_WORKER_NAME,
21
+ PWA_BOOTSTRAP_NAME
22
+ } from './pwa.mjs';
17
23
  import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
18
24
  import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
19
25
  import {refreshAppPackageProjection} from './app-descriptor.mjs';
@@ -293,7 +299,7 @@ async function validatePreparedRuntime(prepared,{signal}={}){
293
299
  return validation;
294
300
  }
295
301
 
296
- async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperationLease}={}){
302
+ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperationLease}={},retireGeneratedPwa=false){
297
303
  const manifest=prepared.validation.app.manifest;
298
304
  const inspected=await inspectPackagedApp({
299
305
  workspaceRoot:prepared.workspaceRoot,
@@ -322,12 +328,12 @@ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperat
322
328
  onEvent
323
329
  });
324
330
  if(prepared.validation.config.appsRoot==='.'){
325
- await refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent});
331
+ await refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent,retireGeneratedPwa});
326
332
  }
327
333
  return importMap;
328
334
  }
329
335
 
330
- async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent}){
336
+ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent,retireGeneratedPwa=false}){
331
337
  const {workspaceRoot,appId}=prepared;
332
338
  const manifest=prepared.validation.app.manifest;
333
339
  const installed=await readInstalledSdkLayout(workspaceRoot,prepared.validation.config);
@@ -385,21 +391,44 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
385
391
  await mkdir(path.dirname(filePath),{recursive:true});
386
392
  await writeFile(filePath,file.content,'utf8');
387
393
  }
388
- if(pwa){
394
+ const pwaDisabled=manifest.pwa?.enabled===false;
395
+ const retireConfirmedOutput=pwaDisabled&&retireGeneratedPwa===true;
396
+ if(pwa||pwaDisabled){
389
397
  for(const document of inspected.browserDocuments){
390
398
  throwIfAborted(signal);
391
399
  const filePath=path.join(prepared.appRoot,...document.path.split('/'));
392
400
  const content=await readFile(filePath,'utf8');
393
- await writeFile(filePath,applyPwaEntryReferences(content,{
401
+ const updated=pwa?applyPwaEntryReferences(content,{
394
402
  manifestUrl:`/${pwa.entryAssets.manifest}`,
395
- bootstrapUrl:`/${pwa.entryAssets.bootstrap}`
396
- }),'utf8');
403
+ bootstrapUrl:`/${pwa.entryAssets.bootstrap}`,
404
+ markGeneratedManifest:true
405
+ }):removePwaEntryReferences(content,{
406
+ documentUrl:new URL(`/${document.path.split('/').map(encodeURIComponent).join('/')}`,sourceOrigin),
407
+ manifestUrl:`/${PWA_MANIFEST_NAME}`,
408
+ markedManifestOnly:!retireConfirmedOutput
409
+ });
410
+ if(updated!==content)await writeFile(filePath,updated,'utf8');
411
+ }
412
+ }
413
+ const retiredPwa=[];
414
+ if(retireConfirmedOutput){
415
+ // The caller declares these standard outputs generated, not merely same-named authored files.
416
+ // Remove selected references before retiring their targets; browser storage is untouched.
417
+ for(const relative of [PWA_MANIFEST_NAME,PWA_OFFLINE_MANIFEST_NAME,PWA_WORKER_NAME,PWA_BOOTSTRAP_NAME]){
418
+ throwIfAborted(signal);
419
+ try{
420
+ await unlink(path.join(workspaceRoot,relative));
421
+ retiredPwa.push(relative);
422
+ }catch(error){
423
+ if(error.code!=='ENOENT')throw error;
424
+ }
397
425
  }
398
426
  }
399
427
  await emit(onEvent,{
400
428
  type:'import-map.root-files.completed',appId,
401
429
  navigation:[],
402
- pwa:pwa?.files.map(file=>file.path)??[]
430
+ pwa:pwa?.files.map(file=>file.path)??[],
431
+ retiredPwa
403
432
  });
404
433
  }
405
434
 
@@ -408,7 +437,7 @@ async function importMapApplication(options={}){
408
437
  const prepared=await preparedWorkspace({...options,allowMissingManagedImportMap:true});
409
438
  const importMap=await ownedWork(
410
439
  'import-map',
411
- context=>refreshPreparedImportMap(prepared,context),
440
+ context=>refreshPreparedImportMap(prepared,context,options.retireGeneratedPwa===true),
412
441
  {...options,workspaceRoot:prepared.workspaceRoot}
413
442
  );
414
443
  await validatePreparedRuntime(prepared,{signal:options.signal});
@@ -868,7 +897,7 @@ export function assertIntegratedNativeToolchain({
868
897
  workspaceMode,workspaceRoot,toolchainRoot,target,providerSource='arcane-checkout'
869
898
  }={}){
870
899
  if(workspaceMode!=='integrated')return;
871
- if(providerSource==='sdk-package'&&(target==='portable'||target==='windows-x64'))return;
900
+ if(providerSource==='sdk-package'&&['portable','windows-x64','macos-arm64','macos-x64'].includes(target))return;
872
901
  if(!is.string(toolchainRoot)||!sameCanonicalPath(workspaceRoot,toolchainRoot)){
873
902
  throw new ArcaneError(
874
903
  ERROR_CODES.policyDenied,