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