deepline 0.1.10 → 0.1.12

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 (34) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/index.js +509 -353
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/index.mjs +513 -358
  5. package/dist/cli/index.mjs.map +1 -1
  6. package/dist/index.d.mts +250 -305
  7. package/dist/index.d.ts +250 -305
  8. package/dist/index.js +174 -286
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +174 -285
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +23 -13
  13. package/dist/repo/apps/play-runner-workers/src/entry.ts +581 -1220
  14. package/dist/repo/sdk/src/cli/commands/play.ts +381 -247
  15. package/dist/repo/sdk/src/cli/commands/tools.ts +1 -1
  16. package/dist/repo/sdk/src/cli/dataset-stats.ts +86 -12
  17. package/dist/repo/sdk/src/client.ts +54 -51
  18. package/dist/repo/sdk/src/index.ts +7 -16
  19. package/dist/repo/sdk/src/play.ts +122 -135
  20. package/dist/repo/sdk/src/plays/bundle-play-file.ts +6 -3
  21. package/dist/repo/sdk/src/tool-output.ts +0 -111
  22. package/dist/repo/sdk/src/types.ts +2 -0
  23. package/dist/repo/sdk/src/version.ts +1 -1
  24. package/dist/repo/sdk/src/worker-play-entry.ts +3 -0
  25. package/dist/repo/shared_libs/play-runtime/context.ts +510 -267
  26. package/dist/repo/shared_libs/play-runtime/csv-rename.ts +180 -0
  27. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +13 -1
  28. package/dist/repo/shared_libs/play-runtime/tool-result.ts +139 -114
  29. package/dist/repo/shared_libs/plays/bundling/index.ts +68 -5
  30. package/dist/repo/shared_libs/plays/compiler-manifest.ts +1 -1
  31. package/dist/repo/shared_libs/plays/dataset.ts +1 -1
  32. package/dist/repo/shared_libs/plays/runtime-validation.ts +8 -28
  33. package/package.json +1 -1
  34. package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +0 -184
@@ -1,7 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import {
3
3
  existsSync,
4
- mkdirSync,
5
4
  readFileSync,
6
5
  readdirSync,
7
6
  realpathSync,
@@ -37,12 +36,10 @@ import {
37
36
  getActiveCliProgress,
38
37
  type CliProgress,
39
38
  } from '../progress.js';
40
- import { recordCliTrace, traceCliSpan } from '../trace.js';
41
39
  import { argsWantJson } from '../utils.js';
42
40
 
43
41
  type PlayRunCommandOptions = {
44
42
  target: { kind: 'file'; path: string } | { kind: 'name'; name: string };
45
- csvPath: string | null;
46
43
  input: Record<string, unknown> | null;
47
44
  revisionId: string | null;
48
45
  revisionSelector: 'live' | 'latest' | null;
@@ -55,6 +52,10 @@ type PlayRunCommandOptions = {
55
52
  outPath: string | null;
56
53
  };
57
54
 
55
+ type FileInputBinding = {
56
+ inputPath: string;
57
+ };
58
+
58
59
  type PlayCheckCommandOptions = {
59
60
  target: string;
60
61
  jsonOutput: boolean;
@@ -141,81 +142,6 @@ function defaultMaterializedPlayPath(reference: string): string {
141
142
  return resolve(`${safeName || 'play'}.play.ts`);
142
143
  }
143
144
 
144
- function sanitizeGeneratedPlayName(value: string): string {
145
- return (
146
- value
147
- .trim()
148
- .toLowerCase()
149
- .replace(/^prebuilt\//, '')
150
- .replace(/[^a-z0-9-]/g, '-')
151
- .replace(/-+/g, '-')
152
- .replace(/^-|-$/g, '') || 'play'
153
- );
154
- }
155
-
156
- function buildGeneratedCsvWrapperSource(input: {
157
- wrapperName: string;
158
- playRef: string;
159
- }): string {
160
- return `import { definePlay } from 'deepline';
161
-
162
- export default definePlay(
163
- ${JSON.stringify(input.wrapperName)},
164
- async (ctx, input: Record<string, unknown> & { file: string }) => {
165
- const rows = await ctx.csv<Record<string, unknown>>(input.file);
166
- const constants = Object.fromEntries(
167
- Object.entries(input).filter(([key]) => key !== 'file'),
168
- );
169
-
170
- const mappedRows = await ctx
171
- .map('csv_rows', rows, {
172
- key: (row, index) =>
173
- String(
174
- row.id ??
175
- row.lead_id ??
176
- row.email ??
177
- row.linkedin_url ??
178
- row.domain ??
179
- index,
180
- ),
181
- })
182
- .step('result', (row, rowCtx) =>
183
- rowCtx.runPlay(
184
- 'row_play',
185
- ${JSON.stringify(input.playRef)},
186
- {
187
- ...constants,
188
- ...row,
189
- },
190
- {
191
- description: 'Run the source play for this CSV row.',
192
- },
193
- ),
194
- )
195
- .run({ description: 'Run the source play once per CSV row.' });
196
-
197
- return { rows: mappedRows };
198
- },
199
- );
200
- `;
201
- }
202
-
203
- function writeGeneratedCsvWrapperPlay(playRef: string): string {
204
- const baseName = sanitizeGeneratedPlayName(
205
- parseReferencedPlayTarget(playRef).unqualifiedPlayName,
206
- );
207
- const wrapperName = `${baseName}-csv`;
208
- const outputDir = resolve('.deepline', 'generated');
209
- const outputPath = join(outputDir, `${wrapperName}.play.ts`);
210
- mkdirSync(outputDir, { recursive: true });
211
- writeFileSync(
212
- outputPath,
213
- buildGeneratedCsvWrapperSource({ wrapperName, playRef }),
214
- 'utf-8',
215
- );
216
- return outputPath;
217
- }
218
-
219
145
  type MaterializedRemotePlaySource = {
220
146
  path: string;
221
147
  status: 'created' | 'updated' | 'unchanged';
@@ -308,8 +234,15 @@ function looksLikeFilePath(target: string): boolean {
308
234
  if (target.trim().toLowerCase().startsWith('prebuilt/')) {
309
235
  return false;
310
236
  }
237
+ if (
238
+ target.startsWith('./') ||
239
+ target.startsWith('../') ||
240
+ target.startsWith('/') ||
241
+ target.startsWith('~/')
242
+ ) {
243
+ return true;
244
+ }
311
245
  return (
312
- target.includes('/') ||
313
246
  target.includes('\\') ||
314
247
  /\.(ts|js|mjs|play\.ts)$/.test(target)
315
248
  );
@@ -334,6 +267,187 @@ function parseJsonInput(raw: string): Record<string, unknown> {
334
267
  return parsed as Record<string, unknown>;
335
268
  }
336
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
+
337
451
  function stageFile(logicalPath: string, absolutePath: string) {
338
452
  const buffer = readFileSync(absolutePath);
339
453
  return {
@@ -937,24 +1051,10 @@ async function startAndWaitForPlayCompletionByStream(input: {
937
1051
  Math.max(1, input.waitTimeoutMs),
938
1052
  );
939
1053
 
940
- recordCliTrace({
941
- phase: 'cli.start_stream_request',
942
- playName: input.playName,
943
- });
944
1054
  try {
945
- let eventCount = 0;
946
1055
  for await (const event of input.client.startPlayRunStream(input.request, {
947
1056
  signal: controller.signal,
948
1057
  })) {
949
- eventCount += 1;
950
- if (eventCount === 1) {
951
- recordCliTrace({
952
- phase: 'cli.start_stream_first_event',
953
- ms: Date.now() - startedAt,
954
- playName: input.playName,
955
- eventType: event.type,
956
- });
957
- }
958
1058
  const eventRunId = getEventPayload(event).runId;
959
1059
  if (
960
1060
  typeof eventRunId === 'string' &&
@@ -1002,14 +1102,6 @@ async function startAndWaitForPlayCompletionByStream(input: {
1002
1102
 
1003
1103
  const finalStatus = getFinalStatusFromLiveEvent(event);
1004
1104
  if (finalStatus) {
1005
- recordCliTrace({
1006
- phase: 'cli.start_stream_final_event',
1007
- ms: Date.now() - startedAt,
1008
- playName: input.playName,
1009
- runId: finalStatus.runId,
1010
- status: finalStatus.status,
1011
- eventCount,
1012
- });
1013
1105
  return finalStatus;
1014
1106
  }
1015
1107
  }
@@ -1488,7 +1580,6 @@ function buildRunNextCommands(runId: string): Record<string, string> {
1488
1580
  return {
1489
1581
  exportCsv: `deepline runs export ${runId} --out output.csv`,
1490
1582
  status: `deepline runs status ${runId} --json`,
1491
- fullStatus: `deepline runs status ${runId} --json --full`,
1492
1583
  logs: `deepline runs logs ${runId}`,
1493
1584
  };
1494
1585
  }
@@ -1542,13 +1633,30 @@ function compactPlayStatus(
1542
1633
  ...(result !== undefined ? { result } : {}),
1543
1634
  ...(status.resultView ? { resultView: status.resultView } : {}),
1544
1635
  ...(datasetStats ? { dataset_stats: datasetStats } : {}),
1545
- ...(rowsInfo ? { previewRows: rowsInfo.rows.slice(0, 10) } : {}),
1636
+ ...(rowsInfo ? { previewRows: rowsInfo.rows.slice(0, 5) } : {}),
1546
1637
  ...(billing ? { billing } : {}),
1547
1638
  ...(status.run ? { run: status.run } : {}),
1548
1639
  next: buildRunNextCommands(status.runId),
1549
1640
  };
1550
1641
  }
1551
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
+
1552
1660
  function formatDatasetStatsLines(datasetStats: DatasetStats | null): string[] {
1553
1661
  if (!datasetStats) {
1554
1662
  return [];
@@ -1559,17 +1667,17 @@ function formatDatasetStatsLines(datasetStats: DatasetStats | null): string[] {
1559
1667
  12,
1560
1668
  )) {
1561
1669
  const topValues = stat.top_values
1562
- ? `, ${Object.entries(stat.top_values)
1670
+ ? `, top_values=${Object.entries(stat.top_values)
1563
1671
  .slice(0, 3)
1564
1672
  .map(([value, count]) => `${value}=${count}`)
1565
1673
  .join(', ')}`
1566
1674
  : '';
1567
1675
  const sample =
1568
1676
  stat.sample_value !== undefined
1569
- ? `, sample=${JSON.stringify(stat.sample_value)}`
1677
+ ? `, sample_value=${JSON.stringify(stat.sample_value)}`
1570
1678
  : '';
1571
1679
  lines.push(
1572
- ` ${column}: ${stat.non_empty}, unique=${stat.unique}${topValues}${sample}`,
1680
+ ` ${column}: non_empty=${stat.non_empty}, unique=${stat.unique}${topValues}${sample}`,
1573
1681
  );
1574
1682
  }
1575
1683
  return lines;
@@ -1583,7 +1691,9 @@ function writePlayResult(
1583
1691
  if (jsonOutput) {
1584
1692
  process.stdout.write(
1585
1693
  `${JSON.stringify(
1586
- options?.fullJson ? status : compactPlayStatus(status, options),
1694
+ options?.fullJson
1695
+ ? enrichPlayStatusWithDatasetStats(status)
1696
+ : compactPlayStatus(status, options),
1587
1697
  )}\n`,
1588
1698
  );
1589
1699
  return;
@@ -1833,13 +1943,12 @@ function writeStartedPlayRun(input: {
1833
1943
 
1834
1944
  function parsePlayRunOptions(args: string[]): PlayRunCommandOptions {
1835
1945
  const usage =
1836
- "Usage: deepline plays run <play-name> [--input '{...}'] [--csv file.csv] [--live|--latest|--revision-id <id>] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force]\n" +
1837
- " deepline plays run <play-file.ts> [--input '{...}'] [--csv file.csv] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force]\n" +
1838
- " deepline plays run --file <play-file.ts> [--input '{...}'] [--csv file.csv] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force]\n" +
1839
- " deepline plays run --name <name> [--input '{...}'] [--csv file.csv] [--live|--latest|--revision-id <id>] [--watch] [--out output.csv] [--tail-timeout-ms 30000] [--force] [--json]";
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]";
1840
1950
  let filePath: string | null = null;
1841
1951
  let playName: string | null = null;
1842
- let csvPath: string | null = null;
1843
1952
  let input: Record<string, unknown> | null = null;
1844
1953
  let revisionId: string | null = null;
1845
1954
  let revisionSelector: 'live' | 'latest' | null = null;
@@ -1861,10 +1970,6 @@ function parsePlayRunOptions(args: string[]): PlayRunCommandOptions {
1861
1970
  playName = parseReferencedPlayTarget(args[++index]!).playName;
1862
1971
  continue;
1863
1972
  }
1864
- if (arg === '--csv' && args[index + 1]) {
1865
- csvPath = resolve(args[++index]!);
1866
- continue;
1867
- }
1868
1973
  if ((arg === '--input' || arg === '-i') && args[index + 1]) {
1869
1974
  input = parseJsonInput(args[++index]!);
1870
1975
  continue;
@@ -1927,7 +2032,13 @@ function parsePlayRunOptions(args: string[]): PlayRunCommandOptions {
1927
2032
  continue;
1928
2033
  }
1929
2034
  if (arg.startsWith('--')) {
1930
- throw new Error(`Unexpected flag: ${arg}\n${usage}`);
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;
1931
2042
  }
1932
2043
  if (!arg.startsWith('--') && !filePath && !playName) {
1933
2044
  if (isFileTarget(arg) || looksLikeFilePath(arg)) {
@@ -1967,7 +2078,6 @@ function parsePlayRunOptions(args: string[]): PlayRunCommandOptions {
1967
2078
  target: filePath
1968
2079
  ? { kind: 'file', path: filePath }
1969
2080
  : { kind: 'name', name: playName! },
1970
- csvPath,
1971
2081
  input,
1972
2082
  revisionId,
1973
2083
  revisionSelector,
@@ -2055,37 +2165,15 @@ async function handleFileBackedRun(
2055
2165
  const progress =
2056
2166
  getActiveCliProgress() ?? createCliProgress(!options.jsonOutput);
2057
2167
  const absolutePlayPath = resolve(options.target.path);
2058
- recordCliTrace({
2059
- phase: 'cli.play_run_file_start',
2060
- playPath: absolutePlayPath,
2061
- watch: options.watch,
2062
- hasCsv: Boolean(options.csvPath),
2063
- force: options.force,
2064
- });
2065
2168
  progress.phase('compiling play');
2066
- const readSourceStartedAt = Date.now();
2067
2169
  const sourceCode = readFileSync(absolutePlayPath, 'utf-8');
2068
- recordCliTrace({
2069
- phase: 'cli.read_play_source',
2070
- ms: Date.now() - readSourceStartedAt,
2071
- bytes: sourceCode.length,
2072
- playPath: absolutePlayPath,
2073
- });
2074
2170
  let graph: {
2075
2171
  root: BundledPlayFileSuccess;
2076
2172
  nodes: Map<string, BundledPlayFileSuccess>;
2077
2173
  };
2078
2174
  try {
2079
- graph = await traceCliSpan(
2080
- 'cli.bundle_play_graph',
2081
- { playPath: absolutePlayPath },
2082
- () => collectBundledPlayGraph(absolutePlayPath),
2083
- );
2084
- await traceCliSpan(
2085
- 'cli.compile_play_manifest',
2086
- { playPath: absolutePlayPath, nodeCount: graph.nodes.size },
2087
- () => compileBundledPlayGraphManifests(client, graph),
2088
- );
2175
+ graph = await collectBundledPlayGraph(absolutePlayPath);
2176
+ await compileBundledPlayGraphManifests(client, graph);
2089
2177
  progress.phase('compiled play');
2090
2178
  } catch (error) {
2091
2179
  progress.fail();
@@ -2099,11 +2187,7 @@ async function handleFileBackedRun(
2099
2187
 
2100
2188
  try {
2101
2189
  progress.phase('publishing imported plays');
2102
- await traceCliSpan(
2103
- 'cli.publish_imported_plays',
2104
- { playName, nodeCount: graph.nodes.size },
2105
- () => publishImportedPlayDependencies(client, graph),
2106
- );
2190
+ await publishImportedPlayDependencies(client, graph);
2107
2191
  } catch (error) {
2108
2192
  progress.fail();
2109
2193
  console.error(error instanceof Error ? error.message : String(error));
@@ -2111,26 +2195,16 @@ async function handleFileBackedRun(
2111
2195
  }
2112
2196
 
2113
2197
  const runtimeInput = options.input ? { ...options.input } : {};
2114
- const prepareFilesStartedAt = Date.now();
2115
2198
  const packagedFileUploads = bundleResult.packagedFiles.map((file) =>
2116
2199
  stageFile(file.logicalPath, file.absolutePath),
2117
2200
  );
2118
- const inputFileUpload = options.csvPath
2119
- ? stageFile(basename(options.csvPath), options.csvPath)
2120
- : (packagedFileUploads[0] ?? null);
2121
- if (
2122
- options.csvPath &&
2123
- typeof runtimeInput.file !== 'string' &&
2124
- typeof runtimeInput.csv !== 'string'
2125
- ) {
2126
- runtimeInput.file = basename(options.csvPath);
2127
- }
2128
- recordCliTrace({
2129
- phase: 'cli.prepare_input_files',
2130
- ms: Date.now() - prepareFilesStartedAt,
2131
- playName,
2132
- packagedFileCount: packagedFileUploads.length,
2133
- hasInputFile: Boolean(inputFileUpload),
2201
+ const stagedFileInputs = await stageFileInputArgs({
2202
+ client,
2203
+ runtimeInput,
2204
+ bindings: fileInputBindingsFromStaticPipeline(
2205
+ requireCompilerManifest(bundleResult).staticPipeline,
2206
+ ),
2207
+ progress,
2134
2208
  });
2135
2209
 
2136
2210
  const startRequest = {
@@ -2138,59 +2212,41 @@ async function handleFileBackedRun(
2138
2212
  sourceCode: bundleResult.sourceCode,
2139
2213
  runtimeArtifact: bundleResult.artifact,
2140
2214
  compilerManifest: requireCompilerManifest(bundleResult),
2141
- inputFileUpload,
2142
2215
  packagedFileUploads,
2143
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
+ : {}),
2144
2223
  ...(options.force ? { force: true } : {}),
2145
2224
  };
2146
2225
 
2147
2226
  if (options.watch) {
2148
2227
  progress.phase('starting run');
2149
- const finalStatus = await traceCliSpan(
2150
- 'cli.start_and_watch',
2151
- { playName },
2152
- () =>
2153
- startAndWaitForPlayCompletionByStream({
2154
- client,
2155
- request: startRequest,
2156
- playName,
2157
- jsonOutput: options.jsonOutput,
2158
- emitLogs: options.emitLogs,
2159
- waitTimeoutMs: options.waitTimeoutMs,
2160
- progress,
2161
- }),
2162
- );
2163
- const exportStartedAt = Date.now();
2164
- const exportedPath = exportPlayStatusRows(finalStatus, options.outPath);
2165
- recordCliTrace({
2166
- phase: 'cli.export_rows',
2167
- ms: Date.now() - exportStartedAt,
2228
+ const finalStatus = await startAndWaitForPlayCompletionByStream({
2229
+ client,
2230
+ request: startRequest,
2168
2231
  playName,
2169
- exported: Boolean(exportedPath),
2232
+ jsonOutput: options.jsonOutput,
2233
+ emitLogs: options.emitLogs,
2234
+ waitTimeoutMs: options.waitTimeoutMs,
2235
+ progress,
2170
2236
  });
2237
+ const exportedPath = exportPlayStatusRows(finalStatus, options.outPath);
2171
2238
  if (finalStatus.status === 'completed') {
2172
2239
  progress.complete();
2173
2240
  } else {
2174
2241
  progress.fail();
2175
2242
  }
2176
- recordCliTrace({
2177
- phase: 'cli.write_play_result',
2178
- playName,
2179
- status: finalStatus.status,
2180
- runId: finalStatus.runId,
2181
- });
2182
2243
  writePlayResult(finalStatus, options.jsonOutput, { exportedPath });
2183
2244
  return finalStatus.status === 'completed' ? 0 : 1;
2184
2245
  }
2185
2246
 
2186
2247
  progress.phase('starting run');
2187
- const started = await traceCliSpan(
2188
- 'cli.start_run',
2189
- { playName },
2190
- () => client.startPlayRun(startRequest),
2191
- );
2192
- const fallbackDashboardUrl = buildPlayDashboardUrl(client.baseUrl, playName);
2193
- const dashboardUrl = started.dashboardUrl ?? fallbackDashboardUrl;
2248
+ const started = await client.startPlayRun(startRequest);
2249
+ const dashboardUrl = buildPlayDashboardUrl(client.baseUrl, playName);
2194
2250
  progress.phase(`loading play on ${dashboardUrl}`);
2195
2251
  progress.complete();
2196
2252
 
@@ -2199,7 +2255,7 @@ async function handleFileBackedRun(
2199
2255
  playName,
2200
2256
  status: started.status,
2201
2257
  statusUrl: started.statusUrl,
2202
- dashboardUrl,
2258
+ dashboardUrl: started.dashboardUrl ?? dashboardUrl,
2203
2259
  jsonOutput: options.jsonOutput,
2204
2260
  progress,
2205
2261
  });
@@ -2233,10 +2289,9 @@ async function handleNamedRun(options: PlayRunCommandOptions): Promise<number> {
2233
2289
  const client = new DeeplineClient();
2234
2290
  const progress =
2235
2291
  getActiveCliProgress() ?? createCliProgress(!options.jsonOutput);
2236
- let stagedInputFile: PlayStagedFileRef | null = null;
2237
2292
 
2238
2293
  progress.phase('loading play definition');
2239
- await assertCanonicalNamedPlayReference(client, options.target.name);
2294
+ const playDetail = await assertCanonicalNamedPlayReference(client, options.target.name);
2240
2295
  progress.phase('selecting revision');
2241
2296
  const selectedRevisionId = await resolveNamedRunRevisionId({
2242
2297
  client,
@@ -2245,21 +2300,27 @@ async function handleNamedRun(options: PlayRunCommandOptions): Promise<number> {
2245
2300
  selector: options.revisionSelector,
2246
2301
  });
2247
2302
 
2248
- if (options.csvPath) {
2249
- progress.phase('staging input file');
2250
- const [staged] = await client.stagePlayFiles([
2251
- stageFile(basename(options.csvPath), options.csvPath),
2252
- ]);
2253
- stagedInputFile = staged ?? null;
2254
- }
2255
-
2256
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
+ });
2257
2313
 
2258
2314
  const startRequest = {
2259
2315
  name: options.target.name,
2260
2316
  ...(selectedRevisionId ? { revisionId: selectedRevisionId } : {}),
2261
2317
  ...(Object.keys(runtimeInput).length > 0 ? { input: runtimeInput } : {}),
2262
- ...(stagedInputFile ? { inputFile: stagedInputFile } : {}),
2318
+ ...(stagedFileInputs.inputFile
2319
+ ? { inputFile: stagedFileInputs.inputFile }
2320
+ : {}),
2321
+ ...(stagedFileInputs.packagedFiles.length
2322
+ ? { packagedFiles: stagedFileInputs.packagedFiles }
2323
+ : {}),
2263
2324
  ...(options.force ? { force: true } : {}),
2264
2325
  };
2265
2326
 
@@ -2274,22 +2335,6 @@ async function handleNamedRun(options: PlayRunCommandOptions): Promise<number> {
2274
2335
  waitTimeoutMs: options.waitTimeoutMs,
2275
2336
  progress,
2276
2337
  });
2277
- if (finalStatus.status !== 'completed' && options.csvPath) {
2278
- progress.phase('generating csv wrapper play');
2279
- const generatedPlayPath = writeGeneratedCsvWrapperPlay(
2280
- options.target.name,
2281
- );
2282
- progress.writeLogLine(
2283
- `Generated CSV wrapper play: ${generatedPlayPath}`,
2284
- );
2285
- progress.phase('running generated csv wrapper play');
2286
- return handleFileBackedRun({
2287
- ...options,
2288
- target: { kind: 'file', path: generatedPlayPath },
2289
- revisionId: null,
2290
- revisionSelector: null,
2291
- });
2292
- }
2293
2338
  const exportedPath = exportPlayStatusRows(finalStatus, options.outPath);
2294
2339
  if (finalStatus.status === 'completed') {
2295
2340
  progress.complete();
@@ -2302,11 +2347,10 @@ async function handleNamedRun(options: PlayRunCommandOptions): Promise<number> {
2302
2347
 
2303
2348
  progress.phase('starting run');
2304
2349
  const started = await client.startPlayRun(startRequest);
2305
- const fallbackDashboardUrl = buildPlayDashboardUrl(
2350
+ const dashboardUrl = buildPlayDashboardUrl(
2306
2351
  client.baseUrl,
2307
2352
  options.target.name,
2308
2353
  );
2309
- const dashboardUrl = started.dashboardUrl ?? fallbackDashboardUrl;
2310
2354
  progress.phase(`loading play on ${dashboardUrl}`);
2311
2355
  progress.complete();
2312
2356
 
@@ -2315,7 +2359,7 @@ async function handleNamedRun(options: PlayRunCommandOptions): Promise<number> {
2315
2359
  playName: started.name ?? options.target.name,
2316
2360
  status: started.status,
2317
2361
  statusUrl: started.statusUrl,
2318
- dashboardUrl,
2362
+ dashboardUrl: started.dashboardUrl ?? dashboardUrl,
2319
2363
  jsonOutput: options.jsonOutput,
2320
2364
  progress,
2321
2365
  });
@@ -2558,8 +2602,9 @@ export async function handlePlayGet(args: string[]): Promise<number> {
2558
2602
  }
2559
2603
 
2560
2604
  const client = new DeeplineClient();
2561
- const jsonOutput = argsWantJson(args);
2605
+ const explicitJson = args.includes('--json');
2562
2606
  const sourceOutput = args.includes('--source');
2607
+ const jsonOutput = sourceOutput ? explicitJson : argsWantJson(args);
2563
2608
  let outPath: string | null = null;
2564
2609
  for (let index = 1; index < args.length; index += 1) {
2565
2610
  const arg = args[index]!;
@@ -2580,7 +2625,7 @@ export async function handlePlayGet(args: string[]): Promise<number> {
2580
2625
  detail.play.sourceCode ??
2581
2626
  '';
2582
2627
  const materializedFile =
2583
- sourceOutput || outPath
2628
+ outPath
2584
2629
  ? materializeRemotePlaySource({
2585
2630
  target,
2586
2631
  playName,
@@ -2628,6 +2673,11 @@ export async function handlePlayGet(args: string[]): Promise<number> {
2628
2673
  return 0;
2629
2674
  }
2630
2675
 
2676
+ if (outPath && loadedMessage) {
2677
+ console.log(loadedMessage);
2678
+ return 0;
2679
+ }
2680
+
2631
2681
  console.log(`Play: ${formatPlayReference(detail.play)}`);
2632
2682
  console.log(
2633
2683
  `Working version: ${detail.play.workingRevision?.version ?? '—'}`,
@@ -2808,6 +2858,20 @@ function printPlayDescription(play: PlayDescription): void {
2808
2858
  console.log(` ${line}`);
2809
2859
  }
2810
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
+ }
2811
2875
  console.log(` Run: ${play.runCommand}`);
2812
2876
  }
2813
2877
 
@@ -2961,6 +3025,47 @@ export async function handlePlayPublish(args: string[]): Promise<number> {
2961
3025
  return result.success ? 0 : 1;
2962
3026
  }
2963
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
+
2964
3069
  export function registerPlayCommands(program: Command): void {
2965
3070
  const play = program
2966
3071
  .command('plays')
@@ -3008,14 +3113,18 @@ Examples:
3008
3113
  play
3009
3114
  .command('run [target]')
3010
3115
  .description('Run a play file or named play.')
3116
+ .allowUnknownOption(true)
3117
+ .allowExcessArguments(true)
3011
3118
  .addHelpText(
3012
3119
  'after',
3013
3120
  `
3014
3121
  Notes:
3015
- Local play files are bundled locally, then validated and executed in Deepline cloud.
3016
- Named plays run the stored live cloud revision.
3017
- Run performs server preflight automatically. Use \`deepline plays check <file>\`
3018
- to validate without starting a run.
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.
3019
3128
 
3020
3129
  Examples:
3021
3130
  deepline plays run my.play.ts --input '{"domain":"stripe.com"}' --watch
@@ -3025,7 +3134,6 @@ Examples:
3025
3134
  )
3026
3135
  .option('--file <path>', 'Local play file to run')
3027
3136
  .option('--name <name>', 'Saved play name to run')
3028
- .option('--csv <path>', 'Attach a CSV file')
3029
3137
  .option('-i, --input <json>', 'Input JSON object or @file path')
3030
3138
  .option('--live', 'Run the current live revision explicitly')
3031
3139
  .option('--latest', 'Run the newest saved revision, even if it is not live')
@@ -3046,12 +3154,26 @@ Examples:
3046
3154
  .option('--tail-timeout-ms <ms>', 'Timeout while tailing')
3047
3155
  .option('--force', 'Supersede any active runs for this play')
3048
3156
  .option('--json', 'Emit JSON output')
3049
- .action(async (target, options) => {
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
+ }
3050
3173
  process.exitCode = await handlePlayRun([
3051
- ...(target ? [target] : []),
3174
+ ...(effectiveTarget ? [effectiveTarget] : []),
3052
3175
  ...(options.file ? ['--file', options.file] : []),
3053
3176
  ...(options.name ? ['--name', options.name] : []),
3054
- ...(options.csv ? ['--csv', options.csv] : []),
3055
3177
  ...(options.input ? ['--input', options.input] : []),
3056
3178
  ...(options.live ? ['--live'] : []),
3057
3179
  ...(options.latest ? ['--latest'] : []),
@@ -3067,6 +3189,7 @@ Examples:
3067
3189
  : []),
3068
3190
  ...(options.force ? ['--force'] : []),
3069
3191
  ...(options.json ? ['--json'] : []),
3192
+ ...passthroughArgs,
3070
3193
  ]);
3071
3194
  });
3072
3195
 
@@ -3084,15 +3207,13 @@ Notes:
3084
3207
  Examples:
3085
3208
  deepline plays get person-linkedin-to-email
3086
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
3087
3212
  `,
3088
3213
  )
3089
3214
  .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3090
- .addOption(
3091
- new Option('--source', 'Materialize or print the source code').hideHelp(),
3092
- )
3093
- .addOption(
3094
- new Option('--out <path>', 'Write source to a specific path').hideHelp(),
3095
- )
3215
+ .option('--source', 'Print raw source code; combine with --out to write a file')
3216
+ .option('--out <path>', 'Write source to a specific path')
3096
3217
  .action(async (target, options) => {
3097
3218
  process.exitCode = await handlePlayGet([
3098
3219
  target,
@@ -3200,7 +3321,7 @@ Examples:
3200
3321
  .option('--run-id <runId>', 'Run id to inspect')
3201
3322
  .option('--name <name>', 'Inspect the latest run for a named play')
3202
3323
  .option('--json', 'Emit JSON output')
3203
- .option('--full', 'With --json, emit the full raw status payload')
3324
+ .option('--full', 'Debug only: with --json, emit the raw status payload')
3204
3325
  .action(async (options) => {
3205
3326
  process.exitCode = await handlePlayStatus([
3206
3327
  ...(options.runId ? ['--run-id', options.runId] : []),
@@ -3254,6 +3375,19 @@ Examples:
3254
3375
  ]);
3255
3376
  });
3256
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
+
3257
3391
  const runs = program
3258
3392
  .command('runs')
3259
3393
  .description('Inspect and export play runs.')
@@ -3271,7 +3405,7 @@ Examples:
3271
3405
  .command('status <runId>')
3272
3406
  .description('Show compact status for a play run.')
3273
3407
  .option('--json', 'Emit JSON output. Also automatic when stdout is piped')
3274
- .option('--full', 'With --json, emit the full raw status payload')
3408
+ .option('--full', 'Debug only: with --json, emit the raw status payload')
3275
3409
  .action(async (runId, options) => {
3276
3410
  process.exitCode = await handleRunStatus([
3277
3411
  runId,