dataiku-sdk 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/types/src/index.d.ts +21 -0
- package/dist/packages/types/src/index.js +7 -0
- package/dist/src/cli.js +1416 -112
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.js +3 -3
- package/dist/src/resources/datasets.d.ts +18 -0
- package/dist/src/resources/datasets.js +61 -0
- package/dist/src/resources/flow-zones.d.ts +1 -1
- package/dist/src/resources/flow-zones.js +3 -1
- package/dist/src/resources/jobs.d.ts +14 -0
- package/dist/src/resources/jobs.js +62 -3
- package/dist/src/resources/recipes.d.ts +27 -0
- package/dist/src/resources/recipes.js +138 -0
- package/dist/src/resources/scenarios.d.ts +38 -2
- package/dist/src/resources/scenarios.js +162 -3
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/skill.d.ts +2 -2
- package/dist/src/skill.js +3 -3
- package/package.json +1 -1
- package/packages/types/dist/index.d.ts +21 -0
- package/packages/types/dist/index.js +7 -0
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
dir = dirname(dir);
|
|
30
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,594 @@ 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
|
+
function optionalStringField(record, keys) {
|
|
344
|
+
for (const key of keys) {
|
|
345
|
+
const value = record[key];
|
|
346
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
347
|
+
return value.trim();
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
function requiredStringArray(value, source) {
|
|
352
|
+
if (!Array.isArray(value)) {
|
|
353
|
+
throw new UsageError(`${source} must be an array of strings.`, "validation_failed");
|
|
354
|
+
}
|
|
355
|
+
return value.map((item, index) => {
|
|
356
|
+
if (typeof item !== "string" || item.trim().length === 0) {
|
|
357
|
+
throw new UsageError(`${source}[${index}] must be a non-empty string.`, "validation_failed");
|
|
358
|
+
}
|
|
359
|
+
return item.trim();
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function finiteNumberField(record, key, source) {
|
|
363
|
+
const value = record[key];
|
|
364
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
365
|
+
throw new UsageError(`${source}.${key} must be a finite number.`, "validation_failed");
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
function flowZonePlanColor(value, source) {
|
|
370
|
+
if (value === undefined)
|
|
371
|
+
return undefined;
|
|
372
|
+
if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value.trim())) {
|
|
373
|
+
throw new UsageError(`${source} must be a hex color like #2ab1ac.`, "validation_failed");
|
|
374
|
+
}
|
|
375
|
+
return value.trim();
|
|
376
|
+
}
|
|
377
|
+
function flowZonePlanPosition(value, source) {
|
|
378
|
+
if (value === undefined)
|
|
379
|
+
return undefined;
|
|
380
|
+
const record = plainRecord(value);
|
|
381
|
+
if (!record) {
|
|
382
|
+
throw new UsageError(`${source} must be an object with x and y.`, "validation_failed");
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
x: finiteNumberField(record, "x", source),
|
|
386
|
+
y: finiteNumberField(record, "y", source),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function flowZoneCurrentPosition(zone) {
|
|
390
|
+
const record = zone;
|
|
391
|
+
const position = plainRecord(record.position);
|
|
392
|
+
if (!position)
|
|
393
|
+
return undefined;
|
|
394
|
+
const x = position.x;
|
|
395
|
+
const y = position.y;
|
|
396
|
+
return typeof x === "number" && Number.isFinite(x) && typeof y === "number" && Number.isFinite(y)
|
|
397
|
+
? { x, y, }
|
|
398
|
+
: undefined;
|
|
399
|
+
}
|
|
400
|
+
function flowZoneSamePosition(a, b) {
|
|
401
|
+
if (a === undefined || b === undefined)
|
|
402
|
+
return a === b;
|
|
403
|
+
return a.x === b.x && a.y === b.y;
|
|
404
|
+
}
|
|
405
|
+
function parseFlowZonePlanItem(value, source) {
|
|
406
|
+
if (typeof value === "string")
|
|
407
|
+
return parseFlowZoneObject(value);
|
|
408
|
+
const record = plainRecord(value);
|
|
409
|
+
if (!record) {
|
|
410
|
+
throw new UsageError(`${source} must be TYPE:ID or an object.`, "validation_failed");
|
|
411
|
+
}
|
|
412
|
+
const object = optionalStringField(record, ["object",]);
|
|
413
|
+
if (object)
|
|
414
|
+
return parseFlowZoneObject(object);
|
|
415
|
+
const objectType = optionalStringField(record, ["objectType", "type",]);
|
|
416
|
+
const objectId = optionalStringField(record, ["objectId", "id", "name",]);
|
|
417
|
+
if (!objectType || !objectId) {
|
|
418
|
+
throw new UsageError(`${source} must include objectType/type and objectId/id, or object as TYPE:ID.`, "validation_failed");
|
|
419
|
+
}
|
|
420
|
+
const projectKey = optionalStringField(record, ["projectKey", "project",]);
|
|
421
|
+
return {
|
|
422
|
+
objectType: flowZoneObjectType(objectType),
|
|
423
|
+
objectId,
|
|
424
|
+
...(projectKey ? { projectKey, } : {}),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function addFlowZonePlanTypedItems(items, record, key, objectType, source) {
|
|
428
|
+
if (record[key] === undefined)
|
|
429
|
+
return;
|
|
430
|
+
for (const objectId of requiredStringArray(record[key], `${source}.${key}`)) {
|
|
431
|
+
items.push({ objectType, objectId, });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function flowZoneItemKey(item) {
|
|
435
|
+
return `${item.projectKey ?? ""}\0${item.objectType}\0${item.objectId}`;
|
|
436
|
+
}
|
|
437
|
+
function flowZonePlanLabel(plan) {
|
|
438
|
+
return plan.id ?? plan.name ?? "<unknown>";
|
|
439
|
+
}
|
|
440
|
+
function dedupeFlowZonePlanItems(items) {
|
|
441
|
+
const seen = new Set();
|
|
442
|
+
const result = [];
|
|
443
|
+
for (const item of items) {
|
|
444
|
+
const key = flowZoneItemKey(item);
|
|
445
|
+
if (seen.has(key))
|
|
446
|
+
continue;
|
|
447
|
+
seen.add(key);
|
|
448
|
+
result.push(item);
|
|
449
|
+
}
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
function flowZonePlanItemKeys(plan) {
|
|
453
|
+
const keys = new Set();
|
|
454
|
+
for (const zone of plan.zones) {
|
|
455
|
+
for (const item of zone.items)
|
|
456
|
+
keys.add(flowZoneItemKey(item));
|
|
457
|
+
}
|
|
458
|
+
return keys;
|
|
459
|
+
}
|
|
460
|
+
function validateUniqueFlowZoneAssignments(plan) {
|
|
461
|
+
const seen = new Map();
|
|
462
|
+
for (const zone of plan.zones) {
|
|
463
|
+
const label = flowZonePlanLabel(zone);
|
|
464
|
+
for (const item of zone.items) {
|
|
465
|
+
const key = flowZoneItemKey(item);
|
|
466
|
+
const previous = seen.get(key);
|
|
467
|
+
if (previous) {
|
|
468
|
+
throw new UsageError(`Flow object ${item.objectType}:${item.objectId} is assigned to both "${previous}" and "${label}".`, "validation_failed");
|
|
469
|
+
}
|
|
470
|
+
seen.set(key, label);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function parseFlowZoneOrganizePlan(input) {
|
|
475
|
+
const zones = input.zones;
|
|
476
|
+
if (!Array.isArray(zones) || zones.length === 0) {
|
|
477
|
+
throw new UsageError("Flow zone organize plan must include a non-empty zones array.", "validation_failed");
|
|
478
|
+
}
|
|
479
|
+
const plan = {
|
|
480
|
+
zones: zones.map((value, index) => {
|
|
481
|
+
const source = `zones[${index}]`;
|
|
482
|
+
const record = plainRecord(value);
|
|
483
|
+
if (!record)
|
|
484
|
+
throw new UsageError(`${source} must be an object.`, "validation_failed");
|
|
485
|
+
const id = optionalStringField(record, ["id", "zoneId",]);
|
|
486
|
+
const name = optionalStringField(record, ["name",]);
|
|
487
|
+
if (!id && !name) {
|
|
488
|
+
throw new UsageError(`${source} must include name or id.`, "validation_failed");
|
|
489
|
+
}
|
|
490
|
+
const items = [];
|
|
491
|
+
const rawItems = record.items ?? record.objects;
|
|
492
|
+
if (rawItems !== undefined) {
|
|
493
|
+
if (!Array.isArray(rawItems)) {
|
|
494
|
+
throw new UsageError(`${source}.items must be an array.`, "validation_failed");
|
|
495
|
+
}
|
|
496
|
+
rawItems.forEach((item, itemIndex) => {
|
|
497
|
+
items.push(parseFlowZonePlanItem(item, `${source}.items[${itemIndex}]`));
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
addFlowZonePlanTypedItems(items, record, "datasets", "DATASET", source);
|
|
501
|
+
addFlowZonePlanTypedItems(items, record, "recipes", "RECIPE", source);
|
|
502
|
+
addFlowZonePlanTypedItems(items, record, "folders", "MANAGED_FOLDER", source);
|
|
503
|
+
addFlowZonePlanTypedItems(items, record, "savedModels", "SAVED_MODEL", source);
|
|
504
|
+
addFlowZonePlanTypedItems(items, record, "modelEvaluationStores", "MODEL_EVALUATION_STORE", source);
|
|
505
|
+
addFlowZonePlanTypedItems(items, record, "streamingEndpoints", "STREAMING_ENDPOINT", source);
|
|
506
|
+
addFlowZonePlanTypedItems(items, record, "labelingTasks", "LABELING_TASK", source);
|
|
507
|
+
addFlowZonePlanTypedItems(items, record, "knowledgeBanks", "RETRIEVABLE_KNOWLEDGE", source);
|
|
508
|
+
return {
|
|
509
|
+
...(id ? { id, } : {}),
|
|
510
|
+
...(name ? { name, } : {}),
|
|
511
|
+
...(record.color !== undefined
|
|
512
|
+
? { color: flowZonePlanColor(record.color, `${source}.color`), }
|
|
513
|
+
: {}),
|
|
514
|
+
...(record.position !== undefined
|
|
515
|
+
? { position: flowZonePlanPosition(record.position, `${source}.position`), }
|
|
516
|
+
: {}),
|
|
517
|
+
items: dedupeFlowZonePlanItems(items),
|
|
518
|
+
};
|
|
519
|
+
}),
|
|
520
|
+
};
|
|
521
|
+
validateUniqueFlowZoneAssignments(plan);
|
|
522
|
+
return plan;
|
|
523
|
+
}
|
|
524
|
+
function readFlowZoneOrganizePlan(flags, usage) {
|
|
525
|
+
const data = typeof flags["file"] === "string"
|
|
526
|
+
? parseJsonObject(readFileSync(flags["file"], "utf-8"), flags["file"])
|
|
527
|
+
: jsonInput(flags);
|
|
528
|
+
if (!data) {
|
|
529
|
+
throw new UsageError(`--data, --data-file, --file, or --stdin is required. Usage: ${usage}`, "missing_required_flag");
|
|
530
|
+
}
|
|
531
|
+
return parseFlowZoneOrganizePlan(data);
|
|
532
|
+
}
|
|
533
|
+
function findFlowZoneForPlan(zones, plan) {
|
|
534
|
+
if (plan.id) {
|
|
535
|
+
const byId = zones.find((zone) => zone.id === plan.id);
|
|
536
|
+
if (byId)
|
|
537
|
+
return byId;
|
|
538
|
+
}
|
|
539
|
+
if (!plan.name)
|
|
540
|
+
return undefined;
|
|
541
|
+
const byName = zones.filter((zone) => zone.name === plan.name);
|
|
542
|
+
if (byName.length > 1) {
|
|
543
|
+
throw new UsageError(`Multiple flow zones named "${plan.name}" exist; use id.`, "validation_failed");
|
|
544
|
+
}
|
|
545
|
+
return byName[0];
|
|
546
|
+
}
|
|
547
|
+
function ensureFlowZonePlanTarget(plan, existing) {
|
|
548
|
+
if (existing || plan.name)
|
|
549
|
+
return;
|
|
550
|
+
throw new UsageError(`Flow zone ${plan.id ?? "<unknown>"} was not found and cannot be created without name.`, "validation_failed");
|
|
551
|
+
}
|
|
552
|
+
function flowZoneExplicitItems(zone) {
|
|
553
|
+
return (zone.items ?? []).map((item) => ({
|
|
554
|
+
objectId: item.objectId,
|
|
555
|
+
objectType: item.objectType,
|
|
556
|
+
...(item.projectKey ? { projectKey: item.projectKey, } : {}),
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
function flowZonePruneItems(existing, plannedItemKeys) {
|
|
560
|
+
if (!existing)
|
|
561
|
+
return [];
|
|
562
|
+
return flowZoneExplicitItems(existing).filter((item) => !plannedItemKeys.has(flowZoneItemKey(item)));
|
|
563
|
+
}
|
|
564
|
+
function flowZoneOrganizeStep(plan, existing, sync, plannedItemKeys) {
|
|
565
|
+
ensureFlowZonePlanTarget(plan, existing);
|
|
566
|
+
const update = {};
|
|
567
|
+
if (existing && plan.name && plan.name !== existing.name)
|
|
568
|
+
update.name = plan.name;
|
|
569
|
+
if (existing && plan.color && plan.color !== existing.color)
|
|
570
|
+
update.color = plan.color;
|
|
571
|
+
if (existing && plan.position !== undefined
|
|
572
|
+
&& !flowZoneSamePosition(flowZoneCurrentPosition(existing), plan.position)) {
|
|
573
|
+
update.position = plan.position;
|
|
574
|
+
}
|
|
575
|
+
const pruneItems = sync ? flowZonePruneItems(existing, plannedItemKeys) : [];
|
|
576
|
+
return {
|
|
577
|
+
target: {
|
|
578
|
+
...(plan.id ? { id: plan.id, } : {}),
|
|
579
|
+
...(plan.name ? { name: plan.name, } : {}),
|
|
580
|
+
...(plan.color ? { color: plan.color, } : {}),
|
|
581
|
+
...(plan.position ? { position: plan.position, } : {}),
|
|
582
|
+
},
|
|
583
|
+
...(existing ? { existing: flowZoneSummary(existing), } : { create: true, }),
|
|
584
|
+
...(Object.keys(update).length > 0 ? { update, } : {}),
|
|
585
|
+
moveItems: plan.items,
|
|
586
|
+
...(pruneItems.length > 0 ? { pruneItems, } : {}),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function flowZoneValidationBucket(index, objectType) {
|
|
590
|
+
switch (objectType) {
|
|
591
|
+
case "DATASET":
|
|
592
|
+
return index.datasets;
|
|
593
|
+
case "RECIPE":
|
|
594
|
+
return index.recipes;
|
|
595
|
+
case "MANAGED_FOLDER":
|
|
596
|
+
return index.folders;
|
|
597
|
+
case "SAVED_MODEL":
|
|
598
|
+
case "MODEL_EVALUATION_STORE":
|
|
599
|
+
case "STREAMING_ENDPOINT":
|
|
600
|
+
case "LABELING_TASK":
|
|
601
|
+
case "RETRIEVABLE_KNOWLEDGE":
|
|
602
|
+
return index.all;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function flowZoneValidationIndex(client, projectKey) {
|
|
606
|
+
const result = await client.projects.map({
|
|
607
|
+
projectKey,
|
|
608
|
+
maxNodes: 100_000,
|
|
609
|
+
maxEdges: 100_000,
|
|
610
|
+
});
|
|
611
|
+
const index = {
|
|
612
|
+
projectKey: result.map.projectKey,
|
|
613
|
+
all: new Set(),
|
|
614
|
+
datasets: new Set(),
|
|
615
|
+
recipes: new Set(),
|
|
616
|
+
folders: new Set(),
|
|
617
|
+
};
|
|
618
|
+
for (const node of result.map.nodes) {
|
|
619
|
+
index.all.add(node.id);
|
|
620
|
+
switch (node.kind) {
|
|
621
|
+
case "dataset":
|
|
622
|
+
index.datasets.add(node.id);
|
|
623
|
+
break;
|
|
624
|
+
case "recipe":
|
|
625
|
+
index.recipes.add(node.id);
|
|
626
|
+
break;
|
|
627
|
+
case "folder":
|
|
628
|
+
index.folders.add(node.id);
|
|
629
|
+
break;
|
|
630
|
+
case "other":
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return index;
|
|
635
|
+
}
|
|
636
|
+
async function validateFlowZoneOrganizeObjects(client, plan, projectKey) {
|
|
637
|
+
const indexes = new Map();
|
|
638
|
+
const missing = [];
|
|
639
|
+
const getIndex = async (itemProjectKey) => {
|
|
640
|
+
const requestedProjectKey = itemProjectKey ?? projectKey;
|
|
641
|
+
const cacheKey = requestedProjectKey ?? "";
|
|
642
|
+
const cached = indexes.get(cacheKey);
|
|
643
|
+
if (cached)
|
|
644
|
+
return cached;
|
|
645
|
+
const index = await flowZoneValidationIndex(client, requestedProjectKey);
|
|
646
|
+
indexes.set(cacheKey, index);
|
|
647
|
+
return index;
|
|
648
|
+
};
|
|
649
|
+
for (const zone of plan.zones) {
|
|
650
|
+
for (const item of zone.items) {
|
|
651
|
+
const index = await getIndex(item.projectKey);
|
|
652
|
+
const bucket = flowZoneValidationBucket(index, item.objectType);
|
|
653
|
+
if (bucket.has(item.objectId))
|
|
654
|
+
continue;
|
|
655
|
+
missing.push({
|
|
656
|
+
zone: flowZonePlanLabel(zone),
|
|
657
|
+
objectId: item.objectId,
|
|
658
|
+
objectType: item.objectType,
|
|
659
|
+
...(item.projectKey ? { projectKey: item.projectKey, } : {}),
|
|
660
|
+
reason: `Object not found in project ${item.projectKey ?? index.projectKey}.`,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return { valid: missing.length === 0, missing, };
|
|
665
|
+
}
|
|
666
|
+
function throwFlowZoneValidationError(validation) {
|
|
667
|
+
if (validation.valid)
|
|
668
|
+
return;
|
|
669
|
+
const first = validation.missing[0];
|
|
670
|
+
const suffix = validation.missing.length > 1 ? ` and ${validation.missing.length - 1} more` : "";
|
|
671
|
+
throw new UsageError(`Flow zone organize validation failed: ${first?.objectType}:${first?.objectId} in zone "${first?.zone}" was not found${suffix}.`, "validation_failed");
|
|
672
|
+
}
|
|
673
|
+
async function resolveFlowZoneIdFromFlags(client, flags, projectKey) {
|
|
674
|
+
const zoneId = typeof flags["zone-id"] === "string" ? flags["zone-id"].trim() : "";
|
|
675
|
+
if (zoneId)
|
|
676
|
+
return zoneId;
|
|
677
|
+
const zone = typeof flags["zone"] === "string" ? flags["zone"].trim() : "";
|
|
678
|
+
if (!zone)
|
|
679
|
+
return undefined;
|
|
680
|
+
const zones = await client.flowZones.list(projectKey);
|
|
681
|
+
const match = zones.find((candidate) => candidate.id === zone || candidate.name === zone);
|
|
682
|
+
if (!match)
|
|
683
|
+
throw new UsageError(`Flow zone not found: ${zone}`, "invalid_enum");
|
|
684
|
+
return match.id;
|
|
685
|
+
}
|
|
686
|
+
async function moveCreatedItemsToZone(client, flags, items, projectKey) {
|
|
687
|
+
const zoneId = await resolveFlowZoneIdFromFlags(client, flags, projectKey);
|
|
688
|
+
if (!zoneId || items.length === 0)
|
|
689
|
+
return {};
|
|
690
|
+
await client.flowZones.moveItems(zoneId, items, projectKey);
|
|
691
|
+
return { zoneId, moved: items, };
|
|
692
|
+
}
|
|
693
|
+
function nestedValue(value, path) {
|
|
694
|
+
let current = value;
|
|
695
|
+
for (const key of path) {
|
|
696
|
+
const record = plainRecord(current);
|
|
697
|
+
if (!record)
|
|
698
|
+
return undefined;
|
|
699
|
+
current = record[key];
|
|
700
|
+
}
|
|
701
|
+
return current;
|
|
702
|
+
}
|
|
703
|
+
function stringPath(value, path) {
|
|
704
|
+
const item = nestedValue(value, path);
|
|
705
|
+
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
706
|
+
}
|
|
707
|
+
function numberPath(value, path) {
|
|
708
|
+
const item = nestedValue(value, path);
|
|
709
|
+
return typeof item === "number" && Number.isFinite(item) ? item : undefined;
|
|
710
|
+
}
|
|
711
|
+
function firstNumberPath(value, paths) {
|
|
712
|
+
for (const path of paths) {
|
|
713
|
+
const item = numberPath(value, path);
|
|
714
|
+
if (item !== undefined)
|
|
715
|
+
return item;
|
|
716
|
+
}
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
function jobSummaryId(job, fallback) {
|
|
720
|
+
return stringPath(job, ["baseStatus", "def", "id",])
|
|
721
|
+
?? stringPath(job, ["def", "id",])
|
|
722
|
+
?? stringPath(job, ["id",])
|
|
723
|
+
?? fallback
|
|
724
|
+
?? "unknown";
|
|
725
|
+
}
|
|
726
|
+
function jobSummaryType(job) {
|
|
727
|
+
return stringPath(job, ["baseStatus", "def", "type",])
|
|
728
|
+
?? stringPath(job, ["def", "type",])
|
|
729
|
+
?? stringPath(job, ["type",])
|
|
730
|
+
?? "unknown";
|
|
731
|
+
}
|
|
732
|
+
function jobSummaryState(job) {
|
|
733
|
+
return stringPath(job, ["baseStatus", "state",])
|
|
734
|
+
?? stringPath(job, ["state",])
|
|
735
|
+
?? "unknown";
|
|
736
|
+
}
|
|
737
|
+
function filteredJobList(jobs, flags) {
|
|
738
|
+
const state = typeof flags["state"] === "string" ? flags["state"].trim().toUpperCase() : "";
|
|
739
|
+
const contains = typeof flags["contains"] === "string"
|
|
740
|
+
? flags["contains"].trim().toLowerCase()
|
|
741
|
+
: "";
|
|
742
|
+
const output = typeof flags["output"] === "string" ? flags["output"].trim().toLowerCase() : "";
|
|
743
|
+
let result = jobs.filter((job) => {
|
|
744
|
+
if (state && jobSummaryState(job).toUpperCase() !== state)
|
|
745
|
+
return false;
|
|
746
|
+
const text = JSON.stringify(job).toLowerCase();
|
|
747
|
+
if (contains && !text.includes(contains))
|
|
748
|
+
return false;
|
|
749
|
+
if (output && !text.includes(output))
|
|
750
|
+
return false;
|
|
751
|
+
return true;
|
|
752
|
+
});
|
|
753
|
+
const limit = flags["latest"] === true ? 1 : num(flags["limit"]);
|
|
754
|
+
if (limit !== undefined)
|
|
755
|
+
result = result.slice(0, Math.max(0, limit));
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
function maxNumber(values) {
|
|
759
|
+
return values.length === 0 ? 0 : Math.max(...values);
|
|
760
|
+
}
|
|
761
|
+
function collectWarningCounts(value, inActivity, counts) {
|
|
762
|
+
if (Array.isArray(value)) {
|
|
763
|
+
for (const item of value)
|
|
764
|
+
collectWarningCounts(item, inActivity, counts);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const record = plainRecord(value);
|
|
768
|
+
if (!record)
|
|
769
|
+
return;
|
|
770
|
+
for (const [key, item,] of Object.entries(record)) {
|
|
771
|
+
const lower = key.toLowerCase();
|
|
772
|
+
const nextInActivity = inActivity || lower.includes("activit");
|
|
773
|
+
if (lower.includes("warn")) {
|
|
774
|
+
const target = nextInActivity ? counts.activity : counts.dss;
|
|
775
|
+
if (typeof item === "number" && Number.isFinite(item))
|
|
776
|
+
target.push(item);
|
|
777
|
+
else if (Array.isArray(item))
|
|
778
|
+
target.push(item.length);
|
|
779
|
+
}
|
|
780
|
+
collectWarningCounts(item, nextInActivity, counts);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function jobWarningSummary(details, log) {
|
|
784
|
+
const counts = { dss: [], activity: [], };
|
|
785
|
+
collectWarningCounts(details, false, counts);
|
|
786
|
+
const warningLines = log
|
|
787
|
+
? log.split(/\r?\n/).map((line) => line.trim()).filter((line) => /\bwarn(?:ing)?\b/i.test(line))
|
|
788
|
+
: [];
|
|
789
|
+
return {
|
|
790
|
+
dssSummaryWarningCount: maxNumber(counts.dss),
|
|
791
|
+
activityWarningCount: maxNumber(counts.activity),
|
|
792
|
+
logWarnLineCount: warningLines.length,
|
|
793
|
+
sampledWarningMessages: warningLines.slice(0, 5),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function jobDurationMs(details) {
|
|
797
|
+
const started = firstNumberPath(details, [
|
|
798
|
+
["baseStatus", "startTime",],
|
|
799
|
+
["baseStatus", "start",],
|
|
800
|
+
["startTime",],
|
|
801
|
+
["start",],
|
|
802
|
+
]);
|
|
803
|
+
const ended = firstNumberPath(details, [
|
|
804
|
+
["baseStatus", "endTime",],
|
|
805
|
+
["baseStatus", "end",],
|
|
806
|
+
["endTime",],
|
|
807
|
+
["end",],
|
|
808
|
+
]);
|
|
809
|
+
return started !== undefined && ended !== undefined && ended >= started
|
|
810
|
+
? ended - started
|
|
811
|
+
: undefined;
|
|
812
|
+
}
|
|
813
|
+
async function jobInspectionSummary(client, jobId, flags) {
|
|
814
|
+
const projectKey = flags["project-key"];
|
|
815
|
+
const details = await client.jobs.get(jobId, projectKey);
|
|
816
|
+
let log;
|
|
817
|
+
let logError;
|
|
818
|
+
try {
|
|
819
|
+
log = await client.jobs.log(jobId, {
|
|
820
|
+
activity: flags["activity"],
|
|
821
|
+
logId: flags["log-id"],
|
|
822
|
+
maxLogLines: maxLogLinesFromFlags(flags),
|
|
823
|
+
projectKey,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
logError = error instanceof Error ? error.message : String(error);
|
|
828
|
+
}
|
|
829
|
+
const durationMs = jobDurationMs(details);
|
|
830
|
+
const progress = log ? parseJobLogProgress(log, durationMs) : undefined;
|
|
831
|
+
const logLines = log
|
|
832
|
+
? log.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
|
|
833
|
+
: [];
|
|
834
|
+
const maxSummaryLines = Math.max(1, maxLogLinesFromFlags(flags) ?? 20);
|
|
835
|
+
const outputs = nestedValue(details, ["baseStatus", "def", "outputs",])
|
|
836
|
+
?? nestedValue(details, ["def", "outputs",])
|
|
837
|
+
?? details.outputs;
|
|
838
|
+
return {
|
|
839
|
+
resource: "job",
|
|
840
|
+
jobId: jobSummaryId(details, jobId),
|
|
841
|
+
state: jobSummaryState(details),
|
|
842
|
+
type: jobSummaryType(details),
|
|
843
|
+
...(durationMs !== undefined ? { durationMs, } : {}),
|
|
844
|
+
...(outputs !== undefined ? { outputs, } : {}),
|
|
845
|
+
warnings: jobWarningSummary(details, log),
|
|
846
|
+
...(progress
|
|
847
|
+
? {
|
|
848
|
+
progress,
|
|
849
|
+
latestUsefulProgressLine: progress.lastProgressLine,
|
|
850
|
+
doneLine: progress.doneLine,
|
|
851
|
+
}
|
|
852
|
+
: {}),
|
|
853
|
+
logSummary: {
|
|
854
|
+
lineCount: logLines.length,
|
|
855
|
+
lines: logLines.slice(-maxSummaryLines),
|
|
856
|
+
...(logError ? { error: logError, } : {}),
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function stripUtf8Bom(text) {
|
|
861
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
862
|
+
}
|
|
863
|
+
function parseJsonValue(text, source) {
|
|
864
|
+
try {
|
|
865
|
+
return JSON.parse(stripUtf8Bom(text));
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
869
|
+
throw new UsageError(`Invalid JSON in ${source}: ${message}`, "validation_failed");
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function expectJsonObject(value, source) {
|
|
873
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
throw new UsageError(`Expected JSON object in ${source}.`, "validation_failed");
|
|
877
|
+
}
|
|
878
|
+
function parseJsonObject(text, source) {
|
|
879
|
+
return expectJsonObject(parseJsonValue(text, source), source);
|
|
880
|
+
}
|
|
881
|
+
function json(v, source = "JSON flag") {
|
|
166
882
|
if (typeof v !== "string")
|
|
167
883
|
return undefined;
|
|
168
|
-
return
|
|
884
|
+
return parseJsonObject(v, source);
|
|
169
885
|
}
|
|
170
886
|
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
887
|
function readStdinText() {
|
|
172
888
|
return readFileSync(0, "utf-8");
|
|
173
889
|
}
|
|
174
890
|
function jsonInput(flags) {
|
|
175
|
-
if (flags["stdin"] === true)
|
|
176
|
-
return
|
|
177
|
-
}
|
|
891
|
+
if (flags["stdin"] === true)
|
|
892
|
+
return parseJsonObject(readStdinText(), "stdin");
|
|
178
893
|
if (typeof flags["data-file"] === "string") {
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
if (typeof flags["data"] === "string") {
|
|
182
|
-
return JSON.parse(flags["data"]);
|
|
894
|
+
return parseJsonObject(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
|
|
183
895
|
}
|
|
896
|
+
if (typeof flags["data"] === "string")
|
|
897
|
+
return parseJsonObject(flags["data"], "--data");
|
|
184
898
|
return undefined;
|
|
185
899
|
}
|
|
186
900
|
function unknownJsonInput(flags) {
|
|
187
901
|
if (flags["stdin"] === true)
|
|
188
|
-
return
|
|
902
|
+
return parseJsonValue(readStdinText(), "stdin");
|
|
189
903
|
if (typeof flags["data-file"] === "string") {
|
|
190
|
-
return
|
|
904
|
+
return parseJsonValue(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
|
|
191
905
|
}
|
|
192
906
|
if (typeof flags["data"] === "string")
|
|
193
|
-
return
|
|
907
|
+
return parseJsonValue(flags["data"], "--data");
|
|
194
908
|
return undefined;
|
|
195
909
|
}
|
|
196
910
|
function schemaColumnsInput(flags, usage) {
|
|
@@ -234,7 +948,7 @@ function textInput(flags) {
|
|
|
234
948
|
}
|
|
235
949
|
function requiredJsonInput(flags, message) {
|
|
236
950
|
const data = jsonInput(flags);
|
|
237
|
-
if (
|
|
951
|
+
if (data === undefined)
|
|
238
952
|
throw new UsageError(message);
|
|
239
953
|
return data;
|
|
240
954
|
}
|
|
@@ -329,7 +1043,7 @@ function resolveSqlInput(args, flags) {
|
|
|
329
1043
|
if (sources.length > 1) {
|
|
330
1044
|
throw new UsageError(`Choose exactly one SQL input source: --sql, --sql-file, --stdin, or one positional SQL argument. Usage: ${SQL_QUERY_USAGE}`);
|
|
331
1045
|
}
|
|
332
|
-
const query = sources[0].read();
|
|
1046
|
+
const query = stripUtf8Bom(sources[0].read());
|
|
333
1047
|
if (query.trim().length === 0) {
|
|
334
1048
|
throw new UsageError(`SQL input from ${sources[0].label} must not be empty. Usage: ${SQL_QUERY_USAGE}`);
|
|
335
1049
|
}
|
|
@@ -400,7 +1114,11 @@ function isFailedWaitResult(result) {
|
|
|
400
1114
|
&& (typeof record.state === "string" || typeof record.outcome === "string");
|
|
401
1115
|
}
|
|
402
1116
|
function commandFailureExitCode(result) {
|
|
403
|
-
|
|
1117
|
+
if (isFailedWaitResult(result))
|
|
1118
|
+
return 4;
|
|
1119
|
+
if (result && typeof result === "object" && result.unchanged === false)
|
|
1120
|
+
return 4;
|
|
1121
|
+
return undefined;
|
|
404
1122
|
}
|
|
405
1123
|
function isNotFoundError(error) {
|
|
406
1124
|
if (error instanceof DataikuError)
|
|
@@ -462,7 +1180,7 @@ function resultRecord(result) {
|
|
|
462
1180
|
: {};
|
|
463
1181
|
}
|
|
464
1182
|
function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
465
|
-
if (!(action.startsWith("create") || action === "upload"))
|
|
1183
|
+
if (!(action.startsWith("create") || action === "clone" || action === "upload"))
|
|
466
1184
|
return undefined;
|
|
467
1185
|
const record = resultRecord(result);
|
|
468
1186
|
if (record.skipped !== undefined)
|
|
@@ -482,6 +1200,16 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
|
482
1200
|
cleanup: { argv: ["dataset", "delete", name, "--if-exists", ...withProject,], },
|
|
483
1201
|
};
|
|
484
1202
|
}
|
|
1203
|
+
case "dataset.clone": {
|
|
1204
|
+
const name = stringField(record, ["target", "created", "name",]) ?? args[1];
|
|
1205
|
+
if (!name)
|
|
1206
|
+
return undefined;
|
|
1207
|
+
return {
|
|
1208
|
+
...base,
|
|
1209
|
+
name,
|
|
1210
|
+
cleanup: { argv: ["dataset", "delete", name, "--if-exists", ...withProject,], },
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
485
1213
|
case "recipe.create": {
|
|
486
1214
|
const name = stringField(record, ["created", "recipeName", "name",])
|
|
487
1215
|
?? flags["name"];
|
|
@@ -493,6 +1221,17 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
|
|
|
493
1221
|
cleanup: { argv: ["recipe", "delete", name, "--if-exists", ...withProject,], },
|
|
494
1222
|
};
|
|
495
1223
|
}
|
|
1224
|
+
case "recipe.clone": {
|
|
1225
|
+
const name = stringField(record, ["recipeName", "target", "created", "name",])
|
|
1226
|
+
?? flags["name"];
|
|
1227
|
+
if (!name)
|
|
1228
|
+
return undefined;
|
|
1229
|
+
return {
|
|
1230
|
+
...base,
|
|
1231
|
+
name,
|
|
1232
|
+
cleanup: { argv: ["recipe", "delete", name, "--if-exists", ...withProject,], },
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
496
1235
|
case "scenario.create": {
|
|
497
1236
|
const id = args[0];
|
|
498
1237
|
return {
|
|
@@ -606,6 +1345,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
606
1345
|
"global",
|
|
607
1346
|
"list-agents",
|
|
608
1347
|
"include-raw",
|
|
1348
|
+
"raw",
|
|
609
1349
|
"include-payload",
|
|
610
1350
|
"no-payload",
|
|
611
1351
|
"include-logs",
|
|
@@ -624,7 +1364,14 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
624
1364
|
"report-json",
|
|
625
1365
|
"no-wait",
|
|
626
1366
|
"force-rebuild",
|
|
1367
|
+
"latest",
|
|
1368
|
+
"copy-output-settings",
|
|
627
1369
|
"continue-on-error",
|
|
1370
|
+
"no-backup",
|
|
1371
|
+
"payload-only",
|
|
1372
|
+
"allow-same-path",
|
|
1373
|
+
"sync",
|
|
1374
|
+
"validate-objects",
|
|
628
1375
|
]);
|
|
629
1376
|
const SHORT_FLAGS = {
|
|
630
1377
|
h: "help",
|
|
@@ -639,6 +1386,7 @@ const FLAG_ALIASES = {
|
|
|
639
1386
|
"skip-tls-verify": "insecure",
|
|
640
1387
|
"extra-ca-certs": "ca-cert",
|
|
641
1388
|
explain: "plan",
|
|
1389
|
+
"zone-name": "zone",
|
|
642
1390
|
};
|
|
643
1391
|
const VALUE_FLAGS = new Set([
|
|
644
1392
|
"activity",
|
|
@@ -646,12 +1394,14 @@ const VALUE_FLAGS = new Set([
|
|
|
646
1394
|
"api-key",
|
|
647
1395
|
"build-mode",
|
|
648
1396
|
"backup-dir",
|
|
1397
|
+
"backup",
|
|
649
1398
|
"ca-cert",
|
|
650
1399
|
"catalog",
|
|
651
1400
|
"cell-id",
|
|
652
1401
|
"allow-types",
|
|
653
1402
|
"color",
|
|
654
1403
|
"connection",
|
|
1404
|
+
"contains",
|
|
655
1405
|
"content",
|
|
656
1406
|
"content-type",
|
|
657
1407
|
"data",
|
|
@@ -665,6 +1415,7 @@ const VALUE_FLAGS = new Set([
|
|
|
665
1415
|
"install-core-packages",
|
|
666
1416
|
"folder",
|
|
667
1417
|
"input",
|
|
1418
|
+
"from",
|
|
668
1419
|
"knowledge-bank",
|
|
669
1420
|
"labeling-task",
|
|
670
1421
|
"lang",
|
|
@@ -677,14 +1428,17 @@ const VALUE_FLAGS = new Set([
|
|
|
677
1428
|
"listed",
|
|
678
1429
|
"max-nodes",
|
|
679
1430
|
"max-rows",
|
|
1431
|
+
"limit",
|
|
680
1432
|
"max-timestamp",
|
|
681
1433
|
"only-monitored",
|
|
682
1434
|
"min-timestamp",
|
|
683
1435
|
"mode",
|
|
684
1436
|
"log-filter",
|
|
1437
|
+
"log-id",
|
|
685
1438
|
"model-evaluation-store",
|
|
686
1439
|
"name",
|
|
687
1440
|
"object",
|
|
1441
|
+
"metastore-table",
|
|
688
1442
|
"output",
|
|
689
1443
|
"output-file",
|
|
690
1444
|
"output-connection",
|
|
@@ -703,18 +1457,38 @@ const VALUE_FLAGS = new Set([
|
|
|
703
1457
|
"retries",
|
|
704
1458
|
"poll-interval",
|
|
705
1459
|
"python-interpreter",
|
|
1460
|
+
"replace-input",
|
|
1461
|
+
"replace-output",
|
|
1462
|
+
"replace-payload-text",
|
|
706
1463
|
"retain",
|
|
707
1464
|
"saved-model",
|
|
708
1465
|
"sql",
|
|
709
1466
|
"schema",
|
|
710
1467
|
"sql-file",
|
|
711
1468
|
"standard",
|
|
1469
|
+
"state",
|
|
712
1470
|
"streaming-endpoint",
|
|
713
1471
|
"target",
|
|
714
1472
|
"target-type",
|
|
715
1473
|
"timeout",
|
|
1474
|
+
"table",
|
|
716
1475
|
"type",
|
|
717
1476
|
"url",
|
|
1477
|
+
"until",
|
|
1478
|
+
"to",
|
|
1479
|
+
"zone",
|
|
1480
|
+
"zone-id",
|
|
1481
|
+
]);
|
|
1482
|
+
const REPEATABLE_VALUE_FLAGS = new Set([
|
|
1483
|
+
"dataset",
|
|
1484
|
+
"folder",
|
|
1485
|
+
"input",
|
|
1486
|
+
"object",
|
|
1487
|
+
"package",
|
|
1488
|
+
"recipe",
|
|
1489
|
+
"replace-input",
|
|
1490
|
+
"replace-output",
|
|
1491
|
+
"replace-payload-text",
|
|
718
1492
|
]);
|
|
719
1493
|
const KNOWN_LONG_FLAGS = new Set([
|
|
720
1494
|
...BOOLEAN_FLAGS,
|
|
@@ -738,6 +1512,14 @@ function requireFlagValue(flagLabel, next) {
|
|
|
738
1512
|
}
|
|
739
1513
|
return next;
|
|
740
1514
|
}
|
|
1515
|
+
function setParsedFlagValue(flags, flagName, value) {
|
|
1516
|
+
const current = flags[flagName];
|
|
1517
|
+
if (REPEATABLE_VALUE_FLAGS.has(flagName) && typeof current === "string" && current.length > 0) {
|
|
1518
|
+
flags[flagName] = `${current},${value}`;
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
flags[flagName] = value;
|
|
1522
|
+
}
|
|
741
1523
|
function parseArgs(argv) {
|
|
742
1524
|
const positional = [];
|
|
743
1525
|
const flags = {};
|
|
@@ -753,7 +1535,7 @@ function parseArgs(argv) {
|
|
|
753
1535
|
if (eqIdx !== -1) {
|
|
754
1536
|
const raw = arg.slice(2, eqIdx);
|
|
755
1537
|
const flagName = normalizeLongFlag(raw);
|
|
756
|
-
flags
|
|
1538
|
+
setParsedFlagValue(flags, flagName, arg.slice(eqIdx + 1));
|
|
757
1539
|
}
|
|
758
1540
|
else {
|
|
759
1541
|
const rawFlagName = arg.slice(2);
|
|
@@ -763,7 +1545,7 @@ function parseArgs(argv) {
|
|
|
763
1545
|
}
|
|
764
1546
|
else {
|
|
765
1547
|
const next = requireFlagValue(`--${rawFlagName}`, argv[i + 1]);
|
|
766
|
-
flags
|
|
1548
|
+
setParsedFlagValue(flags, flagName, next);
|
|
767
1549
|
i++;
|
|
768
1550
|
}
|
|
769
1551
|
}
|
|
@@ -776,7 +1558,7 @@ function parseArgs(argv) {
|
|
|
776
1558
|
}
|
|
777
1559
|
else {
|
|
778
1560
|
const next = requireFlagValue(`-${arg[1]}`, argv[i + 1]);
|
|
779
|
-
flags
|
|
1561
|
+
setParsedFlagValue(flags, long, next);
|
|
780
1562
|
i++;
|
|
781
1563
|
}
|
|
782
1564
|
}
|
|
@@ -1485,10 +2267,45 @@ const commands = {
|
|
|
1485
2267
|
},
|
|
1486
2268
|
"flow-zone": {
|
|
1487
2269
|
list: {
|
|
1488
|
-
handler: (c, _a, f) =>
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
2270
|
+
handler: async (c, _a, f) => {
|
|
2271
|
+
const zones = await c.flowZones.list(f["project-key"]);
|
|
2272
|
+
if (f["summary"] !== true)
|
|
2273
|
+
return zones;
|
|
2274
|
+
const objects = flowZoneMoveItems(f);
|
|
2275
|
+
const object = objects.length === 1 ? objects[0] : undefined;
|
|
2276
|
+
return zones.map((zone) => flowZoneSummary(zone, object));
|
|
2277
|
+
},
|
|
2278
|
+
usage: "dss flow-zone list [--summary] [--object TYPE:ID] [--project-key KEY]",
|
|
2279
|
+
description: "List flow zones in a project, optionally as compact summaries.",
|
|
2280
|
+
examples: ["dss flow-zone list", "dss flow-zone list --summary --object RECIPE:compute_orders",],
|
|
2281
|
+
},
|
|
2282
|
+
find: {
|
|
2283
|
+
handler: async (c, a, f) => {
|
|
2284
|
+
const zones = await c.flowZones.list(f["project-key"]);
|
|
2285
|
+
const objects = flowZoneMoveItems(f);
|
|
2286
|
+
const query = a[0]?.trim();
|
|
2287
|
+
if (query && objects.length === 0) {
|
|
2288
|
+
const normalized = query.toLowerCase();
|
|
2289
|
+
return zones
|
|
2290
|
+
.filter((zone) => zone.id.toLowerCase().includes(normalized)
|
|
2291
|
+
|| zone.name.toLowerCase().includes(normalized))
|
|
2292
|
+
.map((zone) => flowZoneDetailSummary(zone));
|
|
2293
|
+
}
|
|
2294
|
+
if (objects.length !== 1) {
|
|
2295
|
+
throw new UsageError("Exactly one zone name/id or object is required. Use <name>, --object TYPE:ID, --dataset DS, or --recipe R.");
|
|
2296
|
+
}
|
|
2297
|
+
const object = objects[0];
|
|
2298
|
+
return zones
|
|
2299
|
+
.filter((zone) => flowZoneContains(zone, object))
|
|
2300
|
+
.map((zone) => flowZoneSummary(zone, object));
|
|
2301
|
+
},
|
|
2302
|
+
usage: "dss flow-zone find [name-or-id] [--object TYPE:ID | --dataset DS | --recipe R | --folder F] [--project-key KEY]",
|
|
2303
|
+
description: "Find flow zones by name/id or by contained flow object.",
|
|
2304
|
+
examples: [
|
|
2305
|
+
"dss flow-zone find ATH_SNW_MAP_FRG49",
|
|
2306
|
+
"dss flow-zone find --object RECIPE:compute_orders",
|
|
2307
|
+
"dss flow-zone find --dataset orders",
|
|
2308
|
+
],
|
|
1492
2309
|
},
|
|
1493
2310
|
get: {
|
|
1494
2311
|
handler: (c, a, f) => {
|
|
@@ -1583,7 +2400,11 @@ const commands = {
|
|
|
1583
2400
|
},
|
|
1584
2401
|
move: {
|
|
1585
2402
|
handler: async (c, a, f) => {
|
|
1586
|
-
|
|
2403
|
+
const pk = f["project-key"];
|
|
2404
|
+
const zoneId = a[0] ? flowZoneId(a[0]) : await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2405
|
+
if (!zoneId) {
|
|
2406
|
+
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]");
|
|
2407
|
+
}
|
|
1587
2408
|
const items = flowZoneMoveItems(f);
|
|
1588
2409
|
if (items.length === 0) {
|
|
1589
2410
|
throw new UsageError("At least one object is required. Use --dataset, --recipe, --folder, or --object TYPE:ID.");
|
|
@@ -1593,21 +2414,125 @@ const commands = {
|
|
|
1593
2414
|
dryRun: true,
|
|
1594
2415
|
action: "move",
|
|
1595
2416
|
resource: "flow-zone",
|
|
1596
|
-
id:
|
|
2417
|
+
id: zoneId,
|
|
1597
2418
|
items,
|
|
1598
2419
|
};
|
|
1599
2420
|
}
|
|
1600
|
-
return c.flowZones.moveItems(
|
|
2421
|
+
return c.flowZones.moveItems(zoneId, items, pk);
|
|
1601
2422
|
},
|
|
1602
|
-
usage: "dss flow-zone move
|
|
1603
|
-
description: "Move datasets, recipes, managed folders, or other flow objects into a zone.",
|
|
2423
|
+
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]",
|
|
2424
|
+
description: "Move datasets, recipes, managed folders, or other flow objects into a zone by id or --zone name.",
|
|
1604
2425
|
examples: [
|
|
1605
2426
|
"dss flow-zone move ZONE_ID --dataset orders --dry-run",
|
|
1606
|
-
"dss flow-zone move
|
|
2427
|
+
"dss flow-zone move --zone ATH_SNW_MAP_FRG49 --dataset raw_orders,clean_orders --recipe prepare_orders",
|
|
1607
2428
|
"dss flow-zone move ZONE_ID --folder FOLDER_ID",
|
|
1608
2429
|
"dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
|
|
1609
2430
|
],
|
|
1610
2431
|
},
|
|
2432
|
+
organize: {
|
|
2433
|
+
handler: async (c, _a, f) => {
|
|
2434
|
+
const usage = "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]";
|
|
2435
|
+
const pk = f["project-key"];
|
|
2436
|
+
const plan = readFlowZoneOrganizePlan(f, usage);
|
|
2437
|
+
const sync = f["sync"] === true;
|
|
2438
|
+
const validateObjects = f["validate-objects"] === true;
|
|
2439
|
+
const zones = await c.flowZones.list(pk);
|
|
2440
|
+
const plannedItemKeys = flowZonePlanItemKeys(plan);
|
|
2441
|
+
const planned = plan.zones.map((zonePlan) => flowZoneOrganizeStep(zonePlan, findFlowZoneForPlan(zones, zonePlan), sync, plannedItemKeys));
|
|
2442
|
+
const validation = validateObjects
|
|
2443
|
+
? await validateFlowZoneOrganizeObjects(c, plan, pk)
|
|
2444
|
+
: undefined;
|
|
2445
|
+
if (validation)
|
|
2446
|
+
throwFlowZoneValidationError(validation);
|
|
2447
|
+
const itemCount = plan.zones.reduce((count, zonePlan) => count + zonePlan.items.length, 0);
|
|
2448
|
+
const pruneItemCount = planned.reduce((count, step) => {
|
|
2449
|
+
const pruneItems = Array.isArray(step.pruneItems) ? step.pruneItems : [];
|
|
2450
|
+
return count + pruneItems.length;
|
|
2451
|
+
}, 0);
|
|
2452
|
+
if (f["dry-run"] === true) {
|
|
2453
|
+
return {
|
|
2454
|
+
dryRun: true,
|
|
2455
|
+
action: "organize",
|
|
2456
|
+
resource: "flow-zone",
|
|
2457
|
+
projectKey: pk,
|
|
2458
|
+
sync,
|
|
2459
|
+
validateObjects,
|
|
2460
|
+
zoneCount: plan.zones.length,
|
|
2461
|
+
itemCount,
|
|
2462
|
+
pruneItemCount,
|
|
2463
|
+
...(validation ? { validation, } : {}),
|
|
2464
|
+
planned,
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
const currentZones = [...zones,];
|
|
2468
|
+
const created = [];
|
|
2469
|
+
const updated = [];
|
|
2470
|
+
const moved = [];
|
|
2471
|
+
const pruned = [];
|
|
2472
|
+
for (const zonePlan of plan.zones) {
|
|
2473
|
+
let zone = findFlowZoneForPlan(currentZones, zonePlan);
|
|
2474
|
+
ensureFlowZonePlanTarget(zonePlan, zone);
|
|
2475
|
+
const pruneItems = sync ? flowZonePruneItems(zone, plannedItemKeys) : [];
|
|
2476
|
+
if (!zone) {
|
|
2477
|
+
zone = await c.flowZones.create({
|
|
2478
|
+
name: zonePlan.name,
|
|
2479
|
+
color: zonePlan.color,
|
|
2480
|
+
position: zonePlan.position,
|
|
2481
|
+
projectKey: pk,
|
|
2482
|
+
});
|
|
2483
|
+
currentZones.push(zone);
|
|
2484
|
+
created.push(zone);
|
|
2485
|
+
}
|
|
2486
|
+
else {
|
|
2487
|
+
const patch = {
|
|
2488
|
+
...(zonePlan.name && zonePlan.name !== zone.name ? { name: zonePlan.name, } : {}),
|
|
2489
|
+
...(zonePlan.color && zonePlan.color !== zone.color ? { color: zonePlan.color, } : {}),
|
|
2490
|
+
...(zonePlan.position !== undefined
|
|
2491
|
+
&& !flowZoneSamePosition(flowZoneCurrentPosition(zone), zonePlan.position)
|
|
2492
|
+
? { position: zonePlan.position, }
|
|
2493
|
+
: {}),
|
|
2494
|
+
projectKey: pk,
|
|
2495
|
+
};
|
|
2496
|
+
if (patch.name !== undefined || patch.color !== undefined || patch.position !== undefined) {
|
|
2497
|
+
zone = await c.flowZones.update(zone.id, patch);
|
|
2498
|
+
const index = currentZones.findIndex((candidate) => candidate.id === zone.id);
|
|
2499
|
+
if (index !== -1)
|
|
2500
|
+
currentZones[index] = zone;
|
|
2501
|
+
updated.push(zone);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
if (zonePlan.items.length > 0) {
|
|
2505
|
+
await c.flowZones.moveItems(zone.id, zonePlan.items, pk);
|
|
2506
|
+
moved.push({ zoneId: zone.id, name: zone.name, items: zonePlan.items, });
|
|
2507
|
+
}
|
|
2508
|
+
if (pruneItems.length > 0) {
|
|
2509
|
+
await c.flowZones.moveItems("default", pruneItems, pk);
|
|
2510
|
+
pruned.push({ zoneId: "default", fromZoneId: zone.id, name: zone.name, items: pruneItems, });
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
return {
|
|
2514
|
+
organized: true,
|
|
2515
|
+
action: "organize",
|
|
2516
|
+
resource: "flow-zone",
|
|
2517
|
+
projectKey: pk,
|
|
2518
|
+
sync,
|
|
2519
|
+
validateObjects,
|
|
2520
|
+
zoneCount: plan.zones.length,
|
|
2521
|
+
itemCount,
|
|
2522
|
+
pruneItemCount,
|
|
2523
|
+
created,
|
|
2524
|
+
updated,
|
|
2525
|
+
moved,
|
|
2526
|
+
pruned,
|
|
2527
|
+
};
|
|
2528
|
+
},
|
|
2529
|
+
usage: "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]",
|
|
2530
|
+
description: "Create/update flow zones and move objects from a declarative visual organization plan.",
|
|
2531
|
+
examples: [
|
|
2532
|
+
"dss flow-zone organize --file flow-zones.json --dry-run",
|
|
2533
|
+
`dss flow-zone organize --data '{"zones":[{"name":"Raw","color":"#64748b","datasets":["raw_orders"]}]}'`,
|
|
2534
|
+
],
|
|
2535
|
+
},
|
|
1611
2536
|
graph: {
|
|
1612
2537
|
handler: (c, a, f) => {
|
|
1613
2538
|
requireArgs(a, 1, "dss flow-zone graph <id>");
|
|
@@ -1643,6 +2568,15 @@ const commands = {
|
|
|
1643
2568
|
description: "Show the column schema of a dataset.",
|
|
1644
2569
|
examples: ["dss dataset schema orders",],
|
|
1645
2570
|
},
|
|
2571
|
+
source: {
|
|
2572
|
+
handler: async (c, a, f) => {
|
|
2573
|
+
requireArgs(a, 1, "dss dataset source <name>");
|
|
2574
|
+
return datasetSourceSummary(await c.datasets.get(a[0], f["project-key"]));
|
|
2575
|
+
},
|
|
2576
|
+
usage: "dss dataset source <name> [--project-key KEY]",
|
|
2577
|
+
description: "Show backing connection, catalog/schema/table, path, and format for a dataset.",
|
|
2578
|
+
examples: ["dss dataset source orders",],
|
|
2579
|
+
},
|
|
1646
2580
|
"refresh-schema": {
|
|
1647
2581
|
handler: async (c, a, f) => {
|
|
1648
2582
|
const usage = "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]";
|
|
@@ -1727,6 +2661,7 @@ const commands = {
|
|
|
1727
2661
|
dsType,
|
|
1728
2662
|
projectKey: pk,
|
|
1729
2663
|
};
|
|
2664
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
1730
2665
|
if (f["if-not-exists"] === true || f["dry-run"] === true) {
|
|
1731
2666
|
const list = await c.datasets.list(pk);
|
|
1732
2667
|
const existing = list.find((d) => d.name === name);
|
|
@@ -1741,17 +2676,57 @@ const commands = {
|
|
|
1741
2676
|
name,
|
|
1742
2677
|
payload,
|
|
1743
2678
|
...(existing ? { current: existing, } : {}),
|
|
2679
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "DATASET", },], } : {}),
|
|
1744
2680
|
};
|
|
1745
2681
|
}
|
|
1746
2682
|
}
|
|
1747
2683
|
await c.datasets.create(payload);
|
|
1748
|
-
|
|
2684
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "DATASET", },], pk);
|
|
2685
|
+
return { created: name, resource: "dataset", ...moved, };
|
|
1749
2686
|
},
|
|
1750
|
-
usage: "dss dataset create --name NAME --connection CONN --type TYPE [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
2687
|
+
usage: "dss dataset create --name NAME --connection CONN --type TYPE [--zone ZONE|--zone-id ID] [--if-not-exists] [--dry-run] [--project-key KEY]",
|
|
1751
2688
|
description: "Create a new dataset.",
|
|
1752
2689
|
examples: [
|
|
1753
2690
|
"dss dataset create --name orders --connection filesystem --type Filesystem",
|
|
1754
|
-
"dss dataset create --name orders --connection filesystem --type Filesystem --dry-run",
|
|
2691
|
+
"dss dataset create --name orders --connection filesystem --type Filesystem --zone Experiments --dry-run",
|
|
2692
|
+
],
|
|
2693
|
+
},
|
|
2694
|
+
clone: {
|
|
2695
|
+
handler: async (c, a, f) => {
|
|
2696
|
+
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]";
|
|
2697
|
+
requireArgs(a, 2, usage);
|
|
2698
|
+
const pk = f["project-key"];
|
|
2699
|
+
const opts = {
|
|
2700
|
+
projectKey: pk,
|
|
2701
|
+
path: f["path"],
|
|
2702
|
+
table: f["table"],
|
|
2703
|
+
metastoreTableName: f["metastore-table"],
|
|
2704
|
+
allowSamePath: f["allow-same-path"] === true,
|
|
2705
|
+
};
|
|
2706
|
+
const current = await c.datasets.get(a[0], pk);
|
|
2707
|
+
const next = buildDatasetCloneSettings(current, a[1], pk ?? c.resolveProjectKey(pk), opts);
|
|
2708
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2709
|
+
if (f["dry-run"] === true) {
|
|
2710
|
+
return {
|
|
2711
|
+
dryRun: true,
|
|
2712
|
+
action: "clone",
|
|
2713
|
+
resource: "dataset",
|
|
2714
|
+
source: a[0],
|
|
2715
|
+
target: a[1],
|
|
2716
|
+
current,
|
|
2717
|
+
next,
|
|
2718
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: a[1], objectType: "DATASET", },], } : {}),
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
const cloned = await c.datasets.clone(a[0], a[1], opts);
|
|
2722
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: a[1], objectType: "DATASET", },], pk);
|
|
2723
|
+
return { ...cloned, resource: "dataset", ...moved, };
|
|
2724
|
+
},
|
|
2725
|
+
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]",
|
|
2726
|
+
description: "Clone dataset settings into a new dataset, with storage/table overrides.",
|
|
2727
|
+
examples: [
|
|
2728
|
+
"dss dataset clone source_ds experiment_ds --path /dataiku/TEST/experiment_ds --dry-run",
|
|
2729
|
+
"dss dataset clone source_ds experiment_ds --allow-same-path",
|
|
1755
2730
|
],
|
|
1756
2731
|
},
|
|
1757
2732
|
delete: {
|
|
@@ -1942,15 +2917,20 @@ const commands = {
|
|
|
1942
2917
|
}
|
|
1943
2918
|
const name = f["name"];
|
|
1944
2919
|
const pk = f["project-key"];
|
|
2920
|
+
const inputDatasets = recipeInputDatasetsFromFlags(f);
|
|
1945
2921
|
const payload = {
|
|
1946
2922
|
type,
|
|
1947
2923
|
name,
|
|
1948
|
-
inputDatasets
|
|
2924
|
+
inputDatasets,
|
|
1949
2925
|
outputDataset,
|
|
1950
2926
|
outputFolder,
|
|
1951
2927
|
outputConnection: f["output-connection"],
|
|
1952
2928
|
projectKey: pk,
|
|
1953
2929
|
};
|
|
2930
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
2931
|
+
const zoneMove = zoneId && name
|
|
2932
|
+
? [{ objectId: name, objectType: "RECIPE", },]
|
|
2933
|
+
: undefined;
|
|
1954
2934
|
if ((f["if-not-exists"] === true || f["dry-run"] === true) && name) {
|
|
1955
2935
|
const list = await c.recipes.list(pk);
|
|
1956
2936
|
const existing = list.find((r) => r.name === name);
|
|
@@ -1964,24 +2944,106 @@ const commands = {
|
|
|
1964
2944
|
resource: "recipe",
|
|
1965
2945
|
name,
|
|
1966
2946
|
payload,
|
|
2947
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
1967
2948
|
...(existing ? { current: existing, } : {}),
|
|
1968
2949
|
};
|
|
1969
2950
|
}
|
|
1970
2951
|
}
|
|
1971
2952
|
if (f["dry-run"] === true) {
|
|
1972
|
-
return {
|
|
2953
|
+
return {
|
|
2954
|
+
dryRun: true,
|
|
2955
|
+
action: "create",
|
|
2956
|
+
resource: "recipe",
|
|
2957
|
+
payload,
|
|
2958
|
+
...(zoneId ? { zoneId, zoneMove, } : {}),
|
|
2959
|
+
};
|
|
1973
2960
|
}
|
|
1974
2961
|
const created = await c.recipes.create(payload);
|
|
1975
|
-
|
|
2962
|
+
const createdName = created.recipeName;
|
|
2963
|
+
const moved = await moveCreatedItemsToZone(c, f, [{
|
|
2964
|
+
objectId: createdName,
|
|
2965
|
+
objectType: "RECIPE",
|
|
2966
|
+
},], pk);
|
|
2967
|
+
return { created: createdName, resource: "recipe", ...created, ...moved, };
|
|
1976
2968
|
},
|
|
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
|
|
2969
|
+
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]",
|
|
2970
|
+
description: "Create a recipe with one or more inputs and a dataset or managed-folder output.",
|
|
1979
2971
|
examples: [
|
|
1980
|
-
"dss recipe create --type python --input
|
|
1981
|
-
"dss recipe create --type python --input orders --output orders_clean --
|
|
2972
|
+
"dss recipe create --type python --input raw_orders,lookup --output orders_clean",
|
|
2973
|
+
"dss recipe create --type python --input orders --input customers --output orders_clean --zone Experiments",
|
|
1982
2974
|
"dss recipe create --type python --input orders --output-folder LT7TUHJ8 --output-connection filesystem --dry-run",
|
|
1983
2975
|
],
|
|
1984
2976
|
},
|
|
2977
|
+
clone: {
|
|
2978
|
+
handler: async (c, a, f) => {
|
|
2979
|
+
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]";
|
|
2980
|
+
const fromFlag = typeof f["from"] === "string" ? f["from"].trim() : "";
|
|
2981
|
+
const sourceName = a[0] ?? fromFlag;
|
|
2982
|
+
if (!sourceName) {
|
|
2983
|
+
throw new UsageError(`Source recipe is required. Usage: ${usage}`, "missing_required_flag");
|
|
2984
|
+
}
|
|
2985
|
+
if (a[0] && fromFlag && a[0] !== fromFlag) {
|
|
2986
|
+
throw new UsageError("Positional source and --from must match when both are provided.", "invalid_enum");
|
|
2987
|
+
}
|
|
2988
|
+
const pk = f["project-key"];
|
|
2989
|
+
const toFlag = typeof f["to"] === "string" ? f["to"].trim() : "";
|
|
2990
|
+
const nameFlag = typeof f["name"] === "string" ? f["name"].trim() : "";
|
|
2991
|
+
const name = toFlag || nameFlag;
|
|
2992
|
+
if (!name) {
|
|
2993
|
+
throw new UsageError(`--name or --to is required. Usage: ${usage}`, "missing_required_flag");
|
|
2994
|
+
}
|
|
2995
|
+
const inputRewrites = rewritePairsFromFlags(f, "replace-input");
|
|
2996
|
+
const outputRewrites = rewritePairsFromFlags(f, "replace-output");
|
|
2997
|
+
const payloadTextRewrites = rewritePairsFromFlags(f, "replace-payload-text");
|
|
2998
|
+
const opts = {
|
|
2999
|
+
projectKey: pk,
|
|
3000
|
+
name,
|
|
3001
|
+
outputDataset: f["output"],
|
|
3002
|
+
outputRewrites,
|
|
3003
|
+
inputRewrites,
|
|
3004
|
+
payloadTextRewrites,
|
|
3005
|
+
copyOutputSettings: f["copy-output-settings"] === true,
|
|
3006
|
+
outputPath: f["path"],
|
|
3007
|
+
metastoreTableName: f["metastore-table"],
|
|
3008
|
+
};
|
|
3009
|
+
const source = await c.recipes.get(sourceName, { includePayload: true, projectKey: pk, });
|
|
3010
|
+
const outputItems = Object.values((source.recipe.outputs ?? {})).flatMap((role) => role.items ?? []).filter((item) => typeof item.ref === "string");
|
|
3011
|
+
const plannedOutputRewrites = { ...outputRewrites, };
|
|
3012
|
+
if (opts.outputDataset !== undefined && outputItems.length === 1) {
|
|
3013
|
+
plannedOutputRewrites[outputItems[0].ref] = opts.outputDataset;
|
|
3014
|
+
}
|
|
3015
|
+
if (opts.copyOutputSettings === true
|
|
3016
|
+
&& Object.keys(plannedOutputRewrites).length > 1
|
|
3017
|
+
&& (opts.outputPath !== undefined || opts.metastoreTableName !== undefined)) {
|
|
3018
|
+
throw new UsageError("Cannot reuse --path or --metastore-table for multiple cloned output datasets.", "invalid_enum");
|
|
3019
|
+
}
|
|
3020
|
+
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
3021
|
+
if (f["dry-run"] === true) {
|
|
3022
|
+
return {
|
|
3023
|
+
dryRun: true,
|
|
3024
|
+
action: "clone",
|
|
3025
|
+
resource: "recipe",
|
|
3026
|
+
source: sourceName,
|
|
3027
|
+
target: name,
|
|
3028
|
+
inputRewrites,
|
|
3029
|
+
outputRewrites: plannedOutputRewrites,
|
|
3030
|
+
copyOutputSettings: opts.copyOutputSettings,
|
|
3031
|
+
payloadTextRewrites,
|
|
3032
|
+
current: source,
|
|
3033
|
+
...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "RECIPE", },], } : {}),
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
const cloned = await c.recipes.clone(sourceName, opts);
|
|
3037
|
+
const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "RECIPE", },], pk);
|
|
3038
|
+
return { ...cloned, resource: "recipe", ...moved, };
|
|
3039
|
+
},
|
|
3040
|
+
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]",
|
|
3041
|
+
description: "Clone a recipe graph/settings/payload into a separate experiment recipe.",
|
|
3042
|
+
examples: [
|
|
3043
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --copy-output-settings --dry-run",
|
|
3044
|
+
"dss recipe clone compute_orders --name compute_orders_opt --output orders_opt --zone Experiments",
|
|
3045
|
+
],
|
|
3046
|
+
},
|
|
1985
3047
|
diff: {
|
|
1986
3048
|
handler: async (c, a, f) => {
|
|
1987
3049
|
requireArgs(a, 1, "dss recipe diff <name> --file PATH");
|
|
@@ -2046,13 +3108,24 @@ const commands = {
|
|
|
2046
3108
|
}
|
|
2047
3109
|
return payload;
|
|
2048
3110
|
},
|
|
2049
|
-
usage: "dss recipe get-payload <name> [--output PATH] [--project-key KEY]",
|
|
2050
|
-
description: "Print the recipe code payload to stdout.",
|
|
3111
|
+
usage: "dss recipe get-payload <name> [--raw] [--output PATH] [--project-key KEY]",
|
|
3112
|
+
description: "Print the recipe code payload to stdout; use --raw for pipeable code bytes.",
|
|
2051
3113
|
examples: [
|
|
2052
|
-
"dss recipe get-payload compute_orders",
|
|
3114
|
+
"dss recipe get-payload compute_orders --raw",
|
|
2053
3115
|
"dss recipe get-payload compute_orders -o code.py",
|
|
2054
3116
|
],
|
|
2055
3117
|
},
|
|
3118
|
+
cat: {
|
|
3119
|
+
handler: (c, a, f) => {
|
|
3120
|
+
requireArgs(a, 1, "dss recipe cat <name> [--raw]");
|
|
3121
|
+
return c.recipes.getPayload(a[0], {
|
|
3122
|
+
projectKey: f["project-key"],
|
|
3123
|
+
});
|
|
3124
|
+
},
|
|
3125
|
+
usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
|
|
3126
|
+
description: "Print a recipe code payload; combine with --raw for shell pipes and diffs.",
|
|
3127
|
+
examples: ["dss recipe cat compute_orders --raw",],
|
|
3128
|
+
},
|
|
2056
3129
|
"set-payload": {
|
|
2057
3130
|
handler: async (c, a, f) => {
|
|
2058
3131
|
requireArgs(a, 1, "dss recipe set-payload <name> --file PATH");
|
|
@@ -2060,13 +3133,17 @@ const commands = {
|
|
|
2060
3133
|
if (!filePath)
|
|
2061
3134
|
throw new UsageError("--file is required.");
|
|
2062
3135
|
const content = readFileSync(filePath, "utf-8");
|
|
2063
|
-
const
|
|
2064
|
-
const
|
|
3136
|
+
const pk = f["project-key"];
|
|
3137
|
+
const shouldBackup = f["no-backup"] !== true;
|
|
3138
|
+
const backupDir = shouldBackup
|
|
3139
|
+
? f["backup-dir"] ?? join(process.cwd(), ".dss-backups", "recipes")
|
|
3140
|
+
: undefined;
|
|
3141
|
+
const backupPath = backupDir ? recipeBackupPath(a[0], backupDir) : undefined;
|
|
3142
|
+
const current = await c.recipes.get(a[0], {
|
|
3143
|
+
projectKey: pk,
|
|
3144
|
+
includePayload: true,
|
|
3145
|
+
});
|
|
2065
3146
|
if (f["dry-run"] === true) {
|
|
2066
|
-
const current = await c.recipes.get(a[0], {
|
|
2067
|
-
projectKey: f["project-key"],
|
|
2068
|
-
includePayload: true,
|
|
2069
|
-
});
|
|
2070
3147
|
return {
|
|
2071
3148
|
dryRun: true,
|
|
2072
3149
|
action: "set-payload",
|
|
@@ -2075,41 +3152,140 @@ const commands = {
|
|
|
2075
3152
|
file: filePath,
|
|
2076
3153
|
current,
|
|
2077
3154
|
next: { ...current, payload: content, },
|
|
2078
|
-
...(backupPath ? { backupPath, } : {}),
|
|
3155
|
+
...(backupPath ? { backupPath, backup: recipeBackupDocument(a[0], pk, current), } : {}),
|
|
2079
3156
|
};
|
|
2080
3157
|
}
|
|
2081
3158
|
if (backupDir && backupPath) {
|
|
2082
|
-
const current = await c.recipes.get(a[0], {
|
|
2083
|
-
projectKey: f["project-key"],
|
|
2084
|
-
includePayload: true,
|
|
2085
|
-
});
|
|
2086
3159
|
await mkdir(backupDir, { recursive: true, });
|
|
2087
|
-
await writeFile(backupPath,
|
|
3160
|
+
await writeFile(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`, "utf-8");
|
|
2088
3161
|
}
|
|
2089
|
-
await c.recipes.
|
|
2090
|
-
projectKey: f["project-key"],
|
|
2091
|
-
});
|
|
3162
|
+
await c.recipes.replace(a[0], { ...current, payload: content, }, pk);
|
|
2092
3163
|
return {
|
|
2093
3164
|
updated: a[0],
|
|
2094
3165
|
resource: "recipe",
|
|
2095
3166
|
file: filePath,
|
|
3167
|
+
backupCreated: backupPath !== undefined,
|
|
2096
3168
|
...(backupPath ? { backupPath, } : {}),
|
|
2097
3169
|
};
|
|
2098
3170
|
},
|
|
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,
|
|
3171
|
+
usage: "dss recipe set-payload <name> --file PATH [--backup-dir DIR|--no-backup] [--dry-run] [--project-key KEY]",
|
|
3172
|
+
description: "Upload recipe code from a local file, backing up payload, graph, settings, and version metadata by default.",
|
|
2101
3173
|
examples: [
|
|
2102
3174
|
"dss recipe set-payload compute_orders --file code.py --dry-run",
|
|
2103
3175
|
"dss recipe set-payload compute_orders --file code.py --backup-dir ./backups",
|
|
3176
|
+
"dss recipe set-payload compute_orders --file code.py --no-backup",
|
|
3177
|
+
],
|
|
3178
|
+
},
|
|
3179
|
+
restore: {
|
|
3180
|
+
handler: async (c, a, f) => {
|
|
3181
|
+
const usage = "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]";
|
|
3182
|
+
requireArgs(a, 1, usage);
|
|
3183
|
+
const backupPath = requiredStringFlag(f, "backup", usage);
|
|
3184
|
+
const backup = readRecipeBackup(backupPath);
|
|
3185
|
+
const payload = typeof backup.payload === "string" ? backup.payload : "";
|
|
3186
|
+
const pk = f["project-key"];
|
|
3187
|
+
const current = await c.recipes.get(a[0], { includePayload: true, projectKey: pk, });
|
|
3188
|
+
const backupRecipe = backup.recipe && typeof backup.recipe === "object" && !Array.isArray(backup.recipe)
|
|
3189
|
+
? backup.recipe
|
|
3190
|
+
: undefined;
|
|
3191
|
+
const restoredRecipe = backupRecipe
|
|
3192
|
+
? { ...backupRecipe, name: a[0], ...(pk ? { projectKey: pk, } : {}), }
|
|
3193
|
+
: undefined;
|
|
3194
|
+
const next = f["payload-only"] === true || !restoredRecipe
|
|
3195
|
+
? { ...current, payload, }
|
|
3196
|
+
: { ...current, recipe: restoredRecipe, payload, };
|
|
3197
|
+
if (f["dry-run"] === true) {
|
|
3198
|
+
return {
|
|
3199
|
+
dryRun: true,
|
|
3200
|
+
action: "restore",
|
|
3201
|
+
resource: "recipe",
|
|
3202
|
+
name: a[0],
|
|
3203
|
+
backupPath,
|
|
3204
|
+
current,
|
|
3205
|
+
next,
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
await c.recipes.replace(a[0], next, pk);
|
|
3209
|
+
return {
|
|
3210
|
+
restored: a[0],
|
|
3211
|
+
resource: "recipe",
|
|
3212
|
+
backupPath,
|
|
3213
|
+
payloadOnly: f["payload-only"] === true,
|
|
3214
|
+
};
|
|
3215
|
+
},
|
|
3216
|
+
usage: "dss recipe restore <name> --backup FILE [--payload-only] [--dry-run] [--project-key KEY]",
|
|
3217
|
+
description: "Restore a recipe from a set-payload backup.",
|
|
3218
|
+
examples: [
|
|
3219
|
+
"dss recipe restore compute_orders --backup .dss-backups/recipes/backup.recipe-backup.json --dry-run",
|
|
3220
|
+
],
|
|
3221
|
+
},
|
|
3222
|
+
"assert-unchanged": {
|
|
3223
|
+
handler: async (c, a, f) => {
|
|
3224
|
+
const usage = "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]";
|
|
3225
|
+
requireArgs(a, 1, usage);
|
|
3226
|
+
const backupPath = requiredStringFlag(f, "since", usage);
|
|
3227
|
+
const backup = readRecipeBackup(backupPath);
|
|
3228
|
+
const current = await c.recipes.get(a[0], {
|
|
3229
|
+
includePayload: true,
|
|
3230
|
+
projectKey: f["project-key"],
|
|
3231
|
+
});
|
|
3232
|
+
const payloadHash = sha256Hex(current.payload ?? "");
|
|
3233
|
+
const normalizedPayloadHash = sha256Hex(normalizeLineEndings(current.payload ?? ""));
|
|
3234
|
+
const expectedPayloadHash = typeof backup.payloadHash === "string"
|
|
3235
|
+
? backup.payloadHash
|
|
3236
|
+
: undefined;
|
|
3237
|
+
const expectedNormalizedPayloadHash = typeof backup.normalizedPayloadHash === "string"
|
|
3238
|
+
? backup.normalizedPayloadHash
|
|
3239
|
+
: typeof backup.payload === "string"
|
|
3240
|
+
? sha256Hex(normalizeLineEndings(backup.payload))
|
|
3241
|
+
: undefined;
|
|
3242
|
+
const checks = [
|
|
3243
|
+
{
|
|
3244
|
+
name: "payload",
|
|
3245
|
+
expected: expectedPayloadHash,
|
|
3246
|
+
actual: payloadHash,
|
|
3247
|
+
unchanged: expectedPayloadHash === payloadHash
|
|
3248
|
+
|| (expectedNormalizedPayloadHash !== undefined
|
|
3249
|
+
&& expectedNormalizedPayloadHash === normalizedPayloadHash),
|
|
3250
|
+
normalizedExpected: expectedNormalizedPayloadHash,
|
|
3251
|
+
normalizedActual: normalizedPayloadHash,
|
|
3252
|
+
},
|
|
3253
|
+
{
|
|
3254
|
+
name: "graph",
|
|
3255
|
+
expected: backup.graphHash,
|
|
3256
|
+
actual: stableHash(recipeGraph(current.recipe)),
|
|
3257
|
+
unchanged: backup.graphHash === stableHash(recipeGraph(current.recipe)),
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
name: "codeEnv",
|
|
3261
|
+
expected: backup.codeEnvHash,
|
|
3262
|
+
actual: stableHash(recipeCodeEnv(current.recipe)),
|
|
3263
|
+
unchanged: backup.codeEnvHash === stableHash(recipeCodeEnv(current.recipe)),
|
|
3264
|
+
},
|
|
3265
|
+
].filter((check) => typeof check.expected === "string");
|
|
3266
|
+
const failures = checks.filter((check) => !check.unchanged);
|
|
3267
|
+
return {
|
|
3268
|
+
unchanged: failures.length === 0,
|
|
3269
|
+
resource: "recipe",
|
|
3270
|
+
name: a[0],
|
|
3271
|
+
backupPath,
|
|
3272
|
+
checks,
|
|
3273
|
+
failures,
|
|
3274
|
+
};
|
|
3275
|
+
},
|
|
3276
|
+
usage: "dss recipe assert-unchanged <name> --since BACKUP [--project-key KEY]",
|
|
3277
|
+
description: "Compare current recipe payload, graph, and code env against a backup.",
|
|
3278
|
+
examples: [
|
|
3279
|
+
"dss recipe assert-unchanged compute_orders --since .dss-backups/recipes/backup.recipe-backup.json",
|
|
2104
3280
|
],
|
|
2105
3281
|
},
|
|
2106
3282
|
},
|
|
2107
3283
|
job: {
|
|
2108
3284
|
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",],
|
|
3285
|
+
handler: async (c, _a, f) => filteredJobList(await c.jobs.list(f["project-key"]), f),
|
|
3286
|
+
usage: "dss job list [--state STATE] [--contains TEXT] [--output ID] [--latest] [--limit N] [--project-key KEY]",
|
|
3287
|
+
description: "List recent jobs, optionally filtered for automation.",
|
|
3288
|
+
examples: ["dss job list --state DONE --latest", "dss job list --contains WLM225S --limit 10",],
|
|
2113
3289
|
},
|
|
2114
3290
|
get: {
|
|
2115
3291
|
handler: (c, a, f) => {
|
|
@@ -2120,18 +3296,42 @@ const commands = {
|
|
|
2120
3296
|
description: "Get job details.",
|
|
2121
3297
|
examples: ["dss job get JOB_ID",],
|
|
2122
3298
|
},
|
|
3299
|
+
summary: {
|
|
3300
|
+
handler: (c, a, f) => {
|
|
3301
|
+
requireArgs(a, 1, "dss job summary <id>");
|
|
3302
|
+
return jobInspectionSummary(c, a[0], f);
|
|
3303
|
+
},
|
|
3304
|
+
usage: "dss job summary <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
|
|
3305
|
+
description: "Summarize job state, outputs, warnings, progress, and useful terminal log lines.",
|
|
3306
|
+
examples: ["dss job summary JOB_ID --max-log-lines 200",],
|
|
3307
|
+
},
|
|
2123
3308
|
log: {
|
|
2124
3309
|
handler: (c, a, f) => {
|
|
2125
3310
|
requireArgs(a, 1, "dss job log <id>");
|
|
2126
3311
|
return c.jobs.log(a[0], {
|
|
2127
3312
|
activity: f["activity"],
|
|
3313
|
+
logId: f["log-id"],
|
|
2128
3314
|
maxLogLines: maxLogLinesFromFlags(f),
|
|
2129
3315
|
projectKey: f["project-key"],
|
|
2130
3316
|
});
|
|
2131
3317
|
},
|
|
2132
|
-
usage: "dss job log <id> [--activity
|
|
2133
|
-
description: "Get log output for
|
|
2134
|
-
examples: [
|
|
3318
|
+
usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
|
|
3319
|
+
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.",
|
|
3320
|
+
examples: [
|
|
3321
|
+
"dss job log JOB_ID",
|
|
3322
|
+
"dss job log JOB_ID --activity main --max-log-lines 200",
|
|
3323
|
+
],
|
|
3324
|
+
},
|
|
3325
|
+
"log-url": {
|
|
3326
|
+
handler: (c, a, f) => {
|
|
3327
|
+
requireArgs(a, 1, "dss job log-url <url>");
|
|
3328
|
+
return c.jobs.logFromUrl(a[0], { maxLogLines: maxLogLinesFromFlags(f), });
|
|
3329
|
+
},
|
|
3330
|
+
usage: "dss job log-url <url> [--max-lines N|--max-log-lines N]",
|
|
3331
|
+
description: "Fetch a DSS cat-activity-log URL pasted from the UI.",
|
|
3332
|
+
examples: [
|
|
3333
|
+
'dss job log-url "https://dss/dip/api/flow/jobs/cat-activity-log?projectKey=TEST&jobId=JOB&activityId=A&logId=L"',
|
|
3334
|
+
],
|
|
2135
3335
|
},
|
|
2136
3336
|
build: {
|
|
2137
3337
|
handler: async (c, a, f) => {
|
|
@@ -2225,6 +3425,44 @@ const commands = {
|
|
|
2225
3425
|
"dss job wait JOB_ID --include-logs --log-filter stdout --summary --timeout 60000",
|
|
2226
3426
|
],
|
|
2227
3427
|
},
|
|
3428
|
+
monitor: {
|
|
3429
|
+
handler: async (c, a, f) => {
|
|
3430
|
+
requireArgs(a, 1, "dss job monitor <id...>");
|
|
3431
|
+
const options = {
|
|
3432
|
+
includeLogs: f["include-logs"] === true,
|
|
3433
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
3434
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
3435
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
3436
|
+
timeoutMs: num(f["timeout"]),
|
|
3437
|
+
summary: f["summary"] !== false,
|
|
3438
|
+
projectKey: f["project-key"],
|
|
3439
|
+
};
|
|
3440
|
+
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
3441
|
+
return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
|
|
3442
|
+
},
|
|
3443
|
+
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]",
|
|
3444
|
+
description: "Monitor one or more existing jobs and summarize progress counters from logs.",
|
|
3445
|
+
examples: ["dss job monitor JOB_ID --summary", "dss job monitor JOB1 JOB2 --until all-done",],
|
|
3446
|
+
},
|
|
3447
|
+
watch: {
|
|
3448
|
+
handler: async (c, a, f) => {
|
|
3449
|
+
requireArgs(a, 1, "dss job watch <id...>");
|
|
3450
|
+
const options = {
|
|
3451
|
+
includeLogs: f["include-logs"] === true,
|
|
3452
|
+
logFilter: jobLogFilterFromFlag(f["log-filter"]),
|
|
3453
|
+
maxLogLines: maxLogLinesFromFlags(f),
|
|
3454
|
+
pollIntervalMs: num(f["poll-interval"]),
|
|
3455
|
+
timeoutMs: num(f["timeout"]),
|
|
3456
|
+
summary: true,
|
|
3457
|
+
projectKey: f["project-key"],
|
|
3458
|
+
};
|
|
3459
|
+
const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
|
|
3460
|
+
return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
|
|
3461
|
+
},
|
|
3462
|
+
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]",
|
|
3463
|
+
description: "Watch one or more existing jobs with progress extraction enabled.",
|
|
3464
|
+
examples: ["dss job watch JOB_ID", "dss job watch JOB1 JOB2 --until all-done",],
|
|
3465
|
+
},
|
|
2228
3466
|
abort: {
|
|
2229
3467
|
handler: async (c, a, f) => {
|
|
2230
3468
|
requireArgs(a, 1, "dss job abort <id>");
|
|
@@ -2260,7 +3498,7 @@ const commands = {
|
|
|
2260
3498
|
return c.scenarios.get(a[0], { projectKey: f["project-key"], });
|
|
2261
3499
|
},
|
|
2262
3500
|
usage: "dss scenario get <id> [--project-key KEY]",
|
|
2263
|
-
description: "Get scenario definition.",
|
|
3501
|
+
description: "Get raw scenario definition. For step-based scenario edits, patch params.steps; rawParams.params is DSS echo data.",
|
|
2264
3502
|
examples: ["dss scenario get my_scenario",],
|
|
2265
3503
|
},
|
|
2266
3504
|
run: {
|
|
@@ -2391,21 +3629,46 @@ const commands = {
|
|
|
2391
3629
|
handler: async (c, a, f) => {
|
|
2392
3630
|
requireArgs(a, 1, "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
2393
3631
|
const data = jsonInput(f);
|
|
2394
|
-
if (
|
|
3632
|
+
if (data === undefined) {
|
|
2395
3633
|
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
|
|
2396
3634
|
}
|
|
2397
3635
|
const pk = f["project-key"];
|
|
2398
3636
|
if (f["dry-run"] === true) {
|
|
2399
3637
|
const current = await c.scenarios.get(a[0], { projectKey: pk, });
|
|
2400
|
-
const
|
|
2401
|
-
return {
|
|
3638
|
+
const preview = scenarioUpdatePreview(current, data);
|
|
3639
|
+
return {
|
|
3640
|
+
dryRun: true,
|
|
3641
|
+
action: "update",
|
|
3642
|
+
resource: "scenario",
|
|
3643
|
+
id: a[0],
|
|
3644
|
+
canonicalEditableFields: preview.canonicalEditableFields,
|
|
3645
|
+
normalization: preview.normalization,
|
|
3646
|
+
normalizedData: preview.normalizedData,
|
|
3647
|
+
changes: preview.changes,
|
|
3648
|
+
unchangedPaths: preview.unchangedPaths,
|
|
3649
|
+
current: preview.current,
|
|
3650
|
+
next: preview.next,
|
|
3651
|
+
};
|
|
2402
3652
|
}
|
|
2403
|
-
await c.scenarios.update(a[0], data, pk);
|
|
2404
|
-
return {
|
|
3653
|
+
const result = await c.scenarios.update(a[0], data, pk);
|
|
3654
|
+
return {
|
|
3655
|
+
updated: a[0],
|
|
3656
|
+
resource: "scenario",
|
|
3657
|
+
verified: result.verified,
|
|
3658
|
+
changed: result.changes.length > 0,
|
|
3659
|
+
canonicalEditableFields: result.canonicalEditableFields,
|
|
3660
|
+
normalization: result.normalization,
|
|
3661
|
+
...(result.normalization.length > 0 ? { normalizedData: result.normalizedData, } : {}),
|
|
3662
|
+
changes: result.changes,
|
|
3663
|
+
unchangedPaths: result.unchangedPaths,
|
|
3664
|
+
};
|
|
2405
3665
|
},
|
|
2406
3666
|
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: [
|
|
3667
|
+
description: "Update scenario settings via JSON merge; edit step-based scenario steps at params.steps, not rawParams.params.steps.",
|
|
3668
|
+
examples: [
|
|
3669
|
+
'dss scenario update my_scenario --data \'{"params":{"steps":[]}}\' --dry-run',
|
|
3670
|
+
"dss scenario update my_scenario --data-file settings.json --dry-run",
|
|
3671
|
+
],
|
|
2409
3672
|
},
|
|
2410
3673
|
},
|
|
2411
3674
|
folder: {
|
|
@@ -3290,6 +4553,8 @@ function requireArgs(args, count, usage) {
|
|
|
3290
4553
|
// .env auto-loading
|
|
3291
4554
|
// ---------------------------------------------------------------------------
|
|
3292
4555
|
function loadEnvFile() {
|
|
4556
|
+
if (process.env.DATAIKU_DISABLE_ENV === "1")
|
|
4557
|
+
return;
|
|
3293
4558
|
const dirs = [
|
|
3294
4559
|
resolve(dirname(fileURLToPath(import.meta.url)), ".."),
|
|
3295
4560
|
process.cwd(),
|
|
@@ -3461,6 +4726,19 @@ async function probeDoctorPermission(probe) {
|
|
|
3461
4726
|
return { status: permissionStatusForError(error), details: errorDetails(error), };
|
|
3462
4727
|
}
|
|
3463
4728
|
}
|
|
4729
|
+
async function probeReadOnlyPrerequisiteForMutation(probe, readAction) {
|
|
4730
|
+
const readProbe = await probeDoctorPermission(probe);
|
|
4731
|
+
if (readProbe.status !== "yes")
|
|
4732
|
+
return readProbe;
|
|
4733
|
+
return {
|
|
4734
|
+
status: "unknown",
|
|
4735
|
+
details: {
|
|
4736
|
+
reason: "mutation capability was not verified because doctor capabilities are read-only",
|
|
4737
|
+
readAction,
|
|
4738
|
+
readStatus: "yes",
|
|
4739
|
+
},
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
3464
4742
|
function missingProjectPermission() {
|
|
3465
4743
|
return {
|
|
3466
4744
|
status: "unknown",
|
|
@@ -3566,21 +4844,21 @@ async function doctorCapabilities(client, projectKey, accessibleProjects, flags)
|
|
|
3566
4844
|
? probeDoctorPermission(() => client.projects.get(probeProjectKey))
|
|
3567
4845
|
: Promise.resolve(missingProjectPermission()),
|
|
3568
4846
|
canMutateProject: () => probeProjectKey
|
|
3569
|
-
?
|
|
4847
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.variables.get(probeProjectKey), "variables.get")
|
|
3570
4848
|
: Promise.resolve(missingProjectPermission()),
|
|
3571
4849
|
canCreateFolder: () => probeProjectKey
|
|
3572
|
-
?
|
|
4850
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.folders.list(probeProjectKey), "folders.list")
|
|
3573
4851
|
: Promise.resolve(missingProjectPermission()),
|
|
3574
4852
|
canRunJobs: () => probeProjectKey
|
|
3575
|
-
?
|
|
4853
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.jobs.list(probeProjectKey), "jobs.list")
|
|
3576
4854
|
: Promise.resolve(missingProjectPermission()),
|
|
3577
4855
|
canCreateScenario: () => probeProjectKey
|
|
3578
|
-
?
|
|
4856
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.scenarios.list(probeProjectKey), "scenarios.list")
|
|
3579
4857
|
: Promise.resolve(missingProjectPermission()),
|
|
3580
4858
|
canSaveJupyter: () => probeProjectKey
|
|
3581
|
-
?
|
|
4859
|
+
? probeReadOnlyPrerequisiteForMutation(() => client.notebooks.listJupyter(probeProjectKey), "notebooks.listJupyter")
|
|
3582
4860
|
: Promise.resolve(missingProjectPermission()),
|
|
3583
|
-
canMutateConnection: () =>
|
|
4861
|
+
canMutateConnection: () => probeReadOnlyPrerequisiteForMutation(() => client.connections.list(), "connections.list"),
|
|
3584
4862
|
};
|
|
3585
4863
|
const permissions = {};
|
|
3586
4864
|
const permissionDetails = {};
|
|
@@ -3738,6 +5016,7 @@ async function runFixtures(flags) {
|
|
|
3738
5016
|
return discoverFixtureReport(client, projectKey, flags);
|
|
3739
5017
|
}
|
|
3740
5018
|
const READ_ACTIONS = new Set([
|
|
5019
|
+
"cat",
|
|
3741
5020
|
"contents",
|
|
3742
5021
|
"diff",
|
|
3743
5022
|
"download",
|
|
@@ -3758,10 +5037,14 @@ const READ_ACTIONS = new Set([
|
|
|
3758
5037
|
"list-jupyter",
|
|
3759
5038
|
"list-sql",
|
|
3760
5039
|
"log",
|
|
5040
|
+
"log-url",
|
|
3761
5041
|
"map",
|
|
3762
5042
|
"metadata",
|
|
3763
5043
|
"peek",
|
|
5044
|
+
"source",
|
|
5045
|
+
"summary",
|
|
3764
5046
|
"wait",
|
|
5047
|
+
"watch",
|
|
3765
5048
|
"preview",
|
|
3766
5049
|
"query",
|
|
3767
5050
|
"schema",
|
|
@@ -3891,7 +5174,7 @@ function inferSideEffect(resource, action) {
|
|
|
3891
5174
|
return "write";
|
|
3892
5175
|
if (READ_ACTIONS.has(action))
|
|
3893
5176
|
return "read";
|
|
3894
|
-
if (/^(create|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
|
|
5177
|
+
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
|
|
3895
5178
|
.test(action)) {
|
|
3896
5179
|
return "write";
|
|
3897
5180
|
}
|
|
@@ -3911,6 +5194,7 @@ function inferRequiresProject(resource, action, usage) {
|
|
|
3911
5194
|
}
|
|
3912
5195
|
const ARRAY_OUTPUT_ACTIONS = new Set([
|
|
3913
5196
|
"history",
|
|
5197
|
+
"find",
|
|
3914
5198
|
"infer",
|
|
3915
5199
|
"last-results",
|
|
3916
5200
|
"list",
|
|
@@ -3926,7 +5210,9 @@ const STRING_OUTPUT_ACTIONS = new Set([
|
|
|
3926
5210
|
"download",
|
|
3927
5211
|
"download-code",
|
|
3928
5212
|
"get-payload",
|
|
5213
|
+
"cat",
|
|
3929
5214
|
"log",
|
|
5215
|
+
"log-url",
|
|
3930
5216
|
"preview",
|
|
3931
5217
|
]);
|
|
3932
5218
|
function inferOutputShape(resource, action) {
|
|
@@ -3967,7 +5253,7 @@ function inferExitCodes(asyncKind) {
|
|
|
3967
5253
|
};
|
|
3968
5254
|
}
|
|
3969
5255
|
function cleanupCommandFromDeleteUsage(resource, action) {
|
|
3970
|
-
if (!action.startsWith("create"))
|
|
5256
|
+
if (!(action.startsWith("create") || action === "clone"))
|
|
3971
5257
|
return undefined;
|
|
3972
5258
|
const deleteAction = action === "create-rule" ? "delete-rule" : "delete";
|
|
3973
5259
|
const deleteUsage = commands[resource]?.[deleteAction]?.usage;
|
|
@@ -3990,8 +5276,9 @@ function inferDestructiveLevel(sideEffect, action) {
|
|
|
3990
5276
|
return "reversible";
|
|
3991
5277
|
}
|
|
3992
5278
|
function inferAsyncKind(resource, action) {
|
|
3993
|
-
if (resource === "job" && ["build", "build-and-wait", "wait",].includes(action))
|
|
5279
|
+
if (resource === "job" && ["build", "build-and-wait", "wait", "monitor", "watch",].includes(action)) {
|
|
3994
5280
|
return "job";
|
|
5281
|
+
}
|
|
3995
5282
|
if (resource === "recipe" && action === "run")
|
|
3996
5283
|
return "job";
|
|
3997
5284
|
if (resource === "future" && ["get", "peek", "wait", "abort",].includes(action))
|
|
@@ -4013,7 +5300,7 @@ function inferIdempotency(sideEffect, action, usage) {
|
|
|
4013
5300
|
return "none";
|
|
4014
5301
|
}
|
|
4015
5302
|
function inferCleanupHint(resource, action) {
|
|
4016
|
-
if (!action.startsWith("create"))
|
|
5303
|
+
if (!(action.startsWith("create") || action === "clone"))
|
|
4017
5304
|
return undefined;
|
|
4018
5305
|
if (resource === "code-env")
|
|
4019
5306
|
return "Delete with `dss code-env delete <lang> <name> --if-exists`.";
|
|
@@ -4162,7 +5449,7 @@ function requiredPlanFlag(flags, name, usage) {
|
|
|
4162
5449
|
}
|
|
4163
5450
|
function optionalJsonFlag(flags, name) {
|
|
4164
5451
|
const value = flags[name];
|
|
4165
|
-
return typeof value === "string" ?
|
|
5452
|
+
return typeof value === "string" ? parseJsonObject(value, `--${name}`) : undefined;
|
|
4166
5453
|
}
|
|
4167
5454
|
function requiredPlanJsonInput(flags, usage) {
|
|
4168
5455
|
return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
|
|
@@ -4475,8 +5762,11 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4475
5762
|
};
|
|
4476
5763
|
case "recipe.set-payload": {
|
|
4477
5764
|
const file = requiredPlanFlag(flags, "file", entry.usage);
|
|
4478
|
-
const backupDir = flags["backup
|
|
4479
|
-
|
|
5765
|
+
const backupDir = flags["no-backup"] === true
|
|
5766
|
+
? undefined
|
|
5767
|
+
: flags["backup-dir"]
|
|
5768
|
+
?? join(process.cwd(), ".dss-backups", "recipes");
|
|
5769
|
+
const backupPath = backupDir ? recipeBackupPath(id, backupDir) : undefined;
|
|
4480
5770
|
return {
|
|
4481
5771
|
method: "PUT",
|
|
4482
5772
|
endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
|
|
@@ -4487,7 +5777,7 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
|
|
|
4487
5777
|
...(backupPath ? { backupPath, } : {}),
|
|
4488
5778
|
},
|
|
4489
5779
|
...(backupPath
|
|
4490
|
-
? { localWrites: [{ path: backupPath, source: "remote recipe
|
|
5780
|
+
? { localWrites: [{ path: backupPath, source: "remote recipe backup", before: "PUT", },], }
|
|
4491
5781
|
: {}),
|
|
4492
5782
|
};
|
|
4493
5783
|
}
|
|
@@ -4834,17 +6124,26 @@ function promptSecret(label) {
|
|
|
4834
6124
|
// Credential resolution
|
|
4835
6125
|
// ---------------------------------------------------------------------------
|
|
4836
6126
|
function resolveCredentials(flags) {
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
6127
|
+
const hasUrlFlag = Object.hasOwn(flags, "url");
|
|
6128
|
+
const hasApiKeyFlag = Object.hasOwn(flags, "api-key");
|
|
6129
|
+
const hasProjectKeyFlag = Object.hasOwn(flags, "project-key");
|
|
6130
|
+
let url = hasUrlFlag ? flags["url"] : undefined;
|
|
6131
|
+
let apiKey = hasApiKeyFlag ? flags["api-key"] : undefined;
|
|
6132
|
+
let projectKey = hasProjectKeyFlag ? flags["project-key"] : undefined;
|
|
4840
6133
|
const saved = loadCredentials();
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
6134
|
+
if (!hasUrlFlag)
|
|
6135
|
+
url ??= process.env.DATAIKU_URL;
|
|
6136
|
+
if (!hasApiKeyFlag)
|
|
6137
|
+
apiKey ??= process.env.DATAIKU_API_KEY;
|
|
6138
|
+
if (!hasProjectKeyFlag)
|
|
6139
|
+
projectKey ??= process.env.DATAIKU_PROJECT_KEY;
|
|
4844
6140
|
if (saved) {
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
6141
|
+
if (!hasUrlFlag)
|
|
6142
|
+
url ??= saved.url;
|
|
6143
|
+
if (!hasApiKeyFlag)
|
|
6144
|
+
apiKey ??= saved.apiKey;
|
|
6145
|
+
if (!hasProjectKeyFlag)
|
|
6146
|
+
projectKey ??= saved.projectKey;
|
|
4848
6147
|
}
|
|
4849
6148
|
return {
|
|
4850
6149
|
url: url ?? "",
|
|
@@ -4979,7 +6278,7 @@ async function main() {
|
|
|
4979
6278
|
const { positional, flags, } = parseArgs(process.argv.slice(2));
|
|
4980
6279
|
// --version
|
|
4981
6280
|
if (flags["version"] === true) {
|
|
4982
|
-
process.stdout.write(`${
|
|
6281
|
+
process.stdout.write(`${CLI_VERSION_LABEL}\n`);
|
|
4983
6282
|
process.exit(0);
|
|
4984
6283
|
}
|
|
4985
6284
|
// Top-level help
|
|
@@ -5317,7 +6616,12 @@ async function main() {
|
|
|
5317
6616
|
if (entry)
|
|
5318
6617
|
await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
|
|
5319
6618
|
}
|
|
5320
|
-
|
|
6619
|
+
if (flags["raw"] === true && typeof result === "string") {
|
|
6620
|
+
process.stdout.write(result);
|
|
6621
|
+
}
|
|
6622
|
+
else {
|
|
6623
|
+
writeCommandResult(result);
|
|
6624
|
+
}
|
|
5321
6625
|
const failureExitCode = commandFailureExitCode(result);
|
|
5322
6626
|
if (failureExitCode !== undefined)
|
|
5323
6627
|
process.exit(failureExitCode);
|