@dreamlake/dreamlake-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +191 -0
- package/bin/dreamlake.js +41 -0
- package/dist/cli/auth/commands.js +240 -0
- package/dist/cli/auth/constants.js +16 -0
- package/dist/cli/auth/credentials.js +157 -0
- package/dist/cli/auth/device-flow.js +134 -0
- package/dist/cli/auth/device-secret.js +34 -0
- package/dist/cli/client.js +99 -0
- package/dist/cli/config.js +81 -0
- package/dist/cli/create/index.js +204 -0
- package/dist/cli/delete/index.js +228 -0
- package/dist/cli/download/index.js +128 -0
- package/dist/cli/glob.js +45 -0
- package/dist/cli/graphql-helpers.js +42 -0
- package/dist/cli/graphql.js +47 -0
- package/dist/cli/helpers.js +106 -0
- package/dist/cli/index.js +97 -0
- package/dist/cli/list/index.js +254 -0
- package/dist/cli/org/index.js +348 -0
- package/dist/cli/pipeline/index.js +481 -0
- package/dist/cli/progress.js +65 -0
- package/dist/cli/prompt.js +17 -0
- package/dist/cli/resources.js +134 -0
- package/dist/cli/target.js +85 -0
- package/dist/cli/team/index.js +411 -0
- package/dist/cli/update/index.js +256 -0
- package/dist/cli/upload/index.js +263 -0
- package/dist/cli/upload/kinds.js +85 -0
- package/dist/cli/upload/multipart.js +211 -0
- package/package.json +58 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
// `dreamlake pipeline ...` — pipeline management.
|
|
2
|
+
//
|
|
3
|
+
// Covers all 10 pipeline API endpoints:
|
|
4
|
+
// pipeline list / create / show / update / delete
|
|
5
|
+
// pipeline version list / version show
|
|
6
|
+
// pipeline node list / node show / node state
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { HttpError, requestJson } from "../client.js";
|
|
10
|
+
import { resolveRemote, resolveToken, resolveNamespace } from "../config.js";
|
|
11
|
+
import { emitJson, fail, ok, renderTable, splitCsv } from "../helpers.js";
|
|
12
|
+
import { confirm } from "../prompt.js";
|
|
13
|
+
// ─── shared context resolution ────────────────────────────────────────────────
|
|
14
|
+
async function ctx(nsFlag) {
|
|
15
|
+
const token = resolveToken();
|
|
16
|
+
if (!token) {
|
|
17
|
+
fail("not authenticated — run 'dreamlake login' first");
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const remote = resolveRemote();
|
|
21
|
+
const ns = await resolveNamespace(nsFlag, { token, remote });
|
|
22
|
+
if (!ns) {
|
|
23
|
+
fail("could not resolve namespace — run 'dreamlake login' or pass --namespace");
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { token, remote, ns };
|
|
27
|
+
}
|
|
28
|
+
function readFile(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
return readFileSync(resolve(filePath), "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`cannot read file: ${filePath}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalize --source for AI-generated code. Handles three formats:
|
|
38
|
+
*
|
|
39
|
+
* 1. Actual newlines (subprocess args / $'...' / heredoc) — pass through as-is.
|
|
40
|
+
* 2. JSON/shell escape sequences (\n \t \r) — unescape. Most AI agents produce
|
|
41
|
+
* this when building a shell command string with the source embedded inline.
|
|
42
|
+
* 3. Double-escaped (\\n) — unescape one level so \n survives into Python source.
|
|
43
|
+
*
|
|
44
|
+
* Heuristic: if the string already contains a real newline it's already well-formed;
|
|
45
|
+
* only unescape when no real newlines are present (pure escape-sequence format).
|
|
46
|
+
*/
|
|
47
|
+
function unescapeSource(s) {
|
|
48
|
+
if (s.includes("\n"))
|
|
49
|
+
return s; // already has real newlines — trust as-is
|
|
50
|
+
return s
|
|
51
|
+
.replace(/\\n/g, "\n")
|
|
52
|
+
.replace(/\\t/g, "\t")
|
|
53
|
+
.replace(/\\r/g, "\r")
|
|
54
|
+
.replace(/\\'/g, "'")
|
|
55
|
+
.replace(/\\"/g, '"');
|
|
56
|
+
}
|
|
57
|
+
// ─── pipeline list ────────────────────────────────────────────────────────────
|
|
58
|
+
export async function runPipelineList(opts) {
|
|
59
|
+
const c = await ctx(opts.namespace);
|
|
60
|
+
if (!c)
|
|
61
|
+
return 1;
|
|
62
|
+
try {
|
|
63
|
+
const res = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines`, { token: c.token, query: { kind: opts.kind } });
|
|
64
|
+
if (opts.json) {
|
|
65
|
+
emitJson(res.pipelines);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
if (res.pipelines.length === 0) {
|
|
69
|
+
process.stdout.write("No pipelines found.\n");
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const rows = res.pipelines.map((p) => ({
|
|
73
|
+
name: p.name,
|
|
74
|
+
kind: p.kind,
|
|
75
|
+
hash: p.currentVersionHash ?? "—",
|
|
76
|
+
tags: p.tags.join(", "),
|
|
77
|
+
updatedAt: p.updatedAt.slice(0, 10),
|
|
78
|
+
}));
|
|
79
|
+
process.stdout.write(renderTable(rows, ["name", "kind", "hash", "tags", "updatedAt"]));
|
|
80
|
+
process.stdout.write(`\n ${res.pipelines.length} pipeline(s)\n`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
fail(err.message);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ─── pipeline create ──────────────────────────────────────────────────────────
|
|
89
|
+
export async function runPipelineCreate(name, opts) {
|
|
90
|
+
const c = await ctx(opts.namespace);
|
|
91
|
+
if (!c)
|
|
92
|
+
return 1;
|
|
93
|
+
try {
|
|
94
|
+
const body = { name };
|
|
95
|
+
if (opts.kind)
|
|
96
|
+
body.kind = opts.kind;
|
|
97
|
+
if (opts.description)
|
|
98
|
+
body.description = opts.description;
|
|
99
|
+
if (opts.tags)
|
|
100
|
+
body.tags = splitCsv(opts.tags);
|
|
101
|
+
const sourceCode = opts.source ? unescapeSource(opts.source) : (opts.file ? readFile(opts.file) : undefined);
|
|
102
|
+
if (sourceCode) {
|
|
103
|
+
body.sourceCode = sourceCode;
|
|
104
|
+
if (opts.message)
|
|
105
|
+
body.versionMessage = opts.message;
|
|
106
|
+
}
|
|
107
|
+
const pipeline = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines`, { method: "POST", token: c.token, json: body });
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
emitJson(pipeline);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
ok(`Created pipeline: ${pipeline.name}`);
|
|
113
|
+
if (pipeline.currentVersionHash) {
|
|
114
|
+
process.stdout.write(` version: ${pipeline.currentVersionHash}\n`);
|
|
115
|
+
process.stdout.write(` nodes: ${pipeline.graph?.nodeCount ?? 0}\n`);
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err instanceof HttpError && err.status === 409) {
|
|
121
|
+
fail(`pipeline '${name}' already exists in namespace '${c.ns}'`);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
fail(err.message);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ─── pipeline show ────────────────────────────────────────────────────────────
|
|
129
|
+
export async function runPipelineShow(name, opts) {
|
|
130
|
+
const c = await ctx(opts.namespace);
|
|
131
|
+
if (!c)
|
|
132
|
+
return 1;
|
|
133
|
+
try {
|
|
134
|
+
const pipeline = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}`, { token: c.token });
|
|
135
|
+
if (opts.json) {
|
|
136
|
+
emitJson(pipeline);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
process.stdout.write(`Pipeline: ${pipeline.name}\n`);
|
|
140
|
+
process.stdout.write(` kind: ${pipeline.kind}\n`);
|
|
141
|
+
process.stdout.write(` version: ${pipeline.currentVersionHash ?? "—"}\n`);
|
|
142
|
+
process.stdout.write(` nodes: ${pipeline.graph?.nodeCount ?? "—"}\n`);
|
|
143
|
+
if (pipeline.description)
|
|
144
|
+
process.stdout.write(` description: ${pipeline.description}\n`);
|
|
145
|
+
if (pipeline.tags.length)
|
|
146
|
+
process.stdout.write(` tags: ${pipeline.tags.join(", ")}\n`);
|
|
147
|
+
process.stdout.write(` created: ${pipeline.createdAt.slice(0, 10)}\n`);
|
|
148
|
+
process.stdout.write(` updated: ${pipeline.updatedAt.slice(0, 10)}\n`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
153
|
+
fail(`pipeline '${name}' not found`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
fail(err.message);
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ─── pipeline update ──────────────────────────────────────────────────────────
|
|
161
|
+
export async function runPipelineUpdate(name, opts) {
|
|
162
|
+
const c = await ctx(opts.namespace);
|
|
163
|
+
if (!c)
|
|
164
|
+
return 1;
|
|
165
|
+
const body = {};
|
|
166
|
+
if (opts.description !== undefined)
|
|
167
|
+
body.description = opts.description;
|
|
168
|
+
if (opts.tags !== undefined)
|
|
169
|
+
body.tags = splitCsv(opts.tags);
|
|
170
|
+
const sourceCode = opts.source ?? (opts.file ? readFile(opts.file) : undefined);
|
|
171
|
+
if (sourceCode) {
|
|
172
|
+
body.sourceCode = sourceCode;
|
|
173
|
+
if (opts.message)
|
|
174
|
+
body.versionMessage = opts.message;
|
|
175
|
+
}
|
|
176
|
+
if (Object.keys(body).length === 0) {
|
|
177
|
+
fail("nothing to update — use --file, --description, or --tags");
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const pipeline = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}`, { method: "PATCH", token: c.token, json: body });
|
|
182
|
+
if (opts.json) {
|
|
183
|
+
emitJson(pipeline);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
ok(`Updated pipeline: ${pipeline.name}`);
|
|
187
|
+
if (pipeline.currentVersionHash) {
|
|
188
|
+
process.stdout.write(` version: ${pipeline.currentVersionHash}\n`);
|
|
189
|
+
}
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
194
|
+
fail(`pipeline '${name}' not found`);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
fail(err.message);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ─── pipeline delete ──────────────────────────────────────────────────────────
|
|
202
|
+
export async function runPipelineDelete(name, opts) {
|
|
203
|
+
const c = await ctx(opts.namespace);
|
|
204
|
+
if (!c)
|
|
205
|
+
return 1;
|
|
206
|
+
if (!opts.yes) {
|
|
207
|
+
const proceed = await confirm(`Delete pipeline '${name}'?`, false);
|
|
208
|
+
if (!proceed) {
|
|
209
|
+
process.stdout.write("Cancelled.\n");
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}`, { method: "DELETE", token: c.token });
|
|
215
|
+
ok(`Deleted pipeline: ${name}`);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
220
|
+
fail(`pipeline '${name}' not found`);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
fail(err.message);
|
|
224
|
+
return 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// ─── pipeline version list ────────────────────────────────────────────────────
|
|
228
|
+
export async function runVersionList(name, opts) {
|
|
229
|
+
const c = await ctx(opts.namespace);
|
|
230
|
+
if (!c)
|
|
231
|
+
return 1;
|
|
232
|
+
try {
|
|
233
|
+
const res = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}/versions`, { token: c.token });
|
|
234
|
+
if (opts.json) {
|
|
235
|
+
emitJson(res.versions);
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
if (res.versions.length === 0) {
|
|
239
|
+
process.stdout.write("No versions found.\n");
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
const rows = res.versions.map((v) => ({
|
|
243
|
+
hash: v.hash,
|
|
244
|
+
nodes: String(v.nodeCount),
|
|
245
|
+
message: (v.message ?? "—").slice(0, 40),
|
|
246
|
+
createdAt: v.createdAt.slice(0, 10),
|
|
247
|
+
}));
|
|
248
|
+
process.stdout.write(renderTable(rows, ["hash", "nodes", "message", "createdAt"]));
|
|
249
|
+
process.stdout.write(`\n ${res.versions.length} version(s)\n`);
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
254
|
+
fail(`pipeline '${name}' not found`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
fail(err.message);
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ─── pipeline version show ────────────────────────────────────────────────────
|
|
262
|
+
export async function runVersionShow(name, hash, opts) {
|
|
263
|
+
const c = await ctx(opts.namespace);
|
|
264
|
+
if (!c)
|
|
265
|
+
return 1;
|
|
266
|
+
try {
|
|
267
|
+
const version = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}/versions/${hash}`, { token: c.token });
|
|
268
|
+
emitJson(version);
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
273
|
+
fail(`version '${hash}' not found for pipeline '${name}'`);
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
fail(err.message);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ─── pipeline node list ───────────────────────────────────────────────────────
|
|
281
|
+
export async function runNodeList(name, hash, opts) {
|
|
282
|
+
const c = await ctx(opts.namespace);
|
|
283
|
+
if (!c)
|
|
284
|
+
return 1;
|
|
285
|
+
try {
|
|
286
|
+
const res = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}/versions/${hash}/nodes`, { token: c.token });
|
|
287
|
+
if (opts.json) {
|
|
288
|
+
emitJson(res.nodes);
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
const entries = Object.values(res.nodes);
|
|
292
|
+
if (entries.length === 0) {
|
|
293
|
+
process.stdout.write("No nodes found.\n");
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
const rows = entries.map((n) => ({
|
|
297
|
+
id: n.id,
|
|
298
|
+
kind: n.kind ?? "—",
|
|
299
|
+
status: n.state?.status ?? "idle",
|
|
300
|
+
artifacts: n.state?.artifacts ? Object.keys(n.state.artifacts).join(", ") : "—",
|
|
301
|
+
}));
|
|
302
|
+
process.stdout.write(renderTable(rows, ["id", "kind", "status", "artifacts"]));
|
|
303
|
+
process.stdout.write(`\n ${entries.length} node(s)\n`);
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
308
|
+
fail(`pipeline '${name}' or version '${hash}' not found`);
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
fail(err.message);
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ─── pipeline node show ───────────────────────────────────────────────────────
|
|
316
|
+
export async function runNodeShow(name, hash, nodeId, opts) {
|
|
317
|
+
const c = await ctx(opts.namespace);
|
|
318
|
+
if (!c)
|
|
319
|
+
return 1;
|
|
320
|
+
try {
|
|
321
|
+
const node = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}/versions/${hash}/nodes/${nodeId}`, { token: c.token });
|
|
322
|
+
emitJson(node);
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
327
|
+
fail(`node '${nodeId}' not found`);
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
fail(err.message);
|
|
331
|
+
return 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// ─── pipeline node state ──────────────────────────────────────────────────────
|
|
335
|
+
export async function runNodeState(name, hash, nodeId, opts) {
|
|
336
|
+
const c = await ctx(opts.namespace);
|
|
337
|
+
if (!c)
|
|
338
|
+
return 1;
|
|
339
|
+
const body = { status: opts.status };
|
|
340
|
+
if (opts.error !== undefined)
|
|
341
|
+
body.error = opts.error;
|
|
342
|
+
if (opts.startedAt !== undefined)
|
|
343
|
+
body.startedAt = opts.startedAt;
|
|
344
|
+
if (opts.endedAt !== undefined)
|
|
345
|
+
body.endedAt = opts.endedAt;
|
|
346
|
+
if (opts.artifacts !== undefined) {
|
|
347
|
+
try {
|
|
348
|
+
body.artifacts = JSON.parse(opts.artifacts);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
fail("--artifacts must be valid JSON, e.g. '{\"index_html\":\"https://...\"}'");
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const state = await requestJson(c.remote, `/namespaces/${c.ns}/pipelines/${name}/versions/${hash}/nodes/${nodeId}/state`, { method: "PATCH", token: c.token, json: body });
|
|
357
|
+
ok(`Node state updated: ${nodeId} → ${state.status}`);
|
|
358
|
+
if (state.artifacts) {
|
|
359
|
+
process.stdout.write(` artifacts: ${JSON.stringify(state.artifacts)}\n`);
|
|
360
|
+
}
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
365
|
+
fail(`node '${nodeId}' not found`);
|
|
366
|
+
return 1;
|
|
367
|
+
}
|
|
368
|
+
fail(err.message);
|
|
369
|
+
return 1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// ─── registration ─────────────────────────────────────────────────────────────
|
|
373
|
+
export function registerPipelineCommand(program) {
|
|
374
|
+
const pipeline = program
|
|
375
|
+
.command("pipeline")
|
|
376
|
+
.description("manage pipelines, versions, and node execution state");
|
|
377
|
+
// ── pipeline list ──
|
|
378
|
+
pipeline
|
|
379
|
+
.command("list")
|
|
380
|
+
.description("list pipelines in a namespace")
|
|
381
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
382
|
+
.option("--kind <kind>", "filter by kind (e.g. python)")
|
|
383
|
+
.option("--json", "emit JSON")
|
|
384
|
+
.action(async (opts) => process.exit(await runPipelineList(opts)));
|
|
385
|
+
// ── pipeline create ──
|
|
386
|
+
pipeline
|
|
387
|
+
.command("create")
|
|
388
|
+
.description("create a pipeline (optionally with an initial version)")
|
|
389
|
+
.argument("<name>", "pipeline name (unique within namespace)")
|
|
390
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
391
|
+
.option("--source <code>", "Python source code string — triggers parser and creates initial version")
|
|
392
|
+
.option("--file <path>", "Python source file (alternative to --source)")
|
|
393
|
+
.option("--description <text>", "description")
|
|
394
|
+
.option("--tags <csv>", "comma-separated tags")
|
|
395
|
+
.option("--message <text>", "version message (used with --source or --file)")
|
|
396
|
+
.option("--kind <kind>", "pipeline kind (default: python)")
|
|
397
|
+
.option("--json", "emit JSON")
|
|
398
|
+
.action(async (name, opts) => process.exit(await runPipelineCreate(name, opts)));
|
|
399
|
+
// ── pipeline show ──
|
|
400
|
+
pipeline
|
|
401
|
+
.command("show")
|
|
402
|
+
.description("show pipeline detail with current version graph")
|
|
403
|
+
.argument("<name>", "pipeline name")
|
|
404
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
405
|
+
.option("--json", "emit JSON")
|
|
406
|
+
.action(async (name, opts) => process.exit(await runPipelineShow(name, opts)));
|
|
407
|
+
// ── pipeline update ──
|
|
408
|
+
pipeline
|
|
409
|
+
.command("update")
|
|
410
|
+
.description("update metadata or upload a new version")
|
|
411
|
+
.argument("<name>", "pipeline name")
|
|
412
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
413
|
+
.option("--source <code>", "new Python source code string — triggers parser and creates a new version")
|
|
414
|
+
.option("--file <path>", "new Python source file (alternative to --source)")
|
|
415
|
+
.option("--description <text>", "new description")
|
|
416
|
+
.option("--tags <csv>", "new tags (comma-separated, replaces existing)")
|
|
417
|
+
.option("--message <text>", "version message (used with --source or --file)")
|
|
418
|
+
.option("--json", "emit JSON")
|
|
419
|
+
.action(async (name, opts) => process.exit(await runPipelineUpdate(name, opts)));
|
|
420
|
+
// ── pipeline delete ──
|
|
421
|
+
pipeline
|
|
422
|
+
.command("delete")
|
|
423
|
+
.description("soft-delete a pipeline (preserves version history)")
|
|
424
|
+
.argument("<name>", "pipeline name")
|
|
425
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
426
|
+
.option("--yes", "skip confirmation prompt")
|
|
427
|
+
.action(async (name, opts) => process.exit(await runPipelineDelete(name, opts)));
|
|
428
|
+
// ── pipeline version ──
|
|
429
|
+
const version = pipeline
|
|
430
|
+
.command("version")
|
|
431
|
+
.description("manage pipeline versions");
|
|
432
|
+
version
|
|
433
|
+
.command("list")
|
|
434
|
+
.description("list versions of a pipeline (newest first)")
|
|
435
|
+
.argument("<name>", "pipeline name")
|
|
436
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
437
|
+
.option("--json", "emit JSON")
|
|
438
|
+
.action(async (name, opts) => process.exit(await runVersionList(name, opts)));
|
|
439
|
+
version
|
|
440
|
+
.command("show")
|
|
441
|
+
.description("show full version data (sourceCode + graph)")
|
|
442
|
+
.argument("<name>", "pipeline name")
|
|
443
|
+
.argument("<hash>", "12-char version hash")
|
|
444
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
445
|
+
.option("--json", "emit JSON (always, this command outputs JSON)")
|
|
446
|
+
.action(async (name, hash, opts) => process.exit(await runVersionShow(name, hash, opts)));
|
|
447
|
+
// ── pipeline node ──
|
|
448
|
+
const node = pipeline
|
|
449
|
+
.command("node")
|
|
450
|
+
.description("inspect node definitions and execution state");
|
|
451
|
+
node
|
|
452
|
+
.command("list")
|
|
453
|
+
.description("list all nodes in a version with their current state")
|
|
454
|
+
.argument("<name>", "pipeline name")
|
|
455
|
+
.argument("<hash>", "version hash")
|
|
456
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
457
|
+
.option("--json", "emit JSON")
|
|
458
|
+
.action(async (name, hash, opts) => process.exit(await runNodeList(name, hash, opts)));
|
|
459
|
+
node
|
|
460
|
+
.command("show")
|
|
461
|
+
.description("show a single node with its current state")
|
|
462
|
+
.argument("<name>", "pipeline name")
|
|
463
|
+
.argument("<hash>", "version hash")
|
|
464
|
+
.argument("<nodeId>", "node ID (e.g. tr_source)")
|
|
465
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
466
|
+
.option("--json", "emit JSON (always, this command outputs JSON)")
|
|
467
|
+
.action(async (name, hash, nodeId, opts) => process.exit(await runNodeShow(name, hash, nodeId, opts)));
|
|
468
|
+
node
|
|
469
|
+
.command("state")
|
|
470
|
+
.description("write back node execution state (for execution engines)")
|
|
471
|
+
.argument("<name>", "pipeline name")
|
|
472
|
+
.argument("<hash>", "version hash")
|
|
473
|
+
.argument("<nodeId>", "node ID")
|
|
474
|
+
.requiredOption("--status <status>", "idle | queued | running | waiting | done | error | blocked")
|
|
475
|
+
.option("--error <message>", "error message (use with --status error)")
|
|
476
|
+
.option("--artifacts <json>", 'JSON object of artifact URLs, e.g. \'{"index_html":"https://..."}\'')
|
|
477
|
+
.option("--started-at <iso>", "ISO 8601 start time")
|
|
478
|
+
.option("--ended-at <iso>", "ISO 8601 end time")
|
|
479
|
+
.option("--namespace <slug>", "namespace slug (default: active login)")
|
|
480
|
+
.action(async (name, hash, nodeId, opts) => process.exit(await runNodeState(name, hash, nodeId, opts)));
|
|
481
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Zero-dependency progress reporting, replacing dreamlake-py's use of
|
|
2
|
+
// `rich.Progress`. Writes a single repainted line to stderr so stdout
|
|
3
|
+
// stays clean for piping. Falls back to plain newline-terminated lines
|
|
4
|
+
// when stderr is not a TTY (CI logs).
|
|
5
|
+
const isTty = Boolean(process.stderr.isTTY);
|
|
6
|
+
/** A simple "M/N parts" counter that repaints in place. */
|
|
7
|
+
export class PartsProgress {
|
|
8
|
+
total;
|
|
9
|
+
label;
|
|
10
|
+
done = 0;
|
|
11
|
+
constructor(total, label) {
|
|
12
|
+
this.total = total;
|
|
13
|
+
this.label = label;
|
|
14
|
+
}
|
|
15
|
+
advance(n = 1) {
|
|
16
|
+
this.done += n;
|
|
17
|
+
this.render();
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
if (isTty) {
|
|
21
|
+
process.stderr.write(`\r ${this.label} ${this.done}/${this.total} parts`);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
process.stderr.write(` ${this.label} ${this.done}/${this.total} parts\n`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
done_() {
|
|
28
|
+
if (isTty)
|
|
29
|
+
process.stderr.write("\n");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** A byte-oriented download progress line: "12.3/45.6 MB (27%)". */
|
|
33
|
+
export class BytesProgress {
|
|
34
|
+
total;
|
|
35
|
+
downloaded = 0;
|
|
36
|
+
lastDecile = -1; // non-TTY: only log when crossing a 10% step
|
|
37
|
+
constructor(total) {
|
|
38
|
+
this.total = total;
|
|
39
|
+
}
|
|
40
|
+
advance(bytes) {
|
|
41
|
+
this.downloaded += bytes;
|
|
42
|
+
this.render();
|
|
43
|
+
}
|
|
44
|
+
render() {
|
|
45
|
+
const mb = (n) => (n / 1024 / 1024).toFixed(1);
|
|
46
|
+
const pctNum = this.total
|
|
47
|
+
? Math.floor((this.downloaded / this.total) * 100)
|
|
48
|
+
: null;
|
|
49
|
+
const line = ` ${mb(this.downloaded)}${this.total ? `/${mb(this.total)}` : ""} MB${pctNum != null ? ` (${pctNum}%)` : ""}`;
|
|
50
|
+
if (isTty) {
|
|
51
|
+
process.stderr.write(`\r${line}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Non-TTY (logs/CI): one line per 10% step, not per chunk.
|
|
55
|
+
const decile = pctNum != null ? Math.floor(pctNum / 10) : 0;
|
|
56
|
+
if (decile > this.lastDecile) {
|
|
57
|
+
this.lastDecile = decile;
|
|
58
|
+
process.stderr.write(`${line}\n`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finish() {
|
|
62
|
+
if (isTty)
|
|
63
|
+
process.stderr.write("\n");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Interactive confirmation, replacing dreamlake-py's `input()` /
|
|
2
|
+
// `rich.Confirm.ask`. Uses @inquirer/prompts (already a lakeshore dep).
|
|
3
|
+
// Non-TTY stdin returns the default without prompting so scripted runs
|
|
4
|
+
// don't hang.
|
|
5
|
+
import { confirm as inquirerConfirm } from "@inquirer/prompts";
|
|
6
|
+
export async function confirm(message, defaultValue = false) {
|
|
7
|
+
if (!process.stdin.isTTY) {
|
|
8
|
+
return defaultValue;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return await inquirerConfirm({ message, default: defaultValue });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Ctrl-C / closed stream → treat as "no".
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|