@zergai/cyberdeck 0.1.0-beta.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.
@@ -0,0 +1,393 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { DeckClient } from '../client.js';
3
+ import { diagnosticHasFailure, diagnosticsHaveFailures, isTerminalRun, mapLimit, redactLogValue, runHasFailures, selectDiagnostic, } from '../fleet.js';
4
+ const sleep = (ms) => new Promise(resolve => (setTimeout(resolve, ms)));
5
+ const CLONE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
+ function setExitCode(code) {
7
+ const current = typeof process.exitCode === 'number' ? process.exitCode : 0;
8
+ process.exitCode = Math.max(current, code);
9
+ }
10
+ function parseInteger(value, fallback, minimum, maximum, label) {
11
+ if (value === undefined)
12
+ return fallback;
13
+ const parsed = Number(value);
14
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
15
+ throw new Error(`${label} must be an integer between ${minimum} and ${maximum}.`);
16
+ }
17
+ return parsed;
18
+ }
19
+ async function diagnosticsForSelector(client, deckId, selector, tail) {
20
+ if (selector && CLONE_UUID.test(selector)) {
21
+ return client.getDiagnostics(deckId, { cloneId: selector, tail });
22
+ }
23
+ const diagnostics = await client.getDiagnostics(deckId, { tail });
24
+ if (!selector)
25
+ return diagnostics;
26
+ return {
27
+ ...diagnostics,
28
+ clones: [selectDiagnostic(diagnostics, selector)],
29
+ };
30
+ }
31
+ async function resolveTargets(client, options) {
32
+ const modes = [
33
+ options.clone !== undefined,
34
+ options.all === true,
35
+ options.failed === true,
36
+ ].filter(Boolean).length;
37
+ if (modes !== 1) {
38
+ throw new Error('Choose exactly one target: --clone <vendor-or-uuid>, --all, or --failed.');
39
+ }
40
+ const diagnostics = options.clone !== undefined
41
+ ? await diagnosticsForSelector(client, options.deck, options.clone)
42
+ : await client.getDiagnostics(options.deck);
43
+ let clones;
44
+ if (options.clone !== undefined) {
45
+ clones = [selectDiagnostic(diagnostics, options.clone)];
46
+ }
47
+ else if (options.failed) {
48
+ clones = diagnostics.clones.filter(diagnosticHasFailure);
49
+ }
50
+ else {
51
+ clones = diagnostics.clones;
52
+ }
53
+ if (clones.length === 0) {
54
+ throw new Error(options.failed
55
+ ? `Deck ${options.deck} has no failed clones.`
56
+ : `Deck ${options.deck} has no matching clones.`);
57
+ }
58
+ return { diagnostics, clones };
59
+ }
60
+ function printPreflight(preflight) {
61
+ console.log(`Preflight: ${preflight.status}`);
62
+ for (const check of preflight.checks) {
63
+ console.log(` ${check.status.padEnd(4)} ${check.code} — ${check.message}`);
64
+ }
65
+ for (const clone of preflight.clones) {
66
+ console.log(` ${clone.vendor.padEnd(18)} ${clone.status} (${clone.cloneId}) `
67
+ + `artifact=${clone.imageId} runtime=${clone.runtimeMode}`);
68
+ for (const check of clone.checks) {
69
+ console.log(` ${check.status.padEnd(4)} ${check.code} — ${check.message}`);
70
+ }
71
+ }
72
+ }
73
+ function printRun(envelope) {
74
+ console.log(`Operation ${envelope.run.id}: ${envelope.run.status} `
75
+ + `(${envelope.run.succeededCount}/${envelope.run.targetCount} succeeded, `
76
+ + `${envelope.run.failedCount} failed)`);
77
+ for (const clone of envelope.clones) {
78
+ console.log(` ${clone.vendor.padEnd(18)} ${clone.status.padEnd(9)} ${clone.stage} `
79
+ + `artifact=${clone.imageId ?? '—'} `
80
+ + `runtime=${clone.runtimeMode ?? '—'}`);
81
+ if (clone.error) {
82
+ console.log(` ${clone.error.code}: ${clone.error.message} `
83
+ + `(retryable=${clone.error.retryable})`);
84
+ }
85
+ }
86
+ }
87
+ function printDiagnostics(diagnostics) {
88
+ console.log(`Deck "${diagnostics.deck.name}" (${diagnostics.deck.id}) — `
89
+ + `${diagnostics.clones.length} clone(s)`);
90
+ for (const clone of diagnostics.clones) {
91
+ const url = clone.runtime?.serviceUrl ?? clone.serviceUrl ?? '—';
92
+ console.log(` ${clone.vendor.padEnd(18)} ${clone.status.padEnd(10)} ${url}`);
93
+ if (clone.operation) {
94
+ console.log(` operation=${clone.operation.runId} `
95
+ + `stage=${clone.operation.stage} attempt=${clone.operation.attempt} `
96
+ + `artifact=${clone.operation.imageId ?? '—'} `
97
+ + `runtime=${clone.operation.runtimeMode ?? '—'}`);
98
+ }
99
+ if (clone.latestFailure) {
100
+ console.log(` failure=${clone.latestFailure.code}: `
101
+ + clone.latestFailure.message);
102
+ }
103
+ if (clone.error) {
104
+ console.log(` diagnostic=${clone.error.code}: ${clone.error.message}`);
105
+ }
106
+ }
107
+ }
108
+ export async function waitForCloneStartRun(client, deckId, initial, timeoutMs) {
109
+ let current = initial;
110
+ const deadline = Date.now() + timeoutMs;
111
+ while (!isTerminalRun(current.run.status) && Date.now() < deadline) {
112
+ await sleep(2000);
113
+ current = await client.getCloneStartRun(deckId, current.run.id);
114
+ }
115
+ return current;
116
+ }
117
+ export async function submitCloneStartRun(client, deckId, cloneIds) {
118
+ const preflight = await client.preflightClones(deckId, cloneIds);
119
+ if (preflight.status === 'blocked') {
120
+ return { preflight, run: null };
121
+ }
122
+ const run = await client.createCloneStartRun(deckId, cloneIds, randomUUID());
123
+ return { preflight, run };
124
+ }
125
+ async function startSelected(client, options) {
126
+ const { clones } = await resolveTargets(client, options);
127
+ await startClones(client, options, clones);
128
+ }
129
+ async function startClones(client, options, clones) {
130
+ const submitted = await submitCloneStartRun(client, options.deck, clones.map(clone => clone.cloneId));
131
+ if (!submitted.run) {
132
+ if (options.json) {
133
+ console.log(JSON.stringify(submitted.preflight, null, 2));
134
+ }
135
+ else {
136
+ printPreflight(submitted.preflight);
137
+ }
138
+ setExitCode(2);
139
+ return;
140
+ }
141
+ const timeoutMs = parseInteger(options.timeout, 420, 30, 3600, '--timeout') * 1000;
142
+ const result = options.wait
143
+ ? await waitForCloneStartRun(client, options.deck, submitted.run, timeoutMs)
144
+ : submitted.run;
145
+ if (options.json)
146
+ console.log(JSON.stringify(result, null, 2));
147
+ else
148
+ printRun(result);
149
+ if (runHasFailures(result)
150
+ || (options.wait === true && !isTerminalRun(result.run.status))) {
151
+ setExitCode(1);
152
+ }
153
+ }
154
+ async function applyCloneAction(client, options, action) {
155
+ const { clones } = await resolveTargets(client, options);
156
+ const results = await mapLimit(clones, 4, async (clone) => {
157
+ try {
158
+ const result = action === 'stop'
159
+ ? await client.setClone(options.deck, clone.cloneId, 'stopped')
160
+ : await client.resetClone(options.deck, clone.cloneId);
161
+ return {
162
+ cloneId: clone.cloneId,
163
+ vendor: clone.vendor,
164
+ ok: true,
165
+ result,
166
+ error: null,
167
+ };
168
+ }
169
+ catch (error) {
170
+ return {
171
+ cloneId: clone.cloneId,
172
+ vendor: clone.vendor,
173
+ ok: false,
174
+ result: null,
175
+ error: error instanceof Error ? error.message : String(error),
176
+ };
177
+ }
178
+ });
179
+ return {
180
+ schemaVersion: 1,
181
+ action,
182
+ deckId: options.deck,
183
+ results,
184
+ };
185
+ }
186
+ function printAction(envelope) {
187
+ console.log(`${envelope.action} (${envelope.deckId})`);
188
+ for (const result of envelope.results) {
189
+ console.log(` ${result.ok ? '✓' : '✗'} ${result.vendor} (${result.cloneId})`
190
+ + (result.error ? ` — ${result.error}` : ''));
191
+ }
192
+ }
193
+ async function waitUntilStopped(client, deckId, clones, timeoutMs) {
194
+ await mapLimit(clones, 4, async (clone) => {
195
+ const deadline = Date.now() + timeoutMs;
196
+ while (Date.now() < deadline) {
197
+ try {
198
+ const runtime = await client.getRuntime(deckId, clone.cloneId);
199
+ if (runtime.cloneStatus === 'stopped'
200
+ || runtime.status === 'stopped')
201
+ return;
202
+ }
203
+ catch {
204
+ // A runtime can disappear between teardown and reconciliation. The
205
+ // deck diagnostic remains the fallback observation for this poll.
206
+ }
207
+ const diagnostics = await client.getDiagnostics(deckId, {
208
+ cloneId: clone.cloneId,
209
+ });
210
+ const current = diagnostics.clones[0];
211
+ if (current?.status === 'stopped')
212
+ return;
213
+ if (current?.status === 'error') {
214
+ throw new Error(`${clone.vendor} entered error while stopping for restart.`);
215
+ }
216
+ await sleep(2000);
217
+ }
218
+ throw new Error(`${clone.vendor} did not stop before the timeout.`);
219
+ });
220
+ }
221
+ function registerTargetOptions(command) {
222
+ return command
223
+ .requiredOption('--deck <id>', 'deck id')
224
+ .option('--clone <vendor-or-uuid>', 'one vendor or exact clone UUID')
225
+ .option('--all', 'target every clone in the deck')
226
+ .option('--failed', 'target clones with a recorded failure')
227
+ .option('--json', 'emit stable schema-versioned JSON');
228
+ }
229
+ export function registerFleetCommands(program) {
230
+ registerTargetOptions(program
231
+ .command('preflight')
232
+ .description('Check platform admission readiness for selected clones')).action(async (options) => {
233
+ const client = DeckClient.fromConfig();
234
+ const { clones } = await resolveTargets(client, options);
235
+ const preflight = await client.preflightClones(options.deck, clones.map(clone => clone.cloneId));
236
+ if (options.json)
237
+ console.log(JSON.stringify(preflight, null, 2));
238
+ else
239
+ printPreflight(preflight);
240
+ if (preflight.status === 'blocked')
241
+ setExitCode(2);
242
+ });
243
+ registerTargetOptions(program
244
+ .command('start')
245
+ .description('Start selected clones through a durable fleet operation'))
246
+ .option('--wait', 'wait for the fleet operation to finish')
247
+ .option('--timeout <seconds>', 'fleet operation timeout', '420')
248
+ .action(async (options) => {
249
+ await startSelected(DeckClient.fromConfig(), options);
250
+ });
251
+ registerTargetOptions(program
252
+ .command('stop')
253
+ .description('Stop selected existing clones')).action(async (options) => {
254
+ const envelope = await applyCloneAction(DeckClient.fromConfig(), options, 'stop');
255
+ if (options.json)
256
+ console.log(JSON.stringify(envelope, null, 2));
257
+ else
258
+ printAction(envelope);
259
+ if (envelope.results.some(result => !result.ok))
260
+ setExitCode(1);
261
+ });
262
+ registerTargetOptions(program
263
+ .command('reset')
264
+ .description('Reset synthetic data in selected running clones')).action(async (options) => {
265
+ const envelope = await applyCloneAction(DeckClient.fromConfig(), options, 'reset');
266
+ if (options.json)
267
+ console.log(JSON.stringify(envelope, null, 2));
268
+ else
269
+ printAction(envelope);
270
+ if (envelope.results.some(result => !result.ok))
271
+ setExitCode(1);
272
+ });
273
+ registerTargetOptions(program
274
+ .command('restart-runtime')
275
+ .description('Stop selected clones and start a new durable runtime run'))
276
+ .option('--wait', 'wait for the start operation to finish')
277
+ .option('--timeout <seconds>', 'stop and start timeout', '420')
278
+ .action(async (options) => {
279
+ const client = DeckClient.fromConfig();
280
+ const selected = await resolveTargets(client, options);
281
+ const stopped = await applyCloneAction(client, options, 'stop');
282
+ if (stopped.results.some(result => !result.ok)) {
283
+ if (options.json)
284
+ console.log(JSON.stringify(stopped, null, 2));
285
+ else
286
+ printAction(stopped);
287
+ setExitCode(1);
288
+ return;
289
+ }
290
+ const timeoutMs = parseInteger(options.timeout, 420, 30, 3600, '--timeout') * 1000;
291
+ await waitUntilStopped(client, options.deck, selected.clones, timeoutMs);
292
+ await startClones(client, options, selected.clones);
293
+ });
294
+ program
295
+ .command('diagnose')
296
+ .description('Read observational fleet diagnostics without reconciling state')
297
+ .requiredOption('--deck <id>', 'deck id')
298
+ .option('--clone <vendor-or-uuid>', 'one vendor or exact clone UUID')
299
+ .option('--tail <count>', 'include this many recent log entries', '50')
300
+ .option('--json', 'emit stable schema-versioned JSON')
301
+ .action(async (options) => {
302
+ const tail = parseInteger(options.tail, 50, 0, 500, '--tail');
303
+ const diagnostics = await diagnosticsForSelector(DeckClient.fromConfig(), options.deck, options.clone, tail);
304
+ if (options.json)
305
+ console.log(JSON.stringify(diagnostics, null, 2));
306
+ else
307
+ printDiagnostics(diagnostics);
308
+ if (diagnosticsHaveFailures(diagnostics))
309
+ setExitCode(1);
310
+ });
311
+ program
312
+ .command('logs')
313
+ .description('Read clone runtime logs')
314
+ .requiredOption('--deck <id>', 'deck id')
315
+ .requiredOption('--clone <vendor-or-uuid>', 'vendor or exact clone UUID')
316
+ .option('--tail <count>', 'number of recent entries', '100')
317
+ .option('--since <timestamp>', 'only entries at or after this ISO timestamp')
318
+ .option('--json', 'emit stable schema-versioned JSON')
319
+ .action(async (options) => {
320
+ const tail = parseInteger(options.tail, 100, 1, 500, '--tail');
321
+ const client = DeckClient.fromConfig();
322
+ const diagnostics = await diagnosticsForSelector(client, options.deck, options.clone);
323
+ const clone = selectDiagnostic(diagnostics, options.clone);
324
+ if (options.since
325
+ && !Number.isFinite(new Date(options.since).getTime())) {
326
+ throw new Error('--since must be a valid ISO timestamp.');
327
+ }
328
+ const logs = redactLogValue(await client.getCloneLogs(options.deck, clone.cloneId, { tail, since: options.since }));
329
+ if (options.json) {
330
+ console.log(JSON.stringify({
331
+ schemaVersion: 1,
332
+ deckId: options.deck,
333
+ cloneId: clone.cloneId,
334
+ vendor: clone.vendor,
335
+ logs,
336
+ }, null, 2));
337
+ return;
338
+ }
339
+ if (typeof logs === 'string') {
340
+ console.log(logs);
341
+ }
342
+ else {
343
+ console.log(JSON.stringify(logs, null, 2));
344
+ }
345
+ });
346
+ program
347
+ .command('operations')
348
+ .description('List recent durable clone start operations')
349
+ .requiredOption('--deck <id>', 'deck id')
350
+ .option('--limit <count>', 'number of operations', '20')
351
+ .option('--json', 'emit stable schema-versioned JSON')
352
+ .action(async (options) => {
353
+ const limit = parseInteger(options.limit, 20, 1, 100, '--limit');
354
+ const list = await DeckClient.fromConfig().listCloneStartRuns(options.deck, limit);
355
+ if (options.json) {
356
+ console.log(JSON.stringify(list, null, 2));
357
+ return;
358
+ }
359
+ for (const run of list.runs) {
360
+ console.log(`${run.id} ${run.status.padEnd(9)} `
361
+ + `${run.succeededCount}/${run.targetCount} succeeded `
362
+ + `${run.failedCount} failed ${run.createdAt}`);
363
+ }
364
+ });
365
+ program
366
+ .command('operation')
367
+ .description('Inspect or retry one durable clone start operation')
368
+ .requiredOption('--deck <id>', 'deck id')
369
+ .requiredOption('--run <id>', 'operation id')
370
+ .option('--retry-failed', 'requeue retryable failed clones')
371
+ .option('--wait', 'wait for the operation to finish')
372
+ .option('--timeout <seconds>', 'operation timeout', '420')
373
+ .option('--json', 'emit stable schema-versioned JSON')
374
+ .action(async (options) => {
375
+ const client = DeckClient.fromConfig();
376
+ let envelope = options.retryFailed
377
+ ? await client.retryFailedCloneStartRun(options.deck, options.run)
378
+ : await client.getCloneStartRun(options.deck, options.run);
379
+ if (options.wait) {
380
+ const timeoutMs = parseInteger(options.timeout, 420, 30, 3600, '--timeout') * 1000;
381
+ envelope = await waitForCloneStartRun(client, options.deck, envelope, timeoutMs);
382
+ }
383
+ if (options.json)
384
+ console.log(JSON.stringify(envelope, null, 2));
385
+ else
386
+ printRun(envelope);
387
+ if (runHasFailures(envelope)
388
+ || (options.wait === true && !isTerminalRun(envelope.run.status))) {
389
+ setExitCode(1);
390
+ }
391
+ });
392
+ }
393
+ //# sourceMappingURL=fleet.js.map
@@ -0,0 +1,143 @@
1
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { DeckClient, } from '../client.js';
4
+ const ORACLE_EVENTS = new Set([
5
+ 'materialize',
6
+ 'materialization_ready',
7
+ 'approve_parameters',
8
+ 'reject_parameters',
9
+ 'execute',
10
+ 'execution_ready',
11
+ 'approve_answers',
12
+ 'reject_answers',
13
+ 'seal',
14
+ 'sealed',
15
+ 'fail',
16
+ 'mark_stale',
17
+ 'retry',
18
+ ]);
19
+ function isRecord(value) {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
21
+ }
22
+ async function readJsonObject(path, label) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(await readFile(resolve(path), 'utf8'));
26
+ }
27
+ catch (error) {
28
+ throw new Error(`${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ if (!isRecord(parsed))
31
+ throw new Error(`${label} must contain one JSON object.`);
32
+ return parsed;
33
+ }
34
+ function outputJson(value) {
35
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
36
+ }
37
+ function printRecord(record) {
38
+ console.log(`${record.id} ${record.status.padEnd(27)} v${record.currentVersion}`
39
+ + ` ${record.subjectKind}:${record.subjectId}`);
40
+ }
41
+ function printDetail(detail) {
42
+ printRecord(detail.oracleSet);
43
+ console.log(` artifact sha256:${detail.oracleSet.artifact.sha256} (${detail.oracleSet.artifact.sizeBytes} bytes)`);
44
+ console.log(` questions ${detail.document.questions.length}`);
45
+ }
46
+ function positiveInteger(value, label, maximum = Number.MAX_SAFE_INTEGER) {
47
+ const parsed = Number(value);
48
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > maximum) {
49
+ throw new Error(`${label} must be an integer between 1 and ${maximum}.`);
50
+ }
51
+ return parsed;
52
+ }
53
+ function outputDetail(detail, json) {
54
+ if (json)
55
+ outputJson(detail);
56
+ else
57
+ printDetail(detail);
58
+ }
59
+ export function registerOracleCommands(program) {
60
+ const oracle = program.command('oracle').description('Author and replay review-gated QA oracle sets');
61
+ oracle
62
+ .command('list')
63
+ .description('List QA oracle sets for a deck')
64
+ .requiredOption('--deck <id>', 'deck id')
65
+ .option('--limit <count>', 'maximum records (1-100)', '50')
66
+ .option('--json', 'emit stable JSON')
67
+ .action(async (options) => {
68
+ const records = await DeckClient.fromConfig().listOracleSets(options.deck, positiveInteger(options.limit, '--limit', 100));
69
+ if (options.json)
70
+ outputJson(records);
71
+ else
72
+ records.forEach(printRecord);
73
+ });
74
+ oracle
75
+ .command('show')
76
+ .description('Inspect the current immutable oracle-set version')
77
+ .requiredOption('--deck <id>', 'deck id')
78
+ .requiredOption('--id <oracle-id>', 'oracle set id')
79
+ .option('--json', 'emit stable JSON')
80
+ .action(async (options) => {
81
+ outputDetail(await DeckClient.fromConfig().getOracleSet(options.deck, options.id), options.json);
82
+ });
83
+ oracle
84
+ .command('create')
85
+ .description('Create a draft oracle set from an authored subject and questions')
86
+ .requiredOption('--deck <id>', 'deck id')
87
+ .requiredOption('--file <path>', 'JSON file containing subject and questions')
88
+ .option('--json', 'emit stable JSON')
89
+ .action(async (options) => {
90
+ const input = await readJsonObject(options.file, 'Oracle draft');
91
+ if (!isRecord(input.subject) || !Array.isArray(input.questions)) {
92
+ throw new Error('Oracle draft must contain subject and questions.');
93
+ }
94
+ outputDetail(await DeckClient.fromConfig().createOracleSet(options.deck, {
95
+ subject: input.subject,
96
+ questions: input.questions,
97
+ }), options.json);
98
+ });
99
+ oracle
100
+ .command('materialize')
101
+ .description('Generate review-pending connector parameters with Codex')
102
+ .requiredOption('--deck <id>', 'deck id')
103
+ .requiredOption('--id <oracle-id>', 'oracle set id')
104
+ .option('--json', 'emit stable JSON')
105
+ .action(async (options) => {
106
+ outputDetail(await DeckClient.fromConfig().materializeOracleSet(options.deck, options.id), options.json);
107
+ });
108
+ oracle
109
+ .command('transition')
110
+ .description('Apply one optimistic, review-gated oracle lifecycle event')
111
+ .requiredOption('--deck <id>', 'deck id')
112
+ .requiredOption('--id <oracle-id>', 'oracle set id')
113
+ .requiredOption('--expected-version <version>', 'current oracle set version')
114
+ .requiredOption('--event <event>', 'lifecycle event')
115
+ .requiredOption('--file <path>', 'reviewed oracle document JSON')
116
+ .option('--json', 'emit stable JSON')
117
+ .action(async (options) => {
118
+ if (!ORACLE_EVENTS.has(options.event)) {
119
+ throw new Error(`Unknown oracle event '${options.event}'.`);
120
+ }
121
+ const document = await readJsonObject(options.file, 'Oracle document');
122
+ outputDetail(await DeckClient.fromConfig().transitionOracleSet(options.deck, options.id, {
123
+ expectedVersion: positiveInteger(options.expectedVersion, '--expected-version'),
124
+ event: options.event,
125
+ document,
126
+ }), options.json);
127
+ });
128
+ oracle
129
+ .command('export')
130
+ .description('Write the current oracle artifact and public metadata to a private JSON file')
131
+ .requiredOption('--deck <id>', 'deck id')
132
+ .requiredOption('--id <oracle-id>', 'oracle set id')
133
+ .requiredOption('--out <path>', 'output JSON path')
134
+ .action(async (options) => {
135
+ const detail = await DeckClient.fromConfig().getOracleSet(options.deck, options.id);
136
+ const target = resolve(options.out);
137
+ await mkdir(dirname(target), { recursive: true });
138
+ await writeFile(target, `${JSON.stringify(detail, null, 2)}\n`, { mode: 0o600 });
139
+ await chmod(target, 0o600);
140
+ console.log(`Oracle ${detail.oracleSet.id} v${detail.oracleSet.currentVersion} → ${target}`);
141
+ });
142
+ }
143
+ //# sourceMappingURL=oracle.js.map
@@ -0,0 +1,19 @@
1
+ import { DeckClient } from '../client.js';
2
+ import { renderDeckPortfolio } from '../portfolio.js';
3
+ import { resolveDeckId } from './decks.js';
4
+ export function registerPortfolioCommand(program) {
5
+ program
6
+ .command('portfolio')
7
+ .description('Show the 69-target deck qualification and evidence matrix')
8
+ .option('--deck <id>', 'deck id')
9
+ .option('--json', 'emit canonical schema-versioned JSON')
10
+ .action(async (options) => {
11
+ const client = DeckClient.fromConfig();
12
+ const deckId = await resolveDeckId(client, options.deck);
13
+ const portfolio = await client.getPortfolio(deckId);
14
+ console.log(options.json
15
+ ? JSON.stringify(portfolio, null, 2)
16
+ : renderDeckPortfolio(portfolio));
17
+ });
18
+ }
19
+ //# sourceMappingURL=portfolio.js.map