arcane-os 0.29.0 → 0.30.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 +25 -0
- package/README.md +8 -4
- package/browser-runtime/ai/twin-cloud.mjs +230 -0
- package/docs/architecture.md +20 -5
- package/docs/reference/ai/twin-cloud.md +120 -3
- package/docs/reference/asset-versioning.md +7 -6
- package/docs/reference/availability-and-normalization.md +9 -0
- package/docs/reference/inventory/package-api.json +102 -4
- package/docs/reference/pwa.md +4 -3
- package/docs/reference/sdk-api.md +286 -4
- package/package.json +3 -2
- package/runtime/arcane/modules/AI.js +14 -154
- package/schemas/arcane-app-bundle.schema.json +1 -1
- package/src/import-map.mjs +6 -8
- package/src/release-bundle.mjs +100 -12
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'){
|
|
@@ -847,7 +848,7 @@ function assetUrlVersionEdits(value,version){
|
|
|
847
848
|
let decodedKey=key;
|
|
848
849
|
try{decodedKey=decodeURIComponent(key.replaceAll('+',' '));}
|
|
849
850
|
catch{decodedKey=key;}
|
|
850
|
-
const remove=decodedKey==='
|
|
851
|
+
const remove=decodedKey==='arcaneVersion'&&(clean||versionFound);
|
|
851
852
|
parameters.push({start:offset,end:offset+parameter.length,remove});
|
|
852
853
|
if(decodedKey==='arcaneVersion'&&!versionFound&&!clean){
|
|
853
854
|
versionFound=true;
|
|
@@ -859,10 +860,10 @@ function assetUrlVersionEdits(value,version){
|
|
|
859
860
|
}
|
|
860
861
|
offset+=parameter.length+1;
|
|
861
862
|
}
|
|
862
|
-
if(clean
|
|
863
|
-
return
|
|
863
|
+
if(clean&¶meters.every(function removesSdkField(parameter){
|
|
864
|
+
return parameter.remove;
|
|
864
865
|
}))return [{start:queryStart,end:address.length,value:''}];
|
|
865
|
-
// Remove adjacent
|
|
866
|
+
// Remove adjacent SDK version fields together, including only their separator.
|
|
866
867
|
// Other field spelling and source-level escapes remain untouched.
|
|
867
868
|
for(let index=0;index<parameters.length;index+=1){
|
|
868
869
|
if(!parameters[index].remove)continue;
|
|
@@ -874,13 +875,10 @@ function assetUrlVersionEdits(value,version){
|
|
|
874
875
|
value:''
|
|
875
876
|
});
|
|
876
877
|
}
|
|
877
|
-
const lastRetained=parameters.findLast(function retainedParameter(parameter){
|
|
878
|
-
return !parameter.remove;
|
|
879
|
-
});
|
|
880
878
|
if(!clean&&!versionFound)edits.push({
|
|
881
879
|
start:address.length,
|
|
882
880
|
end:address.length,
|
|
883
|
-
value
|
|
881
|
+
value:`&arcaneVersion=${versionValue}`
|
|
884
882
|
});
|
|
885
883
|
return edits;
|
|
886
884
|
}
|
package/src/release-bundle.mjs
CHANGED
|
@@ -93,7 +93,7 @@ function splitUstarPath(archivePath){
|
|
|
93
93
|
return {name,prefix};
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
return null;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function writeTextField(header,offset,length,value,label){
|
|
@@ -112,8 +112,10 @@ function writeOctalField(header,offset,length,value,label,{trailingSpace=false}=
|
|
|
112
112
|
header.write(rendered,offset,length,'ascii');
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
const
|
|
115
|
+
function createTarHeader(archivePath,size,type){
|
|
116
|
+
const fields=splitUstarPath(archivePath);
|
|
117
|
+
if(!fields)fail(`Archive path cannot be represented by ustar: ${archivePath}.`);
|
|
118
|
+
const {name,prefix}=fields;
|
|
117
119
|
const header=Buffer.alloc(TAR_BLOCK_SIZE);
|
|
118
120
|
writeTextField(header,0,100,name,'ustar name');
|
|
119
121
|
writeOctalField(header,100,8,ARCHIVE_MODE,'ustar mode');
|
|
@@ -122,7 +124,7 @@ export function createCanonicalUstarHeader(archivePath,size){
|
|
|
122
124
|
writeOctalField(header,124,12,size,'ustar size');
|
|
123
125
|
writeOctalField(header,136,12,0,'ustar mtime');
|
|
124
126
|
header.fill(0x20,148,156);
|
|
125
|
-
header[156]=
|
|
127
|
+
header[156]=type;
|
|
126
128
|
header.write('ustar\0',257,6,'ascii');
|
|
127
129
|
header.write('00',263,2,'ascii');
|
|
128
130
|
writeTextField(header,265,32,'root','ustar owner');
|
|
@@ -134,13 +136,45 @@ export function createCanonicalUstarHeader(archivePath,size){
|
|
|
134
136
|
return header;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
+
export function createCanonicalUstarHeader(archivePath,size){
|
|
140
|
+
return createTarHeader(archivePath,size,0x30);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function tarRecord(archivePath,content,type=0x30){
|
|
144
|
+
const header=createTarHeader(archivePath,content.length,type);
|
|
139
145
|
const remainder=content.length%TAR_BLOCK_SIZE;
|
|
140
146
|
const padding=remainder===0?Buffer.alloc(0):Buffer.alloc(TAR_BLOCK_SIZE-remainder);
|
|
141
147
|
return Buffer.concat([header,content,padding]);
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
function paxPathRecord(archivePath) {
|
|
151
|
+
const value = Buffer.from(` path=${archivePath}\n`, 'utf8');
|
|
152
|
+
// POSIX PAX framing includes the decimal prefix itself in the record length.
|
|
153
|
+
let length = value.length + 1;
|
|
154
|
+
while(String(length).length + value.length !== length) {
|
|
155
|
+
length = String(length).length + value.length;
|
|
156
|
+
}
|
|
157
|
+
const prefix = Buffer.from(String(length), 'ascii');
|
|
158
|
+
return Buffer.concat(
|
|
159
|
+
[prefix, value]
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function tarEntry(archivePath, content) {
|
|
164
|
+
const fields = splitUstarPath(archivePath);
|
|
165
|
+
if(fields && !/[^\u0000-\u007f]/u.test(archivePath)) {
|
|
166
|
+
return tarRecord(archivePath, content);
|
|
167
|
+
}
|
|
168
|
+
// The following regular header is only a placeholder; PAX owns the complete path.
|
|
169
|
+
const extendedPath = paxPathRecord(archivePath);
|
|
170
|
+
return Buffer.concat(
|
|
171
|
+
[
|
|
172
|
+
tarRecord('PaxHeaders/entry', extendedPath, 0x78),
|
|
173
|
+
tarRecord('PaxPayload/entry', content)
|
|
174
|
+
]
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
144
178
|
async function realDirectory(location,label){
|
|
145
179
|
const requested=path.resolve(location);
|
|
146
180
|
let info;
|
|
@@ -308,9 +342,53 @@ function readOctalField(header,offset,length,label){
|
|
|
308
342
|
return value;
|
|
309
343
|
}
|
|
310
344
|
|
|
345
|
+
function readPaxPath(content) {
|
|
346
|
+
let offset = 0;
|
|
347
|
+
let archivePath;
|
|
348
|
+
while(offset < content.length) {
|
|
349
|
+
const separator = content.indexOf(0x20, offset);
|
|
350
|
+
if(separator < 0) {
|
|
351
|
+
fail('Bundle contains malformed PAX record framing.');
|
|
352
|
+
}
|
|
353
|
+
const lengthText = content.subarray(offset, separator).toString('latin1');
|
|
354
|
+
if(!/^[1-9][0-9]*$/u.test(lengthText)) {
|
|
355
|
+
fail('Bundle contains malformed PAX record framing.');
|
|
356
|
+
}
|
|
357
|
+
const length = Number(lengthText);
|
|
358
|
+
const end = offset + length;
|
|
359
|
+
if(!is.safeInteger(length) || end > content.length || end <= separator + 1
|
|
360
|
+
|| content[end - 1] !== 0x0a) {
|
|
361
|
+
fail('Bundle contains an incomplete or malformed PAX record.');
|
|
362
|
+
}
|
|
363
|
+
const assignment = content.indexOf(0x3d, separator + 1);
|
|
364
|
+
if(assignment <= separator + 1 || assignment >= end - 1) {
|
|
365
|
+
fail('Bundle contains a malformed PAX assignment.');
|
|
366
|
+
}
|
|
367
|
+
const keyword = content.subarray(separator + 1, assignment).toString('utf8');
|
|
368
|
+
if(keyword !== 'path') {
|
|
369
|
+
fail(`Bundle contains an unsupported PAX field: ${keyword}.`);
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const decoder = new TextDecoder(
|
|
373
|
+
'utf-8',
|
|
374
|
+
{fatal: true, ignoreBOM: true}
|
|
375
|
+
);
|
|
376
|
+
archivePath = decoder.decode(content.subarray(assignment + 1, end - 1));
|
|
377
|
+
} catch (error) {
|
|
378
|
+
fail(`Bundle PAX path is not valid UTF-8: ${error.message}.`);
|
|
379
|
+
}
|
|
380
|
+
offset = end;
|
|
381
|
+
}
|
|
382
|
+
if(archivePath === undefined) {
|
|
383
|
+
fail('Bundle PAX header is missing its path.');
|
|
384
|
+
}
|
|
385
|
+
return archivePath;
|
|
386
|
+
}
|
|
387
|
+
|
|
311
388
|
function readTarEntries(archive){
|
|
312
389
|
const entries=new Map();
|
|
313
390
|
let offset=0;
|
|
391
|
+
let pendingPath;
|
|
314
392
|
while(offset+TAR_BLOCK_SIZE<=archive.length){
|
|
315
393
|
const header=archive.subarray(offset,offset+TAR_BLOCK_SIZE);
|
|
316
394
|
if(header.every(value=>value===0))break;
|
|
@@ -321,21 +399,31 @@ function readTarEntries(archive){
|
|
|
321
399
|
let actualChecksum=0;
|
|
322
400
|
for(const value of checksumHeader)actualChecksum+=value;
|
|
323
401
|
if(actualChecksum!==expectedChecksum)fail('Bundle contains a malformed ustar header.');
|
|
324
|
-
|
|
402
|
+
const type=header[156];
|
|
403
|
+
if(type!==0&&type!==0x30&&type!==0x78)fail('Bundle contains a non-file archive entry.');
|
|
325
404
|
const name=readStringField(header,0,100);
|
|
326
405
|
const prefix=readStringField(header,345,155);
|
|
327
|
-
const
|
|
328
|
-
if(entries.has(pathKey(archivePath)))fail(`Bundle contains a duplicate path: ${archivePath}.`);
|
|
406
|
+
const headerPath=prefix?`${prefix}/${name}`:name;
|
|
329
407
|
const size=readOctalField(header,124,12,'ustar size');
|
|
330
408
|
const contentStart=offset+TAR_BLOCK_SIZE;
|
|
331
409
|
const contentEnd=contentStart+size;
|
|
332
|
-
if(contentEnd>archive.length)fail(`Bundle entry is incomplete: ${
|
|
410
|
+
if(contentEnd>archive.length)fail(`Bundle entry is incomplete: ${pendingPath??headerPath}.`);
|
|
411
|
+
const content=archive.subarray(contentStart,contentEnd);
|
|
412
|
+
offset=contentStart+Math.ceil(size/TAR_BLOCK_SIZE)*TAR_BLOCK_SIZE;
|
|
413
|
+
if(type===0x78){
|
|
414
|
+
if(pendingPath!==undefined)fail('Bundle PAX path is missing its following file.');
|
|
415
|
+
pendingPath=readPaxPath(content);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const archivePath=validateAppBundlePath(pendingPath??headerPath,'archive path');
|
|
419
|
+
pendingPath=undefined;
|
|
420
|
+
if(entries.has(pathKey(archivePath)))fail(`Bundle contains a duplicate path: ${archivePath}.`);
|
|
333
421
|
entries.set(pathKey(archivePath),{
|
|
334
422
|
path:archivePath,
|
|
335
|
-
content:Buffer.from(
|
|
423
|
+
content:Buffer.from(content)
|
|
336
424
|
});
|
|
337
|
-
offset=contentStart+Math.ceil(size/TAR_BLOCK_SIZE)*TAR_BLOCK_SIZE;
|
|
338
425
|
}
|
|
426
|
+
if(pendingPath!==undefined)fail('Bundle PAX path is missing its following file.');
|
|
339
427
|
return entries;
|
|
340
428
|
}
|
|
341
429
|
|