@onesi/genesis-cli 0.1.13 → 0.2.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/README.md +29 -2
- package/dist/beacons.js +68 -0
- package/dist/dials.js +7 -4
- package/dist/fleets.js +30 -0
- package/dist/flocks.d.ts +7 -1
- package/dist/flocks.js +281 -4
- package/dist/henry.d.ts +5 -1
- package/dist/henry.js +60 -1
- package/dist/hives-coverage.d.ts +12 -0
- package/dist/hives-coverage.js +133 -0
- package/dist/hives-node-transport.js +4 -1
- package/dist/hives-operations.d.ts +1 -0
- package/dist/hives-operations.js +2 -0
- package/dist/hives-platform.d.ts +13 -0
- package/dist/hives-platform.js +133 -0
- package/dist/hives-resources.d.ts +1 -0
- package/dist/hives-resources.js +5 -3
- package/dist/iam-directory.d.ts +15 -0
- package/dist/iam-directory.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +273 -30
- package/dist/minds.d.ts +1 -1
- package/dist/minds.js +37 -2
- package/dist/onboard/art.d.ts +50 -0
- package/dist/onboard/art.js +139 -0
- package/dist/onboard/brand.d.ts +56 -0
- package/dist/onboard/brand.js +134 -0
- package/dist/onboard/caps.d.ts +90 -0
- package/dist/onboard/caps.js +232 -0
- package/dist/onboard/encode.d.ts +23 -0
- package/dist/onboard/encode.js +235 -0
- package/dist/onboard/eye.d.ts +103 -0
- package/dist/onboard/eye.js +147 -0
- package/dist/onboard/index.d.ts +61 -0
- package/dist/onboard/index.js +562 -0
- package/dist/onboard/mcp.d.ts +35 -0
- package/dist/onboard/mcp.js +118 -0
- package/dist/onboard/raster.d.ts +43 -0
- package/dist/onboard/raster.js +253 -0
- package/dist/onboard/sound.d.ts +2 -0
- package/dist/onboard/sound.js +54 -0
- package/dist/onboard/state.d.ts +24 -0
- package/dist/onboard/state.js +48 -0
- package/dist/onboard/term.d.ts +69 -0
- package/dist/onboard/term.js +292 -0
- package/dist/onboard/ui.d.ts +97 -0
- package/dist/onboard/ui.js +615 -0
- package/dist/orgs.d.ts +16 -0
- package/dist/orgs.js +89 -0
- package/dist/output.d.ts +13 -1
- package/dist/output.js +21 -2
- package/dist/products.d.ts +6 -2
- package/dist/products.js +474 -13
- package/dist/relays-commands/certificates.d.ts +3 -0
- package/dist/relays-commands/certificates.js +20 -0
- package/dist/relays-commands/domains.d.ts +3 -0
- package/dist/relays-commands/domains.js +23 -0
- package/dist/relays-commands/index.d.ts +4 -0
- package/dist/relays-commands/index.js +10 -0
- package/dist/relays-commands/monitoring.d.ts +3 -0
- package/dist/relays-commands/monitoring.js +41 -0
- package/dist/relays-commands/telemetry.d.ts +3 -0
- package/dist/relays-commands/telemetry.js +28 -0
- package/dist/relays-commands/zones.d.ts +7 -0
- package/dist/relays-commands/zones.js +16 -0
- package/dist/runner.js +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { type Genesis, type OperationId } from '@onesi/genesis';
|
|
3
|
+
/** `genesis hives <words>` -> Hives operation. Positional arguments are the path parameters, query
|
|
4
|
+
* parameters become options, a JSON body comes from --body-file. Groups that already exist
|
|
5
|
+
* (functions, deployments, environment, domains, logs) are extended, never replaced. */
|
|
6
|
+
export declare const HIVES_COVERAGE_COMMANDS: Record<string, OperationId>;
|
|
7
|
+
/** Register after every other `hives` group so existing groups are found and extended. */
|
|
8
|
+
export declare function registerHivesCoverage(hives: Command, deps: {
|
|
9
|
+
client: () => Promise<Genesis>;
|
|
10
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
11
|
+
readRequest?: (path: string, limit: number) => Promise<unknown>;
|
|
12
|
+
}): void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { hivesSseWindow, idempotencyHeaders, operationTable } from '@onesi/genesis';
|
|
2
|
+
import { readFunctionJson } from './function-input.js';
|
|
3
|
+
import { bytes } from './hives-resources.js';
|
|
4
|
+
/** `genesis hives <words>` -> Hives operation. Positional arguments are the path parameters, query
|
|
5
|
+
* parameters become options, a JSON body comes from --body-file. Groups that already exist
|
|
6
|
+
* (functions, deployments, environment, domains, logs) are extended, never replaced. */
|
|
7
|
+
export const HIVES_COVERAGE_COMMANDS = {
|
|
8
|
+
'flights list': 'hives_list_flights',
|
|
9
|
+
'flights create': 'hives_create_flight',
|
|
10
|
+
'flights get': 'hives_get_flight',
|
|
11
|
+
'flights files': 'hives_list_flight_files',
|
|
12
|
+
'flights read-file': 'hives_read_flight_file',
|
|
13
|
+
'flights processes': 'hives_list_flight_processes',
|
|
14
|
+
'flights env': 'hives_list_flight_env',
|
|
15
|
+
'flights debug-metrics': 'hives_list_flight_debug_metrics',
|
|
16
|
+
'functions logs': 'hives_query_function_logs',
|
|
17
|
+
'deployments stream-logs': 'hives_stream_deployment_logs',
|
|
18
|
+
'logs aggregate': 'hives_aggregate_logs',
|
|
19
|
+
'logs search': 'hives_search_logs',
|
|
20
|
+
'environment values': 'hives_get_project_env',
|
|
21
|
+
'environment set': 'hives_set_project_env',
|
|
22
|
+
'environment delete': 'hives_delete_project_env',
|
|
23
|
+
'environment delete-key': 'hives_delete_project_env_key',
|
|
24
|
+
'domains get': 'hives_get_domain',
|
|
25
|
+
'domains delete': 'hives_delete_domain',
|
|
26
|
+
'domains verification-record': 'hives_get_verification_record',
|
|
27
|
+
'cells objects put-download': 'hives_upload_object_named_download',
|
|
28
|
+
'cells objects remove-download': 'hives_delete_object_named_download',
|
|
29
|
+
};
|
|
30
|
+
const DESCRIPTIONS = {
|
|
31
|
+
flights: 'Flights: WASM HTTP workloads served at their own address',
|
|
32
|
+
logs: 'Project logs',
|
|
33
|
+
domains: 'Custom domains for a project',
|
|
34
|
+
};
|
|
35
|
+
const BODY_LIMIT = 1024 * 1024;
|
|
36
|
+
const flag = (name) => name.replace(/_/g, '-').toLowerCase();
|
|
37
|
+
const key = (name) => flag(name).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
38
|
+
const integer = (value, name, max) => {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > max)
|
|
42
|
+
throw new Error(`${name} must be an integer from 1 to ${max}.`);
|
|
43
|
+
return Number(value);
|
|
44
|
+
};
|
|
45
|
+
const subcommand = (parent, name) => parent.commands.find((c) => c.name() === name) ?? parent.command(name).description(DESCRIPTIONS[name] ?? name);
|
|
46
|
+
function queryValue(schema, value) {
|
|
47
|
+
const type = schema?.type;
|
|
48
|
+
if (type === 'integer') {
|
|
49
|
+
if (!/^\d+$/.test(value))
|
|
50
|
+
throw new Error(`Expected an integer, got "${value}".`);
|
|
51
|
+
return Number(value);
|
|
52
|
+
}
|
|
53
|
+
if (type === 'boolean') {
|
|
54
|
+
if (value !== 'true' && value !== 'false')
|
|
55
|
+
throw new Error(`Expected true or false, got "${value}".`);
|
|
56
|
+
return value === 'true';
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/** Register after every other `hives` group so existing groups are found and extended. */
|
|
61
|
+
export function registerHivesCoverage(hives, deps) {
|
|
62
|
+
const read = deps.readRequest ?? readFunctionJson;
|
|
63
|
+
const output = (data) => deps.emit(data ?? { ok: true }, () => JSON.stringify(data ?? { ok: true }, null, 2));
|
|
64
|
+
for (const [words, id] of Object.entries(HIVES_COVERAGE_COMMANDS)) {
|
|
65
|
+
const op = operationTable[id];
|
|
66
|
+
const parts = words.split(' ');
|
|
67
|
+
let parent = hives;
|
|
68
|
+
for (const part of parts.slice(0, -1))
|
|
69
|
+
parent = subcommand(parent, part);
|
|
70
|
+
const properties = (op.inputSchema.properties ?? {});
|
|
71
|
+
const required = op.inputSchema.required;
|
|
72
|
+
if (id === 'hives_delete_project_env') {
|
|
73
|
+
// The body is a JSON array of names: take them as arguments instead of a file.
|
|
74
|
+
parent.command(`${parts.at(-1)} <project_id> <names...>`).description(`${op.summary} (${op.method} ${op.path})`)
|
|
75
|
+
.action(async (project, names) => output(await (await deps.client()).request(id, { project_id: project, body: names })));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (id === 'hives_upload_object_named_download') {
|
|
79
|
+
// Raw object bytes from a file, through the same Cells checks as `cells objects put-flat`.
|
|
80
|
+
parent.command(`${parts.at(-1)} <project_id> <bucket_id>`).description(`${op.summary} (${op.method} ${op.path})`)
|
|
81
|
+
.requiredOption('--file <path>', 'Exact binary bytes up to 10 MiB').option('--content-type <value>')
|
|
82
|
+
.option('--idempotency-key <key>', '1-128 printable ASCII; reuse it only to replay the same write (a fresh key is sent when omitted)')
|
|
83
|
+
.action(async (project, bucket, o) => output(await (await deps.client()).hives.cells.objects.putDownload(project, bucket, await bytes(o.file), { content_type: o.contentType, idempotencyKey: o.idempotencyKey })));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (id === 'hives_delete_object_named_download') {
|
|
87
|
+
parent.command(`${parts.at(-1)} <project_id> <bucket_id>`).description(`${op.summary} (${op.method} ${op.path})`)
|
|
88
|
+
.action(async (project, bucket) => { await (await deps.client()).hives.cells.objects.removeDownload(project, bucket); output({ acknowledged: true }); });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const command = parent.command([parts.at(-1), ...op.pathParams.map((p) => `<${p}>`)].join(' '))
|
|
92
|
+
.description(`${op.summary} (${op.method} ${op.path})`);
|
|
93
|
+
for (const q of op.queryParams) {
|
|
94
|
+
const description = properties[q]?.description;
|
|
95
|
+
command[required.includes(q) ? 'requiredOption' : 'option'](`--${flag(q)} <value>`, description);
|
|
96
|
+
}
|
|
97
|
+
if (op.hasBody)
|
|
98
|
+
command[required.includes('body') ? 'requiredOption' : 'option']('--body-file <path>', 'JSON request body, up to 1 MiB');
|
|
99
|
+
if (op.idempotencyKey)
|
|
100
|
+
command.option('--idempotency-key <key>', 'Required when slug is omitted; reuse it to get the original result');
|
|
101
|
+
if (op.stream) {
|
|
102
|
+
command.option('--max-events <number>', 'stop after this many events (1-1000, default 100)')
|
|
103
|
+
.option('--seconds <number>', 'stop after this many seconds (1-15, default 10)');
|
|
104
|
+
}
|
|
105
|
+
if (id === 'hives_get_project_env')
|
|
106
|
+
command.option('--stdout', 'write every decrypted value to stdout (avoid retained output)');
|
|
107
|
+
command.action(async (...args) => {
|
|
108
|
+
const options = args[op.pathParams.length];
|
|
109
|
+
if (id === 'hives_get_project_env' && !options.stdout)
|
|
110
|
+
throw new Error('Reading every environment value requires --stdout. Avoid logging or retaining the plaintext.');
|
|
111
|
+
const input = Object.fromEntries(op.pathParams.map((p, i) => [p, args[i]]));
|
|
112
|
+
for (const q of op.queryParams) {
|
|
113
|
+
const value = options[key(q)];
|
|
114
|
+
if (value === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
input[q] = typeof value === 'boolean' ? value : queryValue(properties[q], value);
|
|
117
|
+
}
|
|
118
|
+
if (typeof options.bodyFile === 'string')
|
|
119
|
+
input.body = await read(options.bodyFile, BODY_LIMIT);
|
|
120
|
+
const client = await deps.client();
|
|
121
|
+
if (op.stream) {
|
|
122
|
+
const response = await client.raw(id, input, { headers: { accept: 'text/event-stream' } });
|
|
123
|
+
output(await hivesSseWindow(response, {
|
|
124
|
+
maxEvents: integer(options.maxEvents, '--max-events', 1000),
|
|
125
|
+
seconds: integer(options.seconds, '--seconds', 15),
|
|
126
|
+
}));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const headers = idempotencyHeaders(id, options.idempotencyKey);
|
|
130
|
+
output(await client.request(id, input, { headers }));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -11,10 +11,13 @@ export async function closeHivesNodeTransport() {
|
|
|
11
11
|
await current?.destroy();
|
|
12
12
|
}
|
|
13
13
|
export function hivesOperationBudget(id) {
|
|
14
|
-
if (['hives_list_buckets', 'hives_create_bucket', 'hives_get_bucket', 'hives_delete_bucket', 'hives_list_objects', 'hives_upload_object', 'hives_upload_object_by_key', 'hives_delete_object', 'hives_delete_object_by_key', 'hives_download_object_by_key', 'hives_get_cors_config', 'hives_set_cors_config', 'hives_generate_presigned_url', 'hives_reconcile'].includes(id))
|
|
14
|
+
if (['hives_list_buckets', 'hives_create_bucket', 'hives_get_bucket', 'hives_delete_bucket', 'hives_list_objects', 'hives_upload_object', 'hives_upload_object_by_key', 'hives_upload_object_named_download', 'hives_delete_object', 'hives_delete_object_by_key', 'hives_delete_object_named_download', 'hives_download_object_by_key', 'hives_get_cors_config', 'hives_set_cors_config', 'hives_generate_presigned_url', 'hives_reconcile'].includes(id))
|
|
15
15
|
return 100_000;
|
|
16
16
|
if (id === 'hives_cancel_deployment' || id === 'hives_delete_deployment' || id === 'hives_stop_deployment')
|
|
17
17
|
return 330_000;
|
|
18
|
+
// A live SSE subscription is bounded by its caller's window or --follow limit (at most 300 s).
|
|
19
|
+
if (id === 'hives_stream_logs' || id === 'hives_stream_deployment_logs')
|
|
20
|
+
return 330_000;
|
|
18
21
|
if (['hives_create_deployment', 'hives_upload_deployment', 'hives_invoke_deployment', 'hives_invoke_function', 'hives_invoke_function_async'].includes(id))
|
|
19
22
|
return 40_000;
|
|
20
23
|
if (['hives_create_function', 'hives_update_function', 'hives_delete_function'].includes(id))
|
|
@@ -4,6 +4,7 @@ type Deps = {
|
|
|
4
4
|
client: () => Promise<Genesis>;
|
|
5
5
|
emit: (data: unknown, pretty: () => string) => void;
|
|
6
6
|
readRequest?: (path: string, limit: number) => Promise<Record<string, unknown>>;
|
|
7
|
+
line?: (text: string) => void;
|
|
7
8
|
};
|
|
8
9
|
export declare function registerHivesOperations(hives: Command, deps: Deps): void;
|
|
9
10
|
export {};
|
package/dist/hives-operations.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerHivesResources } from './hives-resources.js';
|
|
2
|
+
import { registerHivesPlatform } from './hives-platform.js';
|
|
2
3
|
import { Option } from 'commander';
|
|
3
4
|
import { readFunctionJson } from './function-input.js';
|
|
4
5
|
function metricsOptions(command) {
|
|
@@ -18,6 +19,7 @@ const paging = (command) => command.option('--page <number>').option('--per-page
|
|
|
18
19
|
const pagination = (o) => ({ page: integer(o.page, '--page', 10000), per_page: integer(o.perPage, '--per-page', 100) });
|
|
19
20
|
export function registerHivesOperations(hives, deps) {
|
|
20
21
|
registerHivesResources(hives, deps);
|
|
22
|
+
registerHivesPlatform(hives, { client: deps.client, emit: deps.emit, line: deps.line });
|
|
21
23
|
const output = (data) => deps.emit(data, () => JSON.stringify(data, null, 2));
|
|
22
24
|
const read = deps.readRequest ?? readFunctionJson;
|
|
23
25
|
const settings = hives.command('settings').description('Stored project preferences and actual engine capabilities');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
3
|
+
type Deps = {
|
|
4
|
+
client: () => Promise<Genesis>;
|
|
5
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
6
|
+
/** Print one line as it arrives (live log streams). */
|
|
7
|
+
line?: (text: string) => void;
|
|
8
|
+
readText?: (path: string, limit: number) => Promise<Uint8Array>;
|
|
9
|
+
writeNew?: (path: string, bytes: Uint8Array) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
/** Hives logs (query and live SSE), custom domains, regions, the Hives identity, Hives fleets and remote sessions/SFTP. */
|
|
12
|
+
export declare function registerHivesPlatform(hives: Command, deps: Deps): void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
const SFTP_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
const MANIFEST_BYTES = 64 * 1024;
|
|
4
|
+
function integer(value, name, min, max) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max) {
|
|
8
|
+
throw new Error(`${name} must be an integer from ${min} to ${max}.`);
|
|
9
|
+
}
|
|
10
|
+
return Number(value);
|
|
11
|
+
}
|
|
12
|
+
async function readBounded(path, limit) {
|
|
13
|
+
const metadata = await stat(path);
|
|
14
|
+
if (!metadata.isFile() || metadata.size > limit)
|
|
15
|
+
throw new Error(`Choose a regular file of at most ${limit} bytes.`);
|
|
16
|
+
const bytes = await readFile(path);
|
|
17
|
+
if (bytes.byteLength > limit)
|
|
18
|
+
throw new Error(`File exceeds ${limit} bytes.`);
|
|
19
|
+
return Uint8Array.from(bytes);
|
|
20
|
+
}
|
|
21
|
+
/** Hives logs (query and live SSE), custom domains, regions, the Hives identity, Hives fleets and remote sessions/SFTP. */
|
|
22
|
+
export function registerHivesPlatform(hives, deps) {
|
|
23
|
+
const output = (data) => deps.emit(data, () => JSON.stringify(data, null, 2));
|
|
24
|
+
const line = deps.line ?? ((text) => process.stdout.write(text + '\n'));
|
|
25
|
+
const read = deps.readText ?? readBounded;
|
|
26
|
+
const writeNew = deps.writeNew ?? ((path, bytes) => writeFile(path, bytes, { flag: 'wx', mode: 0o600 }));
|
|
27
|
+
const api = async () => (await deps.client()).hives;
|
|
28
|
+
const logs = hives.command('logs').description('Project logs: retained query and live Server-Sent Events');
|
|
29
|
+
logs.command('query <project_id>').option('--level <level>').option('--source <source>').option('--search <text>')
|
|
30
|
+
.option('--start <time>', 'inclusive UTC timestamp').option('--end <time>', 'exclusive UTC timestamp').option('--request-id <id>')
|
|
31
|
+
.option('--page <number>').option('--per-page <number>')
|
|
32
|
+
.action(async (project, o) => output(await (await api()).logs.query(project, { level: o.level, source: o.source, search: o.search, start: o.start, end: o.end, request_id: o.requestId,
|
|
33
|
+
page: integer(o.page, '--page', 1, 10000), per_page: integer(o.perPage, '--per-page', 1, 100) })));
|
|
34
|
+
logs.command('stream <project_id>').description('Live events. Bounded by --max-events and --seconds; --follow prints each event as it arrives')
|
|
35
|
+
.option('--level <level>').option('--source <source>').option('--heartbeat', 'ask the server for heartbeat events')
|
|
36
|
+
.option('--max-events <number>', 'stop after this many events (1-10000; default 100, unlimited with --follow)')
|
|
37
|
+
.option('--seconds <number>', 'stop after this many seconds (1-300; default 10, or 300 with --follow)')
|
|
38
|
+
.option('--follow', 'print one JSON event per line as it arrives')
|
|
39
|
+
.action(async (project, o) => {
|
|
40
|
+
const q = { level: o.level, source: o.source, ...(o.heartbeat ? { heartbeat: true } : {}) };
|
|
41
|
+
const maxEvents = integer(o.maxEvents, '--max-events', 1, 10000);
|
|
42
|
+
const seconds = integer(o.seconds, '--seconds', 1, 300) ?? (o.follow ? 300 : 10);
|
|
43
|
+
const client = await api();
|
|
44
|
+
if (!o.follow) {
|
|
45
|
+
output(await client.logs.window(project, q, { maxEvents: maxEvents ?? 100, seconds }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const stop = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => stop.abort(), seconds * 1000);
|
|
50
|
+
let count = 0;
|
|
51
|
+
try {
|
|
52
|
+
for await (const event of client.logs.stream(project, q, { signal: stop.signal })) {
|
|
53
|
+
line(JSON.stringify(event));
|
|
54
|
+
count++;
|
|
55
|
+
if (maxEvents !== undefined && count >= maxEvents)
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Reaching --seconds closes the connection; that is the bounded end, not a failure.
|
|
61
|
+
if (!stop.signal.aborted)
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const domains = hives.command('domains').description('Custom domains for a project: add, DNS verification and certificate provisioning');
|
|
69
|
+
domains.command('list <project_id>').option('--page <number>').option('--per-page <number>')
|
|
70
|
+
.action(async (project, o) => output(await (await api()).domains.list(project, { page: integer(o.page, '--page', 1, 10000), per_page: integer(o.perPage, '--per-page', 1, 100) })));
|
|
71
|
+
domains.command('add <project_id> <domain>').action(async (project, domain) => output(await (await api()).domains.add(project, { domain })));
|
|
72
|
+
domains.command('verify <project_id> <domain_id>').description('Check the DNS verification record now')
|
|
73
|
+
.action(async (project, id) => output(await (await api()).domains.verify(project, id)));
|
|
74
|
+
domains.command('provision-ssl <project_id> <domain_id>').description('Request a certificate for a verified domain')
|
|
75
|
+
.action(async (project, id) => output(await (await api()).domains.provisionSsl(project, id)));
|
|
76
|
+
hives.command('regions').description('Regions Hives can place projects in').command('list')
|
|
77
|
+
.action(async () => output(await (await api()).regions.list()));
|
|
78
|
+
hives.command('auth').description('The identity Hives sees for this credential').command('me')
|
|
79
|
+
.action(async () => output(await (await api()).auth.me()));
|
|
80
|
+
const fleets = hives.command('fleets').description('Hives fleets deployed from a map-fleet.toml manifest');
|
|
81
|
+
fleets.command('list').action(async () => output(await (await api()).fleets.list()));
|
|
82
|
+
fleets.command('deploy').requiredOption('--manifest-file <path>', 'map-fleet.toml manifest (at most 64 KiB)')
|
|
83
|
+
.action(async (o) => {
|
|
84
|
+
const manifestToml = new TextDecoder('utf-8', { fatal: true }).decode(await read(o.manifestFile, MANIFEST_BYTES));
|
|
85
|
+
output(await (await api()).fleets.deploy({ manifestToml }));
|
|
86
|
+
});
|
|
87
|
+
fleets.command('status <fleet_id>').action(async (id) => output(await (await api()).fleets.status(id)));
|
|
88
|
+
fleets.command('teardown <fleet_id>').action(async (id) => {
|
|
89
|
+
const result = await (await api()).fleets.teardown(id);
|
|
90
|
+
output(result ?? { torn_down: id });
|
|
91
|
+
});
|
|
92
|
+
const remote = hives.command('remote').description('Remote SSH, terminal and debug sessions, and SFTP, for a project target');
|
|
93
|
+
const sessions = remote.command('sessions').description('Open remote sessions');
|
|
94
|
+
sessions.command('list <project_id>').option('--type <session_type>', 'ssh, terminal or debug').option('--target <target_id>')
|
|
95
|
+
.action(async (project, o) => output(await (await api()).remote.sessions.list(project, { session_type: o.type, target_id: o.target })));
|
|
96
|
+
sessions.command('close <project_id> <session_id>').action(async (project, id) => output(await (await api()).remote.sessions.close(project, id)));
|
|
97
|
+
remote.command('ssh <project_id> <target_id>').requiredOption('--public-key-file <path>', 'your SSH public key (never a private key)')
|
|
98
|
+
.option('--ttl-secs <seconds>', 'session lifetime (60-86400; default 3600)')
|
|
99
|
+
.action(async (project, target, o) => {
|
|
100
|
+
const public_key = new TextDecoder('utf-8', { fatal: true }).decode(await read(o.publicKeyFile, 16 * 1024)).trim();
|
|
101
|
+
if (/PRIVATE KEY/.test(public_key) || !/^(ssh-|ecdsa-|sk-)/.test(public_key))
|
|
102
|
+
throw new Error('Provide an OpenSSH public key file (for example ~/.ssh/id_ed25519.pub).');
|
|
103
|
+
output(await (await api()).remote.ssh(project, { target_id: target, public_key, ttl_secs: integer(o.ttlSecs, '--ttl-secs', 60, 86400) }));
|
|
104
|
+
});
|
|
105
|
+
remote.command('terminal <project_id> <target_id>').option('--shell <path>').option('--cols <number>').option('--rows <number>')
|
|
106
|
+
.action(async (project, target, o) => output(await (await api()).remote.terminal(project, { target_id: target, shell: o.shell, cols: integer(o.cols, '--cols', 1, 1000), rows: integer(o.rows, '--rows', 1, 1000) })));
|
|
107
|
+
remote.command('debug <project_id> <target_id>').option('--protocol <protocol>', 'dap or chrome-devtools')
|
|
108
|
+
.action(async (project, target, o) => output(await (await api()).remote.debug(project, { target_id: target, protocol: o.protocol })));
|
|
109
|
+
const sftp = remote.command('sftp').description('Files on a remote target');
|
|
110
|
+
sftp.command('ls <project_id> <target_id>').option('--path <path>')
|
|
111
|
+
.action(async (project, target, o) => output(await (await api()).remote.sftp.list(project, target, { path: o.path })));
|
|
112
|
+
sftp.command('read <project_id> <target_id> <path>').option('--output-file <path>', 'write the decoded content_base64 to a new private file')
|
|
113
|
+
.action(async (project, target, path, o) => {
|
|
114
|
+
const result = await (await api()).remote.sftp.read(project, target, path);
|
|
115
|
+
if (!o.outputFile) {
|
|
116
|
+
output(result);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const encoded = result?.content_base64;
|
|
120
|
+
if (typeof encoded !== 'string' || encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded))
|
|
121
|
+
throw new Error('Hives returned no valid content_base64 for this file.');
|
|
122
|
+
const decoded = Buffer.from(encoded, 'base64');
|
|
123
|
+
if (decoded.toString('base64') !== encoded)
|
|
124
|
+
throw new Error('Hives returned no valid content_base64 for this file.');
|
|
125
|
+
const bytes = Uint8Array.from(decoded);
|
|
126
|
+
await writeNew(o.outputFile, bytes);
|
|
127
|
+
output({ saved: true, bytes: bytes.byteLength });
|
|
128
|
+
});
|
|
129
|
+
sftp.command('write <project_id> <target_id> <path>').requiredOption('--file <local_path>', 'local file to upload (at most 10 MiB)')
|
|
130
|
+
.action(async (project, target, path, o) => output(await (await api()).remote.sftp.write(project, target, { path, content_base64: Buffer.from(await read(o.file, SFTP_BYTES)).toString('base64') })));
|
|
131
|
+
sftp.command('delete <project_id> <target_id> <path>')
|
|
132
|
+
.action(async (project, target, path) => output(await (await api()).remote.sftp.delete(project, target, { path })));
|
|
133
|
+
}
|
|
@@ -5,5 +5,6 @@ type Deps = {
|
|
|
5
5
|
emit: (data: unknown, pretty: () => string) => void;
|
|
6
6
|
readRequest?: (path: string, limit: number) => Promise<Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
|
+
export declare function bytes(path: string): Promise<Uint8Array>;
|
|
8
9
|
export declare function registerHivesResources(hives: Command, deps: Deps): void;
|
|
9
10
|
export {};
|
package/dist/hives-resources.js
CHANGED
|
@@ -2,7 +2,7 @@ import { registerHivesMarketplace } from './hives-marketplace.js';
|
|
|
2
2
|
import { createReadStream } from 'node:fs';
|
|
3
3
|
import { stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { readFunctionJson } from './function-input.js';
|
|
5
|
-
async function bytes(path) {
|
|
5
|
+
export async function bytes(path) {
|
|
6
6
|
const metadata = await stat(path);
|
|
7
7
|
if (!metadata.isFile() || metadata.size > 10 * 1024 * 1024)
|
|
8
8
|
throw new Error('Choose a regular file of at most 10 MiB.');
|
|
@@ -23,6 +23,8 @@ async function bytes(path) {
|
|
|
23
23
|
}
|
|
24
24
|
return Uint8Array.from(Buffer.concat(chunks, total));
|
|
25
25
|
}
|
|
26
|
+
/** Funded writes (topic publish, Cells object writes) carry one Idempotency-Key; without the option a fresh one is sent. */
|
|
27
|
+
const FUNDED_KEY = '1-128 printable ASCII; reuse it only to replay the same write (a fresh key is sent when omitted)';
|
|
26
28
|
function number(value, min, max) { if (value === undefined)
|
|
27
29
|
return undefined; if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > max)
|
|
28
30
|
throw new Error('Invalid bounded numeric option.'); return Number(value); }
|
|
@@ -43,7 +45,7 @@ export function registerHivesResources(hives, deps) {
|
|
|
43
45
|
const objects = cells.command('objects');
|
|
44
46
|
objects.command('list <project_id> <bucket_id>').option('--prefix <value>').option('--delimiter <value>').option('--continuation-token <value>').option('--max-keys <number>').action(async (project, id, o) => output(await (await deps.client()).hives.cells.objects.list(project, id, { prefix: o.prefix, delimiter: o.delimiter, continuation_token: o.continuationToken, max_keys: number(o.maxKeys, 1, 1000) })));
|
|
45
47
|
for (const action of ['put', 'put-flat'])
|
|
46
|
-
objects.command(`${action} <project_id> <bucket_id> <key>`).requiredOption('--file <path>', 'Exact binary bytes up to 10 MiB').option('--content-type <value>').action(async (project, id, key, o) => { const client = await deps.client(); output(await client.hives.cells.objects[action === 'put' ? 'put' : 'putFlat'](project, id, key, await bytes(o.file), { content_type: o.contentType })); });
|
|
48
|
+
objects.command(`${action} <project_id> <bucket_id> <key>`).requiredOption('--file <path>', 'Exact binary bytes up to 10 MiB').option('--content-type <value>').option('--idempotency-key <key>', FUNDED_KEY).action(async (project, id, key, o) => { const client = await deps.client(); output(await client.hives.cells.objects[action === 'put' ? 'put' : 'putFlat'](project, id, key, await bytes(o.file), { content_type: o.contentType, idempotencyKey: o.idempotencyKey })); });
|
|
47
49
|
for (const action of ['remove', 'remove-flat'])
|
|
48
50
|
objects.command(`${action} <project_id> <bucket_id> <key>`).action(async (project, id, key) => { await (await deps.client()).hives.cells.objects[action === 'remove' ? 'remove' : 'removeFlat'](project, id, key); output({ acknowledged: true }); });
|
|
49
51
|
objects.command('download <project_id> <bucket_id> <key>').requiredOption('--output-file <path>', 'Create a new file; existing files are never overwritten').action(async (project, id, key, o) => { const response = await (await deps.client()).hives.cells.objects.download(project, id, key); const data = new Uint8Array(await response.arrayBuffer()); await writeFile(o.outputFile, data, { flag: 'wx', mode: 0o600 }); output({ saved: true, bytes: data.byteLength }); });
|
|
@@ -75,7 +77,7 @@ export function registerHivesResources(hives, deps) {
|
|
|
75
77
|
for (const action of ['get', 'remove'])
|
|
76
78
|
topics.command(`${action} <project_id> <topic_id>`).action(async (project, id) => output(await (await deps.client()).hives.topics[action](project, id)));
|
|
77
79
|
topics.command('create <project_id>').requiredOption('--request-file <path>', 'JSON topic name').action(async (project, o) => output(await (await deps.client()).hives.topics.create(project, await read(o.requestFile, 4096))));
|
|
78
|
-
topics.command('publish <project_id> <topic_id>').requiredOption('--request-file <path>', 'Exactly body or body_base64 plus optional string attributes; no automatic retry').action(async (project, id, o) => output(await (await deps.client()).hives.topics.publish(project, id, await read(o.requestFile, 512 * 1024))));
|
|
80
|
+
topics.command('publish <project_id> <topic_id>').requiredOption('--request-file <path>', 'Exactly body or body_base64 plus optional string attributes; no automatic retry').option('--idempotency-key <key>', FUNDED_KEY).action(async (project, id, o) => output(await (await deps.client()).hives.topics.publish(project, id, await read(o.requestFile, 512 * 1024), { idempotencyKey: o.idempotencyKey })));
|
|
79
81
|
const subscriptions = topics.command('subscriptions').description('Same-project standard queue destinations; removing a subscription retains queued messages');
|
|
80
82
|
subscriptions.command('list <project_id> <topic_id>').action(async (project, id) => output(await (await deps.client()).hives.topics.subscriptions.list(project, id)));
|
|
81
83
|
subscriptions.command('add <project_id> <topic_id> <queue_id>').action(async (project, id, queue) => output(await (await deps.client()).hives.topics.subscriptions.add(project, id, queue)));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { Genesis } from '@onesi/genesis';
|
|
3
|
+
type Dependencies = {
|
|
4
|
+
client: () => Promise<Genesis>;
|
|
5
|
+
orgId: (client: Genesis) => Promise<string>;
|
|
6
|
+
emit: (data: unknown, pretty: () => string) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* `genesis projects` and `genesis workspaces`: the IAM organization directory of the org you are
|
|
10
|
+
* signed in to (or --org). IAM accepts only a One account login; API keys get IAM_HUMAN_SESSION_REQUIRED.
|
|
11
|
+
* Writes need iam:write and owner/admin authority in the organization. The gateway binds the organization
|
|
12
|
+
* context (Auth organization-context exchange) itself; this CLI sends one request and changes no session or context.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerIamDirectory(program: Command, dependencies: Dependencies): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { table } from './output.js';
|
|
3
|
+
const KEY = ['--idempotency-key <key>', 'Reuse the key from an uncertain earlier attempt to replay it; otherwise a new one is minted'];
|
|
4
|
+
const rows = (r) => (r?.items ?? []);
|
|
5
|
+
/**
|
|
6
|
+
* `genesis projects` and `genesis workspaces`: the IAM organization directory of the org you are
|
|
7
|
+
* signed in to (or --org). IAM accepts only a One account login; API keys get IAM_HUMAN_SESSION_REQUIRED.
|
|
8
|
+
* Writes need iam:write and owner/admin authority in the organization. The gateway binds the organization
|
|
9
|
+
* context (Auth organization-context exchange) itself; this CLI sends one request and changes no session or context.
|
|
10
|
+
*/
|
|
11
|
+
export function registerIamDirectory(program, dependencies) {
|
|
12
|
+
const { client, orgId, emit } = dependencies;
|
|
13
|
+
const scope = async () => { const gen = await client(); return { gen, org: await orgId(gen) }; };
|
|
14
|
+
const projects = program.command('projects').description('Projects in your organization (One account login; writes need owner or admin)');
|
|
15
|
+
projects.command('list', { isDefault: true }).action(async () => {
|
|
16
|
+
const { gen, org } = await scope();
|
|
17
|
+
const r = await gen.projects.list(org);
|
|
18
|
+
emit(r, () => table(rows(r), ['id', 'name', 'slug']));
|
|
19
|
+
});
|
|
20
|
+
projects.command('get <project_id>').action(async (id) => {
|
|
21
|
+
const { gen, org } = await scope();
|
|
22
|
+
const r = await gen.projects.get(org, id);
|
|
23
|
+
emit(r, () => table([r], ['id', 'name', 'slug', 'organization_id']));
|
|
24
|
+
});
|
|
25
|
+
projects.command('create <name>').description('Create a project').requiredOption('--slug <slug>', 'unique within the organization').option(...KEY)
|
|
26
|
+
.action(async (name, o) => {
|
|
27
|
+
const { gen, org } = await scope();
|
|
28
|
+
const r = await gen.projects.create(org, { name, slug: o.slug }, o.idempotencyKey ?? randomUUID());
|
|
29
|
+
emit(r, () => table([r], ['id', 'name', 'slug']));
|
|
30
|
+
});
|
|
31
|
+
projects.command('rename <project_id> <name>').description('Rename a project').option('--slug <slug>', 'also change the slug')
|
|
32
|
+
.action(async (id, name, o) => {
|
|
33
|
+
const { gen, org } = await scope();
|
|
34
|
+
const r = await gen.projects.update(org, id, { name, ...(o.slug ? { slug: o.slug } : {}) });
|
|
35
|
+
emit(r, () => table([r], ['id', 'name', 'slug']));
|
|
36
|
+
});
|
|
37
|
+
projects.command('delete <project_id>').description('Permanently delete a project (refused while workspaces, fleets or grants use it)')
|
|
38
|
+
.action(async (id) => {
|
|
39
|
+
const { gen, org } = await scope();
|
|
40
|
+
await gen.projects.delete(org, id);
|
|
41
|
+
emit({ deleted: id }, () => `Deleted project ${id}.`);
|
|
42
|
+
});
|
|
43
|
+
const workspaces = program.command('workspaces').description('Workspaces in your organization (One account login; writes need owner or admin)');
|
|
44
|
+
workspaces.command('list', { isDefault: true }).action(async () => {
|
|
45
|
+
const { gen, org } = await scope();
|
|
46
|
+
const r = await gen.workspaces.list(org);
|
|
47
|
+
emit(r, () => table(rows(r), ['id', 'name', 'projectId']));
|
|
48
|
+
});
|
|
49
|
+
workspaces.command('get <workspace_id>').action(async (id) => {
|
|
50
|
+
const { gen, org } = await scope();
|
|
51
|
+
const r = await gen.workspaces.get(org, id);
|
|
52
|
+
emit(r, () => table([r], ['id', 'name', 'projectId', 'organizationId']));
|
|
53
|
+
});
|
|
54
|
+
workspaces.command('create <name>').description('Create a workspace').option('--project <project_id>', 'the project it belongs to').option(...KEY)
|
|
55
|
+
.action(async (name, o) => {
|
|
56
|
+
const { gen, org } = await scope();
|
|
57
|
+
const r = await gen.workspaces.create(org, { name, ...(o.project ? { project_id: o.project } : {}) }, o.idempotencyKey ?? randomUUID());
|
|
58
|
+
emit(r, () => table([r], ['id', 'name', 'projectId']));
|
|
59
|
+
});
|
|
60
|
+
workspaces.command('rename <workspace_id> <name>').description('Rename a workspace').action(async (id, name) => {
|
|
61
|
+
const { gen, org } = await scope();
|
|
62
|
+
const r = await gen.workspaces.rename(org, id, name);
|
|
63
|
+
emit(r, () => table([r], ['id', 'name', 'projectId']));
|
|
64
|
+
});
|
|
65
|
+
workspaces.command('delete <workspace_id>').description('Permanently delete a workspace (refused while fleets or grants use it)')
|
|
66
|
+
.action(async (id) => {
|
|
67
|
+
const { gen, org } = await scope();
|
|
68
|
+
await gen.workspaces.delete(org, id);
|
|
69
|
+
emit({ deleted: id }, () => `Deleted workspace ${id}.`);
|
|
70
|
+
});
|
|
71
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface Deps {
|
|
|
12
12
|
openBrowser?: (url: string) => void;
|
|
13
13
|
sleep?: (ms: number) => Promise<void>;
|
|
14
14
|
readSecretInput?: (options: SecretInputOptions, limit: number) => Promise<string>;
|
|
15
|
+
/** Reads the pasted sign-in code for `flocks accounts login` and `flocks jobs login-input` (one line, never echoed). */
|
|
16
|
+
readFlocksCode?: () => Promise<string>;
|
|
15
17
|
readWasmFile?: (path: string) => Promise<Blob>;
|
|
16
18
|
writeBytes?: (bytes: Uint8Array) => void;
|
|
17
19
|
writeOutputFile?: (path: string, bytes: Uint8Array) => Promise<void>;
|