@supernovae-st/nika 0.71.0 → 0.118.7

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.
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/nika.ts
4
+ import { spawn as spawn2 } from "child_process";
5
+
6
+ // src/errors.ts
7
+ var NikaError = class extends Error {
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = "NikaError";
11
+ }
12
+ };
13
+ var NikaConfigurationError = class extends NikaError {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = "NikaConfigurationError";
17
+ }
18
+ };
19
+ var NikaTransportError = class extends NikaError {
20
+ transport;
21
+ constructor(transport, message, options) {
22
+ super(message, options);
23
+ this.name = "NikaTransportError";
24
+ this.transport = transport;
25
+ }
26
+ };
27
+ var NikaCompatibilityError = class extends NikaError {
28
+ capability;
29
+ transport;
30
+ constructor(capability, transport, message) {
31
+ super(message);
32
+ this.name = "NikaCompatibilityError";
33
+ this.capability = capability;
34
+ this.transport = transport;
35
+ }
36
+ };
37
+ var NikaProtocolError = class extends NikaTransportError {
38
+ constructor(transport, message, options) {
39
+ super(transport, message, options);
40
+ this.name = "NikaProtocolError";
41
+ }
42
+ };
43
+
44
+ // src/lib/binary/error.ts
45
+ var NikaEngineUnavailable = class extends NikaError {
46
+ code = "NIKA_ENGINE_UNAVAILABLE";
47
+ platform;
48
+ arch;
49
+ packageName;
50
+ constructor(platform, arch, packageName) {
51
+ const target = `${platform}-${arch}`;
52
+ const detail = packageName ? `the optional payload ${packageName} is not installed (npm leaves it UNMET OPTIONAL when the registry has no version matching this client)` : "there is no packaged payload for this host";
53
+ super(
54
+ `Nika engine unavailable for ${target}: ${detail}. A nika found on PATH is deliberately not used. Set NIKA_BIN=/absolute/path/to/nika, pass config.bin, or install the matching payload package.`
55
+ );
56
+ this.name = "NikaEngineUnavailable";
57
+ this.platform = platform;
58
+ this.arch = arch;
59
+ this.packageName = packageName;
60
+ }
61
+ };
62
+
63
+ // src/lib/binary/packages.ts
64
+ var NATIVE_PACKAGES = {
65
+ "darwin-arm64": "@supernovae-st/nika-darwin-arm64",
66
+ "darwin-x64": "@supernovae-st/nika-darwin-x64",
67
+ "linux-x64": "@supernovae-st/nika-linux-x64",
68
+ "linux-arm64": "@supernovae-st/nika-linux-arm64"
69
+ };
70
+ function packageForHost(platform, arch, glibc) {
71
+ if (platform === "linux" && !glibc) return void 0;
72
+ return NATIVE_PACKAGES[`${platform}-${arch}`];
73
+ }
74
+
75
+ // src/lib/binary/resolve.ts
76
+ import { createRequire } from "module";
77
+ import path from "path";
78
+ var requireFromBundle = createRequire(
79
+ typeof import.meta.url === "string" ? import.meta.url : __filename
80
+ );
81
+ function resolveNikaEngine(configuredBin, host = {}) {
82
+ if (configuredBin !== void 0) {
83
+ return { bin: absoluteEnginePath(configuredBin, "bin") };
84
+ }
85
+ const envBin = (host.env ?? process.env).NIKA_BIN;
86
+ if (envBin !== void 0) {
87
+ return { bin: absoluteEnginePath(envBin, "NIKA_BIN") };
88
+ }
89
+ const platform = host.platform ?? process.platform;
90
+ const arch = host.arch ?? process.arch;
91
+ const packageName = packageForHost(
92
+ platform,
93
+ arch,
94
+ host.glibc ?? runtimeIsGlibc(platform)
95
+ );
96
+ if (!packageName) throw new NikaEngineUnavailable(platform, arch);
97
+ try {
98
+ const packageJson = (host.resolvePackageJson ?? requireFromBundle.resolve)(
99
+ `${packageName}/package.json`
100
+ );
101
+ const packageRoot = path.dirname(packageJson);
102
+ return {
103
+ bin: path.join(packageRoot, "bin", "nika"),
104
+ packageName,
105
+ packageRoot
106
+ };
107
+ } catch (cause) {
108
+ if (isMissingModule(cause)) {
109
+ throw new NikaEngineUnavailable(platform, arch, packageName);
110
+ }
111
+ throw cause;
112
+ }
113
+ }
114
+ function absoluteEnginePath(value, source) {
115
+ if (value.length === 0) {
116
+ throw new NikaConfigurationError(`${source} must be a non-empty string`);
117
+ }
118
+ if (!path.isAbsolute(value)) {
119
+ throw new NikaConfigurationError(
120
+ `${source} must be an absolute path to a nika engine (got "${value}"); a bare name or a relative path would be resolved through PATH or the working directory, which this client refuses`
121
+ );
122
+ }
123
+ return value;
124
+ }
125
+ function runtimeIsGlibc(platform) {
126
+ if (platform !== "linux") return true;
127
+ const report = process.report?.getReport();
128
+ return typeof report?.header?.glibcVersionRuntime === "string";
129
+ }
130
+ function isMissingModule(cause) {
131
+ return cause instanceof Error && "code" in cause && cause.code === "MODULE_NOT_FOUND";
132
+ }
133
+
134
+ // src/lib/binary/verify.ts
135
+ import { createHash } from "crypto";
136
+ import { createReadStream } from "fs";
137
+ import { readFile, stat } from "fs/promises";
138
+ import path2 from "path";
139
+
140
+ // src/lib/engine-capture.ts
141
+ import { spawn } from "child_process";
142
+ function captureEngine(bin, args, options) {
143
+ return new Promise((resolve, reject) => {
144
+ if (options.signal?.aborted) {
145
+ reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
146
+ return;
147
+ }
148
+ const child = spawn(bin, args, {
149
+ cwd: options.cwd,
150
+ shell: false,
151
+ stdio: ["ignore", "pipe", "pipe"]
152
+ });
153
+ let stdout = "";
154
+ let stderr = "";
155
+ let overflow = false;
156
+ let spawnError;
157
+ const append = (stream, chunk) => {
158
+ if (overflow) return;
159
+ if (stream === "stdout") stdout += chunk;
160
+ else stderr += chunk;
161
+ if (Buffer.byteLength(stdout) > options.bufferBytes || Buffer.byteLength(stderr) > options.bufferBytes) {
162
+ overflow = true;
163
+ child.kill("SIGTERM");
164
+ }
165
+ };
166
+ child.stdout.setEncoding("utf8");
167
+ child.stderr.setEncoding("utf8");
168
+ child.stdout.on("data", (chunk) => append("stdout", chunk));
169
+ child.stderr.on("data", (chunk) => append("stderr", chunk));
170
+ const abort = () => child.kill("SIGTERM");
171
+ options.signal?.addEventListener("abort", abort, { once: true });
172
+ child.once("error", (cause) => {
173
+ spawnError = cause;
174
+ });
175
+ child.once("close", (code) => {
176
+ options.signal?.removeEventListener("abort", abort);
177
+ if (options.signal?.aborted) {
178
+ reject(new NikaTransportError(options.transport, `${options.label} aborted by caller`));
179
+ } else if (spawnError) {
180
+ reject(new NikaTransportError(
181
+ options.transport,
182
+ `Cannot spawn ${bin} for ${options.label}: ${spawnError.message}`,
183
+ { cause: spawnError }
184
+ ));
185
+ } else if (overflow) {
186
+ reject(new NikaProtocolError(
187
+ options.transport,
188
+ `${options.label} exceeded ${options.bufferBytes} bytes`
189
+ ));
190
+ } else {
191
+ resolve({ exitCode: code ?? 3, stdout, stderr });
192
+ }
193
+ });
194
+ });
195
+ }
196
+
197
+ // src/lib/machine.ts
198
+ function machineObject(value) {
199
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
200
+ }
201
+
202
+ // src/lib/engine-identity.ts
203
+ var MACHINE_PROTOCOL_VERSION = 1;
204
+ var REQUIRED_CAPABILITIES = ["check", "executionSnapshot", "eventStream"];
205
+ var COMPATIBILITY_CLOCKS = [
206
+ "machineProtocolVersion",
207
+ "snapshotFormatVersion",
208
+ "checkReportVersion",
209
+ "eventFormatVersion"
210
+ ];
211
+ function compatibleEngineIdentity(value, transport, peer) {
212
+ const identity = machineObject(value);
213
+ if (!identity) throw incompatible(transport, "Engine identity is not an object");
214
+ if (typeof identity.engineVersion !== "string" || identity.engineVersion.length === 0) {
215
+ throw incompatible(transport, "Engine identity is missing engineVersion");
216
+ }
217
+ for (const clock of COMPATIBILITY_CLOCKS) {
218
+ if (!Number.isSafeInteger(identity[clock])) {
219
+ throw incompatible(transport, `Engine identity is missing ${clock}`);
220
+ }
221
+ }
222
+ if (identity.machineProtocolVersion !== MACHINE_PROTOCOL_VERSION) {
223
+ throw incompatible(
224
+ transport,
225
+ `Engine machine protocol ${String(identity.machineProtocolVersion)} is incompatible with SDK protocol ${MACHINE_PROTOCOL_VERSION}`
226
+ );
227
+ }
228
+ if (!Array.isArray(identity.supportedCapabilities) || !identity.supportedCapabilities.every((item) => typeof item === "string")) {
229
+ throw incompatible(transport, "Engine identity is missing supportedCapabilities");
230
+ }
231
+ for (const capability of REQUIRED_CAPABILITIES) {
232
+ if (!identity.supportedCapabilities.includes(capability)) {
233
+ throw incompatible(transport, `Engine identity does not support ${capability}`);
234
+ }
235
+ }
236
+ const checked = identity;
237
+ if (peer) {
238
+ for (const clock of COMPATIBILITY_CLOCKS) {
239
+ if (checked[clock] !== peer[clock]) {
240
+ throw incompatible(
241
+ transport,
242
+ `Remote ${clock} ${checked[clock]} is incompatible with local ${clock} ${peer[clock]}`
243
+ );
244
+ }
245
+ }
246
+ }
247
+ return checked;
248
+ }
249
+ function incompatible(transport, message) {
250
+ return new NikaCompatibilityError("engineIdentity", transport, message);
251
+ }
252
+
253
+ // src/lib/binary/verify.ts
254
+ var IDENTITY_BUFFER_BYTES = 16 * 1024;
255
+ async function verifyNikaEngine(engine) {
256
+ const expectedVersion = engine.packageRoot ? await verifyManagedPayload(engine) : void 0;
257
+ const identity = await probeIdentity(engine.bin);
258
+ if (expectedVersion !== void 0 && identity.engineVersion !== expectedVersion) {
259
+ throw incompatible2(
260
+ `Packaged engine version ${identity.engineVersion} does not match payload version ${expectedVersion}`
261
+ );
262
+ }
263
+ return identity;
264
+ }
265
+ async function verifyManagedPayload(engine) {
266
+ const packageRoot = engine.packageRoot;
267
+ if (!packageRoot || !engine.packageName) {
268
+ throw incompatible2("Managed engine resolution is missing package identity");
269
+ }
270
+ const manifest = await readJson(path2.join(packageRoot, "package.json"), "payload manifest");
271
+ if (manifest.name !== engine.packageName || typeof manifest.version !== "string") {
272
+ throw incompatible2(`Invalid manifest for ${engine.packageName}`);
273
+ }
274
+ const integrity = await readJson(
275
+ path2.join(packageRoot, "INTEGRITY.json"),
276
+ "payload integrity metadata"
277
+ );
278
+ if (integrity.algorithm !== "sha256" || integrity.executable?.file !== "bin/nika" || !isSha256(integrity.executable?.sha256)) {
279
+ throw incompatible2(`Invalid INTEGRITY.json for ${engine.packageName}`);
280
+ }
281
+ let metadata;
282
+ try {
283
+ metadata = await stat(engine.bin);
284
+ } catch (cause) {
285
+ throw incompatible2(`Cannot read packaged engine ${engine.bin}`, cause);
286
+ }
287
+ if (!metadata.isFile()) {
288
+ throw incompatible2(`Packaged engine is not a file: ${engine.bin}`);
289
+ }
290
+ const actual = await sha256(engine.bin);
291
+ if (actual !== integrity.executable.sha256) {
292
+ throw incompatible2(`Packaged engine checksum mismatch for ${engine.packageName}`);
293
+ }
294
+ return manifest.version;
295
+ }
296
+ async function probeIdentity(bin) {
297
+ const notAnEngine = `is ${bin} a nika engine? (run "${bin} --sdk-identity" by hand)`;
298
+ const captured = await captureEngine(bin, ["--sdk-identity"], {
299
+ bufferBytes: IDENTITY_BUFFER_BYTES,
300
+ transport: "native-process",
301
+ label: "Engine identity probe"
302
+ }).catch((cause) => {
303
+ throw incompatible2(
304
+ `Engine identity probe of ${bin} failed`,
305
+ cause
306
+ );
307
+ });
308
+ if (captured.exitCode !== 0) {
309
+ throw incompatible2(
310
+ `Engine identity probe of ${bin} exited with code ${captured.exitCode}; ${notAnEngine}`
311
+ );
312
+ }
313
+ let value;
314
+ try {
315
+ value = JSON.parse(captured.stdout.trim());
316
+ } catch (cause) {
317
+ throw incompatible2(
318
+ `Engine identity probe of ${bin} did not emit one JSON object; ${notAnEngine}`,
319
+ cause
320
+ );
321
+ }
322
+ if (captured.stderr.trim()) {
323
+ throw incompatible2(`Engine identity probe of ${bin} wrote unexpected diagnostics`);
324
+ }
325
+ try {
326
+ return compatibleEngineIdentity(value, "native-process");
327
+ } catch (cause) {
328
+ if (cause instanceof NikaCompatibilityError) throw cause;
329
+ throw incompatible2("Engine identity probe was incompatible", cause);
330
+ }
331
+ }
332
+ async function readJson(file, label) {
333
+ try {
334
+ const parsed = JSON.parse(await readFile(file, "utf8"));
335
+ if (!isRecord(parsed)) throw new TypeError(`${label} is not an object`);
336
+ return parsed;
337
+ } catch (cause) {
338
+ throw incompatible2(`Cannot verify ${label} at ${file}`, cause);
339
+ }
340
+ }
341
+ function sha256(file) {
342
+ return new Promise((resolve, reject) => {
343
+ const hash = createHash("sha256");
344
+ const stream = createReadStream(file);
345
+ stream.on("data", (chunk) => hash.update(chunk));
346
+ stream.once("error", reject);
347
+ stream.once("end", () => resolve(hash.digest("hex")));
348
+ });
349
+ }
350
+ function isRecord(value) {
351
+ return typeof value === "object" && value !== null && !Array.isArray(value);
352
+ }
353
+ function isSha256(value) {
354
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
355
+ }
356
+ function incompatible2(message, cause) {
357
+ return new NikaCompatibilityError(
358
+ "engineIdentity",
359
+ "native-process",
360
+ cause instanceof Error ? `${message}: ${cause.message}` : message
361
+ );
362
+ }
363
+
364
+ // src/bin/nika.ts
365
+ var FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
366
+ async function main(argv = process.argv.slice(2)) {
367
+ let engine;
368
+ try {
369
+ engine = resolveNikaEngine();
370
+ await verifyNikaEngine(engine);
371
+ } catch (cause) {
372
+ const message = cause instanceof NikaEngineUnavailable || cause instanceof Error ? cause.message : String(cause);
373
+ console.error(`nika: ${message}`);
374
+ process.exitCode = 1;
375
+ return;
376
+ }
377
+ const child = spawn2(engine.bin, argv, {
378
+ shell: false,
379
+ stdio: "inherit"
380
+ });
381
+ const handlers = /* @__PURE__ */ new Map();
382
+ for (const signal of FORWARDED_SIGNALS) {
383
+ const handler = () => child.kill(signal);
384
+ handlers.set(signal, handler);
385
+ process.on(signal, handler);
386
+ }
387
+ const removeHandlers = () => {
388
+ for (const [signal, handler] of handlers) process.off(signal, handler);
389
+ };
390
+ child.once("error", (cause) => {
391
+ removeHandlers();
392
+ console.error(`nika: Cannot spawn ${engine.bin}: ${cause.message}`);
393
+ process.exitCode = 1;
394
+ });
395
+ child.once("exit", (code, signal) => {
396
+ removeHandlers();
397
+ if (signal) {
398
+ process.kill(process.pid, signal);
399
+ return;
400
+ }
401
+ process.exitCode = code ?? 1;
402
+ });
403
+ }
404
+ await main();
405
+ export {
406
+ main
407
+ };