arcane-os 0.5.0 → 0.5.2

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2
4
+
5
+ - Forwarded each application's existing managed browser import map into
6
+ isolated `arcane test` and `arcane check` files so valid `arcane/*` imports
7
+ resolve from the selected workspace runtime while integrated-legacy apps
8
+ retain their established no-map path.
9
+
10
+ ## 0.5.1
11
+
12
+ - Separated the TWiN Cloud LLM access key from the legacy OpenAI speech
13
+ credential so neither provider's key is sent to the other's endpoint while
14
+ retaining the established `ai.license` alias for TWiN Cloud chat.
15
+
3
16
  ## 0.5.0
4
17
 
5
18
  - Added bounded parallel Browser-WASM model downloads across split GGUF
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.5.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.5.2` 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
 
@@ -88,7 +88,10 @@ OpenAI-compatible chat-completion requests to
88
88
  `globalThis.arcane.config.twinCloud.accessKey`. The established `ai.license`
89
89
  property and internal `OPENAI` route identifier remain compatibility aliases;
90
90
  applications should present the service and credential as **TWiN Cloud** and
91
- **TWiN access key**.
91
+ **TWiN access key**. The TWiN key is used only for remote LLM chat. Existing
92
+ OpenAI speech routes remain separate and read only their legacy
93
+ `globalThis.arcane.config.openAI.apiKey`; the SDK never sends a TWiN key to
94
+ those speech endpoints.
92
95
 
93
96
  ## Browser-local AI
94
97
 
@@ -162,7 +165,7 @@ uses the same controller for automatic memory extraction.
162
165
  Create a new repository-shaped Arcane application with the exact stable SDK:
163
166
 
164
167
  ```bash
165
- npx arcane-os@0.5.0 new my-app --path ./my-app --target portable --git
168
+ npx arcane-os@0.5.2 new my-app --path ./my-app --target portable --git
166
169
  cd my-app
167
170
  npm install
168
171
  npm run check
@@ -173,7 +176,7 @@ To enroll an existing repository, install the exact SDK and initialize only
173
176
  missing Arcane files:
174
177
 
175
178
  ```bash
176
- npm install --save-dev --save-exact arcane-os@0.5.0
179
+ npm install --save-dev --save-exact arcane-os@0.5.2
177
180
  npm exec -- arcane init my-app --target portable
178
181
  ```
179
182
 
@@ -189,7 +192,7 @@ npm exec -- arcane-os targets
189
192
  No global SDK install or standalone Arcane CLI is required. The application
190
193
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
191
194
 
192
- Use `npx arcane-os@0.5.0` for the initial bootstrap because it names this npm
195
+ Use `npx arcane-os@0.5.2` for the initial bootstrap because it names this npm
193
196
  package explicitly; bare `npx arcane` outside an installed project could resolve
194
197
  a different package. Both installed commands invoke the same headless toolchain.
195
198
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -210,7 +213,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
210
213
 
211
214
  # From the generated app repository
212
215
  cd ../local-app
213
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.0.tgz
216
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.5.2.tgz
214
217
  npm run check
215
218
  npm ci
216
219
  ```
@@ -220,7 +223,7 @@ same location. The lockfile retains the selected package dependency while
220
223
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
221
224
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
222
225
  runner also needs that tarball at the locked path. After publication, replace
223
- the local declaration with the exact `arcane-os@0.5.0` registry package and
226
+ the local declaration with the exact `arcane-os@0.5.2` registry package and
224
227
  commit the regenerated lock.
225
228
 
226
229
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -358,7 +361,7 @@ package installation, or assertions.
358
361
 
359
362
  ## Current target support
360
363
 
361
- Version `0.5.0` exposes one browser target and five explicitly paired
364
+ Version `0.5.2` exposes one browser target and five explicitly paired
362
365
  native development targets: a non-runnable portable directory, a
363
366
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
364
367
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -81,18 +81,30 @@ async function readManagedImportMapContext(source,signal,testFile){
81
81
  :context.boundary==='test'
82
82
  ?testCodeRoot
83
83
  :path.join(applicationRoot,'dist');
84
- let baseInfo;
85
- let canonicalBase;
86
- try{
87
- baseInfo=await lstat(expectedBase);
88
- canonicalBase=await realpath(expectedBase);
89
- }catch(error){
90
- importMapFailure(`Selected ${context.boundary} application directory is unavailable: ${error.message}`);
84
+ const workspaceRoot=path.resolve(process.cwd());
85
+ const requestedBases=context.boundary==='source'&&pathInside(workspaceRoot,applicationRoot)
86
+ ?[expectedBase,workspaceRoot]
87
+ :[expectedBase];
88
+ let canonicalBase=null;
89
+ for(const requestedBase of requestedBases){
90
+ let baseInfo;
91
+ let candidate;
92
+ try{
93
+ baseInfo=await lstat(requestedBase);
94
+ candidate=await realpath(requestedBase);
95
+ }catch{
96
+ continue;
97
+ }
98
+ if(!baseInfo.isSymbolicLink()&&baseInfo.isDirectory()
99
+ &&samePath(requestedBase,candidate)&&samePath(suppliedBase,candidate)){
100
+ canonicalBase=candidate;
101
+ break;
102
+ }
91
103
  }
92
- if(baseInfo.isSymbolicLink()||!baseInfo.isDirectory()
93
- ||!samePath(expectedBase,canonicalBase)||!samePath(suppliedBase,canonicalBase)){
104
+ if(canonicalBase===null){
94
105
  importMapFailure(
95
- `Managed import-map base URL must select this application's physical ${context.boundary} directory.`
106
+ `Managed import-map base URL must select this application's physical ${context.boundary} directory`
107
+ +(context.boundary==='source'?' or its physical workspace source directory.':'.')
96
108
  );
97
109
  }
98
110
  throwIfCancelled(signal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",
@@ -969,16 +969,21 @@ class AI {
969
969
  return {
970
970
  OPENAI: {
971
971
  'Content-Type': 'application/json',
972
- 'Authorization': `Bearer ${this.license}`
972
+ 'Authorization': `Bearer ${this.twinKey}`
973
973
  }
974
974
  };
975
975
  }
976
976
 
977
+ get #legacyOpenAISpeechKey(){
978
+ const value=globalThis.arcane?.config?.openAI?.apiKey;
979
+ return typeof value==='string'?value:'';
980
+ }
981
+
977
982
  get #ttsHeaders(){
978
983
  return {
979
984
  OPENAI: {
980
985
  'Content-Type': 'application/json',
981
- 'Authorization': `Bearer ${this.license}`
986
+ 'Authorization': `Bearer ${this.#legacyOpenAISpeechKey}`
982
987
  },
983
988
  LOCAL_SPEACH: {
984
989
  'Content-Type': 'application/json',
@@ -989,7 +994,7 @@ class AI {
989
994
  get #sttHeaders(){
990
995
  return {
991
996
  OPENAI: {
992
- 'Authorization': `Bearer ${this.license}`,
997
+ 'Authorization': `Bearer ${this.#legacyOpenAISpeechKey}`,
993
998
  },
994
999
  LOCAL_SPEACH: {}
995
1000
  };
@@ -1160,7 +1165,6 @@ class AI {
1160
1165
  get twinKey(){
1161
1166
  return this.#license
1162
1167
  ||globalThis.arcane?.config?.twinCloud?.accessKey
1163
- ||globalThis.arcane?.config?.openAI?.apiKey
1164
1168
  ||'';
1165
1169
  }
1166
1170
 
@@ -1442,7 +1446,8 @@ class AI {
1442
1446
  return false;
1443
1447
  }
1444
1448
  if(providerId==='OPENAI'){
1445
- return Boolean(this.license)&&typeof globalThis.fetch==='function';
1449
+ return Boolean(this.#legacyOpenAISpeechKey)
1450
+ &&typeof globalThis.fetch==='function';
1446
1451
  }
1447
1452
  if(providerId==='LOCAL_SPEACH'){
1448
1453
  return Boolean(this.#nativeSpeech(service,role));
@@ -1870,7 +1875,11 @@ class AI {
1870
1875
  throw error;
1871
1876
  }
1872
1877
 
1873
- if(service==='OPENAI'&&this.license){
1878
+ if(service==='OPENAI'&&(
1879
+ role==='llm'
1880
+ ?Boolean(this.twinKey)
1881
+ :Boolean(this.#legacyOpenAISpeechKey)
1882
+ )){
1874
1883
  return true;
1875
1884
  }
1876
1885
 
@@ -5,6 +5,7 @@ import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
5
5
  import {runProcess} from './process.mjs';
6
6
 
7
7
  const TEST_RUNNER_PATH=fileURLToPath(new URL('../bin/arcane-test.mjs',import.meta.url));
8
+ const MANAGED_IMPORT_MAP_ENV='ARCANE_TEST_IMPORT_MAP_CONTEXT';
8
9
 
9
10
  function normalizedTestOutput(value){
10
11
  return typeof value==='string'?value:'';
@@ -129,6 +130,7 @@ export async function runApplicationTests({
129
130
  workspaceMode='packager',
130
131
  appId,
131
132
  appRoot,
133
+ managedImportMapContext=null,
132
134
  signal,
133
135
  onEvent
134
136
  }={}){
@@ -160,6 +162,11 @@ export async function runApplicationTests({
160
162
  [TEST_RUNNER_PATH,testFile],
161
163
  {
162
164
  cwd:workspaceRoot,
165
+ env:{
166
+ [MANAGED_IMPORT_MAP_ENV]:managedImportMapContext===null
167
+ ?''
168
+ :JSON.stringify(managedImportMapContext)
169
+ },
163
170
  signal,
164
171
  onEvent,
165
172
  allowNonzero:true
@@ -1602,6 +1602,55 @@ export async function createApplicationTestImportMapContext({
1602
1602
  };
1603
1603
  }
1604
1604
 
1605
+ export async function readApplicationTestImportMapContext({
1606
+ workspaceRoot,
1607
+ applicationRoot,
1608
+ signal
1609
+ }={}){
1610
+ if(typeof workspaceRoot!=='string'||workspaceRoot.trim()===''){
1611
+ throw new TypeError('workspaceRoot must be a nonempty string.');
1612
+ }
1613
+ if(typeof applicationRoot!=='string'||applicationRoot.trim()===''){
1614
+ throw new TypeError('applicationRoot must be a nonempty string.');
1615
+ }
1616
+ throwIfAborted(signal);
1617
+ const resolvedWorkspaceRoot=path.resolve(workspaceRoot);
1618
+ const resolvedApplicationRoot=path.resolve(applicationRoot);
1619
+ await physicalDirectory(resolvedWorkspaceRoot,resolvedApplicationRoot);
1620
+ const artifactPath=path.join(
1621
+ resolvedApplicationRoot,
1622
+ ...IMPORT_MAP_RELATIVE_PATH.split('/')
1623
+ );
1624
+ let source;
1625
+ try{
1626
+ source=await readPhysicalTextFile(
1627
+ resolvedApplicationRoot,
1628
+ artifactPath,
1629
+ 'Application test import-map artifact'
1630
+ );
1631
+ }catch(error){
1632
+ if(error?.code==='ENOENT')return null;
1633
+ throw error;
1634
+ }
1635
+ throwIfAborted(signal);
1636
+ let document;
1637
+ try{document=JSON.parse(source);}
1638
+ catch(error){
1639
+ fail(`Application test import-map artifact is not valid JSON: ${error.message}`);
1640
+ }
1641
+ if(document===null||typeof document!=='object'||Array.isArray(document)
1642
+ ||document.imports===null||typeof document.imports!=='object'
1643
+ ||Array.isArray(document.imports)){
1644
+ fail('Application test import-map artifact must contain an imports object.');
1645
+ }
1646
+ return createApplicationTestImportMapContext({
1647
+ applicationRoot:resolvedWorkspaceRoot,
1648
+ boundary:'source',
1649
+ imports:document.imports,
1650
+ signal
1651
+ });
1652
+ }
1653
+
1605
1654
  async function generateImportMapUnlocked({
1606
1655
  workspaceRoot,
1607
1656
  appId,
package/src/toolchain.mjs CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  } from './workspace.mjs';
11
11
  import {loadArcaneIntegratedProvider} from './integrated-provider-loader.mjs';
12
12
  import {startDevServer} from './dev-server.mjs';
13
- import {generateImportMap} from './import-map.mjs';
13
+ import {generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
14
14
  import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
15
15
  import {
16
16
  inspectApp as inspectPackagedApp,
@@ -389,11 +389,21 @@ export async function doctorApplication(options={}){
389
389
 
390
390
  async function runPreparedApplicationTests(prepared,options={}){
391
391
  throwIfAborted(options.signal);
392
+ const integratedLegacy=prepared.workspaceMode==='integrated'
393
+ &&prepared.validation.config.browserRuntimeLayout==='integrated-legacy';
394
+ const managedImportMapContext=integratedLegacy
395
+ ?null
396
+ :await readApplicationTestImportMapContext({
397
+ workspaceRoot:prepared.workspaceRoot,
398
+ applicationRoot:prepared.appRoot,
399
+ signal:options.signal
400
+ });
392
401
  return runApplicationTests({
393
402
  workspaceRoot:prepared.workspaceRoot,
394
403
  workspaceMode:prepared.workspaceMode,
395
404
  appId:prepared.appId,
396
405
  appRoot:prepared.appRoot,
406
+ managedImportMapContext,
397
407
  signal:options.signal,
398
408
  onEvent:options.onEvent
399
409
  });