arcane-os 0.94.0 → 0.96.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/core-model-assets.mjs +74 -0
  4. package/browser-runtime/core/client.mjs +195 -0
  5. package/docs/reference/README.md +9 -0
  6. package/docs/reference/ai/browser-speech.md +61 -10
  7. package/docs/reference/core-client.md +35 -0
  8. package/docs/reference/core-native-packaging.md +38 -6
  9. package/docs/reference/inventory/package-api.json +133 -5
  10. package/docs/reference/inventory/runtime-modules.json +39 -13
  11. package/docs/reference/model-assets.md +164 -3
  12. package/docs/reference/native-magpie.md +98 -0
  13. package/docs/reference/native-nemotron.md +204 -0
  14. package/docs/reference/native-speech.md +250 -5
  15. package/docs/reference/runtime-modules.md +157 -11
  16. package/docs/reference/sdk-api.md +247 -4
  17. package/package.json +7 -3
  18. package/runtime/arcane/modules/AI.js +114 -14
  19. package/runtime/arcane/modules/AIProviderRuntime.js +32 -15
  20. package/runtime/arcane/modules/ContinuousVoiceCapture.js +118 -21
  21. package/runtime/arcane/modules/NativeSpeechCapture.js +198 -0
  22. package/runtime/arcane/modules/NativeSpeechPlayback.js +304 -0
  23. package/runtime/arcane/modules/VoiceCaptureWorklet.js +62 -2
  24. package/src/core/host.mjs +1 -0
  25. package/src/core/services/local-ai.mjs +46 -12
  26. package/src/core/services/model-assets.mjs +491 -59
  27. package/src/core/services/speech.mjs +237 -4
  28. package/src/local-ai/magpie/build.mjs +36 -0
  29. package/src/local-ai/magpie/helper.mjs +254 -0
  30. package/src/local-ai/magpie/index.mjs +488 -0
  31. package/src/local-ai/magpie/native/CMakeLists.txt +10 -0
  32. package/src/local-ai/magpie/native/main.cpp +270 -0
  33. package/src/local-ai/nemo-models.mjs +160 -0
  34. package/src/local-ai/nemotron/build.mjs +53 -0
  35. package/src/local-ai/nemotron/helper.mjs +308 -0
  36. package/src/local-ai/nemotron/index.mjs +693 -0
  37. package/src/local-ai/nemotron/native/CMakeLists.txt +17 -0
  38. package/src/local-ai/nemotron/native/main.cpp +282 -0
  39. package/src/native/macos-provider.mjs +14 -7
  40. package/src/native/portable-assembly.mjs +180 -0
  41. package/src/native/portable-provider.mjs +17 -161
  42. package/src/native/publish-output.mjs +101 -0
  43. package/src/native/windows-provider.mjs +12 -6
@@ -0,0 +1,17 @@
1
+ cmake_minimum_required(VERSION 3.26)
2
+ project(ArcaneNemotron LANGUAGES CXX)
3
+
4
+ find_package(NeMoSpeech REQUIRED COMPONENTS ASR)
5
+
6
+ add_executable(arcane-nemotron main.cpp)
7
+ target_compile_features(arcane-nemotron PRIVATE cxx_std_17)
8
+ target_link_libraries(arcane-nemotron PRIVATE NeMoSpeech::ASR)
9
+ if(MSVC)
10
+ target_compile_options(arcane-nemotron PRIVATE /utf-8 /EHsc)
11
+ endif()
12
+ if(APPLE)
13
+ set_target_properties(arcane-nemotron PROPERTIES INSTALL_RPATH "@loader_path;@loader_path/../lib")
14
+ elseif(UNIX)
15
+ set_target_properties(arcane-nemotron PROPERTIES INSTALL_RPATH "$ORIGIN;$ORIGIN/../lib")
16
+ endif()
17
+ install(TARGETS arcane-nemotron RUNTIME DESTINATION bin)
@@ -0,0 +1,282 @@
1
+ #include <nemo_speech/asr.h>
2
+
3
+ #include <cstdint>
4
+ #include <cstring>
5
+ #include <iomanip>
6
+ #include <iostream>
7
+ #include <limits>
8
+ #include <locale>
9
+ #include <memory>
10
+ #include <sstream>
11
+ #include <stdexcept>
12
+ #include <string>
13
+ #include <vector>
14
+
15
+ #ifdef _WIN32
16
+ #include <fcntl.h>
17
+ #include <io.h>
18
+ #include <windows.h>
19
+ #endif
20
+
21
+ namespace {
22
+
23
+ using Recognizer = std::unique_ptr<nemo_speech_asr_recognizer, decltype(&nemo_speech_asr_destroy)>;
24
+ using Stream = std::unique_ptr<nemo_speech_asr_stream, decltype(&nemo_speech_asr_stream_close)>;
25
+ using Result = std::unique_ptr<nemo_speech_asr_result, decltype(&nemo_speech_asr_result_destroy)>;
26
+
27
+ void check(nemo_speech_asr_status status) {
28
+ if (status != NEMO_SPEECH_ASR_OK) {
29
+ const char* message = nemo_speech_asr_last_error();
30
+ throw std::runtime_error(message ? message : "The native ASR operation failed.");
31
+ }
32
+ }
33
+
34
+ std::string json_string(const char* value) {
35
+ std::ostringstream output;
36
+ output << '"';
37
+ if (value) {
38
+ for (const unsigned char character : std::string(value)) {
39
+ switch (character) {
40
+ case '"': output << "\\\""; break;
41
+ case '\\': output << "\\\\"; break;
42
+ case '\n': output << "\\n"; break;
43
+ case '\r': output << "\\r"; break;
44
+ case '\t': output << "\\t"; break;
45
+ default:
46
+ if (character < 0x20) {
47
+ output << "\\u" << std::hex << std::setw(4) << std::setfill('0')
48
+ << static_cast<unsigned int>(character) << std::dec;
49
+ } else {
50
+ output << character;
51
+ }
52
+ }
53
+ }
54
+ }
55
+ output << '"';
56
+ return output.str();
57
+ }
58
+
59
+ void emit_error(uint64_t request, const std::string& message) {
60
+ std::cout << "{\"request\":" << request << ",\"error\":" << json_string(message.c_str())
61
+ << "}\n" << std::flush;
62
+ }
63
+
64
+ void emit_ack(uint64_t request) {
65
+ std::cout << "{\"request\":" << request << ",\"ok\":true}\n" << std::flush;
66
+ }
67
+
68
+ void emit_running(uint64_t request) {
69
+ std::cout << "{\"request\":" << request << ",\"running\":true}\n" << std::flush;
70
+ }
71
+
72
+ std::string read_text(size_t count) {
73
+ std::string text(count, '\0');
74
+ if (count && !std::cin.read(text.data(), count)) {
75
+ throw std::runtime_error("Nemotron input ended during a language field.");
76
+ }
77
+ return text;
78
+ }
79
+
80
+ std::vector<float> read_audio(size_t samples) {
81
+ std::vector<float> audio(samples);
82
+ std::vector<unsigned char> encoded(samples * sizeof(float));
83
+ if (!encoded.empty() && !std::cin.read(reinterpret_cast<char*>(encoded.data()), encoded.size())) {
84
+ throw std::runtime_error("Nemotron input ended during a float32 audio frame.");
85
+ }
86
+ for (size_t index = 0; index < samples; ++index) {
87
+ const auto* value = encoded.data() + index * sizeof(float);
88
+ const uint32_t bits = static_cast<uint32_t>(value[0])
89
+ | (static_cast<uint32_t>(value[1]) << 8)
90
+ | (static_cast<uint32_t>(value[2]) << 16)
91
+ | (static_cast<uint32_t>(value[3]) << 24);
92
+ std::memcpy(&audio[index], &bits, sizeof(float));
93
+ }
94
+ return audio;
95
+ }
96
+
97
+ void emit_result(uint64_t request, uint64_t sequence, const nemo_speech_asr_result* result) {
98
+ std::ostringstream output;
99
+ output.imbue(std::locale::classic());
100
+ output << std::setprecision(std::numeric_limits<float>::max_digits10);
101
+ output << "{\"request\":" << request << ",\"update\":{\"sequence\":" << sequence
102
+ << ",\"final\":" << (nemo_speech_asr_result_is_final(result) ? "true" : "false")
103
+ << ",\"audioProcessedSeconds\":" << nemo_speech_asr_result_audio_processed(result)
104
+ << ",\"channelTag\":" << nemo_speech_asr_result_channel_tag(result)
105
+ << ",\"alternatives\":[";
106
+ const size_t alternatives = nemo_speech_asr_result_alternative_count(result);
107
+ for (size_t alternative = 0; alternative < alternatives; ++alternative) {
108
+ if (alternative) output << ',';
109
+ output << "{\"text\":" << json_string(nemo_speech_asr_result_transcript(result, alternative))
110
+ << ",\"confidence\":" << nemo_speech_asr_result_confidence(result, alternative)
111
+ << ",\"words\":[";
112
+ const size_t words = nemo_speech_asr_result_word_count(result, alternative);
113
+ for (size_t index = 0; index < words; ++index) {
114
+ if (index) output << ',';
115
+ output << "{\"word\":" << json_string(nemo_speech_asr_result_word_text(result, alternative, index))
116
+ << ",\"startTimeMs\":" << nemo_speech_asr_result_word_start_time(result, alternative, index)
117
+ << ",\"endTimeMs\":" << nemo_speech_asr_result_word_end_time(result, alternative, index)
118
+ << ",\"confidence\":" << nemo_speech_asr_result_word_confidence(result, alternative, index)
119
+ << ",\"speaker\":" << nemo_speech_asr_result_word_speaker_tag(result, alternative, index) << '}';
120
+ }
121
+ output << "],\"languageCodes\":[";
122
+ const size_t languages = nemo_speech_asr_result_language_count(result, alternative);
123
+ for (size_t index = 0; index < languages; ++index) {
124
+ if (index) output << ',';
125
+ output << json_string(nemo_speech_asr_result_language_code(result, alternative, index));
126
+ }
127
+ output << "]}";
128
+ }
129
+ output << "]}}\n";
130
+ // A complete result is emitted before asking for the next one. An error
131
+ // later in the drain cannot erase earlier native results.
132
+ std::cout << output.str() << std::flush;
133
+ }
134
+
135
+ void drain(uint64_t request, nemo_speech_asr_stream* stream, uint64_t& sequence) {
136
+ while (true) {
137
+ nemo_speech_asr_result* raw = nullptr;
138
+ const auto status = nemo_speech_asr_stream_next(stream, &raw);
139
+ const Result result(raw, nemo_speech_asr_result_destroy);
140
+ check(status);
141
+ if (!result) return;
142
+ emit_result(request, ++sequence, result.get());
143
+ }
144
+ }
145
+
146
+ int serve(const std::string& model_path, const std::string& diarization_path, int gpu) {
147
+ #ifdef _WIN32
148
+ _setmode(_fileno(stdin), _O_BINARY);
149
+ _setmode(_fileno(stdout), _O_BINARY);
150
+ #endif
151
+ nemo_speech_asr_backend_config backend{};
152
+ backend.size = sizeof(backend);
153
+ backend.gpu = gpu;
154
+ nemo_speech_asr_model_config model{};
155
+ model.size = sizeof(model);
156
+ model.path = model_path.c_str();
157
+ nemo_speech_asr_diar_config diarization{};
158
+ diarization.size = sizeof(diarization);
159
+ diarization.model_path = diarization_path.empty() ? nullptr : diarization_path.c_str();
160
+ diarization.left_context_frames = -1;
161
+ nemo_speech_asr_endpointing_config endpointing{};
162
+ endpointing.size = sizeof(endpointing);
163
+ endpointing.enable = false;
164
+ nemo_speech_asr_recognizer_config configuration{};
165
+ configuration.size = sizeof(configuration);
166
+ configuration.backend = &backend;
167
+ configuration.model = &model;
168
+ configuration.diar = &diarization;
169
+ configuration.endpointing = &endpointing;
170
+ nemo_speech_asr_recognizer* raw_recognizer = nullptr;
171
+ const auto status = nemo_speech_asr_create(&configuration, &raw_recognizer);
172
+ const Recognizer recognizer(raw_recognizer, nemo_speech_asr_destroy);
173
+ check(status);
174
+ // The stream is destroyed before its recognizer. All stream calls stay on
175
+ // this thread; the host owns capture, timing and conversational decisions.
176
+ Stream stream(nullptr, nemo_speech_asr_stream_close);
177
+ int sample_rate = 16000;
178
+ uint64_t sequence = 0;
179
+ std::string language;
180
+ std::cout << "{\"ready\":true,\"version\":" << json_string(nemo_speech_asr_version())
181
+ << ",\"requestedGpu\":" << gpu << "}\n" << std::flush;
182
+ std::string line;
183
+ while (std::getline(std::cin, line)) {
184
+ uint64_t request = 0;
185
+ std::string operation;
186
+ std::istringstream command(line);
187
+ if (!(command >> operation >> request)) {
188
+ emit_error(0, "Unreadable Nemotron command.");
189
+ return 1;
190
+ }
191
+ try {
192
+ if (operation == "open") {
193
+ size_t language_length = 0;
194
+ int requested_rate = 0;
195
+ if (!(command >> requested_rate >> language_length)) {
196
+ throw std::runtime_error("Unreadable Nemotron stream configuration.");
197
+ }
198
+ const std::string requested_language = read_text(language_length);
199
+ if (stream) throw std::runtime_error("A Nemotron stream is already open.");
200
+ language = requested_language;
201
+ sample_rate = requested_rate;
202
+ auto options = nemo_speech_asr_recognition_options_default();
203
+ options.language_code = language.empty() ? nullptr : language.c_str();
204
+ options.interim_results = true;
205
+ options.enable_word_time_offsets = true;
206
+ options.enable_speaker_diarization = !diarization_path.empty();
207
+ options.enable_automatic_punctuation = false;
208
+ options.profanity_filter = false;
209
+ options.verbatim_transcripts = true;
210
+ nemo_speech_asr_stream* raw_stream = nullptr;
211
+ emit_running(request);
212
+ const auto opened = nemo_speech_asr_streaming_recognize(recognizer.get(), &options, &raw_stream);
213
+ stream.reset(raw_stream);
214
+ check(opened);
215
+ sequence = 0;
216
+ } else if (operation == "close") {
217
+ stream.reset();
218
+ } else {
219
+ std::vector<float> audio;
220
+ if (operation == "push") {
221
+ size_t samples = 0;
222
+ if (!(command >> samples)) throw std::runtime_error("Unreadable Nemotron audio frame.");
223
+ audio = read_audio(samples);
224
+ }
225
+ if (!stream) throw std::runtime_error("No Nemotron stream is open.");
226
+ emit_running(request);
227
+ if (operation == "push") {
228
+ check(nemo_speech_asr_stream_push_f32(stream.get(), audio.data(), audio.size(), sample_rate));
229
+ } else if (operation == "endpoint") {
230
+ check(nemo_speech_asr_stream_force_endpoint(stream.get()));
231
+ } else if (operation == "finish") {
232
+ check(nemo_speech_asr_stream_finish(stream.get()));
233
+ } else {
234
+ throw std::runtime_error("Unknown Nemotron operation.");
235
+ }
236
+ drain(request, stream.get(), sequence);
237
+ if (operation == "finish") stream.reset();
238
+ }
239
+ emit_ack(request);
240
+ } catch (const std::exception& error) {
241
+ emit_error(request, error.what());
242
+ if (!std::cin) return 1;
243
+ }
244
+ }
245
+ return 0;
246
+ }
247
+
248
+ int run(const std::vector<std::string>& arguments) {
249
+ try {
250
+ if (arguments.size() != 4) {
251
+ throw std::runtime_error("Usage: arcane-nemotron ASR.gguf DIARIZATION.gguf GPU_ORDINAL (-1 selects CPU).");
252
+ }
253
+ return serve(arguments[1], arguments[2], std::stoi(arguments[3]));
254
+ } catch (const std::exception& error) {
255
+ emit_error(0, error.what());
256
+ return 1;
257
+ }
258
+ }
259
+
260
+ } // namespace
261
+
262
+ #ifdef _WIN32
263
+ int wmain(int argc, wchar_t** argv) {
264
+ std::vector<std::string> arguments;
265
+ for (int index = 0; index < argc; ++index) {
266
+ const int length = WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, nullptr, 0, nullptr, nullptr);
267
+ if (length == 0) {
268
+ emit_error(0, "Windows could not encode a Nemotron argument as UTF-8.");
269
+ return 1;
270
+ }
271
+ std::string argument(length, '\0');
272
+ WideCharToMultiByte(CP_UTF8, 0, argv[index], -1, argument.data(), length, nullptr, nullptr);
273
+ argument.pop_back();
274
+ arguments.push_back(argument);
275
+ }
276
+ return run(arguments);
277
+ }
278
+ #else
279
+ int main(int argc, char** argv) {
280
+ return run(std::vector<std::string>(argv, argv + argc));
281
+ }
282
+ #endif
@@ -7,6 +7,8 @@ import Is from 'strong-type';
7
7
  import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
8
8
  import {runProcess} from '../process.mjs';
9
9
  import {createPortableNativeProvider} from './portable-provider.mjs';
10
+ import {assemblePortable} from './portable-assembly.mjs';
11
+ import {publishNativeOutput} from './publish-output.mjs';
10
12
  import {outputFile, writeOutput} from './portable-layout.mjs';
11
13
 
12
14
  const is = new Is(false);
@@ -224,19 +226,19 @@ export function createMacOSNativeProvider({services, hostDirectory} = {}) {
224
226
  const target = macosTarget(request.targetRequest);
225
227
  const selected = hostDirectory ?? request.toolchain?.hostDirectory;
226
228
  const selectedHost = selected === undefined ? null : await requireHostDirectory(selected);
227
- const artifact = await portable.build({
229
+ const artifact = await assemblePortable({
228
230
  ...request, targetRequest: portableRequest(target),
229
231
  async onEvent(event) {
230
- if (event.type !== 'native.payload.completed') await emit(onEvent, {...event, target: target.target});
232
+ await emit(onEvent, {...event, target: target.target});
231
233
  }
232
- });
234
+ }, {services, packagedWeb: true, webKitDocumentLifecycle: true});
233
235
  const portableRoot = artifact.target.rootDir;
234
236
  let assembly;
235
237
  try {
236
238
  const host = selectedHost ?? await releasedHostDirectory(path.resolve(request.outputRoot), artifact.manifest.sdk.version, target, {signal, onEvent});
237
239
  throwIfAborted(signal);
238
240
  assembly = await mkdtemp(path.join(path.resolve(request.outputRoot), 'arcane-macos-'));
239
- const plist = outputFile(assembly, `${request.appDescriptor.id}.app/Contents/Info.plist`);
241
+ const plist = outputFile(assembly, 'Contents/Info.plist');
240
242
  const contents = path.dirname(plist);
241
243
  const root = path.join(contents, 'Resources');
242
244
  const bundleRoot = path.dirname(contents);
@@ -256,9 +258,14 @@ export function createMacOSNativeProvider({services, hostDirectory} = {}) {
256
258
  };
257
259
  await writeFile(path.join(root, MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`, {signal});
258
260
  throwIfAborted(signal);
259
- await emit(onEvent, {type: 'native.payload.completed', target: target.target, appId: request.appDescriptor.id, outputRoot: root, bundleRoot});
260
- throwIfAborted(signal);
261
- return {...artifact, target: {...target, rootDir: root}, bundleRoot, manifest};
261
+ const publishedBundle = await publishNativeOutput({
262
+ sourceRoot: bundleRoot, outputRoot: path.dirname(portableRoot), appId: request.appDescriptor.id, bundle: true, signal,
263
+ async onPublished(finalBundle) {
264
+ await emit(onEvent, {type: 'native.payload.completed', target: target.target, appId: request.appDescriptor.id,
265
+ outputRoot: path.join(finalBundle, 'Contents', 'Resources'), bundleRoot: finalBundle});
266
+ }
267
+ });
268
+ return {...artifact, target: {...target, rootDir: path.join(publishedBundle, 'Contents', 'Resources')}, bundleRoot: publishedBundle, manifest};
262
269
  } catch (error) {
263
270
  return removeOwnedDirectories(assembly ? [portableRoot, assembly] : [portableRoot], error);
264
271
  }
@@ -0,0 +1,180 @@
1
+ import {mkdir, mkdtemp, readFile, realpath, rm} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import Is from 'strong-type';
4
+ import {createCoreClassicSource} from '../core-classic-source.mjs';
5
+ import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
6
+ import {SDK_ROOT, outputFile, writeOutput, copyRelease, copyCoreRuntime, coreEntrySource} from './portable-layout.mjs';
7
+
8
+ const is = new Is(false);
9
+ const MANIFEST = 'arcane-native.json';
10
+
11
+ async function emit(onEvent, event) {
12
+ if (is.function(onEvent)) await onEvent(event);
13
+ }
14
+
15
+ function contains(root, candidate) {
16
+ const relative = path.relative(root, candidate);
17
+ return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
18
+ }
19
+
20
+ async function futureRealPath(location) {
21
+ try { return await realpath(location); }
22
+ catch (error) {
23
+ if (error.code !== 'ENOENT') throw error;
24
+ const parent = path.dirname(location);
25
+ if (parent === location) throw error;
26
+ return path.join(await futureRealPath(parent), path.basename(location));
27
+ }
28
+ }
29
+
30
+ async function outputDirectory(request) {
31
+ if (!is.string(request.outputRoot) || !request.outputRoot) {
32
+ throw new ArcaneError(ERROR_CODES.usage, 'Portable assembly requires an outputRoot.');
33
+ }
34
+ const output = await futureRealPath(path.resolve(request.outputRoot));
35
+ const protectedRoots = [SDK_ROOT, request.appReleaseRoot,
36
+ ...(request.dependencies?.map(function dependencyRoot(item) { return item.releaseRoot; }) ?? []),
37
+ ...(request.protectedRoots ?? [])];
38
+ for (const location of protectedRoots) {
39
+ if (!location) continue;
40
+ const root = await futureRealPath(path.resolve(location));
41
+ // SDK/release roots must never be replaced by generated output. A
42
+ // caller may select its ordinary project-local dist descendant.
43
+ if (contains(output, root) || (location !== SDK_ROOT && contains(root, output))) {
44
+ throw new ArcaneError(ERROR_CODES.usage, `Portable output overlaps selected source: ${location}.`);
45
+ }
46
+ }
47
+ await mkdir(output, {recursive: true});
48
+ return output;
49
+ }
50
+
51
+ function serviceSelection(services, release) {
52
+ return services.map(function selectService(service) {
53
+ if (!is.string(service?.module) || !release.files.includes(service.module)) {
54
+ throw new ArcaneError(ERROR_CODES.prerequisiteMissing, `The selected release does not contain service module ${String(service?.module)}.`);
55
+ }
56
+ return {...service, options: service.options === undefined ? {} : service.options};
57
+ });
58
+ }
59
+
60
+ function eventOwnerPath(release) {
61
+ const paths = release.files.filter(function sdkEventModule(relative) {
62
+ return relative === 'sdk/event-manager.mjs' || relative.endsWith('/sdk/event-manager.mjs')
63
+ || /^node_modules\/(?:@[^/]+\/)?[^/]+\/browser-runtime\/event-manager\.mjs$/u.test(relative);
64
+ });
65
+ if (paths.length !== 1) {
66
+ throw new ArcaneError(ERROR_CODES.prerequisiteMissing,
67
+ 'The portable app release must select one shared SDK event-manager module.',
68
+ {details: {paths}});
69
+ }
70
+ // Native hosts mount the complete app directory at one local URL origin.
71
+ // An origin-root URL keeps nested navigable documents on the same owner.
72
+ return '/' + paths[0].split('/').map(encodeURIComponent).join('/');
73
+ }
74
+
75
+ /** Shared unfinished payload assembly; the final platform provider publishes it. */
76
+ export async function assemblePortable(request = {}, {services, packagedWeb = false, webKitDocumentLifecycle = false} = {}) {
77
+ const {appDescriptor, release, signal, onEvent} = request;
78
+ throwIfAborted(signal);
79
+ const target = {...request.targetRequest};
80
+ const localAI = appDescriptor.native?.localAI;
81
+ const selectedServices = serviceSelection(services ?? appDescriptor.native?.services ?? [], release);
82
+ const eventOwnerModuleURL = eventOwnerPath(release);
83
+ const output = await outputDirectory(request);
84
+ throwIfAborted(signal);
85
+ // Unique assembly directories preserve earlier outputs and avoid
86
+ // overwriting an app's files or a concurrent packaging operation.
87
+ const root = await mkdtemp(path.join(output, 'arcane-portable-'));
88
+ const files = [];
89
+ try {
90
+ await emit(onEvent, {type: 'native.payload.started', appId: appDescriptor.id, outputRoot: root});
91
+ async function bundleSelectedRuntimes() {
92
+ if (localAI === undefined) return {runtimes: [], files: []};
93
+ const {bundleLocalAIRuntimes} = await import('../local-ai/install.mjs');
94
+ return bundleLocalAIRuntimes({
95
+ runtimes: localAI.runtimes,
96
+ directory: path.join(output, 'local-ai-runtimes'),
97
+ outputRoot: root,
98
+ platform: target.platform === 'windows' ? 'win32' : target.platform === 'macos' ? 'darwin' : target.platform,
99
+ architecture: target.architecture,
100
+ signal,
101
+ onEvent
102
+ });
103
+ }
104
+ // These outputs have separate directories. Join both owners
105
+ // before cleanup so neither can keep writing a removed assembly.
106
+ const preparation = await Promise.allSettled([
107
+ copyRelease({...request, releaseRoot: request.appReleaseRoot}, 'app', root, files, signal),
108
+ bundleSelectedRuntimes()
109
+ ]);
110
+ const failures = preparation.filter(function failedPreparation(result) { return result.status === 'rejected'; });
111
+ if (failures.length === 1) throw failures[0].reason;
112
+ if (failures.length > 1) {
113
+ throw new AggregateError(failures.map(function preparationError(result) { return result.reason; }),
114
+ 'Portable application and local runtime preparation failed.');
115
+ }
116
+ const bundled = preparation[1].value;
117
+ files.push(...bundled.files);
118
+ const dependencies = [];
119
+ for (const [index, dependency] of (request.dependencies ?? []).entries()) {
120
+ const directory = `dependencies/${index}`;
121
+ await copyRelease(dependency, directory, root, files, signal);
122
+ dependencies.push({appId: dependency.appId, root: directory, release: dependency.release});
123
+ }
124
+ const sdk = await copyCoreRuntime(root, files, signal);
125
+ const selectedSdk = request.selectedSdk;
126
+ let appSdk;
127
+ if (selectedSdk && release.files.some(function selectedPackageFile(relative) {
128
+ return relative.startsWith(`${selectedSdk.packageSource}/`);
129
+ })) {
130
+ const destination = `app/${selectedSdk.packageSource}`;
131
+ let metadata;
132
+ if (release.files.includes(`${selectedSdk.packageSource}/package.json`)) {
133
+ metadata = JSON.parse(await readFile(outputFile(root, `${destination}/package.json`), 'utf8'));
134
+ } else {
135
+ metadata = await copyCoreRuntime(root, files, signal, {
136
+ sourceRoot: selectedSdk.packageRoot, sdkDestination: destination
137
+ });
138
+ }
139
+ appSdk = {name: metadata.name, version: metadata.version, root: destination};
140
+ }
141
+ await writeOutput(root, 'package.json', '{"type":"module"}\n', files, signal);
142
+ await writeOutput(root, 'runtime/arcane-core.mjs', coreEntrySource(appDescriptor, sdk.version, selectedServices,
143
+ {localAI, runtimes: bundled.runtimes, packagedWeb}), files, signal);
144
+ await writeOutput(root, 'runtime/arcane-api.js', await createCoreClassicSource({
145
+ eventOwnerModuleURL, replayRuntimeState: true, webKitDocumentLifecycle
146
+ }), files, signal);
147
+ const manifest = {
148
+ schemaVersion: 1,
149
+ kind: 'arcane-portable-native',
150
+ sdk: {name: sdk.name, version: sdk.version},
151
+ ...(appSdk ? {appSdk} : {}),
152
+ app: appDescriptor,
153
+ target,
154
+ webRoot: 'app',
155
+ start: release.manifest?.app?.start ?? './index.html',
156
+ ...(appDescriptor.native?.window === undefined ? {} : {window: {...appDescriptor.native.window}}),
157
+ ...(appDescriptor.native?.launchContext === undefined ? {} : {launchContext: appDescriptor.native.launchContext}),
158
+ core: {
159
+ entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
160
+ ...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
161
+ },
162
+ client: {
163
+ source: 'runtime/arcane-api.js', injection: 'document-created', eventOwnerModuleURL, replayRuntimeState: true,
164
+ ...(webKitDocumentLifecycle ? {webKitDocumentLifecycle: true} : {})
165
+ },
166
+ dependencies,
167
+ minimumCoreVersion: request.minimumCoreVersion ?? null,
168
+ files: [...files, MANIFEST]
169
+ };
170
+ await writeOutput(root, MANIFEST, JSON.stringify(manifest, null, 2) + '\n', files, signal);
171
+ throwIfAborted(signal);
172
+ return {app: appDescriptor, target: {...target, rootDir: root}, manifest};
173
+ } catch (error) {
174
+ try { await rm(root, {recursive: true, force: true}); }
175
+ catch (cleanupError) {
176
+ throw new AggregateError([error, cleanupError], `Portable assembly failed; its incomplete output remains at ${root}.`);
177
+ }
178
+ throw error;
179
+ }
180
+ }