arcane-os 0.27.0 → 0.28.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.28.0
4
+
5
+ - Expose existing runtime modules and entities directly through
6
+ `arcane-os/modules/<filename>` and `arcane-os/entities/<filename>`, including
7
+ extensions. Package exports and managed maps point to the actual files, so
8
+ relative imports, component resources and `import.meta.resolve` keep their
9
+ established directory and lifecycle. Existing lowercase exports remain.
10
+ - Root apps using direct npm routes generate package-namespaced maps without
11
+ the old `arcane/*` or `./arcane/*` keys. Update authored root-app bare imports
12
+ before regenerating the managed map. Installed `runtime/arcane/` directories
13
+ remain unchanged; physical, virtual and nested layouts retain compatibility.
14
+ - Use the package namespace for generated root app code and shared PWA imports,
15
+ preserving parallel startup and the existing storage and installation owners.
16
+ - New root apps declare the SDK as a runtime dependency. Init promotes their
17
+ SDK development declaration and preserves existing runtime or optional
18
+ classification in every layout, including aliases. Existing nested scaffold
19
+ defaults remain unchanged. No server, listener or application policy changes.
20
+
21
+ ## 0.27.1
22
+
23
+ - Preserve the previous `apps/<id>/arcane-sw.js` and `arcane-offline.json`
24
+ endpoints when generating a root-layout PWA. Both use the canonical worker
25
+ and current root inventory, including navigation aliases and installed npm
26
+ resource paths; portable output also retains deployment-relative URLs.
27
+ - Serve those previous worker and inventory endpoints directly during SDK
28
+ development instead of redirecting worker-script requests. Retain ordinary
29
+ browser update/activation, caches, application identity and saved data.
30
+ - Keep root layout optional and existing multi-app and Node server behavior
31
+ unchanged. Installed-application execution remains consumer-owned.
32
+
3
33
  ## 0.27.0
4
34
 
5
35
  - Add optional standalone `appsRoot: "."` discovery and `new`/`init --apps-root .`.
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.27.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.28.0` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -33,6 +33,12 @@ Select `appsRoot: "."` for a standalone root app, or scaffold one with
33
33
  `arcane new my-app --apps-root .`. After npm installation, `arcane import-map`
34
34
  refreshes managed maps and the root app's static PWA/navigation files for ordinary
35
35
  static hosting. Portable app packages still contain their selected runtime.
36
+ Root/direct maps use `arcane-os/modules/<filename>` and
37
+ `arcane-os/entities/<filename>` (including extensions) plus the focused lowercase
38
+ exports, rather than `arcane/*` aliases. These paths resolve to the actual npm
39
+ files, preserving relative component URLs. New root apps put the SDK in runtime
40
+ `dependencies`; init preserves existing runtime/optional declarations and promotes
41
+ a root app's SDK development declaration without changing its other packages.
36
42
  Existing `apps/<id>`, virtual `/arcane` routes, and materialized workspaces remain
37
43
  supported. Node services continue to
38
44
  use the installed CLI or public imports such as `arcane-os/mail`.
@@ -89,7 +89,7 @@ export function getPwaInstall() {
89
89
 
90
90
  async function loadInstallStorage() {
91
91
  if (!globalThis.dbopfs) {
92
- await import('arcane/DBOPFS');
92
+ await import('arcane-os/modules/DBOPFS.js');
93
93
  }
94
94
  const storage = globalThis.dbopfs;
95
95
  if (!storage) {
@@ -273,8 +273,8 @@ export function mountPwaInstallPrompt({appName = ''} = {}) {
273
273
  if (!globalThis.document) return null;
274
274
  // Both modules may start independently; saved theme loading is not a barrier.
275
275
  await Promise.all([
276
- import('arcane/HTMLImport'),
277
- import('arcane/ThemeBootstrap')
276
+ import('arcane-os/modules/HTMLImport.js'),
277
+ import('arcane-os/modules/ThemeBootstrap.js')
278
278
  ]);
279
279
  if (owner.state.status === 'disposed') return null;
280
280
  if (!document.body) {
@@ -295,7 +295,7 @@ export function mountPwaInstallPrompt({appName = ''} = {}) {
295
295
  host.hidden = true;
296
296
  host.dataset.appName = String(appName);
297
297
  host.dataset.arcanePwaInstall = '';
298
- host.setAttribute('href', new URL('../components/pwa-install.html', import.meta.resolve('arcane/HTMLImport')).href);
298
+ host.setAttribute('href', new URL('../components/pwa-install.html', import.meta.resolve('arcane-os/modules/HTMLImport.js')).href);
299
299
  return new Promise(function waitForInstallComponent(resolve, reject) {
300
300
  const observer = new MutationObserver(function observeRemovedInstallComponent() {
301
301
  if (!host.isConnected) cancelMount();
@@ -156,7 +156,7 @@ export function registerPwa({workerUrl = './arcane-sw.js', scope} = {}) {
156
156
 
157
157
  async function loadCheckStorage() {
158
158
  if (!globalThis.dbopfs) {
159
- await import('arcane/DBOPFS');
159
+ await import('arcane-os/modules/DBOPFS.js');
160
160
  }
161
161
  const storage = globalThis.dbopfs;
162
162
  if (!storage) {
@@ -7,6 +7,10 @@
7
7
  "moduleSystem": "ESM"
8
8
  },
9
9
  "memberCount": 210,
10
+ "runtimeSubpathPatterns": {
11
+ "arcane-os/modules/*": "./runtime/arcane/modules/*",
12
+ "arcane-os/entities/*": "./runtime/arcane/entities/*"
13
+ },
10
14
  "members": [
11
15
  {
12
16
  "id": "root:APP_BUNDLE_DESCRIPTOR_NAME",
@@ -146,7 +146,7 @@ all four source and destination paths. Package selection and SDK
146
146
  version come from the installed dependency, not a generated runtime lock.
147
147
 
148
148
  `arcane import-map`, `arcane dev`, packaging, and generated PWA inventories use
149
- the same actual npm destinations. For example, `arcane/HTMLImport` resolves to
149
+ the same actual npm destinations. For example, `arcane-os/modules/HTMLImport.js` resolves to
150
150
  `./node_modules/arcane-os/runtime/arcane/modules/HTMLImport.js`, and
151
151
  `arcane-os/event-manager` resolves to
152
152
  `./node_modules/arcane-os/browser-runtime/event-manager.mjs`. A normal static
@@ -180,6 +180,20 @@ query strings and fragments. It preserves authored files at those destinations.
180
180
  For a direct-installed root PWA it generates the static PWA records at the root,
181
181
  so normal static hosting needs no SDK request handler or runtime copy.
182
182
 
183
+ Direct-installed root maps expose `arcane-os/modules/<filename>` and
184
+ `arcane-os/entities/<filename>` (including extensions), plus the existing
185
+ focused lowercase package exports. They omit the old `arcane/*` and
186
+ `./arcane/*` keys. Real installed `runtime/arcane/` directories are unchanged.
187
+ Physical, nested and virtual layouts retain their established aliases and also
188
+ support the package-namespaced module/entity keys. Relative module and component
189
+ URLs still resolve from the actual selected file, not a reexport wrapper.
190
+
191
+ New root apps declare the SDK in `dependencies` because serving their npm
192
+ resources requires the installed package at runtime. Root `init` promotes an
193
+ existing SDK development declaration while preserving other dependencies.
194
+ Existing runtime or optional declarations retain their classification across
195
+ layouts; the default nested scaffold continues to use its development dependency.
196
+
183
197
  ## Installed SDK runtime materialization
184
198
 
185
199
  `materializeInstalledSdkRuntime()` is the Node entrypoint for refreshing one
@@ -59,6 +59,18 @@ authored `manifest.id` remains authoritative. Packaged default identity remains
59
59
  fragments and lead to the selected root document. No stored application data is
60
60
  rewritten by changing the layout.
61
61
 
62
+ Root PWA generation also retains `apps/<id>/arcane-sw.js` and
63
+ `apps/<id>/arcane-offline.json`. These are ordinary generated files served at
64
+ their original URLs, including on a static host. The same canonical worker and
65
+ current inventory use root resource/navigation URLs, rebased for the previous
66
+ scope when deployed beneath a directory. An existing worker's inventory refresh
67
+ can learn the new navigation destinations; the browser can update the worker at
68
+ its retained script URL through normal update and activation. No cache deletion,
69
+ forced activation, user-data migration or application-owned server handler is
70
+ introduced. Offline clients require a later successful connection to receive
71
+ updated files; fixture coverage does not establish a particular installed app's
72
+ actual browser lifecycle.
73
+
62
74
  `offline.include` and `offline.exclude` select literal paths or directory
63
75
  prefixes from the selected emitted inventory. An omitted or empty include list
64
76
  selects that inventory; exclusions subtract from it. App files use app-relative
@@ -5,11 +5,16 @@ The npm package exposes a Node.js ESM control plane, the portable
5
5
  `arcane-os/preference-store`, `arcane-os/speech-playback`,
6
6
  `arcane-os/speech-text`, `arcane-os/ai/tool-text-stream`, and `arcane-os/browser-device` entrypoints, and the browser-only
7
7
  `arcane-os/pwa`, `arcane-os/ai/browser-wasm` and `arcane-os/ai/browser-speech` entrypoints.
8
+ The `arcane-os/modules/<filename>` and `arcane-os/entities/<filename>` paths
9
+ resolve directly to the existing runtime files, with their actual extension.
10
+ They are public Node package exports and managed browser keys, without a wrapper.
11
+ For example, `import.meta.resolve('arcane-os/modules/WaitForComponent.js')`
12
+ retains the module directory needed to resolve `../components/modal.html`.
8
13
  Lowercase runtime subpaths also expose existing module namespaces through both
9
14
  Node package exports and managed browser imports, as listed below. Established
10
15
  browser names such as `arcane/AIProviderRuntime`, `arcane/AIRuntimeState`, and
11
- `arcane/ThemeBootstrap` remain managed-map names rather than Node package
12
- entrypoints. Applications call `globalThis.Arcane` for capability-gated host
16
+ `arcane/ThemeBootstrap` remain compatibility names in physical/nested maps;
17
+ direct-installed root maps use the package namespace instead. Applications call `globalThis.Arcane` for capability-gated host
13
18
  behavior.
14
19
 
15
20
  This page is the canonical inventory for every JavaScript name reachable through `package.json#exports`. The same binding can appear at the root and a focused subpath; those entrypoints are listed together. The root workspace `discoverApps` and the low-level packager `discoverApps` are intentionally separate records because they are different functions.
@@ -36,6 +41,8 @@ runtime layouts.
36
41
  | `arcane-os/packager` | Low-level browser app packager. |
37
42
  | `arcane-os/release-bundle` | Deterministic external release bundles. |
38
43
  | `arcane-os/event-manager` | Central synchronous events, complete time-travel history, playback, and optional DOM instrumentation. |
44
+ | `arcane-os/modules/<filename>` | Direct existing runtime module, including its `.js` or `.mjs` extension. |
45
+ | `arcane-os/entities/<filename>` | Direct existing runtime entity, including its extension. |
39
46
  | `arcane-os/logging` | Shared console diagnostics controlled by the existing `user.developer` preference. |
40
47
  | `arcane-os/preference-store` | Portable preference records and injected storage adapters. |
41
48
  | `arcane-os/ai` | Existing `AI.js` provider-neutral inference and speech namespace. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.27.0",
3
+ "version": "0.28.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",
@@ -13,6 +13,8 @@
13
13
  "./toolchain": "./src/toolchain.mjs",
14
14
  "./events": "./src/events.mjs",
15
15
  "./event-manager": "./src/event-manager.mjs",
16
+ "./modules/*": "./runtime/arcane/modules/*",
17
+ "./entities/*": "./runtime/arcane/entities/*",
16
18
  "./logging": "./browser-runtime/logging.mjs",
17
19
  "./browser-device": "./browser-runtime/browser-device.mjs",
18
20
  "./pwa": "./browser-runtime/pwa.mjs",
@@ -1081,7 +1081,10 @@ async function startOwnedDevServer({
1081
1081
  navigationAliases,
1082
1082
  basePath: '/',
1083
1083
  appBase,
1084
- ...(rootApp ? {installationId:`/apps/${routeSet.appId}/`} : {}),
1084
+ ...(rootApp ? {
1085
+ installationId:`/apps/${routeSet.appId}/`,
1086
+ legacyAppPath:`apps/${routeSet.appId}`
1087
+ } : {}),
1085
1088
  runtimeBase: selectedRoutes.browserRuntimeBase
1086
1089
  ?? selectedRoutes.installed?.browserRuntimeBase ?? '/arcane/sdk/',
1087
1090
  mode: 'development'
@@ -1109,8 +1112,8 @@ async function startOwnedDevServer({
1109
1112
  pwaStates.set(selectedRoutes, state);
1110
1113
  if (selectedRoutes === currentSourceRoutes) currentPwaState = state;
1111
1114
  }
1112
- if (targetPath === '/arcane-sw.js'
1113
- || targetPath === '/arcane-offline.json') {
1115
+ if (targetPath.endsWith('/arcane-sw.js')
1116
+ || targetPath.endsWith('/arcane-offline.json')) {
1114
1117
  if (!state.inventoryTask) {
1115
1118
  // Timestamp selection before traversal, so an older snapshot
1116
1119
  // finishing later cannot acquire a newer HTTP modification date.
@@ -1173,10 +1176,13 @@ async function startOwnedDevServer({
1173
1176
  const target=parseRequestTarget(request.url);
1174
1177
  if(!target){deny(response,400,'Invalid request path.');return;}
1175
1178
  const {segments}=target;
1176
- const generatedPwaPath = ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1177
- .includes(target.path);
1178
1179
  const legacyAppRequest = mode === 'source' && routeSet.config.appsRoot === '.'
1179
1180
  && segments[0] === 'apps' && segments[1] === routeSet.appId;
1181
+ const legacyPwaRequest = legacyAppRequest && segments.length === 3
1182
+ && ['arcane-sw.js', 'arcane-offline.json'].includes(segments[2]);
1183
+ const generatedPwaPath = legacyPwaRequest
1184
+ || ['/arcane.webmanifest', '/arcane-offline.json', '/arcane-sw.js', '/arcane-pwa.mjs']
1185
+ .includes(target.path);
1180
1186
  const requestedMapping = currentSourceRoutes.mappings.find(function currentRequestMapping(route) {
1181
1187
  return route.prefix.every(function currentRequestSegment(segment,index) {
1182
1188
  return segments[index] === segment;
@@ -1186,7 +1192,7 @@ async function startOwnedDevServer({
1186
1192
  const selectedRoutes = mode === 'source' && (appRequest || segments.length === 0 || generatedPwaPath)
1187
1193
  ? await refreshSourceRoutes() : currentSourceRoutes;
1188
1194
  const pwaEnabled = mode === 'source' ? selectedRoutes.app?.pwa?.enabled === true : routeSet.pwa;
1189
- if (legacyAppRequest) {
1195
+ if (legacyAppRequest && !(pwaEnabled && legacyPwaRequest)) {
1190
1196
  const legacyPath = target.pathname.slice(`/apps/${routeSet.appId}`.length);
1191
1197
  const location = !legacyPath || legacyPath === '/' || legacyPath === '/index.html'
1192
1198
  ? selectedRoutes.startPath : legacyPath;
@@ -1390,6 +1390,9 @@ export async function buildImportMap({files,signal,version=SDK_VERSION,encodePat
1390
1390
  for(const relative of [...inventory].sort(compareText)){
1391
1391
  if(JAVASCRIPT_EXTENSION.test(relative)){
1392
1392
  const target=runtimeTarget(relative);
1393
+ if(relative.startsWith('modules/')||relative.startsWith('entities/')){
1394
+ registerSpecifier(namedRegistry,`arcane-os/${relative}`,target);
1395
+ }
1393
1396
  registerSpecifier(namedRegistry,target,target);
1394
1397
  }
1395
1398
  }
@@ -1453,7 +1456,7 @@ async function physicalRuntime(workspaceRoot,signal){
1453
1456
  return {files};
1454
1457
  }
1455
1458
 
1456
- async function managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled=false){
1459
+ async function managedImportMapBuild(resolvedWorkspace,resolvedApp,signal,pwaEnabled=false){
1457
1460
  const installed=await readInstalledSdkLayout(resolvedWorkspace);
1458
1461
  const [runtime,version]=await Promise.all([
1459
1462
  installed?installedRuntimeFiles(resolvedWorkspace,installed,signal):physicalRuntime(resolvedWorkspace,signal),
@@ -1462,13 +1465,15 @@ async function managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled=false){
1462
1465
  const built=await buildImportMap({files:runtime.files,signal,version});
1463
1466
  if(installed?.direct){
1464
1467
  const imports={};
1468
+ const rootApplication=samePath(resolvedWorkspace,resolvedApp);
1465
1469
  function installedBrowserUrl(value){
1466
1470
  return value.startsWith('./')
1467
1471
  ?`./${installedRuntimeTarget(value.slice(2),installed,{browser:true})}`:value;
1468
1472
  }
1469
1473
  for(const [specifier,target] of Object.entries(built.imports)){
1474
+ if(rootApplication&&specifier.startsWith('arcane/'))continue;
1470
1475
  const selected=installedBrowserUrl(target);
1471
- imports[specifier]=selected;
1476
+ if(!rootApplication)imports[specifier]=selected;
1472
1477
  // Relative module imports and bare names must resolve to the same instance.
1473
1478
  imports[installedBrowserUrl(specifier)]=selected;
1474
1479
  }
@@ -2846,7 +2851,7 @@ async function generateImportMapUnlocked({
2846
2851
  }catch(error){
2847
2852
  if(error?.code!=='ENOENT')throw error;
2848
2853
  }
2849
- const {built,json,version}=await managedImportMapBuild(resolvedWorkspace,signal,pwaEnabled);
2854
+ const {built,json,version}=await managedImportMapBuild(resolvedWorkspace,resolvedApp,signal,pwaEnabled);
2850
2855
  const renderedDocuments=documentStates.map(item=>({
2851
2856
  ...item,
2852
2857
  content:rewriteAssetReferences(renderManagedHtml(item.html,json,item.baseHref),{
@@ -757,6 +757,7 @@ async function replaceDirectory(stagingRoot,outputRoot){
757
757
  async function packageWithContext(context,options={}){
758
758
  const {signal,onEvent,browserPwa=true}=options;
759
759
  const pwaEnabled=browserPwa&&context.config.pwa?.enabled===true;
760
+ const legacyAppPath=context.rootConfig.appsRoot==='.'?`apps/${context.appId}`:undefined;
760
761
  const appPath=appRelativeRoot(context.rootConfig,context.appId);
761
762
  const entryPath=appPackagePath(context,context.config.entry);
762
763
  const inspected=await inspectContext(context,{signal});
@@ -775,7 +776,11 @@ async function packageWithContext(context,options={}){
775
776
  PWA_MANIFEST_NAME,
776
777
  PWA_OFFLINE_MANIFEST_NAME,
777
778
  PWA_WORKER_NAME,
778
- PWA_BOOTSTRAP_NAME
779
+ PWA_BOOTSTRAP_NAME,
780
+ ...(legacyAppPath?[
781
+ `${legacyAppPath}/${PWA_WORKER_NAME}`,
782
+ `${legacyAppPath}/${PWA_OFFLINE_MANIFEST_NAME}`
783
+ ]:[])
779
784
  ]:[])
780
785
  ].sort(compareText)
781
786
  };
@@ -962,6 +967,7 @@ async function packageWithContext(context,options={}){
962
967
  entry:packageResourceUrl(entryPath)
963
968
  },
964
969
  appPath,
970
+ ...(legacyAppPath?{legacyAppPath}:{}),
965
971
  ...(installed?.direct?{runtimeBase:`.${installed.browserRuntimeBase}`} : {}),
966
972
  ...(navigationAliases?{navigationAliases}:{}),
967
973
  sdkVersion:assetVersion,
@@ -974,7 +980,9 @@ async function packageWithContext(context,options={}){
974
980
  if(inventory.has(artifact.path)){
975
981
  fail(`Package content overlaps generated PWA file: ${artifact.path}.`);
976
982
  }
977
- await writeFile(path.join(stagingRoot,artifact.path),artifact.content,'utf8');
983
+ const artifactPath=path.join(stagingRoot,...artifact.path.split('/'));
984
+ await mkdir(path.dirname(artifactPath),{recursive:true});
985
+ await writeFile(artifactPath,artifact.content,'utf8');
978
986
  files.push(artifact.path);
979
987
  }
980
988
  for(const [documentPath,document] of pwaDocumentSources){
package/src/pwa.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import Is from 'strong-type';
2
2
  import {randomUUID} from 'node:crypto';
3
+ import path from 'node:path';
3
4
  import {createPwaWorkerScript} from './pwa-worker.mjs';
4
5
  import {versionAssetUrl} from './import-map.mjs';
5
6
 
@@ -203,6 +204,7 @@ export function createPwaArtifacts(
203
204
  runtimeBase = './arcane/sdk/',
204
205
  appBase,
205
206
  installationId,
207
+ legacyAppPath,
206
208
  appPath = '',
207
209
  navigationAliases,
208
210
  revision
@@ -305,18 +307,60 @@ controller.ready.catch(
305
307
  const entryAssets = {manifest: PWA_MANIFEST_NAME, bootstrap: PWA_BOOTSTRAP_NAME};
306
308
  const manifestHref = resourceUrl(basePath, entryAssets.manifest);
307
309
  const bootstrapHref = resourceUrl(basePath, entryAssets.bootstrap);
310
+ const generatedFiles = [
311
+ {path: PWA_MANIFEST_NAME, content: json(manifest)},
312
+ {path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
313
+ {
314
+ path: PWA_WORKER_NAME,
315
+ content: createPwaWorkerScript(offlineManifest, `${runtimeBase}pwa.mjs`)
316
+ },
317
+ {path: PWA_BOOTSTRAP_NAME, content: bootstrap}
318
+ ];
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
+ }
308
360
  return {
309
361
  manifest,
310
362
  offlineManifest,
311
- files: [
312
- {path: PWA_MANIFEST_NAME, content: json(manifest)},
313
- {path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
314
- {
315
- path: PWA_WORKER_NAME,
316
- content: createPwaWorkerScript(offlineManifest, `${runtimeBase}pwa.mjs`)
317
- },
318
- {path: PWA_BOOTSTRAP_NAME, content: bootstrap}
319
- ],
363
+ files: generatedFiles,
320
364
  entryAssets,
321
365
  entryMarkup: `<link rel="manifest" href="${htmlAttribute(manifestHref)}">\n`
322
366
  + `<script type="module" async data-arcane-pwa src="${htmlAttribute(bootstrapHref)}"></script>\n`
package/src/scaffold.mjs CHANGED
@@ -192,21 +192,26 @@ async function prepareExistingPackage(workspaceRoot,files,existingPackage,sdkDec
192
192
  if(conflicts.length){
193
193
  fail(`Existing package.json conflicts with Arcane setup; no values were overwritten. Resolve: ${conflicts.join('; ')}.`);
194
194
  }
195
+ const dependencyGroup=sdkDeclaration.dependencyGroup==='optionalDependencies'
196
+ ?'optionalDependencies'
197
+ :generated.dependencies||sdkDeclaration.dependencyGroup==='dependencies'
198
+ ?'dependencies':'devDependencies';
195
199
  const merged={
196
200
  ...existing,
197
201
  private:true,
198
202
  type:'module',
199
203
  scripts:{...generated.scripts,...(existing.scripts||{})},
200
- devDependencies:{
201
- ...(existing.devDependencies||{}),
204
+ [dependencyGroup]:{
205
+ ...(existing[dependencyGroup]||{}),
202
206
  [sdkDeclaration.dependencyName]:sdkDeclaration.specifier
203
207
  },
204
208
  engines:{...generated.engines,...(existing.engines||{})}
205
209
  };
206
- if(existing.dependencies?.[sdkDeclaration.dependencyName]!==undefined){
207
- merged.dependencies={...existing.dependencies};
208
- delete merged.dependencies[sdkDeclaration.dependencyName];
209
- if(Object.keys(merged.dependencies).length===0)delete merged.dependencies;
210
+ if(dependencyGroup!=='devDependencies'
211
+ &&existing.devDependencies?.[sdkDeclaration.dependencyName]!==undefined){
212
+ merged.devDependencies={...existing.devDependencies};
213
+ delete merged.devDependencies[sdkDeclaration.dependencyName];
214
+ if(Object.keys(merged.devDependencies).length===0)delete merged.devDependencies;
210
215
  }
211
216
  if(JSON.stringify(existing)===JSON.stringify(merged)){
212
217
  return {exists:true,updated:false};
@@ -159,7 +159,7 @@ appropriate.
159
159
 
160
160
  - Use plain JavaScript, HTML, and CSS; do not introduce TypeScript or TSX.
161
161
  - Keep reusable portable mechanisms in the Arcane SDK and app-specific behavior under \`${appPrefix||'./'}\`.
162
- - Keep \`${runtimePrefix}/css/theme.css\` before app styles and import \`arcane/ThemeBootstrap\` before app code runs.
162
+ - Keep \`${runtimePrefix}/css/theme.css\` before app styles and import \`${directRuntime?'arcane-os/modules/ThemeBootstrap.js':'arcane/ThemeBootstrap'}\` before app code runs.
163
163
  - Use \`rgb(...)\` or \`rgba(...)\` for new CSS colors.
164
164
  - Build one named app and one explicit target at a time. Native targets may be unavailable until their adapters are installed.
165
165
  - Preserve complete application, model, document, message, log, diagnostic, process, and tool content. Do not truncate, clip, tail, elide, or silently discard it.
@@ -181,7 +181,7 @@ npm run dev
181
181
 
182
182
  Open the loopback URL printed by the development server. ${directRuntime?'This root app reads SDK files directly from its installed npm package; an ordinary static host uses the same resource paths.':'This app uses the existing physical arcane/ runtime layout.'} The SDK server does not expose an Ollama HTTP endpoint.
183
183
 
184
- Commit the generated \`package-lock.json\` after dependency installation. CI intentionally uses \`npm ci\` and therefore requires that lock. Before the SDK is published, install a locally packed \`${SDK_NAME}\` \`.tgz\` with \`npm install --save-dev --save-exact <path-to-tarball>\`; keep that tarball at the lock file's relative path for repeatable local \`npm ci\` runs.
184
+ Commit the generated \`package-lock.json\` after dependency installation. CI intentionally uses \`npm ci\` and therefore requires that lock. ${directRuntime?'The SDK is a runtime dependency: keep it installed when serving this app directly from node_modules. ':''}Before the SDK is published, install a locally packed \`${SDK_NAME}\` \`.tgz\` with \`npm install ${directRuntime?'--save-prod':'--save-dev'} --save-exact <path-to-tarball>\`; keep that tarball at the lock file's relative path for repeatable local \`npm ci\` runs.
185
185
 
186
186
  ## Optional browser release commands
187
187
 
@@ -199,7 +199,7 @@ import map in every directly navigable descriptor-admitted \`.html\`/\`.htm\`
199
199
  document. HTML component fragments remain package files but do not receive a
200
200
  document-level base or managed import map.
201
201
  Development, package, and build refresh that shared inventory when the selected operation needs it.
202
- Named \`arcane/*\` imports resolve through the managed map to the selected SDK files. Packaging copies the complete selected application, runtime, and specifier
202
+ Named \`${directRuntime?'arcane-os/modules/* and arcane-os/entities/*':'arcane/*'}\` imports resolve through the managed map to the selected SDK files. Packaging copies the complete selected application, runtime, and specifier
203
203
  map to \`dist/${appId}\` without running application tests. Run \`verify\` only when
204
204
  the user explicitly selects verification or a release artifact that requires it;
205
205
  \`bundle\` creates the distributable archive and \`run\` launches the selected
@@ -231,7 +231,7 @@ Every browser release also carries Arcane OS licensing material under \`${direct
231
231
  'run:browser':'arcane run --target browser'
232
232
  }:{})
233
233
  },
234
- devDependencies:{
234
+ [directRuntime?'dependencies':'devDependencies']:{
235
235
  [sdkDependencyName]:sdkDependencySpecifier
236
236
  },
237
237
  engines:{node:'>=22.23.2'}
@@ -369,9 +369,11 @@ ${bootstrapMarkup}</head>
369
369
  files.set(`${appPrefix}modules/arcane.importmap.json`,json({imports:{}}));
370
370
  }
371
371
  const themeSpecifier=namedImports
372
- ?'arcane/ThemeBootstrap':`${appsRoot==='.'?'../':'../../../'}${runtimePrefix.slice(2)}/modules/ThemeBootstrap.js`;
372
+ ?directRuntime?'arcane-os/modules/ThemeBootstrap.js':'arcane/ThemeBootstrap'
373
+ :`${appsRoot==='.'?'../':'../../../'}${runtimePrefix.slice(2)}/modules/ThemeBootstrap.js`;
373
374
  const appDataSpecifier=namedImports
374
- ?'arcane/AppDataScope':`${appsRoot==='.'?'../':'../../../'}${runtimePrefix.slice(2)}/modules/AppDataScope.js`;
375
+ ?directRuntime?'arcane-os/app-data-scope':'arcane/AppDataScope'
376
+ :`${appsRoot==='.'?'../':'../../../'}${runtimePrefix.slice(2)}/modules/AppDataScope.js`;
375
377
  const strongTypeSpecifier=namedImports
376
378
  ?'strong-type':directRuntime?`../${sdkPackageSource}/runtime/strong-type/index.js`:'../../../arcane/dependencies/strong-type/index.js';
377
379
  files.set(`${appPrefix}modules/App.js`,`import Is from '${strongTypeSpecifier}';
package/src/toolchain.mjs CHANGED
@@ -361,6 +361,7 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
361
361
  basePath:'/',
362
362
  appBase:'/',
363
363
  installationId:`/apps/${appId}/`,
364
+ legacyAppPath:`apps/${appId}`,
364
365
  runtimeBase:installed.browserRuntimeBase,
365
366
  mode:'development',
366
367
  navigationAliases
package/src/workspace.mjs CHANGED
@@ -117,7 +117,7 @@ export function resolveSdkPackageDeclaration(rootPackage,{
117
117
  }={}){
118
118
  if(!isObject(rootPackage))fail('package.json must contain a JSON object.');
119
119
  const candidates=[];
120
- for(const groupName of ['devDependencies','dependencies']){
120
+ for(const groupName of ['devDependencies','dependencies','optionalDependencies']){
121
121
  const group=rootPackage[groupName];
122
122
  if(group===undefined)continue;
123
123
  if(!isObject(group))fail(`package.json ${groupName} must be a JSON object.`);