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.
- package/CHANGELOG.md +28 -0
- package/README.md +14 -11
- package/browser-runtime/ai/browser-device-settings.mjs +2 -2
- package/browser-runtime/ai/twin-cloud.mjs +230 -0
- package/docs/reference/ai/twin-cloud.md +120 -3
- package/docs/reference/availability-and-normalization.md +9 -0
- package/docs/reference/cli.md +25 -25
- package/docs/reference/inventory/package-api.json +99 -1
- package/docs/reference/mail.md +5 -5
- package/docs/reference/protocols.md +17 -26
- package/docs/reference/pwa.md +25 -45
- package/docs/reference/sdk-api.md +277 -11
- package/docs/reviews/mail-server-purpose-review.md +1 -1
- package/package.json +3 -2
- package/runtime/arcane/modules/AI.js +14 -154
- package/src/app-layout.mjs +0 -22
- package/src/cli/main.mjs +1 -1
- package/src/dev-server.mjs +4 -40
- package/src/import-map.mjs +1 -0
- package/src/mail-credentials.mjs +3 -3
- package/src/packager/core.mjs +5 -48
- package/src/pwa.mjs +0 -43
- package/src/scaffold.mjs +3 -2
- package/src/templates/workspace-template.mjs +3 -2
- package/src/toolchain.mjs +4 -39
|
@@ -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
|
|
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.
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
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
|
|
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
|
|
package/src/app-layout.mjs
CHANGED
|
@@ -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']??'
|
|
509
|
+
appsRoot:values['apps-root']??'.',
|
|
510
510
|
displayName:values['display-name'],
|
|
511
511
|
target:values.target??'browser',
|
|
512
512
|
initializeGit:flags.has('git')
|
package/src/dev-server.mjs
CHANGED
|
@@ -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
|
|
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
|
|
1181
|
-
|
|
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'
|
|
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);
|
package/src/import-map.mjs
CHANGED
|
@@ -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'){
|
package/src/mail-credentials.mjs
CHANGED
|
@@ -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
|
|
85
|
-
if (
|
|
86
|
-
return {profileSettings
|
|
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
|
}
|
package/src/packager/core.mjs
CHANGED
|
@@ -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
|
|
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'
|
|
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)
|
|
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='
|
|
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??'
|
|
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,
|