arcane-os 0.62.0 → 0.63.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 +18 -0
- package/README.md +1 -1
- package/docs/reference/README.md +5 -0
- package/docs/reference/cli.md +5 -1
- package/docs/reference/core-native-packaging.md +22 -2
- package/docs/reference/inventory/package-api.json +106 -8
- package/docs/reference/local-ai.md +48 -7
- package/docs/reference/local-image-flux.md +154 -0
- package/docs/reference/local-image-generation.md +6 -0
- package/docs/reference/native-speech.md +114 -0
- package/docs/reference/sdk-api.md +173 -4
- package/package.json +2 -1
- package/src/core/services/speech.mjs +360 -0
- package/src/local-ai/config.mjs +4 -2
- package/src/local-ai/image-models/flux.mjs +31 -0
- package/src/native/windows-provider.mjs +39 -15
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {createArcaneEventSource} from '../../event-manager.mjs';
|
|
3
|
+
import {CoreError, serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
|
|
4
|
+
|
|
5
|
+
const is = new Is(false);
|
|
6
|
+
|
|
7
|
+
function failure(code, message) {
|
|
8
|
+
return new CoreError(
|
|
9
|
+
{code, message}
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function waitForLoad(task, signal) {
|
|
14
|
+
signal.throwIfAborted();
|
|
15
|
+
return new Promise(
|
|
16
|
+
function awaitSpeechLoad(resolve, reject) {
|
|
17
|
+
function cancelled() {
|
|
18
|
+
signal.removeEventListener('abort', cancelled);
|
|
19
|
+
reject(signal.reason);
|
|
20
|
+
}
|
|
21
|
+
signal.addEventListener(
|
|
22
|
+
'abort',
|
|
23
|
+
cancelled,
|
|
24
|
+
{once: true}
|
|
25
|
+
);
|
|
26
|
+
task.then(
|
|
27
|
+
function loaded(value) {
|
|
28
|
+
signal.removeEventListener('abort', cancelled);
|
|
29
|
+
if (signal.aborted) {
|
|
30
|
+
reject(signal.reason);
|
|
31
|
+
} else {
|
|
32
|
+
resolve(value);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
function loadFailed(error) {
|
|
36
|
+
signal.removeEventListener('abort', cancelled);
|
|
37
|
+
reject(error);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Hosts supply selected engines; this service owns their independent lifetimes. */
|
|
45
|
+
export function createSpeechService({stt, tts, signal} = {}) {
|
|
46
|
+
const roles = {
|
|
47
|
+
stt: {engine: stt, operation: 'transcribe', state: null, loading: null, unsubscribe: null},
|
|
48
|
+
tts: {engine: tts, operation: 'synthesize', state: null, loading: null, unsubscribe: null}
|
|
49
|
+
};
|
|
50
|
+
for (const [name, role] of Object.entries(roles)) {
|
|
51
|
+
if (role.engine == null) {
|
|
52
|
+
role.state = {providerId: '', modelId: null, state: 'unavailable', loaded: false, busy: false};
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const method of ['current', 'subscribe', 'load', 'close', role.operation]) {
|
|
56
|
+
if (!is.function(role.engine[method])) {
|
|
57
|
+
throw new TypeError(`The ${name} speech engine requires ${method}().`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
role.state = role.engine.current();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lifetime = new AbortController();
|
|
64
|
+
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
65
|
+
const owner = {};
|
|
66
|
+
const events = createArcaneEventSource(
|
|
67
|
+
owner,
|
|
68
|
+
{source: 'core-speech', eventTypes: ['speech.state']}
|
|
69
|
+
);
|
|
70
|
+
const jobs = new Set();
|
|
71
|
+
const publications = [];
|
|
72
|
+
let context;
|
|
73
|
+
let started = false;
|
|
74
|
+
let publishing = false;
|
|
75
|
+
let closing = null;
|
|
76
|
+
let closed = false;
|
|
77
|
+
|
|
78
|
+
function available(role) {
|
|
79
|
+
return started && !closing && !closed && role.state.loaded === true
|
|
80
|
+
&& (role.state.state === 'ready' || role.state.state === 'running');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function current() {
|
|
84
|
+
const transcriptionAvailable = available(roles.stt);
|
|
85
|
+
const synthesisAvailable = available(roles.tts);
|
|
86
|
+
return {
|
|
87
|
+
ready: transcriptionAvailable && synthesisAvailable,
|
|
88
|
+
synthesisAvailable,
|
|
89
|
+
transcriptionAvailable,
|
|
90
|
+
status: closed ? 'closed' : closing ? 'closing' : started ? 'ok' : 'created',
|
|
91
|
+
ttsEngine: roles.tts.state.providerId ?? '',
|
|
92
|
+
sttEngine: roles.stt.state.providerId ?? '',
|
|
93
|
+
roles: {
|
|
94
|
+
stt: {...roles.stt.state, available: transcriptionAvailable},
|
|
95
|
+
tts: {...roles.tts.state, available: synthesisAvailable}
|
|
96
|
+
},
|
|
97
|
+
closed
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function publish() {
|
|
102
|
+
publications.push(current());
|
|
103
|
+
if (publishing) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
publishing = true;
|
|
107
|
+
try {
|
|
108
|
+
// A listener may close the service. Finish this snapshot before
|
|
109
|
+
// publishing the lifecycle change that listener caused.
|
|
110
|
+
while (publications.length) {
|
|
111
|
+
const snapshot = publications.shift();
|
|
112
|
+
events.dispatch('speech.state', snapshot);
|
|
113
|
+
context?.emit('speech.state', snapshot);
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
publishing = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function roleFailed(name, error) {
|
|
121
|
+
const role = roles[name];
|
|
122
|
+
role.state = {...role.state, state: 'error', error: serializeCoreError(error)};
|
|
123
|
+
console.error(`The ${name} speech engine failed.`, error);
|
|
124
|
+
publish();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function startRole(name) {
|
|
128
|
+
const role = roles[name];
|
|
129
|
+
if (!role.engine || closing) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
role.unsubscribe = role.engine.subscribe(
|
|
134
|
+
function speechEngineChanged(state) {
|
|
135
|
+
if (!closed) {
|
|
136
|
+
role.state = state;
|
|
137
|
+
publish();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
roleFailed(name, error);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const task = Promise.resolve().then(
|
|
146
|
+
async function loadSpeechEngine() {
|
|
147
|
+
lifetimeSignal.throwIfAborted();
|
|
148
|
+
await role.engine.load(
|
|
149
|
+
{signal: lifetimeSignal}
|
|
150
|
+
);
|
|
151
|
+
if (!closed) {
|
|
152
|
+
role.state = role.engine.current();
|
|
153
|
+
publish();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
role.loading = task;
|
|
158
|
+
task.then(
|
|
159
|
+
function speechEngineLoaded() {
|
|
160
|
+
role.loading = null;
|
|
161
|
+
},
|
|
162
|
+
function speechEngineLoadFailed(error) {
|
|
163
|
+
role.loading = null;
|
|
164
|
+
if (!closing) {
|
|
165
|
+
roleFailed(name, error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
).catch(
|
|
169
|
+
function speechLoadObserverFailed(error) {
|
|
170
|
+
console.error('Speech load state observer failed.', error);
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function invoke(name, parameters, request) {
|
|
176
|
+
if (closing || closed) {
|
|
177
|
+
throw failure('CORE_CLOSING', 'The speech service is closing.');
|
|
178
|
+
}
|
|
179
|
+
const role = roles[name];
|
|
180
|
+
const operationSignal = request.signal
|
|
181
|
+
? AbortSignal.any([lifetimeSignal, request.signal]) : lifetimeSignal;
|
|
182
|
+
operationSignal.throwIfAborted();
|
|
183
|
+
if (!role.engine) {
|
|
184
|
+
throw failure('SPEECH_ENGINE_UNAVAILABLE', `No ${name} speech engine is configured.`);
|
|
185
|
+
}
|
|
186
|
+
request.emit?.(
|
|
187
|
+
'speech.progress',
|
|
188
|
+
{requestId: request.requestId, role: name, status: 'Thinking', progress: {phase: 'accepted'}}
|
|
189
|
+
);
|
|
190
|
+
const task = (async function invokeSpeechEngine() {
|
|
191
|
+
if (role.loading) {
|
|
192
|
+
await waitForLoad(role.loading, operationSignal);
|
|
193
|
+
}
|
|
194
|
+
operationSignal.throwIfAborted();
|
|
195
|
+
const result = await role.engine[role.operation](
|
|
196
|
+
parameters,
|
|
197
|
+
{
|
|
198
|
+
signal: operationSignal,
|
|
199
|
+
onProgress(progress) {
|
|
200
|
+
if (!operationSignal.aborted) {
|
|
201
|
+
request.emit?.(
|
|
202
|
+
'speech.progress',
|
|
203
|
+
{requestId: request.requestId, role: name, status: 'Thinking', progress}
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
operationSignal.throwIfAborted();
|
|
210
|
+
return result;
|
|
211
|
+
})();
|
|
212
|
+
jobs.add(task);
|
|
213
|
+
function speechRequestSettled() {
|
|
214
|
+
jobs.delete(task);
|
|
215
|
+
}
|
|
216
|
+
task.then(speechRequestSettled, speechRequestSettled);
|
|
217
|
+
return task;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function notifyListener(listener, state) {
|
|
221
|
+
const result = listener(state);
|
|
222
|
+
if (result && is.function(result.then)) {
|
|
223
|
+
Promise.resolve(result).catch(
|
|
224
|
+
function speechListenerFailed(error) {
|
|
225
|
+
console.error('Speech state listener failed.', error);
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function subscribe(listener, {replay = true, signal: subscriptionSignal} = {}) {
|
|
232
|
+
if (!is.function(listener)) {
|
|
233
|
+
throw new TypeError('A speech state listener must be a function.');
|
|
234
|
+
}
|
|
235
|
+
if (closed) {
|
|
236
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
237
|
+
notifyListener(listener, current());
|
|
238
|
+
}
|
|
239
|
+
return function closedSubscription() {};
|
|
240
|
+
}
|
|
241
|
+
const unsubscribe = events.on(
|
|
242
|
+
'speech.state',
|
|
243
|
+
function speechStateChanged(event) {
|
|
244
|
+
notifyListener(listener, event.detail);
|
|
245
|
+
},
|
|
246
|
+
{signal: subscriptionSignal}
|
|
247
|
+
);
|
|
248
|
+
try {
|
|
249
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
250
|
+
notifyListener(listener, current());
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
unsubscribe();
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
return unsubscribe;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function close() {
|
|
260
|
+
if (closing) {
|
|
261
|
+
return closing;
|
|
262
|
+
}
|
|
263
|
+
closing = Promise.resolve().then(
|
|
264
|
+
async function closeSpeechService() {
|
|
265
|
+
const shutdowns = Object.entries(roles).map(
|
|
266
|
+
async function closeSpeechEngine([name, role]) {
|
|
267
|
+
if (!role.engine) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await role.engine.close();
|
|
272
|
+
role.state = role.engine.current();
|
|
273
|
+
} catch (error) {
|
|
274
|
+
roleFailed(name, error);
|
|
275
|
+
throw error;
|
|
276
|
+
} finally {
|
|
277
|
+
role.unsubscribe?.();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
const loading = Object.values(roles).map(
|
|
282
|
+
function speechLoadTask(role) {
|
|
283
|
+
return role.loading;
|
|
284
|
+
}
|
|
285
|
+
).filter(Boolean);
|
|
286
|
+
const results = await Promise.allSettled([...shutdowns, ...loading, ...jobs]);
|
|
287
|
+
lifetimeSignal.removeEventListener('abort', abortService);
|
|
288
|
+
closed = true;
|
|
289
|
+
publish();
|
|
290
|
+
const failures = [];
|
|
291
|
+
for (let index = 0; index < shutdowns.length; index += 1) {
|
|
292
|
+
if (results[index].status === 'rejected') {
|
|
293
|
+
failures.push(results[index].reason);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (failures.length) {
|
|
297
|
+
throw new AggregateError(failures, 'Speech service shutdown failed.');
|
|
298
|
+
}
|
|
299
|
+
return current();
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
lifetime.abort();
|
|
303
|
+
publish();
|
|
304
|
+
return closing;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function abortService() {
|
|
308
|
+
close().catch(
|
|
309
|
+
function speechShutdownFailed(error) {
|
|
310
|
+
console.error('Speech service shutdown failed.', error);
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
lifetimeSignal.addEventListener(
|
|
316
|
+
'abort',
|
|
317
|
+
abortService,
|
|
318
|
+
{once: true}
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
name: 'speech',
|
|
323
|
+
current,
|
|
324
|
+
subscribe,
|
|
325
|
+
close,
|
|
326
|
+
start(currentContext) {
|
|
327
|
+
if (started || closing) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
context = currentContext;
|
|
331
|
+
if (lifetimeSignal.aborted) {
|
|
332
|
+
abortService();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
started = true;
|
|
336
|
+
startRole('stt');
|
|
337
|
+
startRole('tts');
|
|
338
|
+
publish();
|
|
339
|
+
},
|
|
340
|
+
methods: {
|
|
341
|
+
'speech.status': current,
|
|
342
|
+
'speech.transcribe': function transcribe(parameters, request = {}) {
|
|
343
|
+
return invoke('stt', parameters, request);
|
|
344
|
+
},
|
|
345
|
+
'speech.synthesize': function synthesize(parameters, request = {}) {
|
|
346
|
+
return invoke('tts', parameters, request);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
drain: close,
|
|
350
|
+
async dispose() {
|
|
351
|
+
try {
|
|
352
|
+
await close();
|
|
353
|
+
} finally {
|
|
354
|
+
events.dispose();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export default createSpeechService;
|
package/src/local-ai/config.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import Is from 'strong-type';
|
|
4
4
|
import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
|
|
5
5
|
import {SD14_MODEL} from './image-models/sd14.mjs';
|
|
6
|
+
import {FLUX2_KLEIN_4B_MODEL} from './image-models/flux.mjs';
|
|
6
7
|
|
|
7
8
|
const is = new Is(false);
|
|
8
9
|
export const LOCAL_AI_RUNTIME_IDS = ['llama.cpp', 'ollama', 'onnx', 'stable-diffusion.cpp'];
|
|
@@ -18,9 +19,10 @@ export function normalizeImageRuntimeRequirement(record) {
|
|
|
18
19
|
}
|
|
19
20
|
const models = (record.models ?? []).map(
|
|
20
21
|
function selectedImageModel(value) {
|
|
21
|
-
const model = value === 'sd14' ? SD14_MODEL
|
|
22
|
+
const model = value === 'sd14' ? SD14_MODEL
|
|
23
|
+
: value === 'flux2-klein-4b' ? FLUX2_KLEIN_4B_MODEL : value;
|
|
22
24
|
if (!model || !is.object(model) || is.array(model) || !is.string(model.id) || !model.id) {
|
|
23
|
-
throw new ArcaneError(ERROR_CODES.usage, 'An image model needs a descriptor with an id, or
|
|
25
|
+
throw new ArcaneError(ERROR_CODES.usage, 'An image model needs a descriptor with an id, or a built-in model ID (sd14 or flux2-klein-4b).');
|
|
24
26
|
}
|
|
25
27
|
if (!model.resources || !is.object(model.resources) || is.array(model.resources)) {
|
|
26
28
|
throw new ArcaneError(ERROR_CODES.usage, `Image model ${model.id} needs resources keyed by model role.`);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Distilled Klein model resources; acquisition belongs to the model asset owner. */
|
|
2
|
+
export const FLUX2_KLEIN_4B_MODEL = {
|
|
3
|
+
id: 'flux2-klein-4b',
|
|
4
|
+
name: 'FLUX.2 Klein 4B',
|
|
5
|
+
family: 'flux',
|
|
6
|
+
resources: {
|
|
7
|
+
diffusion_model: {
|
|
8
|
+
url: 'https://huggingface.co/black-forest-labs/FLUX.2-klein-4B/resolve/e7b7dc27f91deacad38e78976d1f2b499d76a294/flux-2-klein-4b.safetensors',
|
|
9
|
+
filename: 'flux-2-klein-4b.safetensors'
|
|
10
|
+
},
|
|
11
|
+
vae: {
|
|
12
|
+
url: 'https://huggingface.co/black-forest-labs/FLUX.2-small-decoder/resolve/a3efc24f613ef42d9428af62fdbd6f5fd8856c4a/full_encoder_small_decoder.safetensors',
|
|
13
|
+
filename: 'full_encoder_small_decoder.safetensors'
|
|
14
|
+
},
|
|
15
|
+
llm: {
|
|
16
|
+
url: 'https://huggingface.co/Qwen/Qwen3-4B-GGUF/resolve/bc640142c66e1fdd12af0bd68f40445458f3869b/Qwen3-4B-Q8_0.gguf',
|
|
17
|
+
filename: 'Qwen3-4B-Q8_0.gguf'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
context: {},
|
|
21
|
+
defaults: {
|
|
22
|
+
sample_params: {
|
|
23
|
+
sample_steps: 4,
|
|
24
|
+
sample_method: 'euler',
|
|
25
|
+
guidance: {
|
|
26
|
+
txt_cfg: 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
operations: ['txt2img']
|
|
31
|
+
};
|
|
@@ -41,13 +41,30 @@ function portableRequest(target) {
|
|
|
41
41
|
return {...target, target: 'portable', format: 'portable'};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function applicationExecutable(application) {
|
|
45
|
+
const name = is.string(application.displayName) && application.displayName
|
|
46
|
+
? application.displayName : application.id;
|
|
47
|
+
// Derive a Windows filename without changing the application's name.
|
|
48
|
+
// These substitutions belong only to Windows' reserved filename syntax.
|
|
49
|
+
const filename = `${name}.exe`.replace(/[<>:"/\\|?*\u0000-\u001f]/gu, '_');
|
|
50
|
+
return /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/iu.test(filename)
|
|
51
|
+
? `_${filename}` : filename;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hostAssetPath(relative, executable) {
|
|
55
|
+
if (relative === 'Arcane.exe') return executable;
|
|
56
|
+
if (relative === 'Arcane.exe.config') return `${executable}.config`;
|
|
57
|
+
return relative;
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
async function emit(onEvent, event) {
|
|
45
61
|
if (is.function(onEvent)) await onEvent(event);
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
async function hostFilesMissing(directory) {
|
|
64
|
+
async function hostFilesMissing(directory, executable = 'Arcane.exe') {
|
|
49
65
|
const missing = [];
|
|
50
|
-
for (const
|
|
66
|
+
for (const source of HOST_FILES) {
|
|
67
|
+
const relative = hostAssetPath(source, executable);
|
|
51
68
|
try {
|
|
52
69
|
if (!(await stat(path.join(directory, relative))).isFile()) missing.push(relative);
|
|
53
70
|
} catch (error) {
|
|
@@ -58,9 +75,9 @@ async function hostFilesMissing(directory) {
|
|
|
58
75
|
return missing;
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
async function requireHostDirectory(directory) {
|
|
78
|
+
async function requireHostDirectory(directory, executable = 'Arcane.exe') {
|
|
62
79
|
const resolved = await realpath(directory);
|
|
63
|
-
const missing = await hostFilesMissing(resolved);
|
|
80
|
+
const missing = await hostFilesMissing(resolved, executable);
|
|
64
81
|
if (missing.length) {
|
|
65
82
|
throw new ArcaneError(ERROR_CODES.prerequisiteMissing, 'The selected Windows host assets are incomplete.', {
|
|
66
83
|
details: {hostDirectory: resolved, missing}
|
|
@@ -126,7 +143,7 @@ function contains(root, candidate) {
|
|
|
126
143
|
return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
|
|
127
144
|
}
|
|
128
145
|
|
|
129
|
-
async function copyHostAssets(source, root, files, signal, relative = '', ancestors = new Set()) {
|
|
146
|
+
async function copyHostAssets(source, root, files, signal, executable, relative = '', ancestors = new Set()) {
|
|
130
147
|
throwIfAborted(signal);
|
|
131
148
|
const canonical = await realpath(source);
|
|
132
149
|
const output = await realpath(root);
|
|
@@ -139,11 +156,12 @@ async function copyHostAssets(source, root, files, signal, relative = '', ancest
|
|
|
139
156
|
for (const entry of await readdir(source, {withFileTypes: true})) {
|
|
140
157
|
throwIfAborted(signal);
|
|
141
158
|
const selected = path.join(source, entry.name);
|
|
142
|
-
const
|
|
159
|
+
const sourceRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
160
|
+
const destination = hostAssetPath(sourceRelative, executable);
|
|
143
161
|
const information = await stat(selected);
|
|
144
162
|
if (information.isDirectory()) {
|
|
145
163
|
await mkdir(path.join(root, destination), {recursive: true});
|
|
146
|
-
await copyHostAssets(selected, root, files, signal,
|
|
164
|
+
await copyHostAssets(selected, root, files, signal, executable, sourceRelative, parents);
|
|
147
165
|
} else if (information.isFile()) {
|
|
148
166
|
await writeOutput(root, destination, await readFile(selected, {signal}), files, signal);
|
|
149
167
|
} else {
|
|
@@ -152,7 +170,7 @@ async function copyHostAssets(source, root, files, signal, relative = '', ancest
|
|
|
152
170
|
}
|
|
153
171
|
}
|
|
154
172
|
|
|
155
|
-
async function applyApplicationIcon(request, root, files) {
|
|
173
|
+
async function applyApplicationIcon(request, root, files, executableName) {
|
|
156
174
|
const {appDescriptor, release, signal, onEvent} = request;
|
|
157
175
|
const icon = appDescriptor.native?.icon;
|
|
158
176
|
if (!icon) return {};
|
|
@@ -168,7 +186,7 @@ async function applyApplicationIcon(request, root, files) {
|
|
|
168
186
|
{signal}
|
|
169
187
|
);
|
|
170
188
|
throwIfAborted(signal);
|
|
171
|
-
const executable = path.join(root,
|
|
189
|
+
const executable = path.join(root, executableName);
|
|
172
190
|
let branded;
|
|
173
191
|
try {
|
|
174
192
|
const launcher = await readFile(
|
|
@@ -268,14 +286,15 @@ export function createWindowsNativeProvider({services, hostDirectory} = {}) {
|
|
|
268
286
|
try {
|
|
269
287
|
const host = selectedHost ?? await releasedHostDirectory(path.resolve(request.outputRoot), artifact.manifest.sdk.version, {signal, onEvent});
|
|
270
288
|
const files = [...artifact.manifest.files];
|
|
271
|
-
|
|
272
|
-
|
|
289
|
+
const executable = applicationExecutable(request.appDescriptor);
|
|
290
|
+
await copyHostAssets(host, root, files, signal, executable);
|
|
291
|
+
const branding = await applyApplicationIcon(request, root, files, executable);
|
|
273
292
|
const manifest = {
|
|
274
293
|
...artifact.manifest,
|
|
275
294
|
kind: 'arcane-windows-native',
|
|
276
295
|
target,
|
|
277
296
|
host: {
|
|
278
|
-
executable
|
|
297
|
+
executable,
|
|
279
298
|
platform: 'windows', architecture: 'x64', runtime: 'webview2',
|
|
280
299
|
coreExecutable: 'runtime/ArcaneCore.exe',
|
|
281
300
|
coreLoader: 'runtime/arcane-core-loader.cjs',
|
|
@@ -296,7 +315,7 @@ export function createWindowsNativeProvider({services, hostDirectory} = {}) {
|
|
|
296
315
|
throwIfAborted(signal);
|
|
297
316
|
const target = windowsTarget(targetRequest);
|
|
298
317
|
const result = await portable.verify({artifact, targetRequest: portableRequest(target), signal});
|
|
299
|
-
await requireHostDirectory(result.outputRoot);
|
|
318
|
+
await requireHostDirectory(result.outputRoot, result.manifest.host.executable);
|
|
300
319
|
const verified = {...result, target: 'windows-x64', executable: true};
|
|
301
320
|
await emit(onEvent, {type: 'native.verified', target: 'windows-x64', outputRoot: result.outputRoot});
|
|
302
321
|
throwIfAborted(signal);
|
|
@@ -308,8 +327,13 @@ export function createWindowsNativeProvider({services, hostDirectory} = {}) {
|
|
|
308
327
|
if (process.platform !== 'win32' || process.arch !== 'x64') {
|
|
309
328
|
throw new ArcaneError(ERROR_CODES.nativeRunUnsupported, 'Running this Windows x64 artifact requires a Windows x64 host.');
|
|
310
329
|
}
|
|
311
|
-
const
|
|
312
|
-
|
|
330
|
+
const directory = artifact?.target?.rootDir;
|
|
331
|
+
const manifest = JSON.parse(
|
|
332
|
+
await readFile(path.join(directory, MANIFEST), 'utf8')
|
|
333
|
+
);
|
|
334
|
+
const executable = manifest.host.executable;
|
|
335
|
+
const root = await requireHostDirectory(directory, executable);
|
|
336
|
+
return runProcess(path.join(root, executable), ['--close-on-stdin-eof'], {
|
|
313
337
|
cwd: root, signal, onEvent, cancellationMode: 'close-input'
|
|
314
338
|
});
|
|
315
339
|
}
|