dataiku-sdk 0.5.1 → 0.6.0

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,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, } from "node:fs";
3
- import { writeFile, } from "node:fs/promises";
4
- import { dirname, resolve, } from "node:path";
3
+ import { mkdir, writeFile, } from "node:fs/promises";
4
+ import { dirname, join, resolve, } from "node:path";
5
5
  import { createInterface, } from "node:readline";
6
6
  import { Writable, } from "node:stream";
7
7
  import { fileURLToPath, } from "node:url";
@@ -12,6 +12,7 @@ import { DataikuError, dataikuErrorCode, } from "./errors.js";
12
12
  import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, } from "./skill.js";
13
13
  import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
14
14
  import { deepMerge, } from "./utils/deep-merge.js";
15
+ import { sanitizeFileName, } from "./utils/sanitize.js";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Utility helpers
17
18
  // ---------------------------------------------------------------------------
@@ -43,12 +44,48 @@ function jobBuildTargetType(v) {
43
44
  if (v === undefined)
44
45
  return "DATASET";
45
46
  if (typeof v !== "string") {
46
- throw new UsageError("Invalid --type value for job build. Use DATASET or MANAGED_FOLDER.", "invalid_enum");
47
+ throw new UsageError("Invalid job target type. Use dataset or managed-folder.", "invalid_enum");
47
48
  }
48
49
  const normalized = v.trim().toUpperCase().replace(/-/g, "_");
49
50
  if (normalized === "DATASET" || normalized === "MANAGED_FOLDER")
50
51
  return normalized;
51
- throw new UsageError("Invalid --type value for job build. Use DATASET or MANAGED_FOLDER.", "invalid_enum");
52
+ throw new UsageError("Invalid job target type. Use dataset or managed-folder.", "invalid_enum");
53
+ }
54
+ function jobBuildTargetTypeFromFlags(flags) {
55
+ return jobBuildTargetType(flags["target-type"] ?? flags["type"]);
56
+ }
57
+ function maxLogLinesFromFlags(flags) {
58
+ return num(flags["max-log-lines"] ?? flags["max-lines"]);
59
+ }
60
+ function jobLogFilterFromFlag(v) {
61
+ if (v === undefined)
62
+ return undefined;
63
+ if (typeof v !== "string") {
64
+ throw new UsageError("Invalid --log-filter value. Use stdout, stderr, user, or errors.", "invalid_enum");
65
+ }
66
+ const normalized = v.trim().toLowerCase();
67
+ if (normalized === "stdout" || normalized === "stderr" || normalized === "user"
68
+ || normalized === "errors") {
69
+ return normalized;
70
+ }
71
+ throw new UsageError("Invalid --log-filter value. Use stdout, stderr, user, or errors.", "invalid_enum");
72
+ }
73
+ function recipePayloadBackupPath(recipeName, backupDir) {
74
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
75
+ return join(backupDir, `${sanitizeFileName(recipeName, "recipe")}-${stamp}.payload`);
76
+ }
77
+ function recipeRunShouldWait(flags) {
78
+ if (flags["wait"] === true && flags["no-wait"] === true) {
79
+ throw new UsageError("--wait and --no-wait are mutually exclusive.", "invalid_enum");
80
+ }
81
+ const waitImplied = flags["include-logs"] === true
82
+ || flags["summary"] === true
83
+ || flags["timeout"] !== undefined
84
+ || flags["poll-interval"] !== undefined;
85
+ if (flags["no-wait"] === true && waitImplied) {
86
+ throw new UsageError("--include-logs, --summary, --timeout, and --poll-interval require waiting; remove --no-wait.", "invalid_enum");
87
+ }
88
+ return flags["no-wait"] !== true && (flags["wait"] === true || waitImplied);
52
89
  }
53
90
  function splitCsvFlag(v) {
54
91
  if (typeof v !== "string")
@@ -130,7 +167,7 @@ function json(v) {
130
167
  return undefined;
131
168
  return JSON.parse(v);
132
169
  }
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]";
170
+ 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
171
  function readStdinText() {
135
172
  return readFileSync(0, "utf-8");
136
173
  }
@@ -146,6 +183,48 @@ function jsonInput(flags) {
146
183
  }
147
184
  return undefined;
148
185
  }
186
+ function unknownJsonInput(flags) {
187
+ if (flags["stdin"] === true)
188
+ return JSON.parse(readStdinText());
189
+ if (typeof flags["data-file"] === "string") {
190
+ return JSON.parse(readFileSync(flags["data-file"], "utf-8"));
191
+ }
192
+ if (typeof flags["data"] === "string")
193
+ return JSON.parse(flags["data"]);
194
+ return undefined;
195
+ }
196
+ function schemaColumnsInput(flags, usage) {
197
+ const input = unknownJsonInput(flags);
198
+ if (input === undefined) {
199
+ throw new UsageError(`--data, --data-file, or --stdin is required. Usage: ${usage}`);
200
+ }
201
+ const columns = Array.isArray(input)
202
+ ? input
203
+ : input && typeof input === "object" && Array.isArray(input.columns)
204
+ ? input.columns
205
+ : undefined;
206
+ if (!columns) {
207
+ throw new UsageError("Schema input must be an array of columns or an object with a columns array.");
208
+ }
209
+ return columns.map((column, index) => {
210
+ if (!column || typeof column !== "object" || Array.isArray(column)) {
211
+ throw new UsageError(`Schema column at index ${index} must be an object.`);
212
+ }
213
+ const record = column;
214
+ if (typeof record.name !== "string" || record.name.length === 0) {
215
+ throw new UsageError(`Schema column at index ${index} is missing string field "name".`);
216
+ }
217
+ if (typeof record.type !== "string" || record.type.length === 0) {
218
+ throw new UsageError(`Schema column "${record.name}" is missing string field "type".`);
219
+ }
220
+ return {
221
+ ...record,
222
+ name: record.name,
223
+ type: record.type,
224
+ ...(typeof record.comment === "string" ? { comment: record.comment, } : {}),
225
+ };
226
+ });
227
+ }
149
228
  function textInput(flags) {
150
229
  if (typeof flags["content"] === "string")
151
230
  return flags["content"];
@@ -289,6 +368,28 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
289
368
  function writeCommandResult(result) {
290
369
  process.stdout.write(`${JSON.stringify(result ?? { ok: true, }, null, 2)}\n`);
291
370
  }
371
+ function transientBodyWithTargetContext(body, target, elapsedMs) {
372
+ try {
373
+ const parsed = JSON.parse(body);
374
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
375
+ const record = parsed;
376
+ const message = typeof record.message === "string" && record.message.length > 0
377
+ ? `Target: ${target}\nElapsed: ${elapsedMs}ms\n${record.message}`
378
+ : `Target: ${target}\nElapsed: ${elapsedMs}ms`;
379
+ return JSON.stringify({ ...record, message, target, elapsedMs, });
380
+ }
381
+ }
382
+ catch {
383
+ // Non-JSON DSS bodies are wrapped as text below.
384
+ }
385
+ return `Target: ${target}\nElapsed: ${elapsedMs}ms\n${body}`;
386
+ }
387
+ function addTransientTargetContext(error, target, elapsedMs) {
388
+ if (error instanceof DataikuError && error.category === "transient") {
389
+ throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry);
390
+ }
391
+ throw error;
392
+ }
292
393
  function isFailedWaitResult(result) {
293
394
  if (result === null || typeof result !== "object" || Array.isArray(result))
294
395
  return false;
@@ -331,6 +432,7 @@ function planResult(resource, action, options) {
331
432
  ...(options.method ? { method: options.method, } : {}),
332
433
  ...(options.endpoint ? { endpoint: options.endpoint, } : {}),
333
434
  ...(options.payload !== undefined ? { payload: options.payload, } : {}),
435
+ ...(options.localWrites !== undefined ? { localWrites: options.localWrites, } : {}),
334
436
  ...(options.wait !== undefined ? { wait: options.wait, } : {}),
335
437
  idempotency: options.idempotency,
336
438
  async: options.asyncKind,
@@ -507,6 +609,7 @@ const BOOLEAN_FLAGS = new Set([
507
609
  "include-payload",
508
610
  "no-payload",
509
611
  "include-logs",
612
+ "summary",
510
613
  "replace",
511
614
  "dry-run",
512
615
  "plan",
@@ -542,7 +645,9 @@ const VALUE_FLAGS = new Set([
542
645
  "agent",
543
646
  "api-key",
544
647
  "build-mode",
648
+ "backup-dir",
545
649
  "ca-cert",
650
+ "catalog",
546
651
  "cell-id",
547
652
  "allow-types",
548
653
  "color",
@@ -568,6 +673,7 @@ const VALUE_FLAGS = new Set([
568
673
  "local",
569
674
  "max-edges",
570
675
  "max-lines",
676
+ "max-log-lines",
571
677
  "listed",
572
678
  "max-nodes",
573
679
  "max-rows",
@@ -575,10 +681,12 @@ const VALUE_FLAGS = new Set([
575
681
  "only-monitored",
576
682
  "min-timestamp",
577
683
  "mode",
684
+ "log-filter",
578
685
  "model-evaluation-store",
579
686
  "name",
580
687
  "object",
581
688
  "output",
689
+ "output-file",
582
690
  "output-connection",
583
691
  "output-folder",
584
692
  "page",
@@ -592,15 +700,18 @@ const VALUE_FLAGS = new Set([
592
700
  "results-per-page",
593
701
  "record-cleanup",
594
702
  "rule-id",
703
+ "retries",
595
704
  "poll-interval",
596
705
  "python-interpreter",
597
706
  "retain",
598
707
  "saved-model",
599
708
  "sql",
709
+ "schema",
600
710
  "sql-file",
601
711
  "standard",
602
712
  "streaming-endpoint",
603
713
  "target",
714
+ "target-type",
604
715
  "timeout",
605
716
  "type",
606
717
  "url",
@@ -1532,6 +1643,41 @@ const commands = {
1532
1643
  description: "Show the column schema of a dataset.",
1533
1644
  examples: ["dss dataset schema orders",],
1534
1645
  },
1646
+ "refresh-schema": {
1647
+ handler: async (c, a, f) => {
1648
+ const usage = "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]";
1649
+ requireArgs(a, 1, usage);
1650
+ const columns = schemaColumnsInput(f, usage);
1651
+ const pk = f["project-key"];
1652
+ if (f["dry-run"] === true) {
1653
+ const current = await c.datasets.schema(a[0], pk);
1654
+ return {
1655
+ dryRun: true,
1656
+ action: "refresh-schema",
1657
+ resource: "dataset",
1658
+ name: a[0],
1659
+ current,
1660
+ next: { columns, },
1661
+ };
1662
+ }
1663
+ await c.datasets.updateSchema(a[0], columns, pk);
1664
+ return { updated: a[0], resource: "dataset", schema: { columns, }, };
1665
+ },
1666
+ usage: "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]",
1667
+ description: "Replace a dataset schema through the DSS schema endpoint.",
1668
+ examples: [
1669
+ `dss dataset refresh-schema orders --data '{"columns":[{"name":"id","type":"bigint"}]}' --dry-run`,
1670
+ ],
1671
+ },
1672
+ "validate-build": {
1673
+ handler: (c, a, f) => {
1674
+ requireArgs(a, 1, "dss dataset validate-build <name>");
1675
+ return c.datasets.validateBuildSettings(a[0], f["project-key"]);
1676
+ },
1677
+ usage: "dss dataset validate-build <name> [--project-key KEY]",
1678
+ description: "Check common dataset settings that can make file-backed builds fail.",
1679
+ examples: ["dss dataset validate-build orders",],
1680
+ },
1535
1681
  preview: {
1536
1682
  handler: (c, a, f) => {
1537
1683
  requireArgs(a, 1, "dss dataset preview <name>");
@@ -1674,6 +1820,60 @@ const commands = {
1674
1820
  "dss recipe get compute_orders --include-payload",
1675
1821
  ],
1676
1822
  },
1823
+ "validate-graph": {
1824
+ handler: (c, a, f) => {
1825
+ requireArgs(a, 1, "dss recipe validate-graph <name>");
1826
+ return c.recipes.validateGraph(a[0], {
1827
+ projectKey: f["project-key"],
1828
+ });
1829
+ },
1830
+ usage: "dss recipe validate-graph <name> [--project-key KEY]",
1831
+ description: "Validate declared recipe input/output graph references before building.",
1832
+ examples: ["dss recipe validate-graph compute_orders",],
1833
+ },
1834
+ run: {
1835
+ handler: async (c, a, f) => {
1836
+ requireArgs(a, 1, "dss recipe run <name>");
1837
+ const pk = f["project-key"];
1838
+ const wait = recipeRunShouldWait(f);
1839
+ const options = {
1840
+ buildMode: f["build-mode"],
1841
+ includeLogs: f["include-logs"] === true,
1842
+ logFilter: jobLogFilterFromFlag(f["log-filter"]),
1843
+ maxLogLines: maxLogLinesFromFlags(f),
1844
+ partition: f["partition"],
1845
+ pollIntervalMs: num(f["poll-interval"]),
1846
+ projectKey: pk,
1847
+ timeoutMs: num(f["timeout"]),
1848
+ summary: f["summary"] === true,
1849
+ wait,
1850
+ };
1851
+ if (f["dry-run"] === true) {
1852
+ const outputs = await c.recipes.resolveRunOutputs(a[0], {
1853
+ partition: options.partition,
1854
+ projectKey: pk,
1855
+ });
1856
+ return {
1857
+ dryRun: true,
1858
+ action: "run",
1859
+ resource: "recipe",
1860
+ recipe: a[0],
1861
+ outputs,
1862
+ ...options,
1863
+ endpoint: encodedProjectEndpoint(c, pk, "/jobs/"),
1864
+ method: "POST",
1865
+ };
1866
+ }
1867
+ return c.recipes.run(a[0], options);
1868
+ },
1869
+ 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]",
1870
+ description: "Run a recipe by resolving its outputs and submitting the correct dataset or managed-folder build job.",
1871
+ examples: [
1872
+ "dss recipe run compute_orders --wait",
1873
+ "dss recipe run compute_exports --include-logs --log-filter stdout --summary --timeout 600000",
1874
+ "dss recipe run compute_exports --dry-run",
1875
+ ],
1876
+ },
1677
1877
  delete: {
1678
1878
  handler: async (c, a, f) => {
1679
1879
  requireArgs(a, 1, "dss recipe delete <name>");
@@ -1860,6 +2060,8 @@ const commands = {
1860
2060
  if (!filePath)
1861
2061
  throw new UsageError("--file is required.");
1862
2062
  const content = readFileSync(filePath, "utf-8");
2063
+ const backupDir = f["backup-dir"];
2064
+ const backupPath = backupDir ? recipePayloadBackupPath(a[0], backupDir) : undefined;
1863
2065
  if (f["dry-run"] === true) {
1864
2066
  const current = await c.recipes.get(a[0], {
1865
2067
  projectKey: f["project-key"],
@@ -1873,16 +2075,33 @@ const commands = {
1873
2075
  file: filePath,
1874
2076
  current,
1875
2077
  next: { ...current, payload: content, },
2078
+ ...(backupPath ? { backupPath, } : {}),
1876
2079
  };
1877
2080
  }
2081
+ if (backupDir && backupPath) {
2082
+ const current = await c.recipes.get(a[0], {
2083
+ projectKey: f["project-key"],
2084
+ includePayload: true,
2085
+ });
2086
+ await mkdir(backupDir, { recursive: true, });
2087
+ await writeFile(backupPath, current.payload ?? "", "utf-8");
2088
+ }
1878
2089
  await c.recipes.setPayload(a[0], content, {
1879
2090
  projectKey: f["project-key"],
1880
2091
  });
1881
- return { updated: a[0], resource: "recipe", file: filePath, };
2092
+ return {
2093
+ updated: a[0],
2094
+ resource: "recipe",
2095
+ file: filePath,
2096
+ ...(backupPath ? { backupPath, } : {}),
2097
+ };
1882
2098
  },
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",],
2099
+ usage: "dss recipe set-payload <name> --file PATH [--backup-dir DIR] [--dry-run] [--project-key KEY]",
2100
+ description: "Upload recipe code from a local file, optionally backing up the remote payload first.",
2101
+ examples: [
2102
+ "dss recipe set-payload compute_orders --file code.py --dry-run",
2103
+ "dss recipe set-payload compute_orders --file code.py --backup-dir ./backups",
2104
+ ],
1886
2105
  },
1887
2106
  },
1888
2107
  job: {
@@ -1906,12 +2125,13 @@ const commands = {
1906
2125
  requireArgs(a, 1, "dss job log <id>");
1907
2126
  return c.jobs.log(a[0], {
1908
2127
  activity: f["activity"],
1909
- maxLogLines: num(f["max-lines"]),
2128
+ maxLogLines: maxLogLinesFromFlags(f),
2129
+ projectKey: f["project-key"],
1910
2130
  });
1911
2131
  },
1912
- usage: "dss job log <id> [--activity NAME] [--max-lines N]",
2132
+ usage: "dss job log <id> [--activity NAME] [--max-lines N|--max-log-lines N]",
1913
2133
  description: "Get log output for a job.",
1914
- examples: ["dss job log JOB_ID", "dss job log JOB_ID --activity main --max-lines 200",],
2134
+ examples: ["dss job log JOB_ID", "dss job log JOB_ID --activity main --max-log-lines 200",],
1915
2135
  },
1916
2136
  build: {
1917
2137
  handler: async (c, a, f) => {
@@ -1919,8 +2139,9 @@ const commands = {
1919
2139
  const pk = f["project-key"];
1920
2140
  const options = {
1921
2141
  buildMode: f["build-mode"],
2142
+ partition: f["partition"],
1922
2143
  pollIntervalMs: num(f["poll-interval"]),
1923
- targetType: jobBuildTargetType(f["type"]),
2144
+ targetType: jobBuildTargetTypeFromFlags(f),
1924
2145
  timeoutMs: num(f["timeout"]),
1925
2146
  };
1926
2147
  if (f["dry-run"] === true) {
@@ -1939,12 +2160,12 @@ const commands = {
1939
2160
  }
1940
2161
  return c.jobs.build(a[0], { ...options, projectKey: pk, });
1941
2162
  },
1942
- usage: "dss job build <target> [--type DATASET|MANAGED_FOLDER] [--build-mode MODE] [--wait] [--timeout MS] [--poll-interval MS] [--dry-run] [--project-key KEY]",
2163
+ 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
2164
  description: "Start a dataset or managed-folder build, optionally waiting for completion.",
1944
2165
  examples: [
1945
2166
  "dss job build orders",
1946
2167
  "dss job build orders --build-mode RECURSIVE_BUILD --wait",
1947
- "dss job build LT7TUHJ8 --type MANAGED_FOLDER --dry-run",
2168
+ "dss job build LT7TUHJ8 --target-type managed-folder --dry-run",
1948
2169
  ],
1949
2170
  },
1950
2171
  "build-and-wait": {
@@ -1954,9 +2175,13 @@ const commands = {
1954
2175
  const options = {
1955
2176
  buildMode: f["build-mode"],
1956
2177
  includeLogs: f["include-logs"] === true,
2178
+ logFilter: jobLogFilterFromFlag(f["log-filter"]),
2179
+ maxLogLines: maxLogLinesFromFlags(f),
2180
+ partition: f["partition"],
1957
2181
  pollIntervalMs: num(f["poll-interval"]),
1958
2182
  timeoutMs: num(f["timeout"]),
1959
- targetType: jobBuildTargetType(f["type"]),
2183
+ summary: f["summary"] === true,
2184
+ targetType: jobBuildTargetTypeFromFlags(f),
1960
2185
  };
1961
2186
  if (f["dry-run"] === true) {
1962
2187
  return {
@@ -1971,13 +2196,13 @@ const commands = {
1971
2196
  }
1972
2197
  return c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, });
1973
2198
  },
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]",
2199
+ 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
2200
  description: "Build a dataset or managed folder and wait for completion.",
1976
2201
  examples: [
1977
2202
  "dss job build-and-wait orders",
1978
- "dss job build-and-wait orders --include-logs",
2203
+ "dss job build-and-wait orders --include-logs --log-filter stdout --summary",
1979
2204
  "dss job build-and-wait orders --timeout 300000",
1980
- "dss job build-and-wait LT7TUHJ8 --type MANAGED_FOLDER --dry-run",
2205
+ "dss job build-and-wait LT7TUHJ8 --target-type managed-folder --dry-run",
1981
2206
  ],
1982
2207
  },
1983
2208
  wait: {
@@ -1985,13 +2210,20 @@ const commands = {
1985
2210
  requireArgs(a, 1, "dss job wait <id>");
1986
2211
  return c.jobs.wait(a[0], {
1987
2212
  includeLogs: f["include-logs"] === true,
2213
+ logFilter: jobLogFilterFromFlag(f["log-filter"]),
2214
+ maxLogLines: maxLogLinesFromFlags(f),
1988
2215
  pollIntervalMs: num(f["poll-interval"]),
1989
2216
  timeoutMs: num(f["timeout"]),
2217
+ summary: f["summary"] === true,
2218
+ projectKey: f["project-key"],
1990
2219
  });
1991
2220
  },
1992
- usage: "dss job wait <id> [--include-logs] [--timeout MS] [--poll-interval MS]",
2221
+ usage: "dss job wait <id> [--include-logs] [--log-filter stdout|stderr|user|errors] [--summary] [--max-log-lines N] [--timeout MS] [--poll-interval MS]",
1993
2222
  description: "Wait for an existing job to complete.",
1994
- examples: ["dss job wait JOB_ID", "dss job wait JOB_ID --include-logs --timeout 60000",],
2223
+ examples: [
2224
+ "dss job wait JOB_ID",
2225
+ "dss job wait JOB_ID --include-logs --log-filter stdout --summary --timeout 60000",
2226
+ ],
1995
2227
  },
1996
2228
  abort: {
1997
2229
  handler: async (c, a, f) => {
@@ -2297,13 +2529,24 @@ const commands = {
2297
2529
  contents: {
2298
2530
  handler: async (c, a, f) => {
2299
2531
  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
- });
2532
+ const startedAt = Date.now();
2533
+ let folderId = a[0];
2534
+ try {
2535
+ folderId = await resolveFolderId(c, a[0], f);
2536
+ return await c.folders.contents(folderId, {
2537
+ projectKey: f["project-key"],
2538
+ });
2539
+ }
2540
+ catch (error) {
2541
+ addTransientTargetContext(error, `folder:${folderId}`, Date.now() - startedAt);
2542
+ }
2303
2543
  },
2304
- usage: "dss folder contents <name-or-id> [--project-key KEY]",
2544
+ usage: "dss folder contents <name-or-id> [--retries N] [--request-timeout MS] [--project-key KEY]",
2305
2545
  description: "List files in a managed folder.",
2306
- examples: ["dss folder contents my_folder",],
2546
+ examples: [
2547
+ "dss folder contents my_folder",
2548
+ "dss folder contents my_folder --retries 8 --request-timeout 60000",
2549
+ ],
2307
2550
  },
2308
2551
  download: {
2309
2552
  handler: async (c, a, f) => {
@@ -2425,6 +2668,40 @@ const commands = {
2425
2668
  description: "List connections with inferred types and metadata.",
2426
2669
  examples: ["dss connection infer", "dss connection infer --mode rich",],
2427
2670
  },
2671
+ schemas: {
2672
+ handler: (c, _a, f) => {
2673
+ const connection = f["connection"];
2674
+ if (!connection) {
2675
+ throw new UsageError("--connection is required. Usage: dss connection schemas --connection CONN");
2676
+ }
2677
+ return c.connections.schemas({
2678
+ connection,
2679
+ projectKey: f["project-key"],
2680
+ });
2681
+ },
2682
+ usage: "dss connection schemas --connection CONN [--project-key KEY]",
2683
+ description: "List schemas in a SQL connection.",
2684
+ examples: ["dss connection schemas --connection ATHENA_CONN --project-key MYPROJ",],
2685
+ },
2686
+ tables: {
2687
+ handler: (c, _a, f) => {
2688
+ const connection = f["connection"];
2689
+ if (!connection) {
2690
+ throw new UsageError("--connection is required. Usage: dss connection tables --connection CONN");
2691
+ }
2692
+ return c.connections.tables({
2693
+ connection,
2694
+ catalog: f["catalog"],
2695
+ schema: f["schema"],
2696
+ projectKey: f["project-key"],
2697
+ });
2698
+ },
2699
+ usage: "dss connection tables --connection CONN [--catalog CATALOG] [--schema SCHEMA] [--project-key KEY]",
2700
+ description: "List importable tables in a SQL connection, optionally scoped by catalog and schema.",
2701
+ examples: [
2702
+ "dss connection tables --connection ATHENA_CONN --schema analytics --project-key MYPROJ",
2703
+ ],
2704
+ },
2428
2705
  },
2429
2706
  "code-env": {
2430
2707
  list: {
@@ -2644,20 +2921,35 @@ const commands = {
2644
2921
  },
2645
2922
  sql: {
2646
2923
  query: {
2647
- handler: (c, a, f) => {
2924
+ handler: async (c, a, f) => {
2648
2925
  const query = resolveSqlInput(a, f);
2649
2926
  const connection = f["connection"];
2650
2927
  const datasetFullName = f["dataset"];
2651
2928
  if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
2652
2929
  throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
2653
2930
  }
2654
- return c.sql.query({
2931
+ const result = await c.sql.query({
2655
2932
  query,
2656
2933
  connection,
2657
2934
  datasetFullName,
2658
2935
  database: f["database"],
2659
2936
  projectKey: f["project-key"],
2660
2937
  });
2938
+ const outputFile = f["output"]
2939
+ ?? f["output-file"];
2940
+ if (!outputFile)
2941
+ return result;
2942
+ const outputPath = resolve(outputFile);
2943
+ await mkdir(dirname(outputPath), { recursive: true, });
2944
+ await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
2945
+ return {
2946
+ queryId: result.queryId,
2947
+ schema: result.schema,
2948
+ columns: result.columns ?? result.schema,
2949
+ rowCount: result.rows.length,
2950
+ outputPath,
2951
+ written: outputPath,
2952
+ };
2661
2953
  },
2662
2954
  usage: SQL_QUERY_USAGE,
2663
2955
  description: "Run a SQL query against a DSS connection or dataset.",
@@ -2665,6 +2957,7 @@ const commands = {
2665
2957
  "dss sql query 'SELECT * FROM orders LIMIT 10' --connection my_pg",
2666
2958
  "dss sql query --sql-file query.sql --connection my_pg",
2667
2959
  "echo 'SELECT 1' | dss sql query --stdin --dataset MYPROJ.orders",
2960
+ "dss sql query --sql-file query.sql --connection my_pg --output results.json --request-timeout 120000",
2668
2961
  ],
2669
2962
  },
2670
2963
  },
@@ -2920,7 +3213,7 @@ function printTopLevelHelp() {
2920
3213
  " --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
2921
3214
  " --api-key KEY API key (env: DATAIKU_API_KEY)",
2922
3215
  " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
2923
- " --timeout MS Operation timeout (build-and-wait, run-and-wait)",
3216
+ " --timeout MS Operation timeout (build-and-wait, run-and-wait, recipe run)",
2924
3217
  " --request-timeout MS HTTP request timeout in ms (default: 30000)",
2925
3218
  " --dry-run Preview destructive actions without executing",
2926
3219
  " --if-not-exists Skip create if resource already exists",
@@ -3347,12 +3640,14 @@ async function runDoctor(flags) {
3347
3640
  let accessibleProjects;
3348
3641
  if (credentialsOk) {
3349
3642
  const requestTimeoutMs = num(flags["request-timeout"]);
3643
+ const retryMaxAttempts = num(flags["retries"]);
3350
3644
  const client = new DataikuClient({
3351
3645
  url,
3352
3646
  apiKey,
3353
3647
  projectKey,
3354
3648
  verbose: flags["verbose"] === true,
3355
3649
  requestTimeoutMs,
3650
+ retryMaxAttempts,
3356
3651
  tlsRejectUnauthorized,
3357
3652
  caCertPath,
3358
3653
  });
@@ -3400,13 +3695,14 @@ async function runDoctor(flags) {
3400
3695
  const result = { ok: checks.every((check) => check.ok), checks, context, };
3401
3696
  if (flags["capabilities"] === true && credentialsOk) {
3402
3697
  const requestTimeoutMs = num(flags["request-timeout"]);
3698
+ const retryMaxAttempts = num(flags["retries"]) ?? 1;
3403
3699
  const client = new DataikuClient({
3404
3700
  url,
3405
3701
  apiKey,
3406
3702
  projectKey,
3407
3703
  verbose: flags["verbose"] === true,
3408
3704
  requestTimeoutMs,
3409
- retryMaxAttempts: 1,
3705
+ retryMaxAttempts,
3410
3706
  tlsRejectUnauthorized,
3411
3707
  caCertPath,
3412
3708
  });
@@ -3428,13 +3724,14 @@ async function runFixtures(flags) {
3428
3724
  }
3429
3725
  currentCommandContext.projectKey = projectKey;
3430
3726
  const requestTimeoutMs = num(flags["request-timeout"]);
3727
+ const retryMaxAttempts = num(flags["retries"]) ?? 1;
3431
3728
  const client = new DataikuClient({
3432
3729
  url,
3433
3730
  apiKey,
3434
3731
  projectKey,
3435
3732
  verbose: flags["verbose"] === true,
3436
3733
  requestTimeoutMs,
3437
- retryMaxAttempts: 1,
3734
+ retryMaxAttempts,
3438
3735
  tlsRejectUnauthorized,
3439
3736
  caCertPath,
3440
3737
  });
@@ -3468,6 +3765,7 @@ const READ_ACTIONS = new Set([
3468
3765
  "preview",
3469
3766
  "query",
3470
3767
  "schema",
3768
+ "schemas",
3471
3769
  "sessions-jupyter",
3472
3770
  "status",
3473
3771
  "rules",
@@ -3492,7 +3790,14 @@ const PROJECT_SCOPED_RESOURCES = new Set([
3492
3790
  "wiki",
3493
3791
  ]);
3494
3792
  const GLOBAL_AGENT_FLAGS = ["help", "json", "report-json", "verbose",];
3495
- const AUTHENTICATED_AGENT_FLAGS = ["url", "api-key", "request-timeout", "insecure", "ca-cert",];
3793
+ const AUTHENTICATED_AGENT_FLAGS = [
3794
+ "url",
3795
+ "api-key",
3796
+ "request-timeout",
3797
+ "retries",
3798
+ "insecure",
3799
+ "ca-cert",
3800
+ ];
3496
3801
  const COMMANDS_USAGE = "dss commands [--json]";
3497
3802
  const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
3498
3803
  const COMMANDS_EXAMPLES = ["dss commands", "dss commands --json",];
@@ -3586,7 +3891,7 @@ function inferSideEffect(resource, action) {
3586
3891
  return "write";
3587
3892
  if (READ_ACTIONS.has(action))
3588
3893
  return "read";
3589
- if (/^(create|update|delete|set|save|upload|run|build|abort|move|clear|unload|install|login|logout)/
3894
+ if (/^(create|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
3590
3895
  .test(action)) {
3591
3896
  return "write";
3592
3897
  }
@@ -3612,6 +3917,7 @@ const ARRAY_OUTPUT_ACTIONS = new Set([
3612
3917
  "list-jupyter",
3613
3918
  "list-sql",
3614
3919
  "rules",
3920
+ "schemas",
3615
3921
  "sessions-jupyter",
3616
3922
  "usages",
3617
3923
  ]);
@@ -3686,6 +3992,8 @@ function inferDestructiveLevel(sideEffect, action) {
3686
3992
  function inferAsyncKind(resource, action) {
3687
3993
  if (resource === "job" && ["build", "build-and-wait", "wait",].includes(action))
3688
3994
  return "job";
3995
+ if (resource === "recipe" && action === "run")
3996
+ return "job";
3689
3997
  if (resource === "future" && ["get", "peek", "wait", "abort",].includes(action))
3690
3998
  return "future";
3691
3999
  if (resource === "scenario" && ["run", "run-and-wait", "status",].includes(action)) {
@@ -3762,7 +4070,8 @@ function buildRegistryEntry(resource, action, meta) {
3762
4070
  outputShape: inferOutputShape(resource, action),
3763
4071
  inputContract,
3764
4072
  destructive,
3765
- producesLocalFile: meta.usage.includes("--output PATH"),
4073
+ producesLocalFile: meta.usage.includes("--output PATH")
4074
+ || meta.usage.includes("--output-file PATH"),
3766
4075
  mutatesDss,
3767
4076
  async: asyncKind,
3768
4077
  idempotency: inferIdempotency(sideEffect, action, meta.usage),
@@ -3874,9 +4183,20 @@ function querySuffix(params) {
3874
4183
  return raw ? `?${raw}` : "";
3875
4184
  }
3876
4185
  function jobBuildPayload(target, projectKey, flags) {
3877
- const targetType = jobBuildTargetType(flags["type"]);
4186
+ const targetType = jobBuildTargetTypeFromFlags(flags);
4187
+ const partition = flags["partition"];
4188
+ const output = { projectKey, id: target, type: targetType, };
4189
+ if (targetType === "DATASET") {
4190
+ if (partition !== undefined)
4191
+ output.partition = partition;
4192
+ }
4193
+ else {
4194
+ output.targetManagedFolderProjectKey = projectKey;
4195
+ output.targetManagedFolder = target;
4196
+ output.targetPartition = partition ?? "NP";
4197
+ }
3878
4198
  const payload = {
3879
- outputs: [{ projectKey, id: target, type: targetType, },],
4199
+ outputs: [output,],
3880
4200
  type: flags["build-mode"] ?? "NON_RECURSIVE_FORCED_BUILD",
3881
4201
  };
3882
4202
  if (flags["force-rebuild"] === true && targetType === "DATASET") {
@@ -4062,9 +4382,9 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4062
4382
  case "flow-zone.move":
4063
4383
  return {
4064
4384
  method: "POST",
4065
- endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/move-to`),
4385
+ endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/add-items`),
4066
4386
  identifiers: { id, },
4067
- payload: { items: flowZoneMoveItems(flags), },
4387
+ payload: flowZoneMoveItems(flags),
4068
4388
  };
4069
4389
  case "dataset.create": {
4070
4390
  const name = requiredPlanFlag(flags, "name", entry.usage);
@@ -4083,6 +4403,15 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4083
4403
  endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
4084
4404
  identifiers: { name: id, },
4085
4405
  };
4406
+ case "dataset.refresh-schema": {
4407
+ const columns = schemaColumnsInput(flags, entry.usage);
4408
+ return {
4409
+ method: "PUT",
4410
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/schema`),
4411
+ identifiers: { name: id, },
4412
+ payload: { columns, },
4413
+ };
4414
+ }
4086
4415
  case "dataset.update":
4087
4416
  return {
4088
4417
  method: "PUT",
@@ -4124,6 +4453,19 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4124
4453
  },
4125
4454
  };
4126
4455
  }
4456
+ case "recipe.run":
4457
+ return {
4458
+ method: "POST",
4459
+ endpoint: projectEndpoint("/jobs/"),
4460
+ identifiers: { recipe: id, },
4461
+ payload: {
4462
+ recipe: id,
4463
+ outputResolution: "dynamic",
4464
+ projectKey,
4465
+ partition: flags["partition"],
4466
+ },
4467
+ wait: recipeRunShouldWait(flags),
4468
+ };
4127
4469
  case "recipe.update":
4128
4470
  return {
4129
4471
  method: "PUT",
@@ -4131,13 +4473,24 @@ function commandPlanShape(resource, action, args, flags, entry, projectKey) {
4131
4473
  identifiers: { name: id, },
4132
4474
  payload: requiredPlanJsonInput(flags, entry.usage),
4133
4475
  };
4134
- case "recipe.set-payload":
4476
+ case "recipe.set-payload": {
4477
+ const file = requiredPlanFlag(flags, "file", entry.usage);
4478
+ const backupDir = flags["backup-dir"];
4479
+ const backupPath = backupDir ? recipePayloadBackupPath(id, backupDir) : undefined;
4135
4480
  return {
4136
4481
  method: "PUT",
4137
- endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}/payload`),
4482
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
4138
4483
  identifiers: { name: id, },
4139
- payload: { file: flags["file"], content: textInput(flags), },
4484
+ payload: {
4485
+ file,
4486
+ content: textInput(flags),
4487
+ ...(backupPath ? { backupPath, } : {}),
4488
+ },
4489
+ ...(backupPath
4490
+ ? { localWrites: [{ path: backupPath, source: "remote recipe payload", before: "PUT", },], }
4491
+ : {}),
4140
4492
  };
4493
+ }
4141
4494
  case "job.build":
4142
4495
  case "job.build-and-wait":
4143
4496
  return {
@@ -4404,12 +4757,14 @@ async function runCleanup(flags) {
4404
4757
  throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
4405
4758
  }
4406
4759
  const requestTimeoutMs = num(flags["request-timeout"]);
4760
+ const retryMaxAttempts = num(flags["retries"]);
4407
4761
  const client = new DataikuClient({
4408
4762
  url,
4409
4763
  apiKey,
4410
4764
  projectKey,
4411
4765
  verbose: flags["verbose"] === true,
4412
4766
  requestTimeoutMs,
4767
+ retryMaxAttempts,
4413
4768
  tlsRejectUnauthorized,
4414
4769
  caCertPath,
4415
4770
  });
@@ -4940,12 +5295,14 @@ async function main() {
4940
5295
  throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
4941
5296
  }
4942
5297
  const requestTimeoutMs = num(flags["request-timeout"]);
5298
+ const retryMaxAttempts = num(flags["retries"]);
4943
5299
  const client = new DataikuClient({
4944
5300
  url,
4945
5301
  apiKey,
4946
5302
  projectKey,
4947
5303
  verbose: flags["verbose"] === true,
4948
5304
  requestTimeoutMs,
5305
+ retryMaxAttempts,
4949
5306
  tlsRejectUnauthorized,
4950
5307
  caCertPath,
4951
5308
  });