arcane-os 0.29.1 → 0.31.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.
@@ -489,7 +489,7 @@ files. Their explicit precedence and credential compatibility rules are in the
489
489
  | CLI's early default host, port, and send/serve profile values | Y/Y/Y | Remove premature default assignment. The same defaults remain after file configuration resolves, while explicit CLI values retain priority. Assigning defaults before reading JSON would conceal the deployment's chosen settings. |
490
490
  | `origins` string array and list replacement | Y/Y/N | Keep one exact list for multiple caller domains. Explicit options replace the file list, including empty arrays. Existing `origin`, `allowTo`, `errorTo`, and `requestTimeout` aliases remain compatible and win over their canonical programmatic names when both are supplied. No normalization or list-merging helper is needed. |
491
491
  | Root provider-key, named-profile, and TLS compatibility | Y/Y/N | Preserve supported inputs using `RESEND_API_KEY`, `MAIL_PROFILES`, `MAIL_TLS_CERT_PATH`, and `MAIL_TLS_KEY_PATH`. Nested selected key presence takes priority even when null or empty. Named profiles never select a different account's default key. Named-profile deployment use is unverified; the retained behavior is a compatibility contract. |
492
- | Key set/status/delete operations | Y/Y/N | Preserve credential management and secret-free status. New keys use the nested member; existing legacy keys are updated in place unless a nested key exists. Delete removes both selected representations to prevent an old credential reappearing, preserving other keys, settings, and profile containers. Key commands still default to `mail` independently of the serving profile. |
492
+ | Key set/status/delete operations | Y/Y/N | Preserve credential management and secret-free status. New keys use the nested member; existing root or profile keys are updated in place unless a nested key exists. Delete removes both selected representations to prevent an old credential reappearing, preserving other keys, settings, and profile containers. Key commands still default to `mail` independently of the serving profile. |
493
493
  | Send configuration | Y/Y/N | Use the same profile, sender, provider-timeout, and retry-guidance settings for direct sending. A send needs provider authority and its report, so listener certificates remain a serve-only prerequisite. Reports and idempotency keys remain per-operation inputs. |
494
494
  | Explicit runtime dependencies and callbacks | Y/Y/N | Preserve `readCredential`, provider injection, observation, cancellation, and subscription callbacks as programmatic inputs. A function or signal is not JSON configuration. The portable browser mail import retains its existing dependency boundary. |
495
495
  | Shared sender omission | Y/Y/N | Preserve per-report `from` and provider-template sender selection when no shared override is configured. One listener can therefore serve independent application sender identities. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.29.1",
3
+ "version": "0.31.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -38,6 +38,7 @@
38
38
  "./speech-text": "./browser-runtime/speech-text.mjs",
39
39
  "./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
40
40
  "./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
41
+ "./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
41
42
  "./ai/browser-speech": "./browser-runtime/ai/browser-speech.mjs",
42
43
  "./mail": "./src/mail-api.mjs",
43
44
  "./testing": "./src/testing.mjs",
@@ -85,7 +86,7 @@
85
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
86
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
87
88
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
88
- "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs",
89
90
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
90
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
91
92
  "check": "node tools/check-source.mjs && npm test",
@@ -16,6 +16,14 @@ import {
16
16
  } from './AIProviderRuntime.js';
17
17
  import {normalizeOllamaModelIdentifier} from './OllamaModelIdentifier.js';
18
18
  import {arcaneLogging} from 'arcane-os/logging';
19
+ import {
20
+ fetchHTTPResponse,
21
+ fetchJSONResponse,
22
+ isAIRequestAbort,
23
+ normalizeAIRequestAbort,
24
+ structuredOutputFormat as normalizeStructuredOutput,
25
+ openAIResponseFormat
26
+ } from 'arcane-os/ai/twin-cloud';
19
27
  import {MarkdownSpeech,stripSpeechFormatting} from 'arcane-os/speech-text';
20
28
  import {prepareSpeech} from './PreparedSpeech.js';
21
29
  import {createToolTextObserver} from 'arcane-os/ai/tool-text-stream';
@@ -121,24 +129,6 @@ function aiInitializationError(code,reason,message){
121
129
  return error;
122
130
  }
123
131
 
124
- function isAIRequestAbort(error,signal){
125
- return signal?.aborted
126
- ||error?.name==='AbortError'
127
- ||error?.code==='ARCANE_REQUEST_ABORTED'
128
- ||error?.code==='ARCANE_AI_REQUEST_ABORTED'
129
- ||error?.code==='AI_REQUEST_ABORTED';
130
- }
131
-
132
- function normalizeAIRequestAbort(error){
133
- if(error?.code==='ARCANE_AI_REQUEST_ABORTED'){
134
- return error;
135
- }
136
- const normalized=new Error('The AI request was cancelled.',{cause:error});
137
- normalized.name='AbortError';
138
- normalized.code='ARCANE_AI_REQUEST_ABORTED';
139
- return normalized;
140
- }
141
-
142
132
  function normalizeAIReasoningEffort(value){
143
133
  if(value===undefined||value===null||value===''){
144
134
  return '';
@@ -3776,69 +3766,6 @@ class AI {
3776
3766
  return true;
3777
3767
  }
3778
3768
 
3779
- async #fetchHTTPResponse(url,options){
3780
- const {signal}=options;
3781
- const retryDelayMs=3000;
3782
- try{
3783
- while(true){
3784
- if(signal?.aborted){
3785
- throw normalizeAIRequestAbort(signal.reason);
3786
- }
3787
- const response=await fetch(url,options);
3788
- if(signal?.aborted){
3789
- throw normalizeAIRequestAbort(signal.reason);
3790
- }
3791
- if(response.ok){
3792
- return response;
3793
- }
3794
-
3795
- const contentType=response.headers.get('content-type')||'';
3796
- const error=contentType.includes('application/json')
3797
- ?await response.json()
3798
- :await response.text();
3799
- if(signal?.aborted){
3800
- throw normalizeAIRequestAbort(signal.reason);
3801
- }
3802
- const message=is.string(error)
3803
- ?error
3804
- :error?.error?.message??error?.message;
3805
- if(
3806
- response.status!==429
3807
- ||!is.string(message)
3808
- ||!message.toLowerCase().includes('overload')
3809
- ){
3810
- throw error;
3811
- }
3812
-
3813
- arcaneLogging.warn(
3814
- `${message}\nRetrying in ${retryDelayMs / 1000} seconds`,
3815
- error
3816
- );
3817
- await new Promise(function waitForOverloadRetry(resolve,reject){
3818
- function finishRetryDelay(){
3819
- signal?.removeEventListener('abort',cancelRetryDelay);
3820
- resolve();
3821
- }
3822
- function cancelRetryDelay(){
3823
- clearTimeout(timer);
3824
- signal.removeEventListener('abort',cancelRetryDelay);
3825
- reject(normalizeAIRequestAbort(signal.reason));
3826
- }
3827
- const timer=setTimeout(finishRetryDelay,retryDelayMs);
3828
- signal?.addEventListener('abort',cancelRetryDelay,{once:true});
3829
- if(signal?.aborted){
3830
- cancelRetryDelay();
3831
- }
3832
- });
3833
- }
3834
- }catch(error){
3835
- if(isAIRequestAbort(error,signal)){
3836
- throw normalizeAIRequestAbort(error);
3837
- }
3838
- throw error;
3839
- }
3840
- }
3841
-
3842
3769
  #nativeOllama(){
3843
3770
  const client=globalThis.Arcane?.ollama;
3844
3771
 
@@ -4171,48 +4098,6 @@ class AI {
4171
4098
  ];
4172
4099
  }
4173
4100
 
4174
- #structuredOutputFormat(value=false){
4175
- if(value===false||value===null||value===undefined){
4176
- return null;
4177
- }
4178
- if(value===true||value==='json'){
4179
- return 'json';
4180
- }
4181
- if(
4182
- is.object(value)
4183
- &&!is.array(value)
4184
- &&(
4185
- Object.getPrototypeOf(value)===Object.prototype
4186
- ||Object.getPrototypeOf(value)===null
4187
- )
4188
- ){
4189
- return value;
4190
- }
4191
-
4192
- const error=new TypeError(
4193
- 'AI structured output must be enabled with true, json, or a JSON Schema object.'
4194
- );
4195
- error.code='AI_STRUCTURED_OUTPUT_INVALID';
4196
- throw error;
4197
- }
4198
-
4199
- #openAIResponseFormat(structuredOutputFormat){
4200
- if(structuredOutputFormat==='json'){
4201
- return {type:'json_object'};
4202
- }
4203
- if(structuredOutputFormat){
4204
- return {
4205
- type:'json_schema',
4206
- json_schema:{
4207
- name:'structured_response',
4208
- strict:true,
4209
- schema:structuredOutputFormat
4210
- }
4211
- };
4212
- }
4213
- return null;
4214
- }
4215
-
4216
4101
  async #reportRequest(requestHandler,request,id,metadata){
4217
4102
  if(!is.function(requestHandler)){
4218
4103
  throw new TypeError('AI onRequest callback must be a function.');
@@ -4762,7 +4647,7 @@ class AI {
4762
4647
  if(signal?.aborted){
4763
4648
  throw normalizeAIRequestAbort();
4764
4649
  }
4765
- const structuredOutputFormat=this.#structuredOutputFormat(
4650
+ const structuredOutputFormat=normalizeStructuredOutput(
4766
4651
  structuredOutput
4767
4652
  );
4768
4653
 
@@ -4776,7 +4661,7 @@ class AI {
4776
4661
  }
4777
4662
 
4778
4663
  if(structuredOutputFormat){
4779
- request.response_format=this.#openAIResponseFormat(
4664
+ request.response_format=openAIResponseFormat(
4780
4665
  structuredOutputFormat
4781
4666
  );
4782
4667
  }
@@ -4970,7 +4855,7 @@ class AI {
4970
4855
  destination:this.url
4971
4856
  });
4972
4857
  const body = JSON.stringify(request);
4973
- const response=await this.#fetchHTTPResponse(
4858
+ const response=await fetchHTTPResponse(
4974
4859
  this.url,
4975
4860
  {
4976
4861
  method:'POST',
@@ -5622,7 +5507,7 @@ class AI {
5622
5507
  if(signal?.aborted){
5623
5508
  throw normalizeAIRequestAbort();
5624
5509
  }
5625
- const structuredOutputFormat=this.#structuredOutputFormat(structuredOutput);
5510
+ const structuredOutputFormat=normalizeStructuredOutput(structuredOutput);
5626
5511
 
5627
5512
  const normalizedReasoningEffort=normalizeAIReasoningEffort(
5628
5513
  reasoningEffort===undefined?this.reasoningEffort:reasoningEffort
@@ -5634,7 +5519,7 @@ class AI {
5634
5519
  }
5635
5520
 
5636
5521
  if(structuredOutputFormat){
5637
- request.response_format=this.#openAIResponseFormat(
5522
+ request.response_format=openAIResponseFormat(
5638
5523
  structuredOutputFormat
5639
5524
  );
5640
5525
  }
@@ -5718,7 +5603,7 @@ class AI {
5718
5603
  destination:this.url
5719
5604
  });
5720
5605
  const body = JSON.stringify(request);
5721
- const response=await this.#fetchHTTPResponse(
5606
+ const responseJSON=await fetchJSONResponse(
5722
5607
  this.url,
5723
5608
  {
5724
5609
  method:'POST',
@@ -5729,36 +5614,11 @@ class AI {
5729
5614
  }
5730
5615
  );
5731
5616
 
5732
- const contentType=response.headers.get('content-type')||'';
5733
-
5734
- if(!contentType.includes('application/json')){
5735
- throw new TypeError(
5736
- `AI request returned ${contentType||'an unknown content type'} instead of JSON.`
5737
- );
5738
- }
5739
-
5740
- let responseJSON;
5741
- try{
5742
- responseJSON=await response.json();
5743
- }catch(error){
5744
- if(isAIRequestAbort(error,signal)){
5745
- throw normalizeAIRequestAbort(error);
5746
- }
5747
- throw error;
5748
- }
5749
5617
  if(signal?.aborted){
5750
5618
  throw normalizeAIRequestAbort();
5751
5619
  }
5752
-
5753
- if(!response.id){
5754
- response.id=id;
5755
- }
5756
-
5757
- //console.log(responseJSON);
5758
- //async
5759
5620
  normalizeAICompletionToolCalls(responseJSON);
5760
5621
  await responseHandler(responseJSON,id,false);
5761
- //sync
5762
5622
  return responseJSON;
5763
5623
  }
5764
5624
 
@@ -5,12 +5,6 @@ export function appRelativeRoot(config, appId) {
5
5
  return config.appsRoot === '.' ? '' : `apps/${appId}`;
6
6
  }
7
7
 
8
- // Generated compatibility URLs are separate from installation and storage identity.
9
- export function rootAppLegacyPath(config, appId) {
10
- return config.appsRoot === '.' && config.legacyAppPaths !== false
11
- ? `apps/${appId}` : undefined;
12
- }
13
-
14
8
  export function resolveAppRoot(workspaceRoot, config, appId) {
15
9
  return path.resolve(workspaceRoot, appRelativeRoot(config, appId));
16
10
  }
@@ -20,19 +14,3 @@ export function appBaseHref(workspaceRoot, appRoot, document = 'index.html') {
20
14
  const relative = path.relative(directory, workspaceRoot).split(path.sep).join('/');
21
15
  return relative ? `${relative}/` : './';
22
16
  }
23
-
24
- export function rootAppNavigation(appId, entry, documents = []) {
25
- const redirects=new Map();
26
- for(const document of documents)redirects.set(`apps/${appId}/${document}`,document);
27
- redirects.set(`apps/${appId}/index.html`,entry);
28
- return [...redirects].map(([file,target])=>{
29
- const relative=path.posix.relative(path.posix.dirname(file),target);
30
- const destination=JSON.stringify(relative).replaceAll('<','\\u003c');
31
- return {
32
- path:file,target:`/${target}`,
33
- content:'<!doctype html>\n<!-- Arcane root application navigation -->\n'
34
- +'<meta charset="utf-8">\n<title>Opening application</title>\n'
35
- +`<script>const target=new URL(${destination},location.href);target.search=location.search;target.hash=location.hash;location.replace(target.href);</script>\n`
36
- };
37
- });
38
- }
package/src/cli/main.mjs CHANGED
@@ -506,7 +506,7 @@ function operationOptions(command,parsed,cwd){
506
506
  return {
507
507
  targetPath:path.resolve(cwd,values.path??appId),
508
508
  appId,
509
- appsRoot:values['apps-root']??'apps',
509
+ appsRoot:values['apps-root']??'.',
510
510
  displayName:values['display-name'],
511
511
  target:values.target??'browser',
512
512
  initializeGit:flags.has('git')
@@ -6,7 +6,7 @@ import https from 'node:https';
6
6
  import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import {resolveWorkspace} from './workspace.mjs';
9
- import {appRelativeRoot,rootAppLegacyPath} from './app-layout.mjs';
9
+ import {appRelativeRoot} from './app-layout.mjs';
10
10
  import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
11
11
  import {APP_DESCRIPTOR_NAME, projectPackageManifest} from './app-descriptor.mjs';
12
12
  import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
@@ -1047,26 +1047,8 @@ async function startOwnedDevServer({
1047
1047
  const pwaResourceUrls = new Set();
1048
1048
  function developmentPwaArtifacts(selectedRoutes, assets = [], version = assetVersion) {
1049
1049
  const rootApp = selectedRoutes.config.appsRoot === '.';
1050
- const legacyAppPath = rootAppLegacyPath(selectedRoutes.config, selectedRoutes.appId);
1051
1050
  const appBase = applicationSourcePath(selectedRoutes.config,selectedRoutes.appId);
1052
1051
  const navigationAliases = {'/': selectedRoutes.startPath};
1053
- if (legacyAppPath) {
1054
- const legacyBase = `/${legacyAppPath}`;
1055
- navigationAliases[legacyBase] = selectedRoutes.startPath;
1056
- navigationAliases[`${legacyBase}/`] = selectedRoutes.startPath;
1057
- for (const asset of [selectedRoutes.startPath,...assets]) {
1058
- const pathname = new URL(asset,'http://arcane.invalid').pathname;
1059
- if (!/\.html?$/iu.test(pathname)) continue;
1060
- const segments = decodeURIComponent(pathname).split('/').filter(Boolean);
1061
- const mapping = selectedRoutes.mappings.find(function matchingNavigationRoute(route) {
1062
- return route.prefix.every(function matchingNavigationSegment(segment,index) {
1063
- return segments[index] === segment;
1064
- });
1065
- });
1066
- if (mapping?.kind === 'app') navigationAliases[`${legacyBase}${pathname}`] = pathname;
1067
- }
1068
- navigationAliases[`${legacyBase}/index.html`] = selectedRoutes.startPath;
1069
- }
1070
1052
  return createPwaArtifacts(
1071
1053
  {
1072
1054
  app: {
@@ -1085,7 +1067,6 @@ async function startOwnedDevServer({
1085
1067
  ...(rootApp ? {
1086
1068
  installationId:`/apps/${routeSet.appId}/`
1087
1069
  } : {}),
1088
- legacyAppPath,
1089
1070
  runtimeBase: selectedRoutes.browserRuntimeBase
1090
1071
  ?? selectedRoutes.installed?.browserRuntimeBase ?? '/arcane/sdk/',
1091
1072
  mode: 'development'
@@ -1177,34 +1158,17 @@ async function startOwnedDevServer({
1177
1158
  const target=parseRequestTarget(request.url);
1178
1159
  if(!target){deny(response,400,'Invalid request path.');return;}
1179
1160
  const {segments}=target;
1180
- const legacyAppRequest = mode === 'source' && rootAppLegacyPath(routeSet.config, routeSet.appId)
1181
- && segments[0] === 'apps' && segments[1] === routeSet.appId;
1182
- const legacyPwaRequest = legacyAppRequest && segments.length === 3
1183
- && ['arcane-sw.js', 'arcane-offline.json'].includes(segments[2]);
1184
- const generatedPwaPath = legacyPwaRequest
1185
- || ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1186
- .includes(target.path);
1161
+ const generatedPwaPath = ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1162
+ .includes(target.path);
1187
1163
  const requestedMapping = currentSourceRoutes.mappings.find(function currentRequestMapping(route) {
1188
1164
  return route.prefix.every(function currentRequestSegment(segment,index) {
1189
1165
  return segments[index] === segment;
1190
1166
  });
1191
1167
  });
1192
- const appRequest = requestedMapping?.kind === 'app' || legacyAppRequest;
1168
+ const appRequest = requestedMapping?.kind === 'app';
1193
1169
  const selectedRoutes = mode === 'source' && (appRequest || segments.length === 0 || generatedPwaPath)
1194
1170
  ? await refreshSourceRoutes() : currentSourceRoutes;
1195
1171
  const pwaEnabled = mode === 'source' ? selectedRoutes.app?.pwa?.enabled === true : routeSet.pwa;
1196
- const authoredLegacyResource = legacyAppRequest
1197
- && sourcePathAllowed(segments, selectedRoutes.app);
1198
- if (legacyAppRequest
1199
- && !(pwaEnabled && legacyPwaRequest)
1200
- && !authoredLegacyResource) {
1201
- const legacyPath = target.pathname.slice(`/apps/${routeSet.appId}`.length);
1202
- const location = !legacyPath || legacyPath === '/' || legacyPath === '/index.html'
1203
- ? selectedRoutes.startPath : legacyPath;
1204
- response.writeHead(302,{location:`${location}${target.search}`});
1205
- response.end();
1206
- return;
1207
- }
1208
1172
  if (mode === 'source' && pwaEnabled
1209
1173
  && generatedPwaPath) {
1210
1174
  const generated = await sourcePwaArtifact(target.path, selectedRoutes);
@@ -47,6 +47,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
47
47
  ['arcane-os/speech-text','sdk/speech-text.mjs'],
48
48
  ['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
49
49
  ['arcane-os/ai/tool-text-stream','sdk/ai/tool-text-stream.mjs'],
50
+ ['arcane-os/ai/twin-cloud','sdk/ai/twin-cloud.mjs'],
50
51
  ['arcane-os/ai/browser-speech',SDK_BROWSER_SPEECH_ENTRY]
51
52
  ]);
52
53
  function fail(message,code='ARCANE_IMPORT_MAP_INVALID'){
@@ -81,9 +81,9 @@ function mailCredentialEntry(settings, location) {
81
81
  : `mail.profiles[${JSON.stringify(location.profile)}].apiKey`
82
82
  };
83
83
  if (nestedSettings && Object.hasOwn(nestedSettings, 'apiKey')) return nestedEntry;
84
- const legacySettings = mailProfileSettings(settings, location);
85
- if (legacySettings && Object.hasOwn(legacySettings, 'RESEND_API_KEY')) {
86
- return {profileSettings: legacySettings, key: 'RESEND_API_KEY', setting: location.setting};
84
+ const profileSettings = mailProfileSettings(settings, location);
85
+ if (profileSettings && Object.hasOwn(profileSettings, 'RESEND_API_KEY')) {
86
+ return {profileSettings, key: 'RESEND_API_KEY', setting: location.setting};
87
87
  }
88
88
  return nestedEntry;
89
89
  }
@@ -12,7 +12,7 @@ import {
12
12
  } from 'node:fs/promises';
13
13
  import path from 'node:path';
14
14
  import {pathToFileURL} from 'node:url';
15
- import {appRelativeRoot,resolveAppRoot,rootAppLegacyPath,rootAppNavigation} from '../app-layout.mjs';
15
+ import {appRelativeRoot,resolveAppRoot} from '../app-layout.mjs';
16
16
  import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
17
17
  import {withWorkspaceOperationLock} from '../workspace-operation-lock.mjs';
18
18
  import {
@@ -275,14 +275,11 @@ function validateSharedRoute(route,label){
275
275
  }
276
276
 
277
277
  export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
278
- assertOnlyKeys(value,new Set(['schemaVersion','appsRoot','distRoot','sharedPayloads','legacyAppPaths']),ROOT_CONFIG_NAME);
278
+ assertOnlyKeys(value,new Set(['schemaVersion','appsRoot','distRoot','sharedPayloads']),ROOT_CONFIG_NAME);
279
279
  if(value.schemaVersion!==1)fail(`${ROOT_CONFIG_NAME}.schemaVersion must be 1.`);
280
280
  if(!['apps','.'].includes(value.appsRoot)||value.distRoot!=='dist'){
281
281
  fail(`${ROOT_CONFIG_NAME} must bind appsRoot to "apps" or "." and distRoot to "dist".`);
282
282
  }
283
- if (value.legacyAppPaths !== undefined && !is.boolean(value.legacyAppPaths)) {
284
- fail(`${ROOT_CONFIG_NAME}.legacyAppPaths must be a boolean.`);
285
- }
286
283
  if(!isPlainObject(value.sharedPayloads)){
287
284
  fail(`${ROOT_CONFIG_NAME}.sharedPayloads must be an object.`);
288
285
  }
@@ -297,8 +294,7 @@ export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
297
294
  );
298
295
  }
299
296
  return {
300
- schemaVersion:1,appsRoot:value.appsRoot,distRoot:'dist',sharedPayloads,configPath,
301
- legacyAppPaths:value.legacyAppPaths ?? true
297
+ schemaVersion:1,appsRoot:value.appsRoot,distRoot:'dist',sharedPayloads,configPath
302
298
  };
303
299
  }
304
300
 
@@ -581,15 +577,6 @@ async function optionalDescriptor(context){
581
577
  async function inspectContext(context,{signal}={}){
582
578
  const records=await collectPackageRecords(context,{signal});
583
579
  const documents=await browserDocuments(records,context.config.entry);
584
- const navigation=rootAppLegacyPath(context.rootConfig,context.appId)?rootAppNavigation(
585
- context.appId,context.config.entry,documents.map(document=>document.path)
586
- ):[];
587
- for(const redirect of navigation){
588
- const selected=records.find(record=>pathKey(record.destination)===pathKey(redirect.path));
589
- if(selected&&!(await readFile(selected.source,'utf8')).includes('<!-- Arcane root application navigation -->')){
590
- fail(`Root application navigation would replace selected content: ${redirect.path}.`);
591
- }
592
- }
593
580
  return {
594
581
  appId:context.appId,
595
582
  displayName:context.config.displayName,
@@ -607,7 +594,7 @@ async function inspectContext(context,{signal}={}){
607
594
  ...(context.config.adapter===undefined?{}:{adapter:context.config.adapter}),
608
595
  descriptor:await optionalDescriptor(context),
609
596
  browserDocuments:documents,
610
- files:[...new Set(['index.html',...records.map(record=>record.destination),...navigation.map(redirect=>redirect.path)])].sort(compareText),
597
+ files:[...new Set(['index.html',...records.map(record=>record.destination)])].sort(compareText),
611
598
  output:path.relative(context.workspaceRoot,context.outputRoot).split(path.sep).join('/')
612
599
  };
613
600
  }
@@ -763,13 +750,9 @@ async function replaceDirectory(stagingRoot,outputRoot){
763
750
  async function packageWithContext(context,options={}){
764
751
  const {signal,onEvent,browserPwa=true}=options;
765
752
  const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
766
- const legacyAppPath=rootAppLegacyPath(context.rootConfig,context.appId);
767
753
  const appPath=appRelativeRoot(context.rootConfig,context.appId);
768
754
  const entryPath=appPackagePath(context,context.config.entry);
769
755
  const inspected=await inspectContext(context,{signal});
770
- const navigation=legacyAppPath?rootAppNavigation(
771
- context.appId,context.config.entry,inspected.browserDocuments.map(document=>document.path)
772
- ):[];
773
756
  if(options.dryRun){
774
757
  return {
775
758
  appId:context.appId,
@@ -782,11 +765,7 @@ async function packageWithContext(context,options={}){
782
765
  PWA_MANIFEST_NAME,
783
766
  PWA_OFFLINE_MANIFEST_NAME,
784
767
  PWA_WORKER_NAME,
785
- PWA_BOOTSTRAP_NAME,
786
- ...(legacyAppPath?[
787
- `${legacyAppPath}/${PWA_WORKER_NAME}`,
788
- `${legacyAppPath}/${PWA_OFFLINE_MANIFEST_NAME}`
789
- ]:[])
768
+ PWA_BOOTSTRAP_NAME
790
769
  ]:[])
791
770
  ].sort(compareText)
792
771
  };
@@ -824,18 +803,6 @@ async function packageWithContext(context,options={}){
824
803
  }else{
825
804
  await copyBase();
826
805
  }
827
- for(const redirect of navigation){
828
- throwIfAborted(signal);
829
- const filePath=path.join(stagingRoot,...redirect.path.split('/'));
830
- try{
831
- const current=await readFile(filePath,'utf8');
832
- if(!current.includes('<!-- Arcane root application navigation -->')){
833
- fail(`Root application navigation would replace package content: ${redirect.path}.`);
834
- }
835
- }catch(error){if(error.code!=='ENOENT')throw error;}
836
- await mkdir(path.dirname(filePath),{recursive:true});
837
- await writeFile(filePath,redirect.content,'utf8');
838
- }
839
806
  const files=await listOutputFiles(stagingRoot,{signal});
840
807
  // Traverse actual browser resources after the adapter finishes. Files
841
808
  // included only as application documents retain their original content.
@@ -957,14 +924,6 @@ async function packageWithContext(context,options={}){
957
924
  fail(`Package output is missing its entry file: ${context.config.entry}.`);
958
925
  }
959
926
  const installed=pwaEnabled?await readInstalledSdkLayout(context.workspaceRoot,context.rootConfig):null;
960
- const navigationAliases=navigation.length?{
961
- './':packageResourceUrl(entryPath),
962
- [`./apps/${context.appId}`]:packageResourceUrl(entryPath),
963
- [`./apps/${context.appId}/`]:packageResourceUrl(entryPath),
964
- ...Object.fromEntries(navigation.map(redirect=>[
965
- packageResourceUrl(redirect.path),packageResourceUrl(redirect.target.slice(1))
966
- ]))
967
- }:undefined;
968
927
  const pwaArtifacts=pwaEnabled?createPwaArtifacts({
969
928
  app:{
970
929
  id:context.appId,
@@ -973,9 +932,7 @@ async function packageWithContext(context,options={}){
973
932
  entry:packageResourceUrl(entryPath)
974
933
  },
975
934
  appPath,
976
- ...(legacyAppPath?{legacyAppPath}:{}),
977
935
  ...(installed?.direct?{runtimeBase:`.${installed.browserRuntimeBase}`} : {}),
978
- ...(navigationAliases?{navigationAliases}:{}),
979
936
  sdkVersion:assetVersion,
980
937
  pwa:context.config.pwa,
981
938
  files,
package/src/pwa.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import Is from 'strong-type';
2
2
  import {randomUUID} from 'node:crypto';
3
- import path from 'node:path';
4
3
  import {createPwaWorkerScript} from './pwa-worker.mjs';
5
4
  import {versionAssetUrl} from './import-map.mjs';
6
5
 
@@ -204,7 +203,6 @@ export function createPwaArtifacts(
204
203
  runtimeBase = './arcane/sdk/',
205
204
  appBase,
206
205
  installationId,
207
- legacyAppPath,
208
206
  appPath = '',
209
207
  navigationAliases,
210
208
  revision
@@ -316,47 +314,6 @@ controller.ready.catch(
316
314
  },
317
315
  {path: PWA_BOOTSTRAP_NAME, content: bootstrap}
318
316
  ];
319
- if (legacyAppPath) {
320
- const directory = legacyAppPath.endsWith('/') ? legacyAppPath : `${legacyAppPath}/`;
321
- const priorDirectory = new URL(resourceUrl('./', directory), 'https://arcane.invalid/').pathname;
322
- // Existing registrations continue updating their own script and inventory URLs.
323
- function priorScopeUrl(value) {
324
- if (value.startsWith('/') || /^[A-Za-z][A-Za-z0-9+.-]*:/u.test(value)) return value;
325
- const resolved = new URL(value, 'https://arcane.invalid/');
326
- let relative = path.posix.relative(priorDirectory, resolved.pathname);
327
- if (!relative) {
328
- relative = resolved.pathname.endsWith('/')
329
- ? './'
330
- : `../${path.posix.basename(resolved.pathname)}`;
331
- }
332
- else {
333
- if (!relative.startsWith('.')) relative = `./${relative}`;
334
- if (resolved.pathname.endsWith('/') && !relative.endsWith('/')) relative += '/';
335
- }
336
- return `${relative}${resolved.search}${resolved.hash}`;
337
- }
338
- const legacyOfflineManifest = {
339
- ...offlineManifest,
340
- assets: [...new Set([
341
- ...offlineManifest.assets.map(priorScopeUrl),
342
- `./${PWA_OFFLINE_MANIFEST_NAME}`
343
- ])],
344
- navigationAliases: Object.fromEntries(
345
- Object.entries(offlineManifest.navigationAliases).map(
346
- function priorScopeNavigation([alias, target]) {
347
- return [priorScopeUrl(alias), priorScopeUrl(target)];
348
- }
349
- )
350
- )
351
- };
352
- generatedFiles.push(
353
- {
354
- path: `${directory}${PWA_WORKER_NAME}`,
355
- content: createPwaWorkerScript(legacyOfflineManifest, priorScopeUrl(`${runtimeBase}pwa.mjs`))
356
- },
357
- {path: `${directory}${PWA_OFFLINE_MANIFEST_NAME}`, content: json(legacyOfflineManifest)}
358
- );
359
- }
360
317
  return {
361
318
  manifest,
362
319
  offlineManifest,
package/src/scaffold.mjs CHANGED
@@ -316,7 +316,7 @@ async function runGitInit(workspaceRoot,signal,onEvent){
316
316
  export async function createWorkspace({
317
317
  targetPath,
318
318
  appId,
319
- appsRoot='apps',
319
+ appsRoot='.',
320
320
  displayName,
321
321
  target='browser',
322
322
  initializeGit=false,
@@ -400,7 +400,7 @@ export async function initWorkspace({
400
400
  },async workspaceOperationLease=>{
401
401
  const profile=await existingWorkspaceProfile(resolvedRoot);
402
402
  const workspaceMode=profile?.workspaceMode??'external';
403
- const selectedAppsRoot=appsRoot??profile?.config.appsRoot??'apps';
403
+ const selectedAppsRoot=appsRoot??profile?.config.appsRoot??'.';
404
404
  if(!['apps','.'].includes(selectedAppsRoot))fail('appsRoot must be apps or .','ARCANE_USAGE');
405
405
  if(profile&&selectedAppsRoot!==profile.config.appsRoot){
406
406
  fail('appsRoot must match the existing workspace layout; init does not relocate an application.','ARCANE_USAGE');
@@ -439,6 +439,7 @@ export async function initWorkspace({
439
439
  const template=workspaceMode==='integrated'
440
440
  ?workspaceTemplate({
441
441
  appId,
442
+ appsRoot:selectedAppsRoot,
442
443
  displayName,
443
444
  appOnly:true,
444
445
  namedImports:true,