deepline 0.1.12 → 0.1.20

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.
Files changed (82) hide show
  1. package/README.md +14 -6
  2. package/dist/cli/index.js +1346 -717
  3. package/dist/cli/index.mjs +1342 -713
  4. package/dist/index.d.mts +199 -23
  5. package/dist/index.d.ts +199 -23
  6. package/dist/index.js +221 -14
  7. package/dist/index.mjs +221 -14
  8. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +214 -77
  9. package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +85 -60
  10. package/dist/repo/apps/play-runner-workers/src/entry.ts +385 -66
  11. package/dist/repo/sdk/src/client.ts +237 -0
  12. package/dist/repo/sdk/src/config.ts +125 -8
  13. package/dist/repo/sdk/src/http.ts +29 -5
  14. package/dist/repo/sdk/src/play.ts +19 -36
  15. package/dist/repo/sdk/src/plays/bundle-play-file.ts +22 -8
  16. package/dist/repo/sdk/src/plays/local-file-discovery.ts +207 -160
  17. package/dist/repo/sdk/src/types.ts +25 -0
  18. package/dist/repo/sdk/src/version.ts +2 -2
  19. package/dist/repo/shared_libs/play-runtime/tool-result.ts +237 -145
  20. package/dist/repo/shared_libs/plays/bundling/index.ts +206 -229
  21. package/dist/repo/shared_libs/plays/dataset.ts +28 -0
  22. package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
  23. package/package.json +5 -4
  24. package/dist/cli/index.js.map +0 -1
  25. package/dist/cli/index.mjs.map +0 -1
  26. package/dist/index.js.map +0 -1
  27. package/dist/index.mjs.map +0 -1
  28. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +0 -21
  29. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +0 -177
  30. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +0 -52
  31. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +0 -100
  32. package/dist/repo/sdk/src/cli/commands/auth.ts +0 -500
  33. package/dist/repo/sdk/src/cli/commands/billing.ts +0 -188
  34. package/dist/repo/sdk/src/cli/commands/csv.ts +0 -123
  35. package/dist/repo/sdk/src/cli/commands/db.ts +0 -119
  36. package/dist/repo/sdk/src/cli/commands/feedback.ts +0 -40
  37. package/dist/repo/sdk/src/cli/commands/org.ts +0 -117
  38. package/dist/repo/sdk/src/cli/commands/play.ts +0 -3441
  39. package/dist/repo/sdk/src/cli/commands/tools.ts +0 -687
  40. package/dist/repo/sdk/src/cli/dataset-stats.ts +0 -415
  41. package/dist/repo/sdk/src/cli/index.ts +0 -148
  42. package/dist/repo/sdk/src/cli/progress.ts +0 -149
  43. package/dist/repo/sdk/src/cli/skills-sync.ts +0 -141
  44. package/dist/repo/sdk/src/cli/trace.ts +0 -61
  45. package/dist/repo/sdk/src/cli/utils.ts +0 -145
  46. package/dist/repo/sdk/src/compat.ts +0 -77
  47. package/dist/repo/shared_libs/observability/node-tracing.ts +0 -129
  48. package/dist/repo/shared_libs/observability/tracing.ts +0 -98
  49. package/dist/repo/shared_libs/play-runtime/context.ts +0 -4242
  50. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +0 -250
  51. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +0 -725
  52. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +0 -10
  53. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +0 -304
  54. package/dist/repo/shared_libs/play-runtime/db-session.ts +0 -462
  55. package/dist/repo/shared_libs/play-runtime/live-events.ts +0 -214
  56. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +0 -50
  57. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +0 -114
  58. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +0 -158
  59. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +0 -172
  60. package/dist/repo/shared_libs/play-runtime/protocol.ts +0 -121
  61. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +0 -42
  62. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +0 -33
  63. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +0 -1873
  64. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +0 -2
  65. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +0 -201
  66. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +0 -48
  67. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +0 -84
  68. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +0 -147
  69. package/dist/repo/shared_libs/play-runtime/suspension.ts +0 -68
  70. package/dist/repo/shared_libs/play-runtime/tracing.ts +0 -31
  71. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +0 -75
  72. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +0 -140
  73. package/dist/repo/shared_libs/plays/artifact-transport.ts +0 -14
  74. package/dist/repo/shared_libs/plays/artifact-types.ts +0 -49
  75. package/dist/repo/shared_libs/plays/compiler-manifest.ts +0 -186
  76. package/dist/repo/shared_libs/plays/definition.ts +0 -264
  77. package/dist/repo/shared_libs/plays/file-refs.ts +0 -11
  78. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +0 -206
  79. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +0 -164
  80. package/dist/repo/shared_libs/plays/runtime-validation.ts +0 -395
  81. package/dist/repo/shared_libs/temporal/constants.ts +0 -39
  82. package/dist/repo/shared_libs/temporal/preview-config.ts +0 -153
@@ -1,3441 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import {
3
- existsSync,
4
- readFileSync,
5
- readdirSync,
6
- realpathSync,
7
- writeFileSync,
8
- } from 'node:fs';
9
- import { basename, dirname, join, resolve } from 'node:path';
10
- import { Command, Option } from 'commander';
11
- import { DeeplineClient, type PlayStatus } from '../../client.js';
12
- import { DeeplineError } from '../../errors.js';
13
- import {
14
- bundlePlayFile,
15
- extractDefinedPlayName,
16
- type BundledPlayFileSuccess,
17
- } from '../../plays/bundle-play-file.js';
18
- import type { PlayStagedFileRef } from '../../plays/local-file-discovery.js';
19
- import type {
20
- PlayDescription,
21
- PlayDetail,
22
- PlayLiveEvent,
23
- PlayListItem,
24
- PlayRevisionSummary,
25
- PlayRunListItem,
26
- } from '../../types.js';
27
- import {
28
- buildDatasetStats,
29
- extractCanonicalRowsInfo,
30
- writeCanonicalRowsCsv,
31
- type CanonicalRowsInfo,
32
- type DatasetStats,
33
- } from '../dataset-stats.js';
34
- import {
35
- createCliProgress,
36
- getActiveCliProgress,
37
- type CliProgress,
38
- } from '../progress.js';
39
- import { argsWantJson } from '../utils.js';
40
-
41
- type PlayRunCommandOptions = {
42
- target: { kind: 'file'; path: string } | { kind: 'name'; name: string };
43
- input: Record<string, unknown> | null;
44
- revisionId: string | null;
45
- revisionSelector: 'live' | 'latest' | null;
46
- watch: boolean;
47
- emitLogs: boolean;
48
- jsonOutput: boolean;
49
- pollIntervalMs: number;
50
- waitTimeoutMs: number | null;
51
- force: boolean;
52
- outPath: string | null;
53
- };
54
-
55
- type FileInputBinding = {
56
- inputPath: string;
57
- };
58
-
59
- type PlayCheckCommandOptions = {
60
- target: string;
61
- jsonOutput: boolean;
62
- };
63
-
64
- type PlaySearchOptions = {
65
- query: string;
66
- jsonOutput: boolean;
67
- compact: boolean;
68
- origin: 'prebuilt' | 'owned' | undefined;
69
- };
70
-
71
- function parseReferencedPlayTarget(target: string): {
72
- ownerSlug: string | null;
73
- playName: string;
74
- unqualifiedPlayName: string;
75
- } {
76
- const trimmed = target.trim();
77
- const slashIndex = trimmed.indexOf('/');
78
- if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
79
- return { ownerSlug: null, playName: trimmed, unqualifiedPlayName: trimmed };
80
- }
81
- return {
82
- ownerSlug: trimmed.slice(0, slashIndex),
83
- playName: trimmed,
84
- unqualifiedPlayName: trimmed.slice(slashIndex + 1),
85
- };
86
- }
87
-
88
- function isPrebuiltReferenceTarget(target: string): boolean {
89
- return target.trim().toLowerCase().startsWith('prebuilt/');
90
- }
91
-
92
- function buildBarePrebuiltReferenceError(input: {
93
- requested: string;
94
- reference: string;
95
- }): Error {
96
- return new Error(
97
- `Prebuilt play "${input.requested}" must be referenced as "${input.reference}". ` +
98
- `Use the prebuilt/ namespace anywhere you run, describe, get, or link to Deepline-managed plays.`,
99
- );
100
- }
101
-
102
- async function assertCanonicalNamedPlayReference(
103
- client: DeeplineClient,
104
- target: string,
105
- ): Promise<PlayDetail> {
106
- const parsed = parseReferencedPlayTarget(target);
107
- const detail = await client.getPlay(parsed.playName);
108
- if (
109
- detail.play.ownerType === 'deepline' &&
110
- !isPrebuiltReferenceTarget(target)
111
- ) {
112
- throw buildBarePrebuiltReferenceError({
113
- requested: target,
114
- reference: formatPlayReference(detail.play),
115
- });
116
- }
117
- return detail;
118
- }
119
-
120
- function formatPlayReference(
121
- play: Pick<PlayDetail['play'], 'reference' | 'ownerSlug' | 'name'>,
122
- ): string {
123
- if (play.reference) {
124
- return play.reference;
125
- }
126
- const ownerSlug = play.ownerSlug?.trim() || 'org';
127
- return `${ownerSlug}/${play.name}`;
128
- }
129
-
130
- function formatPlayListReference(play: PlayListItem | PlayDescription): string {
131
- return play.reference || play.name;
132
- }
133
-
134
- function defaultMaterializedPlayPath(reference: string): string {
135
- const playName = parseReferencedPlayTarget(reference).unqualifiedPlayName;
136
- const safeName = playName
137
- .trim()
138
- .toLowerCase()
139
- .replace(/[^a-z0-9-]/g, '-')
140
- .replace(/-+/g, '-')
141
- .replace(/^-|-$/g, '');
142
- return resolve(`${safeName || 'play'}.play.ts`);
143
- }
144
-
145
- type MaterializedRemotePlaySource = {
146
- path: string;
147
- status: 'created' | 'updated' | 'unchanged';
148
- created: boolean;
149
- };
150
-
151
- function materializeRemotePlaySource(input: {
152
- target: string;
153
- playName: string;
154
- sourceCode: string;
155
- outPath: string | null;
156
- }): MaterializedRemotePlaySource | null {
157
- if (isFileTarget(input.target)) {
158
- return null;
159
- }
160
- if (!input.sourceCode.trim()) {
161
- return null;
162
- }
163
-
164
- const outputPath =
165
- input.outPath ?? defaultMaterializedPlayPath(input.playName);
166
- if (existsSync(outputPath)) {
167
- const existingSource = readFileSync(outputPath, 'utf-8');
168
- if (existingSource === input.sourceCode) {
169
- return { path: outputPath, status: 'unchanged', created: false };
170
- }
171
- writeFileSync(outputPath, input.sourceCode, 'utf-8');
172
- return { path: outputPath, status: 'updated', created: false };
173
- }
174
-
175
- writeFileSync(outputPath, input.sourceCode, 'utf-8');
176
- return { path: outputPath, status: 'created', created: true };
177
- }
178
-
179
- function formatLoadedPlayMessage(
180
- materializedFile: MaterializedRemotePlaySource,
181
- ): string {
182
- if (materializedFile.status === 'unchanged') {
183
- return `Loaded play here: ${materializedFile.path} (unchanged)`;
184
- }
185
- if (materializedFile.status === 'updated') {
186
- return `Loaded play here: ${materializedFile.path} (updated)`;
187
- }
188
- return `Loaded play here: ${materializedFile.path}`;
189
- }
190
-
191
- function buildReadonlyPrebuiltPlayError(reference: string): Error {
192
- return new Error(
193
- `Cannot edit or push ${reference} because Deepline prebuilt plays are read-only.\n` +
194
- `To make your own version:\n` +
195
- `1. Copy the source into a new local file.\n` +
196
- `2. Change definePlay('${reference.split('/').slice(1).join('/')}', ...) to a new play name you own.\n` +
197
- `3. Run: deepline plays publish <your-file.play.ts>\n` +
198
- `4. Your play will then live under your workspace namespace.`,
199
- );
200
- }
201
-
202
- async function ensureEditableRemotePlay(
203
- client: DeeplineClient,
204
- target: string,
205
- ): Promise<PlayDetail> {
206
- const parsed = parseReferencedPlayTarget(target);
207
- const detail = await client.getPlay(parsed.playName);
208
- if (detail.play.ownerType === 'deepline') {
209
- throw buildReadonlyPrebuiltPlayError(formatPlayReference(detail.play));
210
- }
211
- return detail;
212
- }
213
-
214
- function buildMissingDefinePlayError(filePath: string): Error {
215
- return new Error(
216
- `Play file ${filePath} must export definePlay('play-name', async (...) => { ... }). ` +
217
- 'Plain `export default async function ...` plays are no longer supported for SDK/CLI file-backed runs.',
218
- );
219
- }
220
-
221
- function extractPlayName(code: string, filePath: string): string {
222
- const definedPlayName = extractDefinedPlayName(code, filePath);
223
- if (definedPlayName) {
224
- return definedPlayName;
225
- }
226
- throw buildMissingDefinePlayError(filePath);
227
- }
228
-
229
- function isFileTarget(target: string): boolean {
230
- return existsSync(resolve(target));
231
- }
232
-
233
- function looksLikeFilePath(target: string): boolean {
234
- if (target.trim().toLowerCase().startsWith('prebuilt/')) {
235
- return false;
236
- }
237
- if (
238
- target.startsWith('./') ||
239
- target.startsWith('../') ||
240
- target.startsWith('/') ||
241
- target.startsWith('~/')
242
- ) {
243
- return true;
244
- }
245
- return (
246
- target.includes('\\') ||
247
- /\.(ts|js|mjs|play\.ts)$/.test(target)
248
- );
249
- }
250
-
251
- function parsePositiveInteger(value: string, flagName: string): number {
252
- const parsed = Number.parseInt(value, 10);
253
- if (!Number.isFinite(parsed) || parsed <= 0) {
254
- throw new Error(`${flagName} must be a positive integer.`);
255
- }
256
- return parsed;
257
- }
258
-
259
- function parseJsonInput(raw: string): Record<string, unknown> {
260
- const source = raw.startsWith('@')
261
- ? readFileSync(resolve(raw.slice(1)), 'utf-8')
262
- : raw;
263
- const parsed = JSON.parse(source);
264
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
265
- throw new Error('--input must be a JSON object.');
266
- }
267
- return parsed as Record<string, unknown>;
268
- }
269
-
270
- function parseInputFieldFlag(
271
- rawFlag: string,
272
- nextArg: string | undefined,
273
- ): { path: string; value: string } {
274
- const flag = rawFlag.slice(2);
275
- const equalsIndex = flag.indexOf('=');
276
- if (equalsIndex > 0) {
277
- const path = flag.slice(0, equalsIndex).trim();
278
- const value = flag.slice(equalsIndex + 1);
279
- if (!path) {
280
- throw new Error(`Invalid play input flag: ${rawFlag}`);
281
- }
282
- return { path, value };
283
- }
284
- if (!nextArg || nextArg.startsWith('--')) {
285
- throw new Error(`Play input flag ${rawFlag} requires a value.`);
286
- }
287
- return { path: flag, value: nextArg };
288
- }
289
-
290
- function parseInputFlagValue(raw: string): unknown {
291
- const trimmed = raw.trim();
292
- if (!trimmed) return '';
293
- if (
294
- trimmed === 'true' ||
295
- trimmed === 'false' ||
296
- trimmed === 'null' ||
297
- trimmed.startsWith('{') ||
298
- trimmed.startsWith('[') ||
299
- /^-?\d+(\.\d+)?$/.test(trimmed)
300
- ) {
301
- try {
302
- return JSON.parse(trimmed);
303
- } catch {
304
- return raw;
305
- }
306
- }
307
- return raw;
308
- }
309
-
310
- function getDottedInputValue(
311
- input: Record<string, unknown>,
312
- path: string,
313
- ): unknown {
314
- const parts = path.split('.').map((part) => part.trim()).filter(Boolean);
315
- let cursor: unknown = input;
316
- for (const part of parts) {
317
- if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor)) {
318
- return undefined;
319
- }
320
- cursor = (cursor as Record<string, unknown>)[part];
321
- }
322
- return cursor;
323
- }
324
-
325
- function setDottedInputValue(
326
- input: Record<string, unknown>,
327
- path: string,
328
- value: unknown,
329
- ): void {
330
- const parts = path.split('.').map((part) => part.trim()).filter(Boolean);
331
- if (parts.length === 0) {
332
- throw new Error(`Invalid play input flag path: ${path}`);
333
- }
334
- let cursor: Record<string, unknown> = input;
335
- for (const part of parts.slice(0, -1)) {
336
- const existing = cursor[part];
337
- if (
338
- existing !== undefined &&
339
- (!existing || typeof existing !== 'object' || Array.isArray(existing))
340
- ) {
341
- throw new Error(
342
- `Cannot set --${path}; input.${part} is already a non-object value.`,
343
- );
344
- }
345
- if (!existing) {
346
- cursor[part] = {};
347
- }
348
- cursor = cursor[part] as Record<string, unknown>;
349
- }
350
- cursor[parts[parts.length - 1]!] = value;
351
- }
352
-
353
- function schemaMetadata(
354
- schema: Record<string, unknown> | null | undefined,
355
- key: string,
356
- ): Record<string, unknown> | null {
357
- if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return null;
358
- const value = schema[key];
359
- return value && typeof value === 'object' && !Array.isArray(value)
360
- ? (value as Record<string, unknown>)
361
- : null;
362
- }
363
-
364
- function stringMetadata(
365
- metadata: Record<string, unknown> | null | undefined,
366
- key: string,
367
- ): string | null {
368
- const value = metadata?.[key];
369
- return typeof value === 'string' && value.trim() ? value.trim() : null;
370
- }
371
-
372
- function inputFieldFromCsvArg(csvArg: unknown): string | null {
373
- if (typeof csvArg !== 'string') return null;
374
- const match = /^input\.([A-Za-z_$][\w$]*)$/.exec(csvArg.trim());
375
- return match?.[1] ?? null;
376
- }
377
-
378
- function fileInputBindingsFromPlaySchema(
379
- inputSchema: Record<string, unknown> | null | undefined,
380
- ): FileInputBinding[] {
381
- const csvInput = schemaMetadata(inputSchema, 'csvInput');
382
- if (!csvInput) return [];
383
- return [
384
- {
385
- inputPath: stringMetadata(csvInput, 'inputField') ?? 'csv',
386
- },
387
- ];
388
- }
389
-
390
- function fileInputBindingsFromStaticPipeline(
391
- staticPipeline: unknown,
392
- ): FileInputBinding[] {
393
- if (!staticPipeline || typeof staticPipeline !== 'object' || Array.isArray(staticPipeline)) {
394
- return [];
395
- }
396
- const inputField = inputFieldFromCsvArg(
397
- (staticPipeline as Record<string, unknown>).csvArg,
398
- );
399
- return inputField ? [{ inputPath: inputField }] : [];
400
- }
401
-
402
- function isLocalFilePathValue(value: unknown): value is string {
403
- if (typeof value !== 'string' || !value.trim()) return false;
404
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value.trim())) return false;
405
- return existsSync(resolve(value));
406
- }
407
-
408
- async function stageFileInputArgs(input: {
409
- client: DeeplineClient;
410
- runtimeInput: Record<string, unknown>;
411
- bindings: FileInputBinding[];
412
- progress: CliProgress;
413
- }): Promise<{
414
- inputFile: PlayStagedFileRef | null;
415
- packagedFiles: PlayStagedFileRef[];
416
- }> {
417
- const uniqueBindings = [
418
- ...new Map(input.bindings.map((binding) => [binding.inputPath, binding])).values(),
419
- ];
420
- const localFiles = uniqueBindings.flatMap((binding) => {
421
- const value = getDottedInputValue(input.runtimeInput, binding.inputPath);
422
- if (!isLocalFilePathValue(value)) return [];
423
- const absolutePath = resolve(value);
424
- return [{ binding, absolutePath, logicalPath: basename(absolutePath) }];
425
- });
426
-
427
- if (localFiles.length === 0) {
428
- return { inputFile: null, packagedFiles: [] };
429
- }
430
-
431
- input.progress.phase(
432
- localFiles.length === 1 ? 'staging input file' : 'staging input files',
433
- );
434
- const staged = await input.client.stagePlayFiles(
435
- localFiles.map((file) => stageFile(file.logicalPath, file.absolutePath)),
436
- );
437
- for (const [index, file] of localFiles.entries()) {
438
- setDottedInputValue(input.runtimeInput, file.binding.inputPath, file.logicalPath);
439
- const stagedFile = staged[index];
440
- if (stagedFile && stagedFile.logicalPath !== file.logicalPath) {
441
- setDottedInputValue(input.runtimeInput, file.binding.inputPath, stagedFile.logicalPath);
442
- }
443
- }
444
-
445
- return {
446
- inputFile: staged[0] ?? null,
447
- packagedFiles: staged.slice(1),
448
- };
449
- }
450
-
451
- function stageFile(logicalPath: string, absolutePath: string) {
452
- const buffer = readFileSync(absolutePath);
453
- return {
454
- logicalPath,
455
- contentBase64: buffer.toString('base64'),
456
- contentHash: createHash('sha256').update(buffer).digest('hex'),
457
- contentType: absolutePath.toLowerCase().endsWith('.csv')
458
- ? 'text/csv'
459
- : absolutePath.toLowerCase().endsWith('.json')
460
- ? 'application/json'
461
- : 'application/octet-stream',
462
- bytes: buffer.byteLength,
463
- };
464
- }
465
-
466
- function normalizePlayPath(filePath: string): string {
467
- try {
468
- return realpathSync.native(resolve(filePath));
469
- } catch {
470
- return resolve(filePath);
471
- }
472
- }
473
-
474
- function formatBundlingErrors(filePath: string, errors: string[]): string {
475
- return `Failed to bundle ${filePath}: ${errors.join('; ')}`;
476
- }
477
-
478
- function formatUnresolvedPackagedFiles(
479
- filePath: string,
480
- unresolvedFileReferences: BundledPlayFileSuccess['unresolvedFileReferences'],
481
- ): string {
482
- const details = unresolvedFileReferences
483
- .map((unresolved) => `${unresolved.sourceFragment}: ${unresolved.message}`)
484
- .join('; ');
485
- return `Failed to package local ctx.csv(...) files in ${filePath}: ${details}`;
486
- }
487
-
488
- async function collectBundledPlayGraph(entryFile: string): Promise<{
489
- root: BundledPlayFileSuccess;
490
- nodes: Map<string, BundledPlayFileSuccess>;
491
- }> {
492
- const nodes = new Map<string, BundledPlayFileSuccess>();
493
- const visiting = new Set<string>();
494
-
495
- const visit = async (filePath: string): Promise<BundledPlayFileSuccess> => {
496
- const absolutePath = normalizePlayPath(filePath);
497
- const cached = nodes.get(absolutePath);
498
- if (cached) {
499
- return cached;
500
- }
501
- if (visiting.has(absolutePath)) {
502
- throw new Error(
503
- `Recursive imported play graph detected while bundling ${absolutePath}. ` +
504
- 'Break the import cycle and compose plays with ctx.runPlay(...) instead.',
505
- );
506
- }
507
-
508
- visiting.add(absolutePath);
509
- try {
510
- // Bundle target is hardcoded to esm_workers — workers_edge is the
511
- // server-side default execution profile and the only deployable
512
- // runtime. The legacy cjs_node20 target remains in the bundler for
513
- // benchmarks that pass `--profile legacy`, but the CLI does not
514
- // auto-select it.
515
- const bundleResult = await bundlePlayFile(absolutePath, {
516
- target: 'esm_workers',
517
- });
518
- if (bundleResult.success === false) {
519
- throw new Error(
520
- formatBundlingErrors(absolutePath, bundleResult.errors),
521
- );
522
- }
523
- if (bundleResult.unresolvedFileReferences.length > 0) {
524
- throw new Error(
525
- formatUnresolvedPackagedFiles(
526
- absolutePath,
527
- bundleResult.unresolvedFileReferences,
528
- ),
529
- );
530
- }
531
-
532
- nodes.set(absolutePath, bundleResult);
533
- for (const dependency of bundleResult.importedPlayDependencies) {
534
- await visit(dependency.filePath);
535
- }
536
- return bundleResult;
537
- } finally {
538
- visiting.delete(absolutePath);
539
- }
540
- };
541
-
542
- const root = await visit(entryFile);
543
- return { root, nodes };
544
- }
545
-
546
- async function compileBundledPlayGraphManifests(
547
- client: DeeplineClient,
548
- graph: {
549
- root: BundledPlayFileSuccess;
550
- nodes: Map<string, BundledPlayFileSuccess>;
551
- },
552
- ): Promise<void> {
553
- const compiling = new Map<string, Promise<void>>();
554
- const compileNode = (node: BundledPlayFileSuccess): Promise<void> => {
555
- const existing = compiling.get(node.filePath);
556
- if (existing) return existing;
557
-
558
- const promise = (async () => {
559
- for (const dependency of node.importedPlayDependencies) {
560
- const child = graph.nodes.get(normalizePlayPath(dependency.filePath));
561
- if (!child) {
562
- throw new Error(
563
- `Missing bundled play graph node for imported dependency ${dependency.filePath}.`,
564
- );
565
- }
566
- await compileNode(child);
567
- }
568
-
569
- const name =
570
- node.playName ?? extractPlayName(node.sourceCode, node.filePath);
571
- node.compilerManifest = await client.compilePlayManifest({
572
- name,
573
- sourceCode: node.sourceCode,
574
- artifact: node.artifact,
575
- importedPlayDependencies: node.importedPlayDependencies.map(
576
- (dependency) => {
577
- const child = graph.nodes.get(
578
- normalizePlayPath(dependency.filePath),
579
- );
580
- if (!child?.compilerManifest) {
581
- throw new Error(
582
- `Missing compiler manifest for imported dependency ${dependency.filePath}.`,
583
- );
584
- }
585
- return child.compilerManifest;
586
- },
587
- ),
588
- });
589
- })();
590
-
591
- compiling.set(node.filePath, promise);
592
- return promise;
593
- };
594
-
595
- await compileNode(graph.root);
596
- }
597
-
598
- function requireCompilerManifest(node: BundledPlayFileSuccess) {
599
- if (!node.compilerManifest) {
600
- throw new Error(`Missing compiler manifest for ${node.filePath}.`);
601
- }
602
- return node.compilerManifest;
603
- }
604
-
605
- async function publishImportedPlayDependencies(
606
- client: DeeplineClient,
607
- graph: {
608
- root: BundledPlayFileSuccess;
609
- nodes: Map<string, BundledPlayFileSuccess>;
610
- },
611
- ): Promise<void> {
612
- const published = new Set<string>();
613
-
614
- const publishNode = async (
615
- filePath: string,
616
- skipPublish: boolean,
617
- ): Promise<void> => {
618
- const absolutePath = normalizePlayPath(filePath);
619
- const node = graph.nodes.get(absolutePath);
620
- if (!node) {
621
- throw new Error(`Missing bundled play graph node for ${absolutePath}.`);
622
- }
623
-
624
- for (const dependency of node.importedPlayDependencies) {
625
- await publishNode(dependency.filePath, false);
626
- }
627
-
628
- if (skipPublish || published.has(absolutePath)) {
629
- return;
630
- }
631
-
632
- if (!node.playName) {
633
- throw new Error(
634
- `Imported play ${absolutePath} must export definePlay(...) so it can be published for runtime composition.`,
635
- );
636
- }
637
-
638
- await client.registerPlayArtifact({
639
- name: node.playName,
640
- sourceCode: node.sourceCode,
641
- artifact: node.artifact,
642
- compilerManifest: requireCompilerManifest(node),
643
- publish: true,
644
- });
645
- published.add(absolutePath);
646
- };
647
-
648
- await publishNode(graph.root.filePath, true);
649
- }
650
-
651
- function formatTimestamp(value: string | number | null | undefined): string {
652
- if (!value) return '—';
653
- const date = typeof value === 'number' ? new Date(value) : new Date(value);
654
- if (Number.isNaN(date.getTime())) return '—';
655
- return date.toISOString();
656
- }
657
-
658
- function formatRunLine(run: PlayRunListItem): string {
659
- return `${run.workflowId} ${run.status} ${formatTimestamp(run.startTime)}`;
660
- }
661
-
662
- type PlayRunTarget =
663
- | { kind: 'run'; runId: string }
664
- | { kind: 'name'; name: string };
665
-
666
- function parsePlayRunTarget(input: {
667
- args: string[];
668
- usage: string;
669
- allowName: boolean;
670
- }): PlayRunTarget {
671
- const { args, usage } = input;
672
- let runId: string | null = null;
673
- let playName: string | null = null;
674
-
675
- for (let index = 0; index < args.length; index += 1) {
676
- const arg = args[index]!;
677
- if (arg === '--json') {
678
- continue;
679
- }
680
- if (
681
- arg === '--reason' ||
682
- arg === '--interval-ms' ||
683
- arg === '--poll-interval-ms'
684
- ) {
685
- index += 1;
686
- continue;
687
- }
688
- if (arg === '--run-id' && args[index + 1]) {
689
- runId = args[++index]!.trim();
690
- continue;
691
- }
692
- if (arg === '--name' && args[index + 1] && input.allowName) {
693
- playName = parseReferencedPlayTarget(args[++index]!).playName;
694
- continue;
695
- }
696
- if (arg.startsWith('--')) {
697
- continue;
698
- }
699
- throw new DeeplineError(
700
- `Unexpected positional target "${arg}". Use --run-id for run ids.\n${usage}`,
701
- );
702
- }
703
-
704
- const explicitTargets = [runId, playName].filter(Boolean).length;
705
- if (explicitTargets > 1) {
706
- throw new DeeplineError(`Choose exactly one play run target.\n${usage}`);
707
- }
708
-
709
- if (runId) {
710
- return { kind: 'run', runId };
711
- }
712
- if (playName) {
713
- return { kind: 'name', name: playName };
714
- }
715
- throw new DeeplineError(usage);
716
- }
717
-
718
- async function resolvePlayRunId(
719
- client: DeeplineClient,
720
- target: PlayRunTarget,
721
- ): Promise<string> {
722
- if (target.kind === 'run') {
723
- try {
724
- const status = await client.getPlayStatus(target.runId);
725
- return status.runId;
726
- } catch (error) {
727
- if (!(error instanceof DeeplineError) || error.statusCode !== 404) {
728
- throw error;
729
- }
730
- throw new DeeplineError(`No play run found for run id: ${target.runId}`);
731
- }
732
- }
733
-
734
- const runs = await client.listPlayRuns(target.name);
735
- const workflowId = runs[0]?.workflowId ?? '';
736
- if (!workflowId) {
737
- throw new DeeplineError(`No runs found for play: ${target.name}`);
738
- }
739
- return workflowId;
740
- }
741
-
742
- function isTransientPlayStatusPollError(error: unknown): boolean {
743
- if (error instanceof DeeplineError && typeof error.statusCode === 'number') {
744
- // Server-shaped errors with a definite status code are NOT transient by
745
- // pattern — only network-level failures are. 5xx counts as transient
746
- // since the server may recover, but 4xx (especially 404 = run gone) is
747
- // terminal and should not be hidden behind a silent retry loop.
748
- return error.statusCode >= 500 && error.statusCode < 600;
749
- }
750
- const text = error instanceof Error ? error.message : String(error);
751
- return /auth validation backend timed out|fetch failed|eaddrnotavail|econnreset|etimedout|eai_again|socket hang up/i.test(
752
- text,
753
- );
754
- }
755
-
756
- function isTerminalPlayStatusPollError(input: {
757
- error: unknown;
758
- hasSeenRun: boolean;
759
- }): boolean {
760
- // A 404 *after* we've already seen the run means the run was deleted or
761
- // the backend lost it — never recover, fail loud. A 404 *before* we've
762
- // seen the run is the persistence race (submit accepted but the Convex
763
- // record hasn't been written yet); treat that as transient.
764
- if (
765
- input.error instanceof DeeplineError &&
766
- input.error.statusCode === 404 &&
767
- input.hasSeenRun
768
- ) {
769
- return true;
770
- }
771
- return false;
772
- }
773
-
774
- const TERMINAL_PLAY_STATUSES = new Set<PlayStatus['status']>([
775
- 'completed',
776
- 'failed',
777
- 'cancelled',
778
- ]);
779
-
780
- type PlayTailPrintState = {
781
- lastLogIndex: number;
782
- emittedRunnerStarted: boolean;
783
- };
784
-
785
- function getEventPayload(event: PlayLiveEvent): Record<string, unknown> {
786
- return event.payload && typeof event.payload === 'object'
787
- ? (event.payload as Record<string, unknown>)
788
- : {};
789
- }
790
-
791
- function getStatusFromLiveEvent(
792
- event: PlayLiveEvent,
793
- ): PlayStatus['status'] | null {
794
- if (event.type !== 'play.run.status' && event.type !== 'play.run.snapshot') {
795
- return null;
796
- }
797
- const status = getEventPayload(event).status;
798
- return status === 'queued' ||
799
- status === 'running' ||
800
- status === 'waiting' ||
801
- status === 'completed' ||
802
- status === 'failed' ||
803
- status === 'cancelled'
804
- ? status
805
- : null;
806
- }
807
-
808
- function getFinalStatusFromLiveEvent(event: PlayLiveEvent): PlayStatus | null {
809
- if (event.type !== 'play.run.final_status') {
810
- return null;
811
- }
812
- const payload = getEventPayload(event);
813
- const status = payload.status;
814
- if (status !== 'completed' && status !== 'failed' && status !== 'cancelled') {
815
- return null;
816
- }
817
- return {
818
- ...(payload as unknown as Omit<PlayStatus, 'runId' | 'status'>),
819
- runId: typeof payload.runId === 'string' ? payload.runId : '',
820
- status,
821
- };
822
- }
823
-
824
- function getLogLinesFromLiveEvent(event: PlayLiveEvent): string[] {
825
- if (event.type !== 'play.run.log') {
826
- return [];
827
- }
828
- const lines = getEventPayload(event).lines;
829
- return Array.isArray(lines)
830
- ? lines.filter((line): line is string => typeof line === 'string')
831
- : [];
832
- }
833
-
834
- function describeLiveEventPhase(event: PlayLiveEvent): string | null {
835
- const payload = getEventPayload(event);
836
- if (event.type === 'play.run.status') {
837
- const status = getStatusFromLiveEvent(event);
838
- if (status === 'running') {
839
- return null;
840
- }
841
- const runId =
842
- typeof payload.runId === 'string' && payload.runId
843
- ? ` ${payload.runId}`
844
- : '';
845
- return status ? `${status}${runId}` : null;
846
- }
847
- if (
848
- event.type === 'play.step.status' ||
849
- event.type === 'play.step.progress'
850
- ) {
851
- const label =
852
- typeof payload.label === 'string' && payload.label.trim()
853
- ? payload.label.trim()
854
- : typeof payload.stepId === 'string' && payload.stepId.trim()
855
- ? payload.stepId.trim()
856
- : 'step';
857
- const completed =
858
- typeof payload.completed === 'number' ? payload.completed : null;
859
- const total = typeof payload.total === 'number' ? payload.total : null;
860
- const progress =
861
- completed !== null && total !== null ? ` ${completed}/${total}` : '';
862
- return `step ${label}${progress}`;
863
- }
864
- if (
865
- event.type === 'play.sheet.summary' ||
866
- event.type === 'play.sheet.delta'
867
- ) {
868
- const table =
869
- typeof payload.tableNamespace === 'string' ? payload.tableNamespace : '';
870
- return table ? `updating table ${table}` : 'updating table';
871
- }
872
- return null;
873
- }
874
-
875
- function buildPlayDashboardUrl(baseUrl: string, playName: string): string {
876
- const trimmedBase = baseUrl.replace(/\/$/, '');
877
- const encodedPlayName = encodeURIComponent(playName);
878
- return `${trimmedBase}/dashboard/plays/${encodedPlayName}`;
879
- }
880
-
881
- function getDashboardUrlFromLiveEvent(event: PlayLiveEvent): string | null {
882
- const dashboardUrl = getEventPayload(event).dashboardUrl;
883
- return typeof dashboardUrl === 'string' && dashboardUrl.trim()
884
- ? dashboardUrl.trim()
885
- : null;
886
- }
887
-
888
- function printPlayLogLines(input: {
889
- lines: string[];
890
- status: PlayStatus | null;
891
- jsonOutput: boolean;
892
- emitLogs: boolean;
893
- state: PlayTailPrintState;
894
- progress: CliProgress;
895
- }) {
896
- for (const line of input.lines) {
897
- if (input.emitLogs) {
898
- const formatted = formatPlayLogLine(
899
- line,
900
- input.status ?? undefined,
901
- input.state,
902
- );
903
- if (formatted) {
904
- input.progress.writeLogLine(formatted);
905
- }
906
- }
907
- input.state.lastLogIndex += 1;
908
- }
909
- }
910
-
911
- function assertPlayWaitNotTimedOut(input: {
912
- workflowId: string;
913
- startedAt: number;
914
- waitTimeoutMs: number | null;
915
- lastPhase?: string | null;
916
- }) {
917
- if (
918
- input.waitTimeoutMs !== null &&
919
- Date.now() - input.startedAt >= input.waitTimeoutMs
920
- ) {
921
- const hasRealRunId =
922
- input.workflowId.length > 0 && input.workflowId !== 'pending';
923
- const phaseSuffix =
924
- input.lastPhase && input.lastPhase.trim()
925
- ? ` (last observed phase: ${input.lastPhase.trim()})`
926
- : '';
927
- const tailHint = hasRealRunId
928
- ? ` Run 'deepline play tail --run-id ${input.workflowId} --json' to inspect it, or rerun with a larger --tail-timeout-ms.`
929
- : ` The run never reported a workflow id — the start request likely failed before reaching the scheduler. Check server logs and rerun with a larger --tail-timeout-ms.`;
930
- throw new DeeplineError(
931
- `Timed out waiting for play ${hasRealRunId ? input.workflowId : '<no run id>'} after ${Math.ceil(input.waitTimeoutMs / 1000)}s${phaseSuffix}.${tailHint}`,
932
- undefined,
933
- 'PLAY_WAIT_TIMEOUT',
934
- {
935
- ...(hasRealRunId
936
- ? { runId: input.workflowId, workflowId: input.workflowId }
937
- : {}),
938
- ...(input.lastPhase ? { phase: input.lastPhase } : {}),
939
- timeoutMs: input.waitTimeoutMs,
940
- },
941
- );
942
- }
943
- }
944
-
945
- async function waitForPlayCompletionByStream(input: {
946
- client: DeeplineClient;
947
- workflowId: string;
948
- jsonOutput: boolean;
949
- emitLogs: boolean;
950
- waitTimeoutMs: number | null;
951
- startedAt: number;
952
- state: PlayTailPrintState;
953
- progress: CliProgress;
954
- }): Promise<PlayStatus> {
955
- const controller = new AbortController();
956
- let timedOut = false;
957
- let lastPhase: string | null = null;
958
- const timeout =
959
- input.waitTimeoutMs === null
960
- ? null
961
- : setTimeout(
962
- () => {
963
- timedOut = true;
964
- controller.abort();
965
- },
966
- Math.max(1, input.waitTimeoutMs - (Date.now() - input.startedAt)),
967
- );
968
-
969
- try {
970
- for await (const event of input.client.streamPlayRunEvents(
971
- input.workflowId,
972
- { signal: controller.signal },
973
- )) {
974
- assertPlayWaitNotTimedOut({ ...input, lastPhase });
975
- const phase = describeLiveEventPhase(event);
976
- if (phase) {
977
- lastPhase = phase;
978
- input.progress.phase(phase);
979
- }
980
- printPlayLogLines({
981
- lines: getLogLinesFromLiveEvent(event),
982
- status: null,
983
- jsonOutput: input.jsonOutput,
984
- emitLogs: input.emitLogs,
985
- state: input.state,
986
- progress: input.progress,
987
- });
988
-
989
- const status = getStatusFromLiveEvent(event);
990
- if (status && TERMINAL_PLAY_STATUSES.has(status)) {
991
- const finalStatus = await input.client.getPlayStatus(input.workflowId);
992
- if (TERMINAL_PLAY_STATUSES.has(finalStatus.status)) {
993
- return finalStatus;
994
- }
995
- }
996
- }
997
- } catch (error) {
998
- if (timedOut) {
999
- assertPlayWaitNotTimedOut({ ...input, lastPhase });
1000
- }
1001
- throw error;
1002
- } finally {
1003
- if (timeout) {
1004
- clearTimeout(timeout);
1005
- }
1006
- }
1007
-
1008
- const phaseSuffix =
1009
- lastPhase && lastPhase.trim()
1010
- ? ` (last observed phase: ${lastPhase.trim()})`
1011
- : '';
1012
- throw new DeeplineError(
1013
- `Play live stream ended before the run reached a terminal state runId=${input.workflowId}${phaseSuffix}.`,
1014
- undefined,
1015
- 'PLAY_LIVE_STREAM_ENDED',
1016
- {
1017
- runId: input.workflowId,
1018
- workflowId: input.workflowId,
1019
- ...(lastPhase ? { phase: lastPhase } : {}),
1020
- },
1021
- );
1022
- }
1023
-
1024
- async function startAndWaitForPlayCompletionByStream(input: {
1025
- client: DeeplineClient;
1026
- request: Parameters<DeeplineClient['startPlayRun']>[0];
1027
- playName: string;
1028
- jsonOutput: boolean;
1029
- emitLogs: boolean;
1030
- waitTimeoutMs: number | null;
1031
- progress: CliProgress;
1032
- }): Promise<PlayStatus> {
1033
- const startedAt = Date.now();
1034
- const state: PlayTailPrintState = {
1035
- lastLogIndex: 0,
1036
- emittedRunnerStarted: false,
1037
- };
1038
- const controller = new AbortController();
1039
- let timedOut = false;
1040
- let emittedDashboardUrl = false;
1041
- let lastKnownWorkflowId = '';
1042
- let lastPhase: string | null = null;
1043
- const timeout =
1044
- input.waitTimeoutMs === null
1045
- ? null
1046
- : setTimeout(
1047
- () => {
1048
- timedOut = true;
1049
- controller.abort();
1050
- },
1051
- Math.max(1, input.waitTimeoutMs),
1052
- );
1053
-
1054
- try {
1055
- for await (const event of input.client.startPlayRunStream(input.request, {
1056
- signal: controller.signal,
1057
- })) {
1058
- const eventRunId = getEventPayload(event).runId;
1059
- if (
1060
- typeof eventRunId === 'string' &&
1061
- eventRunId &&
1062
- eventRunId !== 'pending'
1063
- ) {
1064
- lastKnownWorkflowId = eventRunId;
1065
- }
1066
- const workflowId = lastKnownWorkflowId || 'pending';
1067
- if (workflowId !== 'pending' && !emittedDashboardUrl) {
1068
- const dashboardUrl =
1069
- getDashboardUrlFromLiveEvent(event) ??
1070
- buildPlayDashboardUrl(input.client.baseUrl, input.playName);
1071
- if (!input.jsonOutput) {
1072
- writeStartedPlayRun({
1073
- runId: workflowId,
1074
- playName: input.playName,
1075
- dashboardUrl,
1076
- jsonOutput: false,
1077
- progress: input.progress,
1078
- });
1079
- }
1080
- input.progress.phase(`loading play on ${dashboardUrl}`);
1081
- emittedDashboardUrl = true;
1082
- }
1083
- assertPlayWaitNotTimedOut({
1084
- workflowId,
1085
- startedAt,
1086
- waitTimeoutMs: input.waitTimeoutMs,
1087
- lastPhase,
1088
- });
1089
- const phase = describeLiveEventPhase(event);
1090
- if (phase) {
1091
- lastPhase = phase;
1092
- input.progress.phase(phase);
1093
- }
1094
- printPlayLogLines({
1095
- lines: getLogLinesFromLiveEvent(event),
1096
- status: null,
1097
- jsonOutput: input.jsonOutput,
1098
- emitLogs: input.emitLogs,
1099
- state,
1100
- progress: input.progress,
1101
- });
1102
-
1103
- const finalStatus = getFinalStatusFromLiveEvent(event);
1104
- if (finalStatus) {
1105
- return finalStatus;
1106
- }
1107
- }
1108
- } catch (error) {
1109
- if (timedOut) {
1110
- assertPlayWaitNotTimedOut({
1111
- workflowId: lastKnownWorkflowId,
1112
- startedAt,
1113
- waitTimeoutMs: input.waitTimeoutMs,
1114
- lastPhase,
1115
- });
1116
- }
1117
- if (lastKnownWorkflowId && isTransientPlayStatusPollError(error)) {
1118
- if (timeout) {
1119
- clearTimeout(timeout);
1120
- }
1121
- const reason = error instanceof Error ? error.message : String(error);
1122
- process.stderr.write(
1123
- `[play watch] start stream failed after run ${lastKnownWorkflowId}; falling back to polling (${reason})\n`,
1124
- );
1125
- return waitForPlayCompletionByPolling({
1126
- client: input.client,
1127
- workflowId: lastKnownWorkflowId,
1128
- pollIntervalMs: 500,
1129
- jsonOutput: input.jsonOutput,
1130
- emitLogs: input.emitLogs,
1131
- waitTimeoutMs: input.waitTimeoutMs,
1132
- startedAt,
1133
- state,
1134
- progress: input.progress,
1135
- });
1136
- }
1137
- throw error;
1138
- } finally {
1139
- if (timeout) {
1140
- clearTimeout(timeout);
1141
- }
1142
- }
1143
-
1144
- const phaseSuffix =
1145
- lastPhase && lastPhase.trim()
1146
- ? ` (last observed phase: ${lastPhase.trim()})`
1147
- : '';
1148
- const idSuffix = lastKnownWorkflowId ? ` runId=${lastKnownWorkflowId}` : '';
1149
- throw new DeeplineError(
1150
- `Play start stream ended before the run reached a terminal state${idSuffix}${phaseSuffix}.`,
1151
- undefined,
1152
- 'PLAY_START_STREAM_ENDED',
1153
- {
1154
- ...(lastKnownWorkflowId
1155
- ? { runId: lastKnownWorkflowId, workflowId: lastKnownWorkflowId }
1156
- : {}),
1157
- ...(lastPhase ? { phase: lastPhase } : {}),
1158
- },
1159
- );
1160
- }
1161
-
1162
- async function waitForPlayCompletionByPolling(input: {
1163
- client: DeeplineClient;
1164
- workflowId: string;
1165
- pollIntervalMs: number;
1166
- jsonOutput: boolean;
1167
- emitLogs: boolean;
1168
- waitTimeoutMs: number | null;
1169
- startedAt: number;
1170
- state: PlayTailPrintState;
1171
- progress: CliProgress;
1172
- }): Promise<PlayStatus> {
1173
- let lastTransientPollWarningAt = 0;
1174
- let hasSeenRun = false;
1175
-
1176
- while (true) {
1177
- assertPlayWaitNotTimedOut(input);
1178
-
1179
- let status: PlayStatus;
1180
- try {
1181
- status = await input.client.getPlayTailStatus(input.workflowId, {
1182
- afterLogIndex: input.state.lastLogIndex,
1183
- // Keep the server-side tail wait close to the caller's requested poll
1184
- // cadence. A long wait makes tiny remote runs look slow whenever the
1185
- // terminal update lands just after the held request starts.
1186
- waitMs: Math.max(50, Math.min(input.pollIntervalMs, 1_000)),
1187
- });
1188
- } catch (error) {
1189
- if (isTerminalPlayStatusPollError({ error, hasSeenRun })) {
1190
- throw new DeeplineError(
1191
- `Play run ${input.workflowId} no longer exists on the server (404). The run was deleted or the backend lost it.`,
1192
- 404,
1193
- 'PLAY_RUN_NOT_FOUND',
1194
- );
1195
- }
1196
- if (!isTransientPlayStatusPollError(error)) {
1197
- throw error;
1198
- }
1199
- const now = Date.now();
1200
- if (now - lastTransientPollWarningAt >= 30_000) {
1201
- const message = error instanceof Error ? error.message : String(error);
1202
- input.progress.writeLine(
1203
- `[play tail] transient status poll failed; retrying: ${message}`,
1204
- );
1205
- lastTransientPollWarningAt = now;
1206
- }
1207
- await new Promise((resolvePromise) =>
1208
- setTimeout(resolvePromise, input.pollIntervalMs),
1209
- );
1210
- continue;
1211
- }
1212
- hasSeenRun = true;
1213
- const logs = status.progress?.logs ?? [];
1214
- input.progress.phase(status.status);
1215
- printPlayLogLines({
1216
- lines: logs.slice(input.state.lastLogIndex),
1217
- status,
1218
- jsonOutput: input.jsonOutput,
1219
- emitLogs: input.emitLogs,
1220
- state: input.state,
1221
- progress: input.progress,
1222
- });
1223
-
1224
- if (TERMINAL_PLAY_STATUSES.has(status.status)) {
1225
- return status.result !== undefined
1226
- ? status
1227
- : await input.client.getPlayStatus(input.workflowId);
1228
- }
1229
-
1230
- const authoritativeStatus = await input.client.getPlayStatus(
1231
- input.workflowId,
1232
- );
1233
- if (TERMINAL_PLAY_STATUSES.has(authoritativeStatus.status)) {
1234
- return authoritativeStatus;
1235
- }
1236
-
1237
- if ((status.progress?.logs ?? []).length === input.state.lastLogIndex) {
1238
- await new Promise((resolvePromise) =>
1239
- setTimeout(resolvePromise, input.pollIntervalMs),
1240
- );
1241
- }
1242
- }
1243
- }
1244
-
1245
- async function waitForPlayCompletion(input: {
1246
- client: DeeplineClient;
1247
- workflowId: string;
1248
- pollIntervalMs: number;
1249
- jsonOutput: boolean;
1250
- emitLogs: boolean;
1251
- waitTimeoutMs: number | null;
1252
- progress: CliProgress;
1253
- }): Promise<PlayStatus> {
1254
- const startedAt = Date.now();
1255
- const state: PlayTailPrintState = {
1256
- lastLogIndex: 0,
1257
- emittedRunnerStarted: false,
1258
- };
1259
- try {
1260
- return await waitForPlayCompletionByStream({
1261
- ...input,
1262
- startedAt,
1263
- state,
1264
- progress: input.progress,
1265
- });
1266
- } catch (error) {
1267
- assertPlayWaitNotTimedOut({
1268
- workflowId: input.workflowId,
1269
- startedAt,
1270
- waitTimeoutMs: input.waitTimeoutMs,
1271
- });
1272
- // Loud one-line note so the user can tell SSE failed and we are now
1273
- // polling. Repeated SSE failures should be diagnosed, not hidden.
1274
- const reason = error instanceof Error ? error.message : String(error);
1275
- process.stderr.write(
1276
- `[play watch] SSE stream failed; falling back to polling (${reason})\n`,
1277
- );
1278
- return waitForPlayCompletionByPolling({
1279
- ...input,
1280
- startedAt,
1281
- state,
1282
- progress: input.progress,
1283
- });
1284
- }
1285
- }
1286
-
1287
- function formatInteger(value: unknown): string {
1288
- return typeof value === 'number' && Number.isFinite(value)
1289
- ? value.toLocaleString('en-US')
1290
- : String(value ?? '-');
1291
- }
1292
-
1293
- function formatPercent(numerator: number, denominator: number): string {
1294
- if (denominator <= 0) {
1295
- return '-';
1296
- }
1297
- const percent = (numerator / denominator) * 100;
1298
- return `${percent.toFixed(percent >= 10 ? 0 : 1)}%`;
1299
- }
1300
-
1301
- function isFilledValue(value: unknown): boolean {
1302
- if (value === null || value === undefined) {
1303
- return false;
1304
- }
1305
- if (typeof value === 'string') {
1306
- return value.trim().length > 0;
1307
- }
1308
- if (Array.isArray(value)) {
1309
- return value.length > 0;
1310
- }
1311
- if (typeof value === 'object') {
1312
- return Object.keys(value).length > 0;
1313
- }
1314
- return true;
1315
- }
1316
-
1317
- const BULKY_RETURN_KEYS = new Set([
1318
- 'contract',
1319
- 'staticPipeline',
1320
- 'batchingTrace',
1321
- 'sequenceTrace',
1322
- 'logs',
1323
- ]);
1324
-
1325
- function formatPlayLogLine(
1326
- line: string,
1327
- status: PlayStatus | undefined,
1328
- state: PlayTailPrintState,
1329
- ): string | null {
1330
- const timestampMatch = line.match(/^\[([^\]]+)\]\s*(.*)$/);
1331
- const parsedTimestamp = timestampMatch?.[1]
1332
- ? new Date(timestampMatch[1])
1333
- : null;
1334
- const timestamp =
1335
- parsedTimestamp && !Number.isNaN(parsedTimestamp.getTime())
1336
- ? parsedTimestamp.toLocaleTimeString('en-US', {
1337
- hour12: false,
1338
- hour: '2-digit',
1339
- minute: '2-digit',
1340
- second: '2-digit',
1341
- })
1342
- : null;
1343
- const message = timestampMatch?.[2] ?? line;
1344
- const prefix = timestamp ? `${timestamp} ` : '';
1345
-
1346
- if (/\[worker\] picked up run\b/.test(message)) {
1347
- if (state.emittedRunnerStarted) {
1348
- return null;
1349
- }
1350
- state.emittedRunnerStarted = true;
1351
- return `${prefix}runner started`;
1352
- }
1353
-
1354
- if (
1355
- /\[worker\] step started\b/.test(message) ||
1356
- /\[worker\] Preparing run files\b/.test(message) ||
1357
- /\[worker\] Run files ready\b/.test(message) ||
1358
- /\[worker\] Runtime ready\b/.test(message) ||
1359
- /\[worker\] Sandbox (?:starting|create start|create done|workspace ready|upload start|runner uploaded)\b/.test(
1360
- message,
1361
- )
1362
- ) {
1363
- return null;
1364
- }
1365
-
1366
- const stages = (
1367
- (
1368
- status?.contract as
1369
- | {
1370
- staticPipeline?: {
1371
- stages?: Array<{
1372
- tableNamespace?: string;
1373
- sourceRange?: { startLine?: number; endLine?: number };
1374
- }>;
1375
- };
1376
- }
1377
- | null
1378
- | undefined
1379
- )?.staticPipeline?.stages ?? []
1380
- ).filter((stage) => stage.tableNamespace);
1381
- const sourceLabelForNamespace = (namespace: string): string => {
1382
- const stage = stages.find((entry) => entry.tableNamespace === namespace);
1383
- const range = stage?.sourceRange;
1384
- return range?.startLine && range?.endLine
1385
- ? `${namespace} lines ${range.startLine}-${range.endLine}`
1386
- : namespace;
1387
- };
1388
-
1389
- const mapStart = message.match(
1390
- /^Starting map over (\d+) items with (\d+) fields \(key: ([^;]+); (\d+) already satisfied; (\d+) pending\)$/,
1391
- );
1392
- if (mapStart) {
1393
- const [, rows, fields, namespace, cached, pending] = mapStart;
1394
- return `${prefix}map ${sourceLabelForNamespace(namespace!)}: ${formatInteger(Number(rows))} rows, ${fields} fields, ${formatInteger(Number(cached))} cached, ${formatInteger(Number(pending))} pending`;
1395
- }
1396
-
1397
- const mapDone = message.match(
1398
- /^Map completed: (\d+) results \((\d+) executed, (\d+) already satisfied\)$/,
1399
- );
1400
- if (mapDone) {
1401
- const [, results, executed, cached] = mapDone;
1402
- return `${prefix}done: ${formatInteger(Number(results))} results, ${formatInteger(Number(executed))} executed, ${formatInteger(Number(cached))} cached`;
1403
- }
1404
-
1405
- return `${prefix}${message}`;
1406
- }
1407
-
1408
- function compactReturnValue(value: unknown, depth = 0): unknown {
1409
- if (depth >= 4) {
1410
- return value && typeof value === 'object' ? '[Object]' : value;
1411
- }
1412
- if (Array.isArray(value)) {
1413
- const compacted = value
1414
- .slice(0, 5)
1415
- .map((entry) => compactReturnValue(entry, depth + 1));
1416
- return value.length > 5
1417
- ? [...compacted, `... ${value.length - 5} more`]
1418
- : compacted;
1419
- }
1420
- if (!value || typeof value !== 'object') {
1421
- return value;
1422
- }
1423
-
1424
- const output: Record<string, unknown> = {};
1425
- for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
1426
- if (depth === 0 && key === '_metadata') {
1427
- continue;
1428
- }
1429
- if (BULKY_RETURN_KEYS.has(key)) {
1430
- continue;
1431
- }
1432
- if (key === 'access') {
1433
- continue;
1434
- }
1435
- output[key] = compactReturnValue(entry, depth + 1);
1436
- }
1437
- return output;
1438
- }
1439
-
1440
- function extractResultRowsInfo(result: Record<string, unknown>): {
1441
- rows: unknown[];
1442
- totalRows: number | null;
1443
- sampled: boolean;
1444
- } {
1445
- const output = result.output as Record<string, unknown> | undefined;
1446
- const candidate =
1447
- (output?.results as unknown[] | undefined) ??
1448
- (output?.rows as unknown[] | undefined) ??
1449
- (result.results as unknown[] | undefined) ??
1450
- (result.rows as unknown[] | undefined) ??
1451
- (result.previewRows as unknown[] | undefined);
1452
- if (Array.isArray(candidate)) {
1453
- return { rows: candidate, totalRows: candidate.length, sampled: false };
1454
- }
1455
-
1456
- const dataset =
1457
- (result.rows &&
1458
- typeof result.rows === 'object' &&
1459
- !Array.isArray(result.rows)
1460
- ? (result.rows as Record<string, unknown>)
1461
- : null) ??
1462
- (output?.rows &&
1463
- typeof output.rows === 'object' &&
1464
- !Array.isArray(output.rows)
1465
- ? (output.rows as Record<string, unknown>)
1466
- : null);
1467
- if (dataset && Array.isArray(dataset.preview)) {
1468
- return {
1469
- rows: dataset.preview,
1470
- totalRows:
1471
- typeof dataset.count === 'number' && Number.isFinite(dataset.count)
1472
- ? dataset.count
1473
- : dataset.preview.length,
1474
- sampled: true,
1475
- };
1476
- }
1477
-
1478
- return { rows: [], totalRows: null, sampled: false };
1479
- }
1480
-
1481
- function formatJsonPreview(value: unknown): string[] {
1482
- const json = JSON.stringify(value, null, 2);
1483
- if (!json || json === '{}') {
1484
- return [];
1485
- }
1486
-
1487
- const MAX_CHARS = 4_000;
1488
- const truncated =
1489
- json.length > MAX_CHARS
1490
- ? `${json.slice(0, MAX_CHARS)}\n... truncated; use --json for full output`
1491
- : json;
1492
- return truncated.split('\n').map((line) => ` ${line}`);
1493
- }
1494
-
1495
- function formatReturnValue(result: Record<string, unknown>): string[] {
1496
- const previewLines = formatJsonPreview(compactReturnValue(result));
1497
- if (previewLines.length === 0) {
1498
- return [];
1499
- }
1500
- const lines = [' return value:'];
1501
- lines.push(...previewLines);
1502
- const hiddenKeys = [...BULKY_RETURN_KEYS].filter(
1503
- (key) => result[key] !== undefined,
1504
- );
1505
- if (hiddenKeys.length > 0) {
1506
- lines.push(` omitted metadata from preview: ${hiddenKeys.join(', ')}`);
1507
- }
1508
- return lines;
1509
- }
1510
-
1511
- function formatTableSummary(result: Record<string, unknown>): string[] {
1512
- const { rows, totalRows, sampled } = extractResultRowsInfo(result);
1513
- const recordRows = rows.filter(
1514
- (row): row is Record<string, unknown> =>
1515
- Boolean(row) && typeof row === 'object' && !Array.isArray(row),
1516
- );
1517
- if (recordRows.length === 0) {
1518
- return [];
1519
- }
1520
-
1521
- const columns = [...new Set(recordRows.flatMap((row) => Object.keys(row)))];
1522
- const displayedRows = recordRows.length;
1523
- const denominator =
1524
- sampled && totalRows !== null ? displayedRows : recordRows.length;
1525
- const lines = [
1526
- ` rows=${formatInteger(totalRows ?? recordRows.length)} columns=${formatInteger(columns.length)}${sampled ? ` previewRows=${formatInteger(displayedRows)}` : ''}`,
1527
- ];
1528
- const fillStats = columns
1529
- .filter((column) => !column.startsWith('_'))
1530
- .map((column) => {
1531
- const filled = recordRows.filter((row) =>
1532
- isFilledValue(row[column]),
1533
- ).length;
1534
- return { column, filled };
1535
- })
1536
- .sort((a, b) => b.filled - a.filled)
1537
- .slice(0, 8);
1538
- if (fillStats.length > 0) {
1539
- lines.push(
1540
- ` fill rates: ${fillStats
1541
- .map(
1542
- (stat) =>
1543
- `${stat.column}=${formatInteger(stat.filled)}/${formatInteger(denominator)} (${formatPercent(stat.filled, denominator)})${sampled ? ' sample' : ''}`,
1544
- )
1545
- .join(', ')}`,
1546
- );
1547
- }
1548
- return lines;
1549
- }
1550
-
1551
- function buildOutputSummary(
1552
- rowsInfo: CanonicalRowsInfo | null,
1553
- exportedPath?: string | null,
1554
- ): Record<string, unknown> | null {
1555
- if (!rowsInfo) {
1556
- return exportedPath ? { csv_path: exportedPath } : null;
1557
- }
1558
- return {
1559
- kind: 'rows',
1560
- rowCount: rowsInfo.totalRows,
1561
- previewRowCount: rowsInfo.rows.length,
1562
- complete: rowsInfo.complete,
1563
- columns: rowsInfo.columns,
1564
- source: rowsInfo.source,
1565
- ...(exportedPath ? { csv_path: exportedPath } : {}),
1566
- };
1567
- }
1568
-
1569
- function buildRunWarnings(
1570
- status: PlayStatus,
1571
- rowsInfo: CanonicalRowsInfo | null,
1572
- ): string[] {
1573
- if (status.status === 'completed' && rowsInfo?.totalRows === 0) {
1574
- return ['Run completed with 0 output rows.'];
1575
- }
1576
- return [];
1577
- }
1578
-
1579
- function buildRunNextCommands(runId: string): Record<string, string> {
1580
- return {
1581
- exportCsv: `deepline runs export ${runId} --out output.csv`,
1582
- status: `deepline runs status ${runId} --json`,
1583
- logs: `deepline runs logs ${runId}`,
1584
- };
1585
- }
1586
-
1587
- function isTerminalPlayStatus(status: PlayStatus): boolean {
1588
- return TERMINAL_PLAY_STATUSES.has(status.status);
1589
- }
1590
-
1591
- function compactPlayStatus(
1592
- status: PlayStatus,
1593
- options?: { exportedPath?: string | null },
1594
- ): Record<string, unknown> {
1595
- const rowsInfo = extractCanonicalRowsInfo(status);
1596
- const result =
1597
- status && typeof status === 'object'
1598
- ? (status as unknown as { result?: unknown }).result
1599
- : null;
1600
- const warnings = buildRunWarnings(status, rowsInfo);
1601
- const datasetStats =
1602
- rowsInfo && rowsInfo.complete
1603
- ? buildDatasetStats(rowsInfo.rows, rowsInfo.totalRows, rowsInfo.columns)
1604
- : null;
1605
- const billing =
1606
- status && typeof status === 'object'
1607
- ? (status as unknown as { billing?: unknown }).billing
1608
- : null;
1609
- const progressError = (
1610
- status.progress as unknown as Record<string, unknown> | undefined
1611
- )?.error;
1612
- const error =
1613
- typeof progressError === 'string'
1614
- ? progressError
1615
- : typeof (status as unknown as { error?: unknown }).error === 'string'
1616
- ? String((status as unknown as { error?: unknown }).error)
1617
- : null;
1618
- return {
1619
- runId: status.runId,
1620
- apiVersion: status.apiVersion ?? 1,
1621
- ...(typeof (status as unknown as { name?: unknown }).name === 'string'
1622
- ? { name: (status as unknown as { name: string }).name }
1623
- : {}),
1624
- ...(typeof (status as unknown as { playName?: unknown }).playName ===
1625
- 'string'
1626
- ? { playName: (status as unknown as { playName: string }).playName }
1627
- : {}),
1628
- status: status.status,
1629
- ...(error ? { error } : {}),
1630
- ...(warnings.length > 0 ? { warnings } : {}),
1631
- output:
1632
- buildOutputSummary(rowsInfo, options?.exportedPath) ?? result ?? null,
1633
- ...(result !== undefined ? { result } : {}),
1634
- ...(status.resultView ? { resultView: status.resultView } : {}),
1635
- ...(datasetStats ? { dataset_stats: datasetStats } : {}),
1636
- ...(rowsInfo ? { previewRows: rowsInfo.rows.slice(0, 5) } : {}),
1637
- ...(billing ? { billing } : {}),
1638
- ...(status.run ? { run: status.run } : {}),
1639
- next: buildRunNextCommands(status.runId),
1640
- };
1641
- }
1642
-
1643
- function enrichPlayStatusWithDatasetStats(
1644
- status: PlayStatus,
1645
- ): PlayStatus & { dataset_stats?: DatasetStats } {
1646
- const rowsInfo = extractCanonicalRowsInfo(status);
1647
- if (!rowsInfo?.complete) {
1648
- return status;
1649
- }
1650
- return {
1651
- ...status,
1652
- dataset_stats: buildDatasetStats(
1653
- rowsInfo.rows,
1654
- rowsInfo.totalRows,
1655
- rowsInfo.columns,
1656
- ),
1657
- };
1658
- }
1659
-
1660
- function formatDatasetStatsLines(datasetStats: DatasetStats | null): string[] {
1661
- if (!datasetStats) {
1662
- return [];
1663
- }
1664
- const lines = [' column stats:'];
1665
- for (const [column, stat] of Object.entries(datasetStats.columnStats).slice(
1666
- 0,
1667
- 12,
1668
- )) {
1669
- const topValues = stat.top_values
1670
- ? `, top_values=${Object.entries(stat.top_values)
1671
- .slice(0, 3)
1672
- .map(([value, count]) => `${value}=${count}`)
1673
- .join(', ')}`
1674
- : '';
1675
- const sample =
1676
- stat.sample_value !== undefined
1677
- ? `, sample_value=${JSON.stringify(stat.sample_value)}`
1678
- : '';
1679
- lines.push(
1680
- ` ${column}: non_empty=${stat.non_empty}, unique=${stat.unique}${topValues}${sample}`,
1681
- );
1682
- }
1683
- return lines;
1684
- }
1685
-
1686
- function writePlayResult(
1687
- status: PlayStatus,
1688
- jsonOutput: boolean,
1689
- options?: { exportedPath?: string | null; fullJson?: boolean },
1690
- ): void {
1691
- if (jsonOutput) {
1692
- process.stdout.write(
1693
- `${JSON.stringify(
1694
- options?.fullJson
1695
- ? enrichPlayStatusWithDatasetStats(status)
1696
- : compactPlayStatus(status, options),
1697
- )}\n`,
1698
- );
1699
- return;
1700
- }
1701
-
1702
- const result = status.result as Record<string, unknown> | undefined;
1703
- const publicStatus = status.status ?? 'running';
1704
- const success = publicStatus === 'completed';
1705
- const runId = status.runId ?? 'unknown';
1706
-
1707
- const lines: string[] = [];
1708
- lines.push(`${success ? '✓' : '✗'} ${publicStatus} ${runId}`);
1709
- const rowsInfo = extractCanonicalRowsInfo(status);
1710
- const warnings = buildRunWarnings(status, rowsInfo);
1711
- const datasetStats =
1712
- rowsInfo && rowsInfo.complete
1713
- ? buildDatasetStats(rowsInfo.rows, rowsInfo.totalRows, rowsInfo.columns)
1714
- : null;
1715
- const outputSummary = buildOutputSummary(rowsInfo, options?.exportedPath);
1716
- if (outputSummary) {
1717
- const columns = Array.isArray(outputSummary.columns)
1718
- ? outputSummary.columns.length
1719
- : 0;
1720
- const path =
1721
- typeof outputSummary.csv_path === 'string'
1722
- ? ` file=${outputSummary.csv_path}`
1723
- : '';
1724
- lines.push(
1725
- ` output: rows=${formatInteger(outputSummary.rowCount)} columns=${formatInteger(columns)}${path}`,
1726
- );
1727
- }
1728
- for (const warning of warnings) {
1729
- lines.push(` warning: ${warning}`);
1730
- }
1731
- lines.push(...formatDatasetStatsLines(datasetStats));
1732
-
1733
- const progressError = (
1734
- status.progress as unknown as Record<string, unknown> | undefined
1735
- )?.error;
1736
- if (progressError && typeof progressError === 'string') {
1737
- lines.push(` error: ${progressError.slice(0, 200)}`);
1738
- }
1739
-
1740
- const renderedServerView = renderServerResultView(status.resultView);
1741
- if (result) {
1742
- lines.push(...formatReturnValue(result));
1743
- }
1744
- if (renderedServerView.lines.length > 0) {
1745
- lines.push(...renderedServerView.lines);
1746
- }
1747
- lines.push(...renderedServerView.actions);
1748
-
1749
- console.log(lines.join('\n'));
1750
- }
1751
-
1752
- function exportPlayStatusRows(
1753
- status: PlayStatus,
1754
- outPath: string | null,
1755
- ): string | null {
1756
- if (!outPath) {
1757
- return null;
1758
- }
1759
- const rowsInfo = extractCanonicalRowsInfo(status);
1760
- if (!rowsInfo) {
1761
- throw new DeeplineError(
1762
- `Run ${status.runId} did not expose a row-shaped final output to export.`,
1763
- );
1764
- }
1765
- return writeCanonicalRowsCsv(rowsInfo, outPath);
1766
- }
1767
-
1768
- function renderServerResultView(value: unknown): {
1769
- lines: string[];
1770
- actions: string[];
1771
- } {
1772
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
1773
- return { lines: [], actions: [] };
1774
- }
1775
- const view = value as {
1776
- render?: {
1777
- sections?: Array<{ title?: unknown; lines?: unknown }>;
1778
- actions?: Array<{ label?: unknown; command?: unknown }>;
1779
- };
1780
- scalars?: Array<{ key?: unknown; value?: unknown }>;
1781
- byStep?: Array<{ stepId?: unknown; count?: unknown; rate?: unknown }>;
1782
- tables?: Array<{
1783
- tableNamespace?: unknown;
1784
- rowCount?: unknown;
1785
- sourceLines?: unknown;
1786
- inputFields?: unknown;
1787
- outputFields?: unknown;
1788
- waterfallIds?: unknown;
1789
- columnCounts?: unknown;
1790
- }>;
1791
- datasetAccess?: {
1792
- sqlTableName?: unknown;
1793
- tableNamespace?: unknown;
1794
- sqlSchemaName?: unknown;
1795
- cliCommand?: unknown;
1796
- toolCommand?: unknown;
1797
- api?: unknown;
1798
- note?: unknown;
1799
- } | null;
1800
- };
1801
- const renderedSections = Array.isArray(view.render?.sections)
1802
- ? view.render.sections
1803
- : [];
1804
- const renderedActions = Array.isArray(view.render?.actions)
1805
- ? view.render.actions
1806
- : [];
1807
- if (renderedSections.length > 0 || renderedActions.length > 0) {
1808
- const lines: string[] = [];
1809
- for (const section of renderedSections) {
1810
- if (typeof section.title !== 'string') {
1811
- continue;
1812
- }
1813
- const sectionLines = Array.isArray(section.lines)
1814
- ? section.lines.filter(
1815
- (line): line is string => typeof line === 'string',
1816
- )
1817
- : [];
1818
- if (sectionLines.length === 0) {
1819
- continue;
1820
- }
1821
- lines.push(` ${section.title}:`);
1822
- lines.push(...sectionLines.map((line) => ` ${line}`));
1823
- }
1824
- const actions = renderedActions
1825
- .filter(
1826
- (action): action is { label: string; command: string } =>
1827
- typeof action.label === 'string' &&
1828
- typeof action.command === 'string',
1829
- )
1830
- .map((action) => ` ${action.label}: ${action.command}`);
1831
- return { lines, actions };
1832
- }
1833
-
1834
- const lines = [' execution statistics:'];
1835
- const scalars = Array.isArray(view.scalars) ? view.scalars : [];
1836
- const scalarParts = scalars
1837
- .filter((entry) => typeof entry.key === 'string')
1838
- .map((entry) => `${entry.key}=${formatInteger(entry.value)}`);
1839
- if (scalarParts.length > 0) {
1840
- lines.push(` ${scalarParts.join(' ')}`);
1841
- }
1842
-
1843
- const byStep = Array.isArray(view.byStep) ? view.byStep : [];
1844
- if (byStep.length > 0) {
1845
- lines.push(
1846
- ` byStep=${byStep
1847
- .slice(0, 8)
1848
- .map((entry) => {
1849
- const rate =
1850
- typeof entry.rate === 'number'
1851
- ? ` (${(entry.rate * 100).toFixed(entry.rate >= 0.1 ? 0 : 1)}%)`
1852
- : '';
1853
- return `${String(entry.stepId)}:${formatInteger(entry.count)}${rate}`;
1854
- })
1855
- .join(', ')}`,
1856
- );
1857
- }
1858
-
1859
- const tables = Array.isArray(view.tables) ? view.tables : [];
1860
- if (tables.length > 0) {
1861
- lines.push(' tables:');
1862
- for (const table of tables.slice(0, 6)) {
1863
- const details = [
1864
- Array.isArray(table.inputFields) && table.inputFields.length
1865
- ? `inputs=${table.inputFields.join(',')}`
1866
- : null,
1867
- Array.isArray(table.outputFields) && table.outputFields.length
1868
- ? `outputs=${table.outputFields.join(',')}`
1869
- : null,
1870
- Array.isArray(table.waterfallIds) && table.waterfallIds.length
1871
- ? `waterfalls=${table.waterfallIds.join(',')}`
1872
- : null,
1873
- table.columnCounts &&
1874
- typeof table.columnCounts === 'object' &&
1875
- !Array.isArray(table.columnCounts)
1876
- ? `columns=${Object.entries(
1877
- table.columnCounts as Record<string, unknown>,
1878
- )
1879
- .map(([key, count]) => `${key}:${String(count)}`)
1880
- .join(',')}`
1881
- : null,
1882
- ].filter(Boolean);
1883
- const rowLabel =
1884
- typeof table.rowCount === 'number'
1885
- ? `rows=${formatInteger(table.rowCount)} `
1886
- : '';
1887
- const lineLabel =
1888
- typeof table.sourceLines === 'string'
1889
- ? ` lines ${table.sourceLines}`
1890
- : '';
1891
- lines.push(
1892
- ` ${String(table.tableNamespace ?? 'table')}${lineLabel}: ${rowLabel}${details.join(' ')}`,
1893
- );
1894
- }
1895
- }
1896
-
1897
- return { lines: lines.length > 1 ? lines : [], actions: [] };
1898
- }
1899
-
1900
- function writeStartedPlayRun(input: {
1901
- runId: string;
1902
- playName: string;
1903
- status?: string;
1904
- statusUrl?: string;
1905
- dashboardUrl?: string;
1906
- jsonOutput: boolean;
1907
- progress?: CliProgress;
1908
- }): void {
1909
- const payload = {
1910
- runId: input.runId,
1911
- workflowId: input.runId,
1912
- name: input.playName,
1913
- status: input.status ?? 'started',
1914
- statusUrl: input.statusUrl,
1915
- dashboardUrl: input.dashboardUrl,
1916
- };
1917
-
1918
- if (input.jsonOutput) {
1919
- process.stdout.write(`${JSON.stringify(payload)}\n`);
1920
- return;
1921
- }
1922
-
1923
- const lines = [
1924
- `Started ${input.playName}`,
1925
- ` run id: ${input.runId}`,
1926
- ` check status: deepline play status --run-id ${input.runId}`,
1927
- ` tail logs: deepline play tail --run-id ${input.runId}`,
1928
- ` stop run: deepline play stop --run-id ${input.runId}`,
1929
- ` result JSON: deepline play status --run-id ${input.runId} --json`,
1930
- ];
1931
-
1932
- if (input.dashboardUrl) {
1933
- lines.push(` play page: ${input.dashboardUrl}`);
1934
- }
1935
-
1936
- const output = lines.join('\n');
1937
- if (input.progress) {
1938
- input.progress.writeLine(output, process.stdout);
1939
- return;
1940
- }
1941
- console.log(output);
1942
- }
1943
-
1944
- function parsePlayRunOptions(args: string[]): PlayRunCommandOptions {
1945
- const usage =
1946
- "Usage: deepline plays run <play-name> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n" +
1947
- " deepline plays run <play-file.ts> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n" +
1948
- " deepline plays run --file <play-file.ts> [--input '{...}'] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--<input> value]\n" +
1949
- " deepline plays run --name <name> [--input '{...}'] [--live|--latest|--revision-id <id>] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--json] [--<input> value]";
1950
- let filePath: string | null = null;
1951
- let playName: string | null = null;
1952
- let input: Record<string, unknown> | null = null;
1953
- let revisionId: string | null = null;
1954
- let revisionSelector: 'live' | 'latest' | null = null;
1955
- const watch = args.includes('--watch');
1956
- let jsonOutput = watch ? args.includes('--json') : argsWantJson(args);
1957
- const emitLogs = !jsonOutput || args.includes('--logs');
1958
- const force = args.includes('--force');
1959
- let outPath: string | null = null;
1960
- let pollIntervalMs = 500;
1961
- let waitTimeoutMs: number | null = null;
1962
-
1963
- for (let index = 0; index < args.length; index += 1) {
1964
- const arg = args[index]!;
1965
- if (arg === '--file' && args[index + 1]) {
1966
- filePath = args[++index]!;
1967
- continue;
1968
- }
1969
- if (arg === '--name' && args[index + 1]) {
1970
- playName = parseReferencedPlayTarget(args[++index]!).playName;
1971
- continue;
1972
- }
1973
- if ((arg === '--input' || arg === '-i') && args[index + 1]) {
1974
- input = parseJsonInput(args[++index]!);
1975
- continue;
1976
- }
1977
- if (arg === '--revision-id' && args[index + 1]) {
1978
- revisionId = args[++index]!;
1979
- continue;
1980
- }
1981
- if (arg === '--live') {
1982
- revisionSelector = 'live';
1983
- continue;
1984
- }
1985
- if (arg === '--latest') {
1986
- revisionSelector = 'latest';
1987
- continue;
1988
- }
1989
- if (arg === '--out' && args[index + 1]) {
1990
- outPath = resolve(args[++index]!);
1991
- continue;
1992
- }
1993
- if (
1994
- (arg === '--poll-interval-ms' || arg === '--interval-ms') &&
1995
- args[index + 1]
1996
- ) {
1997
- pollIntervalMs = parsePositiveInteger(args[++index]!, arg);
1998
- continue;
1999
- }
2000
- if (
2001
- (arg === '--tail-timeout-ms' || arg === '--timeout-ms') &&
2002
- args[index + 1]
2003
- ) {
2004
- waitTimeoutMs = parsePositiveInteger(args[++index]!, arg);
2005
- continue;
2006
- }
2007
- if (
2008
- arg === '--json' ||
2009
- arg === '--wait' ||
2010
- arg === '--tail' ||
2011
- arg === '--watch' ||
2012
- arg === '--logs' ||
2013
- arg === '--force'
2014
- ) {
2015
- if (arg === '--watch') {
2016
- continue;
2017
- }
2018
- if (arg === '--json') {
2019
- jsonOutput = true;
2020
- continue;
2021
- }
2022
- if (arg === '--wait') {
2023
- throw new Error(
2024
- '--wait is removed for `plays run`; use `--watch` to stream completion output.',
2025
- );
2026
- }
2027
- if (arg === '--tail') {
2028
- throw new Error(
2029
- '--tail is removed for `plays run`; use `--watch` to stream completion output.',
2030
- );
2031
- }
2032
- continue;
2033
- }
2034
- if (arg.startsWith('--')) {
2035
- const { path, value } = parseInputFieldFlag(arg, args[index + 1]);
2036
- input ??= {};
2037
- setDottedInputValue(input, path, parseInputFlagValue(value));
2038
- if (!arg.includes('=')) {
2039
- index += 1;
2040
- }
2041
- continue;
2042
- }
2043
- if (!arg.startsWith('--') && !filePath && !playName) {
2044
- if (isFileTarget(arg) || looksLikeFilePath(arg)) {
2045
- filePath = arg;
2046
- } else {
2047
- playName = parseReferencedPlayTarget(arg).playName;
2048
- }
2049
- continue;
2050
- }
2051
- }
2052
-
2053
- if ((filePath && playName) || (!filePath && !playName)) {
2054
- throw new Error(usage);
2055
- }
2056
- const explicitRevisionSelectors = [
2057
- revisionId ? '--revision-id' : null,
2058
- revisionSelector === 'live' ? '--live' : null,
2059
- revisionSelector === 'latest' ? '--latest' : null,
2060
- ].filter(Boolean);
2061
- if (explicitRevisionSelectors.length > 1) {
2062
- throw new Error(
2063
- `Choose only one revision selector: ${explicitRevisionSelectors.join(', ')}.`,
2064
- );
2065
- }
2066
- if (filePath && explicitRevisionSelectors.length > 0) {
2067
- throw new Error(
2068
- '--live, --latest, and --revision-id only apply to named plays.',
2069
- );
2070
- }
2071
- if (outPath && !watch) {
2072
- throw new Error(
2073
- '--out requires --watch so the CLI can export the completed run output.',
2074
- );
2075
- }
2076
-
2077
- return {
2078
- target: filePath
2079
- ? { kind: 'file', path: filePath }
2080
- : { kind: 'name', name: playName! },
2081
- input,
2082
- revisionId,
2083
- revisionSelector,
2084
- watch,
2085
- emitLogs,
2086
- jsonOutput,
2087
- pollIntervalMs,
2088
- waitTimeoutMs,
2089
- force,
2090
- outPath,
2091
- };
2092
- }
2093
-
2094
- function parsePlayCheckOptions(args: string[]): PlayCheckCommandOptions {
2095
- const target = args[0];
2096
- if (!target) {
2097
- throw new Error('Usage: deepline play check <play-file.ts> [--json]');
2098
- }
2099
-
2100
- const jsonOutput = argsWantJson(args);
2101
- return { target, jsonOutput };
2102
- }
2103
-
2104
- export async function handlePlayCheck(args: string[]): Promise<number> {
2105
- const options = parsePlayCheckOptions(args);
2106
- if (!isFileTarget(options.target)) {
2107
- const resolved = resolve(options.target);
2108
- console.error(`File not found: ${resolved}`);
2109
- return 1;
2110
- }
2111
-
2112
- const absolutePlayPath = resolve(options.target);
2113
- const sourceCode = readFileSync(absolutePlayPath, 'utf-8');
2114
- let graph: {
2115
- root: BundledPlayFileSuccess;
2116
- nodes: Map<string, BundledPlayFileSuccess>;
2117
- };
2118
- try {
2119
- graph = await collectBundledPlayGraph(absolutePlayPath);
2120
- } catch (error) {
2121
- const message = error instanceof Error ? error.message : String(error);
2122
- if (options.jsonOutput) {
2123
- process.stdout.write(
2124
- `${JSON.stringify({ valid: false, stage: 'bundle', errors: [message] })}\n`,
2125
- );
2126
- } else {
2127
- console.error(message);
2128
- }
2129
- return 1;
2130
- }
2131
-
2132
- const playName =
2133
- graph.root.playName ?? extractPlayName(sourceCode, absolutePlayPath);
2134
- const client = new DeeplineClient();
2135
- const result = await client.checkPlayArtifact({
2136
- name: playName,
2137
- sourceCode: graph.root.sourceCode,
2138
- artifact: graph.root.artifact,
2139
- });
2140
-
2141
- if (options.jsonOutput) {
2142
- process.stdout.write(`${JSON.stringify({ name: playName, ...result })}\n`);
2143
- } else if (result.valid) {
2144
- console.log(`✓ ${playName} passed cloud play check`);
2145
- if (result.artifactHash) {
2146
- console.log(` artifact: ${result.artifactHash.slice(0, 12)}`);
2147
- }
2148
- } else {
2149
- console.error(`✗ ${playName} failed cloud play check`);
2150
- for (const error of result.errors) {
2151
- console.error(` ${error}`);
2152
- }
2153
- }
2154
-
2155
- return result.valid ? 0 : 1;
2156
- }
2157
-
2158
- async function handleFileBackedRun(
2159
- options: PlayRunCommandOptions,
2160
- ): Promise<number> {
2161
- if (options.target.kind !== 'file') {
2162
- throw new Error('Expected a file-backed play run target.');
2163
- }
2164
- const client = new DeeplineClient();
2165
- const progress =
2166
- getActiveCliProgress() ?? createCliProgress(!options.jsonOutput);
2167
- const absolutePlayPath = resolve(options.target.path);
2168
- progress.phase('compiling play');
2169
- const sourceCode = readFileSync(absolutePlayPath, 'utf-8');
2170
- let graph: {
2171
- root: BundledPlayFileSuccess;
2172
- nodes: Map<string, BundledPlayFileSuccess>;
2173
- };
2174
- try {
2175
- graph = await collectBundledPlayGraph(absolutePlayPath);
2176
- await compileBundledPlayGraphManifests(client, graph);
2177
- progress.phase('compiled play');
2178
- } catch (error) {
2179
- progress.fail();
2180
- console.error(error instanceof Error ? error.message : String(error));
2181
- return 1;
2182
- }
2183
-
2184
- const bundleResult = graph.root;
2185
- const playName =
2186
- bundleResult.playName ?? extractPlayName(sourceCode, absolutePlayPath);
2187
-
2188
- try {
2189
- progress.phase('publishing imported plays');
2190
- await publishImportedPlayDependencies(client, graph);
2191
- } catch (error) {
2192
- progress.fail();
2193
- console.error(error instanceof Error ? error.message : String(error));
2194
- return 1;
2195
- }
2196
-
2197
- const runtimeInput = options.input ? { ...options.input } : {};
2198
- const packagedFileUploads = bundleResult.packagedFiles.map((file) =>
2199
- stageFile(file.logicalPath, file.absolutePath),
2200
- );
2201
- const stagedFileInputs = await stageFileInputArgs({
2202
- client,
2203
- runtimeInput,
2204
- bindings: fileInputBindingsFromStaticPipeline(
2205
- requireCompilerManifest(bundleResult).staticPipeline,
2206
- ),
2207
- progress,
2208
- });
2209
-
2210
- const startRequest = {
2211
- name: playName,
2212
- sourceCode: bundleResult.sourceCode,
2213
- runtimeArtifact: bundleResult.artifact,
2214
- compilerManifest: requireCompilerManifest(bundleResult),
2215
- packagedFileUploads,
2216
- ...(Object.keys(runtimeInput).length > 0 ? { input: runtimeInput } : {}),
2217
- ...(stagedFileInputs.inputFile
2218
- ? { inputFile: stagedFileInputs.inputFile }
2219
- : {}),
2220
- ...(stagedFileInputs.packagedFiles.length
2221
- ? { packagedFiles: stagedFileInputs.packagedFiles }
2222
- : {}),
2223
- ...(options.force ? { force: true } : {}),
2224
- };
2225
-
2226
- if (options.watch) {
2227
- progress.phase('starting run');
2228
- const finalStatus = await startAndWaitForPlayCompletionByStream({
2229
- client,
2230
- request: startRequest,
2231
- playName,
2232
- jsonOutput: options.jsonOutput,
2233
- emitLogs: options.emitLogs,
2234
- waitTimeoutMs: options.waitTimeoutMs,
2235
- progress,
2236
- });
2237
- const exportedPath = exportPlayStatusRows(finalStatus, options.outPath);
2238
- if (finalStatus.status === 'completed') {
2239
- progress.complete();
2240
- } else {
2241
- progress.fail();
2242
- }
2243
- writePlayResult(finalStatus, options.jsonOutput, { exportedPath });
2244
- return finalStatus.status === 'completed' ? 0 : 1;
2245
- }
2246
-
2247
- progress.phase('starting run');
2248
- const started = await client.startPlayRun(startRequest);
2249
- const dashboardUrl = buildPlayDashboardUrl(client.baseUrl, playName);
2250
- progress.phase(`loading play on ${dashboardUrl}`);
2251
- progress.complete();
2252
-
2253
- writeStartedPlayRun({
2254
- runId: started.workflowId,
2255
- playName,
2256
- status: started.status,
2257
- statusUrl: started.statusUrl,
2258
- dashboardUrl: started.dashboardUrl ?? dashboardUrl,
2259
- jsonOutput: options.jsonOutput,
2260
- progress,
2261
- });
2262
- return 0;
2263
- }
2264
-
2265
- async function resolveNamedRunRevisionId(input: {
2266
- client: DeeplineClient;
2267
- playName: string;
2268
- revisionId: string | null;
2269
- selector: 'live' | 'latest' | null;
2270
- }): Promise<string | null> {
2271
- if (input.revisionId) {
2272
- return input.revisionId;
2273
- }
2274
- if (input.selector === 'latest') {
2275
- const versions = await input.client.listPlayVersions(input.playName);
2276
- const latest = versions[0];
2277
- if (!latest?._id) {
2278
- throw new Error(`No saved revisions found for ${input.playName}.`);
2279
- }
2280
- return latest._id;
2281
- }
2282
- return null;
2283
- }
2284
-
2285
- async function handleNamedRun(options: PlayRunCommandOptions): Promise<number> {
2286
- if (options.target.kind !== 'name') {
2287
- throw new Error('Expected a named play run target.');
2288
- }
2289
- const client = new DeeplineClient();
2290
- const progress =
2291
- getActiveCliProgress() ?? createCliProgress(!options.jsonOutput);
2292
-
2293
- progress.phase('loading play definition');
2294
- const playDetail = await assertCanonicalNamedPlayReference(client, options.target.name);
2295
- progress.phase('selecting revision');
2296
- const selectedRevisionId = await resolveNamedRunRevisionId({
2297
- client,
2298
- playName: options.target.name,
2299
- revisionId: options.revisionId,
2300
- selector: options.revisionSelector,
2301
- });
2302
-
2303
- const runtimeInput = options.input ? { ...options.input } : {};
2304
- const stagedFileInputs = await stageFileInputArgs({
2305
- client,
2306
- runtimeInput,
2307
- bindings: [
2308
- ...fileInputBindingsFromPlaySchema(playDetail.play.inputSchema),
2309
- ...fileInputBindingsFromStaticPipeline(playDetail.play.staticPipeline),
2310
- ],
2311
- progress,
2312
- });
2313
-
2314
- const startRequest = {
2315
- name: options.target.name,
2316
- ...(selectedRevisionId ? { revisionId: selectedRevisionId } : {}),
2317
- ...(Object.keys(runtimeInput).length > 0 ? { input: runtimeInput } : {}),
2318
- ...(stagedFileInputs.inputFile
2319
- ? { inputFile: stagedFileInputs.inputFile }
2320
- : {}),
2321
- ...(stagedFileInputs.packagedFiles.length
2322
- ? { packagedFiles: stagedFileInputs.packagedFiles }
2323
- : {}),
2324
- ...(options.force ? { force: true } : {}),
2325
- };
2326
-
2327
- if (options.watch) {
2328
- progress.phase('starting run');
2329
- const finalStatus = await startAndWaitForPlayCompletionByStream({
2330
- client,
2331
- request: startRequest,
2332
- playName: options.target.name,
2333
- jsonOutput: options.jsonOutput,
2334
- emitLogs: options.emitLogs,
2335
- waitTimeoutMs: options.waitTimeoutMs,
2336
- progress,
2337
- });
2338
- const exportedPath = exportPlayStatusRows(finalStatus, options.outPath);
2339
- if (finalStatus.status === 'completed') {
2340
- progress.complete();
2341
- } else {
2342
- progress.fail();
2343
- }
2344
- writePlayResult(finalStatus, options.jsonOutput, { exportedPath });
2345
- return finalStatus.status === 'completed' ? 0 : 1;
2346
- }
2347
-
2348
- progress.phase('starting run');
2349
- const started = await client.startPlayRun(startRequest);
2350
- const dashboardUrl = buildPlayDashboardUrl(
2351
- client.baseUrl,
2352
- options.target.name,
2353
- );
2354
- progress.phase(`loading play on ${dashboardUrl}`);
2355
- progress.complete();
2356
-
2357
- writeStartedPlayRun({
2358
- runId: started.workflowId,
2359
- playName: started.name ?? options.target.name,
2360
- status: started.status,
2361
- statusUrl: started.statusUrl,
2362
- dashboardUrl: started.dashboardUrl ?? dashboardUrl,
2363
- jsonOutput: options.jsonOutput,
2364
- progress,
2365
- });
2366
- return 0;
2367
- }
2368
-
2369
- export async function handlePlayRun(args: string[]): Promise<number> {
2370
- const options = parsePlayRunOptions(args);
2371
- if (options.target.kind === 'file') {
2372
- if (isFileTarget(options.target.path)) {
2373
- return handleFileBackedRun(options);
2374
- }
2375
- const resolved = resolve(options.target.path);
2376
- console.error(`File not found: ${resolved}`);
2377
- // Suggest close matches
2378
- const dir = dirname(resolved);
2379
- if (existsSync(dir)) {
2380
- const base = basename(resolved);
2381
- try {
2382
- const siblings = readdirSync(dir).filter(
2383
- (f) =>
2384
- f.includes(base.replace(/\.(play\.)?ts$/, '')) ||
2385
- f.endsWith('.play.ts'),
2386
- );
2387
- if (siblings.length > 0) {
2388
- console.error(`Did you mean one of these?`);
2389
- for (const s of siblings.slice(0, 5)) {
2390
- console.error(` ${join(dir, s)}`);
2391
- }
2392
- }
2393
- } catch {}
2394
- }
2395
- return 1;
2396
- }
2397
- return handleNamedRun(options);
2398
- }
2399
-
2400
- export async function handlePlayTail(args: string[]): Promise<number> {
2401
- const usage =
2402
- 'Usage: deepline play tail --run-id <run-id> [--interval-ms 1000] [--json]\n' +
2403
- ' deepline play tail --name <name> [--interval-ms 1000] [--json]';
2404
- let target: PlayRunTarget;
2405
- try {
2406
- target = parsePlayRunTarget({ args, usage, allowName: true });
2407
- } catch (error) {
2408
- console.error(error instanceof Error ? error.message : usage);
2409
- return 1;
2410
- }
2411
-
2412
- const client = new DeeplineClient();
2413
- const jsonOutput = argsWantJson(args);
2414
- const emitLogs = !jsonOutput || args.includes('--logs');
2415
- let intervalMs = 500;
2416
- for (let index = 0; index < args.length; index += 1) {
2417
- const arg = args[index]!;
2418
- if (
2419
- (arg === '--interval-ms' || arg === '--poll-interval-ms') &&
2420
- args[index + 1]
2421
- ) {
2422
- intervalMs = parsePositiveInteger(args[++index]!, arg);
2423
- }
2424
- }
2425
-
2426
- const workflowId = await resolvePlayRunId(client, target);
2427
- const progress = getActiveCliProgress() ?? createCliProgress(!jsonOutput);
2428
- progress.phase(`tailing ${workflowId}`);
2429
- const finalStatus = await waitForPlayCompletion({
2430
- client,
2431
- workflowId,
2432
- pollIntervalMs: intervalMs,
2433
- jsonOutput,
2434
- emitLogs,
2435
- waitTimeoutMs: null,
2436
- progress,
2437
- });
2438
-
2439
- if (finalStatus.status === 'completed') {
2440
- progress.complete();
2441
- } else {
2442
- progress.fail();
2443
- }
2444
- writePlayResult(finalStatus, jsonOutput);
2445
- return finalStatus.status === 'completed' ? 0 : 1;
2446
- }
2447
-
2448
- export async function handlePlayStatus(args: string[]): Promise<number> {
2449
- const usage =
2450
- 'Usage: deepline play status --run-id <run-id> [--json] [--full]\n' +
2451
- ' deepline play status --name <name> [--json] [--full]';
2452
- let target: PlayRunTarget;
2453
- try {
2454
- target = parsePlayRunTarget({ args, usage, allowName: true });
2455
- } catch (error) {
2456
- console.error(error instanceof Error ? error.message : usage);
2457
- return 1;
2458
- }
2459
-
2460
- const client = new DeeplineClient();
2461
- const workflowId = await resolvePlayRunId(client, target);
2462
- const status = await client.getPlayStatus(workflowId);
2463
- writePlayResult(status, argsWantJson(args), {
2464
- fullJson: args.includes('--full'),
2465
- });
2466
- return 0;
2467
- }
2468
-
2469
- function parseRunIdPositional(args: string[], usage: string): string {
2470
- for (let index = 0; index < args.length; index += 1) {
2471
- const arg = args[index]!;
2472
- if (arg === '--json' || arg === '--full' || arg === '--logs') {
2473
- continue;
2474
- }
2475
- if (arg === '--out' && args[index + 1]) {
2476
- index += 1;
2477
- continue;
2478
- }
2479
- if (!arg.startsWith('--')) {
2480
- return arg;
2481
- }
2482
- }
2483
- throw new DeeplineError(usage);
2484
- }
2485
-
2486
- export async function handleRunStatus(args: string[]): Promise<number> {
2487
- const usage = 'Usage: deepline runs status <run-id> [--json] [--full]';
2488
- let runId: string;
2489
- try {
2490
- runId = parseRunIdPositional(args, usage);
2491
- } catch (error) {
2492
- console.error(error instanceof Error ? error.message : usage);
2493
- return 1;
2494
- }
2495
- const client = new DeeplineClient();
2496
- const status = await client.getPlayStatus(runId);
2497
- writePlayResult(status, argsWantJson(args), {
2498
- fullJson: args.includes('--full'),
2499
- });
2500
- return 0;
2501
- }
2502
-
2503
- export async function handleRunLogs(args: string[]): Promise<number> {
2504
- const usage = 'Usage: deepline runs logs <run-id> [--json]';
2505
- let runId: string;
2506
- try {
2507
- runId = parseRunIdPositional(args, usage);
2508
- } catch (error) {
2509
- console.error(error instanceof Error ? error.message : usage);
2510
- return 1;
2511
- }
2512
- const client = new DeeplineClient();
2513
- const status = await client.getPlayStatus(runId);
2514
- const logs = status.progress?.logs ?? [];
2515
- if (argsWantJson(args)) {
2516
- process.stdout.write(`${JSON.stringify({ runId: status.runId, logs })}\n`);
2517
- } else {
2518
- process.stdout.write(`${logs.join('\n')}${logs.length > 0 ? '\n' : ''}`);
2519
- }
2520
- return 0;
2521
- }
2522
-
2523
- export async function handleRunExport(args: string[]): Promise<number> {
2524
- const usage =
2525
- 'Usage: deepline runs export <run-id> --out output.csv [--json]';
2526
- let runId: string;
2527
- try {
2528
- runId = parseRunIdPositional(args, usage);
2529
- } catch (error) {
2530
- console.error(error instanceof Error ? error.message : usage);
2531
- return 1;
2532
- }
2533
- let outPath: string | null = null;
2534
- for (let index = 0; index < args.length; index += 1) {
2535
- const arg = args[index]!;
2536
- if (arg === '--out' && args[index + 1]) {
2537
- outPath = resolve(args[++index]!);
2538
- }
2539
- }
2540
- if (!outPath) {
2541
- console.error(usage);
2542
- return 1;
2543
- }
2544
- const client = new DeeplineClient();
2545
- const status = await client.getPlayStatus(runId);
2546
- const exportedPath = exportPlayStatusRows(status, outPath);
2547
- if (argsWantJson(args)) {
2548
- const rowsInfo = extractCanonicalRowsInfo(status);
2549
- process.stdout.write(
2550
- `${JSON.stringify({
2551
- runId: status.runId,
2552
- csv_path: exportedPath,
2553
- rowCount: rowsInfo?.totalRows ?? null,
2554
- columns: rowsInfo?.columns ?? [],
2555
- })}\n`,
2556
- );
2557
- } else {
2558
- console.log(`Exported ${status.runId} to ${exportedPath}`);
2559
- }
2560
- return 0;
2561
- }
2562
-
2563
- export async function handlePlayStop(args: string[]): Promise<number> {
2564
- const usage =
2565
- 'Usage: deepline play stop --run-id <run-id> [--reason "text"] [--json]';
2566
- let target: PlayRunTarget;
2567
- try {
2568
- target = parsePlayRunTarget({ args, usage, allowName: false });
2569
- } catch (error) {
2570
- console.error(error instanceof Error ? error.message : usage);
2571
- return 1;
2572
- }
2573
-
2574
- const client = new DeeplineClient();
2575
- const jsonOutput = argsWantJson(args);
2576
- let reason: string | undefined;
2577
- for (let index = 0; index < args.length; index += 1) {
2578
- const arg = args[index]!;
2579
- if (arg === '--reason' && args[index + 1]) {
2580
- reason = args[++index]!;
2581
- }
2582
- }
2583
-
2584
- const workflowId = await resolvePlayRunId(client, target);
2585
- const result = await client.stopPlay(workflowId, { reason });
2586
- if (jsonOutput) {
2587
- process.stdout.write(`${JSON.stringify(result)}\n`);
2588
- } else {
2589
- console.log(`Stopped ${result.runId}`);
2590
- if (result.hitlCancelledCount > 0) {
2591
- console.log(` cancelled HITL waits: ${result.hitlCancelledCount}`);
2592
- }
2593
- }
2594
- return 0;
2595
- }
2596
-
2597
- export async function handlePlayGet(args: string[]): Promise<number> {
2598
- const target = args[0];
2599
- if (!target) {
2600
- console.error('Usage: deepline play get <play-file.ts|play-name> [--json]');
2601
- return 1;
2602
- }
2603
-
2604
- const client = new DeeplineClient();
2605
- const explicitJson = args.includes('--json');
2606
- const sourceOutput = args.includes('--source');
2607
- const jsonOutput = sourceOutput ? explicitJson : argsWantJson(args);
2608
- let outPath: string | null = null;
2609
- for (let index = 1; index < args.length; index += 1) {
2610
- const arg = args[index]!;
2611
- if (arg === '--out' && args[index + 1]) {
2612
- outPath = resolve(args[++index]!);
2613
- }
2614
- }
2615
- const playName = isFileTarget(target)
2616
- ? extractPlayName(readFileSync(resolve(target), 'utf-8'), resolve(target))
2617
- : parseReferencedPlayTarget(target).playName;
2618
- const detail = isFileTarget(target)
2619
- ? await client.getPlay(playName)
2620
- : await assertCanonicalNamedPlayReference(client, target);
2621
- const resolvedSource =
2622
- detail.play.workingRevision?.sourceCode ??
2623
- detail.play.liveRevision?.sourceCode ??
2624
- detail.play.currentRevision?.sourceCode ??
2625
- detail.play.sourceCode ??
2626
- '';
2627
- const materializedFile =
2628
- outPath
2629
- ? materializeRemotePlaySource({
2630
- target,
2631
- playName,
2632
- sourceCode: resolvedSource,
2633
- outPath,
2634
- })
2635
- : null;
2636
- const loadedMessage = materializedFile
2637
- ? formatLoadedPlayMessage(materializedFile)
2638
- : null;
2639
-
2640
- if (jsonOutput) {
2641
- process.stdout.write(
2642
- `${JSON.stringify({
2643
- ...detail,
2644
- ...(materializedFile
2645
- ? {
2646
- message: loadedMessage,
2647
- materializedFile: {
2648
- path: materializedFile.path,
2649
- created: materializedFile.created,
2650
- status: materializedFile.status,
2651
- message: loadedMessage,
2652
- },
2653
- }
2654
- : {}),
2655
- })}\n`,
2656
- );
2657
- return 0;
2658
- }
2659
-
2660
- if (sourceOutput) {
2661
- if (!resolvedSource.trim()) {
2662
- console.error(`No source code available for ${playName}.`);
2663
- return 1;
2664
- }
2665
- if (materializedFile) {
2666
- console.log(loadedMessage);
2667
- return 0;
2668
- }
2669
- process.stdout.write(resolvedSource);
2670
- if (!resolvedSource.endsWith('\n')) {
2671
- process.stdout.write('\n');
2672
- }
2673
- return 0;
2674
- }
2675
-
2676
- if (outPath && loadedMessage) {
2677
- console.log(loadedMessage);
2678
- return 0;
2679
- }
2680
-
2681
- console.log(`Play: ${formatPlayReference(detail.play)}`);
2682
- console.log(
2683
- `Working version: ${detail.play.workingRevision?.version ?? '—'}`,
2684
- );
2685
- console.log(`Live version: ${detail.play.liveRevision?.version ?? '—'}`);
2686
- console.log(`Draft dirty: ${detail.play.isDraftDirty ? 'yes' : 'no'}`);
2687
- console.log(`Runs: ${detail.latestRuns.length}`);
2688
- console.log(`Updated: ${formatTimestamp(detail.play.updatedAt)}`);
2689
- console.log(`Sheet rows: ${detail.sheetSummary?.stats?.total ?? 0}`);
2690
- if (detail.customerDbUrl) {
2691
- console.log(`Customer DB: ${detail.customerDbUrl}`);
2692
- }
2693
- if (materializedFile) {
2694
- console.log(loadedMessage);
2695
- }
2696
- if (detail.latestRuns.length > 0) {
2697
- console.log('Latest runs:');
2698
- for (const run of detail.latestRuns.slice(0, 5)) {
2699
- console.log(` ${formatRunLine(run)}`);
2700
- }
2701
- }
2702
- return 0;
2703
- }
2704
-
2705
- export async function handlePlayRuns(args: string[]): Promise<number> {
2706
- const nameIndex = args.indexOf('--name');
2707
- const name = nameIndex >= 0 ? args[nameIndex + 1] : undefined;
2708
- if (!name) {
2709
- console.error('Usage: deepline play runs --name <name> [--json]');
2710
- return 1;
2711
- }
2712
-
2713
- const client = new DeeplineClient();
2714
- const jsonOutput = argsWantJson(args);
2715
- await assertCanonicalNamedPlayReference(client, name);
2716
- const runs = await client.listPlayRuns(
2717
- parseReferencedPlayTarget(name).playName,
2718
- );
2719
- if (jsonOutput) {
2720
- process.stdout.write(`${JSON.stringify({ runs })}\n`);
2721
- return 0;
2722
- }
2723
-
2724
- if (runs.length === 0) {
2725
- console.log(`No runs found for ${name}.`);
2726
- return 0;
2727
- }
2728
-
2729
- for (const run of runs) {
2730
- console.log(formatRunLine(run));
2731
- }
2732
- return 0;
2733
- }
2734
-
2735
- function formatVersionLine(version: PlayRevisionSummary): string {
2736
- const revisionLabel =
2737
- version.artifactHash?.slice(0, 12) ?? 'unknown-revision';
2738
- return `v${version.version} ${revisionLabel} ${formatTimestamp(version.createdAt)}`;
2739
- }
2740
-
2741
- export async function handlePlayVersions(args: string[]): Promise<number> {
2742
- const nameIndex = args.indexOf('--name');
2743
- const playName = nameIndex >= 0 ? args[nameIndex + 1] : undefined;
2744
- if (!playName) {
2745
- console.error('Usage: deepline play versions --name <name> [--json]');
2746
- return 1;
2747
- }
2748
-
2749
- const client = new DeeplineClient();
2750
- const jsonOutput = argsWantJson(args);
2751
- await assertCanonicalNamedPlayReference(client, playName);
2752
- const versions = await client.listPlayVersions(
2753
- parseReferencedPlayTarget(playName).playName,
2754
- );
2755
- if (jsonOutput) {
2756
- process.stdout.write(`${JSON.stringify({ versions })}\n`);
2757
- return 0;
2758
- }
2759
-
2760
- if (versions.length === 0) {
2761
- console.log(`No versions found for ${playName}.`);
2762
- return 0;
2763
- }
2764
-
2765
- for (const version of versions) {
2766
- console.log(formatVersionLine(version));
2767
- }
2768
- return 0;
2769
- }
2770
-
2771
- export async function handlePlayList(args: string[]): Promise<number> {
2772
- const jsonOutput = argsWantJson(args);
2773
- const client = new DeeplineClient();
2774
- const plays = await client.listPlays();
2775
-
2776
- if (jsonOutput) {
2777
- process.stdout.write(`${JSON.stringify(plays)}\n`);
2778
- return 0;
2779
- }
2780
-
2781
- process.stdout.write(`${plays.length} plays available:\n\n`);
2782
- for (const play of plays) {
2783
- const flags = [
2784
- play.origin === 'prebuilt' || play.ownerType === 'deepline'
2785
- ? 'prebuilt'
2786
- : 'owned',
2787
- play.canEdit ? 'editable' : 'readonly',
2788
- play.isDraftDirty ? 'draft-dirty' : null,
2789
- ]
2790
- .filter(Boolean)
2791
- .join(', ');
2792
- const reference = formatPlayListReference(play);
2793
- process.stdout.write(` ${reference}${flags ? ` [${flags}]` : ''}\n`);
2794
- if (play.inputSchema) {
2795
- process.stdout.write(' inputSchema: yes\n');
2796
- }
2797
- process.stdout.write(` run: deepline plays run ${reference} --watch\n`);
2798
- }
2799
- return 0;
2800
- }
2801
-
2802
- function parsePlaySearchOptions(args: string[]): PlaySearchOptions {
2803
- const query = args[0]?.trim();
2804
- if (!query) {
2805
- throw new Error(
2806
- 'Usage: deepline plays search <query> [--origin prebuilt|owned] [--compact] [--json]',
2807
- );
2808
- }
2809
-
2810
- let origin: 'prebuilt' | 'owned' | undefined;
2811
- for (let index = 1; index < args.length; index += 1) {
2812
- const arg = args[index]!;
2813
- if (arg === '--origin' && args[index + 1]) {
2814
- const rawOrigin = args[++index]!.trim().toLowerCase();
2815
- if (rawOrigin !== 'prebuilt' && rawOrigin !== 'owned') {
2816
- throw new Error(`Invalid value for --origin: ${rawOrigin}`);
2817
- }
2818
- origin = rawOrigin;
2819
- }
2820
- }
2821
-
2822
- return {
2823
- query,
2824
- jsonOutput: argsWantJson(args),
2825
- compact: args.includes('--compact'),
2826
- origin,
2827
- };
2828
- }
2829
-
2830
- function printPlayDescription(play: PlayDescription): void {
2831
- const reference = formatPlayListReference(play);
2832
- const labels = [
2833
- play.origin ?? null,
2834
- play.ownerType ?? null,
2835
- play.canEdit ? 'editable' : 'readonly',
2836
- play.isDraftDirty ? 'draft-dirty' : null,
2837
- ].filter(Boolean);
2838
- console.log(
2839
- `Play: ${reference}${labels.length ? ` [${labels.join(', ')}]` : ''}`,
2840
- );
2841
- if (play.displayName && play.displayName !== play.name) {
2842
- console.log(` Display name: ${play.displayName}`);
2843
- }
2844
- if (play.aliases.length > 0) {
2845
- console.log(` Aliases: ${play.aliases.join(', ')}`);
2846
- }
2847
- if (play.inputSchema) {
2848
- console.log(' Input schema:');
2849
- const rendered = JSON.stringify(play.inputSchema, null, 2);
2850
- for (const line of rendered.split('\n')) {
2851
- console.log(` ${line}`);
2852
- }
2853
- }
2854
- if (play.outputSchema) {
2855
- console.log(' Output schema:');
2856
- const rendered = JSON.stringify(play.outputSchema, null, 2);
2857
- for (const line of rendered.split('\n')) {
2858
- console.log(` ${line}`);
2859
- }
2860
- }
2861
- if (play.csvInput) {
2862
- console.log(' CSV input:');
2863
- const rendered = JSON.stringify(play.csvInput, null, 2);
2864
- for (const line of rendered.split('\n')) {
2865
- console.log(` ${line}`);
2866
- }
2867
- }
2868
- if (play.rowOutputSchema) {
2869
- console.log(' Row output schema:');
2870
- const rendered = JSON.stringify(play.rowOutputSchema, null, 2);
2871
- for (const line of rendered.split('\n')) {
2872
- console.log(` ${line}`);
2873
- }
2874
- }
2875
- console.log(` Run: ${play.runCommand}`);
2876
- }
2877
-
2878
- export async function handlePlaySearch(args: string[]): Promise<number> {
2879
- let options: PlaySearchOptions;
2880
- try {
2881
- options = parsePlaySearchOptions(args);
2882
- } catch (error) {
2883
- console.error(error instanceof Error ? error.message : String(error));
2884
- return 1;
2885
- }
2886
-
2887
- const client = new DeeplineClient();
2888
- const plays = await client.searchPlays({
2889
- query: options.query,
2890
- ...(options.origin ? { origin: options.origin } : {}),
2891
- compact: options.compact,
2892
- });
2893
-
2894
- if (options.jsonOutput) {
2895
- process.stdout.write(`${JSON.stringify({ plays })}\n`);
2896
- return 0;
2897
- }
2898
-
2899
- process.stdout.write(`${plays.length} plays found:\n\n`);
2900
- for (const play of plays) {
2901
- printPlayDescription(play);
2902
- console.log('');
2903
- }
2904
- return 0;
2905
- }
2906
-
2907
- export async function handlePlayDescribe(args: string[]): Promise<number> {
2908
- const playName = args[0];
2909
- if (!playName) {
2910
- console.error(
2911
- 'Usage: deepline plays describe <play-name> [--compact] [--json]',
2912
- );
2913
- return 1;
2914
- }
2915
-
2916
- const client = new DeeplineClient();
2917
- await assertCanonicalNamedPlayReference(client, playName);
2918
- const play = await client.describePlay(
2919
- parseReferencedPlayTarget(playName).playName,
2920
- {
2921
- compact: args.includes('--compact'),
2922
- },
2923
- );
2924
-
2925
- if (argsWantJson(args)) {
2926
- process.stdout.write(`${JSON.stringify(play)}\n`);
2927
- return 0;
2928
- }
2929
-
2930
- printPlayDescription(play);
2931
- return 0;
2932
- }
2933
-
2934
- export async function handlePlayPublish(args: string[]): Promise<number> {
2935
- const playName = args[0];
2936
- if (!playName) {
2937
- console.error(
2938
- 'Usage: deepline play publish <play-file.ts|play-name> [--latest|--revision-id <id>] [--json]',
2939
- );
2940
- return 1;
2941
- }
2942
-
2943
- let revisionId: string | undefined;
2944
- let useLatest = false;
2945
- for (let index = 1; index < args.length; index += 1) {
2946
- const arg = args[index]!;
2947
- if (arg === '--revision-id' && args[index + 1]) {
2948
- revisionId = args[++index]!;
2949
- }
2950
- if (arg === '--latest') {
2951
- useLatest = true;
2952
- }
2953
- }
2954
- if (revisionId && useLatest) {
2955
- console.error('Choose only one live target: --latest or --revision-id.');
2956
- return 1;
2957
- }
2958
-
2959
- const client = new DeeplineClient();
2960
- if (isFileTarget(playName)) {
2961
- if (revisionId || useLatest) {
2962
- console.error(
2963
- '--latest and --revision-id cannot be used when the target is a local play file.',
2964
- );
2965
- return 1;
2966
- }
2967
-
2968
- let graph: {
2969
- root: BundledPlayFileSuccess;
2970
- nodes: Map<string, BundledPlayFileSuccess>;
2971
- };
2972
- try {
2973
- graph = await collectBundledPlayGraph(resolve(playName));
2974
- await compileBundledPlayGraphManifests(client, graph);
2975
- await publishImportedPlayDependencies(client, graph);
2976
- } catch (error) {
2977
- console.error(error instanceof Error ? error.message : String(error));
2978
- return 1;
2979
- }
2980
-
2981
- const rootPlayName =
2982
- graph.root.playName ??
2983
- extractPlayName(graph.root.sourceCode, graph.root.filePath);
2984
- const published = await client.registerPlayArtifact({
2985
- name: rootPlayName,
2986
- sourceCode: graph.root.sourceCode,
2987
- artifact: graph.root.artifact,
2988
- compilerManifest: requireCompilerManifest(graph.root),
2989
- publish: true,
2990
- });
2991
- process.stdout.write(
2992
- `${JSON.stringify({
2993
- success: true,
2994
- name: rootPlayName,
2995
- liveVersion: published.version ?? null,
2996
- revisionId: published.revisionId ?? null,
2997
- triggerMetadata: published.triggerMetadata ?? null,
2998
- triggerBindings: published.triggerBindings ?? [],
2999
- })}\n`,
3000
- );
3001
- return 0;
3002
- }
3003
-
3004
- const resolvedName = parseReferencedPlayTarget(playName).playName;
3005
- if (useLatest) {
3006
- const versions = await client.listPlayVersions(resolvedName);
3007
- const latest = versions[0];
3008
- if (!latest?._id) {
3009
- console.error(`No saved revisions found for ${resolvedName}.`);
3010
- return 1;
3011
- }
3012
- revisionId = latest._id;
3013
- }
3014
- try {
3015
- await ensureEditableRemotePlay(client, resolvedName);
3016
- } catch (error) {
3017
- console.error(error instanceof Error ? error.message : String(error));
3018
- return 1;
3019
- }
3020
- const result = await client.publishPlayVersion(
3021
- resolvedName,
3022
- revisionId ? { revisionId } : {},
3023
- );
3024
- process.stdout.write(`${JSON.stringify(result)}\n`);
3025
- return result.success ? 0 : 1;
3026
- }
3027
-
3028
- export async function handlePlayDelete(args: string[]): Promise<number> {
3029
- const playName = args[0];
3030
- if (!playName) {
3031
- console.error('Usage: deepline plays delete <play-name> --yes [--json]');
3032
- return 1;
3033
- }
3034
- const confirmed =
3035
- args.includes('--yes') || args.includes('-y') || args.includes('--force');
3036
- if (!confirmed) {
3037
- console.error(
3038
- 'Refusing to delete without --yes. This deletes the org-owned play, its revisions, trigger bindings, and local run records.',
3039
- );
3040
- return 1;
3041
- }
3042
-
3043
- const client = new DeeplineClient();
3044
- let detail: PlayDetail;
3045
- try {
3046
- detail = await client.getPlay(parseReferencedPlayTarget(playName).playName);
3047
- } catch (error) {
3048
- console.error(error instanceof Error ? error.message : String(error));
3049
- return 1;
3050
- }
3051
- if (detail.play.ownerType === 'deepline' || detail.play.origin === 'prebuilt') {
3052
- console.error(`Cannot delete prebuilt play: ${formatPlayReference(detail.play)}`);
3053
- return 1;
3054
- }
3055
-
3056
- const result = await client.deletePlay(
3057
- parseReferencedPlayTarget(formatPlayReference(detail.play)).playName,
3058
- );
3059
- if (argsWantJson(args)) {
3060
- process.stdout.write(`${JSON.stringify(result)}\n`);
3061
- return result.deleted ? 0 : 1;
3062
- }
3063
- process.stdout.write(
3064
- `Deleted ${result.name}: revisions=${result.deletedRevisionCount}, bindings=${result.deletedBindingCount}, runs=${result.deletedRunCount}\n`,
3065
- );
3066
- return result.deleted ? 0 : 1;
3067
- }
3068
-
3069
- export function registerPlayCommands(program: Command): void {
3070
- const play = program
3071
- .command('plays')
3072
- .alias('play')
3073
- .description('Search, validate, run, and manage cloud plays.')
3074
- .addHelpText(
3075
- 'after',
3076
- `
3077
- Concepts:
3078
- Plays are durable Deepline cloud workflows. Local .play.ts files are bundled locally,
3079
- then validated and executed in Deepline cloud.
3080
-
3081
- Common commands:
3082
- deepline plays search email --json
3083
- deepline plays describe person-linkedin-to-email --json
3084
- deepline plays check my.play.ts
3085
- deepline plays run my.play.ts --input '{"domain":"stripe.com"}' --watch
3086
- deepline plays get person-linkedin-to-email --json
3087
- `,
3088
- );
3089
-
3090
- play
3091
- .command('check <target>')
3092
- .description('Bundle-check a local play file.')
3093
- .addHelpText(
3094
- 'after',
3095
- `
3096
- Notes:
3097
- Validates a local play without storing it, promoting it, or starting a run.
3098
- This uses the authoritative cloud preflight path.
3099
-
3100
- Examples:
3101
- deepline plays check my.play.ts
3102
- deepline plays check my.play.ts --json
3103
- `,
3104
- )
3105
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3106
- .action(async (target, options) => {
3107
- process.exitCode = await handlePlayCheck([
3108
- target,
3109
- ...(options.json ? ['--json'] : []),
3110
- ]);
3111
- });
3112
-
3113
- play
3114
- .command('run [target]')
3115
- .description('Run a play file or named play.')
3116
- .allowUnknownOption(true)
3117
- .allowExcessArguments(true)
3118
- .addHelpText(
3119
- 'after',
3120
- `
3121
- Notes:
3122
- Local play files are bundled locally, then validated and executed in Deepline cloud.
3123
- Named plays run the stored live cloud revision.
3124
- Unknown --foo and --foo.bar flags are treated as play input args.
3125
- File-like input args accept local paths; the CLI stages those files before submit.
3126
- Run performs server preflight automatically. Use \`deepline plays check <file>\`
3127
- to validate without starting a run.
3128
-
3129
- Examples:
3130
- deepline plays run my.play.ts --input '{"domain":"stripe.com"}' --watch
3131
- deepline plays run person-linkedin-to-email --input '{"linkedin_url":"..."}' --watch
3132
- deepline plays run enrich.play.ts --csv leads.csv --watch --out leads-enriched.csv
3133
- `,
3134
- )
3135
- .option('--file <path>', 'Local play file to run')
3136
- .option('--name <name>', 'Saved play name to run')
3137
- .option('-i, --input <json>', 'Input JSON object or @file path')
3138
- .option('--live', 'Run the current live revision explicitly')
3139
- .option('--latest', 'Run the newest saved revision, even if it is not live')
3140
- .option(
3141
- '--revision-id <id>',
3142
- 'Run a specific saved revision instead of the live revision',
3143
- )
3144
- .option(
3145
- '--out <path>',
3146
- 'Write the completed row output to CSV; requires --watch',
3147
- )
3148
- .option('--watch', 'Stream logs until completion')
3149
- .option(
3150
- '--logs',
3151
- 'When output is non-interactive, stream play logs to stderr while waiting',
3152
- )
3153
- .option('--poll-interval-ms <ms>', 'Polling interval while tailing')
3154
- .option('--tail-timeout-ms <ms>', 'Timeout while tailing')
3155
- .option('--force', 'Supersede any active runs for this play')
3156
- .option('--json', 'Emit JSON output')
3157
- .action(async (target, options, command) => {
3158
- const passthroughArgs = [...command.args];
3159
- const explicitTarget = options.file || options.name;
3160
- const targetIsInputFlag = typeof target === 'string' && target.startsWith('--');
3161
- const effectiveTarget = explicitTarget || targetIsInputFlag ? null : target;
3162
- if (
3163
- explicitTarget &&
3164
- typeof target === 'string' &&
3165
- !targetIsInputFlag &&
3166
- !passthroughArgs.includes(target)
3167
- ) {
3168
- passthroughArgs.push(target);
3169
- }
3170
- if (effectiveTarget && passthroughArgs[0] === effectiveTarget) {
3171
- passthroughArgs.shift();
3172
- }
3173
- process.exitCode = await handlePlayRun([
3174
- ...(effectiveTarget ? [effectiveTarget] : []),
3175
- ...(options.file ? ['--file', options.file] : []),
3176
- ...(options.name ? ['--name', options.name] : []),
3177
- ...(options.input ? ['--input', options.input] : []),
3178
- ...(options.live ? ['--live'] : []),
3179
- ...(options.latest ? ['--latest'] : []),
3180
- ...(options.revisionId ? ['--revision-id', options.revisionId] : []),
3181
- ...(options.out ? ['--out', options.out] : []),
3182
- ...(options.watch ? ['--watch'] : []),
3183
- ...(options.logs ? ['--logs'] : []),
3184
- ...(options.pollIntervalMs
3185
- ? ['--poll-interval-ms', options.pollIntervalMs]
3186
- : []),
3187
- ...(options.tailTimeoutMs
3188
- ? ['--tail-timeout-ms', options.tailTimeoutMs]
3189
- : []),
3190
- ...(options.force ? ['--force'] : []),
3191
- ...(options.json ? ['--json'] : []),
3192
- ...passthroughArgs,
3193
- ]);
3194
- });
3195
-
3196
- play
3197
- .command('get <target>')
3198
- .description('Fetch full play details.')
3199
- .addHelpText(
3200
- 'after',
3201
- `
3202
- Notes:
3203
- Full-detail read for metadata, revisions/source fields, latest runs, sheet state,
3204
- customer DB state, and local file comparison. Use \`describe\` for the compact
3205
- contract and examples.
3206
-
3207
- Examples:
3208
- deepline plays get person-linkedin-to-email
3209
- deepline plays get person-linkedin-to-email --json | jq '.play.liveRevision'
3210
- deepline plays get prebuilt/name-and-domain-to-email-waterfall-batch --source > email-waterfall.play.ts
3211
- deepline plays get prebuilt/name-and-domain-to-email-waterfall-batch --source --out ./email-waterfall.play.ts
3212
- `,
3213
- )
3214
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3215
- .option('--source', 'Print raw source code; combine with --out to write a file')
3216
- .option('--out <path>', 'Write source to a specific path')
3217
- .action(async (target, options) => {
3218
- process.exitCode = await handlePlayGet([
3219
- target,
3220
- ...(options.json ? ['--json'] : []),
3221
- ...(options.source ? ['--source'] : []),
3222
- ...(options.out ? ['--out', options.out] : []),
3223
- ]);
3224
- });
3225
-
3226
- play
3227
- .command('list')
3228
- .description('List saved and prebuilt plays.')
3229
- .option('--json', 'Emit JSON output')
3230
- .action(async (options) => {
3231
- process.exitCode = await handlePlayList([
3232
- ...(options.json ? ['--json'] : []),
3233
- ]);
3234
- });
3235
-
3236
- play
3237
- .command('search <query>')
3238
- .description('Search saved and prebuilt plays.')
3239
- .option('--origin <origin>', 'Filter to prebuilt or owned plays')
3240
- .option('--compact', 'Emit compact schemas')
3241
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3242
- .action(async (query, options) => {
3243
- process.exitCode = await handlePlaySearch([
3244
- query,
3245
- ...(options.origin ? ['--origin', options.origin] : []),
3246
- ...(options.compact ? ['--compact'] : []),
3247
- ...(options.json ? ['--json'] : []),
3248
- ]);
3249
- });
3250
-
3251
- play
3252
- .command('describe <target>')
3253
- .description('Describe a play contract and how to run it.')
3254
- .addHelpText(
3255
- 'after',
3256
- `
3257
- Notes:
3258
- Compact contract read for schemas, aliases, examples, ownership, and the run command.
3259
- Use \`get\` when you need full metadata, revisions, source fields, or latest runs.
3260
-
3261
- Examples:
3262
- deepline plays describe person-linkedin-to-email
3263
- deepline plays describe person-linkedin-to-email --json
3264
- `,
3265
- )
3266
- .option('--compact', 'Emit compact schemas')
3267
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3268
- .action(async (target, options) => {
3269
- process.exitCode = await handlePlayDescribe([
3270
- target,
3271
- ...(options.compact ? ['--compact'] : []),
3272
- ...(options.json ? ['--json'] : []),
3273
- ]);
3274
- });
3275
-
3276
- play
3277
- .command('runs')
3278
- .description('List runs for a named play.')
3279
- .option('--name <name>', 'Saved play name')
3280
- .option('--json', 'Emit JSON output')
3281
- .action(async (options) => {
3282
- process.exitCode = await handlePlayRuns([
3283
- ...(options.name ? ['--name', options.name] : []),
3284
- ...(options.json ? ['--json'] : []),
3285
- ]);
3286
- });
3287
-
3288
- play
3289
- .command('versions')
3290
- .description('List revisions for a named play.')
3291
- .option('--name <name>', 'Saved play name')
3292
- .option('--json', 'Emit JSON output')
3293
- .action(async (options) => {
3294
- process.exitCode = await handlePlayVersions([
3295
- ...(options.name ? ['--name', options.name] : []),
3296
- ...(options.json ? ['--json'] : []),
3297
- ]);
3298
- });
3299
-
3300
- play
3301
- .command('tail')
3302
- .description('Tail events for a play run.')
3303
- .option('--run-id <runId>', 'Run id to tail')
3304
- .option('--name <name>', 'Tail the latest run for a named play')
3305
- .option('--interval-ms <ms>', 'Polling interval while tailing')
3306
- .option('--logs', 'With --json, stream play logs to stderr while waiting')
3307
- .option('--json', 'Emit JSON output')
3308
- .action(async (options) => {
3309
- process.exitCode = await handlePlayTail([
3310
- ...(options.runId ? ['--run-id', options.runId] : []),
3311
- ...(options.name ? ['--name', options.name] : []),
3312
- ...(options.intervalMs ? ['--interval-ms', options.intervalMs] : []),
3313
- ...(options.logs ? ['--logs'] : []),
3314
- ...(options.json ? ['--json'] : []),
3315
- ]);
3316
- });
3317
-
3318
- play
3319
- .command('status')
3320
- .description('Show status for a play run.')
3321
- .option('--run-id <runId>', 'Run id to inspect')
3322
- .option('--name <name>', 'Inspect the latest run for a named play')
3323
- .option('--json', 'Emit JSON output')
3324
- .option('--full', 'Debug only: with --json, emit the raw status payload')
3325
- .action(async (options) => {
3326
- process.exitCode = await handlePlayStatus([
3327
- ...(options.runId ? ['--run-id', options.runId] : []),
3328
- ...(options.name ? ['--name', options.name] : []),
3329
- ...(options.json ? ['--json'] : []),
3330
- ...(options.full ? ['--full'] : []),
3331
- ]);
3332
- });
3333
-
3334
- play
3335
- .command('export')
3336
- .description('Export a completed play run to CSV.')
3337
- .requiredOption('--run-id <runId>', 'Run id to export')
3338
- .requiredOption('--out <path>', 'Output CSV path')
3339
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3340
- .action(async (options) => {
3341
- process.exitCode = await handleRunExport([
3342
- options.runId,
3343
- '--out',
3344
- options.out,
3345
- ...(options.json ? ['--json'] : []),
3346
- ]);
3347
- });
3348
-
3349
- play
3350
- .command('stop')
3351
- .description('Stop a play run.')
3352
- .option('--run-id <runId>', 'Run id to stop')
3353
- .option('--reason <text>', 'Reason to include with the stop request')
3354
- .option('--json', 'Emit JSON output')
3355
- .action(async (options) => {
3356
- process.exitCode = await handlePlayStop([
3357
- ...(options.runId ? ['--run-id', options.runId] : []),
3358
- ...(options.reason ? ['--reason', options.reason] : []),
3359
- ...(options.json ? ['--json'] : []),
3360
- ]);
3361
- });
3362
-
3363
- play
3364
- .command('publish <target>')
3365
- .description('Bundle, validate, save, and publish a play.')
3366
- .option('--latest', 'Promote the newest saved revision')
3367
- .option('--revision-id <id>', 'Revision to promote')
3368
- .option('--json', 'Emit JSON output')
3369
- .action(async (target, options) => {
3370
- process.exitCode = await handlePlayPublish([
3371
- target,
3372
- ...(options.latest ? ['--latest'] : []),
3373
- ...(options.revisionId ? ['--revision-id', options.revisionId] : []),
3374
- ...(options.json ? ['--json'] : []),
3375
- ]);
3376
- });
3377
-
3378
- play
3379
- .command('delete <target>')
3380
- .description('Delete an org-owned play and its saved revisions/runs.')
3381
- .option('-y, --yes', 'Confirm deletion')
3382
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3383
- .action(async (target, options) => {
3384
- process.exitCode = await handlePlayDelete([
3385
- target,
3386
- ...(options.yes ? ['--yes'] : []),
3387
- ...(options.json ? ['--json'] : []),
3388
- ]);
3389
- });
3390
-
3391
- const runs = program
3392
- .command('runs')
3393
- .description('Inspect and export play runs.')
3394
- .addHelpText(
3395
- 'after',
3396
- `
3397
- Examples:
3398
- deepline runs status play/my-play/run/20260501t000000-000
3399
- deepline runs export play/my-play/run/20260501t000000-000 --out output.csv
3400
- deepline runs logs play/my-play/run/20260501t000000-000
3401
- `,
3402
- );
3403
-
3404
- runs
3405
- .command('status <runId>')
3406
- .description('Show compact status for a play run.')
3407
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3408
- .option('--full', 'Debug only: with --json, emit the raw status payload')
3409
- .action(async (runId, options) => {
3410
- process.exitCode = await handleRunStatus([
3411
- runId,
3412
- ...(options.json ? ['--json'] : []),
3413
- ...(options.full ? ['--full'] : []),
3414
- ]);
3415
- });
3416
-
3417
- runs
3418
- .command('export <runId>')
3419
- .description('Export the completed row output for a play run to CSV.')
3420
- .requiredOption('--out <path>', 'Output CSV path')
3421
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3422
- .action(async (runId, options) => {
3423
- process.exitCode = await handleRunExport([
3424
- runId,
3425
- '--out',
3426
- options.out,
3427
- ...(options.json ? ['--json'] : []),
3428
- ]);
3429
- });
3430
-
3431
- runs
3432
- .command('logs <runId>')
3433
- .description('Print logs for a play run.')
3434
- .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3435
- .action(async (runId, options) => {
3436
- process.exitCode = await handleRunLogs([
3437
- runId,
3438
- ...(options.json ? ['--json'] : []),
3439
- ]);
3440
- });
3441
- }