arcane-os 0.80.0 → 0.82.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 +44 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
- package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
- package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
- package/browser-runtime/core/app-control.mjs +439 -0
- package/docs/reference/ai/browser-speech.md +11 -0
- package/docs/reference/ai/browser-wasm.md +21 -10
- package/docs/reference/cli.md +9 -1
- package/docs/reference/core-native-packaging.md +14 -0
- package/docs/reference/inventory/package-api.json +23 -9
- package/docs/reference/inventory/runtime-components.json +2 -1
- package/docs/reference/native-app-control.md +147 -0
- package/docs/reference/native-decisions.md +74 -16
- package/docs/reference/runtime-components.md +20 -0
- package/docs/reference/sdk-api.md +49 -6
- package/package.json +2 -1
- package/runtime/arcane/components/chat.html +140 -18
- package/runtime/arcane/modules/AI.js +10 -5
- package/src/cli/app-control.mjs +106 -0
- package/src/cli/main.mjs +5 -0
- package/src/core/app-control.mjs +23 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
- package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
- package/src/core/hosts/windows/ArcaneHost.cs +45 -7
- package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
- package/src/core/services/decisions.mjs +16 -2
- package/src/core/shared-host.mjs +7 -91
- package/src/core/socket-connection.mjs +109 -0
- package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
- package/src/local-ai/decision-tokenizer.mjs +2 -2
- package/src/local-ai/decisions.mjs +33 -10
|
@@ -886,6 +886,7 @@
|
|
|
886
886
|
const aiActivationButton=shadowRoot.querySelector('#ai_activation_button');
|
|
887
887
|
const [
|
|
888
888
|
{default:MD},
|
|
889
|
+
{hydrateMarkdownMedia},
|
|
889
890
|
{default:FileEntity},
|
|
890
891
|
{ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
|
|
891
892
|
{requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
@@ -893,6 +894,7 @@
|
|
|
893
894
|
{getBrowserDeviceClass}
|
|
894
895
|
]=await Promise.all([
|
|
895
896
|
import('../modules/MD.js'),
|
|
897
|
+
import('../modules/MarkdownMedia.js'),
|
|
896
898
|
import('../entities/File.js'),
|
|
897
899
|
import('../modules/ConversationTimebox.js'),
|
|
898
900
|
import('../modules/AIRuntimeState.js'),
|
|
@@ -946,15 +948,19 @@
|
|
|
946
948
|
let destroyed=false;
|
|
947
949
|
let recognitionDraft=null;
|
|
948
950
|
let writingRecognitionDraft=false;
|
|
951
|
+
const transcriptMedia=new Map();
|
|
952
|
+
let printMediaRetains=null;
|
|
949
953
|
const printViewPromise=import('../modules/PrintView.js').then(
|
|
950
954
|
function registerChatPrint({createPrintView}){
|
|
951
955
|
if(destroyed) return null;
|
|
952
956
|
return createPrintView({
|
|
953
957
|
host,
|
|
954
|
-
content:
|
|
958
|
+
content:renderedChatPrintContent,
|
|
955
959
|
title:function chatPrintTitle(){return host.printTitle||'Conversation';},
|
|
956
960
|
active:function chatPrintActive(){return !destroyed&&chatOutput.children.length>0;},
|
|
957
961
|
signal:aiRuntimeStateAbortController.signal,
|
|
962
|
+
retain:retainChatPrintMedia,
|
|
963
|
+
prepare:prepareChatPrintMedia,
|
|
958
964
|
onError:reportChatPrintFailure
|
|
959
965
|
});
|
|
960
966
|
}
|
|
@@ -1190,6 +1196,7 @@
|
|
|
1190
1196
|
for(const item of record.removedNodes) {
|
|
1191
1197
|
if(item.localName === 'li' && item.parentElement !== chatOutput) {
|
|
1192
1198
|
transcriptResizeObserver?.unobserve(item);
|
|
1199
|
+
releaseTranscriptMessageMedia(item);
|
|
1193
1200
|
}
|
|
1194
1201
|
}
|
|
1195
1202
|
for(const item of record.addedNodes) {
|
|
@@ -1427,14 +1434,72 @@
|
|
|
1427
1434
|
return entry;
|
|
1428
1435
|
}
|
|
1429
1436
|
|
|
1437
|
+
function renderTranscriptMarkdown(target,content){
|
|
1438
|
+
if(destroyed) return null;
|
|
1439
|
+
const template=target.ownerDocument.createElement('template');
|
|
1440
|
+
template.innerHTML=new MD(content).rendered;
|
|
1441
|
+
const pending=[...template.content.querySelectorAll('img')].some(
|
|
1442
|
+
function localChatImage(image){
|
|
1443
|
+
return image.getAttribute('src')?.startsWith('arcane-media:');
|
|
1444
|
+
}
|
|
1445
|
+
);
|
|
1446
|
+
// Remove local-reference src values while detached; text renders now,
|
|
1447
|
+
// and the shared media owner observes independent storage and decoding.
|
|
1448
|
+
const handle=hydrateMarkdownMedia(template.content,{
|
|
1449
|
+
signal:aiRuntimeStateAbortController.signal
|
|
1450
|
+
});
|
|
1451
|
+
releaseTranscriptMarkdown(target);
|
|
1452
|
+
const media={handle,pending,error:null,status:null,ready:null};
|
|
1453
|
+
transcriptMedia.set(target,media);
|
|
1454
|
+
if(printMediaRetains){
|
|
1455
|
+
printMediaRetains.add(handle.retain());
|
|
1456
|
+
}
|
|
1457
|
+
target.raw=content;
|
|
1458
|
+
target.replaceChildren(template.content);
|
|
1459
|
+
media.ready=handle.ready.then(function transcriptImagesReady(){
|
|
1460
|
+
media.pending=false;
|
|
1461
|
+
},function transcriptImagesFailed(error){
|
|
1462
|
+
media.pending=false;
|
|
1463
|
+
media.error=error;
|
|
1464
|
+
if(destroyed||transcriptMedia.get(target)!==media) return;
|
|
1465
|
+
arcaneLogging.error('Unable to display saved Chat images.',error);
|
|
1466
|
+
const status=target.ownerDocument.createElement('p');
|
|
1467
|
+
status.className='message_media_status';
|
|
1468
|
+
status.setAttribute('role','status');
|
|
1469
|
+
status.textContent='Some saved images could not be displayed.';
|
|
1470
|
+
media.status=status;
|
|
1471
|
+
target.after(status);
|
|
1472
|
+
scrollTranscriptToBottom();
|
|
1473
|
+
});
|
|
1474
|
+
return media;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function releaseTranscriptMarkdown(target){
|
|
1478
|
+
const media=transcriptMedia.get(target);
|
|
1479
|
+
if(!media) return;
|
|
1480
|
+
transcriptMedia.delete(target);
|
|
1481
|
+
media.status?.remove();
|
|
1482
|
+
media.handle.destroy();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function releaseTranscriptMessageMedia(item){
|
|
1486
|
+
const markdown=item?.querySelector('.markdown');
|
|
1487
|
+
if(markdown) releaseTranscriptMarkdown(markdown);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function removeTranscriptMessage(item){
|
|
1491
|
+
if(!item) return;
|
|
1492
|
+
releaseTranscriptMessageMedia(item);
|
|
1493
|
+
item.remove();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1430
1496
|
function setTranscriptMessageContent(item,content,timestamp){
|
|
1431
1497
|
if(!is.string(content)){
|
|
1432
1498
|
throw new TypeError('Chat session message content must be a string.');
|
|
1433
1499
|
}
|
|
1434
1500
|
item.querySelector('.thinking')?.remove();
|
|
1435
1501
|
const markdown=item.querySelector('.markdown');
|
|
1436
|
-
markdown
|
|
1437
|
-
markdown.innerHTML=new MD(content).rendered;
|
|
1502
|
+
renderTranscriptMarkdown(markdown,content);
|
|
1438
1503
|
if(timestamp!==undefined){
|
|
1439
1504
|
item.querySelector('.message_timestamp')?.replaceWith(
|
|
1440
1505
|
transcriptTime(timestamp)
|
|
@@ -1495,12 +1560,11 @@
|
|
|
1495
1560
|
header.append(heading);
|
|
1496
1561
|
const markdown=chatOutput.ownerDocument.createElement('div');
|
|
1497
1562
|
markdown.className='markdown';
|
|
1498
|
-
markdown.raw=content;
|
|
1499
|
-
markdown.innerHTML=new MD(content).rendered;
|
|
1500
1563
|
item.append(header,markdown,transcriptTime(timestamp));
|
|
1501
1564
|
for(const call of normalizeVisibleToolCalls(toolCalls)){
|
|
1502
1565
|
appendVisibleToolCall(item,call);
|
|
1503
1566
|
}
|
|
1567
|
+
renderTranscriptMarkdown(markdown,content);
|
|
1504
1568
|
return item;
|
|
1505
1569
|
}
|
|
1506
1570
|
|
|
@@ -1642,6 +1706,9 @@
|
|
|
1642
1706
|
setPendingStructuralToolCalls(
|
|
1643
1707
|
actionable?[...restoredPendingToolCalls.values()]:[]
|
|
1644
1708
|
);
|
|
1709
|
+
for(const item of chatOutput.children){
|
|
1710
|
+
releaseTranscriptMessageMedia(item);
|
|
1711
|
+
}
|
|
1645
1712
|
chatOutput.replaceChildren(fragment);
|
|
1646
1713
|
transcriptFollowing = true;
|
|
1647
1714
|
transcriptScrollTop = chatOutput.scrollTop;
|
|
@@ -3034,9 +3101,7 @@
|
|
|
3034
3101
|
thinkingHTML.remove();
|
|
3035
3102
|
}
|
|
3036
3103
|
|
|
3037
|
-
target.raw
|
|
3038
|
-
|
|
3039
|
-
target.innerHTML=new MD(target.raw).rendered;
|
|
3104
|
+
renderTranscriptMarkdown(target,target.raw?target.raw+text:text);
|
|
3040
3105
|
|
|
3041
3106
|
scrollTranscriptToBottom();
|
|
3042
3107
|
}
|
|
@@ -3393,8 +3458,7 @@
|
|
|
3393
3458
|
message.querySelector('.thinking')?.remove();
|
|
3394
3459
|
const markdown=message.querySelector('.markdown');
|
|
3395
3460
|
const text=visibleErrorMessage(error,'The AI request failed.');
|
|
3396
|
-
markdown
|
|
3397
|
-
markdown.innerHTML=new MD(text).rendered;
|
|
3461
|
+
renderTranscriptMarkdown(markdown,text);
|
|
3398
3462
|
scrollTranscriptToBottom();
|
|
3399
3463
|
return true;
|
|
3400
3464
|
}
|
|
@@ -3421,8 +3485,8 @@
|
|
|
3421
3485
|
const response=[...chatOutput.children].find(
|
|
3422
3486
|
candidate=>candidate.id===`message-${messageId}`
|
|
3423
3487
|
)||null;
|
|
3424
|
-
response
|
|
3425
|
-
for(const request of requestMessages) request
|
|
3488
|
+
removeTranscriptMessage(response);
|
|
3489
|
+
for(const request of requestMessages) removeTranscriptMessage(request);
|
|
3426
3490
|
const existing=textArea.value;
|
|
3427
3491
|
if(existing!==text){
|
|
3428
3492
|
textArea.value=existing
|
|
@@ -3500,7 +3564,7 @@
|
|
|
3500
3564
|
function removeTransientSessionCards(){
|
|
3501
3565
|
if(retainTurn||!transientCards.length) return;
|
|
3502
3566
|
// The submitter supplies only nodes created for this submission.
|
|
3503
|
-
for(const card of transientCards) card
|
|
3567
|
+
for(const card of transientCards) removeTranscriptMessage(card);
|
|
3504
3568
|
transientCards.length=0;
|
|
3505
3569
|
scrollTranscriptToBottom();
|
|
3506
3570
|
}
|
|
@@ -3675,9 +3739,9 @@
|
|
|
3675
3739
|
const response=[...chatOutput.children].find(
|
|
3676
3740
|
candidate=>candidate.id===`message-${messageId}`
|
|
3677
3741
|
)||null;
|
|
3678
|
-
response
|
|
3742
|
+
removeTranscriptMessage(response);
|
|
3679
3743
|
for(const requestMessage of requestMessages){
|
|
3680
|
-
requestMessage
|
|
3744
|
+
removeTranscriptMessage(requestMessage);
|
|
3681
3745
|
}
|
|
3682
3746
|
scrollTranscriptToBottom();
|
|
3683
3747
|
}else if(retainTurn){
|
|
@@ -3692,7 +3756,7 @@
|
|
|
3692
3756
|
arcaneLogging.error('Arcane chat request failed.',error);
|
|
3693
3757
|
if(sessionRequestMessages.every(message=>message.role==='tool')){
|
|
3694
3758
|
for(const requestMessage of requestMessages){
|
|
3695
|
-
requestMessage
|
|
3759
|
+
removeTranscriptMessage(requestMessage);
|
|
3696
3760
|
}
|
|
3697
3761
|
}
|
|
3698
3762
|
if(retainTurn) renderSessionMessageFailure(messageId,error);
|
|
@@ -4136,11 +4200,64 @@
|
|
|
4136
4200
|
return message;
|
|
4137
4201
|
}
|
|
4138
4202
|
|
|
4203
|
+
function renderedChatPrintContent(){
|
|
4204
|
+
for(const media of transcriptMedia.values()){
|
|
4205
|
+
if(media.pending){
|
|
4206
|
+
const error=new Error('Saved images are still loading. Print again when they finish.');
|
|
4207
|
+
error.code='ARCANE_MARKDOWN_MEDIA_PENDING';
|
|
4208
|
+
throw error;
|
|
4209
|
+
}
|
|
4210
|
+
if(media.error) throw media.error;
|
|
4211
|
+
}
|
|
4212
|
+
return chatOutput;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
function retainChatPrintMedia(){
|
|
4216
|
+
renderedChatPrintContent();
|
|
4217
|
+
const releases=new Set();
|
|
4218
|
+
for(const media of transcriptMedia.values()){
|
|
4219
|
+
releases.add(media.handle.retain());
|
|
4220
|
+
}
|
|
4221
|
+
printMediaRetains=releases;
|
|
4222
|
+
return function releaseChatPrintMedia(){
|
|
4223
|
+
if(printMediaRetains===releases){
|
|
4224
|
+
printMediaRetains=null;
|
|
4225
|
+
}
|
|
4226
|
+
for(const release of releases) release();
|
|
4227
|
+
releases.clear();
|
|
4228
|
+
};
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
async function prepareChatPrintMedia(signal){
|
|
4232
|
+
let cancel;
|
|
4233
|
+
const cancelled=new Promise(function waitForChatPrintCancellation(resolve){cancel=resolve;});
|
|
4234
|
+
signal?.addEventListener('abort',cancel,{once:true});
|
|
4235
|
+
try{
|
|
4236
|
+
while(!destroyed&&!signal?.aborted){
|
|
4237
|
+
const selected=[...transcriptMedia];
|
|
4238
|
+
await Promise.race([
|
|
4239
|
+
Promise.all(selected.map(function readyChatMedia([,media]){return media.ready;})),
|
|
4240
|
+
cancelled
|
|
4241
|
+
]);
|
|
4242
|
+
if(destroyed||signal?.aborted) return false;
|
|
4243
|
+
if(selected.length!==transcriptMedia.size||selected.some(
|
|
4244
|
+
function replacedChatMedia([target,media]){return transcriptMedia.get(target)!==media;}
|
|
4245
|
+
)) continue;
|
|
4246
|
+
renderedChatPrintContent();
|
|
4247
|
+
return true;
|
|
4248
|
+
}
|
|
4249
|
+
return false;
|
|
4250
|
+
}finally{
|
|
4251
|
+
signal?.removeEventListener('abort',cancel);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4139
4255
|
async function print(){
|
|
4140
4256
|
if(destroyed) return false;
|
|
4141
4257
|
try{
|
|
4142
4258
|
const view=await printViewPromise;
|
|
4143
|
-
|
|
4259
|
+
if(destroyed||!view||!await prepareChatPrintMedia()) return false;
|
|
4260
|
+
return await view.print();
|
|
4144
4261
|
}catch(error){
|
|
4145
4262
|
reportChatPrintFailure(error);
|
|
4146
4263
|
throw error;
|
|
@@ -4150,7 +4267,9 @@
|
|
|
4150
4267
|
function reportChatPrintFailure(error){
|
|
4151
4268
|
arcaneLogging.error('Unable to print the conversation.',error);
|
|
4152
4269
|
if(!destroyed){
|
|
4153
|
-
setSessionStatus('error','
|
|
4270
|
+
setSessionStatus('error',error?.code==='ARCANE_MARKDOWN_MEDIA_PENDING'
|
|
4271
|
+
?'Saved images are still loading. Print again when they finish.'
|
|
4272
|
+
:'Unable to open print preview. Please try again.');
|
|
4154
4273
|
}
|
|
4155
4274
|
}
|
|
4156
4275
|
|
|
@@ -4235,6 +4354,9 @@
|
|
|
4235
4354
|
ownership.abort(destroyReason);
|
|
4236
4355
|
}
|
|
4237
4356
|
aiRuntimeStateAbortController.abort(destroyReason);
|
|
4357
|
+
for(const target of transcriptMedia.keys()){
|
|
4358
|
+
releaseTranscriptMarkdown(target);
|
|
4359
|
+
}
|
|
4238
4360
|
aiActivationController.destroy();
|
|
4239
4361
|
window.removeEventListener('pagehide',handlePageHide);
|
|
4240
4362
|
window.removeEventListener('pageshow',handlePageShow);
|
|
@@ -5018,6 +5018,7 @@ class AI {
|
|
|
5018
5018
|
model,
|
|
5019
5019
|
structuredOutput=false,
|
|
5020
5020
|
localOnly=false,
|
|
5021
|
+
speech=true,
|
|
5021
5022
|
onChunk=function ignoreStreamChunk(){},
|
|
5022
5023
|
toolText,
|
|
5023
5024
|
onToolText,
|
|
@@ -5062,6 +5063,9 @@ class AI {
|
|
|
5062
5063
|
if(localOnly!==true&&localOnly!==false){
|
|
5063
5064
|
throw new TypeError('AI localOnly must be a boolean.');
|
|
5064
5065
|
}
|
|
5066
|
+
if(speech!==true&&speech!==false){
|
|
5067
|
+
throw new TypeError('AI speech must be a boolean.');
|
|
5068
|
+
}
|
|
5065
5069
|
if(localOnly&&!this.#hasLocalRoute('llm',this.llmService)){
|
|
5066
5070
|
const error=new Error(
|
|
5067
5071
|
'This AI request requires a configured local model.'
|
|
@@ -5163,7 +5167,7 @@ class AI {
|
|
|
5163
5167
|
if(signal?.aborted){
|
|
5164
5168
|
throw normalizeAIRequestAbort();
|
|
5165
5169
|
}
|
|
5166
|
-
this.#finishStreamingSpeech();
|
|
5170
|
+
if(speech)this.#finishStreamingSpeech();
|
|
5167
5171
|
return result;
|
|
5168
5172
|
}catch(error){
|
|
5169
5173
|
if(handle){
|
|
@@ -5171,7 +5175,7 @@ class AI {
|
|
|
5171
5175
|
function retainProviderStreamCleanupFailure() {}
|
|
5172
5176
|
);
|
|
5173
5177
|
}
|
|
5174
|
-
this.stopAudio();
|
|
5178
|
+
if(speech)this.stopAudio();
|
|
5175
5179
|
throw isAIRequestAbort(error,signal)
|
|
5176
5180
|
?normalizeAIRequestAbort(error)
|
|
5177
5181
|
:error;
|
|
@@ -5223,10 +5227,10 @@ class AI {
|
|
|
5223
5227
|
if(signal?.aborted){
|
|
5224
5228
|
throw normalizeAIRequestAbort();
|
|
5225
5229
|
}
|
|
5226
|
-
this.#finishStreamingSpeech();
|
|
5230
|
+
if(speech)this.#finishStreamingSpeech();
|
|
5227
5231
|
return result;
|
|
5228
5232
|
}catch(error){
|
|
5229
|
-
this.stopAudio();
|
|
5233
|
+
if(speech)this.stopAudio();
|
|
5230
5234
|
throw isAIRequestAbort(error,signal)
|
|
5231
5235
|
?normalizeAIRequestAbort(error)
|
|
5232
5236
|
:error;
|
|
@@ -6026,7 +6030,8 @@ class AI {
|
|
|
6026
6030
|
}
|
|
6027
6031
|
throw error;
|
|
6028
6032
|
}finally{
|
|
6029
|
-
|
|
6033
|
+
// Provider transports leave speech lifecycle to their public caller.
|
|
6034
|
+
if(finishSpeech&&!speechTurnCompleted){
|
|
6030
6035
|
this.stopAudio();
|
|
6031
6036
|
}
|
|
6032
6037
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {readFile, writeFile} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {connectAppControl} from '../core/app-control.mjs';
|
|
4
|
+
import {CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
|
|
5
|
+
|
|
6
|
+
const HELP_TEXT=`Usage:
|
|
7
|
+
arcane app-control status --endpoint <pipe-or-socket>
|
|
8
|
+
arcane app-control inspect --endpoint <pipe-or-socket> [--selector <css-selector>]
|
|
9
|
+
arcane app-control capture --endpoint <pipe-or-socket> --output <png-file>
|
|
10
|
+
arcane app-control act --endpoint <pipe-or-socket> --request <json-file>
|
|
11
|
+
|
|
12
|
+
Connects to the selected running native window. Results are complete JSON.
|
|
13
|
+
Capture writes the PNG to --output and prints its metadata. Request and output
|
|
14
|
+
filenames are relative to the current directory. This command does not launch
|
|
15
|
+
an application or change its profile. --output selects the capture filename;
|
|
16
|
+
the other Arcane commands retain their existing output-format option.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
function usage(message) {
|
|
20
|
+
throw new CoreError({code: 'APP_CONTROL_USAGE', message, exitCode: 2});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseArguments(argv) {
|
|
24
|
+
if (!argv.length || argv.includes('--help') || argv.includes('-h')) return {help: true};
|
|
25
|
+
const [action] = argv;
|
|
26
|
+
const allowed = {
|
|
27
|
+
status: ['endpoint'],
|
|
28
|
+
inspect: ['endpoint', 'selector'],
|
|
29
|
+
capture: ['endpoint', 'output'],
|
|
30
|
+
act: ['endpoint', 'request']
|
|
31
|
+
}[action];
|
|
32
|
+
if (!Array.isArray(allowed)) usage('Expected app-control status, inspect, capture, or act.');
|
|
33
|
+
const values = {};
|
|
34
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
35
|
+
const argument = argv[index];
|
|
36
|
+
const equal = argument.indexOf('=');
|
|
37
|
+
const option = equal === -1 ? argument : argument.slice(0, equal);
|
|
38
|
+
if (!option.startsWith('--') || !allowed.includes(option.slice(2))) {
|
|
39
|
+
usage(`Unexpected argument for app-control ${action}: ${argument}`);
|
|
40
|
+
}
|
|
41
|
+
const value = equal === -1 ? argv[++index] : argument.slice(equal + 1);
|
|
42
|
+
if (value === undefined || value === '' || (equal === -1 && value.startsWith('--'))) {
|
|
43
|
+
usage(`${option} requires a value.`);
|
|
44
|
+
}
|
|
45
|
+
values[option.slice(2)] = value;
|
|
46
|
+
}
|
|
47
|
+
if (!values.endpoint) usage('App control requires --endpoint <pipe-or-socket>.');
|
|
48
|
+
if (action === 'capture' && !values.output) usage('App-control capture requires --output <png-file>.');
|
|
49
|
+
if (action === 'act' && !values.request) usage('App-control act requires --request <json-file>.');
|
|
50
|
+
return {action, ...values};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** This command owns its filename option before the general output-mode parser. */
|
|
54
|
+
export async function runAppControlCli(argv, {cwd = process.cwd(), stdout = process.stdout,
|
|
55
|
+
stderr = process.stderr, controller = new AbortController()} = {}) {
|
|
56
|
+
let connection;
|
|
57
|
+
let result;
|
|
58
|
+
const failures = [];
|
|
59
|
+
function failed(error) {
|
|
60
|
+
if (!failures.includes(error)) failures.push(error);
|
|
61
|
+
}
|
|
62
|
+
function interrupted() { controller.abort(new Error('Interrupted')); }
|
|
63
|
+
process.once('SIGINT', interrupted);
|
|
64
|
+
process.once('SIGTERM', interrupted);
|
|
65
|
+
try {
|
|
66
|
+
const selection = parseArguments(argv);
|
|
67
|
+
if (selection.help) {
|
|
68
|
+
stdout.write(HELP_TEXT);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
stderr.write(`${JSON.stringify({status: 'running', operation: selection.action,
|
|
72
|
+
endpoint: selection.endpoint})}\n`);
|
|
73
|
+
const signal = controller.signal;
|
|
74
|
+
const parameters = selection.action === 'act'
|
|
75
|
+
? JSON.parse(await readFile(path.resolve(cwd, selection.request), {encoding: 'utf8', signal}))
|
|
76
|
+
: selection.selector === undefined ? {} : {selector: selection.selector};
|
|
77
|
+
connection = await connectAppControl({endpoint: selection.endpoint, signal, onError: failed});
|
|
78
|
+
if (selection.action === 'status') result = await connection.status({signal});
|
|
79
|
+
else if (selection.action === 'inspect') result = await connection.inspect(parameters, {signal});
|
|
80
|
+
else if (selection.action === 'act') result = await connection.act(parameters, {signal});
|
|
81
|
+
else {
|
|
82
|
+
result = await connection.capture(parameters, {signal});
|
|
83
|
+
const output = path.resolve(cwd, selection.output);
|
|
84
|
+
await writeFile(output, Buffer.from(result.data, 'base64'), {signal});
|
|
85
|
+
const {data, ...metadata} = result;
|
|
86
|
+
result = {...metadata, output};
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
failed(error);
|
|
90
|
+
} finally {
|
|
91
|
+
if (connection) {
|
|
92
|
+
try { await connection.close(); } catch (error) { failed(error); }
|
|
93
|
+
}
|
|
94
|
+
process.removeListener('SIGINT', interrupted);
|
|
95
|
+
process.removeListener('SIGTERM', interrupted);
|
|
96
|
+
}
|
|
97
|
+
if (failures.length) {
|
|
98
|
+
const error = failures.length === 1 ? failures[0]
|
|
99
|
+
: new AggregateError(failures, 'App control completed with errors.');
|
|
100
|
+
stderr.write(`${JSON.stringify({...(result === undefined ? {} : {result}),
|
|
101
|
+
error: serializeCoreError(error)}, null, 2)}\n`);
|
|
102
|
+
return controller.signal.aborted ? 130 : error.exitCode ?? 1;
|
|
103
|
+
}
|
|
104
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
package/src/cli/main.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {ArcaneError,ERROR_CODES,normalizeError} from '../errors.mjs';
|
|
|
6
6
|
import {CLI_NAME,SDK_NAME,SDK_VERSION,OUTPUT_MODES} from '../constants.mjs';
|
|
7
7
|
import {loadArcaneNativeProvider} from '../native-provider-loader.mjs';
|
|
8
8
|
import {APP_BUNDLE_EXTENSION} from '../release-bundle.mjs';
|
|
9
|
+
import {runAppControlCli} from './app-control.mjs';
|
|
9
10
|
|
|
10
11
|
const is = new Is(false);
|
|
11
12
|
|
|
@@ -82,6 +83,7 @@ Usage:
|
|
|
82
83
|
${CLI_NAME} run [--target <target>] [--app <id>] [--cert <pem>] [--key <pem>] [--port 8000] [--http-port 0] [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
|
|
83
84
|
${CLI_NAME} update-check
|
|
84
85
|
${CLI_NAME} targets
|
|
86
|
+
${CLI_NAME} app-control status|inspect|capture|act --endpoint <pipe-or-socket> [--help]
|
|
85
87
|
${CLI_NAME} repo status|pull|push
|
|
86
88
|
${CLI_NAME} mail key set [profile] [--secret-stdin]
|
|
87
89
|
${CLI_NAME} mail key status [profile]
|
|
@@ -942,6 +944,9 @@ export async function runCli(argv=process.argv.slice(2),{
|
|
|
942
944
|
loadNativeProvider=loadArcaneNativeProvider,
|
|
943
945
|
controller=new AbortController()
|
|
944
946
|
}={}){
|
|
947
|
+
if(argv[0]==='app-control'){
|
|
948
|
+
return runAppControlCli(argv.slice(1),{cwd,stdout,stderr,controller});
|
|
949
|
+
}
|
|
945
950
|
let reporter;
|
|
946
951
|
let command='help';
|
|
947
952
|
const onSignal=()=>controller.abort(new Error('Interrupted'));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {connectCoreSocket, createCoreSocketConnection} from './socket-connection.mjs';
|
|
2
|
+
|
|
3
|
+
function reportAppControlError(error) { console.error('Arcane app control failed:', error); }
|
|
4
|
+
|
|
5
|
+
/** Connect to one explicitly selected running native window without launching it. */
|
|
6
|
+
export async function connectAppControl({endpoint, signal, onError = reportAppControlError} = {}) {
|
|
7
|
+
if (typeof endpoint !== 'string' || !endpoint) {
|
|
8
|
+
throw new TypeError('App control requires an explicit local pipe/socket endpoint.');
|
|
9
|
+
}
|
|
10
|
+
const socket = await connectCoreSocket(endpoint, signal, onError);
|
|
11
|
+
const connection = createCoreSocketConnection({socket, endpoint, signal, onError,
|
|
12
|
+
name: 'app-control', closedMessage: 'The app-control connection closed.'});
|
|
13
|
+
const {client} = connection;
|
|
14
|
+
return {
|
|
15
|
+
endpoint,
|
|
16
|
+
closed: connection.closed,
|
|
17
|
+
status(options) { return client.invoke('app.control.status', {}, options); },
|
|
18
|
+
inspect(parameters = {}, options) { return client.invoke('app.control.inspect', parameters, options); },
|
|
19
|
+
capture(parameters = {}, options) { return client.invoke('app.control.capture', parameters, options); },
|
|
20
|
+
act(parameters, options) { return client.invoke('app.control.act', parameters, options); },
|
|
21
|
+
close() { return connection.close(); }
|
|
22
|
+
};
|
|
23
|
+
}
|