@lifesi/genesis-cli 0.1.0 → 0.1.1
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/dist/dials.js +16 -1
- package/dist/function-input.d.ts +3 -0
- package/dist/function-input.js +49 -0
- package/dist/hives-node-transport.d.ts +6 -0
- package/dist/hives-node-transport.js +145 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +217 -9
- package/dist/secret-input.d.ts +14 -0
- package/dist/secret-input.js +85 -0
- package/dist/wasm-input.d.ts +2 -0
- package/dist/wasm-input.js +31 -0
- package/package.json +3 -2
package/dist/dials.js
CHANGED
|
@@ -13,11 +13,26 @@ export function registerDials(program, dependencies) {
|
|
|
13
13
|
const emit = (data) => dependencies.emit(data, () => JSON.stringify(data, null, 2));
|
|
14
14
|
const numbers = dials.command('numbers').description('Carrier number inventory');
|
|
15
15
|
numbers.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.list(scope)); });
|
|
16
|
-
numbers.command('search').option('--area-code <code>', 'three digit area code').
|
|
16
|
+
numbers.command('search').option('--area-code <code>', 'three digit area code').option('--filters <json-or-@file>', 'carrier search filters, including capabilities and rate center').action(async (options) => {
|
|
17
|
+
const { client, scope } = await context();
|
|
18
|
+
const text = options.filters?.startsWith('@') ? await readFile(options.filters.slice(1), 'utf8') : options.filters;
|
|
19
|
+
const filters = text ? JSON.parse(text) : {};
|
|
20
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters))
|
|
21
|
+
throw new Error('Search filters must be a JSON object.');
|
|
22
|
+
const allowed = new Set(['areaCode', 'exchangeCode', 'localCallingArea', 'contains', 'startsWith', 'endsWith', 'repeatingDigit', 'repeatingDigitCount', 'region', 'locality', 'postalCode', 'rateCenter', 'lata', 'nearNumber', 'nearLatLong', 'distance', 'excludeAddressRequired', 'excludeLocalAddressRequired', 'excludeForeignAddressRequired', 'beta', 'voice', 'sms', 'mms', 'fax', 'numberType', 'limit']);
|
|
23
|
+
if (Object.keys(filters).some(key => !allowed.has(key)))
|
|
24
|
+
throw new Error('Unsupported Dials search filter. Scope is selected by the command context.');
|
|
25
|
+
emit(await client.dials.numbers.search(scope, { ...filters, ...(options.areaCode ? { areaCode: options.areaCode } : {}) }));
|
|
26
|
+
});
|
|
27
|
+
numbers.command('reconciliations').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.reconciliations(scope)); });
|
|
28
|
+
numbers.command('drift').action(async () => { const { client, scope } = await context(); emit(await client.dials.numbers.drift(scope)); });
|
|
29
|
+
dials.command('overview').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.summary(scope)); });
|
|
30
|
+
dials.command('operating-surface').action(async () => { const { client, scope } = await context(); emit(await client.dials.overview.operatingSurface(scope)); });
|
|
17
31
|
const calls = dials.command('calls').description('Call activity and approved outbound calls');
|
|
18
32
|
calls.command('list', { isDefault: true }).action(async () => { const { client, scope } = await context(); emit(await client.dials.calls.list(scope)); });
|
|
19
33
|
// @file keeps structured approval and idempotency fields out of shell history.
|
|
20
34
|
for (const [parent, name, operation] of [
|
|
35
|
+
[numbers, 'caller-name', 'dials_updateNumberCallerName'], [numbers, 'reconcile', 'dials_reconcileNumberInventory'],
|
|
21
36
|
[numbers, 'provision', 'dials_provisionNumber'], [numbers, 'assign', 'dials_assignNumber'], [numbers, 'release', 'dials_releaseNumber'], [numbers, 'e911', 'dials_registerEmergencyAddress'],
|
|
22
37
|
[calls, 'validate', 'dials_validateOutboundCall'], [calls, 'create', 'dials_createOutboundCall'],
|
|
23
38
|
]) {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function readFunctionFile(path: string, limit: number): Promise<Uint8Array>;
|
|
2
|
+
export declare function readFunctionJson(path: string, limit: number): Promise<Record<string, unknown>>;
|
|
3
|
+
export declare function writeFunctionOutput(path: string, bytes: Uint8Array): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { open, writeFile } from 'node:fs/promises';
|
|
2
|
+
export async function readFunctionFile(path, limit) {
|
|
3
|
+
let file;
|
|
4
|
+
try {
|
|
5
|
+
file = await open(path, 'r');
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error('Could not open the function input file. Check its path and permissions.');
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const stat = await file.stat();
|
|
12
|
+
if (!stat.isFile() || stat.size > limit)
|
|
13
|
+
throw new Error('Choose a regular function input file within the documented size limit.');
|
|
14
|
+
const bytes = new Uint8Array(limit + 1);
|
|
15
|
+
let size = 0;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const { bytesRead } = await file.read(bytes, size, bytes.length - size, null);
|
|
18
|
+
if (!bytesRead)
|
|
19
|
+
break;
|
|
20
|
+
size += bytesRead;
|
|
21
|
+
if (size > limit)
|
|
22
|
+
throw new Error('Function input file exceeds the documented size limit.');
|
|
23
|
+
}
|
|
24
|
+
return bytes.slice(0, size);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await file.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function readFunctionJson(path, limit) {
|
|
31
|
+
const bytes = await readFunctionFile(path, limit);
|
|
32
|
+
try {
|
|
33
|
+
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
35
|
+
throw new Error();
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error('Function request file must contain a JSON object. No input value is echoed.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function writeFunctionOutput(path, bytes) {
|
|
43
|
+
try {
|
|
44
|
+
await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error('Could not create the output file. Choose a new path with write permission. Existing files are preserved.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type GenesisOptions } from '@lifesi/genesis';
|
|
2
|
+
export declare function closeHivesNodeTransport(): Promise<void>;
|
|
3
|
+
export declare function hivesOperationBudget(id: string): number;
|
|
4
|
+
/** Hives leaf transport. Caller fetch overrides stay authoritative; only native
|
|
5
|
+
* global fetch gets our Agent. Every operation retains an overall body deadline. */
|
|
6
|
+
export declare function createHivesNodeTransport(opts: GenesisOptions, callerSignal?: AbortSignal): typeof fetch;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Agent } from 'undici';
|
|
2
|
+
import { Genesis, operationTable } from '@lifesi/genesis';
|
|
3
|
+
// Node's native fetch accepts a compatible per-request dispatcher. Never change
|
|
4
|
+
// the global dispatcher or the portable SDK: other products retain their transport.
|
|
5
|
+
const agentOptions = { headersTimeout: 340_000, bodyTimeout: 340_000, connect: { timeout: 5_000 }, connections: 4, pipelining: 1 };
|
|
6
|
+
let agent;
|
|
7
|
+
const nativeAgent = () => agent ??= new Agent(agentOptions);
|
|
8
|
+
export async function closeHivesNodeTransport() {
|
|
9
|
+
const current = agent;
|
|
10
|
+
agent = undefined;
|
|
11
|
+
await current?.destroy();
|
|
12
|
+
}
|
|
13
|
+
export function hivesOperationBudget(id) {
|
|
14
|
+
if (id === 'hives_cancel_deployment' || id === 'hives_delete_deployment')
|
|
15
|
+
return 330_000;
|
|
16
|
+
if (['hives_create_deployment', 'hives_upload_deployment', 'hives_invoke_deployment', 'hives_invoke_function', 'hives_invoke_function_async'].includes(id))
|
|
17
|
+
return 40_000;
|
|
18
|
+
if (['hives_create_function', 'hives_update_function', 'hives_delete_function'].includes(id))
|
|
19
|
+
return 100_000;
|
|
20
|
+
return 20_000;
|
|
21
|
+
}
|
|
22
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
23
|
+
const templatePattern = (value) => new RegExp('^' + value.split(/(\{[^}]+\})/).map((part) => part.startsWith('{') ? '[^/]+' : escape(part)).join('') + '$');
|
|
24
|
+
/** Hives leaf transport. Caller fetch overrides stay authoritative; only native
|
|
25
|
+
* global fetch gets our Agent. Every operation retains an overall body deadline. */
|
|
26
|
+
export function createHivesNodeTransport(opts, callerSignal) {
|
|
27
|
+
const supplied = opts.fetch ?? globalThis.fetch;
|
|
28
|
+
const underlying = supplied.bind(globalThis);
|
|
29
|
+
const useNative = supplied === globalThis.fetch;
|
|
30
|
+
const resolver = new Genesis(opts);
|
|
31
|
+
const routes = Object.entries(operationTable)
|
|
32
|
+
.filter(([, op]) => op.service === 'hives' && !op.internal)
|
|
33
|
+
.flatMap(([id, op]) => {
|
|
34
|
+
const direct = resolver.routing === 'direct';
|
|
35
|
+
let base;
|
|
36
|
+
try {
|
|
37
|
+
base = new URL(direct ? (opts.upstreams?.hives ?? op.upstream.base) : resolver.baseUrl);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error('Invalid Hives service address.');
|
|
41
|
+
}
|
|
42
|
+
const path = direct ? op.upstream.path : op.path;
|
|
43
|
+
const prefix = base.pathname.replace(/\/+$/, '');
|
|
44
|
+
const paths = [path];
|
|
45
|
+
if (!direct && path.startsWith('/v1/hives/'))
|
|
46
|
+
paths.push(path.replace('/v1/hives/', '/hives/v1/'));
|
|
47
|
+
return paths.map((p) => ({ id, origin: base.origin, method: direct ? op.upstream.method : op.method, pattern: templatePattern(prefix + p) }));
|
|
48
|
+
});
|
|
49
|
+
return (async (input, init = {}) => {
|
|
50
|
+
let url;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(input instanceof Request ? input.url : String(input));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error('Invalid Hives request address.');
|
|
56
|
+
}
|
|
57
|
+
const method = (init.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
58
|
+
const route = routes.find((entry) => entry.origin === url.origin && entry.method === method && entry.pattern.test(url.pathname));
|
|
59
|
+
if (!route)
|
|
60
|
+
return underlying(input, init);
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
let message = 'Hives transport failed. Refresh the resource and retry.';
|
|
63
|
+
const safeError = () => new Error(message);
|
|
64
|
+
const signals = [...new Set([callerSignal, init.signal, input instanceof Request ? input.signal : undefined].filter((signal) => !!signal))];
|
|
65
|
+
const canceled = () => {
|
|
66
|
+
message = 'Hives request canceled before completion. Refresh the resource before retrying.';
|
|
67
|
+
controller.abort();
|
|
68
|
+
};
|
|
69
|
+
for (const signal of signals)
|
|
70
|
+
signal.addEventListener('abort', canceled, { once: true });
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
message = 'Hives request deadline exceeded. Refresh the resource before retrying.';
|
|
73
|
+
controller.abort();
|
|
74
|
+
}, hivesOperationBudget(route.id));
|
|
75
|
+
timer.unref();
|
|
76
|
+
let abortReject = () => { };
|
|
77
|
+
let reader;
|
|
78
|
+
let bodyController;
|
|
79
|
+
const aborted = () => {
|
|
80
|
+
abortReject(safeError());
|
|
81
|
+
bodyController?.error(safeError());
|
|
82
|
+
void reader?.cancel().catch(() => { });
|
|
83
|
+
cleanup();
|
|
84
|
+
};
|
|
85
|
+
controller.signal.addEventListener('abort', aborted, { once: true });
|
|
86
|
+
const abortPromise = new Promise((_, reject) => { abortReject = reject; });
|
|
87
|
+
void abortPromise.catch(() => { });
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
for (const signal of signals)
|
|
91
|
+
signal.removeEventListener('abort', canceled);
|
|
92
|
+
controller.signal.removeEventListener('abort', aborted);
|
|
93
|
+
};
|
|
94
|
+
if (signals.some((signal) => signal.aborted))
|
|
95
|
+
canceled();
|
|
96
|
+
try {
|
|
97
|
+
if (controller.signal.aborted)
|
|
98
|
+
throw safeError();
|
|
99
|
+
const requestInit = {
|
|
100
|
+
...init, signal: controller.signal, cache: 'no-store',
|
|
101
|
+
redirect: init.redirect === 'manual' ? 'manual' : 'error',
|
|
102
|
+
...(useNative ? { dispatcher: nativeAgent() } : {}),
|
|
103
|
+
};
|
|
104
|
+
const pending = underlying(input, requestInit);
|
|
105
|
+
// A caller transport may ignore AbortSignal. Reject on our budget anyway,
|
|
106
|
+
// and discard any late response rather than presenting an acknowledgement.
|
|
107
|
+
void pending.then((response) => { if (controller.signal.aborted)
|
|
108
|
+
void response.body?.cancel().catch(() => { }); }, () => { });
|
|
109
|
+
const response = await Promise.race([pending, abortPromise]);
|
|
110
|
+
if (controller.signal.aborted)
|
|
111
|
+
throw safeError();
|
|
112
|
+
if (!response.body) {
|
|
113
|
+
cleanup();
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
reader = response.body.getReader();
|
|
117
|
+
const body = new ReadableStream({
|
|
118
|
+
start(value) { bodyController = value; },
|
|
119
|
+
async pull(value) {
|
|
120
|
+
try {
|
|
121
|
+
const next = await reader.read();
|
|
122
|
+
if (controller.signal.aborted)
|
|
123
|
+
return;
|
|
124
|
+
if (next.done) {
|
|
125
|
+
cleanup();
|
|
126
|
+
value.close();
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
value.enqueue(next.value);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
cleanup();
|
|
133
|
+
value.error(safeError());
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async cancel() { cleanup(); await reader.cancel().catch(() => { }); },
|
|
137
|
+
});
|
|
138
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
cleanup();
|
|
142
|
+
throw safeError();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type CredentialStore } from './store.js';
|
|
3
3
|
import { type Out } from './output.js';
|
|
4
|
+
import { type SecretInputOptions } from './secret-input.js';
|
|
4
5
|
export interface Deps {
|
|
5
6
|
out?: Out;
|
|
6
7
|
store?: () => Promise<CredentialStore>;
|
|
7
8
|
fetch?: typeof fetch;
|
|
9
|
+
/** Cancel Hives calls through response-body completion; other products retain their transport. */
|
|
10
|
+
signal?: AbortSignal;
|
|
8
11
|
env?: Record<string, string | undefined>;
|
|
9
12
|
openBrowser?: (url: string) => void;
|
|
10
13
|
sleep?: (ms: number) => Promise<void>;
|
|
14
|
+
readSecretInput?: (options: SecretInputOptions, limit: number) => Promise<string>;
|
|
15
|
+
readWasmFile?: (path: string) => Promise<Blob>;
|
|
16
|
+
writeBytes?: (bytes: Uint8Array) => void;
|
|
17
|
+
writeOutputFile?: (path: string, bytes: Uint8Array) => Promise<void>;
|
|
11
18
|
}
|
|
12
19
|
export declare function buildProgram(deps?: Deps): Command;
|
|
13
20
|
export declare function main(argv: string[], deps?: Deps): Promise<number>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerBeacons } from './beacons.js';
|
|
2
2
|
import { registerDials } from './dials.js';
|
|
3
3
|
import { Command, CommanderError } from 'commander';
|
|
4
|
-
import { Genesis, GenesisError, services } from '@lifesi/genesis';
|
|
4
|
+
import { Genesis, GenesisError, hivesResponseBytes, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES, services } from '@lifesi/genesis';
|
|
5
5
|
import { authConfig, pollDevice, refresh, savedLoginProvider, startDevice } from './auth.js';
|
|
6
6
|
import { keychainStore } from './store.js';
|
|
7
7
|
import { c, listOf, stdio, table } from './output.js';
|
|
@@ -9,6 +9,11 @@ import { spawn } from 'node:child_process';
|
|
|
9
9
|
import { addHenryCommands } from './henry.js';
|
|
10
10
|
import { registerFleets } from './fleets.js';
|
|
11
11
|
import { registerRunner } from './runner.js';
|
|
12
|
+
import { createHivesNodeTransport } from './hives-node-transport.js';
|
|
13
|
+
import { readFunctionFile, readFunctionJson, writeFunctionOutput } from './function-input.js';
|
|
14
|
+
import { basename } from 'node:path';
|
|
15
|
+
import { readWasmFile } from './wasm-input.js';
|
|
16
|
+
import { readSecretInput, validateSecret, validateSecretValue, parseSecretBatch } from './secret-input.js';
|
|
12
17
|
function defaultOpen(url) {
|
|
13
18
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
14
19
|
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
@@ -25,7 +30,7 @@ export function buildProgram(deps = {}) {
|
|
|
25
30
|
const auth = { ...authConfig(env, f), sleep: deps.sleep };
|
|
26
31
|
const program = new Command('genesis')
|
|
27
32
|
.description('One CLI for every Genesis service.')
|
|
28
|
-
.version('0.1.
|
|
33
|
+
.version('0.1.1')
|
|
29
34
|
.option('--json', 'machine-readable JSON output')
|
|
30
35
|
.option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
|
|
31
36
|
.option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://console.life.one/api)')
|
|
@@ -35,18 +40,32 @@ export function buildProgram(deps = {}) {
|
|
|
35
40
|
.exitOverride()
|
|
36
41
|
.configureOutput({ writeOut: (s) => out.log(s.trimEnd()), writeErr: (s) => out.err(s.trimEnd()) });
|
|
37
42
|
const g = () => program.opts();
|
|
38
|
-
const client = async () => {
|
|
43
|
+
const client = async (hivesTransport = false) => {
|
|
39
44
|
const o = g();
|
|
40
45
|
const store = await getStore().catch(() => undefined);
|
|
41
46
|
const opts = {
|
|
42
47
|
apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
|
|
43
48
|
baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
|
|
44
|
-
fetch: f, userAgent: 'genesis-cli/0.1.
|
|
49
|
+
fetch: f, userAgent: 'genesis-cli/0.1.1',
|
|
45
50
|
credentials: store ? savedLoginProvider(auth, store) : undefined,
|
|
46
51
|
};
|
|
47
|
-
return new Genesis(opts);
|
|
52
|
+
return new Genesis(hivesTransport ? { ...opts, fetch: createHivesNodeTransport(opts, deps.signal) } : opts);
|
|
48
53
|
};
|
|
49
54
|
const emit = (data, pretty) => out.log(g().json ? JSON.stringify(data, null, 2) : pretty());
|
|
55
|
+
const positiveInteger = (value, option, maximum) => {
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > maximum) {
|
|
59
|
+
throw new Error(`${option} must be an integer from 1 to ${maximum}.`);
|
|
60
|
+
}
|
|
61
|
+
return Number(value);
|
|
62
|
+
};
|
|
63
|
+
const projectName = (name) => {
|
|
64
|
+
if (name.length < 3 || name.length > 64 || !/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name)) {
|
|
65
|
+
throw new Error('Project name must be 3–64 lowercase letters, numbers or hyphens, start with a letter and end with a letter or number.');
|
|
66
|
+
}
|
|
67
|
+
return name;
|
|
68
|
+
};
|
|
50
69
|
registerRunner(program, client, emit);
|
|
51
70
|
const orgId = async (gen) => {
|
|
52
71
|
if (g().org)
|
|
@@ -156,17 +175,206 @@ export function buildProgram(deps = {}) {
|
|
|
156
175
|
const r = await gen.foundry.chat({ model: o.model, messages });
|
|
157
176
|
emit(r, () => r.choices?.[0]?.message?.content ?? JSON.stringify(r));
|
|
158
177
|
});
|
|
178
|
+
const hivesClient = () => client(true);
|
|
159
179
|
// ---- hives -----------------------------------------------------------
|
|
160
|
-
const hives = program.command('hives').description('Hives: projects, deployments, functions');
|
|
180
|
+
const hives = program.command('hives').description('Hives: projects, secrets, deployments, functions');
|
|
181
|
+
const deployments = hives.command('deployments').description('Real WASM artifacts and deployment lifecycle');
|
|
182
|
+
const emitDeployment = (data) => emit(data, () => table(data && typeof data === 'object' && !Array.isArray(data) && !('data' in data) ? [data] : listOf(data), ['id', 'version', 'status', 'module_hash', 'created_at']));
|
|
183
|
+
deployments.command('list <project_id>')
|
|
184
|
+
.option('--page <number>', 'page (1–10000)').option('--per-page <number>', 'items (1–100)')
|
|
185
|
+
.option('--status <status>', 'persisted deployment status').option('--author <id>', 'creator ID')
|
|
186
|
+
.action(async (project, o) => {
|
|
187
|
+
emitDeployment(await (await hivesClient()).hives.deployments.list(project, {
|
|
188
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100), status: o.status, author: o.author,
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
deployments.command('get <project_id> <deployment_id>')
|
|
192
|
+
.action(async (project, deployment) => emitDeployment(await (await hivesClient()).hives.deployments.get(project, deployment)));
|
|
193
|
+
deployments.command('upload <project_id> <wasm_file>').description('Upload actual WASM bytes; Git options are labels only')
|
|
194
|
+
.option('--git-sha <sha>', 'commit label (7–40 characters)').option('--git-branch <branch>', 'branch label').option('--git-message <message>', 'commit label')
|
|
195
|
+
.action(async (project, path, o) => {
|
|
196
|
+
const file = await (deps.readWasmFile ?? readWasmFile)(path);
|
|
197
|
+
emitDeployment(await (await hivesClient()).hives.deployments.upload(project, file, {
|
|
198
|
+
filename: basename(path), git_sha: o.gitSha, git_branch: o.gitBranch, git_message: o.gitMessage,
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
deployments.command('reuse <project_id> <module_hash>').description('Deploy a real artifact already owned by this org and project')
|
|
202
|
+
.option('--no-auto-promote', 'keep the completed deployment ready for explicit promotion')
|
|
203
|
+
.action(async (project, module_hash, o) => {
|
|
204
|
+
if (!/^[a-f0-9]{64}$/.test(module_hash))
|
|
205
|
+
throw new Error('module_hash must be the lowercase SHA-256 returned by a real Hives upload.');
|
|
206
|
+
emitDeployment(await (await hivesClient()).hives.deployments.create(project, { module_hash, auto_promote: o.autoPromote }));
|
|
207
|
+
});
|
|
208
|
+
deployments.command('promote <project_id> <deployment_id>')
|
|
209
|
+
.action(async (project, deployment) => emitDeployment(await (await hivesClient()).hives.deployments.promote(project, deployment)));
|
|
210
|
+
deployments.command('rollback <project_id> <deployment_id>').description('Activate this owned previous deployment')
|
|
211
|
+
.option('--reason <text>', 'audit reason')
|
|
212
|
+
.action(async (project, deployment, o) => emitDeployment(await (await hivesClient()).hives.deployments.rollback(project, deployment, { reason: o.reason })));
|
|
213
|
+
deployments.command('cancel <project_id> <deployment_id>')
|
|
214
|
+
.action(async (project, deployment) => emitDeployment(await (await hivesClient()).hives.deployments.cancel(project, deployment)));
|
|
215
|
+
deployments.command('delete <project_id> <deployment_id>').description('Delete a deployment that is not currently active')
|
|
216
|
+
.action(async (project, deployment) => { const result = await (await hivesClient()).hives.deployments.delete(project, deployment); emit(result, () => `Deleted deployment ${deployment}.`); });
|
|
217
|
+
deployments.command('logs <project_id> <deployment_id>').description('Read real persisted deployment logs')
|
|
218
|
+
.option('--page <number>', 'page (1–10000)').option('--per-page <number>', 'items (1–100)')
|
|
219
|
+
.option('--level <level>', 'minimum level').option('--search <text>', 'search log messages')
|
|
220
|
+
.option('--start <iso>', 'RFC 3339 start time').option('--end <iso>', 'RFC 3339 end time')
|
|
221
|
+
.action(async (project, deployment, o) => {
|
|
222
|
+
for (const timestamp of [o.start, o.end])
|
|
223
|
+
if (timestamp !== undefined && !Number.isFinite(Date.parse(timestamp)))
|
|
224
|
+
throw new Error('Log timestamps must use RFC 3339.');
|
|
225
|
+
const result = await (await hivesClient()).hives.deployments.logs(project, deployment, {
|
|
226
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100),
|
|
227
|
+
level: o.level, search: o.search, start: o.start, end: o.end,
|
|
228
|
+
});
|
|
229
|
+
emit(result, () => table(result.logs, ['timestamp', 'level', 'message', 'source']));
|
|
230
|
+
});
|
|
231
|
+
const emitFunction = (value) => emit(value, () => table(listOf(value).length ? listOf(value) : [value], ['id', 'name', 'status', 'handler', 'memory_mb', 'timeout_ms']));
|
|
232
|
+
const functions = hives.command('functions').description('Owned WASM functions; invocation returns the completed raw guest response');
|
|
233
|
+
functions.command('list <project_id>').option('--page <n>').option('--per-page <n>').option('--status <status>').option('--search <text>')
|
|
234
|
+
.action(async (project, o) => emitFunction(await (await hivesClient()).hives.functions.list(project, {
|
|
235
|
+
page: positiveInteger(o.page, '--page', 1000000), per_page: positiveInteger(o.perPage, '--per-page', 100), status: o.status, search: o.search,
|
|
236
|
+
})));
|
|
237
|
+
functions.command('get <project_id> <function_id>').action(async (project, id) => emitFunction(await (await hivesClient()).hives.functions.get(project, id)));
|
|
238
|
+
functions.command('create <project_id> <name> <module_hash>').option('--request-file <file>', 'JSON metadata and encrypted env overrides; input values are never printed')
|
|
239
|
+
.action(async (project, name, hash, o) => {
|
|
240
|
+
if (!/^[a-f0-9]{64}$/.test(hash))
|
|
241
|
+
throw new Error('Use a full lowercase artifact hash from an owned Hives upload.');
|
|
242
|
+
const extra = o.requestFile ? await readFunctionJson(o.requestFile, 256 * 1024) : {};
|
|
243
|
+
emitFunction(await (await hivesClient()).hives.functions.create(project, { ...extra, name, handler: `wasm://sha256/${hash}` }));
|
|
244
|
+
});
|
|
245
|
+
functions.command('update <project_id> <function_id>').requiredOption('--request-file <file>', 'JSON supported metadata/env/remove_env fields; values are never printed')
|
|
246
|
+
.action(async (project, id, o) => emitFunction(await (await hivesClient()).hives.functions.update(project, id, await readFunctionJson(o.requestFile, 256 * 1024))));
|
|
247
|
+
functions.command('delete <project_id> <function_id>').action(async (project, id) => {
|
|
248
|
+
const result = await (await hivesClient()).hives.functions.delete(project, id);
|
|
249
|
+
emit(result, () => `Deleted function ${id}.`);
|
|
250
|
+
});
|
|
251
|
+
function invocationCommand(command, kind) {
|
|
252
|
+
command.description('Complete real guest execution; write exact bytes, including executed HTTP error bodies')
|
|
253
|
+
.option('--request-file <file>', 'JSON method/path/headers/body or body_base64')
|
|
254
|
+
.option('--body-file <file>', 'Exact binary body (up to 10 MiB; serialized guest envelope may reach its cap earlier)')
|
|
255
|
+
.option('--method <method>', 'guest HTTP method', 'POST').option('--path <path>', 'guest path including query', '/')
|
|
256
|
+
.option('--output <file>', 'create a new private file containing exact response bytes')
|
|
257
|
+
.action(async (project, id, o) => {
|
|
258
|
+
if (o.requestFile && o.bodyFile)
|
|
259
|
+
throw new Error('Choose --request-file or --body-file.');
|
|
260
|
+
if (g().json && o.output)
|
|
261
|
+
throw new Error('Choose --json or --output.');
|
|
262
|
+
let request = o.requestFile ? await readFunctionJson(o.requestFile, MAX_FUNCTION_JSON_BYTES) : { method: o.method, path: o.path };
|
|
263
|
+
if (o.bodyFile)
|
|
264
|
+
request.body_base64 = Buffer.from(await readFunctionFile(o.bodyFile, MAX_FUNCTION_BODY_BYTES)).toString('base64');
|
|
265
|
+
const response = await (await hivesClient()).hives[kind].invoke(project, id, request);
|
|
266
|
+
const bytes = await hivesResponseBytes(response);
|
|
267
|
+
if (g().json)
|
|
268
|
+
out.log(JSON.stringify({ status: response.status, headers: Object.fromEntries(response.headers), body_base64: Buffer.from(bytes).toString('base64') }));
|
|
269
|
+
else {
|
|
270
|
+
out.err(JSON.stringify({ status: response.status, headers: Object.fromEntries(response.headers), bytes: bytes.length }));
|
|
271
|
+
if (o.output)
|
|
272
|
+
await (deps.writeOutputFile ?? writeFunctionOutput)(o.output, bytes);
|
|
273
|
+
else
|
|
274
|
+
(deps.writeBytes ?? ((value) => { process.stdout.write(value); }))(bytes);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
invocationCommand(functions.command('invoke <project_id> <function_id>'), 'functions');
|
|
279
|
+
invocationCommand(deployments.command('invoke <project_id> <deployment_id>'), 'deployments');
|
|
280
|
+
const secrets = hives.command('secrets').description('Project secrets encrypted with Genesis KMS');
|
|
281
|
+
const readInput = async (options, limit) => {
|
|
282
|
+
if (Boolean(options.file) === Boolean(options.stdin))
|
|
283
|
+
throw new Error('Choose exactly one input source: a file or --stdin. Secret values are not accepted as command arguments.');
|
|
284
|
+
return (deps.readSecretInput ?? readSecretInput)(options, limit);
|
|
285
|
+
};
|
|
286
|
+
const emitSecret = (data) => emit(data, () => table(listOf(data), ['id', 'name', 'version', 'description', 'updated_at']));
|
|
287
|
+
secrets.command('list <project_id>').description('List secret metadata without values')
|
|
288
|
+
.option('--page <number>', 'page number (1–10000)').option('--per-page <number>', 'items per page (1–100)')
|
|
289
|
+
.action(async (id, o) => {
|
|
290
|
+
const q = { page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) };
|
|
291
|
+
emitSecret(await (await hivesClient()).hives.secrets.list(id, q));
|
|
292
|
+
});
|
|
293
|
+
secrets.command('get <project_id> <secret_id>').description('Inspect secret metadata without its value')
|
|
294
|
+
.action(async (project, secret) => {
|
|
295
|
+
const r = await (await hivesClient()).hives.secrets.get(project, secret);
|
|
296
|
+
emit(r, () => table([r], ['id', 'name', 'version', 'description', 'updated_at']));
|
|
297
|
+
});
|
|
298
|
+
secrets.command('create <project_id> <name>').description('Create a secret; plaintext is read from a file or stdin')
|
|
299
|
+
.option('--value-file <path>', 'UTF-8 file containing the value').option('--stdin', 'read value from stdin')
|
|
300
|
+
.option('--description <text>', 'description (up to 500 characters)')
|
|
301
|
+
.action(async (project, name, o) => {
|
|
302
|
+
const body = validateSecret({ name, value: await readInput({ file: o.valueFile, stdin: o.stdin }, 65536), description: o.description });
|
|
303
|
+
const r = await (await hivesClient()).hives.secrets.create(project, body);
|
|
304
|
+
emit(r, () => table([r], ['id', 'name', 'version']));
|
|
305
|
+
});
|
|
306
|
+
secrets.command('update <project_id> <secret_id>').description('Replace a secret value, creating a new version')
|
|
307
|
+
.option('--value-file <path>', 'UTF-8 file containing the new value').option('--stdin', 'read value from stdin')
|
|
308
|
+
.action(async (project, secret, o) => {
|
|
309
|
+
const value = validateSecretValue(await readInput({ file: o.valueFile, stdin: o.stdin }, 65536));
|
|
310
|
+
const r = await (await hivesClient()).hives.secrets.update(project, secret, { value });
|
|
311
|
+
emit(r, () => table([r], ['id', 'name', 'version']));
|
|
312
|
+
});
|
|
313
|
+
secrets.command('delete <project_id> <secret_id>').description('Delete a secret in your authenticated org')
|
|
314
|
+
.action(async (project, secret) => { const r = await (await hivesClient()).hives.secrets.delete(project, secret); emit(r, () => `Deleted secret ${secret}.`); });
|
|
315
|
+
secrets.command('reveal <project_id> <secret_id>').description('Explicit, audit-logged plaintext reveal; requires --stdout')
|
|
316
|
+
.option('--stdout', 'write sensitive plaintext to stdout (avoid logs and retained output)')
|
|
317
|
+
.action(async (project, secret, o) => {
|
|
318
|
+
if (!o.stdout)
|
|
319
|
+
throw new Error('Reveal requires --stdout. This writes sensitive plaintext to your terminal or pipe; avoid logging or retaining it.');
|
|
320
|
+
const r = await (await hivesClient()).hives.secrets.reveal(project, secret);
|
|
321
|
+
emit(r, () => r.value);
|
|
322
|
+
});
|
|
323
|
+
secrets.command('bulk <project_id>').description('PUT a JSON array of secret names and values; existing names receive new versions')
|
|
324
|
+
.option('--file <path>', 'UTF-8 JSON input file').option('--stdin', 'read JSON from stdin')
|
|
325
|
+
.action(async (project, o) => {
|
|
326
|
+
const batch = parseSecretBatch(await readInput(o, 256 * 1024));
|
|
327
|
+
const r = await (await hivesClient()).hives.secrets.bulkUpsert(project, batch);
|
|
328
|
+
emit(r, () => table(r.secrets, ['id', 'name', 'version']));
|
|
329
|
+
});
|
|
161
330
|
const projects = hives.command('projects').description('Hives projects');
|
|
162
|
-
projects.command('list', { isDefault: true }).
|
|
163
|
-
|
|
331
|
+
projects.command('list', { isDefault: true }).option('--search <text>', 'filter by project name')
|
|
332
|
+
.option('--page <number>', 'page number (1–10000)').option('--per-page <number>', 'items per page (1–100)')
|
|
333
|
+
.action(async (o) => {
|
|
334
|
+
const r = await (await hivesClient()).hives.projects.list({ search: o.search,
|
|
335
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) });
|
|
164
336
|
emit(r, () => table(listOf(r), ['id', 'name', 'slug', 'region', 'total_deployments', 'active_instances']));
|
|
165
337
|
});
|
|
166
338
|
projects.command('get <project_id>').action(async (id) => {
|
|
167
|
-
const r = await (await
|
|
339
|
+
const r = await (await hivesClient()).hives.projects.get(id);
|
|
168
340
|
emit(r, () => JSON.stringify(r, null, 2));
|
|
169
341
|
});
|
|
342
|
+
projects.command('create <name>').description('Create a project in your authenticated org')
|
|
343
|
+
.option('--kind <kind>', 'worker, function, site or api', 'worker')
|
|
344
|
+
.option('--description <text>', 'project description (up to 500 characters)')
|
|
345
|
+
.action(async (name, o) => {
|
|
346
|
+
if (!['worker', 'function', 'site', 'api'].includes(o.kind))
|
|
347
|
+
throw new Error('--kind must be worker, function, site or api.');
|
|
348
|
+
if (o.description && o.description.length > 500)
|
|
349
|
+
throw new Error('--description must be at most 500 characters.');
|
|
350
|
+
const r = await (await hivesClient()).hives.projects.create({ name: projectName(name), kind: o.kind,
|
|
351
|
+
...(o.description === undefined ? {} : { description: o.description }) });
|
|
352
|
+
emit(r, () => table([r], ['id', 'name', 'slug', 'status']));
|
|
353
|
+
});
|
|
354
|
+
projects.command('rename <project_id> <name>').description('Rename a project in your authenticated org')
|
|
355
|
+
.action(async (id, name) => {
|
|
356
|
+
const r = await (await hivesClient()).hives.projects.update(id, { name: projectName(name) });
|
|
357
|
+
emit(r, () => table([r], ['id', 'name', 'slug', 'status']));
|
|
358
|
+
});
|
|
359
|
+
projects.command('activity <project_id>').description('Read persisted project activity')
|
|
360
|
+
.option('--action <action>', 'filter by action')
|
|
361
|
+
.option('--actor-type <type>', 'filter by actor type')
|
|
362
|
+
.option('--resource-type <type>', 'filter by resource type')
|
|
363
|
+
.option('--since <timestamp>', 'inclusive start timestamp in ISO 8601')
|
|
364
|
+
.option('--until <timestamp>', 'inclusive end timestamp in ISO 8601')
|
|
365
|
+
.option('--page <number>', 'page number (1–10000)')
|
|
366
|
+
.option('--per-page <number>', 'items per page (1–100)')
|
|
367
|
+
.action(async (id, o) => {
|
|
368
|
+
for (const [option, value] of [['--since', o.since], ['--until', o.until]]) {
|
|
369
|
+
if (value !== undefined && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) || !Number.isFinite(Date.parse(value)))) {
|
|
370
|
+
throw new Error(`${option} must be an ISO 8601 timestamp.`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const r = await (await hivesClient()).hives.projects.activity(id, { action: o.action,
|
|
374
|
+
actor_type: o.actorType, resource_type: o.resourceType, since: o.since, until: o.until,
|
|
375
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) });
|
|
376
|
+
emit(r, () => table(listOf(r), ['created_at', 'action', 'actor_id', 'resource_type', 'resource_name']));
|
|
377
|
+
});
|
|
170
378
|
// ---- relays ----------------------------------------------------------
|
|
171
379
|
const relays = program.command('relays').description('Relays: DNS zones and records');
|
|
172
380
|
relays.command('zones').description('List DNS zones').option('--search <q>').action(async (o) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SecretInputOptions {
|
|
2
|
+
file?: string;
|
|
3
|
+
stdin?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface SecretInput {
|
|
6
|
+
name: string;
|
|
7
|
+
value: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Read plaintext only from an explicitly selected file or stdin, never argv. */
|
|
11
|
+
export declare function readSecretInput(options: SecretInputOptions, limit: number): Promise<string>;
|
|
12
|
+
export declare function validateSecretValue(value: string): string;
|
|
13
|
+
export declare function validateSecret(input: unknown): SecretInput;
|
|
14
|
+
export declare function parseSecretBatch(text: string): SecretInput[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
/** Read plaintext only from an explicitly selected file or stdin, never argv. */
|
|
3
|
+
export async function readSecretInput(options, limit) {
|
|
4
|
+
if (Boolean(options.file) === Boolean(options.stdin))
|
|
5
|
+
throw new Error('Choose exactly one input source: a file or --stdin. Secret values are not accepted as command arguments.');
|
|
6
|
+
const chunks = [];
|
|
7
|
+
let size = 0;
|
|
8
|
+
const append = (chunk) => {
|
|
9
|
+
size += chunk.byteLength;
|
|
10
|
+
if (size > limit)
|
|
11
|
+
throw new Error(`Secret input exceeds ${limit} UTF-8 bytes. Use a smaller value or batch.`);
|
|
12
|
+
chunks.push(chunk);
|
|
13
|
+
};
|
|
14
|
+
if (options.file) {
|
|
15
|
+
let file;
|
|
16
|
+
try {
|
|
17
|
+
file = await open(options.file, 'r');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('Could not open the secret input file. Check its path and permissions.');
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
for (;;) {
|
|
24
|
+
const buffer = new Uint8Array(Math.min(8192, limit + 1));
|
|
25
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, null);
|
|
26
|
+
if (!bytesRead)
|
|
27
|
+
break;
|
|
28
|
+
append(buffer.subarray(0, bytesRead));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await file.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
for await (const chunk of process.stdin)
|
|
37
|
+
append(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk);
|
|
38
|
+
}
|
|
39
|
+
const bytes = new Uint8Array(size);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (const chunk of chunks) {
|
|
42
|
+
bytes.set(chunk, offset);
|
|
43
|
+
offset += chunk.byteLength;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error('Secret input must be valid UTF-8 text.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function validateSecretValue(value) {
|
|
53
|
+
if (!value.length || new TextEncoder().encode(value).length > 65536)
|
|
54
|
+
throw new Error('Secret values must contain 1–65,536 UTF-8 bytes.');
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
export function validateSecret(input) {
|
|
58
|
+
if (!input || typeof input !== 'object')
|
|
59
|
+
throw new Error('Each secret needs a name and value.');
|
|
60
|
+
const { name, value, description } = input;
|
|
61
|
+
if (typeof name !== 'string' || name.length > 255 || !/^[A-Za-z][A-Za-z0-9_]*$/.test(name))
|
|
62
|
+
throw new Error('Secret names must start with a letter and contain only letters, numbers and underscores (up to 255 characters).');
|
|
63
|
+
if (typeof value !== 'string')
|
|
64
|
+
throw new Error('Each secret value must be a string.');
|
|
65
|
+
if (description !== undefined && description !== null && (typeof description !== 'string' || description.length > 500))
|
|
66
|
+
throw new Error('Secret descriptions must contain at most 500 characters.');
|
|
67
|
+
return { name, value: validateSecretValue(value), ...(typeof description === 'string' ? { description } : {}) };
|
|
68
|
+
}
|
|
69
|
+
export function parseSecretBatch(text) {
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(text);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error('Bulk input must be a JSON array with secret name and value fields.');
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(data) || !data.length)
|
|
78
|
+
throw new Error('Bulk input must be a non-empty JSON array.');
|
|
79
|
+
const batch = data.map(validateSecret);
|
|
80
|
+
if (new Set(batch.map((secret) => secret.name)).size !== batch.length)
|
|
81
|
+
throw new Error('Each secret name must appear once in a bulk update.');
|
|
82
|
+
if (new TextEncoder().encode(JSON.stringify(batch)).length > 256 * 1024)
|
|
83
|
+
throw new Error('Bulk input must fit within 256 KiB. Split it into smaller batches.');
|
|
84
|
+
return batch;
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import { MAX_WASM_BYTES } from '@lifesi/genesis';
|
|
3
|
+
/** Read only the explicitly requested regular file, with a bounded allocation. */
|
|
4
|
+
export async function readWasmFile(path) {
|
|
5
|
+
let file;
|
|
6
|
+
try {
|
|
7
|
+
file = await open(path, 'r');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error('Could not open the WASM file. Check its path and permissions.');
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const stat = await file.stat();
|
|
14
|
+
if (!stat.isFile() || stat.size < 4 || stat.size > MAX_WASM_BYTES)
|
|
15
|
+
throw new Error('Choose a regular WASM file of 4 bytes to 10 MiB.');
|
|
16
|
+
const bytes = new Uint8Array(MAX_WASM_BYTES + 1);
|
|
17
|
+
let size = 0;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const { bytesRead } = await file.read(bytes, size, bytes.length - size, null);
|
|
20
|
+
if (!bytesRead)
|
|
21
|
+
break;
|
|
22
|
+
size += bytesRead;
|
|
23
|
+
if (size > MAX_WASM_BYTES)
|
|
24
|
+
throw new Error('WASM file exceeds 10 MiB.');
|
|
25
|
+
}
|
|
26
|
+
return new Blob([bytes.slice(0, size)], { type: 'application/wasm' });
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await file.close();
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifesi/genesis-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "genesis: one CLI for every Genesis service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"commander": "^14.0.0",
|
|
15
|
-
"
|
|
15
|
+
"undici": "6.29.0",
|
|
16
|
+
"@lifesi/genesis": "0.1.1"
|
|
16
17
|
},
|
|
17
18
|
"optionalDependencies": {
|
|
18
19
|
"@napi-rs/keyring": "^1.2.0"
|