dataiku-sdk 0.6.0 → 0.6.1
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/src/cli.js +979 -111
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +2 -2
- package/dist/src/resources/datasets.d.ts +18 -0
- package/dist/src/resources/datasets.js +39 -0
- package/dist/src/resources/jobs.d.ts +14 -0
- package/dist/src/resources/jobs.js +62 -3
- package/dist/src/resources/recipes.d.ts +26 -0
- package/dist/src/resources/recipes.js +114 -0
- package/dist/src/resources/scenarios.d.ts +38 -2
- package/dist/src/resources/scenarios.js +162 -3
- package/dist/src/skill.d.ts +2 -2
- package/dist/src/skill.js +3 -3
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash, } from "node:crypto";
|
|
2
3
|
import { readFileSync, } from "node:fs";
|
|
3
4
|
import { mkdir, writeFile, } from "node:fs/promises";
|
|
4
5
|
import { dirname, join, resolve, } from "node:path";
|
|
@@ -9,6 +10,9 @@ import { validateCredentials, } from "./auth.js";
|
|
|
9
10
|
import { DataikuClient, } from "./client.js";
|
|
10
11
|
import { deleteCredentials, getCredentialsPath, loadCredentials, maskApiKey, saveCredentials, } from "./config.js";
|
|
11
12
|
import { DataikuError, dataikuErrorCode, } from "./errors.js";
|
|
13
|
+
import { buildDatasetCloneSettings, } from "./resources/datasets.js";
|
|
14
|
+
import { parseJobLogProgress, } from "./resources/jobs.js";
|
|
15
|
+
import { scenarioUpdatePreview, } from "./resources/scenarios.js";
|
|
12
16
|
import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, } from "./skill.js";
|
|
13
17
|
import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
|
|
14
18
|
import { deepMerge, } from "./utils/deep-merge.js";
|
|
@@ -16,23 +20,62 @@ import { sanitizeFileName, } from "./utils/sanitize.js";
|
|
|
16
20
|
// ---------------------------------------------------------------------------
|
|
17
21
|
// Utility helpers
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return JSON.parse(readFileSync(candidate, "utf-8")).version;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
dir = dirname(dir);
|
|
29
|
-
}
|
|
23
|
+
function findPackageRoot() {
|
|
24
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
for (let i = 0; i < 5; i++) {
|
|
26
|
+
try {
|
|
27
|
+
readFileSync(resolve(dir, "package.json"), "utf-8");
|
|
28
|
+
return dir;
|
|
30
29
|
}
|
|
30
|
+
catch {
|
|
31
|
+
dir = dirname(dir);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function packageVersion(packageRoot) {
|
|
37
|
+
if (!packageRoot)
|
|
31
38
|
return "unknown";
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8")).version;
|
|
32
41
|
}
|
|
33
42
|
catch {
|
|
34
43
|
return "unknown";
|
|
35
44
|
}
|
|
45
|
+
}
|
|
46
|
+
function gitDirectory(packageRoot) {
|
|
47
|
+
try {
|
|
48
|
+
const gitFile = readFileSync(resolve(packageRoot, ".git"), "utf-8").trim();
|
|
49
|
+
if (gitFile.startsWith("gitdir:")) {
|
|
50
|
+
return resolve(packageRoot, gitFile.slice("gitdir:".length).trim());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Normal checkouts have a .git directory, not a .git file.
|
|
55
|
+
}
|
|
56
|
+
return resolve(packageRoot, ".git");
|
|
57
|
+
}
|
|
58
|
+
function gitRevision(packageRoot) {
|
|
59
|
+
if (!packageRoot)
|
|
60
|
+
return undefined;
|
|
61
|
+
try {
|
|
62
|
+
const gitDir = gitDirectory(packageRoot);
|
|
63
|
+
const head = readFileSync(resolve(gitDir, "HEAD"), "utf-8").trim();
|
|
64
|
+
if (!head.startsWith("ref:"))
|
|
65
|
+
return head.slice(0, 7);
|
|
66
|
+
const ref = head.slice("ref:".length).trim();
|
|
67
|
+
const full = readFileSync(resolve(gitDir, ref), "utf-8").trim();
|
|
68
|
+
return full.slice(0, 7);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const PACKAGE_ROOT = findPackageRoot();
|
|
75
|
+
const CLI_VERSION = packageVersion(PACKAGE_ROOT);
|
|
76
|
+
const CLI_VERSION_LABEL = (() => {
|
|
77
|
+
const revision = gitRevision(PACKAGE_ROOT);
|
|
78
|
+
return revision ? `${CLI_VERSION}+g${revision}` : CLI_VERSION;
|
|
36
79
|
})();
|
|
37
80
|
function num(v) {
|
|
38
81
|
if (typeof v !== "string")
|
|
@@ -70,9 +113,73 @@ function jobLogFilterFromFlag(v) {
|
|
|
70
113
|
}
|
|
71
114
|
throw new UsageError("Invalid --log-filter value. Use stdout, stderr, user, or errors.", "invalid_enum");
|
|
72
115
|
}
|
|
73
|
-
function
|
|
116
|
+
function recipeBackupPath(recipeName, backupDir) {
|
|
74
117
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
75
|
-
return join(backupDir, `${sanitizeFileName(recipeName, "recipe")}-${stamp}.
|
|
118
|
+
return join(backupDir, `${sanitizeFileName(recipeName, "recipe")}-${stamp}.recipe-backup.json`);
|
|
119
|
+
}
|
|
120
|
+
function sha256Hex(value) {
|
|
121
|
+
return createHash("sha256").update(value).digest("hex");
|
|
122
|
+
}
|
|
123
|
+
function normalizeLineEndings(value) {
|
|
124
|
+
return value.replace(/\r\n/g, "\n");
|
|
125
|
+
}
|
|
126
|
+
function stableJson(value) {
|
|
127
|
+
if (value === undefined)
|
|
128
|
+
return "undefined";
|
|
129
|
+
if (value === null || typeof value !== "object")
|
|
130
|
+
return JSON.stringify(value);
|
|
131
|
+
if (Array.isArray(value))
|
|
132
|
+
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
133
|
+
const entries = Object.entries(value).sort(([a,], [b,]) => a.localeCompare(b));
|
|
134
|
+
return `{${entries.map(([key, item,]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
135
|
+
}
|
|
136
|
+
function stableHash(value) {
|
|
137
|
+
return sha256Hex(stableJson(value));
|
|
138
|
+
}
|
|
139
|
+
function recipeCodeEnv(recipe) {
|
|
140
|
+
const params = recipe.params;
|
|
141
|
+
if (!params || typeof params !== "object" || Array.isArray(params))
|
|
142
|
+
return undefined;
|
|
143
|
+
return params.envSelection;
|
|
144
|
+
}
|
|
145
|
+
function recipeGraph(recipe) {
|
|
146
|
+
return {
|
|
147
|
+
inputs: recipe.inputs,
|
|
148
|
+
outputs: recipe.outputs,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function recipeBackupDocument(recipeName, projectKey, current) {
|
|
152
|
+
return {
|
|
153
|
+
resource: "recipe",
|
|
154
|
+
recipeName,
|
|
155
|
+
projectKey,
|
|
156
|
+
createdAt: new Date().toISOString(),
|
|
157
|
+
versionTag: current.recipe.versionTag,
|
|
158
|
+
payloadHash: sha256Hex(current.payload ?? ""),
|
|
159
|
+
graphHash: stableHash(recipeGraph(current.recipe)),
|
|
160
|
+
normalizedPayloadHash: sha256Hex(normalizeLineEndings(current.payload ?? "")),
|
|
161
|
+
codeEnvHash: stableHash(recipeCodeEnv(current.recipe)),
|
|
162
|
+
codeEnv: recipeCodeEnv(current.recipe),
|
|
163
|
+
recipe: current.recipe,
|
|
164
|
+
payload: current.payload ?? "",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function readRecipeBackup(backupPath) {
|
|
168
|
+
const raw = readFileSync(backupPath, "utf-8");
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(raw);
|
|
171
|
+
if (parsed && typeof parsed === "object" && parsed.resource === "recipe")
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Backward-compatible payload-only backups are handled below.
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
resource: "recipe",
|
|
179
|
+
recipeName: "unknown",
|
|
180
|
+
payloadHash: sha256Hex(raw),
|
|
181
|
+
payload: raw,
|
|
182
|
+
};
|
|
76
183
|
}
|
|
77
184
|
function recipeRunShouldWait(flags) {
|
|
78
185
|
if (flags["wait"] === true && flags["no-wait"] === true) {
|
|
@@ -92,6 +199,54 @@ function splitCsvFlag(v) {
|
|
|
92
199
|
return [];
|
|
93
200
|
return v.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
94
201
|
}
|
|
202
|
+
function recipeInputDatasetsFromFlags(flags) {
|
|
203
|
+
const inputs = splitCsvFlag(flags["input"]);
|
|
204
|
+
return inputs.length > 0 ? inputs : undefined;
|
|
205
|
+
}
|
|
206
|
+
function rewritePairsFromFlags(flags, flagName) {
|
|
207
|
+
const rewrites = {};
|
|
208
|
+
for (const spec of splitCsvFlag(flags[flagName])) {
|
|
209
|
+
const idx = spec.indexOf("=");
|
|
210
|
+
if (idx <= 0 || idx === spec.length - 1) {
|
|
211
|
+
throw new UsageError(`--${flagName} values must use FROM=TO.`, "invalid_enum");
|
|
212
|
+
}
|
|
213
|
+
const from = spec.slice(0, idx).trim();
|
|
214
|
+
const to = spec.slice(idx + 1).trim();
|
|
215
|
+
if (!from || !to)
|
|
216
|
+
throw new UsageError(`--${flagName} values must use FROM=TO.`, "invalid_enum");
|
|
217
|
+
rewrites[from] = to;
|
|
218
|
+
}
|
|
219
|
+
return rewrites;
|
|
220
|
+
}
|
|
221
|
+
function plainRecord(value) {
|
|
222
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
223
|
+
? value
|
|
224
|
+
: undefined;
|
|
225
|
+
}
|
|
226
|
+
function datasetSourceSummary(details) {
|
|
227
|
+
const params = details.params ?? {};
|
|
228
|
+
return {
|
|
229
|
+
resource: "dataset",
|
|
230
|
+
name: details.name,
|
|
231
|
+
projectKey: details.projectKey,
|
|
232
|
+
type: details.type,
|
|
233
|
+
managed: details.managed,
|
|
234
|
+
connection: params.connection,
|
|
235
|
+
catalog: params.catalog,
|
|
236
|
+
schema: params.schema,
|
|
237
|
+
table: params.table,
|
|
238
|
+
path: params.path,
|
|
239
|
+
folderSmartId: params.folderSmartId,
|
|
240
|
+
formatType: details.formatType,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function requiredStringFlag(flags, name, usage) {
|
|
244
|
+
const value = flags[name];
|
|
245
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
246
|
+
throw new UsageError(`--${name} is required. Usage: ${usage}`, "missing_required_flag");
|
|
247
|
+
}
|
|
248
|
+
return value.trim();
|
|
249
|
+
}
|
|
95
250
|
function flowZoneId(value) {
|
|
96
251
|
const trimmed = value.trim();
|
|
97
252
|
if (!trimmed)
|
|
@@ -162,35 +317,264 @@ function flowZoneMoveItems(flags) {
|
|
|
162
317
|
}
|
|
163
318
|
return items;
|
|
164
319
|
}
|
|
165
|
-
function
|
|
320
|
+
function flowZoneItems(zone) {
|
|
321
|
+
return [...(zone.items ?? []), ...(zone.shared ?? []),];
|
|
322
|
+
}
|
|
323
|
+
function flowZoneContains(zone, object) {
|
|
324
|
+
return flowZoneItems(zone).some((item) => item.objectId === object.objectId
|
|
325
|
+
&& item.objectType === object.objectType
|
|
326
|
+
&& (object.projectKey === undefined || item.projectKey === object.projectKey));
|
|
327
|
+
}
|
|
328
|
+
function flowZoneSummary(zone, object) {
|
|
329
|
+
const items = flowZoneItems(zone);
|
|
330
|
+
return {
|
|
331
|
+
id: zone.id,
|
|
332
|
+
name: zone.name,
|
|
333
|
+
itemCount: items.length,
|
|
334
|
+
...(object ? { containsMatchingObject: flowZoneContains(zone, object), } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function flowZoneDetailSummary(zone) {
|
|
338
|
+
return {
|
|
339
|
+
...flowZoneSummary(zone),
|
|
340
|
+
items: flowZoneItems(zone),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function resolveFlowZoneIdFromFlags(client, flags, projectKey) {
|
|
344
|
+
const zoneId = typeof flags["zone-id"] === "string" ? flags["zone-id"].trim() : "";
|
|
345
|
+
if (zoneId)
|
|
346
|
+
return zoneId;
|
|
347
|
+
const zone = typeof flags["zone"] === "string" ? flags["zone"].trim() : "";
|
|
348
|
+
if (!zone)
|
|
349
|
+
return undefined;
|
|
350
|
+
const zones = await client.flowZones.list(projectKey);
|
|
351
|
+
const match = zones.find((candidate) => candidate.id === zone || candidate.name === zone);
|
|
352
|
+
if (!match)
|
|
353
|
+
throw new UsageError(`Flow zone not found: ${zone}`, "invalid_enum");
|
|
354
|
+
return match.id;
|
|
355
|
+
}
|
|
356
|
+
async function moveCreatedItemsToZone(client, flags, items, projectKey) {
|
|
357
|
+
const zoneId = await resolveFlowZoneIdFromFlags(client, flags, projectKey);
|
|
358
|
+
if (!zoneId || items.length === 0)
|
|
359
|
+
return {};
|
|
360
|
+
await client.flowZones.moveItems(zoneId, items, projectKey);
|
|
361
|
+
return { zoneId, moved: items, };
|
|
362
|
+
}
|
|
363
|
+
function nestedValue(value, path) {
|
|
364
|
+
let current = value;
|
|
365
|
+
for (const key of path) {
|
|
366
|
+
const record = plainRecord(current);
|
|
367
|
+
if (!record)
|
|
368
|
+
return undefined;
|
|
369
|
+
current = record[key];
|
|
370
|
+
}
|
|
371
|
+
return current;
|
|
372
|
+
}
|
|
373
|
+
function stringPath(value, path) {
|
|
374
|
+
const item = nestedValue(value, path);
|
|
375
|
+
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
376
|
+
}
|
|
377
|
+
function numberPath(value, path) {
|
|
378
|
+
const item = nestedValue(value, path);
|
|
379
|
+
return typeof item === "number" && Number.isFinite(item) ? item : undefined;
|
|
380
|
+
}
|
|
381
|
+
function firstNumberPath(value, paths) {
|
|
382
|
+
for (const path of paths) {
|
|
383
|
+
const item = numberPath(value, path);
|
|
384
|
+
if (item !== undefined)
|
|
385
|
+
return item;
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
function jobSummaryId(job, fallback) {
|
|
390
|
+
return stringPath(job, ["baseStatus", "def", "id",])
|
|
391
|
+
?? stringPath(job, ["def", "id",])
|
|
392
|
+
?? stringPath(job, ["id",])
|
|
393
|
+
?? fallback
|
|
394
|
+
?? "unknown";
|
|
395
|
+
}
|
|
396
|
+
function jobSummaryType(job) {
|
|
397
|
+
return stringPath(job, ["baseStatus", "def", "type",])
|
|
398
|
+
?? stringPath(job, ["def", "type",])
|
|
399
|
+
?? stringPath(job, ["type",])
|
|
400
|
+
?? "unknown";
|
|
401
|
+
}
|
|
402
|
+
function jobSummaryState(job) {
|
|
403
|
+
return stringPath(job, ["baseStatus", "state",])
|
|
404
|
+
?? stringPath(job, ["state",])
|
|
405
|
+
?? "unknown";
|
|
406
|
+
}
|
|
407
|
+
function filteredJobList(jobs, flags) {
|
|
408
|
+
const state = typeof flags["state"] === "string" ? flags["state"].trim().toUpperCase() : "";
|
|
409
|
+
const contains = typeof flags["contains"] === "string"
|
|
410
|
+
? flags["contains"].trim().toLowerCase()
|
|
411
|
+
: "";
|
|
412
|
+
const output = typeof flags["output"] === "string" ? flags["output"].trim().toLowerCase() : "";
|
|
413
|
+
let result = jobs.filter((job) => {
|
|
414
|
+
if (state && jobSummaryState(job).toUpperCase() !== state)
|
|
415
|
+
return false;
|
|
416
|
+
const text = JSON.stringify(job).toLowerCase();
|
|
417
|
+
if (contains && !text.includes(contains))
|
|
418
|
+
return false;
|
|
419
|
+
if (output && !text.includes(output))
|
|
420
|
+
return false;
|
|
421
|
+
return true;
|
|
422
|
+
});
|
|
423
|
+
const limit = flags["latest"] === true ? 1 : num(flags["limit"]);
|
|
424
|
+
if (limit !== undefined)
|
|
425
|
+
result = result.slice(0, Math.max(0, limit));
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
function maxNumber(values) {
|
|
429
|
+
return values.length === 0 ? 0 : Math.max(...values);
|
|
430
|
+
}
|
|
431
|
+
function collectWarningCounts(value, inActivity, counts) {
|
|
432
|
+
if (Array.isArray(value)) {
|
|
433
|
+
for (const item of value)
|
|
434
|
+
collectWarningCounts(item, inActivity, counts);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const record = plainRecord(value);
|
|
438
|
+
if (!record)
|
|
439
|
+
return;
|
|
440
|
+
for (const [key, item,] of Object.entries(record)) {
|
|
441
|
+
const lower = key.toLowerCase();
|
|
442
|
+
const nextInActivity = inActivity || lower.includes("activit");
|
|
443
|
+
if (lower.includes("warn")) {
|
|
444
|
+
const target = nextInActivity ? counts.activity : counts.dss;
|
|
445
|
+
if (typeof item === "number" && Number.isFinite(item))
|
|
446
|
+
target.push(item);
|
|
447
|
+
else if (Array.isArray(item))
|
|
448
|
+
target.push(item.length);
|
|
449
|
+
}
|
|
450
|
+
collectWarningCounts(item, nextInActivity, counts);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function jobWarningSummary(details, log) {
|
|
454
|
+
const counts = { dss: [], activity: [], };
|
|
455
|
+
collectWarningCounts(details, false, counts);
|
|
456
|
+
const warningLines = log
|
|
457
|
+
? log.split(/\r?\n/).map((line) => line.trim()).filter((line) => /\bwarn(?:ing)?\b/i.test(line))
|
|
458
|
+
: [];
|
|
459
|
+
return {
|
|
460
|
+
dssSummaryWarningCount: maxNumber(counts.dss),
|
|
461
|
+
activityWarningCount: maxNumber(counts.activity),
|
|
462
|
+
logWarnLineCount: warningLines.length,
|
|
463
|
+
sampledWarningMessages: warningLines.slice(0, 5),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function jobDurationMs(details) {
|
|
467
|
+
const started = firstNumberPath(details, [
|
|
468
|
+
["baseStatus", "startTime",],
|
|
469
|
+
["baseStatus", "start",],
|
|
470
|
+
["startTime",],
|
|
471
|
+
["start",],
|
|
472
|
+
]);
|
|
473
|
+
const ended = firstNumberPath(details, [
|
|
474
|
+
["baseStatus", "endTime",],
|
|
475
|
+
["baseStatus", "end",],
|
|
476
|
+
["endTime",],
|
|
477
|
+
["end",],
|
|
478
|
+
]);
|
|
479
|
+
return started !== undefined && ended !== undefined && ended >= started
|
|
480
|
+
? ended - started
|
|
481
|
+
: undefined;
|
|
482
|
+
}
|
|
483
|
+
async function jobInspectionSummary(client, jobId, flags) {
|
|
484
|
+
const projectKey = flags["project-key"];
|
|
485
|
+
const details = await client.jobs.get(jobId, projectKey);
|
|
486
|
+
let log;
|
|
487
|
+
let logError;
|
|
488
|
+
try {
|
|
489
|
+
log = await client.jobs.log(jobId, {
|
|
490
|
+
activity: flags["activity"],
|
|
491
|
+
logId: flags["log-id"],
|
|
492
|
+
maxLogLines: maxLogLinesFromFlags(flags),
|
|
493
|
+
projectKey,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
logError = error instanceof Error ? error.message : String(error);
|
|
498
|
+
}
|
|
499
|
+
const durationMs = jobDurationMs(details);
|
|
500
|
+
const progress = log ? parseJobLogProgress(log, durationMs) : undefined;
|
|
501
|
+
const logLines = log
|
|
502
|
+
? log.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
|
|
503
|
+
: [];
|
|
504
|
+
const maxSummaryLines = Math.max(1, maxLogLinesFromFlags(flags) ?? 20);
|
|
505
|
+
const outputs = nestedValue(details, ["baseStatus", "def", "outputs",])
|
|
506
|
+
?? nestedValue(details, ["def", "outputs",])
|
|
507
|
+
?? details.outputs;
|
|
508
|
+
return {
|
|
509
|
+
resource: "job",
|
|
510
|
+
jobId: jobSummaryId(details, jobId),
|
|
511
|
+
state: jobSummaryState(details),
|
|
512
|
+
type: jobSummaryType(details),
|
|
513
|
+
...(durationMs !== undefined ? { durationMs, } : {}),
|
|
514
|
+
...(outputs !== undefined ? { outputs, } : {}),
|
|
515
|
+
warnings: jobWarningSummary(details, log),
|
|
516
|
+
...(progress
|
|
517
|
+
? {
|
|
518
|
+
progress,
|
|
519
|
+
latestUsefulProgressLine: progress.lastProgressLine,
|
|
520
|
+
doneLine: progress.doneLine,
|
|
521
|
+
}
|
|
522
|
+
: {}),
|
|
523
|
+
logSummary: {
|
|
524
|
+
lineCount: logLines.length,
|
|
525
|
+
lines: logLines.slice(-maxSummaryLines),
|
|
526
|
+
...(logError ? { error: logError, } : {}),
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function stripUtf8Bom(text) {
|
|
531
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
532
|
+
}
|
|
533
|
+
function parseJsonValue(text, source) {
|
|
534
|
+
try {
|
|
535
|
+
return JSON.parse(stripUtf8Bom(text));
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
539
|
+
throw new UsageError(`Invalid JSON in ${source}: ${message}`, "validation_failed");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function expectJsonObject(value, source) {
|
|
543
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
throw new UsageError(`Expected JSON object in ${source}.`, "validation_failed");
|
|
547
|
+
}
|
|
548
|
+
function parseJsonObject(text, source) {
|
|
549
|
+
return expectJsonObject(parseJsonValue(text, source), source);
|
|
550
|
+
}
|
|
551
|
+
function json(v, source = "JSON flag") {
|
|
166
552
|
if (typeof v !== "string")
|
|
167
553
|
return undefined;
|
|
168
|
-
return
|
|
554
|
+
return parseJsonObject(v, source);
|
|
169
555
|
}
|
|
170
556
|
const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--output PATH|--output-file PATH] [--request-timeout MS] [--project-key KEY]";
|
|
171
557
|
function readStdinText() {
|
|
172
558
|
return readFileSync(0, "utf-8");
|
|
173
559
|
}
|
|
174
560
|
function jsonInput(flags) {
|
|
175
|
-
if (flags["stdin"] === true)
|
|
176
|
-
return
|
|
177
|
-
}
|
|
561
|
+
if (flags["stdin"] === true)
|
|
562
|
+
return parseJsonObject(readStdinText(), "stdin");
|
|
178
563
|
if (typeof flags["data-file"] === "string") {
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
if (typeof flags["data"] === "string") {
|
|
182
|
-
return JSON.parse(flags["data"]);
|
|
564
|
+
return parseJsonObject(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
|
|
183
565
|
}
|
|
566
|
+
if (typeof flags["data"] === "string")
|
|
567
|
+
return parseJsonObject(flags["data"], "--data");
|
|
184
568
|
return undefined;
|
|
185
569
|
}
|
|
186
570
|
function unknownJsonInput(flags) {
|
|
187
571
|
if (flags["stdin"] === true)
|
|
188
|
-
return
|
|
572
|
+
return parseJsonValue(readStdinText(), "stdin");
|
|
189
573
|
if (typeof flags["data-file"] === "string") {
|
|
190
|
-
return
|
|
574
|
+
return parseJsonValue(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
|
|
191
575
|
}
|
|
192
576
|
if (typeof flags["data"] === "string")
|
|
193
|
-
return
|
|
577
|
+
return parseJsonValue(flags["data"], "--data");
|
|
194
578
|
return undefined;
|
|
195
579
|
}
|
|
196
580
|
function schemaColumnsInput(flags, usage) {
|
|
@@ -234,7 +618,7 @@ function textInput(flags) {
|
|
|
234
618
|
}
|
|
235
619
|
function requiredJsonInput(flags, message) {
|
|
236
620
|
const data = jsonInput(flags);
|
|
237
|
-
if (
|
|
621
|
+
if (data === undefined)
|
|
238
622
|
throw new UsageError(message);
|
|
239
623
|
return data;
|
|
240
624
|
}
|
|
@@ -400,7 +784,11 @@ function isFailedWaitResult(result) {
|
|
|
400
784
|
&& (typeof record.state === "string" || typeof record.outcome === "string");
|
|
401
785
|
}
|
|
402
786
|
function commandFailureExitCode(result) {
|
|
403
|
-
|
|
787
|
+
if (isFailedWaitResult(result))
|
|
788
|
+
return 4;
|
|
789
|
+
if (result && typeof result === "object" && result.unchanged === false)
|
|
790
|
+
return 4;
|
|
791
|
+
return undefined;
|
|
404
792
|
}
|
|
405
793
|
function isNotFoundError(error) {
|
|
406
794
|
if (error instanceof DataikuError)
|
|
@@ -462,7 +850,7 @@ function resultRecord(result) {
|
|
|
462
850
|
: {};
|
|
463
851
|
}
|
|
464
852
|
function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
465
|
-
if (!(action.startsWith("create") || action === "upload"))
|
|
853
|
+
if (!(action.startsWith("create") || action === "clone" || action === "upload"))
|
|
466
854
|
return undefined;
|
|
467
855
|
const record = resultRecord(result);
|
|
468
856
|
if (record.skipped !== undefined)
|
|
@@ -482,6 +870,16 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
|
482
870
|
cleanup: { argv: ["dataset", "delete", name, "--if-exists", ...withProject,], },
|
|
483
871
|
};
|
|
484
872
|
}
|
|
873
|
+
case "dataset.clone": {
|
|
874
|
+
const name = stringField(record, ["target", "created", "name",]) ?? args[1];
|
|
875
|
+
if (!name)
|
|
876
|
+
return undefined;
|
|
877
|
+
return {
|
|
878
|
+
...base,
|
|
879
|
+
name,
|
|
880
|
+
cleanup: { argv: ["dataset", "delete", name, "--if-exists", ...withProject,], },
|
|
881
|
+
};
|
|
882
|
+
}
|
|
485
883
|
case "recipe.create": {
|
|
486
884
|
const name = stringField(record, ["created", "recipeName", "name",])
|
|
487
885
|
?? flags["name"];
|
|
@@ -493,6 +891,17 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
|
493
891
|
cleanup: { argv: ["recipe", "delete", name, "--if-exists", ...withProject,], },
|
|
494
892
|
};
|
|
495
893
|
}
|
|
894
|
+
case "recipe.clone": {
|
|
895
|
+
const name = stringField(record, ["recipeName", "target", "created", "name",])
|
|
896
|
+
?? flags["name"];
|
|
897
|
+
if (!name)
|
|
898
|
+
return undefined;
|
|
899
|
+
return {
|
|
900
|
+
...base,
|
|
901
|
+
name,
|
|
902
|
+
cleanup: { argv: ["recipe", "delete", name, "--if-exists", ...withProject,], },
|
|
903
|
+
};
|
|
904
|
+
}
|
|
496
905
|
case "scenario.create": {
|
|
497
906
|
const id = args[0];
|
|
498
907
|
return {
|
|
@@ -606,6 +1015,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
606
1015
|
"global",
|
|
607
1016
|
"list-agents",
|
|
608
1017
|
"include-raw",
|
|
1018
|
+
"raw",
|
|
609
1019
|
"include-payload",
|
|
610
1020
|
"no-payload",
|
|
611
1021
|
"include-logs",
|
|
@@ -624,7 +1034,12 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
624
1034
|
"report-json",
|
|
625
1035
|
"no-wait",
|
|
626
1036
|
"force-rebuild",
|
|
1037
|
+
"latest",
|
|
1038
|
+
"copy-output-settings",
|
|
627
1039
|
"continue-on-error",
|
|
1040
|
+
"no-backup",
|
|
1041
|
+
"payload-only",
|
|
1042
|
+
"allow-same-path",
|
|
628
1043
|
]);
|
|
629
1044
|
const SHORT_FLAGS = {
|
|
630
1045
|
h: "help",
|
|
@@ -639,6 +1054,7 @@ const FLAG_ALIASES = {
|
|
|
639
1054
|
"skip-tls-verify": "insecure",
|
|
640
1055
|
"extra-ca-certs": "ca-cert",
|
|
641
1056
|
explain: "plan",
|
|
1057
|
+
"zone-name": "zone",
|
|
642
1058
|
};
|
|
643
1059
|
const VALUE_FLAGS = new Set([
|
|
644
1060
|
"activity",
|
|
@@ -646,12 +1062,14 @@ const VALUE_FLAGS = new Set([
|
|
|
646
1062
|
"api-key",
|
|
647
1063
|
"build-mode",
|
|
648
1064
|
"backup-dir",
|
|
1065
|
+
"backup",
|
|
649
1066
|
"ca-cert",
|
|
650
1067
|
"catalog",
|
|
651
1068
|
"cell-id",
|
|
652
1069
|
"allow-types",
|
|
653
1070
|
"color",
|
|
654
1071
|
"connection",
|
|
1072
|
+
"contains",
|
|
655
1073
|
"content",
|
|
656
1074
|
"content-type",
|
|
657
1075
|
"data",
|
|
@@ -665,6 +1083,7 @@ const VALUE_FLAGS = new Set([
|
|
|
665
1083
|
"install-core-packages",
|
|
666
1084
|
"folder",
|
|
667
1085
|
"input",
|
|
1086
|
+
"from",
|
|
668
1087
|
"knowledge-bank",
|
|
669
1088
|
"labeling-task",
|
|
670
1089
|
"lang",
|
|
@@ -677,14 +1096,17 @@ const VALUE_FLAGS = new Set([
|
|
|
677
1096
|
"listed",
|
|
678
1097
|
"max-nodes",
|
|
679
1098
|
"max-rows",
|
|
1099
|
+
"limit",
|
|
680
1100
|
"max-timestamp",
|
|
681
1101
|
"only-monitored",
|
|
682
1102
|
"min-timestamp",
|
|
683
1103
|
"mode",
|
|
684
1104
|
"log-filter",
|
|
1105
|
+
"log-id",
|
|
685
1106
|
"model-evaluation-store",
|
|
686
1107
|
"name",
|
|
687
1108
|
"object",
|
|
1109
|
+
"metastore-table",
|
|
688
1110
|
"output",
|
|
689
1111
|
"output-file",
|
|
690
1112
|
"output-connection",
|
|
@@ -703,18 +1125,38 @@ const VALUE_FLAGS = new Set([
|
|
|
703
1125
|
"retries",
|
|
704
1126
|
"poll-interval",
|
|
705
1127
|
"python-interpreter",
|
|
1128
|
+
"replace-input",
|
|
1129
|
+
"replace-output",
|
|
1130
|
+
"replace-payload-text",
|
|
706
1131
|
"retain",
|
|
707
1132
|
"saved-model",
|
|
708
1133
|
"sql",
|
|
709
1134
|
"schema",
|
|
710
1135
|
"sql-file",
|
|
711
1136
|
"standard",
|
|
1137
|
+
"state",
|
|
712
1138
|
"streaming-endpoint",
|
|
713
1139
|
"target",
|
|
714
1140
|
"target-type",
|
|
715
1141
|
"timeout",
|
|
1142
|
+
"table",
|
|
716
1143
|
"type",
|
|
717
1144
|
"url",
|
|
1145
|
+
"until",
|
|
1146
|
+
"to",
|
|
1147
|
+
"zone",
|
|
1148
|
+
"zone-id",
|
|
1149
|
+
]);
|
|
1150
|
+
const REPEATABLE_VALUE_FLAGS = new Set([
|
|
1151
|
+
"dataset",
|
|
1152
|
+
"folder",
|
|
1153
|
+
"input",
|
|
1154
|
+
"object",
|
|
1155
|
+
"package",
|
|
1156
|
+
"recipe",
|
|
1157
|
+
"replace-input",
|
|
1158
|
+
"replace-output",
|
|
1159
|
+
"replace-payload-text",
|
|
718
1160
|
]);
|
|
719
1161
|
const KNOWN_LONG_FLAGS = new Set([
|
|
720
1162
|
...BOOLEAN_FLAGS,
|
|
@@ -738,6 +1180,14 @@ function requireFlagValue(flagLabel, next) {
|
|
|
738
1180
|
}
|
|
739
1181
|
return next;
|
|
740
1182
|
}
|
|
1183
|
+
function setParsedFlagValue(flags, flagName, value) {
|
|
1184
|
+
const current = flags[flagName];
|
|
1185
|
+
if (REPEATABLE_VALUE_FLAGS.has(flagName) && typeof current === "string" && current.length > 0) {
|
|
1186
|
+
flags[flagName] = `${current},${value}`;
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
flags[flagName] = value;
|
|
1190
|
+
}
|
|
741
1191
|
function parseArgs(argv) {
|
|
742
1192
|
const positional = [];
|
|
743
1193
|
const flags = {};
|
|
@@ -753,7 +1203,7 @@ function parseArgs(argv) {
|
|
|
753
1203
|
if (eqIdx !== -1) {
|
|
754
1204
|
const raw = arg.slice(2, eqIdx);
|
|
755
1205
|
const flagName = normalizeLongFlag(raw);
|
|
756
|
-
flags
|
|
1206
|
+
setParsedFlagValue(flags, flagName, arg.slice(eqIdx + 1));
|
|
757
1207
|
}
|
|
758
1208
|
else {
|
|
759
1209
|
const rawFlagName = arg.slice(2);
|
|
@@ -763,7 +1213,7 @@ function parseArgs(argv) {
|
|
|
763
1213
|
}
|
|
764
1214
|
else {
|
|
765
1215
|
const next = requireFlagValue(`--${rawFlagName}`, argv[i + 1]);
|
|
766
|
-
flags
|
|
1216
|
+
setParsedFlagValue(flags, flagName, next);
|
|
767
1217
|
i++;
|
|
768
1218
|
}
|
|
769
1219
|
}
|
|
@@ -776,7 +1226,7 @@ function parseArgs(argv) {
|
|
|
776
1226
|
}
|
|
777
1227
|
else {
|
|
778
1228
|
const next = requireFlagValue(`-${arg[1]}`, argv[i + 1]);
|
|
779
|
-
flags
|
|
1229
|
+
setParsedFlagValue(flags, long, next);
|
|
780
1230
|
i++;
|
|
781
1231
|
}
|
|
782
1232
|
}
|
|
@@ -1485,10 +1935,45 @@ const commands = {
|
|
|
1485
1935
|
},
|
|
1486
1936
|
"flow-zone": {
|
|
1487
1937
|
list: {
|
|
1488
|
-
handler: (c, _a, f) =>
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1938
|
+
handler: async (c, _a, f) => {
|
|
1939
|
+
const zones = await c.flowZones.list(f["project-key"]);
|
|
1940
|
+
if (f["summary"] !== true)
|
|
1941
|
+
return zones;
|
|
1942
|
+
const objects = flowZoneMoveItems(f);
|
|
1943
|
+
const object = objects.length === 1 ? objects[0] : undefined;
|
|
1944
|
+
return zones.map((zone) => flowZoneSummary(zone, object));
|
|
1945
|
+
},
|
|
1946
|
+
usage: "dss flow-zone list [--summary] [--object TYPE:ID] [--project-key KEY]",
|
|
1947
|
+
description: "List flow zones in a project, optionally as compact summaries.",
|
|
1948
|
+
examples: ["dss flow-zone list", "dss flow-zone list --summary --object RECIPE:compute_orders",],
|
|
1949
|
+
},
|
|
1950
|
+
find: {
|
|
1951
|
+
handler: async (c, a, f) => {
|
|
1952
|
+
const zones = await c.flowZones.list(f["project-key"]);
|
|
1953
|
+
const objects = flowZoneMoveItems(f);
|
|
1954
|
+
const query = a[0]?.trim();
|
|
1955
|
+
if (query && objects.length === 0) {
|
|
1956
|
+
const normalized = query.toLowerCase();
|
|
1957
|
+
return zones
|
|
1958
|
+
.filter((zone) => zone.id.toLowerCase().includes(normalized)
|
|
1959
|
+
|| zone.name.toLowerCase().includes(normalized))
|
|
1960
|
+
.map((zone) => flowZoneDetailSummary(zone));
|
|
1961
|
+
}
|
|
1962
|
+
if (objects.length !== 1) {
|
|
1963
|
+
throw new UsageError("Exactly one zone name/id or object is required. Use <name>, --object TYPE:ID, --dataset DS, or --recipe R.");
|
|
1964
|
+
}
|
|
1965
|
+
const object = objects[0];
|
|
1966
|
+
return zones
|
|
1967
|
+
.filter((zone) => flowZoneContains(zone, object))
|
|
1968
|
+
.map((zone) => flowZoneSummary(zone, object));
|
|
1969
|
+
},
|
|
1970
|
+
usage: "dss flow-zone find [name-or-id] [--object TYPE:ID | --dataset DS | --recipe R | --folder F] [--project-key KEY]",
|
|
1971
|
+
description: "Find flow zones by name/id or by contained flow object.",
|
|
1972
|
+
examples: [
|
|
1973
|
+
"dss flow-zone find ATH_SNW_MAP_FRG49",
|
|
1974
|
+
"dss flow-zone find --object RECIPE:compute_orders",
|
|
1975
|
+
"dss flow-zone find --dataset orders",
|
|
1976
|
+
],
|
|
1492
1977
|
},
|
|
1493
1978
|
get: {
|
|
1494
1979
|
handler: (c, a, f) => {
|
|
@@ -1583,7 +2068,11 @@ const commands = {
|
|
|
1583
2068
|
},
|
|
1584
2069
|
move: {
|
|
1585
2070
|
handler: async (c, a, f) => {
|
|
1586
|
-
|
|
2071
|
+
const pk = f["project-key"];
|
|
2072
|
+
const zoneId = a[0] ? flowZoneId(a[0]) : await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2073
|
+
if (!zoneId) {
|
|
2074
|
+
throw new UsageError("A zone id or --zone/--zone-id is required. Usage: dss flow-zone move <id> [--dataset DS] [--recipe R] [--folder F] [--object TYPE:ID]");
|
|
2075
|
+
}
|
|
1587
2076
|
const items = flowZoneMoveItems(f);
|
|
1588
2077
|
if (items.length === 0) {
|
|
1589
2078
|
throw new UsageError("At least one object is required. Use --dataset, --recipe, --folder, or --object TYPE:ID.");
|
|
@@ -1593,17 +2082,17 @@ const commands = {
|
|
|
1593
2082
|
dryRun: true,
|
|
1594
2083
|
action: "move",
|
|
1595
2084
|
resource: "flow-zone",
|
|
1596
|
-
id:
|
|
2085
|
+
id: zoneId,
|
|
1597
2086
|
items,
|
|
1598
2087
|
};
|
|
1599
2088
|
}
|
|
1600
|
-
return c.flowZones.moveItems(
|
|
2089
|
+
return c.flowZones.moveItems(zoneId, items, pk);
|
|
1601
2090
|
},
|
|
1602
|
-
usage: "dss flow-zone move
|
|
1603
|
-
description: "Move datasets, recipes, managed folders, or other flow objects into a zone.",
|
|
2091
|
+
usage: "dss flow-zone move [id] [--zone ZONE|--zone-id ID] [--dataset DS[,DS2]] [--recipe R] [--folder F] [--object TYPE:ID] [--dry-run] [--project-key KEY]",
|
|
2092
|
+
description: "Move datasets, recipes, managed folders, or other flow objects into a zone by id or --zone name.",
|
|
1604
2093
|
examples: [
|
|
1605
2094
|
"dss flow-zone move ZONE_ID --dataset orders --dry-run",
|
|
1606
|
-
"dss flow-zone move
|
|
2095
|
+
"dss flow-zone move --zone ATH_SNW_MAP_FRG49 --dataset raw_orders,clean_orders --recipe prepare_orders",
|
|
1607
2096
|
"dss flow-zone move ZONE_ID --folder FOLDER_ID",
|
|
1608
2097
|
"dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
|
|
1609
2098
|
],
|
|
@@ -1643,6 +2132,15 @@ const commands = {
|
|
|
1643
2132
|
description: "Show the column schema of a dataset.",
|
|
1644
2133
|
examples: ["dss dataset schema orders",],
|
|
1645
2134
|
},
|
|
2135
|
+
source: {
|
|
2136
|
+
handler: async (c, a, f) => {
|
|
2137
|
+
requireArgs(a, 1, "dss dataset source <name>");
|
|
2138
|
+
return datasetSourceSummary(await c.datasets.get(a[0], f["project-key"]));
|
|
2139
|
+
},
|
|
2140
|
+
usage: "dss dataset source <name> [--project-key KEY]",
|
|
2141
|
+
description: "Show backing connection, catalog/schema/table, path, and format for a dataset.",
|
|
2142
|
+
examples: ["dss dataset source orders",],
|
|
2143
|
+
},
|
|
1646
2144
|
"refresh-schema": {
|
|
1647
2145
|
handler: async (c, a, f) => {
|
|
1648
2146
|
const usage = "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]";
|
|
@@ -1727,6 +2225,7 @@ const commands = {
|
|
|
1727
2225
|
dsType,
|
|
1728
2226
|
projectKey: pk,
|
|
1729
2227
|
};
|
|
2228
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
1730
2229
|
if (f["if-not-exists"] === true || f["dry-run"] === true) {
|
|
1731
2230
|
const list = await c.datasets.list(pk);
|
|
1732
2231
|
const existing = list.find((d) => d.name === name);
|
|
@@ -1741,17 +2240,57 @@ const commands = {
|
|
|
1741
2240
|
name,
|
|
1742
2241
|
payload,
|
|
1743
2242
|
...(existing ? { current: existing, } : {}),
|
|
2243
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "DATASET", },], } : {}),
|
|
1744
2244
|
};
|
|
1745
2245
|
}
|
|
1746
2246
|
}
|
|
1747
2247
|
await c.datasets.create(payload);
|
|
1748
|
-
|
|
2248
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "DATASET", },], pk);
|
|
2249
|
+
return { created: name, resource: "dataset", ...moved, };
|
|
1749
2250
|
},
|
|
1750
|
-
usage: "dss dataset create --name NAME --connection CONN --type TYPE [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
2251
|
+
usage: "dss dataset create --name NAME --connection CONN --type TYPE [--zone ZONE|--zone-id ID] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
1751
2252
|
description: "Create a new dataset.",
|
|
1752
2253
|
examples: [
|
|
1753
2254
|
"dss dataset create --name orders --connection filesystem --type Filesystem",
|
|
1754
|
-
"dss dataset create --name orders --connection filesystem --type Filesystem --dry-run",
|
|
2255
|
+
"dss dataset create --name orders --connection filesystem --type Filesystem --zone Experiments --dry-run",
|
|
2256
|
+
],
|
|
2257
|
+
},
|
|
2258
|
+
clone: {
|
|
2259
|
+
handler: async (c, a, f) => {
|
|
2260
|
+
const usage = "dss dataset clone <source> <target> [--path PATH] [--table TABLE] [--metastore-table TABLE] [--allow-same-path] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]";
|
|
2261
|
+
requireArgs(a, 2, usage);
|
|
2262
|
+
const pk = f["project-key"];
|
|
2263
|
+
const opts = {
|
|
2264
|
+
projectKey: pk,
|
|
2265
|
+
path: f["path"],
|
|
2266
|
+
table: f["table"],
|
|
2267
|
+
metastoreTableName: f["metastore-table"],
|
|
2268
|
+
allowSamePath: f["allow-same-path"] === true,
|
|
2269
|
+
};
|
|
2270
|
+
const current = await c.datasets.get(a[0], pk);
|
|
2271
|
+
const next = buildDatasetCloneSettings(current, a[1], pk ?? c.resolveProjectKey(pk), opts);
|
|
2272
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2273
|
+
if (f["dry-run"] === true) {
|
|
2274
|
+
return {
|
|
2275
|
+
dryRun: true,
|
|
2276
|
+
action: "clone",
|
|
2277
|
+
resource: "dataset",
|
|
2278
|
+
source: a[0],
|
|
2279
|
+
target: a[1],
|
|
2280
|
+
current,
|
|
2281
|
+
next,
|
|
2282
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: a[1], objectType: "DATASET", },], } : {}),
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
const cloned = await c.datasets.clone(a[0], a[1], opts);
|
|
2286
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: a[1], objectType: "DATASET", },], pk);
|
|
2287
|
+
return { ...cloned, resource: "dataset", ...moved, };
|
|
2288
|
+
},
|
|
2289
|
+
usage: "dss dataset clone <source> <target> [--path PATH] [--table TABLE] [--metastore-table TABLE] [--allow-same-path] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]",
|
|
2290
|
+
description: "Clone dataset settings into a new dataset, with storage/table overrides.",
|
|
2291
|
+
examples: [
|
|
2292
|
+
"dss dataset clone source_ds experiment_ds --path /dataiku/TEST/experiment_ds --dry-run",
|
|
2293
|
+
"dss dataset clone source_ds experiment_ds --allow-same-path",
|
|
1755
2294
|
],
|
|
1756
2295
|
},
|
|
1757
2296
|
delete: {
|
|
@@ -1942,15 +2481,20 @@ const commands = {
|
|
|
1942
2481
|
}
|
|
1943
2482
|
const name = f["name"];
|
|
1944
2483
|
const pk = f["project-key"];
|
|
2484
|
+
const inputDatasets = recipeInputDatasetsFromFlags(f);
|
|
1945
2485
|
const payload = {
|
|
1946
2486
|
type,
|
|
1947
2487
|
name,
|
|
1948
|
-
inputDatasets
|
|
2488
|
+
inputDatasets,
|
|
1949
2489
|
outputDataset,
|
|
1950
2490
|
outputFolder,
|
|
1951
2491
|
outputConnection: f["output-connection"],
|
|
1952
2492
|
projectKey: pk,
|
|
1953
2493
|
};
|
|
2494
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2495
|
+
const zoneMove = zoneId && name
|
|
2496
|
+
? [{ objectId: name, objectType: "RECIPE", },]
|
|
2497
|
+
: undefined;
|
|
1954
2498
|
if ((f["if-not-exists"] === true || f["dry-run"] === true) && name) {
|
|
1955
2499
|
const list = await c.recipes.list(pk);
|
|
1956
2500
|
const existing = list.find((r) => r.name === name);
|
|
@@ -1964,24 +2508,106 @@ const commands = {
|
|
|
1964
2508
|
resource: "recipe",
|
|
1965
2509
|
name,
|
|
1966
2510
|
payload,
|
|
2511
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
1967
2512
|
...(existing ? { current: existing, } : {}),
|
|
1968
2513
|
};
|
|
1969
2514
|
}
|
|
1970
2515
|
}
|
|
1971
2516
|
if (f["dry-run"] === true) {
|
|
1972
|
-
return {
|
|
2517
|
+
return {
|
|
2518
|
+
dryRun: true,
|
|
2519
|
+
action: "create",
|
|
2520
|
+
resource: "recipe",
|
|
2521
|
+
payload,
|
|
2522
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
2523
|
+
};
|
|
1973
2524
|
}
|
|
1974
2525
|
const created = await c.recipes.create(payload);
|
|
1975
|
-
|
|
2526
|
+
const createdName = created.recipeName;
|
|
2527
|
+
const moved = await moveCreatedItemsToZone(c, f, [{
|
|
2528
|
+
objectId: createdName,
|
|
2529
|
+
objectType: "RECIPE",
|
|
2530
|
+
},], pk);
|
|
2531
|
+
return { created: createdName, resource: "recipe", ...created, ...moved, };
|
|
1976
2532
|
},
|
|
1977
|
-
usage: "dss recipe create --type TYPE --input DS (--output DS | --output-folder FOLDER_ID) [--name NAME] [--output-connection CONN] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
1978
|
-
description: "Create a recipe with
|
|
2533
|
+
usage: "dss recipe create --type TYPE --input DS[,DS2] (--output DS | --output-folder FOLDER_ID) [--name NAME] [--output-connection CONN] [--zone ZONE|--zone-id ID] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
2534
|
+
description: "Create a recipe with one or more inputs and a dataset or managed-folder output.",
|
|
1979
2535
|
examples: [
|
|
1980
|
-
"dss recipe create --type python --input
|
|
1981
|
-
"dss recipe create --type python --input orders --output orders_clean --
|
|
2536
|
+
"dss recipe create --type python --input raw_orders,lookup --output orders_clean",
|
|
2537
|
+
"dss recipe create --type python --input orders --input customers --output orders_clean --zone Experiments",
|
|
1982
2538
|
"dss recipe create --type python --input orders --output-folder LT7TUHJ8 --output-connection filesystem --dry-run",
|
|
1983
2539
|
],
|
|
1984
2540
|
},
|
|
2541
|
+
clone: {
|
|
2542
|
+
handler: async (c, a, f) => {
|
|
2543
|
+
const usage = "dss recipe clone [source|--from SOURCE] (--name NAME|--to NAME) [--replace-input FROM=TO] [--replace-output FROM=TO] [--replace-payload-text FROM=TO] [--output DATASET] [--copy-output-settings] [--path PATH] [--metastore-table TABLE] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]";
|
|
2544
|
+
const fromFlag = typeof f["from"] === "string" ? f["from"].trim() : "";
|
|
2545
|
+
const sourceName = a[0] ?? fromFlag;
|
|
2546
|
+
if (!sourceName) {
|
|
2547
|
+
throw new UsageError(`Source recipe is required. Usage: ${usage}`, "missing_required_flag");
|
|
2548
|
+
}
|
|
2549
|
+
if (a[0] && fromFlag && a[0] !== fromFlag) {
|
|
2550
|
+
throw new UsageError("Positional source and --from must match when both are provided.", "invalid_enum");
|
|
2551
|
+
}
|
|
2552
|
+
const pk = f["project-key"];
|
|
2553
|
+
const toFlag = typeof f["to"] === "string" ? f["to"].trim() : "";
|
|
2554
|
+
const nameFlag = typeof f["name"] === "string" ? f["name"].trim() : "";
|
|
2555
|
+
const name = toFlag || nameFlag;
|
|
2556
|
+
if (!name) {
|
|
2557
|
+
throw new UsageError(`--name or --to is required. Usage: ${usage}`, "missing_required_flag");
|
|
2558
|
+
}
|
|
2559
|
+
const inputRewrites = rewritePairsFromFlags(f, "replace-input");
|
|
2560
|
+
const outputRewrites = rewritePairsFromFlags(f, "replace-output");
|
|
2561
|
+
const payloadTextRewrites = rewritePairsFromFlags(f, "replace-payload-text");
|
|
2562
|
+
const opts = {
|
|
2563
|
+
projectKey: pk,
|
|
2564
|
+
name,
|
|
2565
|
+
outputDataset: f["output"],
|
|
2566
|
+
outputRewrites,
|
|
2567
|
+
inputRewrites,
|
|
2568
|
+
payloadTextRewrites,
|
|
2569
|
+
copyOutputSettings: f["copy-output-settings"] === true,
|
|
2570
|
+
outputPath: f["path"],
|
|
2571
|
+
metastoreTableName: f["metastore-table"],
|
|
2572
|
+
};
|
|
2573
|
+
const source = await c.recipes.get(sourceName, { includePayload: true, projectKey: pk, });
|
|
2574
|
+
const outputItems = Object.values((source.recipe.outputs ?? {})).flatMap((role) => role.items ?? []).filter((item) => typeof item.ref === "string");
|
|
2575
|
+
const plannedOutputRewrites = { ...outputRewrites, };
|
|
2576
|
+
if (opts.outputDataset !== undefined && outputItems.length === 1) {
|
|
2577
|
+
plannedOutputRewrites[outputItems[0].ref] = opts.outputDataset;
|
|
2578
|
+
}
|
|
2579
|
+
if (opts.copyOutputSettings === true
|
|
2580
|
+
&& Object.keys(plannedOutputRewrites).length > 1
|
|
2581
|
+
&& (opts.outputPath !== undefined || opts.metastoreTableName !== undefined)) {
|
|
2582
|
+
throw new UsageError("Cannot reuse --path or --metastore-table for multiple cloned output datasets.", "invalid_enum");
|
|
2583
|
+
}
|
|
2584
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2585
|
+
if (f["dry-run"] === true) {
|
|
2586
|
+
return {
|
|
2587
|
+
dryRun: true,
|
|
2588
|
+
action: "clone",
|
|
2589
|
+
resource: "recipe",
|
|
2590
|
+
source: sourceName,
|
|
2591
|
+
target: name,
|
|
2592
|
+
inputRewrites,
|
|
2593
|
+
outputRewrites: plannedOutputRewrites,
|
|
2594
|
+
copyOutputSettings: opts.copyOutputSettings,
|
|
2595
|
+
payloadTextRewrites,
|
|
2596
|
+
current: source,
|
|
2597
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "RECIPE", },], } : {}),
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
const cloned = await c.recipes.clone(sourceName, opts);
|
|
2601
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "RECIPE", },], pk);
|
|
2602
|
+
return { ...cloned, resource: "recipe", ...moved, };
|
|
2603
|
+
},
|
|
2604
|
+
usage: "dss recipe clone [source|--from SOURCE] (--name NAME|--to NAME) [--replace-input FROM=TO] [--replace-output FROM=TO] [--replace-payload-text FROM=TO] [--output DATASET] [--copy-output-settings] [--path PATH] [--metastore-table TABLE] [--zone ZONE|--zone-id ID] [--dry-run] [--project-key KEY]",
|
|
2605
|
+
description: "Clone a recipe graph/settings/payload into a separate experiment recipe.",
|
|
2606
|
+
examples: [
|
|
2607
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --copy-output-settings --dry-run",
|
|
2608
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --zone Experiments",
|
|
2609
|
+
],
|
|
2610
|
+
},
|
|
1985
2611
|
diff: {
|
|
1986
2612
|
handler: async (c, a, f) => {
|
|
1987
2613
|
requireArgs(a, 1, "dss recipe diff <name> --file PATH");
|
|
@@ -2046,13 +2672,24 @@ const commands = {
|
|
|
2046
2672
|
}
|
|
2047
2673
|
return payload;
|
|
2048
2674
|
},
|
|
2049
|
-
usage: "dss recipe get-payload <name> [--output PATH] [--project-key KEY]",
|
|
2050
|
-
description: "Print the recipe code payload to stdout.",
|
|
2675
|
+
usage: "dss recipe get-payload <name> [--raw] [--output PATH] [--project-key KEY]",
|
|
2676
|
+
description: "Print the recipe code payload to stdout; use --raw for pipeable code bytes.",
|
|
2051
2677
|
examples: [
|
|
2052
|
-
"dss recipe get-payload compute_orders",
|
|
2678
|
+
"dss recipe get-payload compute_orders --raw",
|
|
2053
2679
|
"dss recipe get-payload compute_orders -o code.py",
|
|
2054
2680
|
],
|
|
2055
2681
|
},
|
|
2682
|
+
cat: {
|
|
2683
|
+
handler: (c, a, f) => {
|
|
2684
|
+
requireArgs(a, 1, "dss recipe cat <name> [--raw]");
|
|
2685
|
+
return c.recipes.getPayload(a[0], {
|
|
2686
|
+
projectKey: f["project-key"],
|
|
2687
|
+
});
|
|
2688
|
+
},
|
|
2689
|
+
usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
|
|
2690
|
+
description: "Print a recipe code payload; combine with --raw for shell pipes and diffs.",
|
|
2691
|
+
examples: ["dss recipe cat compute_orders --raw",],
|
|
2692
|
+
},
|
|
2056
2693
|
"set-payload": {
|
|
2057
2694
|
handler: async (c, a, f) => {
|
|
2058
2695
|
requireArgs(a, 1, "dss recipe set-payload <name> --file PATH");
|
|
@@ -2060,13 +2697,17 @@ const commands = {
|
|
|
2060
2697
|
if (!filePath)
|
|
2061
2698
|
throw new UsageError("--file is required.");
|
|
2062
2699
|
const content = readFileSync(filePath, "utf-8");
|
|
2063
|
-
const
|
|
2064
|
-
const
|
|
2700
|
+
const pk = f["project-key"];
|
|
2701
|
+
const shouldBackup = f["no-backup"] !== true;
|
|
2702
|
+
const backupDir = shouldBackup
|
|
2703
|
+
? f["backup-dir"] ?? join(process.cwd(), ".dss-backups", "recipes")
|
|
2704
|
+
: undefined;
|
|
2705
|
+
const backupPath = backupDir ? recipeBackupPath(a[0], backupDir) : undefined;
|
|
2706
|
+
const current = await c.recipes.get(a[0], {
|
|
2707
|
+
projectKey: pk,
|
|
2708
|
+
includePayload: true,
|
|
2709
|
+
});
|
|
2065
2710
|
if (f["dry-run"] === true) {
|
|
2066
|
-
const current = await c.recipes.get(a[0], {
|
|
2067
|
-
projectKey: f["project-key"],
|
|
2068
|
-
includePayload: true,
|
|
2069
|
-
});
|
|
2070
2711
|
return {
|
|
2071
2712
|
dryRun: true,
|
|
2072
2713
|
action: "set-payload",
|
|
@@ -2075,41 +2716,140 @@ const commands = {
|
|
|
2075
2716
|
file: filePath,
|
|
2076
2717
|
current,
|
|
2077
2718
|
next: { ...current, payload: content, },
|
|
2078
|
-
...(backupPath ? { backupPath, } : {}),
|
|
2719
|
+
...(backupPath ? { backupPath, backup: recipeBackupDocument(a[0], pk, current), } : {}),
|
|
2079
2720
|
};
|
|
2080
2721
|
}
|
|
2081
2722
|
if (backupDir && backupPath) {
|
|
2082
|
-
const current = await c.recipes.get(a[0], {
|
|
2083
|
-
projectKey: f["project-key"],
|
|
2084
|
-
includePayload: true,
|
|
2085
|
-
});
|
|
2086
2723
|
await mkdir(backupDir, { recursive: true, });
|
|
2087
|
-
await writeFile(backupPath,
|
|
2724
|
+
await writeFile(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`, "utf-8");
|
|
2088
2725
|
}
|
|
2089
|
-
await c.recipes.
|
|
2090
|
-
projectKey: f["project-key"],
|
|
2091
|
-
});
|
|
2726
|
+
await c.recipes.replace(a[0], { ...current, payload: content, }, pk);
|
|
2092
2727
|
return {
|
|
2093
2728
|
updated: a[0],
|
|
2094
2729
|
resource: "recipe",
|
|
2095
2730
|
file: filePath,
|
|
2731
|
+
backupCreated: backupPath !== undefined,
|
|
2096
2732
|
...(backupPath ? { backupPath, } : {}),
|
|
2097
2733
|
};
|
|
2098
2734
|
},
|
|
2099
|
-
usage: "dss recipe set-payload <name> --file PATH [--backup-dir DIR] [--dry-run] [--project-key KEY]",
|
|
2100
|
-
description: "Upload recipe code from a local file,
|
|
2735
|
+
usage: "dss recipe set-payload <name> --file PATH [--backup-dir DIR|--no-backup] [--dry-run] [--project-key KEY]",
|
|
2736
|
+
description: "Upload recipe code from a local file, backing up payload, graph, settings, and version metadata by default.",
|
|
2101
2737
|
examples: [
|
|
2102
2738
|
"dss recipe set-payload compute_orders --file code.py --dry-run",
|
|
2103
2739
|
"dss recipe set-payload compute_orders --file code.py --backup-dir ./backups",
|
|
2740
|
+
"dss recipe set-payload compute_orders --file code.py --no-backup",
|
|
2741
|
+
],
|
|
2742
|
+
},
|
|
2743
|
+
restore: {
|
|
2744
|
+
handler: async (c, a, f) => {
|
|
2745
|
+
const usage = "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]";
|
|
2746
|
+
requireArgs(a, 1, usage);
|
|
2747
|
+
const backupPath = requiredStringFlag(f, "backup", usage);
|
|
2748
|
+
const backup = readRecipeBackup(backupPath);
|
|
2749
|
+
const payload = typeof backup.payload === "string" ? backup.payload : "";
|
|
2750
|
+
const pk = f["project-key"];
|
|
2751
|
+
const current = await c.recipes.get(a[0], { includePayload: true, projectKey: pk, });
|
|
2752
|
+
const backupRecipe = backup.recipe && typeof backup.recipe === "object" && !Array.isArray(backup.recipe)
|
|
2753
|
+
? backup.recipe
|
|
2754
|
+
: undefined;
|
|
2755
|
+
const restoredRecipe = backupRecipe
|
|
2756
|
+
? { ...backupRecipe, name: a[0], ...(pk ? { projectKey: pk, } : {}), }
|
|
2757
|
+
: undefined;
|
|
2758
|
+
const next = f["payload-only"] === true || !restoredRecipe
|
|
2759
|
+
? { ...current, payload, }
|
|
2760
|
+
: { ...current, recipe: restoredRecipe, payload, };
|
|
2761
|
+
if (f["dry-run"] === true) {
|
|
2762
|
+
return {
|
|
2763
|
+
dryRun: true,
|
|
2764
|
+
action: "restore",
|
|
2765
|
+
resource: "recipe",
|
|
2766
|
+
name: a[0],
|
|
2767
|
+
backupPath,
|
|
2768
|
+
current,
|
|
2769
|
+
next,
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
await c.recipes.replace(a[0], next, pk);
|
|
2773
|
+
return {
|
|
2774
|
+
restored: a[0],
|
|
2775
|
+
resource: "recipe",
|
|
2776
|
+
backupPath,
|
|
2777
|
+
payloadOnly: f["payload-only"] === true,
|
|
2778
|
+
};
|
|
2779
|
+
},
|
|
2780
|
+
usage: "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]",
|
|
2781
|
+
description: "Restore a recipe from a set-payload backup.",
|
|
2782
|
+
examples: [
|
|
2783
|
+
"dss recipe restore compute_orders --backup .dss-backups/recipes/backup.recipe-backup.json --dry-run",
|
|
2784
|
+
],
|
|
2785
|
+
},
|
|
2786
|
+
"assert-unchanged": {
|
|
2787
|
+
handler: async (c, a, f) => {
|
|
2788
|
+
const usage = "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]";
|
|
2789
|
+
requireArgs(a, 1, usage);
|
|
2790
|
+
const backupPath = requiredStringFlag(f, "since", usage);
|
|
2791
|
+
const backup = readRecipeBackup(backupPath);
|
|
2792
|
+
const current = await c.recipes.get(a[0], {
|
|
2793
|
+
includePayload: true,
|
|
2794
|
+
projectKey: f["project-key"],
|
|
2795
|
+
});
|
|
2796
|
+
const payloadHash = sha256Hex(current.payload ?? "");
|
|
2797
|
+
const normalizedPayloadHash = sha256Hex(normalizeLineEndings(current.payload ?? ""));
|
|
2798
|
+
const expectedPayloadHash = typeof backup.payloadHash === "string"
|
|
2799
|
+
? backup.payloadHash
|
|
2800
|
+
: undefined;
|
|
2801
|
+
const expectedNormalizedPayloadHash = typeof backup.normalizedPayloadHash === "string"
|
|
2802
|
+
? backup.normalizedPayloadHash
|
|
2803
|
+
: typeof backup.payload === "string"
|
|
2804
|
+
? sha256Hex(normalizeLineEndings(backup.payload))
|
|
2805
|
+
: undefined;
|
|
2806
|
+
const checks = [
|
|
2807
|
+
{
|
|
2808
|
+
name: "payload",
|
|
2809
|
+
expected: expectedPayloadHash,
|
|
2810
|
+
actual: payloadHash,
|
|
2811
|
+
unchanged: expectedPayloadHash === payloadHash
|
|
2812
|
+
|| (expectedNormalizedPayloadHash !== undefined
|
|
2813
|
+
&& expectedNormalizedPayloadHash === normalizedPayloadHash),
|
|
2814
|
+
normalizedExpected: expectedNormalizedPayloadHash,
|
|
2815
|
+
normalizedActual: normalizedPayloadHash,
|
|
2816
|
+
},
|
|
2817
|
+
{
|
|
2818
|
+
name: "graph",
|
|
2819
|
+
expected: backup.graphHash,
|
|
2820
|
+
actual: stableHash(recipeGraph(current.recipe)),
|
|
2821
|
+
unchanged: backup.graphHash === stableHash(recipeGraph(current.recipe)),
|
|
2822
|
+
},
|
|
2823
|
+
{
|
|
2824
|
+
name: "codeEnv",
|
|
2825
|
+
expected: backup.codeEnvHash,
|
|
2826
|
+
actual: stableHash(recipeCodeEnv(current.recipe)),
|
|
2827
|
+
unchanged: backup.codeEnvHash === stableHash(recipeCodeEnv(current.recipe)),
|
|
2828
|
+
},
|
|
2829
|
+
].filter((check) => typeof check.expected === "string");
|
|
2830
|
+
const failures = checks.filter((check) => !check.unchanged);
|
|
2831
|
+
return {
|
|
2832
|
+
unchanged: failures.length === 0,
|
|
2833
|
+
resource: "recipe",
|
|
2834
|
+
name: a[0],
|
|
2835
|
+
backupPath,
|
|
2836
|
+
checks,
|
|
2837
|
+
failures,
|
|
2838
|
+
};
|
|
2839
|
+
},
|
|
2840
|
+
usage: "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]",
|
|
2841
|
+
description: "Compare current recipe payload, graph, and code env against a backup.",
|
|
2842
|
+
examples: [
|
|
2843
|
+
"dss recipe assert-unchanged compute_orders --since .dss-backups/recipes/backup.recipe-backup.json",
|
|
2104
2844
|
],
|
|
2105
2845
|
},
|
|
2106
2846
|
},
|
|
2107
2847
|
job: {
|
|
2108
2848
|
list: {
|
|
2109
|
-
handler: (c, _a, f) => c.jobs.list(f["project-key"]),
|
|
2110
|
-
usage: "dss job list [--project-key KEY]",
|
|
2111
|
-
description: "List recent jobs.",
|
|
2112
|
-
examples: ["dss job list",],
|
|
2849
|
+
handler: async (c, _a, f) => filteredJobList(await c.jobs.list(f["project-key"]), f),
|
|
2850
|
+
usage: "dss job list [--state STATE] [--contains TEXT] [--output ID] [--latest] [--limit N] [--project-key KEY]",
|
|
2851
|
+
description: "List recent jobs, optionally filtered for automation.",
|
|
2852
|
+
examples: ["dss job list --state DONE --latest", "dss job list --contains WLM225S --limit 10",],
|
|
2113
2853
|
},
|
|
2114
2854
|
get: {
|
|
2115
2855
|
handler: (c, a, f) => {
|
|
@@ -2120,18 +2860,42 @@ const commands = {
|
|
|
2120
2860
|
description: "Get job details.",
|
|
2121
2861
|
examples: ["dss job get JOB_ID",],
|
|
2122
2862
|
},
|
|
2863
|
+
summary: {
|
|
2864
|
+
handler: (c, a, f) => {
|
|
2865
|
+
requireArgs(a, 1, "dss job summary <id>");
|
|
2866
|
+
return jobInspectionSummary(c, a[0], f);
|
|
2867
|
+
},
|
|
2868
|
+
usage: "dss job summary <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
|
|
2869
|
+
description: "Summarize job state, outputs, warnings, progress, and useful terminal log lines.",
|
|
2870
|
+
examples: ["dss job summary JOB_ID --max-log-lines 200",],
|
|
2871
|
+
},
|
|
2123
2872
|
log: {
|
|
2124
2873
|
handler: (c, a, f) => {
|
|
2125
2874
|
requireArgs(a, 1, "dss job log <id>");
|
|
2126
2875
|
return c.jobs.log(a[0], {
|
|
2127
2876
|
activity: f["activity"],
|
|
2877
|
+
logId: f["log-id"],
|
|
2128
2878
|
maxLogLines: maxLogLinesFromFlags(f),
|
|
2129
2879
|
projectKey: f["project-key"],
|
|
2130
2880
|
});
|
|
2131
2881
|
},
|
|
2132
|
-
usage: "dss job log <id> [--activity
|
|
2133
|
-
description: "Get log output for
|
|
2134
|
-
examples: [
|
|
2882
|
+
usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
|
|
2883
|
+
description: "Get public API job log output. --log-id is accepted for UI parity but DSS API-key auth cannot select browser-only cat-activity-log files.",
|
|
2884
|
+
examples: [
|
|
2885
|
+
"dss job log JOB_ID",
|
|
2886
|
+
"dss job log JOB_ID --activity main --max-log-lines 200",
|
|
2887
|
+
],
|
|
2888
|
+
},
|
|
2889
|
+
"log-url": {
|
|
2890
|
+
handler: (c, a, f) => {
|
|
2891
|
+
requireArgs(a, 1, "dss job log-url <url>");
|
|
2892
|
+
return c.jobs.logFromUrl(a[0], { maxLogLines: maxLogLinesFromFlags(f), });
|
|
2893
|
+
},
|
|
2894
|
+
usage: "dss job log-url <url> [--max-lines N|--max-log-lines N]",
|
|
2895
|
+
description: "Fetch a DSS cat-activity-log URL pasted from the UI.",
|
|
2896
|
+
examples: [
|
|
2897
|
+
'dss job log-url "https://dss/dip/api/flow/jobs/cat-activity-log?projectKey=TEST&jobId=JOB&activityId=A&logId=L"',
|
|
2898
|
+
],
|
|
2135
2899
|
},
|
|
2136
2900
|
build: {
|
|
2137
2901
|
handler: async (c, a, f) => {
|
|
@@ -2225,6 +2989,44 @@ const commands = {
|
|
|
2225
2989
|
"dss job wait JOB_ID --include-logs --log-filter stdout --summary --timeout 60000",
|
|
2226
2990
|
],
|
|
2227
2991
|
},
|
|
2992
|
+
monitor: {
|
|
2993
|
+
handler: async (c, a, f) => {
|
|
2994
|
+
requireArgs(a, 1, "dss job monitor <id...>");
|
|
2995
|
+
const options = {
|
|
2996
|
+
includeLogs: f["include-logs"] === true,
|
|
2997
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
2998
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
2999
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
3000
|
+
timeoutMs: num(f["timeout"]),
|
|
3001
|
+
summary: f["summary"] !== false,
|
|
3002
|
+
projectKey: f["project-key"],
|
|
3003
|
+
};
|
|
3004
|
+
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
3005
|
+
return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
|
|
3006
|
+
},
|
|
3007
|
+
usage: "dss job monitor <id...> [--summary] [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
|
|
3008
|
+
description: "Monitor one or more existing jobs and summarize progress counters from logs.",
|
|
3009
|
+
examples: ["dss job monitor JOB_ID --summary", "dss job monitor JOB1 JOB2 --until all-done",],
|
|
3010
|
+
},
|
|
3011
|
+
watch: {
|
|
3012
|
+
handler: async (c, a, f) => {
|
|
3013
|
+
requireArgs(a, 1, "dss job watch <id...>");
|
|
3014
|
+
const options = {
|
|
3015
|
+
includeLogs: f["include-logs"] === true,
|
|
3016
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
3017
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
3018
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
3019
|
+
timeoutMs: num(f["timeout"]),
|
|
3020
|
+
summary: true,
|
|
3021
|
+
projectKey: f["project-key"],
|
|
3022
|
+
};
|
|
3023
|
+
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
3024
|
+
return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
|
|
3025
|
+
},
|
|
3026
|
+
usage: "dss job watch <id...> [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
|
|
3027
|
+
description: "Watch one or more existing jobs with progress extraction enabled.",
|
|
3028
|
+
examples: ["dss job watch JOB_ID", "dss job watch JOB1 JOB2 --until all-done",],
|
|
3029
|
+
},
|
|
2228
3030
|
abort: {
|
|
2229
3031
|
handler: async (c, a, f) => {
|
|
2230
3032
|
requireArgs(a, 1, "dss job abort <id>");
|
|
@@ -2260,7 +3062,7 @@ const commands = {
|
|
|
2260
3062
|
return c.scenarios.get(a[0], { projectKey: f["project-key"], });
|
|
2261
3063
|
},
|
|
2262
3064
|
usage: "dss scenario get <id> [--project-key KEY]",
|
|
2263
|
-
description: "Get scenario definition.",
|
|
3065
|
+
description: "Get raw scenario definition. For step-based scenario edits, patch params.steps; rawParams.params is DSS echo data.",
|
|
2264
3066
|
examples: ["dss scenario get my_scenario",],
|
|
2265
3067
|
},
|
|
2266
3068
|
run: {
|
|
@@ -2391,21 +3193,46 @@ const commands = {
|
|
|
2391
3193
|
handler: async (c, a, f) => {
|
|
2392
3194
|
requireArgs(a, 1, "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
2393
3195
|
const data = jsonInput(f);
|
|
2394
|
-
if (
|
|
3196
|
+
if (data === undefined) {
|
|
2395
3197
|
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
2396
3198
|
}
|
|
2397
3199
|
const pk = f["project-key"];
|
|
2398
3200
|
if (f["dry-run"] === true) {
|
|
2399
3201
|
const current = await c.scenarios.get(a[0], { projectKey: pk, });
|
|
2400
|
-
const
|
|
2401
|
-
return {
|
|
3202
|
+
const preview = scenarioUpdatePreview(current, data);
|
|
3203
|
+
return {
|
|
3204
|
+
dryRun: true,
|
|
3205
|
+
action: "update",
|
|
3206
|
+
resource: "scenario",
|
|
3207
|
+
id: a[0],
|
|
3208
|
+
canonicalEditableFields: preview.canonicalEditableFields,
|
|
3209
|
+
normalization: preview.normalization,
|
|
3210
|
+
normalizedData: preview.normalizedData,
|
|
3211
|
+
changes: preview.changes,
|
|
3212
|
+
unchangedPaths: preview.unchangedPaths,
|
|
3213
|
+
current: preview.current,
|
|
3214
|
+
next: preview.next,
|
|
3215
|
+
};
|
|
2402
3216
|
}
|
|
2403
|
-
await c.scenarios.update(a[0], data, pk);
|
|
2404
|
-
return {
|
|
3217
|
+
const result = await c.scenarios.update(a[0], data, pk);
|
|
3218
|
+
return {
|
|
3219
|
+
updated: a[0],
|
|
3220
|
+
resource: "scenario",
|
|
3221
|
+
verified: result.verified,
|
|
3222
|
+
changed: result.changes.length > 0,
|
|
3223
|
+
canonicalEditableFields: result.canonicalEditableFields,
|
|
3224
|
+
normalization: result.normalization,
|
|
3225
|
+
...(result.normalization.length > 0 ? { normalizedData: result.normalizedData, } : {}),
|
|
3226
|
+
changes: result.changes,
|
|
3227
|
+
unchangedPaths: result.unchangedPaths,
|
|
3228
|
+
};
|
|
2405
3229
|
},
|
|
2406
3230
|
usage: "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]",
|
|
2407
|
-
description: "Update scenario settings via JSON merge.",
|
|
2408
|
-
examples: [
|
|
3231
|
+
description: "Update scenario settings via JSON merge; edit step-based scenario steps at params.steps, not rawParams.params.steps.",
|
|
3232
|
+
examples: [
|
|
3233
|
+
'dss scenario update my_scenario --data \'{"params":{"steps":[]}}\' --dry-run',
|
|
3234
|
+
"dss scenario update my_scenario --data-file settings.json --dry-run",
|
|
3235
|
+
],
|
|
2409
3236
|
},
|
|
2410
3237
|
},
|
|
2411
3238
|
folder: {
|
|
@@ -3290,6 +4117,8 @@ function requireArgs(args, count, usage) {
|
|
|
3290
4117
|
// .env auto-loading
|
|
3291
4118
|
// ---------------------------------------------------------------------------
|
|
3292
4119
|
function loadEnvFile() {
|
|
4120
|
+
if (process.env.DATAIKU_DISABLE_ENV === "1")
|
|
4121
|
+
return;
|
|
3293
4122
|
const dirs = [
|
|
3294
4123
|
resolve(dirname(fileURLToPath(import.meta.url)), ".."),
|
|
3295
4124
|
process.cwd(),
|
|
@@ -3461,6 +4290,19 @@ async function probeDoctorPermission(probe) {
|
|
|
3461
4290
|
return { status: permissionStatusForError(error), details: errorDetails(error), };
|
|
3462
4291
|
}
|
|
3463
4292
|
}
|
|
4293
|
+
async function probeReadOnlyPrerequisiteForMutation(probe, readAction) {
|
|
4294
|
+
const readProbe = await probeDoctorPermission(probe);
|
|
4295
|
+
if (readProbe.status !== "yes")
|
|
4296
|
+
return readProbe;
|
|
4297
|
+
return {
|
|
4298
|
+
status: "unknown",
|
|
4299
|
+
details: {
|
|
4300
|
+
reason: "mutation capability was not verified because doctor capabilities are read-only",
|
|
4301
|
+
readAction,
|
|
4302
|
+
readStatus: "yes",
|
|
4303
|
+
},
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
3464
4306
|
function missingProjectPermission() {
|
|
3465
4307
|
return {
|
|
3466
4308
|
status: "unknown",
|
|
@@ -3566,21 +4408,21 @@ async function doctorCapabilities(client, projectKey, accessibleProjects, flags)
|
|
|
3566
4408
|
? probeDoctorPermission(() => client.projects.get(probeProjectKey))
|
|
3567
4409
|
: Promise.resolve(missingProjectPermission()),
|
|
3568
4410
|
canMutateProject: () => probeProjectKey
|
|
3569
|
-
?
|
|
4411
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.variables.get(probeProjectKey), "variables.get")
|
|
3570
4412
|
: Promise.resolve(missingProjectPermission()),
|
|
3571
4413
|
canCreateFolder: () => probeProjectKey
|
|
3572
|
-
?
|
|
4414
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.folders.list(probeProjectKey), "folders.list")
|
|
3573
4415
|
: Promise.resolve(missingProjectPermission()),
|
|
3574
4416
|
canRunJobs: () => probeProjectKey
|
|
3575
|
-
?
|
|
4417
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.jobs.list(probeProjectKey), "jobs.list")
|
|
3576
4418
|
: Promise.resolve(missingProjectPermission()),
|
|
3577
4419
|
canCreateScenario: () => probeProjectKey
|
|
3578
|
-
?
|
|
4420
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.scenarios.list(probeProjectKey), "scenarios.list")
|
|
3579
4421
|
: Promise.resolve(missingProjectPermission()),
|
|
3580
4422
|
canSaveJupyter: () => probeProjectKey
|
|
3581
|
-
?
|
|
4423
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.notebooks.listJupyter(probeProjectKey), "notebooks.listJupyter")
|
|
3582
4424
|
: Promise.resolve(missingProjectPermission()),
|
|
3583
|
-
canMutateConnection: () =>
|
|
4425
|
+
canMutateConnection: () => probeReadOnlyPrerequisiteForMutation(() => client.connections.list(), "connections.list"),
|
|
3584
4426
|
};
|
|
3585
4427
|
const permissions = {};
|
|
3586
4428
|
const permissionDetails = {};
|
|
@@ -3738,6 +4580,7 @@ async function runFixtures(flags) {
|
|
|
3738
4580
|
return discoverFixtureReport(client, projectKey, flags);
|
|
3739
4581
|
}
|
|
3740
4582
|
const READ_ACTIONS = new Set([
|
|
4583
|
+
"cat",
|
|
3741
4584
|
"contents",
|
|
3742
4585
|
"diff",
|
|
3743
4586
|
"download",
|
|
@@ -3758,10 +4601,14 @@ const READ_ACTIONS = new Set([
|
|
|
3758
4601
|
"list-jupyter",
|
|
3759
4602
|
"list-sql",
|
|
3760
4603
|
"log",
|
|
4604
|
+
"log-url",
|
|
3761
4605
|
"map",
|
|
3762
4606
|
"metadata",
|
|
3763
4607
|
"peek",
|
|
4608
|
+
"source",
|
|
4609
|
+
"summary",
|
|
3764
4610
|
"wait",
|
|
4611
|
+
"watch",
|
|
3765
4612
|
"preview",
|
|
3766
4613
|
"query",
|
|
3767
4614
|
"schema",
|
|
@@ -3891,7 +4738,7 @@ function inferSideEffect(resource, action) {
|
|
|
3891
4738
|
return "write";
|
|
3892
4739
|
if (READ_ACTIONS.has(action))
|
|
3893
4740
|
return "read";
|
|
3894
|
-
if (/^(create|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
|
|
4741
|
+
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
|
|
3895
4742
|
.test(action)) {
|
|
3896
4743
|
return "write";
|
|
3897
4744
|
}
|
|
@@ -3911,6 +4758,7 @@ function inferRequiresProject(resource, action, usage) {
|
|
|
3911
4758
|
}
|
|
3912
4759
|
const ARRAY_OUTPUT_ACTIONS = new Set([
|
|
3913
4760
|
"history",
|
|
4761
|
+
"find",
|
|
3914
4762
|
"infer",
|
|
3915
4763
|
"last-results",
|
|
3916
4764
|
"list",
|
|
@@ -3926,7 +4774,9 @@ const STRING_OUTPUT_ACTIONS = new Set([
|
|
|
3926
4774
|
"download",
|
|
3927
4775
|
"download-code",
|
|
3928
4776
|
"get-payload",
|
|
4777
|
+
"cat",
|
|
3929
4778
|
"log",
|
|
4779
|
+
"log-url",
|
|
3930
4780
|
"preview",
|
|
3931
4781
|
]);
|
|
3932
4782
|
function inferOutputShape(resource, action) {
|
|
@@ -3967,7 +4817,7 @@ function inferExitCodes(asyncKind) {
|
|
|
3967
4817
|
};
|
|
3968
4818
|
}
|
|
3969
4819
|
function cleanupCommandFromDeleteUsage(resource, action) {
|
|
3970
|
-
if (!action.startsWith("create"))
|
|
4820
|
+
if (!(action.startsWith("create") || action === "clone"))
|
|
3971
4821
|
return undefined;
|
|
3972
4822
|
const deleteAction = action === "create-rule" ? "delete-rule" : "delete";
|
|
3973
4823
|
const deleteUsage = commands[resource]?.[deleteAction]?.usage;
|
|
@@ -3990,8 +4840,9 @@ function inferDestructiveLevel(sideEffect, action) {
|
|
|
3990
4840
|
return "reversible";
|
|
3991
4841
|
}
|
|
3992
4842
|
function inferAsyncKind(resource, action) {
|
|
3993
|
-
if (resource === "job" && ["build", "build-and-wait", "wait",].includes(action))
|
|
4843
|
+
if (resource === "job" && ["build", "build-and-wait", "wait", "monitor", "watch",].includes(action)) {
|
|
3994
4844
|
return "job";
|
|
4845
|
+
}
|
|
3995
4846
|
if (resource === "recipe" && action === "run")
|
|
3996
4847
|
return "job";
|
|
3997
4848
|
if (resource === "future" && ["get", "peek", "wait", "abort",].includes(action))
|
|
@@ -4013,7 +4864,7 @@ function inferIdempotency(sideEffect, action, usage) {
|
|
|
4013
4864
|
return "none";
|
|
4014
4865
|
}
|
|
4015
4866
|
function inferCleanupHint(resource, action) {
|
|
4016
|
-
if (!action.startsWith("create"))
|
|
4867
|
+
if (!(action.startsWith("create") || action === "clone"))
|
|
4017
4868
|
return undefined;
|
|
4018
4869
|
if (resource === "code-env")
|
|
4019
4870
|
return "Delete with `dss code-env delete <lang> <name> --if-exists`.";
|
|
@@ -4162,7 +5013,7 @@ function requiredPlanFlag(flags, name, usage) {
|
|
|
4162
5013
|
}
|
|
4163
5014
|
function optionalJsonFlag(flags, name) {
|
|
4164
5015
|
const value = flags[name];
|
|
4165
|
-
return typeof value === "string" ?
|
|
5016
|
+
return typeof value === "string" ? parseJsonObject(value, `--${name}`) : undefined;
|
|
4166
5017
|
}
|
|
4167
5018
|
function requiredPlanJsonInput(flags, usage) {
|
|
4168
5019
|
return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
|
|
@@ -4475,8 +5326,11 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4475
5326
|
};
|
|
4476
5327
|
case "recipe.set-payload": {
|
|
4477
5328
|
const file = requiredPlanFlag(flags, "file", entry.usage);
|
|
4478
|
-
const backupDir = flags["backup
|
|
4479
|
-
|
|
5329
|
+
const backupDir = flags["no-backup"] === true
|
|
5330
|
+
? undefined
|
|
5331
|
+
: flags["backup-dir"]
|
|
5332
|
+
?? join(process.cwd(), ".dss-backups", "recipes");
|
|
5333
|
+
const backupPath = backupDir ? recipeBackupPath(id, backupDir) : undefined;
|
|
4480
5334
|
return {
|
|
4481
5335
|
method: "PUT",
|
|
4482
5336
|
endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
|
|
@@ -4487,7 +5341,7 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4487
5341
|
...(backupPath ? { backupPath, } : {}),
|
|
4488
5342
|
},
|
|
4489
5343
|
...(backupPath
|
|
4490
|
-
? { localWrites: [{ path: backupPath, source: "remote recipe
|
|
5344
|
+
? { localWrites: [{ path: backupPath, source: "remote recipe backup", before: "PUT", },], }
|
|
4491
5345
|
: {}),
|
|
4492
5346
|
};
|
|
4493
5347
|
}
|
|
@@ -4834,17 +5688,26 @@ function promptSecret(label) {
|
|
|
4834
5688
|
// Credential resolution
|
|
4835
5689
|
// ---------------------------------------------------------------------------
|
|
4836
5690
|
function resolveCredentials(flags) {
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
5691
|
+
const hasUrlFlag = Object.hasOwn(flags, "url");
|
|
5692
|
+
const hasApiKeyFlag = Object.hasOwn(flags, "api-key");
|
|
5693
|
+
const hasProjectKeyFlag = Object.hasOwn(flags, "project-key");
|
|
5694
|
+
let url = hasUrlFlag ? flags["url"] : undefined;
|
|
5695
|
+
let apiKey = hasApiKeyFlag ? flags["api-key"] : undefined;
|
|
5696
|
+
let projectKey = hasProjectKeyFlag ? flags["project-key"] : undefined;
|
|
4840
5697
|
const saved = loadCredentials();
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
5698
|
+
if (!hasUrlFlag)
|
|
5699
|
+
url ??= process.env.DATAIKU_URL;
|
|
5700
|
+
if (!hasApiKeyFlag)
|
|
5701
|
+
apiKey ??= process.env.DATAIKU_API_KEY;
|
|
5702
|
+
if (!hasProjectKeyFlag)
|
|
5703
|
+
projectKey ??= process.env.DATAIKU_PROJECT_KEY;
|
|
4844
5704
|
if (saved) {
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
5705
|
+
if (!hasUrlFlag)
|
|
5706
|
+
url ??= saved.url;
|
|
5707
|
+
if (!hasApiKeyFlag)
|
|
5708
|
+
apiKey ??= saved.apiKey;
|
|
5709
|
+
if (!hasProjectKeyFlag)
|
|
5710
|
+
projectKey ??= saved.projectKey;
|
|
4848
5711
|
}
|
|
4849
5712
|
return {
|
|
4850
5713
|
url: url ?? "",
|
|
@@ -4979,7 +5842,7 @@ async function main() {
|
|
|
4979
5842
|
const { positional, flags, } = parseArgs(process.argv.slice(2));
|
|
4980
5843
|
// --version
|
|
4981
5844
|
if (flags["version"] === true) {
|
|
4982
|
-
process.stdout.write(`${
|
|
5845
|
+
process.stdout.write(`${CLI_VERSION_LABEL}\n`);
|
|
4983
5846
|
process.exit(0);
|
|
4984
5847
|
}
|
|
4985
5848
|
// Top-level help
|
|
@@ -5317,7 +6180,12 @@ async function main() {
|
|
|
5317
6180
|
if (entry)
|
|
5318
6181
|
await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
|
|
5319
6182
|
}
|
|
5320
|
-
|
|
6183
|
+
if (flags["raw"] === true && typeof result === "string") {
|
|
6184
|
+
process.stdout.write(result);
|
|
6185
|
+
}
|
|
6186
|
+
else {
|
|
6187
|
+
writeCommandResult(result);
|
|
6188
|
+
}
|
|
5321
6189
|
const failureExitCode = commandFailureExitCode(result);
|
|
5322
6190
|
if (failureExitCode !== undefined)
|
|
5323
6191
|
process.exit(failureExitCode);
|