dataiku-sdk 0.6.0 → 0.6.1

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