dataiku-sdk 0.5.1 → 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,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { createHash, } from "node:crypto";
2
3
  import { readFileSync, } from "node:fs";
3
- import { writeFile, } from "node:fs/promises";
4
- import { dirname, resolve, } from "node:path";
4
+ import { mkdir, writeFile, } from "node:fs/promises";
5
+ import { dirname, join, resolve, } from "node:path";
5
6
  import { createInterface, } from "node:readline";
6
7
  import { Writable, } from "node:stream";
7
8
  import { fileURLToPath, } from "node:url";
@@ -9,29 +10,72 @@ 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";
19
+ import { sanitizeFileName, } from "./utils/sanitize.js";
15
20
  // ---------------------------------------------------------------------------
16
21
  // Utility helpers
17
22
  // ---------------------------------------------------------------------------
18
- const CLI_VERSION = (() => {
19
- try {
20
- let dir = dirname(fileURLToPath(import.meta.url));
21
- for (let i = 0; i < 5; i++) {
22
- const candidate = resolve(dir, "package.json");
23
- try {
24
- return JSON.parse(readFileSync(candidate, "utf-8")).version;
25
- }
26
- catch {
27
- dir = dirname(dir);
28
- }
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);
29
32
  }
33
+ }
34
+ return undefined;
35
+ }
36
+ function packageVersion(packageRoot) {
37
+ if (!packageRoot)
30
38
  return "unknown";
39
+ try {
40
+ return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf-8")).version;
31
41
  }
32
42
  catch {
33
43
  return "unknown";
34
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;
35
79
  })();
36
80
  function num(v) {
37
81
  if (typeof v !== "string")
@@ -43,18 +87,166 @@ function jobBuildTargetType(v) {
43
87
  if (v === undefined)
44
88
  return "DATASET";
45
89
  if (typeof v !== "string") {
46
- throw new UsageError("Invalid --type value for job build. Use DATASET or MANAGED_FOLDER.", "invalid_enum");
90
+ throw new UsageError("Invalid job target type. Use dataset or managed-folder.", "invalid_enum");
47
91
  }
48
92
  const normalized = v.trim().toUpperCase().replace(/-/g, "_");
49
93
  if (normalized === "DATASET" || normalized === "MANAGED_FOLDER")
50
94
  return normalized;
51
- throw new UsageError("Invalid --type value for job build. Use DATASET or MANAGED_FOLDER.", "invalid_enum");
95
+ throw new UsageError("Invalid job target type. Use dataset or managed-folder.", "invalid_enum");
96
+ }
97
+ function jobBuildTargetTypeFromFlags(flags) {
98
+ return jobBuildTargetType(flags["target-type"] ?? flags["type"]);
99
+ }
100
+ function maxLogLinesFromFlags(flags) {
101
+ return num(flags["max-log-lines"] ?? flags["max-lines"]);
102
+ }
103
+ function jobLogFilterFromFlag(v) {
104
+ if (v === undefined)
105
+ return undefined;
106
+ if (typeof v !== "string") {
107
+ throw new UsageError("Invalid --log-filter value. Use stdout, stderr, user, or errors.", "invalid_enum");
108
+ }
109
+ const normalized = v.trim().toLowerCase();
110
+ if (normalized === "stdout" || normalized === "stderr" || normalized === "user"
111
+ || normalized === "errors") {
112
+ return normalized;
113
+ }
114
+ throw new UsageError("Invalid --log-filter value. Use stdout, stderr, user, or errors.", "invalid_enum");
115
+ }
116
+ function recipeBackupPath(recipeName, backupDir) {
117
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
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
+ };
183
+ }
184
+ function recipeRunShouldWait(flags) {
185
+ if (flags["wait"] === true && flags["no-wait"] === true) {
186
+ throw new UsageError("--wait and --no-wait are mutually exclusive.", "invalid_enum");
187
+ }
188
+ const waitImplied = flags["include-logs"] === true
189
+ || flags["summary"] === true
190
+ || flags["timeout"] !== undefined
191
+ || flags["poll-interval"] !== undefined;
192
+ if (flags["no-wait"] === true && waitImplied) {
193
+ throw new UsageError("--include-logs, --summary, --timeout, and --poll-interval require waiting; remove --no-wait.", "invalid_enum");
194
+ }
195
+ return flags["no-wait"] !== true && (flags["wait"] === true || waitImplied);
52
196
  }
53
197
  function splitCsvFlag(v) {
54
198
  if (typeof v !== "string")
55
199
  return [];
56
200
  return v.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
57
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
+ }
58
250
  function flowZoneId(value) {
59
251
  const trimmed = value.trim();
60
252
  if (!trimmed)
@@ -125,27 +317,298 @@ function flowZoneMoveItems(flags) {
125
317
  }
126
318
  return items;
127
319
  }
128
- 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") {
129
552
  if (typeof v !== "string")
130
553
  return undefined;
131
- return JSON.parse(v);
554
+ return parseJsonObject(v, source);
132
555
  }
133
- const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--project-key KEY]";
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]";
134
557
  function readStdinText() {
135
558
  return readFileSync(0, "utf-8");
136
559
  }
137
560
  function jsonInput(flags) {
138
- if (flags["stdin"] === true) {
139
- return JSON.parse(readStdinText());
140
- }
561
+ if (flags["stdin"] === true)
562
+ return parseJsonObject(readStdinText(), "stdin");
141
563
  if (typeof flags["data-file"] === "string") {
142
- return JSON.parse(readFileSync(flags["data-file"], "utf-8"));
564
+ return parseJsonObject(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
143
565
  }
144
- if (typeof flags["data"] === "string") {
145
- return JSON.parse(flags["data"]);
566
+ if (typeof flags["data"] === "string")
567
+ return parseJsonObject(flags["data"], "--data");
568
+ return undefined;
569
+ }
570
+ function unknownJsonInput(flags) {
571
+ if (flags["stdin"] === true)
572
+ return parseJsonValue(readStdinText(), "stdin");
573
+ if (typeof flags["data-file"] === "string") {
574
+ return parseJsonValue(readFileSync(flags["data-file"], "utf-8"), flags["data-file"]);
146
575
  }
576
+ if (typeof flags["data"] === "string")
577
+ return parseJsonValue(flags["data"], "--data");
147
578
  return undefined;
148
579
  }
580
+ function schemaColumnsInput(flags, usage) {
581
+ const input = unknownJsonInput(flags);
582
+ if (input === undefined) {
583
+ throw new UsageError(`--data, --data-file, or --stdin is required. Usage: ${usage}`);
584
+ }
585
+ const columns = Array.isArray(input)
586
+ ? input
587
+ : input && typeof input === "object" && Array.isArray(input.columns)
588
+ ? input.columns
589
+ : undefined;
590
+ if (!columns) {
591
+ throw new UsageError("Schema input must be an array of columns or an object with a columns array.");
592
+ }
593
+ return columns.map((column, index) => {
594
+ if (!column || typeof column !== "object" || Array.isArray(column)) {
595
+ throw new UsageError(`Schema column at index ${index} must be an object.`);
596
+ }
597
+ const record = column;
598
+ if (typeof record.name !== "string" || record.name.length === 0) {
599
+ throw new UsageError(`Schema column at index ${index} is missing string field "name".`);
600
+ }
601
+ if (typeof record.type !== "string" || record.type.length === 0) {
602
+ throw new UsageError(`Schema column "${record.name}" is missing string field "type".`);
603
+ }
604
+ return {
605
+ ...record,
606
+ name: record.name,
607
+ type: record.type,
608
+ ...(typeof record.comment === "string" ? { comment: record.comment, } : {}),
609
+ };
610
+ });
611
+ }
149
612
  function textInput(flags) {
150
613
  if (typeof flags["content"] === "string")
151
614
  return flags["content"];
@@ -155,7 +618,7 @@ function textInput(flags) {
155
618
  }
156
619
  function requiredJsonInput(flags, message) {
157
620
  const data = jsonInput(flags);
158
- if (!data)
621
+ if (data === undefined)
159
622
  throw new UsageError(message);
160
623
  return data;
161
624
  }
@@ -289,6 +752,28 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
289
752
  function writeCommandResult(result) {
290
753
  process.stdout.write(`${JSON.stringify(result ?? { ok: true, }, null, 2)}\n`);
291
754
  }
755
+ function transientBodyWithTargetContext(body, target, elapsedMs) {
756
+ try {
757
+ const parsed = JSON.parse(body);
758
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
759
+ const record = parsed;
760
+ const message = typeof record.message === "string" && record.message.length > 0
761
+ ? `Target: ${target}\nElapsed: ${elapsedMs}ms\n${record.message}`
762
+ : `Target: ${target}\nElapsed: ${elapsedMs}ms`;
763
+ return JSON.stringify({ ...record, message, target, elapsedMs, });
764
+ }
765
+ }
766
+ catch {
767
+ // Non-JSON DSS bodies are wrapped as text below.
768
+ }
769
+ return `Target: ${target}\nElapsed: ${elapsedMs}ms\n${body}`;
770
+ }
771
+ function addTransientTargetContext(error, target, elapsedMs) {
772
+ if (error instanceof DataikuError && error.category === "transient") {
773
+ throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry);
774
+ }
775
+ throw error;
776
+ }
292
777
  function isFailedWaitResult(result) {
293
778
  if (result === null || typeof result !== "object" || Array.isArray(result))
294
779
  return false;
@@ -299,7 +784,11 @@ function isFailedWaitResult(result) {
299
784
  && (typeof record.state === "string" || typeof record.outcome === "string");
300
785
  }
301
786
  function commandFailureExitCode(result) {
302
- 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;
303
792
  }
304
793
  function isNotFoundError(error) {
305
794
  if (error instanceof DataikuError)
@@ -331,6 +820,7 @@ function planResult(resource, action, options) {
331
820
  ...(options.method ? { method: options.method, } : {}),
332
821
  ...(options.endpoint ? { endpoint: options.endpoint, } : {}),
333
822
  ...(options.payload !== undefined ? { payload: options.payload, } : {}),
823
+ ...(options.localWrites !== undefined ? { localWrites: options.localWrites, } : {}),
334
824
  ...(options.wait !== undefined ? { wait: options.wait, } : {}),
335
825
  idempotency: options.idempotency,
336
826
  async: options.asyncKind,
@@ -360,7 +850,7 @@ function resultRecord(result) {
360
850
  : {};
361
851
  }
362
852
  function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
363
- if (!(action.startsWith("create") || action === "upload"))
853
+ if (!(action.startsWith("create") || action === "clone" || action === "upload"))
364
854
  return undefined;
365
855
  const record = resultRecord(result);
366
856
  if (record.skipped !== undefined)
@@ -380,6 +870,16 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
380
870
  cleanup: { argv: ["dataset", "delete", name, "--if-exists", ...withProject,], },
381
871
  };
382
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
+ }
383
883
  case "recipe.create": {
384
884
  const name = stringField(record, ["created", "recipeName", "name",])
385
885
  ?? flags["name"];
@@ -391,6 +891,17 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
391
891
  cleanup: { argv: ["recipe", "delete", name, "--if-exists", ...withProject,], },
392
892
  };
393
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
+ }
394
905
  case "scenario.create": {
395
906
  const id = args[0];
396
907
  return {
@@ -504,9 +1015,11 @@ const BOOLEAN_FLAGS = new Set([
504
1015
  "global",
505
1016
  "list-agents",
506
1017
  "include-raw",
1018
+ "raw",
507
1019
  "include-payload",
508
1020
  "no-payload",
509
1021
  "include-logs",
1022
+ "summary",
510
1023
  "replace",
511
1024
  "dry-run",
512
1025
  "plan",
@@ -521,7 +1034,12 @@ const BOOLEAN_FLAGS = new Set([
521
1034
  "report-json",
522
1035
  "no-wait",
523
1036
  "force-rebuild",
1037
+ "latest",
1038
+ "copy-output-settings",
524
1039
  "continue-on-error",
1040
+ "no-backup",
1041
+ "payload-only",
1042
+ "allow-same-path",
525
1043
  ]);
526
1044
  const SHORT_FLAGS = {
527
1045
  h: "help",
@@ -536,17 +1054,22 @@ const FLAG_ALIASES = {
536
1054
  "skip-tls-verify": "insecure",
537
1055
  "extra-ca-certs": "ca-cert",
538
1056
  explain: "plan",
1057
+ "zone-name": "zone",
539
1058
  };
540
1059
  const VALUE_FLAGS = new Set([
541
1060
  "activity",
542
1061
  "agent",
543
1062
  "api-key",
544
1063
  "build-mode",
1064
+ "backup-dir",
1065
+ "backup",
545
1066
  "ca-cert",
1067
+ "catalog",
546
1068
  "cell-id",
547
1069
  "allow-types",
548
1070
  "color",
549
1071
  "connection",
1072
+ "contains",
550
1073
  "content",
551
1074
  "content-type",
552
1075
  "data",
@@ -560,6 +1083,7 @@ const VALUE_FLAGS = new Set([
560
1083
  "install-core-packages",
561
1084
  "folder",
562
1085
  "input",
1086
+ "from",
563
1087
  "knowledge-bank",
564
1088
  "labeling-task",
565
1089
  "lang",
@@ -568,17 +1092,23 @@ const VALUE_FLAGS = new Set([
568
1092
  "local",
569
1093
  "max-edges",
570
1094
  "max-lines",
1095
+ "max-log-lines",
571
1096
  "listed",
572
1097
  "max-nodes",
573
1098
  "max-rows",
1099
+ "limit",
574
1100
  "max-timestamp",
575
1101
  "only-monitored",
576
1102
  "min-timestamp",
577
1103
  "mode",
1104
+ "log-filter",
1105
+ "log-id",
578
1106
  "model-evaluation-store",
579
1107
  "name",
580
1108
  "object",
1109
+ "metastore-table",
581
1110
  "output",
1111
+ "output-file",
582
1112
  "output-connection",
583
1113
  "output-folder",
584
1114
  "page",
@@ -592,18 +1122,41 @@ const VALUE_FLAGS = new Set([
592
1122
  "results-per-page",
593
1123
  "record-cleanup",
594
1124
  "rule-id",
1125
+ "retries",
595
1126
  "poll-interval",
596
1127
  "python-interpreter",
1128
+ "replace-input",
1129
+ "replace-output",
1130
+ "replace-payload-text",
597
1131
  "retain",
598
1132
  "saved-model",
599
1133
  "sql",
1134
+ "schema",
600
1135
  "sql-file",
601
1136
  "standard",
1137
+ "state",
602
1138
  "streaming-endpoint",
603
1139
  "target",
1140
+ "target-type",
604
1141
  "timeout",
1142
+ "table",
605
1143
  "type",
606
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",
607
1160
  ]);
608
1161
  const KNOWN_LONG_FLAGS = new Set([
609
1162
  ...BOOLEAN_FLAGS,
@@ -627,6 +1180,14 @@ function requireFlagValue(flagLabel, next) {
627
1180
  }
628
1181
  return next;
629
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
+ }
630
1191
  function parseArgs(argv) {
631
1192
  const positional = [];
632
1193
  const flags = {};
@@ -642,7 +1203,7 @@ function parseArgs(argv) {
642
1203
  if (eqIdx !== -1) {
643
1204
  const raw = arg.slice(2, eqIdx);
644
1205
  const flagName = normalizeLongFlag(raw);
645
- flags[flagName] = arg.slice(eqIdx + 1);
1206
+ setParsedFlagValue(flags, flagName, arg.slice(eqIdx + 1));
646
1207
  }
647
1208
  else {
648
1209
  const rawFlagName = arg.slice(2);
@@ -652,7 +1213,7 @@ function parseArgs(argv) {
652
1213
  }
653
1214
  else {
654
1215
  const next = requireFlagValue(`--${rawFlagName}`, argv[i + 1]);
655
- flags[flagName] = next;
1216
+ setParsedFlagValue(flags, flagName, next);
656
1217
  i++;
657
1218
  }
658
1219
  }
@@ -665,7 +1226,7 @@ function parseArgs(argv) {
665
1226
  }
666
1227
  else {
667
1228
  const next = requireFlagValue(`-${arg[1]}`, argv[i + 1]);
668
- flags[long] = next;
1229
+ setParsedFlagValue(flags, long, next);
669
1230
  i++;
670
1231
  }
671
1232
  }
@@ -1374,10 +1935,45 @@ const commands = {
1374
1935
  },
1375
1936
  "flow-zone": {
1376
1937
  list: {
1377
- handler: (c, _a, f) => c.flowZones.list(f["project-key"]),
1378
- usage: "dss flow-zone list [--project-key KEY]",
1379
- description: "List flow zones in a project.",
1380
- 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
+ ],
1381
1977
  },
1382
1978
  get: {
1383
1979
  handler: (c, a, f) => {
@@ -1472,7 +2068,11 @@ const commands = {
1472
2068
  },
1473
2069
  move: {
1474
2070
  handler: async (c, a, f) => {
1475
- 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
+ }
1476
2076
  const items = flowZoneMoveItems(f);
1477
2077
  if (items.length === 0) {
1478
2078
  throw new UsageError("At least one object is required. Use --dataset, --recipe, --folder, or --object TYPE:ID.");
@@ -1482,17 +2082,17 @@ const commands = {
1482
2082
  dryRun: true,
1483
2083
  action: "move",
1484
2084
  resource: "flow-zone",
1485
- id: flowZoneId(a[0]),
2085
+ id: zoneId,
1486
2086
  items,
1487
2087
  };
1488
2088
  }
1489
- return c.flowZones.moveItems(flowZoneId(a[0]), items, f["project-key"]);
2089
+ return c.flowZones.moveItems(zoneId, items, pk);
1490
2090
  },
1491
- usage: "dss flow-zone move <id> [--dataset DS[,DS2]] [--recipe R] [--folder F] [--object TYPE:ID] [--dry-run] [--project-key KEY]",
1492
- 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.",
1493
2093
  examples: [
1494
2094
  "dss flow-zone move ZONE_ID --dataset orders --dry-run",
1495
- "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",
1496
2096
  "dss flow-zone move ZONE_ID --folder FOLDER_ID",
1497
2097
  "dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
1498
2098
  ],
@@ -1532,6 +2132,50 @@ const commands = {
1532
2132
  description: "Show the column schema of a dataset.",
1533
2133
  examples: ["dss dataset schema orders",],
1534
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
+ },
2144
+ "refresh-schema": {
2145
+ handler: async (c, a, f) => {
2146
+ const usage = "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]";
2147
+ requireArgs(a, 1, usage);
2148
+ const columns = schemaColumnsInput(f, usage);
2149
+ const pk = f["project-key"];
2150
+ if (f["dry-run"] === true) {
2151
+ const current = await c.datasets.schema(a[0], pk);
2152
+ return {
2153
+ dryRun: true,
2154
+ action: "refresh-schema",
2155
+ resource: "dataset",
2156
+ name: a[0],
2157
+ current,
2158
+ next: { columns, },
2159
+ };
2160
+ }
2161
+ await c.datasets.updateSchema(a[0], columns, pk);
2162
+ return { updated: a[0], resource: "dataset", schema: { columns, }, };
2163
+ },
2164
+ usage: "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]",
2165
+ description: "Replace a dataset schema through the DSS schema endpoint.",
2166
+ examples: [
2167
+ `dss dataset refresh-schema orders --data '{"columns":[{"name":"id","type":"bigint"}]}' --dry-run`,
2168
+ ],
2169
+ },
2170
+ "validate-build": {
2171
+ handler: (c, a, f) => {
2172
+ requireArgs(a, 1, "dss dataset validate-build <name>");
2173
+ return c.datasets.validateBuildSettings(a[0], f["project-key"]);
2174
+ },
2175
+ usage: "dss dataset validate-build <name> [--project-key KEY]",
2176
+ description: "Check common dataset settings that can make file-backed builds fail.",
2177
+ examples: ["dss dataset validate-build orders",],
2178
+ },
1535
2179
  preview: {
1536
2180
  handler: (c, a, f) => {
1537
2181
  requireArgs(a, 1, "dss dataset preview <name>");
@@ -1581,6 +2225,7 @@ const commands = {
1581
2225
  dsType,
1582
2226
  projectKey: pk,
1583
2227
  };
2228
+ const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
1584
2229
  if (f["if-not-exists"] === true || f["dry-run"] === true) {
1585
2230
  const list = await c.datasets.list(pk);
1586
2231
  const existing = list.find((d) => d.name === name);
@@ -1595,17 +2240,57 @@ const commands = {
1595
2240
  name,
1596
2241
  payload,
1597
2242
  ...(existing ? { current: existing, } : {}),
2243
+ ...(zoneId ? { zoneId, zoneMove: [{ objectId: name, objectType: "DATASET", },], } : {}),
1598
2244
  };
1599
2245
  }
1600
2246
  }
1601
2247
  await c.datasets.create(payload);
1602
- return { created: name, resource: "dataset", };
2248
+ const moved = await moveCreatedItemsToZone(c, f, [{ objectId: name, objectType: "DATASET", },], pk);
2249
+ return { created: name, resource: "dataset", ...moved, };
1603
2250
  },
1604
- 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]",
1605
2252
  description: "Create a new dataset.",
1606
2253
  examples: [
1607
2254
  "dss dataset create --name orders --connection filesystem --type Filesystem",
1608
- "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",
1609
2294
  ],
1610
2295
  },
1611
2296
  delete: {
@@ -1674,6 +2359,60 @@ const commands = {
1674
2359
  "dss recipe get compute_orders --include-payload",
1675
2360
  ],
1676
2361
  },
2362
+ "validate-graph": {
2363
+ handler: (c, a, f) => {
2364
+ requireArgs(a, 1, "dss recipe validate-graph <name>");
2365
+ return c.recipes.validateGraph(a[0], {
2366
+ projectKey: f["project-key"],
2367
+ });
2368
+ },
2369
+ usage: "dss recipe validate-graph <name> [--project-key KEY]",
2370
+ description: "Validate declared recipe input/output graph references before building.",
2371
+ examples: ["dss recipe validate-graph compute_orders",],
2372
+ },
2373
+ run: {
2374
+ handler: async (c, a, f) => {
2375
+ requireArgs(a, 1, "dss recipe run <name>");
2376
+ const pk = f["project-key"];
2377
+ const wait = recipeRunShouldWait(f);
2378
+ const options = {
2379
+ buildMode: f["build-mode"],
2380
+ includeLogs: f["include-logs"] === true,
2381
+ logFilter: jobLogFilterFromFlag(f["log-filter"]),
2382
+ maxLogLines: maxLogLinesFromFlags(f),
2383
+ partition: f["partition"],
2384
+ pollIntervalMs: num(f["poll-interval"]),
2385
+ projectKey: pk,
2386
+ timeoutMs: num(f["timeout"]),
2387
+ summary: f["summary"] === true,
2388
+ wait,
2389
+ };
2390
+ if (f["dry-run"] === true) {
2391
+ const outputs = await c.recipes.resolveRunOutputs(a[0], {
2392
+ partition: options.partition,
2393
+ projectKey: pk,
2394
+ });
2395
+ return {
2396
+ dryRun: true,
2397
+ action: "run",
2398
+ resource: "recipe",
2399
+ recipe: a[0],
2400
+ outputs,
2401
+ ...options,
2402
+ endpoint: encodedProjectEndpoint(c, pk, "/jobs/"),
2403
+ method: "POST",
2404
+ };
2405
+ }
2406
+ return c.recipes.run(a[0], options);
2407
+ },
2408
+ usage: "dss recipe run <name> [--wait|--no-wait] [--build-mode MODE] [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
2409
+ description: "Run a recipe by resolving its outputs and submitting the correct dataset or managed-folder build job.",
2410
+ examples: [
2411
+ "dss recipe run compute_orders --wait",
2412
+ "dss recipe run compute_exports --include-logs --log-filter stdout --summary --timeout 600000",
2413
+ "dss recipe run compute_exports --dry-run",
2414
+ ],
2415
+ },
1677
2416
  delete: {
1678
2417
  handler: async (c, a, f) => {
1679
2418
  requireArgs(a, 1, "dss recipe delete <name>");
@@ -1742,15 +2481,20 @@ const commands = {
1742
2481
  }
1743
2482
  const name = f["name"];
1744
2483
  const pk = f["project-key"];
2484
+ const inputDatasets = recipeInputDatasetsFromFlags(f);
1745
2485
  const payload = {
1746
2486
  type,
1747
2487
  name,
1748
- inputDatasets: f["input"] ? [f["input"],] : undefined,
2488
+ inputDatasets,
1749
2489
  outputDataset,
1750
2490
  outputFolder,
1751
2491
  outputConnection: f["output-connection"],
1752
2492
  projectKey: pk,
1753
2493
  };
2494
+ const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
2495
+ const zoneMove = zoneId && name
2496
+ ? [{ objectId: name, objectType: "RECIPE", },]
2497
+ : undefined;
1754
2498
  if ((f["if-not-exists"] === true || f["dry-run"] === true) && name) {
1755
2499
  const list = await c.recipes.list(pk);
1756
2500
  const existing = list.find((r) => r.name === name);
@@ -1764,24 +2508,106 @@ const commands = {
1764
2508
  resource: "recipe",
1765
2509
  name,
1766
2510
  payload,
2511
+ ...(zoneId ? { zoneId, zoneMove, } : {}),
1767
2512
  ...(existing ? { current: existing, } : {}),
1768
2513
  };
1769
2514
  }
1770
2515
  }
1771
2516
  if (f["dry-run"] === true) {
1772
- 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
+ };
1773
2524
  }
1774
2525
  const created = await c.recipes.create(payload);
1775
- 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, };
1776
2532
  },
1777
- 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]",
1778
- 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.",
1779
2535
  examples: [
1780
- "dss recipe create --type python --input orders --output orders_clean",
1781
- "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",
1782
2538
  "dss recipe create --type python --input orders --output-folder LT7TUHJ8 --output-connection filesystem --dry-run",
1783
2539
  ],
1784
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
+ },
1785
2611
  diff: {
1786
2612
  handler: async (c, a, f) => {
1787
2613
  requireArgs(a, 1, "dss recipe diff <name> --file PATH");
@@ -1846,13 +2672,24 @@ const commands = {
1846
2672
  }
1847
2673
  return payload;
1848
2674
  },
1849
- usage: "dss recipe get-payload <name> [--output PATH] [--project-key KEY]",
1850
- 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.",
1851
2677
  examples: [
1852
- "dss recipe get-payload compute_orders",
2678
+ "dss recipe get-payload compute_orders --raw",
1853
2679
  "dss recipe get-payload compute_orders -o code.py",
1854
2680
  ],
1855
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
+ },
1856
2693
  "set-payload": {
1857
2694
  handler: async (c, a, f) => {
1858
2695
  requireArgs(a, 1, "dss recipe set-payload <name> --file PATH");
@@ -1860,11 +2697,17 @@ const commands = {
1860
2697
  if (!filePath)
1861
2698
  throw new UsageError("--file is required.");
1862
2699
  const content = readFileSync(filePath, "utf-8");
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
+ });
1863
2710
  if (f["dry-run"] === true) {
1864
- const current = await c.recipes.get(a[0], {
1865
- projectKey: f["project-key"],
1866
- includePayload: true,
1867
- });
1868
2711
  return {
1869
2712
  dryRun: true,
1870
2713
  action: "set-payload",
@@ -1873,24 +2716,140 @@ const commands = {
1873
2716
  file: filePath,
1874
2717
  current,
1875
2718
  next: { ...current, payload: content, },
2719
+ ...(backupPath ? { backupPath, backup: recipeBackupDocument(a[0], pk, current), } : {}),
1876
2720
  };
1877
2721
  }
1878
- await c.recipes.setPayload(a[0], content, {
2722
+ if (backupDir && backupPath) {
2723
+ await mkdir(backupDir, { recursive: true, });
2724
+ await writeFile(backupPath, `${JSON.stringify(recipeBackupDocument(a[0], pk, current), null, 2)}\n`, "utf-8");
2725
+ }
2726
+ await c.recipes.replace(a[0], { ...current, payload: content, }, pk);
2727
+ return {
2728
+ updated: a[0],
2729
+ resource: "recipe",
2730
+ file: filePath,
2731
+ backupCreated: backupPath !== undefined,
2732
+ ...(backupPath ? { backupPath, } : {}),
2733
+ };
2734
+ },
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.",
2737
+ examples: [
2738
+ "dss recipe set-payload compute_orders --file code.py --dry-run",
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,
1879
2794
  projectKey: f["project-key"],
1880
2795
  });
1881
- return { updated: a[0], resource: "recipe", file: filePath, };
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
+ };
1882
2839
  },
1883
- usage: "dss recipe set-payload <name> --file PATH [--dry-run] [--project-key KEY]",
1884
- description: "Upload recipe code from a local file.",
1885
- examples: ["dss recipe set-payload compute_orders --file code.py --dry-run",],
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",
2844
+ ],
1886
2845
  },
1887
2846
  },
1888
2847
  job: {
1889
2848
  list: {
1890
- handler: (c, _a, f) => c.jobs.list(f["project-key"]),
1891
- usage: "dss job list [--project-key KEY]",
1892
- description: "List recent jobs.",
1893
- 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",],
1894
2853
  },
1895
2854
  get: {
1896
2855
  handler: (c, a, f) => {
@@ -1901,17 +2860,42 @@ const commands = {
1901
2860
  description: "Get job details.",
1902
2861
  examples: ["dss job get JOB_ID",],
1903
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
+ },
1904
2872
  log: {
1905
2873
  handler: (c, a, f) => {
1906
2874
  requireArgs(a, 1, "dss job log <id>");
1907
2875
  return c.jobs.log(a[0], {
1908
2876
  activity: f["activity"],
1909
- maxLogLines: num(f["max-lines"]),
2877
+ logId: f["log-id"],
2878
+ maxLogLines: maxLogLinesFromFlags(f),
2879
+ projectKey: f["project-key"],
1910
2880
  });
1911
2881
  },
1912
- usage: "dss job log <id> [--activity NAME] [--max-lines N]",
1913
- description: "Get log output for a job.",
1914
- examples: ["dss job log JOB_ID", "dss job log JOB_ID --activity main --max-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
+ ],
1915
2899
  },
1916
2900
  build: {
1917
2901
  handler: async (c, a, f) => {
@@ -1919,8 +2903,9 @@ const commands = {
1919
2903
  const pk = f["project-key"];
1920
2904
  const options = {
1921
2905
  buildMode: f["build-mode"],
2906
+ partition: f["partition"],
1922
2907
  pollIntervalMs: num(f["poll-interval"]),
1923
- targetType: jobBuildTargetType(f["type"]),
2908
+ targetType: jobBuildTargetTypeFromFlags(f),
1924
2909
  timeoutMs: num(f["timeout"]),
1925
2910
  };
1926
2911
  if (f["dry-run"] === true) {
@@ -1939,12 +2924,12 @@ const commands = {
1939
2924
  }
1940
2925
  return c.jobs.build(a[0], { ...options, projectKey: pk, });
1941
2926
  },
1942
- usage: "dss job build <target> [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--wait] [--timeout MS] [--poll-interval MS] [--dry-run] [--project-key KEY]",
2927
+ usage: "dss job build <target> [--target-type dataset|managed-folder] [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--wait] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
1943
2928
  description: "Start a dataset or managed-folder build, optionally waiting for completion.",
1944
2929
  examples: [
1945
2930
  "dss job build orders",
1946
2931
  "dss job build orders --build-mode RECURSIVE_BUILD --wait",
1947
- "dss job build LT7TUHJ8 --type MANAGED_FOLDER --dry-run",
2932
+ "dss job build LT7TUHJ8 --target-type managed-folder --dry-run",
1948
2933
  ],
1949
2934
  },
1950
2935
  "build-and-wait": {
@@ -1954,9 +2939,13 @@ const commands = {
1954
2939
  const options = {
1955
2940
  buildMode: f["build-mode"],
1956
2941
  includeLogs: f["include-logs"] === true,
2942
+ logFilter: jobLogFilterFromFlag(f["log-filter"]),
2943
+ maxLogLines: maxLogLinesFromFlags(f),
2944
+ partition: f["partition"],
1957
2945
  pollIntervalMs: num(f["poll-interval"]),
1958
2946
  timeoutMs: num(f["timeout"]),
1959
- targetType: jobBuildTargetType(f["type"]),
2947
+ summary: f["summary"] === true,
2948
+ targetType: jobBuildTargetTypeFromFlags(f),
1960
2949
  };
1961
2950
  if (f["dry-run"] === true) {
1962
2951
  return {
@@ -1971,13 +2960,13 @@ const commands = {
1971
2960
  }
1972
2961
  return c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, });
1973
2962
  },
1974
- usage: "dss job build-and-wait <target> [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--include-logs] [--timeout MS] [--poll-interval MS] [--dry-run] [--project-key KEY]",
2963
+ usage: "dss job build-and-wait <target> [--target-type dataset|managed-folder] [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--partition PARTITION] [--dry-run] [--project-key KEY]",
1975
2964
  description: "Build a dataset or managed folder and wait for completion.",
1976
2965
  examples: [
1977
2966
  "dss job build-and-wait orders",
1978
- "dss job build-and-wait orders --include-logs",
2967
+ "dss job build-and-wait orders --include-logs --log-filter stdout --summary",
1979
2968
  "dss job build-and-wait orders --timeout 300000",
1980
- "dss job build-and-wait LT7TUHJ8 --type MANAGED_FOLDER --dry-run",
2969
+ "dss job build-and-wait LT7TUHJ8 --target-type managed-folder --dry-run",
1981
2970
  ],
1982
2971
  },
1983
2972
  wait: {
@@ -1985,13 +2974,58 @@ const commands = {
1985
2974
  requireArgs(a, 1, "dss job wait <id>");
1986
2975
  return c.jobs.wait(a[0], {
1987
2976
  includeLogs: f["include-logs"] === true,
2977
+ logFilter: jobLogFilterFromFlag(f["log-filter"]),
2978
+ maxLogLines: maxLogLinesFromFlags(f),
1988
2979
  pollIntervalMs: num(f["poll-interval"]),
1989
2980
  timeoutMs: num(f["timeout"]),
2981
+ summary: f["summary"] === true,
2982
+ projectKey: f["project-key"],
1990
2983
  });
1991
2984
  },
1992
- usage: "dss job wait <id> [--include-logs] [--timeout MS] [--poll-interval MS]",
2985
+ usage: "dss job wait <id> [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS]",
1993
2986
  description: "Wait for an existing job to complete.",
1994
- examples: ["dss job wait JOB_ID", "dss job wait JOB_ID --include-logs --timeout 60000",],
2987
+ examples: [
2988
+ "dss job wait JOB_ID",
2989
+ "dss job wait JOB_ID --include-logs --log-filter stdout --summary --timeout 60000",
2990
+ ],
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",],
1995
3029
  },
1996
3030
  abort: {
1997
3031
  handler: async (c, a, f) => {
@@ -2028,7 +3062,7 @@ const commands = {
2028
3062
  return c.scenarios.get(a[0], { projectKey: f["project-key"], });
2029
3063
  },
2030
3064
  usage: "dss scenario get <id> [--project-key KEY]",
2031
- description: "Get scenario definition.",
3065
+ description: "Get raw scenario definition. For step-based scenario edits, patch params.steps; rawParams.params is DSS echo data.",
2032
3066
  examples: ["dss scenario get my_scenario",],
2033
3067
  },
2034
3068
  run: {
@@ -2159,21 +3193,46 @@ const commands = {
2159
3193
  handler: async (c, a, f) => {
2160
3194
  requireArgs(a, 1, "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
2161
3195
  const data = jsonInput(f);
2162
- if (!data) {
3196
+ if (data === undefined) {
2163
3197
  throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
2164
3198
  }
2165
3199
  const pk = f["project-key"];
2166
3200
  if (f["dry-run"] === true) {
2167
3201
  const current = await c.scenarios.get(a[0], { projectKey: pk, });
2168
- const next = deepMerge(current, data);
2169
- 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
+ };
2170
3216
  }
2171
- await c.scenarios.update(a[0], data, pk);
2172
- 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
+ };
2173
3229
  },
2174
3230
  usage: "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]",
2175
- description: "Update scenario settings via JSON merge.",
2176
- 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
+ ],
2177
3236
  },
2178
3237
  },
2179
3238
  folder: {
@@ -2297,13 +3356,24 @@ const commands = {
2297
3356
  contents: {
2298
3357
  handler: async (c, a, f) => {
2299
3358
  requireArgs(a, 1, "dss folder contents <name-or-id>");
2300
- return c.folders.contents(await resolveFolderId(c, a[0], f), {
2301
- projectKey: f["project-key"],
2302
- });
3359
+ const startedAt = Date.now();
3360
+ let folderId = a[0];
3361
+ try {
3362
+ folderId = await resolveFolderId(c, a[0], f);
3363
+ return await c.folders.contents(folderId, {
3364
+ projectKey: f["project-key"],
3365
+ });
3366
+ }
3367
+ catch (error) {
3368
+ addTransientTargetContext(error, `folder:${folderId}`, Date.now() - startedAt);
3369
+ }
2303
3370
  },
2304
- usage: "dss folder contents <name-or-id> [--project-key KEY]",
3371
+ usage: "dss folder contents <name-or-id> [--retries N] [--request-timeout MS] [--project-key KEY]",
2305
3372
  description: "List files in a managed folder.",
2306
- examples: ["dss folder contents my_folder",],
3373
+ examples: [
3374
+ "dss folder contents my_folder",
3375
+ "dss folder contents my_folder --retries 8 --request-timeout 60000",
3376
+ ],
2307
3377
  },
2308
3378
  download: {
2309
3379
  handler: async (c, a, f) => {
@@ -2425,6 +3495,40 @@ const commands = {
2425
3495
  description: "List connections with inferred types and metadata.",
2426
3496
  examples: ["dss connection infer", "dss connection infer --mode rich",],
2427
3497
  },
3498
+ schemas: {
3499
+ handler: (c, _a, f) => {
3500
+ const connection = f["connection"];
3501
+ if (!connection) {
3502
+ throw new UsageError("--connection is required. Usage: dss connection schemas --connection CONN");
3503
+ }
3504
+ return c.connections.schemas({
3505
+ connection,
3506
+ projectKey: f["project-key"],
3507
+ });
3508
+ },
3509
+ usage: "dss connection schemas --connection CONN [--project-key KEY]",
3510
+ description: "List schemas in a SQL connection.",
3511
+ examples: ["dss connection schemas --connection ATHENA_CONN --project-key MYPROJ",],
3512
+ },
3513
+ tables: {
3514
+ handler: (c, _a, f) => {
3515
+ const connection = f["connection"];
3516
+ if (!connection) {
3517
+ throw new UsageError("--connection is required. Usage: dss connection tables --connection CONN");
3518
+ }
3519
+ return c.connections.tables({
3520
+ connection,
3521
+ catalog: f["catalog"],
3522
+ schema: f["schema"],
3523
+ projectKey: f["project-key"],
3524
+ });
3525
+ },
3526
+ usage: "dss connection tables --connection CONN [--catalog CATALOG] [--schema SCHEMA] [--project-key KEY]",
3527
+ description: "List importable tables in a SQL connection, optionally scoped by catalog and schema.",
3528
+ examples: [
3529
+ "dss connection tables --connection ATHENA_CONN --schema analytics --project-key MYPROJ",
3530
+ ],
3531
+ },
2428
3532
  },
2429
3533
  "code-env": {
2430
3534
  list: {
@@ -2644,20 +3748,35 @@ const commands = {
2644
3748
  },
2645
3749
  sql: {
2646
3750
  query: {
2647
- handler: (c, a, f) => {
3751
+ handler: async (c, a, f) => {
2648
3752
  const query = resolveSqlInput(a, f);
2649
3753
  const connection = f["connection"];
2650
3754
  const datasetFullName = f["dataset"];
2651
3755
  if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
2652
3756
  throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
2653
3757
  }
2654
- return c.sql.query({
3758
+ const result = await c.sql.query({
2655
3759
  query,
2656
3760
  connection,
2657
3761
  datasetFullName,
2658
3762
  database: f["database"],
2659
3763
  projectKey: f["project-key"],
2660
3764
  });
3765
+ const outputFile = f["output"]
3766
+ ?? f["output-file"];
3767
+ if (!outputFile)
3768
+ return result;
3769
+ const outputPath = resolve(outputFile);
3770
+ await mkdir(dirname(outputPath), { recursive: true, });
3771
+ await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
3772
+ return {
3773
+ queryId: result.queryId,
3774
+ schema: result.schema,
3775
+ columns: result.columns ?? result.schema,
3776
+ rowCount: result.rows.length,
3777
+ outputPath,
3778
+ written: outputPath,
3779
+ };
2661
3780
  },
2662
3781
  usage: SQL_QUERY_USAGE,
2663
3782
  description: "Run a SQL query against a DSS connection or dataset.",
@@ -2665,6 +3784,7 @@ const commands = {
2665
3784
  "dss sql query 'SELECT * FROM orders LIMIT 10' --connection my_pg",
2666
3785
  "dss sql query --sql-file query.sql --connection my_pg",
2667
3786
  "echo 'SELECT 1' | dss sql query --stdin --dataset MYPROJ.orders",
3787
+ "dss sql query --sql-file query.sql --connection my_pg --output results.json --request-timeout 120000",
2668
3788
  ],
2669
3789
  },
2670
3790
  },
@@ -2920,7 +4040,7 @@ function printTopLevelHelp() {
2920
4040
  " --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
2921
4041
  " --api-key KEY API key (env: DATAIKU_API_KEY)",
2922
4042
  " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
2923
- " --timeout MS Operation timeout (build-and-wait, run-and-wait)",
4043
+ " --timeout MS Operation timeout (build-and-wait, run-and-wait, recipe run)",
2924
4044
  " --request-timeout MS HTTP request timeout in ms (default: 30000)",
2925
4045
  " --dry-run Preview destructive actions without executing",
2926
4046
  " --if-not-exists Skip create if resource already exists",
@@ -2997,6 +4117,8 @@ function requireArgs(args, count, usage) {
2997
4117
  // .env auto-loading
2998
4118
  // ---------------------------------------------------------------------------
2999
4119
  function loadEnvFile() {
4120
+ if (process.env.DATAIKU_DISABLE_ENV === "1")
4121
+ return;
3000
4122
  const dirs = [
3001
4123
  resolve(dirname(fileURLToPath(import.meta.url)), ".."),
3002
4124
  process.cwd(),
@@ -3168,6 +4290,19 @@ async function probeDoctorPermission(probe) {
3168
4290
  return { status: permissionStatusForError(error), details: errorDetails(error), };
3169
4291
  }
3170
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
+ }
3171
4306
  function missingProjectPermission() {
3172
4307
  return {
3173
4308
  status: "unknown",
@@ -3273,21 +4408,21 @@ async function doctorCapabilities(client, projectKey, accessibleProjects, flags)
3273
4408
  ? probeDoctorPermission(() => client.projects.get(probeProjectKey))
3274
4409
  : Promise.resolve(missingProjectPermission()),
3275
4410
  canMutateProject: () => probeProjectKey
3276
- ? probeDoctorPermission(() => client.variables.get(probeProjectKey))
4411
+ ? probeReadOnlyPrerequisiteForMutation(() => client.variables.get(probeProjectKey), "variables.get")
3277
4412
  : Promise.resolve(missingProjectPermission()),
3278
4413
  canCreateFolder: () => probeProjectKey
3279
- ? probeDoctorPermission(() => client.folders.list(probeProjectKey))
4414
+ ? probeReadOnlyPrerequisiteForMutation(() => client.folders.list(probeProjectKey), "folders.list")
3280
4415
  : Promise.resolve(missingProjectPermission()),
3281
4416
  canRunJobs: () => probeProjectKey
3282
- ? probeDoctorPermission(() => client.jobs.list(probeProjectKey))
4417
+ ? probeReadOnlyPrerequisiteForMutation(() => client.jobs.list(probeProjectKey), "jobs.list")
3283
4418
  : Promise.resolve(missingProjectPermission()),
3284
4419
  canCreateScenario: () => probeProjectKey
3285
- ? probeDoctorPermission(() => client.scenarios.list(probeProjectKey))
4420
+ ? probeReadOnlyPrerequisiteForMutation(() => client.scenarios.list(probeProjectKey), "scenarios.list")
3286
4421
  : Promise.resolve(missingProjectPermission()),
3287
4422
  canSaveJupyter: () => probeProjectKey
3288
- ? probeDoctorPermission(() => client.notebooks.listJupyter(probeProjectKey))
4423
+ ? probeReadOnlyPrerequisiteForMutation(() => client.notebooks.listJupyter(probeProjectKey), "notebooks.listJupyter")
3289
4424
  : Promise.resolve(missingProjectPermission()),
3290
- canMutateConnection: () => probeDoctorPermission(() => client.connections.list()),
4425
+ canMutateConnection: () => probeReadOnlyPrerequisiteForMutation(() => client.connections.list(), "connections.list"),
3291
4426
  };
3292
4427
  const permissions = {};
3293
4428
  const permissionDetails = {};
@@ -3347,12 +4482,14 @@ async function runDoctor(flags) {
3347
4482
  let accessibleProjects;
3348
4483
  if (credentialsOk) {
3349
4484
  const requestTimeoutMs = num(flags["request-timeout"]);
4485
+ const retryMaxAttempts = num(flags["retries"]);
3350
4486
  const client = new DataikuClient({
3351
4487
  url,
3352
4488
  apiKey,
3353
4489
  projectKey,
3354
4490
  verbose: flags["verbose"] === true,
3355
4491
  requestTimeoutMs,
4492
+ retryMaxAttempts,
3356
4493
  tlsRejectUnauthorized,
3357
4494
  caCertPath,
3358
4495
  });
@@ -3400,13 +4537,14 @@ async function runDoctor(flags) {
3400
4537
  const result = { ok: checks.every((check) => check.ok), checks, context, };
3401
4538
  if (flags["capabilities"] === true && credentialsOk) {
3402
4539
  const requestTimeoutMs = num(flags["request-timeout"]);
4540
+ const retryMaxAttempts = num(flags["retries"]) ?? 1;
3403
4541
  const client = new DataikuClient({
3404
4542
  url,
3405
4543
  apiKey,
3406
4544
  projectKey,
3407
4545
  verbose: flags["verbose"] === true,
3408
4546
  requestTimeoutMs,
3409
- retryMaxAttempts: 1,
4547
+ retryMaxAttempts,
3410
4548
  tlsRejectUnauthorized,
3411
4549
  caCertPath,
3412
4550
  });
@@ -3428,19 +4566,21 @@ async function runFixtures(flags) {
3428
4566
  }
3429
4567
  currentCommandContext.projectKey = projectKey;
3430
4568
  const requestTimeoutMs = num(flags["request-timeout"]);
4569
+ const retryMaxAttempts = num(flags["retries"]) ?? 1;
3431
4570
  const client = new DataikuClient({
3432
4571
  url,
3433
4572
  apiKey,
3434
4573
  projectKey,
3435
4574
  verbose: flags["verbose"] === true,
3436
4575
  requestTimeoutMs,
3437
- retryMaxAttempts: 1,
4576
+ retryMaxAttempts,
3438
4577
  tlsRejectUnauthorized,
3439
4578
  caCertPath,
3440
4579
  });
3441
4580
  return discoverFixtureReport(client, projectKey, flags);
3442
4581
  }
3443
4582
  const READ_ACTIONS = new Set([
4583
+ "cat",
3444
4584
  "contents",
3445
4585
  "diff",
3446
4586
  "download",
@@ -3461,13 +4601,18 @@ const READ_ACTIONS = new Set([
3461
4601
  "list-jupyter",
3462
4602
  "list-sql",
3463
4603
  "log",
4604
+ "log-url",
3464
4605
  "map",
3465
4606
  "metadata",
3466
4607
  "peek",
4608
+ "source",
4609
+ "summary",
3467
4610
  "wait",
4611
+ "watch",
3468
4612
  "preview",
3469
4613
  "query",
3470
4614
  "schema",
4615
+ "schemas",
3471
4616
  "sessions-jupyter",
3472
4617
  "status",
3473
4618
  "rules",
@@ -3492,7 +4637,14 @@ const PROJECT_SCOPED_RESOURCES = new Set([
3492
4637
  "wiki",
3493
4638
  ]);
3494
4639
  const GLOBAL_AGENT_FLAGS = ["help", "json", "report-json", "verbose",];
3495
- const AUTHENTICATED_AGENT_FLAGS = ["url", "api-key", "request-timeout", "insecure", "ca-cert",];
4640
+ const AUTHENTICATED_AGENT_FLAGS = [
4641
+ "url",
4642
+ "api-key",
4643
+ "request-timeout",
4644
+ "retries",
4645
+ "insecure",
4646
+ "ca-cert",
4647
+ ];
3496
4648
  const COMMANDS_USAGE = "dss commands [--json]";
3497
4649
  const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
3498
4650
  const COMMANDS_EXAMPLES = ["dss commands", "dss commands --json",];
@@ -3586,7 +4738,7 @@ function inferSideEffect(resource, action) {
3586
4738
  return "write";
3587
4739
  if (READ_ACTIONS.has(action))
3588
4740
  return "read";
3589
- if (/^(create|update|delete|set|save|upload|run|build|abort|move|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)/
3590
4742
  .test(action)) {
3591
4743
  return "write";
3592
4744
  }
@@ -3606,12 +4758,14 @@ function inferRequiresProject(resource, action, usage) {
3606
4758
  }
3607
4759
  const ARRAY_OUTPUT_ACTIONS = new Set([
3608
4760
  "history",
4761
+ "find",
3609
4762
  "infer",
3610
4763
  "last-results",
3611
4764
  "list",
3612
4765
  "list-jupyter",
3613
4766
  "list-sql",
3614
4767
  "rules",
4768
+ "schemas",
3615
4769
  "sessions-jupyter",
3616
4770
  "usages",
3617
4771
  ]);
@@ -3620,7 +4774,9 @@ const STRING_OUTPUT_ACTIONS = new Set([
3620
4774
  "download",
3621
4775
  "download-code",
3622
4776
  "get-payload",
4777
+ "cat",
3623
4778
  "log",
4779
+ "log-url",
3624
4780
  "preview",
3625
4781
  ]);
3626
4782
  function inferOutputShape(resource, action) {
@@ -3661,7 +4817,7 @@ function inferExitCodes(asyncKind) {
3661
4817
  };
3662
4818
  }
3663
4819
  function cleanupCommandFromDeleteUsage(resource, action) {
3664
- if (!action.startsWith("create"))
4820
+ if (!(action.startsWith("create") || action === "clone"))
3665
4821
  return undefined;
3666
4822
  const deleteAction = action === "create-rule" ? "delete-rule" : "delete";
3667
4823
  const deleteUsage = commands[resource]?.[deleteAction]?.usage;
@@ -3684,7 +4840,10 @@ function inferDestructiveLevel(sideEffect, action) {
3684
4840
  return "reversible";
3685
4841
  }
3686
4842
  function inferAsyncKind(resource, action) {
3687
- if (resource === "job" && ["build", "build-and-wait", "wait",].includes(action))
4843
+ if (resource === "job" && ["build", "build-and-wait", "wait", "monitor", "watch",].includes(action)) {
4844
+ return "job";
4845
+ }
4846
+ if (resource === "recipe" && action === "run")
3688
4847
  return "job";
3689
4848
  if (resource === "future" && ["get", "peek", "wait", "abort",].includes(action))
3690
4849
  return "future";
@@ -3705,7 +4864,7 @@ function inferIdempotency(sideEffect, action, usage) {
3705
4864
  return "none";
3706
4865
  }
3707
4866
  function inferCleanupHint(resource, action) {
3708
- if (!action.startsWith("create"))
4867
+ if (!(action.startsWith("create") || action === "clone"))
3709
4868
  return undefined;
3710
4869
  if (resource === "code-env")
3711
4870
  return "Delete with `dss code-env delete <lang> <name> --if-exists`.";
@@ -3762,7 +4921,8 @@ function buildRegistryEntry(resource, action, meta) {
3762
4921
  outputShape: inferOutputShape(resource, action),
3763
4922
  inputContract,
3764
4923
  destructive,
3765
- producesLocalFile: meta.usage.includes("--output PATH"),
4924
+ producesLocalFile: meta.usage.includes("--output PATH")
4925
+ || meta.usage.includes("--output-file PATH"),
3766
4926
  mutatesDss,
3767
4927
  async: asyncKind,
3768
4928
  idempotency: inferIdempotency(sideEffect, action, meta.usage),
@@ -3853,7 +5013,7 @@ function requiredPlanFlag(flags, name, usage) {
3853
5013
  }
3854
5014
  function optionalJsonFlag(flags, name) {
3855
5015
  const value = flags[name];
3856
- return typeof value === "string" ? JSON.parse(value) : undefined;
5016
+ return typeof value === "string" ? parseJsonObject(value, `--${name}`) : undefined;
3857
5017
  }
3858
5018
  function requiredPlanJsonInput(flags, usage) {
3859
5019
  return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
@@ -3874,9 +5034,20 @@ function querySuffix(params) {
3874
5034
  return raw ? `?${raw}` : "";
3875
5035
  }
3876
5036
  function jobBuildPayload(target, projectKey, flags) {
3877
- const targetType = jobBuildTargetType(flags["type"]);
5037
+ const targetType = jobBuildTargetTypeFromFlags(flags);
5038
+ const partition = flags["partition"];
5039
+ const output = { projectKey, id: target, type: targetType, };
5040
+ if (targetType === "DATASET") {
5041
+ if (partition !== undefined)
5042
+ output.partition = partition;
5043
+ }
5044
+ else {
5045
+ output.targetManagedFolderProjectKey = projectKey;
5046
+ output.targetManagedFolder = target;
5047
+ output.targetPartition = partition ?? "NP";
5048
+ }
3878
5049
  const payload = {
3879
- outputs: [{ projectKey, id: target, type: targetType, },],
5050
+ outputs: [output,],
3880
5051
  type: flags["build-mode"] ?? "NON_RECURSIVE_FORCED_BUILD",
3881
5052
  };
3882
5053
  if (flags["force-rebuild"] === true && targetType === "DATASET") {
@@ -4062,9 +5233,9 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4062
5233
  case "flow-zone.move":
4063
5234
  return {
4064
5235
  method: "POST",
4065
- endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/move-to`),
5236
+ endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/add-items`),
4066
5237
  identifiers: { id, },
4067
- payload: { items: flowZoneMoveItems(flags), },
5238
+ payload: flowZoneMoveItems(flags),
4068
5239
  };
4069
5240
  case "dataset.create": {
4070
5241
  const name = requiredPlanFlag(flags, "name", entry.usage);
@@ -4083,6 +5254,15 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4083
5254
  endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
4084
5255
  identifiers: { name: id, },
4085
5256
  };
5257
+ case "dataset.refresh-schema": {
5258
+ const columns = schemaColumnsInput(flags, entry.usage);
5259
+ return {
5260
+ method: "PUT",
5261
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/schema`),
5262
+ identifiers: { name: id, },
5263
+ payload: { columns, },
5264
+ };
5265
+ }
4086
5266
  case "dataset.update":
4087
5267
  return {
4088
5268
  method: "PUT",
@@ -4124,6 +5304,19 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4124
5304
  },
4125
5305
  };
4126
5306
  }
5307
+ case "recipe.run":
5308
+ return {
5309
+ method: "POST",
5310
+ endpoint: projectEndpoint("/jobs/"),
5311
+ identifiers: { recipe: id, },
5312
+ payload: {
5313
+ recipe: id,
5314
+ outputResolution: "dynamic",
5315
+ projectKey,
5316
+ partition: flags["partition"],
5317
+ },
5318
+ wait: recipeRunShouldWait(flags),
5319
+ };
4127
5320
  case "recipe.update":
4128
5321
  return {
4129
5322
  method: "PUT",
@@ -4131,13 +5324,27 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4131
5324
  identifiers: { name: id, },
4132
5325
  payload: requiredPlanJsonInput(flags, entry.usage),
4133
5326
  };
4134
- case "recipe.set-payload":
5327
+ case "recipe.set-payload": {
5328
+ const file = requiredPlanFlag(flags, "file", entry.usage);
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;
4135
5334
  return {
4136
5335
  method: "PUT",
4137
- endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}/payload`),
5336
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
4138
5337
  identifiers: { name: id, },
4139
- payload: { file: flags["file"], content: textInput(flags), },
5338
+ payload: {
5339
+ file,
5340
+ content: textInput(flags),
5341
+ ...(backupPath ? { backupPath, } : {}),
5342
+ },
5343
+ ...(backupPath
5344
+ ? { localWrites: [{ path: backupPath, source: "remote recipe backup", before: "PUT", },], }
5345
+ : {}),
4140
5346
  };
5347
+ }
4141
5348
  case "job.build":
4142
5349
  case "job.build-and-wait":
4143
5350
  return {
@@ -4404,12 +5611,14 @@ async function runCleanup(flags) {
4404
5611
  throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
4405
5612
  }
4406
5613
  const requestTimeoutMs = num(flags["request-timeout"]);
5614
+ const retryMaxAttempts = num(flags["retries"]);
4407
5615
  const client = new DataikuClient({
4408
5616
  url,
4409
5617
  apiKey,
4410
5618
  projectKey,
4411
5619
  verbose: flags["verbose"] === true,
4412
5620
  requestTimeoutMs,
5621
+ retryMaxAttempts,
4413
5622
  tlsRejectUnauthorized,
4414
5623
  caCertPath,
4415
5624
  });
@@ -4479,17 +5688,26 @@ function promptSecret(label) {
4479
5688
  // Credential resolution
4480
5689
  // ---------------------------------------------------------------------------
4481
5690
  function resolveCredentials(flags) {
4482
- let url = flags["url"];
4483
- let apiKey = flags["api-key"];
4484
- 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;
4485
5697
  const saved = loadCredentials();
4486
- url ??= process.env.DATAIKU_URL;
4487
- apiKey ??= process.env.DATAIKU_API_KEY;
4488
- 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;
4489
5704
  if (saved) {
4490
- url ||= saved.url;
4491
- apiKey ||= saved.apiKey;
4492
- projectKey ??= saved.projectKey;
5705
+ if (!hasUrlFlag)
5706
+ url ??= saved.url;
5707
+ if (!hasApiKeyFlag)
5708
+ apiKey ??= saved.apiKey;
5709
+ if (!hasProjectKeyFlag)
5710
+ projectKey ??= saved.projectKey;
4493
5711
  }
4494
5712
  return {
4495
5713
  url: url ?? "",
@@ -4624,7 +5842,7 @@ async function main() {
4624
5842
  const { positional, flags, } = parseArgs(process.argv.slice(2));
4625
5843
  // --version
4626
5844
  if (flags["version"] === true) {
4627
- process.stdout.write(`${CLI_VERSION}\n`);
5845
+ process.stdout.write(`${CLI_VERSION_LABEL}\n`);
4628
5846
  process.exit(0);
4629
5847
  }
4630
5848
  // Top-level help
@@ -4940,12 +6158,14 @@ async function main() {
4940
6158
  throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
4941
6159
  }
4942
6160
  const requestTimeoutMs = num(flags["request-timeout"]);
6161
+ const retryMaxAttempts = num(flags["retries"]);
4943
6162
  const client = new DataikuClient({
4944
6163
  url,
4945
6164
  apiKey,
4946
6165
  projectKey,
4947
6166
  verbose: flags["verbose"] === true,
4948
6167
  requestTimeoutMs,
6168
+ retryMaxAttempts,
4949
6169
  tlsRejectUnauthorized,
4950
6170
  caCertPath,
4951
6171
  });
@@ -4960,7 +6180,12 @@ async function main() {
4960
6180
  if (entry)
4961
6181
  await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
4962
6182
  }
4963
- writeCommandResult(result);
6183
+ if (flags["raw"] === true && typeof result === "string") {
6184
+ process.stdout.write(result);
6185
+ }
6186
+ else {
6187
+ writeCommandResult(result);
6188
+ }
4964
6189
  const failureExitCode = commandFailureExitCode(result);
4965
6190
  if (failureExitCode !== undefined)
4966
6191
  process.exit(failureExitCode);