arcane-os 0.56.0 → 0.57.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 +22 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-local.mjs +306 -0
- package/docs/reference/README.md +20 -0
- package/docs/reference/cli.md +9 -1
- package/docs/reference/core-native-packaging.md +195 -11
- package/docs/reference/inventory/package-api.json +138 -12
- package/docs/reference/local-ai.md +197 -0
- package/docs/reference/protocols.md +69 -1
- package/docs/reference/runtime-modules.md +6 -5
- package/docs/reference/sdk-api.md +165 -26
- package/package.json +5 -1
- package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
- package/schemas/arcane-app.schema.json +54 -0
- package/schemas/arcane-package.schema.json +44 -0
- package/src/app-descriptor.mjs +17 -1
- package/src/cli/main.mjs +8 -2
- package/src/core/host.mjs +23 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +3 -3
- package/src/core/hosts/windows/ArcaneHost.cs +695 -0
- package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
- package/src/core/services/local-ai.mjs +543 -0
- package/src/dev-server.mjs +7 -0
- package/src/import-map.mjs +1 -0
- package/src/local-ai/archive.mjs +52 -0
- package/src/local-ai/config.mjs +62 -0
- package/src/local-ai/dev.mjs +390 -0
- package/src/local-ai/discover.mjs +71 -0
- package/src/local-ai/http.mjs +106 -0
- package/src/local-ai/install.mjs +344 -0
- package/src/local-ai/server.mjs +262 -0
- package/src/native/portable-layout.mjs +29 -4
- package/src/native/portable-provider.mjs +39 -8
- package/src/native/windows-provider.mjs +262 -0
- package/src/native-provider-loader.mjs +8 -2
- package/src/packager/core.mjs +6 -2
- package/src/process.mjs +66 -8
- package/src/toolchain.mjs +33 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.57.0
|
|
4
|
+
|
|
5
|
+
- Add local model chat through explicit llama.cpp/Ollama runtime requirements,
|
|
6
|
+
reusable installation and bundling, a retained Core local-AI service, and an
|
|
7
|
+
optional browser-to-Core provider. Preserve complete requests and streamed
|
|
8
|
+
responses, model readiness, cancellation and owned process shutdown. Apps own
|
|
9
|
+
model selection and defaults; preparation does not block ordinary page startup.
|
|
10
|
+
- Add authored Core service selection and launch context. Factories receive
|
|
11
|
+
authored options and the separate app-root/runtime context; explicit launch
|
|
12
|
+
context overrides remain supported. Portable packages can bundle explicitly
|
|
13
|
+
selected local-AI runtimes without changing browser-only output.
|
|
14
|
+
- Add Windows x64 executable packaging through the public Windows native
|
|
15
|
+
provider and the matching release's retained WebView2/Node SEA host asset.
|
|
16
|
+
Client injection precedes app scripts, service requests retain complete JSON,
|
|
17
|
+
and accepted service work drains before the owned Core process exits.
|
|
18
|
+
- Add `runProcess` cancellation mode `close-input` for owned graceful shutdown;
|
|
19
|
+
the existing `terminate-tree` default remains unchanged. Includes separately
|
|
20
|
+
delivered macOS process source, without claiming a complete macOS host.
|
|
21
|
+
- Correct the documented Node document-library subpath and lexical-search
|
|
22
|
+
example. No preferences service, Nemotron diarization, native ONNX, native
|
|
23
|
+
Kokoro or local image generation is introduced in this release.
|
|
24
|
+
|
|
3
25
|
## 0.56.0
|
|
4
26
|
|
|
5
27
|
- Add explicit `package.nativeResources` selection for native-only application
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.57.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
import {getInstalledCoreClient} from '../core/client.mjs';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
const PROVIDER_PROTOCOL = 'arcane-ai-provider/2';
|
|
6
|
+
const AUTHORITY_PROTOCOL = 'arcane-ai-model-authority/1';
|
|
7
|
+
|
|
8
|
+
function localAIError(code, message, cause) {
|
|
9
|
+
const error = cause === undefined ? new Error(message) : new Error(message, {cause});
|
|
10
|
+
error.code = code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cancelled(reason) {
|
|
15
|
+
const error = localAIError('ARCANE_AI_REQUEST_ABORTED', 'The local AI request was cancelled.', reason);
|
|
16
|
+
error.name = 'AbortError';
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Browser-safe llama.cpp access through an existing Core connection. The app
|
|
22
|
+
* selects its model; Core owns the running engine and its loaded sessions.
|
|
23
|
+
* Creating this provider neither connects a transport nor installs a runtime.
|
|
24
|
+
*/
|
|
25
|
+
export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id = 'llama.cpp'} = {}) {
|
|
26
|
+
if (!is.string(id) || !id.trim()) throw new TypeError('The Core AI provider id must be a nonempty string.');
|
|
27
|
+
if (client !== null && (!is.function(client?.invoke) || !is.function(client?.events?.on))) {
|
|
28
|
+
throw new TypeError('The Core AI client must expose invoke and events.on.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let runtime = null;
|
|
32
|
+
let models = [];
|
|
33
|
+
let selection = null;
|
|
34
|
+
let disposed = false;
|
|
35
|
+
let loading = false;
|
|
36
|
+
let sequence = 0;
|
|
37
|
+
let lifecycleRevision = 0;
|
|
38
|
+
const requests = new Set();
|
|
39
|
+
|
|
40
|
+
function requireClient() {
|
|
41
|
+
if (disposed) throw localAIError('ARCANE_AI_PROVIDER_DISPOSED', 'The Core AI provider is disposed.');
|
|
42
|
+
if (!client) throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'Local llama.cpp requires an available Core connection.');
|
|
43
|
+
return client;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireSelection(value) {
|
|
47
|
+
if (value?.providerId !== id || !is.string(value?.modelId) || !value.modelId
|
|
48
|
+
|| value.localOnly !== true) {
|
|
49
|
+
throw localAIError('ARCANE_AI_MODEL_AUTHORITY_REQUIRED', 'The local provider requires an explicit matching provider and model.');
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadedModel() {
|
|
55
|
+
return selection && models.find(function selectedModel(model) {
|
|
56
|
+
return model.id === selection.modelId && model.loaded === true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function modelReady() {
|
|
61
|
+
return runtime?.available === true && runtime.state === 'ready' && Boolean(loadedModel());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function status() {
|
|
65
|
+
const loaded = !disposed && modelReady();
|
|
66
|
+
return {
|
|
67
|
+
state: disposed ? 'disposed' : loading ? 'loading' : loaded ? 'ready'
|
|
68
|
+
: runtime?.error ? 'error' : runtime?.available === true ? 'unloaded' : 'unavailable',
|
|
69
|
+
loaded,
|
|
70
|
+
busy: loading || requests.size > 0,
|
|
71
|
+
modelId: selection?.modelId ?? null,
|
|
72
|
+
available: !disposed && runtime?.available === true,
|
|
73
|
+
installed: runtime?.installed === true,
|
|
74
|
+
error: runtime?.error ?? null
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function acceptRuntime(value) {
|
|
79
|
+
runtime = value ?? null;
|
|
80
|
+
models = is.array(runtime?.models) ? runtime.models : [];
|
|
81
|
+
for (const request of requests) {
|
|
82
|
+
const model = models.find(function requestModel(record) { return record.id === request.modelId; });
|
|
83
|
+
if (model?.loaded === true) request.loaded = true;
|
|
84
|
+
if (runtime?.available !== true || (request.loaded && (runtime.state !== 'ready' || model?.loaded !== true))) {
|
|
85
|
+
request.controller.abort(localAIError('ARCANE_AI_MODEL_NOT_READY', 'The requested local model is no longer ready.', runtime?.error));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return status();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const unsubscribe = client?.events.on('localai.state', function localRuntimeChanged(snapshot) {
|
|
92
|
+
if (disposed || !is.array(snapshot?.runtimes)) return;
|
|
93
|
+
lifecycleRevision += 1;
|
|
94
|
+
acceptRuntime(snapshot.runtimes.find(function llamaRuntime(value) {
|
|
95
|
+
return value.id === 'llama.cpp';
|
|
96
|
+
}));
|
|
97
|
+
}) ?? function noCoreSubscription() {};
|
|
98
|
+
|
|
99
|
+
function authority(value) {
|
|
100
|
+
return {protocol: AUTHORITY_PROTOCOL, providerId: id, modelId: value.modelId};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function inspect(value, {signal} = {}) {
|
|
104
|
+
requireSelection(value);
|
|
105
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
106
|
+
try {
|
|
107
|
+
const core = requireClient();
|
|
108
|
+
const statusRevision = lifecycleRevision;
|
|
109
|
+
const current = await core.invoke('llama.status', {}, {signal});
|
|
110
|
+
if (lifecycleRevision === statusRevision) acceptRuntime(current);
|
|
111
|
+
if (runtime?.available !== true) {
|
|
112
|
+
return {
|
|
113
|
+
available: false,
|
|
114
|
+
code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
115
|
+
message: 'The local llama.cpp runtime is unavailable.',
|
|
116
|
+
error: runtime?.error ?? null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!models.some(function matchesModel(model) { return model.id === value.modelId; })) {
|
|
120
|
+
return {
|
|
121
|
+
available: false,
|
|
122
|
+
code: 'ARCANE_AI_MODEL_UNAVAILABLE',
|
|
123
|
+
message: 'The selected model is unavailable in the local llama.cpp runtime.'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {available: true, authority: authority(value)};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (signal?.aborted || error?.name === 'AbortError') throw error;
|
|
129
|
+
acceptRuntime({...runtime, available: false, state: 'error', error});
|
|
130
|
+
return {available: false, code: error.code ?? 'ARCANE_AI_CORE_UNAVAILABLE', message: error.message, error};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function load({selection: value, signal, progress} = {}) {
|
|
135
|
+
const core = requireClient();
|
|
136
|
+
requireSelection(value);
|
|
137
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
138
|
+
selection = value;
|
|
139
|
+
loading = true;
|
|
140
|
+
try {
|
|
141
|
+
progress?.({phase: 'loading', modelId: value.modelId});
|
|
142
|
+
const revision = lifecycleRevision;
|
|
143
|
+
const current = await core.invoke('llama.load', {model: value.modelId}, {signal});
|
|
144
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
145
|
+
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core did not report the selected model loaded and ready.', current?.error);
|
|
146
|
+
progress?.({phase: 'ready', modelId: value.modelId});
|
|
147
|
+
return {authority: authority(value), status: {...status(), state: 'ready', busy: false}};
|
|
148
|
+
} finally {
|
|
149
|
+
loading = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ownRequest(signal, modelId) {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
function forwardAbort() { controller.abort(signal.reason); }
|
|
156
|
+
signal?.addEventListener('abort', forwardAbort, {once: true});
|
|
157
|
+
if (signal?.aborted) forwardAbort();
|
|
158
|
+
const request = {controller, result: null, modelId, loaded: models.some(function loadedTarget(model) {
|
|
159
|
+
return model.id === modelId && model.loaded === true;
|
|
160
|
+
})};
|
|
161
|
+
requests.add(request);
|
|
162
|
+
request.release = function releaseRequest() {
|
|
163
|
+
signal?.removeEventListener('abort', forwardAbort);
|
|
164
|
+
requests.delete(request);
|
|
165
|
+
};
|
|
166
|
+
return request;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function streamRequest(core, parameters, signal) {
|
|
170
|
+
const request = ownRequest(signal, parameters.model);
|
|
171
|
+
const chunks = [];
|
|
172
|
+
const readers = [];
|
|
173
|
+
let complete = false;
|
|
174
|
+
let failure = null;
|
|
175
|
+
sequence += 1;
|
|
176
|
+
const streamId = is.function(core.uuid) ? core.uuid()
|
|
177
|
+
: globalThis.crypto?.randomUUID?.() ?? `${id}:${Date.now()}:${sequence}:${Math.random()}`;
|
|
178
|
+
|
|
179
|
+
function flush() {
|
|
180
|
+
while (readers.length && chunks.length && !request.controller.signal.aborted) {
|
|
181
|
+
readers.shift().resolve({value: chunks.shift(), done: false});
|
|
182
|
+
}
|
|
183
|
+
if (!complete || (chunks.length && !request.controller.signal.aborted)) return;
|
|
184
|
+
while (readers.length) {
|
|
185
|
+
const reader = readers.shift();
|
|
186
|
+
if (failure) reader.reject(failure);
|
|
187
|
+
else reader.resolve({value: undefined, done: true});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stopChunks = core.events.on('llama.chunk', function receiveChunk(event) {
|
|
192
|
+
if (event?.streamId !== streamId || complete || request.controller.signal.aborted) return;
|
|
193
|
+
chunks.push(event.chunk);
|
|
194
|
+
flush();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
request.result = Promise.resolve().then(function invokeStream() {
|
|
198
|
+
return core.invoke('llama.chat', {...parameters, stream: true, streamId}, {signal: request.controller.signal});
|
|
199
|
+
}).then(function streamCompleted(result) {
|
|
200
|
+
if (request.controller.signal.aborted) throw cancelled(request.controller.signal.reason);
|
|
201
|
+
return result;
|
|
202
|
+
}).catch(function streamFailed(error) {
|
|
203
|
+
failure = error;
|
|
204
|
+
throw error;
|
|
205
|
+
}).finally(function releaseStream() {
|
|
206
|
+
complete = true;
|
|
207
|
+
stopChunks();
|
|
208
|
+
request.release();
|
|
209
|
+
flush();
|
|
210
|
+
});
|
|
211
|
+
request.result.catch(function observeRetainedStreamFailure() {});
|
|
212
|
+
|
|
213
|
+
function cancel(reason) {
|
|
214
|
+
if (!complete) request.controller.abort(reason);
|
|
215
|
+
return request.result.then(function cancelledAfterSuccess() {}, function cancelledAfterFailure() {});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
result: request.result,
|
|
220
|
+
cancel,
|
|
221
|
+
[Symbol.asyncIterator]() {
|
|
222
|
+
return {
|
|
223
|
+
next() {
|
|
224
|
+
if (request.controller.signal.aborted) return Promise.reject(failure ?? cancelled(request.controller.signal.reason));
|
|
225
|
+
if (chunks.length) return Promise.resolve({value: chunks.shift(), done: false});
|
|
226
|
+
if (complete) return failure ? Promise.reject(failure) : Promise.resolve({value: undefined, done: true});
|
|
227
|
+
return new Promise(function awaitChunk(resolve, reject) { readers.push({resolve, reject}); });
|
|
228
|
+
},
|
|
229
|
+
async return() {
|
|
230
|
+
await cancel();
|
|
231
|
+
return {value: undefined, done: true};
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function request({selection: value, operation, payload, signal} = {}) {
|
|
239
|
+
const core = requireClient();
|
|
240
|
+
requireSelection(value);
|
|
241
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
242
|
+
if (loading || selection?.modelId !== value.modelId) {
|
|
243
|
+
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Load the selected local model before requesting inference.');
|
|
244
|
+
}
|
|
245
|
+
const model = payload?.model ?? value.modelId;
|
|
246
|
+
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
247
|
+
const revision = lifecycleRevision;
|
|
248
|
+
const current = await core.invoke('llama.status', {}, {signal});
|
|
249
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
250
|
+
}
|
|
251
|
+
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
252
|
+
throw localAIError('ARCANE_AI_MODEL_UNAVAILABLE', 'The requested model is unavailable in the local llama.cpp runtime.');
|
|
253
|
+
}
|
|
254
|
+
if (runtime?.available !== true || runtime.state !== 'ready'
|
|
255
|
+
|| (model === value.modelId && !modelReady())) {
|
|
256
|
+
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'The requested local runtime or selected model is not ready.');
|
|
257
|
+
}
|
|
258
|
+
const parameters = {model, payload};
|
|
259
|
+
if (operation === 'stream') return streamRequest(core, parameters, signal);
|
|
260
|
+
if (operation !== 'chat') throw localAIError('ARCANE_AI_PROVIDER_OPERATION_UNAVAILABLE', 'The local llama.cpp provider supports chat and stream.');
|
|
261
|
+
const pending = ownRequest(signal, model);
|
|
262
|
+
pending.result = Promise.resolve().then(function invokeChat() {
|
|
263
|
+
return core.invoke('llama.chat', {...parameters, stream: false}, {signal: pending.controller.signal});
|
|
264
|
+
}).then(function chatCompleted(result) {
|
|
265
|
+
if (pending.controller.signal.aborted) throw cancelled(pending.controller.signal.reason);
|
|
266
|
+
return result;
|
|
267
|
+
}).finally(pending.release);
|
|
268
|
+
return pending.result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function unload({selection: value = selection, signal} = {}) {
|
|
272
|
+
const core = requireClient();
|
|
273
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
274
|
+
for (const request of requests) request.controller.abort();
|
|
275
|
+
await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
|
|
276
|
+
if (!value) return status();
|
|
277
|
+
requireSelection(value);
|
|
278
|
+
const revision = lifecycleRevision;
|
|
279
|
+
const current = await core.invoke('llama.unload', {model: value.modelId}, {signal});
|
|
280
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
281
|
+
selection = null;
|
|
282
|
+
return status();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function dispose(options = {}) {
|
|
286
|
+
if (disposed) return status();
|
|
287
|
+
if (selection || requests.size) await unload(options);
|
|
288
|
+
disposed = true;
|
|
289
|
+
unsubscribe();
|
|
290
|
+
return status();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
protocol: PROVIDER_PROTOCOL,
|
|
295
|
+
role: 'llm',
|
|
296
|
+
id,
|
|
297
|
+
localOnly: true,
|
|
298
|
+
catalog: function catalog() { return [...models]; },
|
|
299
|
+
status,
|
|
300
|
+
inspect,
|
|
301
|
+
load,
|
|
302
|
+
request,
|
|
303
|
+
unload,
|
|
304
|
+
dispose
|
|
305
|
+
};
|
|
306
|
+
}
|
package/docs/reference/README.md
CHANGED
|
@@ -111,6 +111,26 @@ The [changelog](../../CHANGELOG.md) records changes by version; the
|
|
|
111
111
|
identify the corresponding published package source. A newer website does not
|
|
112
112
|
change the version installed in your application.
|
|
113
113
|
|
|
114
|
+
### Generate the selected reference site
|
|
115
|
+
|
|
116
|
+
The existing `node tools/build-reference-site.mjs --write` command reads the
|
|
117
|
+
live checkout. For release documentation while disjoint source work continues,
|
|
118
|
+
select the reviewed documentation commit explicitly:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
node tools/build-reference-site.mjs --write --source-ref <documentation-commit>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The same option is accepted by `createReferenceSite({sourceRef})` and
|
|
125
|
+
`writeReferenceSite({sourceRef})`. It reads the selected commit's package
|
|
126
|
+
version, complete reference documents and inventories, inventory-selected
|
|
127
|
+
runtime sources, and authored site inputs directly from Git in memory. It
|
|
128
|
+
creates no checkout or export and leaves source drafts untouched. Rendering
|
|
129
|
+
uses the current canonical generator and its authored semantic contracts;
|
|
130
|
+
select a documentation commit whose contracts match the published package.
|
|
131
|
+
Generated output still belongs to the canonical `site/` tree and requires its
|
|
132
|
+
usual coordinated output ownership. Generation does not publish hosted Pages.
|
|
133
|
+
|
|
114
134
|
### Historical 0.3.4 publication record
|
|
115
135
|
|
|
116
136
|
The following records describe that earlier release only. They do not identify
|
package/docs/reference/cli.md
CHANGED
|
@@ -49,6 +49,7 @@ meaning and cardinality rules:
|
|
|
49
49
|
| `--app` | app id or label | Workspace/app operations except shared scope and `verify-bundle`; optional diagnostic label for `mail serve`. |
|
|
50
50
|
| `--apps-root` | `.` or `apps` | `new`, `init`; standalone setup defaults to the repository root. |
|
|
51
51
|
| `--sdk-runtime-source` | SDK checkout directory | `dev` only; maps live SDK source through the selected browser destinations for that server process. |
|
|
52
|
+
| `--local-ai` | comma-separated runtime IDs | `dev` only; adds `llama.cpp` and/or `ollama` requirements to application configuration and prepares missing runtimes alongside serving. See [local AI through Core](local-ai.md). |
|
|
52
53
|
| `--arcane-root` | directory | `doctor`, native `build`/`run`, `native-doctor`, `native-prepare` |
|
|
53
54
|
| `--host` / `--port` | host / integer 0–65535 | Browser `dev`/`run` default to HTTPS at `127.0.0.1:8000`; `mail serve` defaults to HTTPS with HTTP/2 at `0.0.0.0:4433` and accepts an explicit bind host. |
|
|
54
55
|
| `--http-port` | integer 0–65535 | Browser `dev`/`run` HTTP redirect listener; defaults to `0`, which selects an available port. |
|
|
@@ -366,9 +367,16 @@ An explicit live-source SDK mapping follows the selected browser destinations
|
|
|
366
367
|
while reading canonical SDK source; it never replaces installed files.
|
|
367
368
|
|
|
368
369
|
```text
|
|
369
|
-
arcane dev [--app <id>] [--public] [--http | --https] [--cert <file> --key <file>] [--host <address>] [--port 8000] [--http-port 0] [--sdk-runtime-source <sdk-root>]
|
|
370
|
+
arcane dev [--app <id>] [--public] [--http | --https] [--cert <file> --key <file>] [--host <address>] [--port 8000] [--http-port 0] [--sdk-runtime-source <sdk-root>] [--local-ai llama.cpp,ollama]
|
|
370
371
|
```
|
|
371
372
|
|
|
373
|
+
`--local-ai` saves the selected requirements in `native.localAI`, or in
|
|
374
|
+
`arcane-package.json.localAI` for a package-only app. Later development runs
|
|
375
|
+
reuse them. Available services and executables are reused; missing runtimes
|
|
376
|
+
are installed inside the application while serving starts independently.
|
|
377
|
+
Applications choose their model files and optional browser Core connection.
|
|
378
|
+
See [local AI through Core](local-ai.md) for configuration and native bundling.
|
|
379
|
+
|
|
372
380
|
### Lifecycle
|
|
373
381
|
|
|
374
382
|
Startup refreshes the selected authored `arcane-app.json` projection into
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
# Portable Core packaging
|
|
2
2
|
|
|
3
3
|
`arcane-os/native/portable-provider` owns the portable native payload in the SDK
|
|
4
|
-
package. It does not load Arcane OS source, choose models
|
|
5
|
-
|
|
4
|
+
package. It does not load Arcane OS source, choose models or compile an
|
|
5
|
+
executable. An application's explicit `native.localAI` selection can include
|
|
6
|
+
its selected upstream runtimes through the SDK local-AI installer. Without that
|
|
7
|
+
selection, assembly does no local-AI runtime preparation. Windows, Linux and macOS hosts may compose this
|
|
6
8
|
Node-based payload; Android needs its separate native host adaptation. The
|
|
7
9
|
current CLI's portable platform selections remain Windows and Linux. A portable
|
|
8
10
|
artifact is not a Windows executable, Linux package or Android application.
|
|
9
11
|
|
|
10
12
|
`loadArcaneNativeProvider({target:'portable'})` selects the installed SDK provider
|
|
11
13
|
without an `arcaneRoot`. Supplying an explicit `arcaneRoot` retains the existing
|
|
12
|
-
checkout-provider route.
|
|
14
|
+
checkout-provider route. The SDK also owns the `windows-x64` executable provider
|
|
15
|
+
described below. Other native targets remain with their actual platform
|
|
13
16
|
providers; the loader does not substitute this payload for an unavailable host.
|
|
14
17
|
|
|
15
18
|
## Provider and selected input
|
|
16
19
|
|
|
17
20
|
The default export and `arcaneNativeBuilderProvider` are the same provider.
|
|
18
|
-
`createPortableNativeProvider({services
|
|
19
|
-
service configuration.
|
|
21
|
+
`createPortableNativeProvider({services})` creates one with application-owned
|
|
22
|
+
service configuration. Without that override, the provider uses the selected
|
|
23
|
+
app descriptor's `native.services`, or an empty selection when omitted. An
|
|
24
|
+
explicit `services:[]` overrides the descriptor with no app services.
|
|
25
|
+
All implement `arcane-native-builder/1`:
|
|
20
26
|
|
|
21
27
|
- `describe()` identifies the portable target and `executable:false`.
|
|
22
28
|
- `doctor({targetRequest, signal, onEvent})` reports missing SDK Core source files.
|
|
@@ -42,6 +48,45 @@ receipts, hashes, content limits or security policy. The package-relative source
|
|
|
42
48
|
root comes from the provider module's own location, not `toolchainRoot` or an
|
|
43
49
|
Arcane OS machine bundle.
|
|
44
50
|
|
|
51
|
+
## Native-only application resources
|
|
52
|
+
|
|
53
|
+
Select application service modules and their complete local source closure in
|
|
54
|
+
the authored schema-2 descriptor's `package.nativeResources`, separately from
|
|
55
|
+
its browser `include`/`exclude` selection:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"package": {
|
|
60
|
+
"nativeResources": {
|
|
61
|
+
"include": ["native", "src", "package.json"],
|
|
62
|
+
"exclude": ["native/drafts"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This is a descriptor excerpt for the app-owned service example below. The
|
|
69
|
+
projected schema-1 field is top-level `nativeResources`. Paths are literal and
|
|
70
|
+
app-relative; directories include descendants. Omission or an empty include
|
|
71
|
+
keeps previous selection and output behavior. The app supplies every required
|
|
72
|
+
source file explicitly; packaging does not discover or execute services.
|
|
73
|
+
|
|
74
|
+
Paired `buildApplication()` passes the actual native target to the selected app
|
|
75
|
+
and every explicitly bundled app. With a nonempty native include list, their
|
|
76
|
+
input releases use `dist/.native/<target>/<app-id>` instead of the saved browser
|
|
77
|
+
output, preserving existing browser/PWA artifacts. Each app's own selection is
|
|
78
|
+
applied; complete releases and actual roots are forwarded to the provider.
|
|
79
|
+
The provider then copies them unchanged into its artifact layout described below.
|
|
80
|
+
|
|
81
|
+
At the low-level `packageApp()` boundary, `target` defaults to `'browser'` and
|
|
82
|
+
is independent of `moduleFormat`. Nonbrowser selection disables generated PWA
|
|
83
|
+
content. Native-only files bypass browser document/module/asset transformations;
|
|
84
|
+
a file also in the ordinary browser selection retains that existing processing.
|
|
85
|
+
An explicit low-level `outputDirectory` still wins. Inspect the emitted input
|
|
86
|
+
with `verifyApp({workspaceRoot,appId,outputDirectory:release.output})`; neither
|
|
87
|
+
that inventory verification nor packaging launches the service or platform host.
|
|
88
|
+
See [the complete selection contract](protocols.md#native-only-application-resources).
|
|
89
|
+
|
|
45
90
|
## Output and host contract
|
|
46
91
|
|
|
47
92
|
Each build creates a fresh `arcane-portable-*` directory beneath `outputRoot`.
|
|
@@ -85,7 +130,9 @@ host must:
|
|
|
85
130
|
1. Mount `app/` at the root of its local application URL origin, preserving the
|
|
86
131
|
selected start path and all navigable documents.
|
|
87
132
|
2. Inject `runtime/arcane-api.js` at document creation. It is produced by the
|
|
88
|
-
canonical `createCoreClassicSource()` generator
|
|
133
|
+
canonical `createCoreClassicSource()` generator with `replayRuntimeState:true`.
|
|
134
|
+
Once its transport listener is attached, each document requests the current
|
|
135
|
+
Core and service state without starting services again.
|
|
89
136
|
3. Start `runtime/arcane-core.mjs` with the host's supported Node runtime and
|
|
90
137
|
connect the existing framed stdin/stdout transport.
|
|
91
138
|
4. Close input and await Core's drain before terminating the child process.
|
|
@@ -100,7 +147,21 @@ is not the native serving contract. No host is launched during packaging.
|
|
|
100
147
|
|
|
101
148
|
## Explicit application services
|
|
102
149
|
|
|
103
|
-
|
|
150
|
+
For the ordinary SDK build command, select native service modules in the
|
|
151
|
+
application's authored descriptor. Include their source closure through
|
|
152
|
+
`package.nativeResources` as described above:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"native": {
|
|
157
|
+
"services": [
|
|
158
|
+
{"module":"native/moon-ledger.mjs", "options":{"ledgerName":"Cheese debts"}}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Direct SDK compositions can instead pass that selection to the provider:
|
|
104
165
|
|
|
105
166
|
```js
|
|
106
167
|
import {createPortableNativeProvider} from 'arcane-os/native/portable-provider';
|
|
@@ -111,7 +172,8 @@ const nativeBuilder = createPortableNativeProvider({
|
|
|
111
172
|
```
|
|
112
173
|
|
|
113
174
|
Each module default-exports a synchronous factory returning the existing Core
|
|
114
|
-
service definition. Its
|
|
175
|
+
service definition. Its first argument is the complete authored options record;
|
|
176
|
+
its second is the shared launch context described below. Options must be JSON-serializable data. Module imports
|
|
115
177
|
and factories run only when the generated Core entry is launched, never during
|
|
116
178
|
packaging. Put asynchronous initialization in `start()`, not a top-level await
|
|
117
179
|
or asynchronous factory, so unrelated services and dispatcher readiness remain
|
|
@@ -142,8 +204,130 @@ observable through `onError` and `closed`; no process is killed or forcibly exit
|
|
|
142
204
|
See [the Core runtime contract](core-runtime.md) for request and service lifetime
|
|
143
205
|
details and [the client contract](core-client.md) for native bridge integration.
|
|
144
206
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
207
|
+
## Launch-time locations
|
|
208
|
+
|
|
209
|
+
The generated Core entry accepts `--arcane-launch-config <path>`. The public
|
|
210
|
+
`readCoreLaunchContext({argv=process.argv.slice(2)}={})` function in
|
|
211
|
+
`arcane-os/core/host` reads that explicitly selected JSON object. Without the
|
|
212
|
+
flag it returns `{}`. Missing files, missing flag values and malformed JSON
|
|
213
|
+
report their actual errors; the reader does not search for configuration,
|
|
214
|
+
rewrite it or change the process environment.
|
|
215
|
+
|
|
216
|
+
The generated entry supplies an artifact-derived `appRoot` and then applies the
|
|
217
|
+
complete explicit launch context. Every service factory receives that same
|
|
218
|
+
object as its second argument, preserving its authored first argument. A
|
|
219
|
+
launcher can supply the user's selected `workspaceRoot` and writable
|
|
220
|
+
`stateRoot` at launch instead of embedding this machine's paths in an app
|
|
221
|
+
package. The host owns those selections; the SDK does not choose a product's
|
|
222
|
+
workspace, namespace, saved preferences or models. JSON values and additional
|
|
223
|
+
application-owned fields remain unchanged.
|
|
224
|
+
|
|
225
|
+
## Selected local-AI runtimes
|
|
226
|
+
|
|
227
|
+
When `appDescriptor.native.localAI` is present, portable assembly calls the
|
|
228
|
+
SDK's `bundleLocalAIRuntimes()` owner with its `runtimes` selection and the
|
|
229
|
+
requested target platform/architecture. Its reusable installation directory is
|
|
230
|
+
`<outputRoot>/local-ai-runtimes`; each new payload receives the complete selected
|
|
231
|
+
runtime trees beneath `runtime/local-ai/`. Runtime preparation and app copying
|
|
232
|
+
proceed independently and both settle before assembly can clean up an incomplete
|
|
233
|
+
output. No application service or model executes during packaging.
|
|
234
|
+
|
|
235
|
+
`manifest.core.localAIRuntimes` records the bundled runtime locations relative
|
|
236
|
+
to the artifact root. Only the explicitly configured entry imports
|
|
237
|
+
`createLocalAIService` from `arcane-os/core/local-ai`. It resolves those bundled
|
|
238
|
+
locations against the installed artifact and supplies them in the launch
|
|
239
|
+
context's `runtimes` array, alongside `appRoot`, before applying explicit launch
|
|
240
|
+
configuration. It passes the complete authored `native.localAI` record to the
|
|
241
|
+
factory. Applications without that selection have no generated local-AI import
|
|
242
|
+
or service. Availability of a particular engine belongs to the selected
|
|
243
|
+
local-AI installer and service; a generic ONNX runtime is not a speech model.
|
|
244
|
+
|
|
245
|
+
## Windows executable
|
|
246
|
+
|
|
247
|
+
The ordinary application command is:
|
|
248
|
+
|
|
249
|
+
```sh
|
|
250
|
+
npm exec -- arcane build --target windows-x64
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`loadArcaneNativeProvider({target:'windows-x64'})` selects the SDK's own
|
|
254
|
+
`arcane-os/native/windows-provider` without an OS checkout or `arcaneRoot`.
|
|
255
|
+
Its default export and `arcaneNativeBuilderProvider` are the same provider;
|
|
256
|
+
`createWindowsNativeProvider({services,hostDirectory})` permits explicit
|
|
257
|
+
same-project composition. Service selection follows the portable provider's
|
|
258
|
+
descriptor/override contract above.
|
|
259
|
+
|
|
260
|
+
The provider assembles the selected app with the precompiled host asset
|
|
261
|
+
`arcane-native-windows-x64.tar.gz` from the numeric GitHub release matching the
|
|
262
|
+
builder's installed SDK version. The asset contains the launcher, WebView2
|
|
263
|
+
support libraries, Node SEA Core executable and required license files. It does
|
|
264
|
+
not select the newest host independently of that package. Downloads occur only
|
|
265
|
+
during `build()`, use the caller's output directory, and are reused from
|
|
266
|
+
`<outputRoot>/.arcane-native-hosts/<sdk-version>/windows-x64`. The build machine
|
|
267
|
+
needs the standard `tar` command for extraction; application builds do not need
|
|
268
|
+
a C# compiler, system Node installation for the generated executable, or host
|
|
269
|
+
source scripts. The SDK build command itself still runs through the application's
|
|
270
|
+
normal Node/npm toolchain.
|
|
271
|
+
|
|
272
|
+
An explicit `hostDirectory` selects an already-built complete host tree instead
|
|
273
|
+
of downloading one. This is useful for SDK collaborators verifying the next
|
|
274
|
+
host output; downstream applications use the published package and matching
|
|
275
|
+
release asset. `doctor()` reports the selected prerequisites and missing files;
|
|
276
|
+
it does not download the host or probe the installed WebView runtime.
|
|
277
|
+
|
|
278
|
+
The assembled directory contains `Arcane.exe`, the unchanged `app/` payload,
|
|
279
|
+
Core runtime and semantic `arcane-native.json`. Launch `Arcane.exe` on Windows
|
|
280
|
+
x64 with the Microsoft Edge WebView2 Runtime installed. It opens the selected
|
|
281
|
+
app in WebView2 and owns its framed connection to the bundled Core process.
|
|
282
|
+
`verify()` observes the assembled files and manifest; it does not execute the
|
|
283
|
+
window or prove application behavior. `run()` launches the executable and
|
|
284
|
+
observes complete diagnostics and exit. Cancelling that operation closes its
|
|
285
|
+
input, allowing the window and accepted Core service work to drain without a
|
|
286
|
+
forced process termination.
|
|
287
|
+
|
|
288
|
+
For explicit user-selected workspace/state locations, launch:
|
|
289
|
+
|
|
290
|
+
```sh
|
|
291
|
+
Arcane.exe --arcane-launch-config path/to/launch.json
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The complete JSON object reaches the service factories as their second
|
|
295
|
+
argument. A supplied `stateRoot` also owns the WebView2 profile and diagnostic
|
|
296
|
+
directory. Otherwise the launcher uses the current user's local application
|
|
297
|
+
data under `Arcane/<app-id>`. The launcher does not invent an application
|
|
298
|
+
workspace. Relative launch-file paths resolve from the invocation's working
|
|
299
|
+
directory. `--close-on-stdin-eof` is an explicit process-owner option used by the
|
|
300
|
+
SDK runner; an ordinary double-clicked window closes through its own UI.
|
|
301
|
+
|
|
302
|
+
## Windows host source and selected output
|
|
303
|
+
|
|
304
|
+
The SDK's `src/core/hosts/windows/ArcaneCoreProcess.cs` owns an explicitly
|
|
305
|
+
selected executable, argument string and working directory. It reads complete
|
|
306
|
+
framed Core messages and diagnostics, acknowledges ordered asynchronous writes,
|
|
307
|
+
and closes stdin before awaiting accepted Core work, final output and process
|
|
308
|
+
exit. It does not kill the child on an ordinary close.
|
|
309
|
+
|
|
310
|
+
`ArcaneHost.cs` composes that process with a WebView2 window through
|
|
311
|
+
`ArcaneHost.Run(options,onDiagnostic,onError)` or `ArcaneHostForm`. Options
|
|
312
|
+
select the app root/start path, stable virtual HTTPS origin, profile directory,
|
|
313
|
+
title, canonical classic-client source and Core process inputs. The form exposes
|
|
314
|
+
`Ready`, `Completion` and `CloseAsync()`. Page navigation, transport connection,
|
|
315
|
+
Core readiness and service readiness are separate states. Accepted work drains
|
|
316
|
+
before the window closes; complete engineering errors go to the supplied
|
|
317
|
+
diagnostic/error callbacks. Callbacks must not synchronously wait for the
|
|
318
|
+
window's lifetime promises.
|
|
319
|
+
|
|
320
|
+
`ArcaneLauncher.cs` supplies the executable entry point for that composition.
|
|
321
|
+
`tools/build-core-windows-host.mjs` builds the selected SDK host output from the
|
|
322
|
+
approved WebView2 package, existing .NET Framework compiler and selected Node
|
|
323
|
+
SEA output. It retains complete compiler diagnostics and produces a fresh
|
|
324
|
+
directory rather than overwriting an earlier artifact. The host requires
|
|
325
|
+
.NET Framework 4.6.2 or later. This is SDK release tooling, not an application
|
|
326
|
+
build requirement. Source compilation, selected host execution and real
|
|
327
|
+
application behavior are separate evidence boundaries; an assembled app is not
|
|
328
|
+
an installer or proof of its domain behavior.
|
|
329
|
+
|
|
330
|
+
Linux, macOS, Android dispatch and each platform's output verification retain
|
|
331
|
+
their separate delivery boundaries. Published
|
|
148
332
|
consumers must adopt the numeric package that includes this increment; a source
|
|
149
333
|
checkout or a successful source push is not that public package authority.
|