@onesi/genesis-cli 0.0.1-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/bin/genesis.js +3 -0
- package/dist/auth.d.ts +30 -0
- package/dist/auth.js +273 -0
- package/dist/beacons.d.ts +3 -0
- package/dist/beacons.js +162 -0
- package/dist/device-login.d.ts +25 -0
- package/dist/device-login.js +173 -0
- package/dist/dials.d.ts +7 -0
- package/dist/dials.js +99 -0
- package/dist/fleets.d.ts +4 -0
- package/dist/fleets.js +102 -0
- package/dist/flocks.d.ts +9 -0
- package/dist/flocks.js +326 -0
- package/dist/function-input.d.ts +3 -0
- package/dist/function-input.js +49 -0
- package/dist/henry.d.ts +7 -0
- package/dist/henry.js +85 -0
- package/dist/hives-coverage.d.ts +12 -0
- package/dist/hives-coverage.js +116 -0
- package/dist/hives-marketplace.d.ts +10 -0
- package/dist/hives-marketplace.js +29 -0
- package/dist/hives-node-transport.d.ts +6 -0
- package/dist/hives-node-transport.js +150 -0
- package/dist/hives-operations.d.ts +10 -0
- package/dist/hives-operations.js +63 -0
- package/dist/hives-platform.d.ts +13 -0
- package/dist/hives-platform.js +133 -0
- package/dist/hives-resources.d.ts +9 -0
- package/dist/hives-resources.js +94 -0
- package/dist/iam-directory.d.ts +15 -0
- package/dist/iam-directory.js +71 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +872 -0
- package/dist/lifeid-native.d.ts +24 -0
- package/dist/lifeid-native.js +113 -0
- package/dist/minds-owner.d.ts +3 -0
- package/dist/minds-owner.js +42 -0
- package/dist/minds.d.ts +3 -0
- package/dist/minds.js +125 -0
- 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 +28 -0
- package/dist/output.js +46 -0
- package/dist/products.d.ts +18 -0
- package/dist/products.js +311 -0
- 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 +5 -0
- package/dist/relays-commands/zones.js +9 -0
- package/dist/relays-enrollment.d.ts +3 -0
- package/dist/relays-enrollment.js +7 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +57 -0
- package/dist/secret-input.d.ts +14 -0
- package/dist/secret-input.js +85 -0
- package/dist/store.d.ts +39 -0
- package/dist/store.js +33 -0
- package/dist/wasm-input.d.ts +2 -0
- package/dist/wasm-input.js +31 -0
- package/package.json +54 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import { registerMinds } from './minds.js';
|
|
2
|
+
import { registerRelaysEnrollment } from './relays-enrollment.js';
|
|
3
|
+
import { RELAYS_COMMANDS } from './relays-commands/index.js';
|
|
4
|
+
import { registerBeacons } from './beacons.js';
|
|
5
|
+
import { registerDials } from './dials.js';
|
|
6
|
+
import { registerIamDirectory } from './iam-directory.js';
|
|
7
|
+
import { registerOperationCommands, registerProducts } from './products.js';
|
|
8
|
+
import { registerOrgs } from './orgs.js';
|
|
9
|
+
import { Command, CommanderError } from 'commander';
|
|
10
|
+
import { Genesis, GenesisError, errorCatalog, errorOutcome, isMindsOperation, HivesOperationError, HivesQueueOperationError, MindsAgentError, MindsOwnerError, hivesResponseBytes, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES, DEFAULT_BASE_URL, requireSecureUrl } from '@onesi/genesis';
|
|
11
|
+
import { authConfig, completeLogin, selectSavedContext, savedLoginProvider, startLogin } from './auth.js';
|
|
12
|
+
import { completeDeviceLogin, startDeviceLogin } from './device-login.js';
|
|
13
|
+
import { keychainStore } from './store.js';
|
|
14
|
+
import { c, listOf, safe, stdio, table } from './output.js';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { addHenryCommands } from './henry.js';
|
|
17
|
+
import { registerFleets } from './fleets.js';
|
|
18
|
+
import { registerFlocks } from './flocks.js';
|
|
19
|
+
import { registerRunner } from './runner.js';
|
|
20
|
+
import { createHivesNodeTransport } from './hives-node-transport.js';
|
|
21
|
+
import { registerHivesOperations } from './hives-operations.js';
|
|
22
|
+
import { registerHivesCoverage } from './hives-coverage.js';
|
|
23
|
+
import { readFunctionFile, readFunctionJson, writeFunctionOutput } from './function-input.js';
|
|
24
|
+
import { basename } from 'node:path';
|
|
25
|
+
import { readWasmFile } from './wasm-input.js';
|
|
26
|
+
import { readSecretInput, validateSecret, validateSecretValue, parseSecretBatch } from './secret-input.js';
|
|
27
|
+
function defaultOpen(url) {
|
|
28
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
29
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
30
|
+
try {
|
|
31
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => { }).unref();
|
|
32
|
+
}
|
|
33
|
+
catch { /* print-only fallback */ }
|
|
34
|
+
}
|
|
35
|
+
export function buildProgram(deps = {}) {
|
|
36
|
+
const out = deps.out ?? stdio;
|
|
37
|
+
const env = deps.env ?? process.env;
|
|
38
|
+
const f = deps.fetch ?? fetch;
|
|
39
|
+
const getStore = deps.store ?? keychainStore;
|
|
40
|
+
const auth = { ...authConfig(env, f), sleep: deps.sleep };
|
|
41
|
+
const program = new Command('genesis')
|
|
42
|
+
.description('One CLI for every Genesis service.')
|
|
43
|
+
.version('0.2.0')
|
|
44
|
+
.option('--json', 'machine-readable JSON output')
|
|
45
|
+
.option('--api-key <key>', 'use this API key (default: GENESIS_API_KEY, then your `genesis login`)')
|
|
46
|
+
.option('--base-url <url>', 'gateway URL (default: GENESIS_BASE_URL or https://os.one.ml/api)')
|
|
47
|
+
.option('--direct', 'skip the gateway and call each service host directly (for local dev)')
|
|
48
|
+
.option('--org <org_id>', 'organization to act on')
|
|
49
|
+
.showHelpAfterError()
|
|
50
|
+
.exitOverride()
|
|
51
|
+
.configureOutput({ writeOut: (s) => out.log(s.trimEnd()), writeErr: (s) => out.err(s.trimEnd()) });
|
|
52
|
+
const g = () => program.opts();
|
|
53
|
+
const client = async (hivesTransport = false) => {
|
|
54
|
+
const o = g();
|
|
55
|
+
const store = await getStore().catch(() => undefined);
|
|
56
|
+
const opts = {
|
|
57
|
+
apiKey: o.apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN,
|
|
58
|
+
baseUrl: o.baseUrl ?? env.GENESIS_BASE_URL, routing: o.direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway',
|
|
59
|
+
fetch: f, userAgent: 'genesis-cli/0.2.0',
|
|
60
|
+
credentials: store ? savedLoginProvider(auth, store) : undefined,
|
|
61
|
+
};
|
|
62
|
+
return new Genesis(hivesTransport ? { ...opts, fetch: createHivesNodeTransport(opts, deps.signal) } : opts);
|
|
63
|
+
};
|
|
64
|
+
const emit = (data, pretty) => out.log(g().json ? JSON.stringify(data, null, 2) : pretty());
|
|
65
|
+
const positiveInteger = (value, option, maximum) => {
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1 || Number(value) > maximum) {
|
|
69
|
+
throw new Error(`${option} must be an integer from 1 to ${maximum}.`);
|
|
70
|
+
}
|
|
71
|
+
return Number(value);
|
|
72
|
+
};
|
|
73
|
+
const projectName = (name) => {
|
|
74
|
+
if (name.length < 3 || name.length > 64 || !/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name)) {
|
|
75
|
+
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.');
|
|
76
|
+
}
|
|
77
|
+
return name;
|
|
78
|
+
};
|
|
79
|
+
registerRunner(program, client, emit);
|
|
80
|
+
const orgId = async (gen) => {
|
|
81
|
+
if (g().org)
|
|
82
|
+
return g().org;
|
|
83
|
+
const s = await (await getStore().catch(() => undefined))?.get();
|
|
84
|
+
if (s?.organization_id && !g().apiKey && !env.GENESIS_API_KEY)
|
|
85
|
+
return s.organization_id;
|
|
86
|
+
return (await gen.whoami()).org;
|
|
87
|
+
};
|
|
88
|
+
const silos = program.command('silos').description('Serverless PostgreSQL');
|
|
89
|
+
const silosInstances = silos.command('instances').description('Database instances in the selected Genesis project (genesis select)');
|
|
90
|
+
silosInstances.command('list').action(async () => out.log(JSON.stringify(await (await client()).silos.instances.list(), null, 2)));
|
|
91
|
+
silosInstances.command('get <instance_id>').action(async (id) => out.log(JSON.stringify(await (await client()).silos.instances.get(id), null, 2)));
|
|
92
|
+
silosInstances.command('create').requiredOption('--name <name>', 'Instance name (lowercase, 1-63 characters)')
|
|
93
|
+
.action(async (options) => out.log(JSON.stringify(await (await client()).silos.instances.create({ name: options.name }), null, 2)));
|
|
94
|
+
const silosJson = (value) => out.log(JSON.stringify(value, null, 2));
|
|
95
|
+
silosInstances.command('rename <instance_id>').requiredOption('--name <name>', 'New instance name (lowercase, 1-63 characters)')
|
|
96
|
+
.action(async (id, options) => silosJson(await (await client()).silos.instances.update(id, { name: options.name })));
|
|
97
|
+
silosInstances.command('delete <instance_id>').description('Delete an instance and stop it; its name is free again at once')
|
|
98
|
+
.action(async (id) => silosJson((await (await client()).silos.instances.delete(id)) ?? { deleted: id }));
|
|
99
|
+
silosInstances.command('suspend <instance_id>').description('Checkpoint, then stop the instance')
|
|
100
|
+
.action(async (id) => silosJson(await (await client()).silos.instances.suspend(id)));
|
|
101
|
+
silosInstances.command('resume <instance_id>').description('Start the instance now')
|
|
102
|
+
.action(async (id) => silosJson(await (await client()).silos.instances.resume(id)));
|
|
103
|
+
silosInstances.command('query <instance_id>').description('Run one SQL statement (never replayed)')
|
|
104
|
+
.requiredOption('--sql <sql>', 'SQL statement').option('--params <json>', 'JSON array of positional parameters for $1, $2, …')
|
|
105
|
+
.option('--branch <branch_id>', 'run on this branch instead of the default branch')
|
|
106
|
+
.action(async (id, options) => {
|
|
107
|
+
let params;
|
|
108
|
+
if (options.params !== undefined) {
|
|
109
|
+
try {
|
|
110
|
+
params = JSON.parse(options.params);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
params = undefined;
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(params))
|
|
116
|
+
throw new Error('--params must be a JSON array, for example \'[1,"a"]\'.');
|
|
117
|
+
}
|
|
118
|
+
silosJson(await (await client()).silos.instances.query(id, { sql: options.sql, ...(params ? { params } : {}), ...(options.branch ? { branchId: options.branch } : {}) }));
|
|
119
|
+
});
|
|
120
|
+
silosInstances.command('batch <instance_id>').description('Run up to 50 statements in one transaction (never replayed)')
|
|
121
|
+
.option('--file <path>', 'UTF-8 JSON file: {"statements":[{"sql":"…","params":[…]}]}').option('--stdin', 'read that JSON from stdin')
|
|
122
|
+
.action(async (id, o) => {
|
|
123
|
+
let body;
|
|
124
|
+
if (Boolean(o.file) === Boolean(o.stdin))
|
|
125
|
+
throw new Error('Choose exactly one input source: --file or --stdin.');
|
|
126
|
+
const text = await (deps.readSecretInput ?? readSecretInput)(o, 64 * 1024);
|
|
127
|
+
try {
|
|
128
|
+
body = JSON.parse(text);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new Error('Batch input must be JSON: {"statements":[{"sql":"…"}]}.');
|
|
132
|
+
}
|
|
133
|
+
silosJson(await (await client()).silos.instances.batch(id, body));
|
|
134
|
+
});
|
|
135
|
+
silosInstances.command('metrics <instance_id>').description('Live counters, storage and activity times')
|
|
136
|
+
.action(async (id) => silosJson(await (await client()).silos.instances.metrics(id)));
|
|
137
|
+
silosInstances.command('listen <instance_id>').description('Wait for NOTIFY on channels (up to --wait ms, at most 15000)')
|
|
138
|
+
.requiredOption('--channel <name...>', 'channel to listen on (repeatable, up to 10)').option('--wait <ms>', 'how long to wait (default 10000)')
|
|
139
|
+
.option('--branch <branch_id>', 'listen on this branch instead of the default branch')
|
|
140
|
+
.action(async (id, o) => {
|
|
141
|
+
const waitMs = o.wait === undefined ? undefined : Number(o.wait);
|
|
142
|
+
if (waitMs !== undefined && !(Number.isInteger(waitMs) && waitMs >= 0 && waitMs <= 15000))
|
|
143
|
+
throw new Error('--wait must be an integer from 0 to 15000.');
|
|
144
|
+
silosJson(await (await client()).silos.instances.listen(id, { channels: o.channel, ...(waitMs !== undefined ? { waitMs } : {}), ...(o.branch ? { branchId: o.branch } : {}) }));
|
|
145
|
+
});
|
|
146
|
+
silos.command('usage').description('Silos usage of the selected Genesis project, last 30 days')
|
|
147
|
+
.action(async () => silosJson(await (await client()).silos.usage()));
|
|
148
|
+
silos.command('regions').description('Regions Silos instances are created in')
|
|
149
|
+
.action(async () => silosJson(await (await client()).silos.regions.list()));
|
|
150
|
+
const silosBranches = silos.command('branches').description('Branches of a database instance');
|
|
151
|
+
silosBranches.command('list <instance_id>').action(async (id) => silosJson(await (await client()).silos.instances.branches.list(id)));
|
|
152
|
+
silosBranches.command('get <instance_id> <branch_id>').action(async (id, branch) => silosJson(await (await client()).silos.instances.branches.get(id, branch)));
|
|
153
|
+
silosBranches.command('create <instance_id>').description('Copy the latest checkpoint of the parent (default: the default branch)')
|
|
154
|
+
.requiredOption('--name <name>', 'Branch name (lowercase, 1-63 characters)').option('--parent <branch_id>', 'parent branch')
|
|
155
|
+
.action(async (id, o) => silosJson(await (await client()).silos.instances.branches.create(id, o.parent ? { name: o.name, parentBranchId: o.parent } : { name: o.name })));
|
|
156
|
+
silosBranches.command('rename <instance_id> <branch_id>').requiredOption('--name <name>', 'New branch name')
|
|
157
|
+
.action(async (id, branch, o) => silosJson(await (await client()).silos.instances.branches.update(id, branch, { name: o.name })));
|
|
158
|
+
silosBranches.command('delete <instance_id> <branch_id>').description('Delete a branch other than the default branch')
|
|
159
|
+
.action(async (id, branch) => silosJson((await (await client()).silos.instances.branches.delete(id, branch)) ?? { deleted: branch }));
|
|
160
|
+
silosBranches.command('restore <instance_id> <branch_id>').description('Replace the branch data with a stored snapshot')
|
|
161
|
+
.requiredOption('--snapshot <snapshot_id>', 'snapshot id from `genesis silos snapshots list`').option('--from <branch_id>', 'branch the snapshot was taken on (default: this branch)')
|
|
162
|
+
.action(async (id, branch, o) => silosJson(await (await client()).silos.instances.branches.restore(id, branch, o.from ? { snapshotId: o.snapshot, sourceBranchId: o.from } : { snapshotId: o.snapshot })));
|
|
163
|
+
const silosSnapshots = silos.command('snapshots').description('Stored snapshots of a database instance');
|
|
164
|
+
silosSnapshots.command('list <instance_id>').action(async (id) => silosJson(await (await client()).silos.instances.snapshots.list(id)));
|
|
165
|
+
silosSnapshots.command('create <instance_id>').description('Checkpoint a branch and store the snapshot').option('--branch <branch_id>', 'branch (default: the default branch)')
|
|
166
|
+
.action(async (id, o) => silosJson(await (await client()).silos.instances.snapshots.create(id, o.branch ? { branchId: o.branch } : {})));
|
|
167
|
+
const traces = program.command('traces').description('Research and evidence');
|
|
168
|
+
const tracesRuns = traces.command('runs').description('Research runs in your verified organization');
|
|
169
|
+
tracesRuns.command('create').requiredOption('--type <type>', 'Run type, e.g. deep_research').requiredOption('--query <query>', 'Research question')
|
|
170
|
+
.requiredOption('--idempotency-key <key>', 'Stable key; reuse it to replay instead of creating a second run')
|
|
171
|
+
.option('--max-sources <n>', 'Most sources the run may collect (1-100)')
|
|
172
|
+
.option('--include-domain <domain...>', 'Search only these domains, e.g. wikipedia.org')
|
|
173
|
+
.action(async (o) => {
|
|
174
|
+
const config = { ...(o.maxSources !== undefined ? { max_sources: positiveInteger(o.maxSources, '--max-sources', 100) } : {}), ...(o.includeDomain?.length ? { include_domains: o.includeDomain } : {}) };
|
|
175
|
+
out.log(JSON.stringify(await (await client()).traces.runs.create({ type: o.type, query: o.query, ...(Object.keys(config).length ? { config } : {}) }, o.idempotencyKey), null, 2));
|
|
176
|
+
});
|
|
177
|
+
tracesRuns.command('list').action(async () => out.log(JSON.stringify(await (await client()).traces.runs.list(), null, 2)));
|
|
178
|
+
tracesRuns.command('get <run_id>').action(async (id) => out.log(JSON.stringify(await (await client()).traces.runs.get(id), null, 2)));
|
|
179
|
+
traces.command('search <query>').requiredOption('--idempotency-key <key>', 'Stable key; metered once')
|
|
180
|
+
.option('--max-results <n>', 'Results to return (1-20, default 3)')
|
|
181
|
+
.option('--include-domain <domain...>', 'Search only these domains, e.g. wikipedia.org')
|
|
182
|
+
.action(async (query, o) => out.log(JSON.stringify(await (await client()).traces.search({
|
|
183
|
+
query, max_results: positiveInteger(o.maxResults, '--max-results', 20) ?? 3, ...(o.includeDomain?.length ? { include_domains: o.includeDomain } : {})
|
|
184
|
+
}, o.idempotencyKey), null, 2)));
|
|
185
|
+
traces.command('usage').action(async () => out.log(JSON.stringify(await (await client()).traces.usage.events(), null, 2)));
|
|
186
|
+
const blinds = program.command('blinds').description('Blinds credential custody');
|
|
187
|
+
const show = async (call) => out.log(JSON.stringify(await call((await client()).blinds), null, 2));
|
|
188
|
+
const key = (o) => ({ signal: deps.signal, ...(o.idempotencyKey ? { idempotencyKey: o.idempotencyKey } : {}) });
|
|
189
|
+
const keyOption = ['--idempotency-key <uuid>', 'Reuse the UUID from an uncertain earlier attempt; otherwise a new one is minted'];
|
|
190
|
+
const credentials = blinds.command('credentials').description('Credential variable names (values never leave Blinds)');
|
|
191
|
+
credentials.command('list').action(() => show(b => b.credentials.list({ signal: deps.signal })));
|
|
192
|
+
credentials.command('set <variable>').description('Store a static credential from a file or stdin, never argv')
|
|
193
|
+
.requiredOption('--type <type>', 'api_key, bearer, ...').option('--file <path>', 'Read the value from this file').option('--stdin', 'Read the value from stdin')
|
|
194
|
+
.option('--domain <domain...>', 'Destination domains the value may be injected into').option('--provider <slug>').option(...keyOption)
|
|
195
|
+
.action(async (variable, o) => {
|
|
196
|
+
const value = (await (deps.readSecretInput ?? readSecretInput)(o, 16 * 1024)).replace(/\r?\n$/, '');
|
|
197
|
+
await show(b => b.credentials.store({ variable_name: variable, value, credential_type: o.type,
|
|
198
|
+
...(o.domain ? { destination_domains: o.domain } : {}), ...(o.provider ? { provider: o.provider } : {}) }, key(o)));
|
|
199
|
+
});
|
|
200
|
+
credentials.command('revoke <variable>').option(...keyOption).action((variable, o) => show(b => b.credentials.revoke(variable, key(o))));
|
|
201
|
+
credentials.command('revoke-all').description('Revoke every credential in the selected organization (irreversible)')
|
|
202
|
+
.requiredOption('--yes', 'Confirm the bulk revoke').option(...keyOption).action((o) => show(b => b.credentials.revokeAll(key(o))));
|
|
203
|
+
blinds.command('connections').description('Active OAuth connections (metadata only)').action(() => show(b => b.oauth.connections({ signal: deps.signal })));
|
|
204
|
+
const offsetInteger = (value) => {
|
|
205
|
+
if (value === undefined)
|
|
206
|
+
return undefined;
|
|
207
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
208
|
+
throw new Error('--offset must be a non-negative integer.');
|
|
209
|
+
return Number(value);
|
|
210
|
+
};
|
|
211
|
+
const catalog = (o) => {
|
|
212
|
+
const limit = positiveInteger(o.limit, '--limit', 1000), offset = offsetInteger(o.offset);
|
|
213
|
+
return { ...(o.q ? { q: o.q } : {}), ...(o.category ? { category: o.category } : {}),
|
|
214
|
+
...(limit !== undefined ? { limit } : {}), ...(offset !== undefined ? { offset } : {}) };
|
|
215
|
+
};
|
|
216
|
+
const providers = blinds.command('providers').description('Provider catalog: what a credential can be bound to');
|
|
217
|
+
providers.command('list').option('--q <text>', 'Match slug, name or description').option('--category <name>').option('--auth-mode <mode>', 'oauth2, api_key, ...')
|
|
218
|
+
.option('--oauth', 'Only providers that use OAuth 2.0 (cannot be combined with --auth-mode)').option('--limit <n>', 'Page size, 1 to 1000').option('--offset <n>')
|
|
219
|
+
.action((o) => {
|
|
220
|
+
if (o.oauth && o.authMode)
|
|
221
|
+
throw new Error('--oauth already selects auth mode oauth2; drop --auth-mode.');
|
|
222
|
+
const query = catalog(o);
|
|
223
|
+
return show(b => o.oauth ? b.oauth.providers(query, { signal: deps.signal })
|
|
224
|
+
: b.providers.list({ ...query, ...(o.authMode ? { auth_mode: o.authMode } : {}) }, { signal: deps.signal }));
|
|
225
|
+
});
|
|
226
|
+
providers.command('get <slug>').action((slug) => show(b => b.providers.get(slug, { signal: deps.signal })));
|
|
227
|
+
const settings = blinds.command('settings').description('Consent policy and notifications');
|
|
228
|
+
settings.command('get').action(() => show(b => b.settings.get({ signal: deps.signal })));
|
|
229
|
+
settings.command('set').description('Replace the settings: an omitted --email or --slack turns that notification off').requiredOption('--consent-policy <policy>', 'never, per_variable or per_delegate')
|
|
230
|
+
.option('--email', 'Email notifications on').option('--slack', 'Slack notifications on').option(...keyOption)
|
|
231
|
+
.action((o) => show(b => b.settings.update({ consent_policy: o.consentPolicy, email_notifications: !!o.email, slack_notifications: !!o.slack }, key(o))));
|
|
232
|
+
blinds.command('agents').description('Agents seen using Blinds, with request counts').action(() => show(b => b.agents({ signal: deps.signal })));
|
|
233
|
+
blinds.command('alerts').description('Failed or denied Blinds events').action(() => show(b => b.alerts({ signal: deps.signal })));
|
|
234
|
+
blinds.command('operation <operation_id>').description('Receipt for a mutation, by its Idempotency-Key')
|
|
235
|
+
.action((id) => show(b => b.operations.get(id, { signal: deps.signal })));
|
|
236
|
+
const consent = blinds.command('consent').description('Agent consent requests and decisions');
|
|
237
|
+
consent.command('pending').action(() => show(b => b.consent.pending({ signal: deps.signal })));
|
|
238
|
+
consent.command('list').action(() => show(b => b.consent.records({ signal: deps.signal })));
|
|
239
|
+
consent.command('approve <consent_id>').requiredOption('--signature <sig>', 'One account approval signature').option('--expires-in <seconds>').option(...keyOption)
|
|
240
|
+
.action((id, o) => show(b => b.consent.approve(id, { signature: o.signature, ...(o.expiresIn ? { expires_in_seconds: Number(o.expiresIn) } : {}) }, key(o))));
|
|
241
|
+
consent.command('deny <consent_id>').option(...keyOption).action((id, o) => show(b => b.consent.deny(id, key(o))));
|
|
242
|
+
consent.command('revoke <consent_id>').option(...keyOption).action((id, o) => show(b => b.consent.revoke(id, key(o))));
|
|
243
|
+
const delegates = blinds.command('delegates').description('Which agents hold which variables');
|
|
244
|
+
delegates.command('list').action(() => show(b => b.delegates.list({ signal: deps.signal })));
|
|
245
|
+
delegates.command('create <name>').requiredOption('--variable <name...>', 'Variables to delegate').requiredOption('--ttl <seconds>', 'Lifetime in seconds')
|
|
246
|
+
.option('--max-depth <n>', 'Sub-delegation depth', '1').option(...keyOption)
|
|
247
|
+
.action((name, o) => show(b => b.delegates.create({ name, delegated_variables: o.variable, ttl_seconds: Number(o.ttl), max_depth: Number(o.maxDepth) }, key(o))));
|
|
248
|
+
delegates.command('revoke <delegate_id>').option(...keyOption).action((id, o) => show(b => b.delegates.revoke(id, key(o))));
|
|
249
|
+
blinds.command('activity').description('Latest 200 Blinds audit events').action(() => show(b => b.activity({ signal: deps.signal })));
|
|
250
|
+
const owner = blinds.command('owner-enrollment')
|
|
251
|
+
.description('Original-human enrollment only; preserve operation and locator IDs after unknown outcomes');
|
|
252
|
+
for (const [action, operation] of Object.entries({ reserve: 'blinds_ownerReserve', sign: 'blinds_ownerSign', attach: 'blinds_ownerAttach', 'p256-ensure': 'blinds_ownerP256Ensure' })) {
|
|
253
|
+
owner.command(action).requiredOption('--body-file <path>', 'Closed JSON request, up to 2 KiB; original human login required')
|
|
254
|
+
.action(async (options) => {
|
|
255
|
+
const body = await readFunctionJson(options.bodyFile, 2048);
|
|
256
|
+
const result = await (await client()).request(operation, { body }, { signal: deps.signal });
|
|
257
|
+
out.log(JSON.stringify(result, null, 2));
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
registerDials(program, { client, orgId, emit });
|
|
261
|
+
registerIamDirectory(program, { client, orgId, emit });
|
|
262
|
+
registerProducts(program, { client, emit, signal: deps.signal });
|
|
263
|
+
registerOrgs(program, { client, emit, signal: deps.signal });
|
|
264
|
+
// Agent enrollment sends only the grant's enrollment key (mek_...), never this CLI's login or API key.
|
|
265
|
+
(program.commands.find(c => c.name() === 'iam').commands.find(c => c.name() === 'agents')).command('enroll')
|
|
266
|
+
.description('Enroll an agent under an agent enrollment grant (POST /v1/iam/agents/enroll); the credential secret is shown once')
|
|
267
|
+
.requiredOption('--body-file <path>', 'JSON request body (externalKey, displayName, mindId, permissions, ...)')
|
|
268
|
+
.option('--key-file <path>', 'file holding the enrollment key (mek_...)').option('--key-stdin', 'read the enrollment key from stdin')
|
|
269
|
+
.action(async (o) => {
|
|
270
|
+
const key = o.keyFile || o.keyStdin ? (await (deps.readSecretInput ?? readSecretInput)({ file: o.keyFile, stdin: o.keyStdin }, 1024)).trim() : env.GENESIS_ENROLLMENT_KEY;
|
|
271
|
+
if (!key)
|
|
272
|
+
throw new Error('Give the enrollment key with --key-file, --key-stdin or GENESIS_ENROLLMENT_KEY; it is never accepted as an argument.');
|
|
273
|
+
const body = await readFunctionJson(o.bodyFile, 16 * 1024);
|
|
274
|
+
const result = await new Genesis({ baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL, fetch: f, userAgent: 'genesis-cli/0.1.13' }).iam.agents.enroll(key, body);
|
|
275
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
276
|
+
});
|
|
277
|
+
// ---- auth ------------------------------------------------------------
|
|
278
|
+
program.command('login').description('Sign in with One in your browser').option('--no-browser', 'just print the link')
|
|
279
|
+
.option('--device', 'sign in from a machine without a local browser: approve a code from any signed-in browser')
|
|
280
|
+
.action(async (o) => {
|
|
281
|
+
const store = await getStore();
|
|
282
|
+
if (o.device) {
|
|
283
|
+
const start = await startDeviceLogin(auth, deps.signal);
|
|
284
|
+
const uri = safe(start.verificationUri), complete = safe(start.verificationUriComplete), userCode = safe(start.userCode);
|
|
285
|
+
if (g().json)
|
|
286
|
+
out.err(JSON.stringify({ user_code: userCode, verification_uri: uri,
|
|
287
|
+
verification_uri_complete: complete, expires_in: start.expiresIn }));
|
|
288
|
+
else
|
|
289
|
+
out.err(`\n On any device, open ${c.bold(uri)}\n and enter the code ${c.bold(userCode)}\n\n Or open ${complete}\n ${c.dim(`Waiting for approval (expires in ${Math.round(start.expiresIn / 60)} min)…`)}\n`);
|
|
290
|
+
const s = await completeDeviceLogin(auth, start, deps.signal);
|
|
291
|
+
await store.set(s);
|
|
292
|
+
emit({ user: s.user, organization_id: s.organization_id, store: store.kind, via: 'device' }, () => `${c.green('✓')} Signed in as ${c.bold(s.user?.email ?? s.user?.id ?? 'you')}${s.organization_id ? ` (${s.organization_id})` : ''}. Token saved to your ${store.kind === 'os-keychain' ? 'OS keychain' : store.kind}.`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const attempt = await startLogin(auth, { signal: deps.signal });
|
|
296
|
+
let s;
|
|
297
|
+
try {
|
|
298
|
+
out.err(`\n Open ${attempt.browserUrl}\n ${c.dim('Waiting for One account sign-in in the browser…')}\n`);
|
|
299
|
+
if (o.browser)
|
|
300
|
+
(deps.openBrowser ?? defaultOpen)(attempt.browserUrl);
|
|
301
|
+
s = await completeLogin(auth, attempt);
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
attempt.close();
|
|
305
|
+
}
|
|
306
|
+
await store.set(s);
|
|
307
|
+
emit({ user: s.user, organization_id: s.organization_id, store: store.kind }, () => `${c.green('✓')} Signed in as ${c.bold(s.user?.email ?? s.user?.id ?? 'you')}${s.organization_id ? ` (${s.organization_id})` : ''}. Token saved to your ${store.kind === 'os-keychain' ? 'OS keychain' : store.kind}.`);
|
|
308
|
+
});
|
|
309
|
+
program.command('logout').description('Forget your saved login').action(async () => {
|
|
310
|
+
const had = await (await getStore()).clear();
|
|
311
|
+
emit({ logged_out: had }, () => had ? `${c.green('✓')} Signed out.` : 'You were not signed in.');
|
|
312
|
+
});
|
|
313
|
+
program.command('whoami').description('Show who you are signed in as').action(async () => {
|
|
314
|
+
const gen = await client();
|
|
315
|
+
const saved = await (await getStore().catch(() => undefined))?.get();
|
|
316
|
+
try {
|
|
317
|
+
const p = await gen.whoami();
|
|
318
|
+
emit({ ...p, user: saved?.user }, () => [
|
|
319
|
+
`${c.bold('Org')} ${p.org}`, `${c.bold('Principal')} ${p.principal ?? '-'}`,
|
|
320
|
+
`${c.bold('Via')} ${p.kind}`, `${c.bold('Scopes')} ${p.scopes.join(', ') || '-'}`,
|
|
321
|
+
...(saved?.user?.email ? [`${c.bold('User')} ${saved.user.email}`] : [])
|
|
322
|
+
].join('\n'));
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
if (!saved || g().apiKey || env.GENESIS_API_KEY)
|
|
326
|
+
throw e;
|
|
327
|
+
// Saved login exists but One account verify isn't reachable yet (auth-v4-backend PR #30 not deployed).
|
|
328
|
+
emit({ user: saved.user, organization_id: saved.organization_id, verified: false, reason: e.message }, () => `${c.bold('User')} ${saved.user?.email ?? saved.user?.id}\n${c.bold('Org')} ${saved.organization_id ?? '-'}\n${c.yellow('!')} ${c.dim('From your saved login; One account verify unavailable: ' + e.message)}`);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
// `orgs usage|observations|billing|payment-methods|service` and the Orgs (legal entity) commands are registered by registerProducts and registerOrgs.
|
|
332
|
+
const orgs = (program.commands.find(command => command.name() === 'orgs') ?? program.command('orgs')).description('Your organizations, their usage and billing reads, and Orgs: legal entities, seats, resolutions, documents, filings, formation and the record');
|
|
333
|
+
orgs.command('list', { isDefault: true }).description('Show your current organization').action(async () => {
|
|
334
|
+
const s = await (await getStore()).get();
|
|
335
|
+
if (!s)
|
|
336
|
+
throw new Error('Not signed in. Run `genesis login`.');
|
|
337
|
+
const rows = s.organization_id ? [{ id: s.organization_id, current: '*' }] : [];
|
|
338
|
+
emit({ current: s.organization_id ?? null, organizations: rows }, () => `${table(rows, ['id', 'current'])}\n${c.dim('Listing every org you belong to needs a One account memberships endpoint (not built yet). Switch with `genesis orgs switch <org_id>`.')}`);
|
|
339
|
+
});
|
|
340
|
+
orgs.command('switch <org_id>').description('Switch your login to another organization').action(async (id) => {
|
|
341
|
+
const store = await getStore();
|
|
342
|
+
const s = await store.get();
|
|
343
|
+
if (!s)
|
|
344
|
+
throw new Error('Not signed in. Run `genesis login`.');
|
|
345
|
+
const n = await selectSavedContext(auth, store, { organizationId: id }, deps.signal);
|
|
346
|
+
emit({ organization_id: n.organization_id }, () => `${c.green('✓')} Now using ${c.bold(n.organization_id ?? id)}.`);
|
|
347
|
+
});
|
|
348
|
+
const selection = program.command('context').description('Select an authorized Genesis organization, workspace and project');
|
|
349
|
+
selection.command('select').requiredOption('--organization <id>').requiredOption('--workspace <id>').requiredOption('--project <id>')
|
|
350
|
+
.description('Bind the saved human login to a Console Sandbox context and atomically replace its credentials')
|
|
351
|
+
.action(async (options) => {
|
|
352
|
+
const target = { organizationId: options.organization, workspaceId: options.workspace, projectId: options.project,
|
|
353
|
+
platformId: 'pf_console', fleetId: null, environmentId: 'sandbox' };
|
|
354
|
+
const next = await selectSavedContext(auth, await getStore(), target, deps.signal);
|
|
355
|
+
emit({ principal_id: next.auth?.principal_id, ...target }, () => `Selected ${target.organizationId} / ${target.workspaceId} / ${target.projectId} (Sandbox).`);
|
|
356
|
+
});
|
|
357
|
+
// ---- keys ------------------------------------------------------------
|
|
358
|
+
const keys = program.command('keys').description('Org API keys (One account / WorkOS)');
|
|
359
|
+
keys.command('list').action(async () => {
|
|
360
|
+
const gen = await client();
|
|
361
|
+
const org = await orgId(gen);
|
|
362
|
+
const r = await gen.keys.list(org);
|
|
363
|
+
emit(r, () => table(r.data, ['id', 'name', 'obfuscated_value', 'scopes', 'last_used_at', 'expires_at']));
|
|
364
|
+
});
|
|
365
|
+
keys.command('create <name>').description('Create a key (shown once), or with --blinds hand it straight to Blinds so nobody ever sees it')
|
|
366
|
+
.option('--scopes <list>', 'comma-separated scopes, e.g. hives:read,relays:write')
|
|
367
|
+
.option('--blinds', 'store the key in Blinds; the raw key is never shown to anyone')
|
|
368
|
+
.option('--expires <iso>', 'expiry, e.g. 2027-01-01T00:00:00Z')
|
|
369
|
+
.action(async (name, o) => {
|
|
370
|
+
const gen = await client();
|
|
371
|
+
const org = await orgId(gen);
|
|
372
|
+
const k = await gen.keys.create(org, { name, custody: o.blinds ? 'blinds' : 'reveal', ...(o.scopes ? { scopes: o.scopes.split(',').map((x) => x.trim()).filter(Boolean) } : {}), ...(o.expires ? { expires_at: o.expires } : {}) });
|
|
373
|
+
emit(k, () => o.blinds
|
|
374
|
+
? `${c.green('✓')} Key ${c.bold(k.id)} created and stored in Blinds as ${c.bold(String(k.blinds_credential_ref))}.\n ${c.dim('No one, human or agent, will ever see the raw key.')}`
|
|
375
|
+
: `${c.green('✓')} Key ${c.bold(k.id)} created. Copy it now; it won't be shown again:\n\n ${c.bold(String(k.value))}\n`);
|
|
376
|
+
});
|
|
377
|
+
keys.command('revoke <key_id>').action(async (id) => {
|
|
378
|
+
const gen = await client();
|
|
379
|
+
await gen.keys.revoke(id);
|
|
380
|
+
emit({ revoked: id }, () => `${c.green('✓')} Revoked ${id} ${c.dim('(takes effect within 60s)')}`);
|
|
381
|
+
});
|
|
382
|
+
// ---- foundry ---------------------------------------------------------
|
|
383
|
+
// registerProducts (above) already added the generated `foundry` commands; the hand-written ones join them.
|
|
384
|
+
const foundry = program.commands.find(c => c.name() === 'foundry') ?? program.command('foundry').description('Foundry: models and inference');
|
|
385
|
+
const foundryBilling = foundry.command('billing').description('Current organization’s native TEST billing; original One account human login required');
|
|
386
|
+
foundryBilling.command('profile').action(async () => out.log(JSON.stringify(await (await client()).foundry.billing.profile({ signal: deps.signal }), null, 2)));
|
|
387
|
+
foundryBilling.command('ensure').requiredOption('--operation-key <key>', 'Persisted operation key; reconcile unknown outcomes before retrying').action(async (o) => out.log(JSON.stringify(await (await client()).foundry.billing.ensureProfile(o.operationKey, { signal: deps.signal }), null, 2)));
|
|
388
|
+
foundryBilling.command('checkout').requiredOption('--purchase-key <key>', 'Persisted purchase key; server controls the TEST amount').action(async (o) => out.log(JSON.stringify(await (await client()).foundry.billing.checkoutCredits(o.purchaseKey, { signal: deps.signal }), null, 2)));
|
|
389
|
+
foundryBilling.command('balance').action(async () => out.log(JSON.stringify(await (await client()).foundry.billing.creditBalance({ signal: deps.signal }), null, 2)));
|
|
390
|
+
foundry.command('models').action(async () => {
|
|
391
|
+
const r = await (await client()).foundry.models.list();
|
|
392
|
+
const rows = listOf(r).map((m) => ({ id: m.id, owned_by: m.owned_by ?? m.provider ?? '' }));
|
|
393
|
+
emit(r, () => table(rows, ['id', 'owned_by']));
|
|
394
|
+
});
|
|
395
|
+
foundry.command('chat <prompt...>').description('Chat with a model').option('-m, --model <model>', 'model id', env.GENESIS_MODEL)
|
|
396
|
+
.option('--no-stream', 'wait for the full answer').option('-s, --system <text>', 'system prompt')
|
|
397
|
+
.action(async (prompt, o) => {
|
|
398
|
+
if (!o.model)
|
|
399
|
+
throw new Error('Pick a model with --model (see `genesis foundry models`) or set GENESIS_MODEL.');
|
|
400
|
+
const gen = await client();
|
|
401
|
+
const messages = [...(o.system ? [{ role: 'system', content: o.system }] : []), { role: 'user', content: prompt.join(' ') }];
|
|
402
|
+
if (o.stream && !g().json) {
|
|
403
|
+
for await (const d of gen.foundry.chatStream({ model: o.model, messages }))
|
|
404
|
+
process.stdout.write(d);
|
|
405
|
+
process.stdout.write('\n');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const r = await gen.foundry.chat({ model: o.model, messages });
|
|
409
|
+
emit(r, () => r.choices?.[0]?.message?.content ?? JSON.stringify(r));
|
|
410
|
+
});
|
|
411
|
+
registerMinds(program, client, data => emit(data, () => JSON.stringify(data, null, 2)), async () => new Genesis({
|
|
412
|
+
apiKey: g().apiKey ?? env.GENESIS_API_KEY, token: env.GENESIS_TOKEN, baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL,
|
|
413
|
+
routing: g().direct || env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway', fetch: f,
|
|
414
|
+
}), deps.signal, async () => {
|
|
415
|
+
if (g().org !== undefined)
|
|
416
|
+
throw new Error('Minds owner commands use the selected Console organization; --org is not supported.');
|
|
417
|
+
return client();
|
|
418
|
+
});
|
|
419
|
+
const hivesClient = () => client(true);
|
|
420
|
+
// ---- hives -----------------------------------------------------------
|
|
421
|
+
const hives = program.command('hives').description('Hives: projects, secrets, deployments, functions, schedules and activity');
|
|
422
|
+
registerHivesOperations(hives, { client: hivesClient, emit, readRequest: deps.readHivesRequest, line: (text) => out.log(text) });
|
|
423
|
+
const deployments = hives.command('deployments').description('Real WASM artifacts and deployment lifecycle');
|
|
424
|
+
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']));
|
|
425
|
+
deployments.command('list <project_id>')
|
|
426
|
+
.option('--page <number>', 'page (1–10000)').option('--per-page <number>', 'items (1–100)')
|
|
427
|
+
.option('--status <status>', 'persisted deployment status').option('--author <id>', 'creator ID')
|
|
428
|
+
.action(async (project, o) => {
|
|
429
|
+
emitDeployment(await (await hivesClient()).hives.deployments.list(project, {
|
|
430
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100), status: o.status, author: o.author,
|
|
431
|
+
}));
|
|
432
|
+
});
|
|
433
|
+
deployments.command('get <project_id> <deployment_id>')
|
|
434
|
+
.action(async (project, deployment) => emitDeployment(await (await hivesClient()).hives.deployments.get(project, deployment)));
|
|
435
|
+
deployments.command('upload <project_id> <wasm_file>').description('Upload actual WASM bytes; Git options are labels only')
|
|
436
|
+
.option('--git-sha <sha>', 'commit label (7–40 characters)').option('--git-branch <branch>', 'branch label').option('--git-message <message>', 'commit label')
|
|
437
|
+
.action(async (project, path, o) => {
|
|
438
|
+
const file = await (deps.readWasmFile ?? readWasmFile)(path);
|
|
439
|
+
emitDeployment(await (await hivesClient()).hives.deployments.upload(project, file, {
|
|
440
|
+
filename: basename(path), git_sha: o.gitSha, git_branch: o.gitBranch, git_message: o.gitMessage,
|
|
441
|
+
}));
|
|
442
|
+
});
|
|
443
|
+
deployments.command('reuse <project_id> <module_hash>').description('Deploy a real artifact already owned by this org and project')
|
|
444
|
+
.option('--no-auto-promote', 'keep the completed deployment ready for explicit promotion')
|
|
445
|
+
.action(async (project, module_hash, o) => {
|
|
446
|
+
if (!/^[a-f0-9]{64}$/.test(module_hash))
|
|
447
|
+
throw new Error('module_hash must be the lowercase SHA-256 returned by a real Hives upload.');
|
|
448
|
+
emitDeployment(await (await hivesClient()).hives.deployments.create(project, { module_hash, auto_promote: o.autoPromote }));
|
|
449
|
+
});
|
|
450
|
+
deployments.command('promote <project_id> <deployment_id>')
|
|
451
|
+
.action(async (project, deployment) => emitDeployment(await (await hivesClient()).hives.deployments.promote(project, deployment)));
|
|
452
|
+
deployments.command('rollback <project_id> <deployment_id>').description('Activate this owned previous deployment')
|
|
453
|
+
.option('--reason <text>', 'audit reason')
|
|
454
|
+
.action(async (project, deployment, o) => emitDeployment(await (await hivesClient()).hives.deployments.rollback(project, deployment, { reason: o.reason })));
|
|
455
|
+
deployments.command('cancel <project_id> <deployment_id>')
|
|
456
|
+
.action(async (project, deployment) => emitDeployment(await (await hivesClient()).hives.deployments.cancel(project, deployment)));
|
|
457
|
+
deployments.command('stop <project_id> <deployment_id>').description('Stop the runtime; read its state before explicitly reconciling an uncertain result')
|
|
458
|
+
.action(async (project, deployment) => emitDeployment(await (await hivesClient()).hives.deployments.stop(project, deployment)));
|
|
459
|
+
deployments.command('delete <project_id> <deployment_id>').description('Delete a deployment that is not currently active')
|
|
460
|
+
.action(async (project, deployment) => { const result = await (await hivesClient()).hives.deployments.delete(project, deployment); emit(result, () => `Deleted deployment ${deployment}.`); });
|
|
461
|
+
deployments.command('logs <project_id> <deployment_id>').description('Read real persisted deployment logs')
|
|
462
|
+
.option('--page <number>', 'page (1–10000)').option('--per-page <number>', 'items (1–100)')
|
|
463
|
+
.option('--level <level>', 'minimum level').option('--search <text>', 'search log messages')
|
|
464
|
+
.option('--start <iso>', 'RFC 3339 start time').option('--end <iso>', 'RFC 3339 end time')
|
|
465
|
+
.action(async (project, deployment, o) => {
|
|
466
|
+
for (const timestamp of [o.start, o.end])
|
|
467
|
+
if (timestamp !== undefined && !Number.isFinite(Date.parse(timestamp)))
|
|
468
|
+
throw new Error('Log timestamps must use RFC 3339.');
|
|
469
|
+
const result = await (await hivesClient()).hives.deployments.logs(project, deployment, {
|
|
470
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100),
|
|
471
|
+
level: o.level, search: o.search, start: o.start, end: o.end,
|
|
472
|
+
});
|
|
473
|
+
emit(result, () => table(result.logs, ['timestamp', 'level', 'message', 'source']));
|
|
474
|
+
});
|
|
475
|
+
const emitFunction = (value) => emit(value, () => table(listOf(value).length ? listOf(value) : [value], ['id', 'name', 'status', 'handler', 'memory_mb', 'timeout_ms']));
|
|
476
|
+
const functions = hives.command('functions').description('Owned WASM functions; invocation returns the completed raw guest response');
|
|
477
|
+
functions.command('list <project_id>').option('--page <n>').option('--per-page <n>').option('--status <status>').option('--search <text>')
|
|
478
|
+
.action(async (project, o) => emitFunction(await (await hivesClient()).hives.functions.list(project, {
|
|
479
|
+
page: positiveInteger(o.page, '--page', 1000000), per_page: positiveInteger(o.perPage, '--per-page', 100), status: o.status, search: o.search,
|
|
480
|
+
})));
|
|
481
|
+
functions.command('get <project_id> <function_id>').action(async (project, id) => emitFunction(await (await hivesClient()).hives.functions.get(project, id)));
|
|
482
|
+
functions.command('create <project_id> <name> <module_hash>').option('--request-file <file>', 'JSON metadata and encrypted env overrides; input values are never printed')
|
|
483
|
+
.action(async (project, name, hash, o) => {
|
|
484
|
+
if (!/^[a-f0-9]{64}$/.test(hash))
|
|
485
|
+
throw new Error('Use a full lowercase artifact hash from an owned Hives upload.');
|
|
486
|
+
const extra = o.requestFile ? await readFunctionJson(o.requestFile, 256 * 1024) : {};
|
|
487
|
+
emitFunction(await (await hivesClient()).hives.functions.create(project, { ...extra, name, handler: `wasm://sha256/${hash}` }));
|
|
488
|
+
});
|
|
489
|
+
functions.command('update <project_id> <function_id>').requiredOption('--request-file <file>', 'JSON supported metadata/env/remove_env fields; values are never printed')
|
|
490
|
+
.action(async (project, id, o) => emitFunction(await (await hivesClient()).hives.functions.update(project, id, await readFunctionJson(o.requestFile, 256 * 1024))));
|
|
491
|
+
functions.command('delete <project_id> <function_id>').action(async (project, id) => {
|
|
492
|
+
const result = await (await hivesClient()).hives.functions.delete(project, id);
|
|
493
|
+
emit(result, () => `Deleted function ${id}.`);
|
|
494
|
+
});
|
|
495
|
+
function invocationCommand(command, kind, alias = false) {
|
|
496
|
+
if (kind === 'functions')
|
|
497
|
+
command.option('--idempotency-key <key>', 'Persisted original invocation key; required by Genesis funding. Reuse it after uncertainty.');
|
|
498
|
+
command.description(alias ? 'Compatibility alias of invoke (/invoke/async): the same completed guest response, no durable queue' : 'Complete real guest execution; write exact bytes, including executed HTTP error bodies')
|
|
499
|
+
.option('--request-file <file>', 'JSON method/path/headers/body or body_base64')
|
|
500
|
+
.option('--body-file <file>', 'Exact binary body (up to 10 MiB; serialized guest envelope may reach its cap earlier)')
|
|
501
|
+
.option('--method <method>', 'guest HTTP method', 'POST').option('--path <path>', 'guest path including query', '/')
|
|
502
|
+
.option('--output <file>', 'create a new private file containing exact response bytes')
|
|
503
|
+
.action(async (project, id, o) => {
|
|
504
|
+
if (o.requestFile && o.bodyFile)
|
|
505
|
+
throw new Error('Choose --request-file or --body-file.');
|
|
506
|
+
if (g().json && o.output)
|
|
507
|
+
throw new Error('Choose --json or --output.');
|
|
508
|
+
let request = o.requestFile ? await readFunctionJson(o.requestFile, MAX_FUNCTION_JSON_BYTES) : { method: o.method, path: o.path };
|
|
509
|
+
if (o.bodyFile)
|
|
510
|
+
request.body_base64 = Buffer.from(await readFunctionFile(o.bodyFile, MAX_FUNCTION_BODY_BYTES)).toString('base64');
|
|
511
|
+
const hivesApi = (await hivesClient()).hives;
|
|
512
|
+
const response = alias ? await hivesApi.functions.invokeAsync(project, id, request, { idempotencyKey: o.idempotencyKey }) : await hivesApi[kind].invoke(project, id, request, { idempotencyKey: o.idempotencyKey });
|
|
513
|
+
const bytes = await hivesResponseBytes(response);
|
|
514
|
+
if (g().json)
|
|
515
|
+
out.log(JSON.stringify({ status: response.status, headers: Object.fromEntries(response.headers), body_base64: Buffer.from(bytes).toString('base64') }));
|
|
516
|
+
else {
|
|
517
|
+
out.err(JSON.stringify({ status: response.status, headers: Object.fromEntries(response.headers), bytes: bytes.length }));
|
|
518
|
+
if (o.output)
|
|
519
|
+
await (deps.writeOutputFile ?? writeFunctionOutput)(o.output, bytes);
|
|
520
|
+
else
|
|
521
|
+
(deps.writeBytes ?? ((value) => { process.stdout.write(value); }))(bytes);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
invocationCommand(functions.command('invoke <project_id> <function_id>'), 'functions');
|
|
526
|
+
invocationCommand(deployments.command('invoke <project_id> <deployment_id>'), 'deployments');
|
|
527
|
+
// The service's /invoke/async compatibility alias: the same completed guest response, no durable queue.
|
|
528
|
+
invocationCommand(functions.command('invoke-async <project_id> <function_id>'), 'functions', true);
|
|
529
|
+
const operations = functions.command('operations').description('Read or release the original funding operation without replaying the guest');
|
|
530
|
+
operations.command('get <project_id> <function_id> <operation_id>').action(async (project, fn, operation) => {
|
|
531
|
+
const result = await (await hivesClient()).hives.functions.operations.get(project, fn, operation);
|
|
532
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
533
|
+
});
|
|
534
|
+
operations.command('release <project_id> <function_id> <operation_id>').description('Release an unused reservation only when the backend proves no guest was dispatched')
|
|
535
|
+
.action(async (project, fn, operation) => {
|
|
536
|
+
const result = await (await hivesClient()).hives.functions.operations.release(project, fn, operation);
|
|
537
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
538
|
+
});
|
|
539
|
+
const secrets = hives.command('secrets').description('Project secrets encrypted with Genesis KMS');
|
|
540
|
+
const readInput = async (options, limit) => {
|
|
541
|
+
if (Boolean(options.file) === Boolean(options.stdin))
|
|
542
|
+
throw new Error('Choose exactly one input source: a file or --stdin. Secret values are not accepted as command arguments.');
|
|
543
|
+
return (deps.readSecretInput ?? readSecretInput)(options, limit);
|
|
544
|
+
};
|
|
545
|
+
const emitSecret = (data) => emit(data, () => table(listOf(data), ['id', 'name', 'version', 'description', 'updated_at']));
|
|
546
|
+
const context = hives.command('context').description('Explicit Hives binding for the current verified interactive Genesis selection');
|
|
547
|
+
context.command('get').action(async () => {
|
|
548
|
+
const r = await (await hivesClient()).hives.context.get();
|
|
549
|
+
emit(r, () => table([r.project], ['id', 'name', 'slug', 'kind', 'status']));
|
|
550
|
+
});
|
|
551
|
+
context.command('create <name>').option('--kind <kind>', 'worker|function|site|api', 'worker').option('--description <text>', 'project description')
|
|
552
|
+
.action(async (name, o) => {
|
|
553
|
+
if (!['worker', 'function', 'site', 'api'].includes(o.kind))
|
|
554
|
+
throw new Error('Choose worker, function, site or api.');
|
|
555
|
+
if (o.description && o.description.length > 500)
|
|
556
|
+
throw new Error('Project descriptions must contain at most 500 characters.');
|
|
557
|
+
const r = await (await hivesClient()).hives.context.set({ create: { name: projectName(name), kind: o.kind, description: o.description } });
|
|
558
|
+
emit(r, () => table([r.project], ['id', 'name', 'slug', 'kind', 'status']));
|
|
559
|
+
});
|
|
560
|
+
context.command('link <project_id>').description('Link a Hives project owned by the verified org')
|
|
561
|
+
.action(async (id) => { const r = await (await hivesClient()).hives.context.set({ link: { project_id: id } }); emit(r, () => table([r.project], ['id', 'name', 'slug', 'kind', 'status'])); });
|
|
562
|
+
context.command('detach').description('Remove this selection binding; retain the Hives project and its resources')
|
|
563
|
+
.action(async () => { const r = await (await hivesClient()).hives.context.detach(); emit(r, () => 'Detached selection; retained Hives project ' + r.hivesProjectId + '.'); });
|
|
564
|
+
const environment = hives.command('environment').description('Encrypted project environment; names-only metadata and explicit KMS reveal');
|
|
565
|
+
environment.command('list <project_id>').option('--page <number>', 'page number (1–10000)').option('--per-page <number>', 'items per page (1–100)')
|
|
566
|
+
.action(async (id, o) => {
|
|
567
|
+
const r = await (await hivesClient()).hives.environment.list(id, { page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) });
|
|
568
|
+
emit(r, () => table(r.data, ['name']));
|
|
569
|
+
});
|
|
570
|
+
environment.command('upsert <project_id>').description('Encrypt a flat JSON map; empty string values are allowed')
|
|
571
|
+
.option('--file <path>', 'UTF-8 JSON input file').option('--stdin', 'read JSON from stdin')
|
|
572
|
+
.action(async (id, o) => {
|
|
573
|
+
const text = await readInput(o, 2 * 1024 * 1024);
|
|
574
|
+
let body;
|
|
575
|
+
try {
|
|
576
|
+
body = JSON.parse(text);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
throw new Error('Environment input must be a flat JSON map of names to string values.');
|
|
580
|
+
}
|
|
581
|
+
const r = await (await hivesClient()).hives.environment.upsert(id, body);
|
|
582
|
+
emit(r, () => table(r.updated, ['name']));
|
|
583
|
+
});
|
|
584
|
+
environment.command('reveal <project_id> <key>').description('Explicit audit-logged environment reveal; requires --stdout')
|
|
585
|
+
.option('--stdout', 'write sensitive plaintext to stdout (avoid retained output)')
|
|
586
|
+
.action(async (id, key, o) => {
|
|
587
|
+
if (!o.stdout)
|
|
588
|
+
throw new Error('Environment reveal requires --stdout. Avoid logging or retaining the plaintext.');
|
|
589
|
+
const r = await (await hivesClient()).hives.environment.reveal(id, key);
|
|
590
|
+
emit(r, () => r.value);
|
|
591
|
+
});
|
|
592
|
+
secrets.command('list <project_id>').description('List secret metadata without values')
|
|
593
|
+
.option('--page <number>', 'page number (1–10000)').option('--per-page <number>', 'items per page (1–100)')
|
|
594
|
+
.action(async (id, o) => {
|
|
595
|
+
const q = { page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) };
|
|
596
|
+
emitSecret(await (await hivesClient()).hives.secrets.list(id, q));
|
|
597
|
+
});
|
|
598
|
+
secrets.command('get <project_id> <secret_id>').description('Inspect secret metadata without its value')
|
|
599
|
+
.action(async (project, secret) => {
|
|
600
|
+
const r = await (await hivesClient()).hives.secrets.get(project, secret);
|
|
601
|
+
emit(r, () => table([r], ['id', 'name', 'version', 'description', 'updated_at']));
|
|
602
|
+
});
|
|
603
|
+
secrets.command('create <project_id> <name>').description('Create a secret; plaintext is read from a file or stdin')
|
|
604
|
+
.option('--value-file <path>', 'UTF-8 file containing the value').option('--stdin', 'read value from stdin')
|
|
605
|
+
.option('--description <text>', 'description (up to 500 characters)')
|
|
606
|
+
.action(async (project, name, o) => {
|
|
607
|
+
const body = validateSecret({ name, value: await readInput({ file: o.valueFile, stdin: o.stdin }, 65536), description: o.description });
|
|
608
|
+
const r = await (await hivesClient()).hives.secrets.create(project, body);
|
|
609
|
+
emit(r, () => table([r], ['id', 'name', 'version']));
|
|
610
|
+
});
|
|
611
|
+
secrets.command('update <project_id> <secret_id>').description('Replace a secret value, creating a new version')
|
|
612
|
+
.option('--value-file <path>', 'UTF-8 file containing the new value').option('--stdin', 'read value from stdin')
|
|
613
|
+
.action(async (project, secret, o) => {
|
|
614
|
+
const value = validateSecretValue(await readInput({ file: o.valueFile, stdin: o.stdin }, 65536));
|
|
615
|
+
const r = await (await hivesClient()).hives.secrets.update(project, secret, { value });
|
|
616
|
+
emit(r, () => table([r], ['id', 'name', 'version']));
|
|
617
|
+
});
|
|
618
|
+
secrets.command('delete <project_id> <secret_id>').description('Delete a secret in your authenticated org')
|
|
619
|
+
.action(async (project, secret) => { const r = await (await hivesClient()).hives.secrets.delete(project, secret); emit(r, () => `Deleted secret ${secret}.`); });
|
|
620
|
+
secrets.command('reveal <project_id> <secret_id>').description('Explicit, audit-logged plaintext reveal; requires --stdout')
|
|
621
|
+
.option('--stdout', 'write sensitive plaintext to stdout (avoid logs and retained output)')
|
|
622
|
+
.action(async (project, secret, o) => {
|
|
623
|
+
if (!o.stdout)
|
|
624
|
+
throw new Error('Reveal requires --stdout. This writes sensitive plaintext to your terminal or pipe; avoid logging or retaining it.');
|
|
625
|
+
const r = await (await hivesClient()).hives.secrets.reveal(project, secret);
|
|
626
|
+
emit(r, () => r.value);
|
|
627
|
+
});
|
|
628
|
+
secrets.command('bulk <project_id>').description('PUT a JSON array of secret names and values; existing names receive new versions')
|
|
629
|
+
.option('--file <path>', 'UTF-8 JSON input file').option('--stdin', 'read JSON from stdin')
|
|
630
|
+
.action(async (project, o) => {
|
|
631
|
+
const batch = parseSecretBatch(await readInput(o, 256 * 1024));
|
|
632
|
+
const r = await (await hivesClient()).hives.secrets.bulkUpsert(project, batch);
|
|
633
|
+
emit(r, () => table(r.secrets, ['id', 'name', 'version']));
|
|
634
|
+
});
|
|
635
|
+
const projects = hives.command('projects').description('Hives projects');
|
|
636
|
+
projects.command('list', { isDefault: true }).option('--search <text>', 'filter by project name')
|
|
637
|
+
.option('--page <number>', 'page number (1–10000)').option('--per-page <number>', 'items per page (1–100)')
|
|
638
|
+
.action(async (o) => {
|
|
639
|
+
const r = await (await hivesClient()).hives.projects.list({ search: o.search,
|
|
640
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) });
|
|
641
|
+
emit(r, () => table(listOf(r), ['id', 'name', 'slug', 'region', 'total_deployments', 'active_instances']));
|
|
642
|
+
});
|
|
643
|
+
projects.command('get <project_id>').action(async (id) => {
|
|
644
|
+
const r = await (await hivesClient()).hives.projects.get(id);
|
|
645
|
+
emit(r, () => JSON.stringify(r, null, 2));
|
|
646
|
+
});
|
|
647
|
+
projects.command('create <name>').description('Create a project in your authenticated org')
|
|
648
|
+
.option('--kind <kind>', 'worker, function, site or api', 'worker')
|
|
649
|
+
.option('--description <text>', 'project description (up to 500 characters)')
|
|
650
|
+
.action(async (name, o) => {
|
|
651
|
+
if (!['worker', 'function', 'site', 'api'].includes(o.kind))
|
|
652
|
+
throw new Error('--kind must be worker, function, site or api.');
|
|
653
|
+
if (o.description && o.description.length > 500)
|
|
654
|
+
throw new Error('--description must be at most 500 characters.');
|
|
655
|
+
const r = await (await hivesClient()).hives.projects.create({ name: projectName(name), kind: o.kind,
|
|
656
|
+
...(o.description === undefined ? {} : { description: o.description }) });
|
|
657
|
+
emit(r, () => table([r], ['id', 'name', 'slug', 'status']));
|
|
658
|
+
});
|
|
659
|
+
projects.command('delete <project_id>').description('Delete a project in your authenticated org')
|
|
660
|
+
.action(async (id) => { const r = await (await hivesClient()).hives.projects.delete(id); emit(r ?? { deleted: id }, () => `Deleted project ${id}.`); });
|
|
661
|
+
projects.command('rename <project_id> <name>').description('Rename a project in your authenticated org')
|
|
662
|
+
.action(async (id, name) => {
|
|
663
|
+
const r = await (await hivesClient()).hives.projects.update(id, { name: projectName(name) });
|
|
664
|
+
emit(r, () => table([r], ['id', 'name', 'slug', 'status']));
|
|
665
|
+
});
|
|
666
|
+
projects.command('activity <project_id>').description('Read persisted project activity')
|
|
667
|
+
.option('--action <action>', 'filter by action')
|
|
668
|
+
.option('--actor-type <type>', 'filter by actor type')
|
|
669
|
+
.option('--resource-type <type>', 'filter by resource type')
|
|
670
|
+
.option('--since <timestamp>', 'inclusive start timestamp in ISO 8601')
|
|
671
|
+
.option('--until <timestamp>', 'inclusive end timestamp in ISO 8601')
|
|
672
|
+
.option('--page <number>', 'page number (1–10000)')
|
|
673
|
+
.option('--per-page <number>', 'items per page (1–100)')
|
|
674
|
+
.action(async (id, o) => {
|
|
675
|
+
for (const [option, value] of [['--since', o.since], ['--until', o.until]]) {
|
|
676
|
+
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)))) {
|
|
677
|
+
throw new Error(`${option} must be an ISO 8601 timestamp.`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const r = await (await hivesClient()).hives.projects.activity(id, { action: o.action,
|
|
681
|
+
actor_type: o.actorType, resource_type: o.resourceType, since: o.since, until: o.until,
|
|
682
|
+
page: positiveInteger(o.page, '--page', 10000), per_page: positiveInteger(o.perPage, '--per-page', 100) });
|
|
683
|
+
emit(r, () => table(listOf(r), ['created_at', 'action', 'actor_id', 'resource_type', 'resource_name']));
|
|
684
|
+
});
|
|
685
|
+
// Last: extends the hives groups defined above (flights, logs, env values, domain reads).
|
|
686
|
+
registerHivesCoverage(hives, { client: hivesClient, emit, readRequest: deps.readHivesRequest });
|
|
687
|
+
// ---- relays ----------------------------------------------------------
|
|
688
|
+
const relays = program.command('relays').description('Relays: DNS zones and records');
|
|
689
|
+
registerRelaysEnrollment(relays, client, emit);
|
|
690
|
+
registerOperationCommands(relays, RELAYS_COMMANDS, { client, emit, signal: deps.signal });
|
|
691
|
+
relays.command('zones').description('List DNS zones').option('--search <q>').action(async (o) => {
|
|
692
|
+
const r = await (await client()).relays.zones.list(o.search ? { search: o.search } : {});
|
|
693
|
+
emit(r, () => table((r.zones ?? []), ['name', 'type', 'record_count', 'dnssec_enabled', 'status']));
|
|
694
|
+
});
|
|
695
|
+
relays.command('zone <zone>').description('Read one DNS zone').action(async (zone) => {
|
|
696
|
+
const result = await (await client()).relays.zones.get(zone);
|
|
697
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
698
|
+
});
|
|
699
|
+
relays.command('dnssec <zone>').description('Read DNSSEC status and authoritative key availability').action(async (zone) => {
|
|
700
|
+
const result = await (await client()).relays.zones.dnssec(zone);
|
|
701
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
702
|
+
});
|
|
703
|
+
// Writes need relays:write and act on the selected project's existing Relays account.
|
|
704
|
+
const dnsInt = (value, option, minimum, maximum) => {
|
|
705
|
+
if (value === undefined)
|
|
706
|
+
return undefined;
|
|
707
|
+
if (!/^\d+$/.test(value) || Number(value) < minimum || Number(value) > maximum)
|
|
708
|
+
throw new Error(`${option} must be an integer from ${minimum} to ${maximum}.`);
|
|
709
|
+
return Number(value);
|
|
710
|
+
};
|
|
711
|
+
const recordId = (value) => dnsInt(value, '<record_id>', 1, Number.MAX_SAFE_INTEGER);
|
|
712
|
+
relays.command('create-zone <name>').description('Create a DNS zone')
|
|
713
|
+
.option('--type <type>', 'NATIVE, MASTER or SLAVE (default NATIVE)').option('--no-default-records', 'skip the default records')
|
|
714
|
+
.action(async (name, o) => {
|
|
715
|
+
const result = await (await client()).relays.zones.create({ name, ...(o.type ? { type: o.type.toUpperCase() } : {}),
|
|
716
|
+
create_default_records: o.defaultRecords });
|
|
717
|
+
emit(result, () => `${c.green('✓')} Created zone ${name}`);
|
|
718
|
+
});
|
|
719
|
+
relays.command('update-zone <zone>').description('Update a zone\'s type or SOA settings')
|
|
720
|
+
.option('--type <type>', 'NATIVE, MASTER or SLAVE').option('--primary-ns <host>').option('--hostmaster <email>')
|
|
721
|
+
.option('--refresh <seconds>').option('--retry <seconds>').option('--expire <seconds>').option('--soa-ttl <seconds>')
|
|
722
|
+
.action(async (zone, o) => {
|
|
723
|
+
const soa = Object.fromEntries(Object.entries({ primary_ns: o.primaryNs, hostmaster: o.hostmaster,
|
|
724
|
+
refresh: dnsInt(o.refresh, '--refresh', 60, 2 ** 31), retry: dnsInt(o.retry, '--retry', 60, 2 ** 31),
|
|
725
|
+
expire: dnsInt(o.expire, '--expire', 3600, 2 ** 31), ttl: dnsInt(o.soaTtl, '--soa-ttl', 60, 2 ** 31) }).filter(([, v]) => v !== undefined));
|
|
726
|
+
const body = { ...(o.type ? { type: o.type.toUpperCase() } : {}), ...(Object.keys(soa).length ? { soa } : {}) };
|
|
727
|
+
if (!Object.keys(body).length)
|
|
728
|
+
throw new Error('Nothing to update: pass --type or an SOA option.');
|
|
729
|
+
const result = await (await client()).relays.zones.update(zone, body);
|
|
730
|
+
emit(result, () => `${c.green('✓')} Updated zone ${zone}`);
|
|
731
|
+
});
|
|
732
|
+
relays.command('delete-zone <zone>').description('Delete a zone and all its records').action(async (zone) => {
|
|
733
|
+
const result = await (await client()).relays.zones.delete(zone);
|
|
734
|
+
emit(result, () => `${c.green('✓')} Deleted zone ${zone}`);
|
|
735
|
+
});
|
|
736
|
+
const records = relays.command('records').description('DNS records in a zone');
|
|
737
|
+
records.command('get <zone> <record_id>').description('Read one record').action(async (zone, id) => {
|
|
738
|
+
const result = await (await client()).relays.records.get(zone, recordId(id));
|
|
739
|
+
emit(result, () => JSON.stringify(result, null, 2));
|
|
740
|
+
});
|
|
741
|
+
records.command('update <zone> <record_id>').description('Update a record')
|
|
742
|
+
.option('--name <name>').option('--type <type>').option('--content <content>').option('--ttl <seconds>', 'TTL, 60 to 86400')
|
|
743
|
+
.option('--priority <n>', '0 to 65535').option('--comment <text>').option('--disable', 'disable the record').option('--enable', 'enable the record')
|
|
744
|
+
.action(async (zone, id, o) => {
|
|
745
|
+
if (o.disable && o.enable)
|
|
746
|
+
throw new Error('Pass --disable or --enable, not both.');
|
|
747
|
+
const body = Object.fromEntries(Object.entries({ name: o.name, type: o.type?.toUpperCase(), content: o.content,
|
|
748
|
+
ttl: dnsInt(o.ttl, '--ttl', 60, 86400), priority: dnsInt(o.priority, '--priority', 0, 65535), comment: o.comment,
|
|
749
|
+
disabled: o.disable ? true : o.enable ? false : undefined }).filter(([, v]) => v !== undefined));
|
|
750
|
+
if (!Object.keys(body).length)
|
|
751
|
+
throw new Error('Nothing to update: pass at least one field option.');
|
|
752
|
+
const result = await (await client()).relays.records.update(zone, recordId(id), body);
|
|
753
|
+
emit(result, () => `${c.green('✓')} Updated record ${id} in ${zone}`);
|
|
754
|
+
});
|
|
755
|
+
records.command('list <zone>', { isDefault: true }).option('--type <type>').action(async (zone, o) => {
|
|
756
|
+
const r = await (await client()).relays.records.list(zone, o.type ? { type: o.type } : {});
|
|
757
|
+
emit(r, () => table((r.records ?? []), ['id', 'name', 'type', 'content', 'ttl']));
|
|
758
|
+
});
|
|
759
|
+
records.command('add <zone> <name> <type> <content>').option('--ttl <seconds>', 'TTL', '3600').action(async (zone, name, type, content, o) => {
|
|
760
|
+
const r = await (await client()).relays.records.create(zone, { name, type: type.toUpperCase(), content, ttl: Number(o.ttl) });
|
|
761
|
+
emit(r, () => `${c.green('✓')} Added ${type.toUpperCase()} ${name} → ${content} in ${zone}${r.id ? ` (id ${r.id})` : ''}`);
|
|
762
|
+
});
|
|
763
|
+
records.command('rm <zone> <record_id>').action(async (zone, id) => {
|
|
764
|
+
await (await client()).relays.records.delete(zone, Number(id));
|
|
765
|
+
emit({ deleted: Number(id) }, () => `${c.green('✓')} Deleted record ${id} from ${zone}`);
|
|
766
|
+
});
|
|
767
|
+
addHenryCommands(program, client, value => out.log(JSON.stringify(value, null, 2)), { line: text => out.log(text), ...(deps.signal ? { signal: deps.signal } : {}) });
|
|
768
|
+
registerFleets(program, client, (data) => emit(data, () => JSON.stringify(data, null, 2)));
|
|
769
|
+
registerFlocks(program, client, emit, env, deps.readFlocksCode, () => !!g().json);
|
|
770
|
+
registerBeacons(program, client, emit);
|
|
771
|
+
// ---- setup -----------------------------------------------------------
|
|
772
|
+
// The first-run experience lives in ./onboard and is imported only here, so no other command loads it.
|
|
773
|
+
program.command('setup [step]').description('Set up Genesis: sign in, pick a project, add the MCP server to your agents (resumable)')
|
|
774
|
+
.option('--restart', 'start again from the first step')
|
|
775
|
+
.option('--no-animation', 'draw every step still (also GENESIS_REDUCED_MOTION=1)')
|
|
776
|
+
.option('--screen-reader', 'linear output for screen readers (also GENESIS_SCREEN_READER=1)')
|
|
777
|
+
.option('--screencast', 'mask account names, emails and IDs, for recording demos (also GENESIS_SCREENCAST=1)')
|
|
778
|
+
.option('--sound', 'a soft chime when you are signed in and when setup finishes (also GENESIS_SOUND=1)')
|
|
779
|
+
.action(async (step, o) => {
|
|
780
|
+
const code = await runSetupLazily(o, step);
|
|
781
|
+
if (code)
|
|
782
|
+
throw new CommanderError(code, 'genesis.setup', '');
|
|
783
|
+
});
|
|
784
|
+
const runSetupLazily = async (o, step) => {
|
|
785
|
+
const [{ runSetup }, authModule, deviceModule] = await Promise.all([import('./onboard/index.js'), import('./auth.js'), import('./device-login.js')]);
|
|
786
|
+
return runSetup({ version: program.version() ?? '', env, fetch: f, auth, getStore, client: () => client(), openBrowser: deps.openBrowser ?? defaultOpen,
|
|
787
|
+
baseUrl: g().baseUrl ?? env.GENESIS_BASE_URL ?? DEFAULT_BASE_URL, log: (s) => out.log(s),
|
|
788
|
+
flags: { json: g().json, animation: o.animation, screenReader: o.screenReader, sound: o.sound, screencast: o.screencast, restart: o.restart, step },
|
|
789
|
+
login: { startLogin: authModule.startLogin, completeLogin: authModule.completeLogin, selectSavedContext: authModule.selectSavedContext,
|
|
790
|
+
savedLoginProvider: authModule.savedLoginProvider, startDeviceLogin: deviceModule.startDeviceLogin, completeDeviceLogin: deviceModule.completeDeviceLogin } });
|
|
791
|
+
};
|
|
792
|
+
// ---- status ----------------------------------------------------------
|
|
793
|
+
program.command('status').description('Live state of each Genesis service, from the gateway (GET /v1/status)').action(async () => {
|
|
794
|
+
const configured = g().baseUrl ?? env.GENESIS_BASE_URL ?? DEFAULT_BASE_URL;
|
|
795
|
+
// Same transport rule as the SDK client (https, or http only on exact loopback hosts).
|
|
796
|
+
try {
|
|
797
|
+
requireSecureUrl(configured, 'base URL');
|
|
798
|
+
}
|
|
799
|
+
catch (e) {
|
|
800
|
+
throw new Error(`Refusing base URL ${safe(configured)}: ${safe(e.message)}`);
|
|
801
|
+
}
|
|
802
|
+
const base = new URL(configured);
|
|
803
|
+
const url = `${base.href.replace(/\/+$/, '')}/v1/status`;
|
|
804
|
+
let response;
|
|
805
|
+
try {
|
|
806
|
+
response = await f(url, { method: 'GET', headers: { accept: 'application/json', 'user-agent': 'genesis-cli/0.2.0' }, signal: AbortSignal.timeout(15_000) });
|
|
807
|
+
}
|
|
808
|
+
catch (e) {
|
|
809
|
+
throw new Error(`Gateway unreachable at ${safe(url)}: ${safe(e.message)}`);
|
|
810
|
+
}
|
|
811
|
+
if (!response.ok)
|
|
812
|
+
throw new Error(`Gateway status check failed: GET ${safe(url)} returned HTTP ${response.status}`);
|
|
813
|
+
const report = await response.json().catch(() => undefined);
|
|
814
|
+
if (!report || !Array.isArray(report.services))
|
|
815
|
+
throw new Error(`Gateway status check failed: GET ${safe(url)} did not return a service list`);
|
|
816
|
+
const rows = report.services;
|
|
817
|
+
emit(report, () => [
|
|
818
|
+
table(rows.map((s) => ({ service: s.service, state: s.state, http_status: s.http_status, latency_ms: s.latency_ms, reason: s.reason })), ['service', 'state', 'http_status', 'latency_ms', 'reason']),
|
|
819
|
+
...(report.checked_at ? [c.dim(`checked ${safe(report.checked_at)} via ${safe(url)}`)] : []),
|
|
820
|
+
].join('\n'));
|
|
821
|
+
});
|
|
822
|
+
return program;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* A bare `genesis` on a terminal, before setup is finished, is the first run: it opens `genesis setup`. Everywhere
|
|
826
|
+
* else (a pipe, CI, TERM=dumb, setup complete) it prints the help as before. Only the setup state file is read here.
|
|
827
|
+
*/
|
|
828
|
+
async function firstRun(argv, deps) {
|
|
829
|
+
if (argv.length > 2 || deps.out)
|
|
830
|
+
return false;
|
|
831
|
+
const env = deps.env ?? process.env;
|
|
832
|
+
const set = (v) => !!v && v !== '0' && v !== 'false';
|
|
833
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY || env.TERM === 'dumb'
|
|
834
|
+
|| set(env.CI) || env.GITHUB_ACTIONS || env.BUILDKITE || env.GITLAB_CI || env.TF_BUILD)
|
|
835
|
+
return false;
|
|
836
|
+
const { SetupStore, complete } = await import('./onboard/state.js');
|
|
837
|
+
return !complete(await SetupStore.for(env).read());
|
|
838
|
+
}
|
|
839
|
+
export async function main(argv, deps = {}) {
|
|
840
|
+
const out = deps.out ?? stdio;
|
|
841
|
+
const program = buildProgram(deps);
|
|
842
|
+
if (await firstRun(argv, deps).catch(() => false))
|
|
843
|
+
argv = [...argv, 'setup'];
|
|
844
|
+
try {
|
|
845
|
+
await program.parseAsync(argv);
|
|
846
|
+
return 0;
|
|
847
|
+
}
|
|
848
|
+
catch (e) {
|
|
849
|
+
if (e instanceof CommanderError)
|
|
850
|
+
return e.exitCode;
|
|
851
|
+
const json = program.opts().json;
|
|
852
|
+
if (e instanceof GenesisError) {
|
|
853
|
+
// Dials needs a project, workspace and environment: from flags, a selected context or an org key's binding.
|
|
854
|
+
const dialsScopeHint = e.operationId?.startsWith('dials_') && /invalid_scope/i.test(`${e.code ?? ''} ${e.message} ${e.type}`)
|
|
855
|
+
? ' Pass --workspace, --project and --environment, select a context (genesis context select), or use an org API key bound to a project.' : '';
|
|
856
|
+
if (json)
|
|
857
|
+
out.err(JSON.stringify({ error: { type: e.type, status: e.status, message: e.message, request_id: e.requestId, ...errorCatalog(e),
|
|
858
|
+
...((e instanceof HivesOperationError || e instanceof HivesQueueOperationError) ? { operation: e.operation } : {}),
|
|
859
|
+
...(e.operationId && isMindsOperation(e.operationId) && errorOutcome(e.body) === 'unknown' ? { outcome: 'unknown' } : {}),
|
|
860
|
+
...(e instanceof MindsAgentError || e instanceof MindsOwnerError ? { code: e.code, ...(e instanceof MindsAgentError ? { dpop_nonce: e.dpopNonce } : {}),
|
|
861
|
+
...(errorOutcome(e.body) !== undefined ? { outcome: errorOutcome(e.body) } : {}) } : {}) } }));
|
|
862
|
+
else
|
|
863
|
+
out.err(`${c.red('✗')} ${safe(e.message)}${dialsScopeHint}${(e instanceof HivesOperationError || e instanceof HivesQueueOperationError) ? ` Operation: ${safe(e.operation.operation_id)} (${safe(e.operation.state)}).` : ''}${e.status ? c.dim(` (${e.status} ${safe(e.type)})`) : ''}${e.requestId ? `\n${c.dim(`request_id: ${safe(e.requestId)}`)}` : ''}`);
|
|
864
|
+
return 1;
|
|
865
|
+
}
|
|
866
|
+
const msg = e.message ?? String(e);
|
|
867
|
+
const net = /fetch failed|ENOTFOUND|ECONNREFUSED/.test(msg + (e.cause?.code ?? ''));
|
|
868
|
+
out.err(json ? JSON.stringify({ error: { type: net ? 'network' : 'error', message: msg } })
|
|
869
|
+
: `${c.red('✗')} ${net ? `Could not reach the Genesis API gateway (${safe(msg)}). Try --direct to call service hosts directly.` : safe(msg)}`);
|
|
870
|
+
return 1;
|
|
871
|
+
}
|
|
872
|
+
}
|