arcane-os 0.64.0 → 0.66.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 +50 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +3 -2
- package/browser-runtime/ai/decision-runtime.mjs +14 -24
- package/browser-runtime/core/client.mjs +62 -5
- package/docs/guides/native-whisper-openvino.md +334 -0
- package/docs/publishing.md +39 -0
- package/docs/reference/README.md +11 -1
- package/docs/reference/ai/browser-decisions.md +7 -4
- package/docs/reference/arcane-ollama.md +1 -1
- package/docs/reference/codex-app-server.md +269 -0
- package/docs/reference/core-client.md +60 -2
- package/docs/reference/core-development.md +157 -0
- package/docs/reference/core-native-packaging.md +13 -0
- package/docs/reference/core-repositories.md +232 -4
- package/docs/reference/git-text-snapshot.md +15 -1
- package/docs/reference/inventory/package-api.json +174 -6
- package/docs/reference/inventory/runtime-modules.json +8 -6
- package/docs/reference/local-ai.md +80 -3
- package/docs/reference/local-image-generation.md +5 -0
- package/docs/reference/local-image-sdxl.md +1 -1
- package/docs/reference/local-whisper.md +7 -1
- package/docs/reference/mcp-stdio.md +263 -0
- package/docs/reference/native-speech.md +6 -0
- package/docs/reference/runtime-components.md +18 -1
- package/docs/reference/runtime-modules.md +10 -2
- package/docs/reference/sdk-api.md +346 -4
- package/package.json +6 -2
- package/runtime/arcane/modules/AI.js +156 -13
- package/runtime/arcane/modules/Ollama.js +1 -1
- package/runtime/arcane/modules/OllamaModelIdentifier.js +13 -0
- package/schemas/arcane-app.schema.json +8 -1
- package/schemas/arcane-package.schema.json +8 -1
- package/src/codex/app-server.mjs +845 -0
- package/src/core/development.mjs +457 -0
- package/src/core/repositories.mjs +178 -9
- package/src/core/services/local-ai.mjs +80 -10
- package/src/dev-server.mjs +3 -2
- package/src/git-identity.mjs +132 -0
- package/src/git-text-snapshot.mjs +5 -3
- package/src/index.mjs +1 -0
- package/src/local-ai/dev.mjs +80 -426
- package/src/local-ai/whisper/config.mjs +31 -3
- package/src/local-ai/whisper/index.mjs +62 -5
- package/src/local-ai/whisper/install.mjs +72 -14
- package/src/local-ai/whisper/native/main.cpp +33 -2
- package/src/local-ai/whisper/openvino-build.mjs +138 -0
- package/src/mcp/stdio-server.mjs +579 -0
- package/src/native/portable-layout.mjs +3 -1
- package/src/toolchain.mjs +19 -8
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {mkdir, readdir} from 'node:fs/promises';
|
|
1
|
+
import {mkdir, readdir, writeFile} from 'node:fs/promises';
|
|
2
2
|
import {homedir} from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import Is from 'strong-type';
|
|
5
5
|
import {ArcaneError, throwIfAborted} from '../errors.mjs';
|
|
6
6
|
import {runProcess} from '../process.mjs';
|
|
7
7
|
import {repositoryPull, repositoryPush, repositoryStatus} from '../repository.mjs';
|
|
8
|
+
import {createGitIdentityRunner} from '../git-identity.mjs';
|
|
9
|
+
|
|
10
|
+
export {readGitIdentity} from '../git-identity.mjs';
|
|
8
11
|
|
|
9
12
|
const is = new Is(false);
|
|
10
13
|
const directoryOperations = new Map();
|
|
@@ -41,7 +44,7 @@ export function resolveArcaneDataPaths({dataRoot} = {}) {
|
|
|
41
44
|
|
|
42
45
|
/** One connected checkout, composed into an application's own Core service. */
|
|
43
46
|
export function createRepositoryWorkspace(
|
|
44
|
-
{name, directory, dataRoot, remote, branch, onEvent, run = runProcess} = {}
|
|
47
|
+
{name, directory, dataRoot, remote, branch, gitIdentity, onEvent, run = runProcess} = {}
|
|
45
48
|
) {
|
|
46
49
|
let selected = directory;
|
|
47
50
|
if (selected === undefined) {
|
|
@@ -55,12 +58,13 @@ export function createRepositoryWorkspace(
|
|
|
55
58
|
if (remote !== undefined) requireString(remote, 'remote');
|
|
56
59
|
if (branch !== undefined) requireString(branch, 'branch');
|
|
57
60
|
if (!is.function(run)) throw new TypeError('run must implement the SDK process adapter.');
|
|
61
|
+
const execute = createGitIdentityRunner(run, gitIdentity);
|
|
58
62
|
const repositoryDirectory = path.resolve(selected);
|
|
59
63
|
const pending = new Set();
|
|
60
64
|
let opened = false;
|
|
61
65
|
let closing = null;
|
|
62
66
|
|
|
63
|
-
async function prepare(signal) {
|
|
67
|
+
async function prepare(signal, prepareRun = execute) {
|
|
64
68
|
throwIfAborted(signal);
|
|
65
69
|
if (opened) return {directory: repositoryDirectory, cloned: false};
|
|
66
70
|
let entries;
|
|
@@ -74,7 +78,7 @@ export function createRepositoryWorkspace(
|
|
|
74
78
|
if (entries.length) {
|
|
75
79
|
// A parent checkout or a bare object cache is not this connection's
|
|
76
80
|
// working root. Inspection never changes an existing destination.
|
|
77
|
-
const result = await
|
|
81
|
+
const result = await prepareRun(
|
|
78
82
|
'git',
|
|
79
83
|
['rev-parse', '--is-inside-work-tree', '--show-prefix'],
|
|
80
84
|
{cwd: repositoryDirectory, signal, onEvent}
|
|
@@ -97,7 +101,7 @@ export function createRepositoryWorkspace(
|
|
|
97
101
|
const arguments_ = ['clone', '--progress'];
|
|
98
102
|
if (branch !== undefined) arguments_.push('--branch', branch);
|
|
99
103
|
arguments_.push('--', remote, repositoryDirectory);
|
|
100
|
-
const result = await
|
|
104
|
+
const result = await prepareRun(
|
|
101
105
|
'git',
|
|
102
106
|
arguments_,
|
|
103
107
|
{cwd: path.dirname(repositoryDirectory), signal, onEvent}
|
|
@@ -142,7 +146,7 @@ export function createRepositoryWorkspace(
|
|
|
142
146
|
return accept(
|
|
143
147
|
async function readRepositoryStatus() {
|
|
144
148
|
await prepare(signal);
|
|
145
|
-
return repositoryStatus({workspaceRoot: repositoryDirectory, signal, onEvent, run});
|
|
149
|
+
return repositoryStatus({workspaceRoot: repositoryDirectory, signal, onEvent, run: execute});
|
|
146
150
|
},
|
|
147
151
|
signal
|
|
148
152
|
);
|
|
@@ -152,7 +156,7 @@ export function createRepositoryWorkspace(
|
|
|
152
156
|
return accept(
|
|
153
157
|
async function pullRepository() {
|
|
154
158
|
await prepare(signal);
|
|
155
|
-
return repositoryPull({workspaceRoot: repositoryDirectory, signal, onEvent, run});
|
|
159
|
+
return repositoryPull({workspaceRoot: repositoryDirectory, signal, onEvent, run: execute});
|
|
156
160
|
},
|
|
157
161
|
signal
|
|
158
162
|
);
|
|
@@ -162,7 +166,172 @@ export function createRepositoryWorkspace(
|
|
|
162
166
|
return accept(
|
|
163
167
|
async function pushRepository() {
|
|
164
168
|
await prepare(signal);
|
|
165
|
-
return repositoryPush({workspaceRoot: repositoryDirectory, signal, onEvent, run});
|
|
169
|
+
return repositoryPush({workspaceRoot: repositoryDirectory, signal, onEvent, run: execute});
|
|
170
|
+
},
|
|
171
|
+
signal
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function write({files, message, signal} = {}) {
|
|
176
|
+
let selectedFiles;
|
|
177
|
+
try {
|
|
178
|
+
requireString(message, 'message');
|
|
179
|
+
if (!message.isWellFormed()) throw new TypeError('message cannot be represented completely as UTF-8 text.');
|
|
180
|
+
if (!is.array(files) || files.length === 0) {
|
|
181
|
+
throw new TypeError('write requires at least one explicitly selected file.');
|
|
182
|
+
}
|
|
183
|
+
selectedFiles = Array.from(
|
|
184
|
+
files,
|
|
185
|
+
function snapshotFile(file) {
|
|
186
|
+
const {path: filename, content} = file;
|
|
187
|
+
requireString(filename, 'file.path');
|
|
188
|
+
if (!filename.isWellFormed() || filename.includes('\0')) {
|
|
189
|
+
throw new TypeError('file.path cannot be represented completely as a native filename.');
|
|
190
|
+
}
|
|
191
|
+
if (!is.string(content) || !content.isWellFormed()) {
|
|
192
|
+
throw new TypeError('file.content must be complete UTF-8-representable text.');
|
|
193
|
+
}
|
|
194
|
+
const destination = path.resolve(repositoryDirectory, filename);
|
|
195
|
+
const relative = path.relative(repositoryDirectory, destination);
|
|
196
|
+
const first = relative.split(path.sep)[0];
|
|
197
|
+
if (path.isAbsolute(filename) || !relative || relative === '..'
|
|
198
|
+
|| relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
|
|
199
|
+
|| first.toLowerCase() === '.git') {
|
|
200
|
+
throw new TypeError('file.path must name a working file relative to this repository, outside Git metadata.');
|
|
201
|
+
}
|
|
202
|
+
return {path: filename, content, destination};
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return Promise.reject(error);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return accept(
|
|
210
|
+
async function writeRepositoryFiles() {
|
|
211
|
+
const outcome = {
|
|
212
|
+
directory: repositoryDirectory,
|
|
213
|
+
state: 'local',
|
|
214
|
+
stage: 'prepare',
|
|
215
|
+
paths: selectedFiles.map(function selectedPath(file) { return file.path; }),
|
|
216
|
+
writtenPaths: [],
|
|
217
|
+
written: false,
|
|
218
|
+
staged: false,
|
|
219
|
+
committed: false,
|
|
220
|
+
pushed: false,
|
|
221
|
+
outputs: []
|
|
222
|
+
};
|
|
223
|
+
let pendingWrite = false;
|
|
224
|
+
|
|
225
|
+
async function executeGit(stage, args, input, options = {}) {
|
|
226
|
+
throwIfAborted(signal);
|
|
227
|
+
outcome.stage = stage;
|
|
228
|
+
const output = {stage, stdout: '', stderr: ''};
|
|
229
|
+
const observed = {stdout: false, stderr: false};
|
|
230
|
+
outcome.outputs.push(output);
|
|
231
|
+
let beforeStartFailed = false;
|
|
232
|
+
|
|
233
|
+
function consumeOutput({stream, chunk}) {
|
|
234
|
+
observed[stream] = true;
|
|
235
|
+
output[stream] += chunk;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function observeEvent(event) {
|
|
239
|
+
try {
|
|
240
|
+
await onEvent?.(event);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (event.type === 'process.starting') beforeStartFailed = true;
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function recordCompletion(result) {
|
|
248
|
+
if (!result || !is.integer(result.code)) return;
|
|
249
|
+
output.code = result.code;
|
|
250
|
+
output.signal = result.signal ?? null;
|
|
251
|
+
for (const stream of ['stdout', 'stderr']) {
|
|
252
|
+
if (!observed[stream] && is.string(result[stream])) output[stream] = result[stream];
|
|
253
|
+
}
|
|
254
|
+
if (result.code !== 0) return;
|
|
255
|
+
if (stage === 'stage') outcome.staged = true;
|
|
256
|
+
if (stage === 'commit') {
|
|
257
|
+
outcome.committed = true;
|
|
258
|
+
outcome.state = 'committed';
|
|
259
|
+
}
|
|
260
|
+
if (stage === 'push') {
|
|
261
|
+
outcome.pushed = true;
|
|
262
|
+
outcome.state = 'pushed';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = await execute(
|
|
268
|
+
'git',
|
|
269
|
+
args,
|
|
270
|
+
{cwd: options.cwd ?? repositoryDirectory, signal, onEvent: observeEvent,
|
|
271
|
+
onOutput: consumeOutput, captureOutput: false, input}
|
|
272
|
+
);
|
|
273
|
+
recordCompletion(result);
|
|
274
|
+
if (result.code !== 0) {
|
|
275
|
+
throw new ArcaneError(
|
|
276
|
+
'ARCANE_OPERATION_FAILED',
|
|
277
|
+
`Git ${stage} exited with code ${String(result.code)}.`,
|
|
278
|
+
{details: result}
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return {...result, stdout: output.stdout, stderr: output.stderr};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (!beforeStartFailed) recordCompletion(error?.details);
|
|
284
|
+
const notStarted = beforeStartFailed
|
|
285
|
+
|| (error?.code === 'ARCANE_PREREQUISITE_MISSING' && !error?.details)
|
|
286
|
+
|| (error?.code === 'ARCANE_CANCELLED' && !error?.details);
|
|
287
|
+
if (!notStarted && output.code !== 0 && (stage === 'commit' || stage === 'push')) {
|
|
288
|
+
outcome.state = 'uncertain';
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function* commitMessage() {
|
|
295
|
+
yield message;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await prepare(
|
|
300
|
+
signal,
|
|
301
|
+
function prepareGit(command, args, options) {
|
|
302
|
+
return executeGit('prepare', args, undefined, options);
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
outcome.stage = 'write';
|
|
306
|
+
for (const file of selectedFiles) {
|
|
307
|
+
throwIfAborted(signal);
|
|
308
|
+
outcome.path = file.path;
|
|
309
|
+
await mkdir(path.dirname(file.destination), {recursive: true});
|
|
310
|
+
throwIfAborted(signal);
|
|
311
|
+
pendingWrite = true;
|
|
312
|
+
await writeFile(file.destination, file.content, {encoding: 'utf8', signal});
|
|
313
|
+
pendingWrite = false;
|
|
314
|
+
outcome.writtenPaths.push(file.path);
|
|
315
|
+
}
|
|
316
|
+
outcome.written = true;
|
|
317
|
+
delete outcome.path;
|
|
318
|
+
await executeGit('stage', ['--literal-pathspecs', 'add', '--', ...outcome.paths]);
|
|
319
|
+
await executeGit(
|
|
320
|
+
'commit',
|
|
321
|
+
['--literal-pathspecs', 'commit', '--only', '--cleanup=verbatim', '--file', '-', '--', ...outcome.paths],
|
|
322
|
+
commitMessage()
|
|
323
|
+
);
|
|
324
|
+
await executeGit('push', ['push']);
|
|
325
|
+
outcome.stage = 'complete';
|
|
326
|
+
return outcome;
|
|
327
|
+
} catch (cause) {
|
|
328
|
+
if (pendingWrite) outcome.state = 'uncertain';
|
|
329
|
+
throw new ArcaneError(
|
|
330
|
+
cause?.code ?? 'ARCANE_REPOSITORY_WRITE_FAILED',
|
|
331
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
332
|
+
{cause, details: outcome, exitCode: cause?.exitCode}
|
|
333
|
+
);
|
|
334
|
+
}
|
|
166
335
|
},
|
|
167
336
|
signal
|
|
168
337
|
);
|
|
@@ -188,7 +357,7 @@ export function createRepositoryWorkspace(
|
|
|
188
357
|
return closing;
|
|
189
358
|
}
|
|
190
359
|
|
|
191
|
-
return {directory: repositoryDirectory, open, status, pull, push, close, drain: close, dispose: close};
|
|
360
|
+
return {directory: repositoryDirectory, open, status, pull, push, write, close, drain: close, dispose: close};
|
|
192
361
|
}
|
|
193
362
|
|
|
194
363
|
function requireString(value, name) {
|
|
@@ -208,17 +208,54 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
208
208
|
publish();
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
function beginOllamaObservation(engine) {
|
|
212
|
+
return {revision: ++engine.ollamaRevision, signal: engine.signal};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function ownsOllamaObservation(engine, observation) {
|
|
216
|
+
return observation.signal === engine.signal && !observation.signal.aborted
|
|
217
|
+
&& engine.server.current.available && observation.revision >= engine.ollamaAppliedRevision;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function recordOllamaFailure(engine, error, observation) {
|
|
221
|
+
if (!ownsOllamaObservation(engine, observation)) return;
|
|
222
|
+
engine.ollamaAppliedRevision = observation.revision;
|
|
223
|
+
recordFailure(engine, error);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function recordOllamaModels(engine, catalog, running, observation) {
|
|
227
|
+
if (!ownsOllamaObservation(engine, observation)) return;
|
|
228
|
+
engine.ollamaAppliedRevision = observation.revision;
|
|
229
|
+
engine.state = {...engine.state, ...engine.server.current, error: null};
|
|
230
|
+
const resident = new Map((running.models ?? []).map(function residentModel(model) {
|
|
231
|
+
return [model.model ?? model.name, model];
|
|
232
|
+
}));
|
|
233
|
+
const models = new Map((catalog.models ?? []).map(function catalogModel(model) {
|
|
234
|
+
const id = model.model ?? model.name ?? model.id;
|
|
235
|
+
return [id, {...model, id, loaded: resident.has(id)}];
|
|
236
|
+
}));
|
|
237
|
+
for (const [id, model] of resident) {
|
|
238
|
+
if (!models.has(id)) models.set(id, {...model, id, loaded: true});
|
|
239
|
+
}
|
|
240
|
+
engine.models = [...models.values()];
|
|
241
|
+
}
|
|
242
|
+
|
|
211
243
|
async function refreshModels(engine, signal) {
|
|
212
244
|
const revision = engine.revision;
|
|
213
245
|
if (engine.id === 'ollama') {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
const observation = beginOllamaObservation(engine);
|
|
247
|
+
const observationSignal = signal ? AbortSignal.any([signal, observation.signal]) : observation.signal;
|
|
248
|
+
try {
|
|
249
|
+
const [catalog, running] = await Promise.all([
|
|
250
|
+
requestLocalJSON({url: engine.server.url, path: '/api/tags', signal: observationSignal}),
|
|
251
|
+
requestLocalJSON({url: engine.server.url, path: '/api/ps', signal: observationSignal})
|
|
252
|
+
]);
|
|
253
|
+
observationSignal.throwIfAborted();
|
|
254
|
+
recordOllamaModels(engine, catalog, running, observation);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (!observationSignal.aborted) recordOllamaFailure(engine, error, observation);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
222
259
|
} else {
|
|
223
260
|
const properties = await requestLocalJSON({url: engine.server.url, path: '/props', signal});
|
|
224
261
|
engine.router = properties.role === 'router';
|
|
@@ -427,6 +464,34 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
427
464
|
|
|
428
465
|
async function ollamaRequest(operation, parameters = {}, request, method = 'POST') {
|
|
429
466
|
const engine = await engineReady('ollama', request.signal);
|
|
467
|
+
const engineSignal = engine.signal;
|
|
468
|
+
const observation = operation === 'ps' ? beginOllamaObservation(engine) : null;
|
|
469
|
+
try {
|
|
470
|
+
const result = await ollamaResponse(engine, operation, parameters, request, method);
|
|
471
|
+
if (operation === 'ps') {
|
|
472
|
+
recordOllamaModels(engine, {models: engine.models}, result, observation);
|
|
473
|
+
publish();
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (operation === 'ps' && !request.signal.aborted) recordOllamaFailure(engine, error, observation);
|
|
478
|
+
throw error;
|
|
479
|
+
} finally {
|
|
480
|
+
// Every owned operation that can change residency/catalog leaves
|
|
481
|
+
// a real upstream observation, including cancellation or failure.
|
|
482
|
+
if (['chat', 'generate', 'embed', 'pull', 'create', 'copy', 'delete'].includes(operation)
|
|
483
|
+
&& engine.signal === engineSignal && !engineSignal.aborted) {
|
|
484
|
+
try {
|
|
485
|
+
await refreshModels(engine, engineSignal);
|
|
486
|
+
} catch {
|
|
487
|
+
// The refresh publishes only its current owned failure;
|
|
488
|
+
// retain the operation's original result or error.
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function ollamaResponse(engine, operation, parameters, request, method) {
|
|
430
495
|
const signal = AbortSignal.any([request.signal, engine.signal]);
|
|
431
496
|
const {streamId, ...payload} = parameters;
|
|
432
497
|
if (payload.stream !== true) {
|
|
@@ -472,6 +537,8 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
472
537
|
function startEngine(engine) {
|
|
473
538
|
engine.controller = new AbortController();
|
|
474
539
|
engine.signal = AbortSignal.any([lifetimeSignal, engine.controller.signal]);
|
|
540
|
+
engine.ollamaRevision = 0;
|
|
541
|
+
engine.ollamaAppliedRevision = 0;
|
|
475
542
|
const signal = engine.signal;
|
|
476
543
|
engine.server = createLocalAIServer({
|
|
477
544
|
id: engine.id, appRoot, runtime: runtimes.find(function runtimeRecord(record) { return record.id === engine.id; }),
|
|
@@ -487,7 +554,10 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
487
554
|
}
|
|
488
555
|
return engine;
|
|
489
556
|
});
|
|
490
|
-
engine.ready.catch(function startupFailed(error) {
|
|
557
|
+
engine.ready.catch(function startupFailed(error) {
|
|
558
|
+
// Ollama refreshes and the server already publish their own failures.
|
|
559
|
+
if (!signal.aborted && engine.id !== 'ollama') recordFailure(engine, error);
|
|
560
|
+
});
|
|
491
561
|
}
|
|
492
562
|
|
|
493
563
|
const service = {
|
|
@@ -557,7 +627,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
557
627
|
await refreshModels(engine, request.signal);
|
|
558
628
|
} catch (error) {
|
|
559
629
|
request.signal.throwIfAborted();
|
|
560
|
-
recordFailure(engine, error);
|
|
630
|
+
if (engine.id !== 'ollama') recordFailure(engine, error);
|
|
561
631
|
}
|
|
562
632
|
}));
|
|
563
633
|
return snapshot();
|
package/src/dev-server.mjs
CHANGED
|
@@ -855,6 +855,7 @@ async function startOwnedDevServer({
|
|
|
855
855
|
certPath,
|
|
856
856
|
keyPath,
|
|
857
857
|
localAI,
|
|
858
|
+
core = localAI,
|
|
858
859
|
signal,
|
|
859
860
|
sdkRuntimeSourceRoot
|
|
860
861
|
}={},events,releaseSignal){
|
|
@@ -1261,7 +1262,7 @@ async function startOwnedDevServer({
|
|
|
1261
1262
|
response.end();
|
|
1262
1263
|
return;
|
|
1263
1264
|
}
|
|
1264
|
-
if (
|
|
1265
|
+
if (core && await core.handler(request, response)) return;
|
|
1265
1266
|
if(request.method!=='GET'&&request.method!=='HEAD'){
|
|
1266
1267
|
deny(response,405,'Method not allowed.');
|
|
1267
1268
|
return;
|
|
@@ -1558,7 +1559,7 @@ async function startOwnedDevServer({
|
|
|
1558
1559
|
}
|
|
1559
1560
|
if(!closeInitiated){
|
|
1560
1561
|
closeInitiated=true;
|
|
1561
|
-
localAIClosing=Promise.resolve().then(function
|
|
1562
|
+
localAIClosing=Promise.resolve().then(function closeCore(){return core?.close(error);});
|
|
1562
1563
|
localAIClosing.catch(function observeLocalAICloseFailure(closeError){operationalError??=closeError;});
|
|
1563
1564
|
try{
|
|
1564
1565
|
closeDevelopmentListeners(fileServer, tlsServer).then(
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import Is from 'strong-type';
|
|
3
|
+
import {ArcaneError, throwIfAborted} from './errors.mjs';
|
|
4
|
+
import {runProcess} from './process.mjs';
|
|
5
|
+
import {createEventQueue} from './event-queue.mjs';
|
|
6
|
+
|
|
7
|
+
const is = new Is(false);
|
|
8
|
+
|
|
9
|
+
/** Read only non-secret identity configuration through ordinary Git scopes. */
|
|
10
|
+
export async function readGitIdentity({directory, signal, onEvent, run = runProcess} = {}) {
|
|
11
|
+
const cwd = directory === undefined ? undefined : path.resolve(directory);
|
|
12
|
+
throwIfAborted(signal);
|
|
13
|
+
const events = createEventQueue(onEvent);
|
|
14
|
+
|
|
15
|
+
async function observeScope(scope) {
|
|
16
|
+
const args = ['config'];
|
|
17
|
+
if (scope !== 'effective') args.push(`--${scope}`);
|
|
18
|
+
args.push('--includes', '--null', '--get-regexp', '^(user[.](name|email)|github[.]user)$');
|
|
19
|
+
const chunks = [];
|
|
20
|
+
try {
|
|
21
|
+
const result = await run('git', args, {
|
|
22
|
+
cwd, signal, onEvent: events.send, allowNonzero: true,
|
|
23
|
+
outputEncoding: {stdout: null},
|
|
24
|
+
captureOutput: {stdout: false},
|
|
25
|
+
emitOutputEvents: {stdout: false},
|
|
26
|
+
// Git configuration may contain non-UTF-8 values. Keep its raw
|
|
27
|
+
// output until close rather than silently replacing characters.
|
|
28
|
+
onOutput: function observeOutput({stream, chunk}) {
|
|
29
|
+
if (stream === 'stdout') chunks.push(chunk);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
try { throwIfAborted(signal); }
|
|
33
|
+
catch (error) { error.details = result; throw error; }
|
|
34
|
+
const identity = {name: null, email: null, githubUser: null};
|
|
35
|
+
if (result.code === 1) return identity;
|
|
36
|
+
if (result.code !== 0) {
|
|
37
|
+
throw new ArcaneError('ARCANE_OPERATION_FAILED',
|
|
38
|
+
`Git ${scope} identity lookup exited with code ${String(result.code)}.`, {details: result});
|
|
39
|
+
}
|
|
40
|
+
let output;
|
|
41
|
+
try { output = new TextDecoder('utf-8', {fatal: true, ignoreBOM: true}).decode(Buffer.concat(chunks)); }
|
|
42
|
+
catch (cause) {
|
|
43
|
+
throw new ArcaneError('ARCANE_GIT_IDENTITY_NOT_TEXT',
|
|
44
|
+
`Git ${scope} identity configuration cannot be represented as UTF-8 text.`, {cause, details: result});
|
|
45
|
+
}
|
|
46
|
+
const fields = {'user.name': 'name', 'user.email': 'email', 'github.user': 'githubUser'};
|
|
47
|
+
// Git's NUL record framing preserves complete values, including newlines
|
|
48
|
+
// and empty strings. The last occurrence is the effective scalar value.
|
|
49
|
+
for (const record of output.split('\0')) {
|
|
50
|
+
if (record === '') continue;
|
|
51
|
+
const separator = record.indexOf('\n');
|
|
52
|
+
const key = separator === -1 ? record : record.slice(0, separator);
|
|
53
|
+
if (Object.hasOwn(fields, key)) {
|
|
54
|
+
identity[fields[key]] = separator === -1 ? '' : record.slice(separator + 1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return identity;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Keep the process error and its native details intact. Raw stdout
|
|
60
|
+
// belongs to this reader because the process owner did not decode it.
|
|
61
|
+
const rawStdout = Buffer.concat(chunks);
|
|
62
|
+
let attached = false;
|
|
63
|
+
let attachmentError;
|
|
64
|
+
try {
|
|
65
|
+
if (error !== null && (is.object(error) || is.function(error)) && !('rawStdout' in error)) {
|
|
66
|
+
Object.defineProperty(error, 'rawStdout', {
|
|
67
|
+
value: rawStdout, writable: true, enumerable: true, configurable: true
|
|
68
|
+
});
|
|
69
|
+
attached = true;
|
|
70
|
+
}
|
|
71
|
+
} catch (cause) { attachmentError = cause; }
|
|
72
|
+
if (attached) throw error;
|
|
73
|
+
const failure = new ArcaneError(error?.code ?? 'ARCANE_OPERATION_FAILED',
|
|
74
|
+
error instanceof Error ? error.message : String(error),
|
|
75
|
+
{cause: error, details: error?.details, exitCode: error?.exitCode});
|
|
76
|
+
failure.rawStdout = rawStdout;
|
|
77
|
+
if (attachmentError) failure.attachmentError = attachmentError;
|
|
78
|
+
throw failure;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const scopes = cwd === undefined ? ['global'] : ['global', 'local', 'effective'];
|
|
83
|
+
const observations = await Promise.allSettled(scopes.map(observeScope));
|
|
84
|
+
const failures = observations.filter(function failed(result) {
|
|
85
|
+
return result.status === 'rejected';
|
|
86
|
+
}).map(function cause(result) { return result.reason; });
|
|
87
|
+
try { await events.drain(); }
|
|
88
|
+
catch (error) {
|
|
89
|
+
const visited = new Set();
|
|
90
|
+
function containsObserverFailure(failure) {
|
|
91
|
+
if (failure === error) return true;
|
|
92
|
+
if (failure === null || failure === undefined || visited.has(failure)) return false;
|
|
93
|
+
visited.add(failure);
|
|
94
|
+
return containsObserverFailure(failure.cause)
|
|
95
|
+
|| (is.array(failure.errors) && failure.errors.some(containsObserverFailure));
|
|
96
|
+
}
|
|
97
|
+
const represented = failures.some(containsObserverFailure);
|
|
98
|
+
if (!represented) failures.push(error);
|
|
99
|
+
}
|
|
100
|
+
if (failures.length === 1) throw failures[0];
|
|
101
|
+
if (failures.length) throw new AggregateError(failures, 'Git identity observations failed.');
|
|
102
|
+
const result = {global: null, local: null, effective: null};
|
|
103
|
+
for (const [index, scope] of scopes.entries()) result[scope] = observations[index].value;
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Capture one factory's selection without changing Git files or process.env. */
|
|
108
|
+
export function createGitIdentityRunner(run, {name, email, username} = {}) {
|
|
109
|
+
const configuration = [];
|
|
110
|
+
const environment = {};
|
|
111
|
+
for (const [field, value] of Object.entries({name, email, username})) {
|
|
112
|
+
if (value === undefined) continue;
|
|
113
|
+
if (!is.string(value) || !value.isWellFormed() || value.includes('\0')) {
|
|
114
|
+
throw new TypeError(`gitIdentity.${field} must be text representable as a native process argument.`);
|
|
115
|
+
}
|
|
116
|
+
configuration.push('-c', `${field === 'username' ? 'credential.username' : `user.${field}`}=${value}`);
|
|
117
|
+
if (field === 'name') {
|
|
118
|
+
environment.GIT_AUTHOR_NAME = value;
|
|
119
|
+
environment.GIT_COMMITTER_NAME = value;
|
|
120
|
+
} else if (field === 'email') {
|
|
121
|
+
environment.GIT_AUTHOR_EMAIL = value;
|
|
122
|
+
environment.GIT_COMMITTER_EMAIL = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!configuration.length) return run;
|
|
126
|
+
const selectsAuthor = name !== undefined || email !== undefined;
|
|
127
|
+
return function runSelectedGit(command, args, options = {}) {
|
|
128
|
+
return run(command, [...configuration, ...args], selectsAuthor
|
|
129
|
+
? {...options, env: {...options.env, ...environment}}
|
|
130
|
+
: options);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import {runProcess} from './process.mjs';
|
|
5
5
|
import {ArcaneError, ERROR_CODES, throwIfAborted} from './errors.mjs';
|
|
6
6
|
import {createEventQueue} from './event-queue.mjs';
|
|
7
|
+
import {createGitIdentityRunner} from './git-identity.mjs';
|
|
7
8
|
|
|
8
9
|
const is = new Is(false);
|
|
9
10
|
const snapshotRef = 'refs/arcane/text-snapshot';
|
|
@@ -13,11 +14,12 @@ const snapshotRef = 'refs/arcane/text-snapshot';
|
|
|
13
14
|
* One application service owns each dedicated cache directory. Construction
|
|
14
15
|
* performs no I/O; refresh coalesces callers and close drains accepted work.
|
|
15
16
|
*/
|
|
16
|
-
export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath, onEvent, run = runProcess} = {}) {
|
|
17
|
+
export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath, gitIdentity, onEvent, run = runProcess} = {}) {
|
|
17
18
|
for (const [name, value] of Object.entries({cacheDirectory, remote, ref})) {
|
|
18
19
|
if (!is.string(value) || value === '') throw new TypeError(`${name} must be a nonempty string.`);
|
|
19
20
|
}
|
|
20
21
|
if (!is.function(selectPath)) throw new TypeError('selectPath must be a function.');
|
|
22
|
+
const execute = createGitIdentityRunner(run, gitIdentity);
|
|
21
23
|
const directory = path.resolve(cacheDirectory);
|
|
22
24
|
let prepared = false;
|
|
23
25
|
let retained = null;
|
|
@@ -34,7 +36,7 @@ export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath,
|
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
try {
|
|
37
|
-
return await
|
|
39
|
+
return await execute('git', ['--git-dir', directory, ...args], execution);
|
|
38
40
|
} catch (error) {
|
|
39
41
|
// runProcess first drains its child and preserves complete transport
|
|
40
42
|
// failures. Keep the Git parser's public code with that full cause.
|
|
@@ -50,7 +52,7 @@ export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath,
|
|
|
50
52
|
if (prepared) return;
|
|
51
53
|
await mkdir(directory, {recursive: true});
|
|
52
54
|
if ((await readdir(directory)).length === 0) {
|
|
53
|
-
await
|
|
55
|
+
await execute('git', ['init', '--bare', directory], {onEvent: events.send});
|
|
54
56
|
}
|
|
55
57
|
const result = await git(['rev-parse', '--is-bare-repository'], {onEvent: events.send});
|
|
56
58
|
// This is the selected cache's functional role: never reinterpret an
|
package/src/index.mjs
CHANGED
|
@@ -146,6 +146,7 @@ export {
|
|
|
146
146
|
} from './workspace.mjs';
|
|
147
147
|
export {materializeInstalledSdkRuntime} from './installed-sdk-runtime.mjs';
|
|
148
148
|
export {startDevServer} from './dev-server.mjs';
|
|
149
|
+
export {createDevelopmentCore} from './core/development.mjs';
|
|
149
150
|
export {startSourceExampleServer} from './source-server.mjs';
|
|
150
151
|
export {acceptWebSocket} from './websocket.mjs';
|
|
151
152
|
export {WS} from '../browser-runtime/websocket-client.mjs';
|