@promptdriven/pds 0.1.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.
@@ -0,0 +1,987 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.dispatchCommand = dispatchCommand;
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_util_1 = require("node:util");
10
+ const config_1 = require("../config/config");
11
+ const errors_1 = require("../errors/errors");
12
+ const DEFAULT_LOGIN_SCOPES = [
13
+ "project:create",
14
+ "project:read",
15
+ "project:write",
16
+ "pipeline:run",
17
+ "artifact:read",
18
+ "distribution:package",
19
+ "distribution:publish",
20
+ "token:manage"
21
+ ];
22
+ const RELEASE_VIDEO_REATTACH_MAX_ATTEMPTS = 8;
23
+ const RELEASE_VIDEO_REATTACH_INITIAL_DELAY_MS = 250;
24
+ const RELEASE_VIDEO_REATTACH_MAX_DELAY_MS = 5_000;
25
+ const RELEASE_VIDEO_WAIT_MAX_CONFIRMATIONS = 60;
26
+ const RELEASE_VIDEO_WAIT_POLL_MS = 1_000;
27
+ const PRIVATE_INTERNAL_ERROR_DETAIL_KEY_PATTERN = /^(?:errorName|rootCause|raw(?:Provider)?(?:Payload|Response|Error|Message)?|providerRaw(?:Payload|Response|Error|Message)?)$/i;
28
+ async function dispatchCommand(commandPath, commandArgs, context) {
29
+ const command = commandPath.join(" ");
30
+ switch (command) {
31
+ case "auth login":
32
+ return await authLogin(commandArgs, context);
33
+ case "auth status":
34
+ ensureNoCommandArgs(commandArgs);
35
+ return await context.client.getAuthStatus();
36
+ case "auth logout":
37
+ ensureNoCommandArgs(commandArgs);
38
+ return await (0, config_1.logoutProfile)({ configRoot: context.configRoot, env: context.env, profile: context.config.profile });
39
+ case "auth token create":
40
+ return await authTokenCreate(commandArgs, context);
41
+ case "auth token list":
42
+ ensureNoCommandArgs(commandArgs);
43
+ return await context.client.listTokens();
44
+ case "auth token revoke":
45
+ return await authTokenRevoke(commandArgs, context);
46
+ case "projects create":
47
+ return await projectsCreate(commandArgs, context);
48
+ case "project get":
49
+ ensureNoCommandArgs(commandArgs);
50
+ return await context.client.getProject(requireProject(context.config));
51
+ case "project reset-pipeline":
52
+ return await projectResetPipeline(commandArgs, context);
53
+ case "script get":
54
+ return await context.client.getScript({ projectId: requireProject(context.config), file: optionalString(parse(commandArgs, { file: "string" }).file) });
55
+ case "script set":
56
+ return await scriptSet(commandArgs, context);
57
+ case "artifacts list":
58
+ ensureNoCommandArgs(commandArgs);
59
+ return await context.client.listArtifacts(requireProject(context.config));
60
+ case "pipeline plan":
61
+ return await context.client.planPipeline({
62
+ projectId: requireProject(context.config),
63
+ to: optionalString(parse(commandArgs, { to: "string" }).to)
64
+ });
65
+ case "pipeline status":
66
+ ensureNoCommandArgs(commandArgs);
67
+ return await context.client.getPipelineStatus({
68
+ projectId: requireProject(context.config)
69
+ });
70
+ case "pipeline run": {
71
+ const values = parse(commandArgs, {
72
+ to: "string",
73
+ stage: "string",
74
+ mode: "string",
75
+ "dry-run": "boolean",
76
+ "force-regenerate": "boolean",
77
+ wait: "boolean",
78
+ "idempotency-key": "string"
79
+ });
80
+ const dryRun = values["dry-run"] === true;
81
+ const response = await context.client.runPipeline(compact({
82
+ projectId: requireProject(context.config),
83
+ to: optionalString(values.to),
84
+ stage: optionalString(values.stage),
85
+ mode: optionalString(values.mode),
86
+ dryRun: dryRun || undefined,
87
+ forceRegenerate: values["force-regenerate"] === true || undefined,
88
+ wait: values.wait === true || undefined,
89
+ idempotencyKey: mutationIdempotencyKey(values["idempotency-key"], dryRun)
90
+ }));
91
+ await followAgentRunEvents(response, context);
92
+ return response;
93
+ }
94
+ case "jobs watch": {
95
+ const values = parse(commandArgs, { "job-id": "string", "run-id": "string" });
96
+ return await context.client.watchJob({
97
+ jobId: requiredRunOrJobId(values),
98
+ onEvent: context.emitJsonlEvent
99
+ });
100
+ }
101
+ case "jobs cancel": {
102
+ const values = parse(commandArgs, { "job-id": "string" });
103
+ return await context.client.cancelJob({ jobId: requiredString(values["job-id"], "--job-id") });
104
+ }
105
+ case "distribution generate": {
106
+ const values = parse(commandArgs, { platform: "string", mode: "string", "dry-run": "boolean", "force-regenerate": "boolean", "idempotency-key": "string" });
107
+ const dryRun = values["dry-run"] === true;
108
+ return await context.client.generateDistribution(compact({
109
+ projectId: requireProject(context.config),
110
+ platform: optionalString(values.platform),
111
+ mode: optionalString(values.mode),
112
+ dryRun: dryRun || undefined,
113
+ forceRegenerate: values["force-regenerate"] === true || undefined,
114
+ idempotencyKey: mutationIdempotencyKey(values["idempotency-key"], dryRun)
115
+ }));
116
+ }
117
+ case "distribution status": {
118
+ const values = parse(commandArgs, { platform: "string" });
119
+ return await context.client.getDistributionStatus(compact({
120
+ projectId: requireProject(context.config),
121
+ platform: optionalString(values.platform)
122
+ }));
123
+ }
124
+ case "distribution connections list": {
125
+ const values = parse(commandArgs, { platform: "string" });
126
+ return await context.client.listDistributionConnections(compact({
127
+ projectId: requireProject(context.config),
128
+ platform: optionalString(values.platform)
129
+ }));
130
+ }
131
+ case "distribution connection select": {
132
+ const values = parse(commandArgs, { platform: "string", "connection-id": "string" });
133
+ return await context.client.selectDistributionConnection(compact({
134
+ projectId: requireProject(context.config),
135
+ platform: optionalString(values.platform),
136
+ connectionId: requiredString(values["connection-id"], "--connection-id")
137
+ }));
138
+ }
139
+ case "distribution publish": {
140
+ const values = parse(commandArgs, {
141
+ platform: "string",
142
+ privacy: "string",
143
+ "dry-run": "boolean",
144
+ "force-regenerate": "boolean",
145
+ "no-generate": "boolean",
146
+ "override-audit": "boolean",
147
+ "audit-override-rationale": "string",
148
+ wait: "boolean",
149
+ "idempotency-key": "string"
150
+ });
151
+ const dryRun = values["dry-run"] === true;
152
+ const response = await context.client.publishDistribution(compact({
153
+ projectId: requireProject(context.config),
154
+ platform: optionalString(values.platform),
155
+ privacy: optionalString(values.privacy),
156
+ dryRun: dryRun || undefined,
157
+ forceRegenerate: values["force-regenerate"] === true || undefined,
158
+ noGenerate: values["no-generate"] === true || undefined,
159
+ overrideAudit: values["override-audit"] === true || undefined,
160
+ auditOverrideRationale: optionalString(values["audit-override-rationale"]),
161
+ wait: values.wait === true || undefined,
162
+ idempotencyKey: mutationIdempotencyKey(values["idempotency-key"], dryRun)
163
+ }));
164
+ await followAgentRunEvents(response, context);
165
+ return response;
166
+ }
167
+ case "release-video create":
168
+ return await releaseVideoCreate(commandArgs, context);
169
+ case "release-video status":
170
+ return await releaseVideoStatus(commandArgs, context);
171
+ default:
172
+ throw new errors_1.CliError(commandPath.length ? `Unknown command: ${commandPath.join(" ")}` : "Missing command", {
173
+ code: "unknown_command",
174
+ exitCode: errors_1.EXIT_CODES.usage
175
+ });
176
+ }
177
+ }
178
+ async function authLogin(commandArgs, context) {
179
+ const values = parse(commandArgs, {
180
+ scopes: "string",
181
+ "expires-in": "string",
182
+ "expires-at": "string"
183
+ });
184
+ const scopes = optionalString(values.scopes)
185
+ ?.split(",")
186
+ .map((scope) => scope.trim())
187
+ .filter(Boolean) ?? DEFAULT_LOGIN_SCOPES;
188
+ const projectIds = context.config.projectId
189
+ ? [context.config.projectId]
190
+ : defaultLoginProjectIds(context.config.apiUrl);
191
+ const expiresAt = resolveExpiryIso(optionalString(values["expires-at"]), optionalString(values["expires-in"]) ?? "30d");
192
+ const startResponse = await context.client.startDeviceLogin({
193
+ scopes,
194
+ projectIds,
195
+ expiresAt
196
+ });
197
+ const startData = readEnvelopeData(startResponse);
198
+ const deviceCode = requiredString(startData.deviceCode, "deviceCode");
199
+ const verificationUri = optionalString(startData.verificationUri);
200
+ const userCode = optionalString(startData.userCode);
201
+ if (verificationUri) {
202
+ context.emitJsonlEvent?.({
203
+ type: "device_login_started",
204
+ verificationUri,
205
+ userCode,
206
+ expiresAt: optionalString(startData.expiresAt)
207
+ });
208
+ context.notice?.(`Open this URL to approve CLI login: ${verificationUri}`);
209
+ if (userCode) {
210
+ context.notice?.(`Device code: ${userCode}`);
211
+ }
212
+ if (context.openUrl) {
213
+ await context.openUrl(verificationUri);
214
+ }
215
+ }
216
+ const completeResponse = await pollDeviceLoginCompletion(context, deviceCode, startData);
217
+ const completeData = readEnvelopeData(completeResponse);
218
+ const rawToken = requiredString(completeData.rawToken, "rawToken");
219
+ const token = isRecord(completeData.token) ? completeData.token : {};
220
+ const tokenId = optionalString(token.id);
221
+ await (0, config_1.loginProfile)({
222
+ configRoot: context.configRoot,
223
+ env: context.env,
224
+ profile: context.config.profile,
225
+ token: rawToken
226
+ });
227
+ await (0, config_1.saveProfileConfig)({
228
+ configRoot: context.configRoot,
229
+ env: context.env,
230
+ profile: context.config.profile,
231
+ apiUrl: context.config.apiUrl,
232
+ projectId: context.config.projectId
233
+ });
234
+ return {
235
+ ok: true,
236
+ profile: context.config.profile,
237
+ apiUrl: context.config.apiUrl,
238
+ tokenId,
239
+ device: {
240
+ userCode: optionalString(readEnvelopeData(startResponse).userCode),
241
+ verificationUri: optionalString(readEnvelopeData(startResponse).verificationUri)
242
+ }
243
+ };
244
+ }
245
+ function defaultLoginProjectIds(apiUrl) {
246
+ if (isLocalApiUrl(apiUrl)) {
247
+ return "*";
248
+ }
249
+ throw new errors_1.CliError("Missing project id. Pass --project or set PDS_PROJECT_ID.", {
250
+ code: "missing_project",
251
+ exitCode: errors_1.EXIT_CODES.usage
252
+ });
253
+ }
254
+ function isLocalApiUrl(apiUrl) {
255
+ try {
256
+ const hostname = new URL(apiUrl).hostname.toLowerCase();
257
+ return hostname === "localhost" ||
258
+ hostname === "127.0.0.1" ||
259
+ hostname === "0.0.0.0" ||
260
+ hostname === "::1" ||
261
+ hostname === "[::1]";
262
+ }
263
+ catch {
264
+ return false;
265
+ }
266
+ }
267
+ async function pollDeviceLoginCompletion(context, deviceCode, startData) {
268
+ const sleep = context.sleep ?? defaultSleep;
269
+ const expiresAtMs = readExpiryMs(startData.expiresAt);
270
+ let intervalMs = readIntervalMs(startData.intervalSeconds, 1000);
271
+ let pendingPolls = 0;
272
+ while (true) {
273
+ try {
274
+ return await context.client.completeDeviceLogin({ deviceCode });
275
+ }
276
+ catch (error) {
277
+ if (!isPendingDeviceAuthorization(error)) {
278
+ throw error;
279
+ }
280
+ pendingPolls += 1;
281
+ intervalMs = readIntervalMs(readPendingIntervalSeconds(error), intervalMs);
282
+ context.emitJsonlEvent?.({
283
+ type: "device_login_pending",
284
+ poll: pendingPolls,
285
+ intervalSeconds: intervalMs / 1000
286
+ });
287
+ const now = Date.now();
288
+ if (now >= expiresAtMs) {
289
+ throw new errors_1.CliError("Device login session expired before approval", {
290
+ code: "auth_required",
291
+ exitCode: errors_1.EXIT_CODES.auth
292
+ });
293
+ }
294
+ await sleep(Math.min(intervalMs, Math.max(0, expiresAtMs - now)));
295
+ }
296
+ }
297
+ }
298
+ async function authTokenCreate(commandArgs, context) {
299
+ const values = parse(commandArgs, {
300
+ label: "string",
301
+ project: "string",
302
+ scopes: "string",
303
+ "expires-in": "string",
304
+ "expires-at": "string"
305
+ });
306
+ const projectId = optionalString(values.project) ?? requireProject(context.config);
307
+ const scopes = requiredString(values.scopes, "--scopes")
308
+ .split(",")
309
+ .map((scope) => scope.trim())
310
+ .filter(Boolean);
311
+ if (scopes.length === 0) {
312
+ throw new errors_1.CliError("Missing required option --scopes", { code: "missing_required_option", exitCode: errors_1.EXIT_CODES.usage });
313
+ }
314
+ return await context.client.createToken({
315
+ label: optionalString(values.label) ?? "pds cli token",
316
+ scopes,
317
+ projectIds: projectId === "*" ? "*" : [projectId],
318
+ expiresAt: resolveExpiryIso(optionalString(values["expires-at"]), optionalString(values["expires-in"]) ?? "30d")
319
+ });
320
+ }
321
+ async function authTokenRevoke(commandArgs, context) {
322
+ const values = parse(commandArgs, { "token-id": "string" });
323
+ return await context.client.revokeToken({
324
+ tokenId: requiredString(values["token-id"], "--token-id")
325
+ });
326
+ }
327
+ async function scriptSet(commandArgs, context) {
328
+ const values = parse(commandArgs, { file: "string", target: "string", "idempotency-key": "string" });
329
+ const idempotencyKey = requiredString(values["idempotency-key"], "--idempotency-key");
330
+ const sourcePath = requiredString(values.file, "--file");
331
+ const targetFile = normalizeScriptTarget(optionalString(values.target) ?? "main");
332
+ const content = await (0, promises_1.readFile)(sourcePath, "utf8");
333
+ return await context.client.setScript({
334
+ projectId: requireProject(context.config),
335
+ file: targetFile,
336
+ content,
337
+ idempotencyKey
338
+ });
339
+ }
340
+ async function projectsCreate(commandArgs, context) {
341
+ const values = parse(commandArgs, {
342
+ name: "string",
343
+ template: "string",
344
+ "project-id": "string",
345
+ "idempotency-key": "string"
346
+ });
347
+ const name = requiredString(values.name, "--name");
348
+ const projectId = optionalString(values["project-id"]) ??
349
+ (context.config.projectIdSource === "flag" ||
350
+ context.config.projectIdSource === "env"
351
+ ? context.config.projectId
352
+ : undefined);
353
+ return await context.client.createProject(compact({
354
+ name,
355
+ projectId,
356
+ template: optionalString(values.template),
357
+ idempotencyKey: requiredString(values["idempotency-key"], "--idempotency-key")
358
+ }));
359
+ }
360
+ async function projectResetPipeline(commandArgs, context) {
361
+ const values = parse(commandArgs, {
362
+ all: "boolean",
363
+ from: "string",
364
+ stage: "string",
365
+ "idempotency-key": "string",
366
+ });
367
+ const all = values.all === true;
368
+ const from = optionalString(values.from);
369
+ const stage = optionalString(values.stage);
370
+ const selectors = [
371
+ ...(all ? ["--all"] : []),
372
+ ...(from ? ["--from"] : []),
373
+ ...(stage ? ["--stage"] : []),
374
+ ];
375
+ if (selectors.length !== 1) {
376
+ throw new errors_1.CliError("Pass exactly one of --all, --from, or --stage", {
377
+ code: "invalid_arguments",
378
+ exitCode: errors_1.EXIT_CODES.usage,
379
+ });
380
+ }
381
+ return await context.client.resetPipeline(compact({
382
+ projectId: requireProject(context.config),
383
+ all: all ? true : undefined,
384
+ from,
385
+ stage,
386
+ idempotencyKey: requiredString(values["idempotency-key"], "--idempotency-key"),
387
+ }));
388
+ }
389
+ async function releaseVideoCreate(commandArgs, context) {
390
+ const values = parse(commandArgs, {
391
+ "project-name": "string",
392
+ script: "string",
393
+ "release-notes": "string",
394
+ changelog: "string",
395
+ "repo-url": "string",
396
+ "repo-name": "string",
397
+ "git-sha": "string",
398
+ "release-tag": "string",
399
+ preset: "string",
400
+ target: "string",
401
+ platform: "string",
402
+ privacy: "string",
403
+ "override-audit": "boolean",
404
+ "audit-override-rationale": "string",
405
+ "dry-run": "boolean",
406
+ "idempotency-key": "string",
407
+ wait: "boolean"
408
+ });
409
+ const dryRun = values["dry-run"] === true;
410
+ const idempotencyKey = mutationIdempotencyKey(values["idempotency-key"], dryRun);
411
+ const script = await fileContentRequest(optionalString(values.script));
412
+ const releaseNotes = await fileContentRequest(optionalString(values["release-notes"]));
413
+ const changelog = await fileContentRequest(optionalString(values.changelog));
414
+ const wait = !dryRun;
415
+ const requestPayload = compact({
416
+ projectName: optionalString(values["project-name"]),
417
+ projectId: context.config.projectIdSource === "flag" || context.config.projectIdSource === "env" ? context.config.projectId : undefined,
418
+ script,
419
+ releaseNotes,
420
+ changelog,
421
+ repoUrl: optionalString(values["repo-url"]),
422
+ repoName: optionalString(values["repo-name"]),
423
+ gitSha: optionalString(values["git-sha"]),
424
+ releaseTag: optionalString(values["release-tag"]),
425
+ preset: optionalString(values.preset),
426
+ target: optionalString(values.target),
427
+ platform: optionalString(values.platform),
428
+ privacy: optionalString(values.privacy),
429
+ overrideAudit: values["override-audit"] === true || undefined,
430
+ auditOverrideRationale: optionalString(values["audit-override-rationale"]),
431
+ dryRun: dryRun || undefined,
432
+ idempotencyKey,
433
+ wait: wait || undefined
434
+ });
435
+ const response = wait && !dryRun
436
+ ? await createReleaseVideoWithReattach(requestPayload, context)
437
+ : await context.client.createReleaseVideo(requestPayload);
438
+ if (wait && !dryRun) {
439
+ return await waitForReleaseVideoRun(response, context);
440
+ }
441
+ await followAgentRunEvents(response, context);
442
+ return response;
443
+ }
444
+ async function releaseVideoStatus(commandArgs, context) {
445
+ const values = parse(commandArgs, { "run-id": "string" });
446
+ const runId = requiredString(values["run-id"], "--run-id");
447
+ const jobResponse = await context.client.getJob({ jobId: runId });
448
+ return releaseVideoStatusFromJob(jobResponse, runId);
449
+ }
450
+ async function createReleaseVideoWithReattach(request, context) {
451
+ let attempt = 0;
452
+ let delayMs = RELEASE_VIDEO_REATTACH_INITIAL_DELAY_MS;
453
+ let sawDroppedCreate = false;
454
+ while (true) {
455
+ try {
456
+ return await context.client.createReleaseVideo(request);
457
+ }
458
+ catch (error) {
459
+ if (isRecoverableCreateWaitDrop(error)) {
460
+ if (attempt >= RELEASE_VIDEO_REATTACH_MAX_ATTEMPTS) {
461
+ throw error;
462
+ }
463
+ attempt += 1;
464
+ sawDroppedCreate = true;
465
+ context.emitJsonlEvent?.({
466
+ type: "wait_reconnect",
467
+ command: "release-video.create",
468
+ reason: "create_request_dropped"
469
+ });
470
+ continue;
471
+ }
472
+ if (sawDroppedCreate && isPendingIdempotencyReplayWithoutRunId(error)) {
473
+ if (attempt >= RELEASE_VIDEO_REATTACH_MAX_ATTEMPTS) {
474
+ throw new errors_1.CliError("Release video run handle was not available before retry limit", {
475
+ code: "release_video_reattach_timeout",
476
+ exitCode: errors_1.EXIT_CODES.timeout,
477
+ details: {
478
+ attempts: attempt,
479
+ reason: "idempotency_in_progress_without_run_id"
480
+ }
481
+ });
482
+ }
483
+ context.emitJsonlEvent?.({
484
+ type: "wait_reconnect",
485
+ command: "release-video.create",
486
+ reason: "idempotency_in_progress_without_run_id",
487
+ attempt: attempt + 1
488
+ });
489
+ await (context.sleep ?? defaultSleep)(delayMs);
490
+ delayMs = Math.min(delayMs * 2, RELEASE_VIDEO_REATTACH_MAX_DELAY_MS);
491
+ attempt += 1;
492
+ continue;
493
+ }
494
+ throw error;
495
+ }
496
+ }
497
+ }
498
+ async function fileContentRequest(sourcePath) {
499
+ if (!sourcePath) {
500
+ return undefined;
501
+ }
502
+ return {
503
+ content: await (0, promises_1.readFile)(sourcePath, "utf8"),
504
+ fileName: node_path_1.default.basename(sourcePath),
505
+ };
506
+ }
507
+ function parse(commandArgs, options) {
508
+ try {
509
+ return (0, node_util_1.parseArgs)({
510
+ args: commandArgs,
511
+ allowPositionals: false,
512
+ strict: true,
513
+ options: Object.fromEntries(Object.entries(options).map(([name, type]) => [name, { type }]))
514
+ }).values;
515
+ }
516
+ catch (error) {
517
+ const message = error instanceof Error ? error.message : String(error);
518
+ throw new errors_1.CliError(message, { code: "invalid_arguments", exitCode: errors_1.EXIT_CODES.usage });
519
+ }
520
+ }
521
+ function requireProject(config) {
522
+ if (!config.projectId) {
523
+ throw new errors_1.CliError("Missing project id. Pass --project or set PDS_PROJECT_ID.", {
524
+ code: "missing_project",
525
+ exitCode: errors_1.EXIT_CODES.usage
526
+ });
527
+ }
528
+ return config.projectId;
529
+ }
530
+ function requiredString(value, optionName) {
531
+ if (typeof value !== "string" || value.length === 0) {
532
+ throw new errors_1.CliError(`Missing required option ${optionName}`, { code: "missing_required_option", exitCode: errors_1.EXIT_CODES.usage });
533
+ }
534
+ return value;
535
+ }
536
+ function requiredRunOrJobId(values) {
537
+ const jobId = optionalString(values["job-id"]);
538
+ const runId = optionalString(values["run-id"]);
539
+ if (jobId && runId && jobId !== runId) {
540
+ throw new errors_1.CliError("Use only one of --job-id or --run-id", {
541
+ code: "conflicting_job_identifiers",
542
+ exitCode: errors_1.EXIT_CODES.usage,
543
+ });
544
+ }
545
+ return jobId ?? requiredString(runId, "--run-id");
546
+ }
547
+ function optionalString(value) {
548
+ return typeof value === "string" && value.length > 0 ? value : undefined;
549
+ }
550
+ function mutationIdempotencyKey(value, dryRun) {
551
+ if (dryRun) {
552
+ return optionalString(value);
553
+ }
554
+ return requiredString(value, "--idempotency-key");
555
+ }
556
+ function normalizeScriptTarget(value) {
557
+ if (value === "main" || value === "tts") {
558
+ return value;
559
+ }
560
+ throw new errors_1.CliError(`Unsupported script target: ${value}`, {
561
+ code: "invalid_script_target",
562
+ exitCode: errors_1.EXIT_CODES.usage,
563
+ details: {
564
+ supported: ["main", "tts"],
565
+ },
566
+ });
567
+ }
568
+ function readEnvelopeData(value) {
569
+ if (!isRecord(value)) {
570
+ return {};
571
+ }
572
+ if (isRecord(value.data)) {
573
+ return value.data;
574
+ }
575
+ return value;
576
+ }
577
+ async function followAgentRunEvents(response, context) {
578
+ const runId = agentRunIdFromResponse(response);
579
+ if (!context.emitJsonlEvent || !runId) {
580
+ return;
581
+ }
582
+ await context.client.watchJob({
583
+ jobId: runId,
584
+ onEvent: context.emitJsonlEvent
585
+ });
586
+ }
587
+ async function waitForReleaseVideoRun(response, context) {
588
+ const runId = agentRunIdFromResponse(response);
589
+ if (!runId) {
590
+ return response;
591
+ }
592
+ let confirmations = 0;
593
+ const seenEvents = new Set();
594
+ const emitRunEvent = context.emitJsonlEvent
595
+ ? (event) => {
596
+ const eventKey = JSON.stringify(event);
597
+ if (seenEvents.has(eventKey)) {
598
+ return;
599
+ }
600
+ context.emitJsonlEvent?.(event);
601
+ seenEvents.add(eventKey);
602
+ }
603
+ : undefined;
604
+ while (true) {
605
+ await context.client.watchJob({
606
+ jobId: runId,
607
+ onEvent: emitRunEvent
608
+ });
609
+ const jobResponse = await context.client.getJob({ jobId: runId });
610
+ const run = readJobRun(jobResponse);
611
+ assertTerminalReleaseVideoRunSucceeded(run, runId);
612
+ const merged = mergeReleaseVideoRun(response, jobResponse);
613
+ if (isTerminalReleaseVideoRun(run)) {
614
+ return merged;
615
+ }
616
+ if (confirmations >= RELEASE_VIDEO_WAIT_MAX_CONFIRMATIONS) {
617
+ throw new errors_1.CliError("Release video run did not reach a terminal state before wait limit", {
618
+ code: "release_video_wait_timeout",
619
+ exitCode: errors_1.EXIT_CODES.timeout,
620
+ details: {
621
+ runId,
622
+ status: run ? optionalString(run.status) ?? null : null,
623
+ confirmations
624
+ }
625
+ });
626
+ }
627
+ context.emitJsonlEvent?.({
628
+ type: "wait_reconnect",
629
+ command: "release-video.create",
630
+ reason: "event_stream_closed_before_terminal",
631
+ runId,
632
+ status: run ? optionalString(run.status) ?? null : null,
633
+ attempt: confirmations + 1
634
+ });
635
+ await (context.sleep ?? defaultSleep)(RELEASE_VIDEO_WAIT_POLL_MS);
636
+ confirmations += 1;
637
+ }
638
+ }
639
+ function agentRunIdFromResponse(value) {
640
+ const data = readEnvelopeData(value);
641
+ return (optionalString(data.runId) ??
642
+ runIdFromNested(data.agentRun) ??
643
+ runIdFromNested(data.pipeline, "run"));
644
+ }
645
+ function mergeReleaseVideoRun(response, jobResponse) {
646
+ const run = readJobRun(jobResponse);
647
+ if (!run || !isRecord(response)) {
648
+ return response;
649
+ }
650
+ const normalizedRun = {
651
+ ...run,
652
+ runId: optionalString(run.runId) ?? optionalString(run.id)
653
+ };
654
+ const data = readEnvelopeData(response);
655
+ if (!isRecord(data.pipeline)) {
656
+ return response;
657
+ }
658
+ const mergedData = {
659
+ ...data,
660
+ status: optionalString(normalizedRun.status) ?? optionalString(data.status) ?? "running",
661
+ pipeline: {
662
+ ...data.pipeline,
663
+ run: normalizedRun,
664
+ steps: Array.isArray(run.steps) ? run.steps : data.pipeline.steps,
665
+ terminalResult: run.terminalResult !== undefined
666
+ ? run.terminalResult
667
+ : data.pipeline.terminalResult
668
+ }
669
+ };
670
+ if (isRecord(response.data)) {
671
+ return {
672
+ ...response,
673
+ data: mergedData
674
+ };
675
+ }
676
+ return mergedData;
677
+ }
678
+ function releaseVideoStatusFromJob(jobResponse, fallbackRunId) {
679
+ const run = readJobRun(jobResponse);
680
+ if (!run) {
681
+ throw new errors_1.CliError("Agent job response did not include a run record", {
682
+ code: "invalid_agent_response",
683
+ exitCode: errors_1.EXIT_CODES.unexpected,
684
+ details: {
685
+ runId: fallbackRunId,
686
+ },
687
+ });
688
+ }
689
+ const runId = optionalString(run.runId) ?? optionalString(run.id) ?? fallbackRunId;
690
+ const normalizedRun = {
691
+ ...run,
692
+ runId,
693
+ };
694
+ const publicRun = publicReleaseVideoRun(normalizedRun);
695
+ const statusData = compact({
696
+ command: "release-video.status",
697
+ runId,
698
+ status: optionalString(publicRun.status),
699
+ projectId: optionalString(publicRun.projectId),
700
+ target: optionalString(publicRun.target),
701
+ pipeline: {
702
+ run: publicRun,
703
+ steps: Array.isArray(publicRun.steps) ? publicRun.steps : undefined,
704
+ terminalResult: publicRun.terminalResult,
705
+ },
706
+ summary: releaseVideoRunSummary(publicRun),
707
+ });
708
+ if (isRecord(jobResponse) && "ok" in jobResponse) {
709
+ return {
710
+ ...jobResponse,
711
+ data: statusData,
712
+ };
713
+ }
714
+ return {
715
+ ok: true,
716
+ data: statusData,
717
+ };
718
+ }
719
+ function publicReleaseVideoRun(run) {
720
+ const publicRun = {
721
+ ...run,
722
+ };
723
+ if (isRecord(run.error)) {
724
+ publicRun.error = publicReleaseVideoFailureError(run.error);
725
+ }
726
+ if (Array.isArray(run.steps)) {
727
+ publicRun.steps = run.steps.map((step) => {
728
+ if (!isRecord(step)) {
729
+ return step;
730
+ }
731
+ return {
732
+ ...step,
733
+ error: isRecord(step.error)
734
+ ? publicReleaseVideoFailureError(step.error)
735
+ : step.error,
736
+ };
737
+ });
738
+ }
739
+ return publicRun;
740
+ }
741
+ function releaseVideoRunSummary(run) {
742
+ const terminalResult = isRecord(run.terminalResult) ? run.terminalResult : {};
743
+ const youtubeUrl = readNestedString(terminalResult, ["publish", "publishStatus", "videoUrl"]) ??
744
+ readNestedString(terminalResult, ["summary", "youtubeUrl"]) ??
745
+ readNestedString(terminalResult, ["youtubeUrl"]);
746
+ const summary = compact({
747
+ youtubeUrl,
748
+ });
749
+ return Object.keys(summary).length > 0 ? summary : undefined;
750
+ }
751
+ function readNestedString(value, path) {
752
+ let cursor = value;
753
+ for (const segment of path) {
754
+ if (!isRecord(cursor)) {
755
+ return undefined;
756
+ }
757
+ cursor = cursor[segment];
758
+ }
759
+ return optionalString(cursor);
760
+ }
761
+ function readJobRun(value) {
762
+ const data = readEnvelopeData(value);
763
+ if (isRecord(data.run)) {
764
+ return data.run;
765
+ }
766
+ return null;
767
+ }
768
+ function assertTerminalReleaseVideoRunSucceeded(run, fallbackRunId) {
769
+ if (!run) {
770
+ return;
771
+ }
772
+ const status = optionalString(run.status);
773
+ if (!status || status === "succeeded") {
774
+ return;
775
+ }
776
+ if (status === "failed" || status === "cancelled" || status === "timed_out") {
777
+ const error = isRecord(run.error) ? run.error : null;
778
+ const publicError = publicReleaseVideoFailureError(error);
779
+ const terminalCode = status === "timed_out"
780
+ ? "timeout"
781
+ : status === "cancelled"
782
+ ? "cancelled"
783
+ : undefined;
784
+ const errorCode = optionalString(error?.code);
785
+ const code = terminalCode ??
786
+ errorCode ??
787
+ "agent_run_failed";
788
+ const message = errorCode === code
789
+ ? optionalString(error?.message) ?? releaseRunFailureMessage(status)
790
+ : releaseRunFailureMessage(status);
791
+ throw new errors_1.CliError(message, {
792
+ code,
793
+ exitCode: (0, errors_1.mapApiErrorToExitCode)(code, undefined),
794
+ details: compact({
795
+ runId: optionalString(run.runId) ?? optionalString(run.id) ?? fallbackRunId,
796
+ status,
797
+ failedStage: releaseVideoFailedStage(run),
798
+ logRef: releaseVideoFailureLogRef(run),
799
+ error: publicError
800
+ })
801
+ });
802
+ }
803
+ }
804
+ function isTerminalReleaseVideoRun(run) {
805
+ const status = run ? optionalString(run.status) : undefined;
806
+ return status === "succeeded" ||
807
+ status === "failed" ||
808
+ status === "cancelled" ||
809
+ status === "timed_out";
810
+ }
811
+ function releaseRunFailureMessage(status) {
812
+ if (status === "timed_out") {
813
+ return "Release video run timed out";
814
+ }
815
+ if (status === "cancelled") {
816
+ return "Release video run cancelled";
817
+ }
818
+ return "Release video run failed";
819
+ }
820
+ function releaseVideoFailedStage(run) {
821
+ const error = isRecord(run.error) ? run.error : null;
822
+ const details = isRecord(error?.details) ? error.details : null;
823
+ return optionalString(details?.failedStage) ?? failedStepStage(run);
824
+ }
825
+ function publicReleaseVideoFailureError(error) {
826
+ if (!error) {
827
+ return null;
828
+ }
829
+ const code = optionalString(error.code);
830
+ if (!isRecord(error.details)) {
831
+ return {
832
+ ...error,
833
+ };
834
+ }
835
+ return {
836
+ ...error,
837
+ details: publicReleaseVideoErrorDetails(code, error.details),
838
+ };
839
+ }
840
+ function publicReleaseVideoErrorDetails(code, details) {
841
+ const sanitized = sanitizeReleaseVideoInternalErrorDetails(details, code === "internal_error");
842
+ return isRecord(sanitized) ? sanitized : {};
843
+ }
844
+ function sanitizeReleaseVideoInternalErrorDetails(value, stripPrivateDetails) {
845
+ if (Array.isArray(value)) {
846
+ return value.map((item) => sanitizeReleaseVideoInternalErrorDetails(item, stripPrivateDetails));
847
+ }
848
+ if (!isRecord(value)) {
849
+ return value;
850
+ }
851
+ const nestedStripPrivateDetails = stripPrivateDetails || value.code === "internal_error";
852
+ return Object.fromEntries(Object.entries(value)
853
+ .filter(([key]) => !(nestedStripPrivateDetails &&
854
+ PRIVATE_INTERNAL_ERROR_DETAIL_KEY_PATTERN.test(key)))
855
+ .map(([key, entryValue]) => [
856
+ key,
857
+ sanitizeReleaseVideoInternalErrorDetails(entryValue, nestedStripPrivateDetails),
858
+ ]));
859
+ }
860
+ function releaseVideoFailureLogRef(run) {
861
+ const error = isRecord(run.error) ? run.error : null;
862
+ const details = isRecord(error?.details) ? error.details : null;
863
+ if (isRecord(details?.logRef)) {
864
+ return details.logRef;
865
+ }
866
+ const failedStage = failedStepStage(run);
867
+ const runId = optionalString(run.runId) ?? optionalString(run.id);
868
+ if (!failedStage || !runId) {
869
+ return undefined;
870
+ }
871
+ return {
872
+ runId,
873
+ stage: failedStage,
874
+ };
875
+ }
876
+ function failedStepStage(run) {
877
+ if (!Array.isArray(run.steps)) {
878
+ return undefined;
879
+ }
880
+ const failedStep = run.steps.find((step) => isRecord(step) && step.status === "failed");
881
+ return isRecord(failedStep)
882
+ ? optionalString(failedStep.id) ?? optionalString(failedStep.stage)
883
+ : undefined;
884
+ }
885
+ function isRecoverableCreateWaitDrop(error) {
886
+ if (error instanceof errors_1.CliError) {
887
+ return error.code === "network_error" || isClientTimeoutDrop(error);
888
+ }
889
+ return error instanceof TypeError || isTransportLikeError(error);
890
+ }
891
+ function isClientTimeoutDrop(error) {
892
+ return error.code === "timeout" &&
893
+ error.status === undefined &&
894
+ /^Request timed out after \d+ms$/.test(error.message);
895
+ }
896
+ function isTransportLikeError(error) {
897
+ if (!(error instanceof Error)) {
898
+ return false;
899
+ }
900
+ return /\b(socket|network|fetch|body|stream|connection|econnreset|terminated|aborted)\b/i.test(error.message);
901
+ }
902
+ function isPendingIdempotencyReplayWithoutRunId(error) {
903
+ if (!(error instanceof errors_1.CliError) || error.code !== "idempotency_key_reused") {
904
+ return false;
905
+ }
906
+ if (!isRecord(error.details)) {
907
+ return false;
908
+ }
909
+ return error.details.status === "in_progress" &&
910
+ optionalString(error.details.runId) === undefined;
911
+ }
912
+ function runIdFromNested(value, childKey) {
913
+ const record = childKey && isRecord(value) ? value[childKey] : value;
914
+ if (!isRecord(record)) {
915
+ return undefined;
916
+ }
917
+ return optionalString(record.runId);
918
+ }
919
+ function isPendingDeviceAuthorization(error) {
920
+ return (error instanceof errors_1.CliError &&
921
+ error.code === "auth_required" &&
922
+ isRecord(error.details) &&
923
+ error.details.reason === "device_authorization_pending");
924
+ }
925
+ function readPendingIntervalSeconds(error) {
926
+ return isRecord(error.details) ? error.details.intervalSeconds : undefined;
927
+ }
928
+ function readExpiryMs(value) {
929
+ const expiresAt = typeof value === "string" ? Date.parse(value) : Number.NaN;
930
+ if (Number.isFinite(expiresAt)) {
931
+ return expiresAt;
932
+ }
933
+ return Date.now() + 10 * 60 * 1000;
934
+ }
935
+ function readIntervalMs(value, fallbackMs) {
936
+ const numeric = typeof value === "number" ? value : Number(value);
937
+ if (!Number.isFinite(numeric)) {
938
+ return fallbackMs;
939
+ }
940
+ return Math.max(0, Math.round(numeric * 1000));
941
+ }
942
+ function isRecord(value) {
943
+ return value !== null && typeof value === "object" && !Array.isArray(value);
944
+ }
945
+ function ensureNoCommandArgs(commandArgs) {
946
+ if (commandArgs.length > 0) {
947
+ throw new errors_1.CliError(`Unexpected arguments: ${commandArgs.join(" ")}`, { code: "invalid_arguments", exitCode: errors_1.EXIT_CODES.usage });
948
+ }
949
+ }
950
+ function resolveExpiryIso(expiresAt, expiresIn) {
951
+ if (expiresAt) {
952
+ const parsed = Date.parse(expiresAt);
953
+ if (!Number.isFinite(parsed)) {
954
+ throw new errors_1.CliError(`Invalid --expires-at value: ${expiresAt}`, { code: "invalid_expiry", exitCode: errors_1.EXIT_CODES.usage });
955
+ }
956
+ return new Date(parsed).toISOString();
957
+ }
958
+ const durationMs = parseExtendedDurationMs(expiresIn);
959
+ return new Date(Date.now() + durationMs).toISOString();
960
+ }
961
+ function parseExtendedDurationMs(value) {
962
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/.exec(value.trim());
963
+ if (!match) {
964
+ throw new errors_1.CliError(`Invalid duration: ${value}`, { code: "invalid_duration", exitCode: errors_1.EXIT_CODES.usage });
965
+ }
966
+ const amount = Number(match[1]);
967
+ const unit = match[2] ?? "ms";
968
+ const multipliers = {
969
+ ms: 1,
970
+ s: 1000,
971
+ m: 60_000,
972
+ h: 60 * 60_000,
973
+ d: 24 * 60 * 60_000
974
+ };
975
+ const durationMs = Math.round(amount * multipliers[unit]);
976
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
977
+ throw new errors_1.CliError(`Invalid duration: ${value}`, { code: "invalid_duration", exitCode: errors_1.EXIT_CODES.usage });
978
+ }
979
+ return durationMs;
980
+ }
981
+ function compact(value) {
982
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
983
+ }
984
+ function defaultSleep(ms) {
985
+ return new Promise((resolve) => setTimeout(resolve, ms));
986
+ }
987
+ //# sourceMappingURL=registry.js.map