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/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
- const CLI_VERSION = (() => {
20
- try {
21
- let dir = dirname(fileURLToPath(import.meta.url));
22
- for (let i = 0; i < 5; i++) {
23
- const candidate = resolve(dir, "package.json");
24
- try {
25
- return JSON.parse(readFileSync(candidate, "utf-8")).version;
26
- }
27
- catch {
28
- dir = dirname(dir);
29
- }
23
+ function findPackageRoot() {
24
+ let dir = dirname(fileURLToPath(import.meta.url));
25
+ for (let i = 0; i < 5; i++) {
26
+ try {
27
+ readFileSync(resolve(dir, "package.json"), "utf-8");
28
+ return dir;
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 recipePayloadBackupPath(recipeName, backupDir) {
116
+ function recipeBackupPath(recipeName, backupDir) {
74
117
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
75
- return join(backupDir, `${sanitizeFileName(recipeName, "recipe")}-${stamp}.payload`);
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 json(v) {
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 JSON.parse(v);
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 JSON.parse(readStdinText());
177
- }
891
+ if (flags["stdin"] === true)
892
+ return parseJsonObject(readStdinText(), "stdin");
178
893
  if (typeof flags["data-file"] === "string") {
179
- return JSON.parse(readFileSync(flags["data-file"], "utf-8"));
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 JSON.parse(readStdinText());
902
+ return parseJsonValue(readStdinText(), "stdin");
189
903
  if (typeof flags["data-file"] === "string") {
190
- return JSON.parse(readFileSync(flags["data-file"], "utf-8"));
904
+ return parseJsonValue(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
191
905
  }
192
906
  if (typeof flags["data"] === "string")
193
- return JSON.parse(flags["data"]);
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 (!data)
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
- return isFailedWaitResult(result) ? 4 : undefined;
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[flagName] = arg.slice(eqIdx + 1);
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[flagName] = next;
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[long] = next;
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) => c.flowZones.list(f["project-key"]),
1489
- usage: "dss flow-zone list [--project-key KEY]",
1490
- description: "List flow zones in a project.",
1491
- examples: ["dss flow-zone list",],
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
- requireArgs(a, 1, "dss flow-zone move <id> [--dataset DS] [--recipe R] [--folder F] [--object TYPE:ID]");
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: flowZoneId(a[0]),
2417
+ id: zoneId,
1597
2418
  items,
1598
2419
  };
1599
2420
  }
1600
- return c.flowZones.moveItems(flowZoneId(a[0]), items, f["project-key"]);
2421
+ return c.flowZones.moveItems(zoneId, items, pk);
1601
2422
  },
1602
- usage: "dss flow-zone move <id> [--dataset DS[,DS2]] [--recipe R] [--folder F] [--object TYPE:ID] [--dry-run] [--project-key KEY]",
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 ZONE_ID --dataset raw_orders,clean_orders --recipe prepare_orders",
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
- return { created: name, resource: "dataset", };
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: f["input"] ? [f["input"],] : undefined,
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 { dryRun: true, action: "create", resource: "recipe", payload, };
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
- return { created: created.recipeName, resource: "recipe", ...created, };
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 a dataset output, or use --output-folder with --output-connection for a managed-folder output.",
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 orders --output orders_clean",
1981
- "dss recipe create --type python --input orders --output orders_clean --output-connection filesystem",
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 backupDir = f["backup-dir"];
2064
- const backupPath = backupDir ? recipePayloadBackupPath(a[0], backupDir) : undefined;
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, current.payload ?? "", "utf-8");
3160
+ await writeFile(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`, "utf-8");
2088
3161
  }
2089
- await c.recipes.setPayload(a[0], content, {
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, optionally backing up the remote payload first.",
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 NAME] [--max-lines N|--max-log-lines N]",
2133
- description: "Get log output for a job.",
2134
- examples: ["dss job log JOB_ID", "dss job log JOB_ID --activity main --max-log-lines 200",],
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 (!data) {
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 next = deepMerge(current, data);
2401
- return { dryRun: true, action: "update", resource: "scenario", id: a[0], current, next, };
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 { updated: a[0], resource: "scenario", };
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: ["dss scenario update my_scenario --data-file settings.json --dry-run",],
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
- ? probeDoctorPermission(() => client.variables.get(probeProjectKey))
4847
+ ? probeReadOnlyPrerequisiteForMutation(() => client.variables.get(probeProjectKey), "variables.get")
3570
4848
  : Promise.resolve(missingProjectPermission()),
3571
4849
  canCreateFolder: () => probeProjectKey
3572
- ? probeDoctorPermission(() => client.folders.list(probeProjectKey))
4850
+ ? probeReadOnlyPrerequisiteForMutation(() => client.folders.list(probeProjectKey), "folders.list")
3573
4851
  : Promise.resolve(missingProjectPermission()),
3574
4852
  canRunJobs: () => probeProjectKey
3575
- ? probeDoctorPermission(() => client.jobs.list(probeProjectKey))
4853
+ ? probeReadOnlyPrerequisiteForMutation(() => client.jobs.list(probeProjectKey), "jobs.list")
3576
4854
  : Promise.resolve(missingProjectPermission()),
3577
4855
  canCreateScenario: () => probeProjectKey
3578
- ? probeDoctorPermission(() => client.scenarios.list(probeProjectKey))
4856
+ ? probeReadOnlyPrerequisiteForMutation(() => client.scenarios.list(probeProjectKey), "scenarios.list")
3579
4857
  : Promise.resolve(missingProjectPermission()),
3580
4858
  canSaveJupyter: () => probeProjectKey
3581
- ? probeDoctorPermission(() => client.notebooks.listJupyter(probeProjectKey))
4859
+ ? probeReadOnlyPrerequisiteForMutation(() => client.notebooks.listJupyter(probeProjectKey), "notebooks.listJupyter")
3582
4860
  : Promise.resolve(missingProjectPermission()),
3583
- canMutateConnection: () => probeDoctorPermission(() => client.connections.list()),
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" ? JSON.parse(value) : undefined;
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-dir"];
4479
- const backupPath = backupDir ? recipePayloadBackupPath(id, backupDir) : undefined;
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 payload", before: "PUT", },], }
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
- let url = flags["url"];
4838
- let apiKey = flags["api-key"];
4839
- let projectKey = flags["project-key"];
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
- url ??= process.env.DATAIKU_URL;
4842
- apiKey ??= process.env.DATAIKU_API_KEY;
4843
- projectKey ??= process.env.DATAIKU_PROJECT_KEY;
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
- url ||= saved.url;
4846
- apiKey ||= saved.apiKey;
4847
- projectKey ??= saved.projectKey;
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(`${CLI_VERSION}\n`);
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
- writeCommandResult(result);
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);