dataiku-sdk 3.9.0 → 4.0.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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +4 -0
  4. package/dist/packages/types/src/index.js +4 -0
  5. package/dist/src/cli/coerce.d.ts +6 -0
  6. package/dist/src/cli/coerce.js +9 -0
  7. package/dist/src/cli/commands/app.js +2 -2
  8. package/dist/src/cli/commands/code-env.js +5 -6
  9. package/dist/src/cli/commands/code.js +16 -2
  10. package/dist/src/cli/commands/dashboard.js +1 -2
  11. package/dist/src/cli/commands/flow-zone.js +1 -1
  12. package/dist/src/cli/commands/job.js +67 -16
  13. package/dist/src/cli/commands/plugin.js +7 -7
  14. package/dist/src/cli/commands/project-library.js +2 -2
  15. package/dist/src/cli/commands/project.js +21 -4
  16. package/dist/src/cli/commands/recipe.js +11 -19
  17. package/dist/src/cli/commands/scenario.js +2 -4
  18. package/dist/src/cli/contract.js +27 -5
  19. package/dist/src/cli/env.js +2 -3
  20. package/dist/src/cli/helpers/app-permissions.d.ts +1 -1
  21. package/dist/src/cli/helpers/app-permissions.js +2 -2
  22. package/dist/src/cli/helpers/cleanup.d.ts +2 -0
  23. package/dist/src/cli/helpers/cleanup.js +122 -8
  24. package/dist/src/cli/helpers/flow-zone.d.ts +1 -1
  25. package/dist/src/cli/helpers/flow-zone.js +3 -4
  26. package/dist/src/cli/list-output.d.ts +5 -0
  27. package/dist/src/cli/list-output.js +11 -2
  28. package/dist/src/cli/list-shapes.d.ts +6 -0
  29. package/dist/src/cli/list-shapes.js +7 -1
  30. package/dist/src/cli/output-schemas.d.ts +1 -1
  31. package/dist/src/cli/output-schemas.js +1 -1
  32. package/dist/src/cli/output.js +7 -0
  33. package/dist/src/cli/version.d.ts +2 -2
  34. package/dist/src/cli/version.js +4 -5
  35. package/dist/src/cli.js +66 -17
  36. package/dist/src/client.d.ts +14 -2
  37. package/dist/src/client.js +18 -10
  38. package/dist/src/generated/action-output-schemas.json +18 -0
  39. package/dist/src/index.d.ts +1 -1
  40. package/dist/src/resources/datasets.d.ts +11 -1
  41. package/dist/src/resources/datasets.js +91 -30
  42. package/dist/src/resources/jobs.d.ts +35 -5
  43. package/dist/src/resources/jobs.js +68 -41
  44. package/dist/src/resources/projects.js +2 -2
  45. package/dist/src/resources/recipes.d.ts +12 -1
  46. package/dist/src/resources/recipes.js +39 -12
  47. package/dist/src/resources/scenarios.d.ts +8 -1
  48. package/dist/src/resources/scenarios.js +51 -12
  49. package/dist/src/resources/wiki.js +3 -7
  50. package/dist/src/skill.js +1 -2
  51. package/dist/src/utils/project-archive.d.ts +25 -0
  52. package/dist/src/utils/project-archive.js +216 -5
  53. package/dist/src/utils/response-file.js +15 -8
  54. package/package.json +11 -12
  55. package/plugin.json +1 -1
  56. package/skills/dataiku-dss/SKILL.md +1 -1
package/README.md CHANGED
@@ -16,7 +16,7 @@ Malformed user/group listings and scenario run histories fail explicitly instead
16
16
 
17
17
  ## Platform support
18
18
 
19
- The published `dss` CLI requires Bun >= 1.4.2 and supports Linux, macOS, and Windows. CI runs Bun 1.4.2 on all three operating systems. The runtime dependency is pure JavaScript, so the same package also runs on every Bun-supported x64 and ARM64 system.
19
+ The published `dss` CLI requires Bun >= 1.4.3 and supports Linux, macOS, and Windows. CI runs Bun 1.4.3 on all three operating systems. The runtime dependency is pure JavaScript, so the same package also runs on every Bun-supported x64 and ARM64 system.
20
20
 
21
21
  Run directly with Bun:
22
22
 
@@ -29,7 +29,7 @@ Or install the npm binary:
29
29
  ```text
30
30
  npm install --global dataiku-sdk
31
31
  ```
32
- The installed `dss` bin bootstraps Bun with automatic `.env` preloading disabled, so Bun >= 1.4.2 must be on `PATH`; the CLI's own `.env` handling stays authoritative. `bunx --bun` forces Bun's Node-compatibility mode, which provides the same no-preload guarantee across operating systems.
32
+ The installed `dss` bin bootstraps Bun with automatic `.env` preloading disabled, so Bun >= 1.4.3 must be on `PATH`; the CLI's own `.env` handling stays authoritative. `bunx --bun` forces Bun's Node-compatibility mode, which provides the same no-preload guarantee across operating systems.
33
33
 
34
34
  Bun is the only supported runtime and the package manager. Examples below assume an installed `dss` binary. From a checkout, use `bun --no-env-file src/cli.ts ...` or the packaged launcher `bun --no-env-file ./bin/dss.js ...`. From another working directory, pass the checkout's absolute `bin/dss.js` path to Bun.
35
35
  `--no-env-file` disables Bun's automatic preloading only; the CLI still applies its documented `.env` handling unless `DATAIKU_DISABLE_ENV=1` is set.
@@ -181,7 +181,7 @@ then update `tests/live-coverage.ts` when the
181
181
  registry changes; catalogue drift fails the offline gate. Plan/dry-run calls do not count
182
182
  as executed live coverage.
183
183
 
184
- Regular `bun run check` and `bun run lint` include the live runner and fixtures.
184
+ Regular `bun run verify` and `bun run lint` include the live runner and fixtures.
185
185
  Lint includes project-owned rules in `tools/oxlint/index.ts`: accumulator-copy, widen-then-assert, and chained-assertion errors, alongside the native accumulating-spread check.
186
186
  For focused verification:
187
187
 
@@ -1 +1 @@
1
- {"format":1,"buildRevision":"3f9f0582b0fa45ee3708215954761d75f6edf9b7"}
1
+ {"format":1,"buildRevision":"ab176cb1de853bfbdc58627b38a131ce1fc1681d"}
@@ -284,6 +284,10 @@ export declare const JobWaitResultSchema: import("@sinclair/typebox").TObject<{
284
284
  pollCount: import("@sinclair/typebox").TNumber;
285
285
  success: import("@sinclair/typebox").TBoolean;
286
286
  timedOut: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
287
+ /** Present on a wait timeout: the wait never cancels the job, so this is `false`. */
288
+ cancelled: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
289
+ /** CLI only, on a wait timeout: the command that resumes waiting for the still-running job. */
290
+ resume: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
287
291
  progress: import("@sinclair/typebox").TObject<{
288
292
  done: import("@sinclair/typebox").TNumber;
289
293
  failed: import("@sinclair/typebox").TNumber;
@@ -308,6 +308,10 @@ export const JobWaitResultSchema = Type.Object({
308
308
  pollCount: Type.Number(),
309
309
  success: Type.Boolean(),
310
310
  timedOut: Type.Optional(Type.Boolean()),
311
+ /** Present on a wait timeout: the wait never cancels the job, so this is `false`. */
312
+ cancelled: Type.Optional(Type.Boolean()),
313
+ /** CLI only, on a wait timeout: the command that resumes waiting for the still-running job. */
314
+ resume: Type.Optional(Type.String()),
311
315
  progress: Type.Object({
312
316
  done: Type.Number(),
313
317
  failed: Type.Number(),
@@ -21,6 +21,12 @@ export declare function parseJsonObject(text: string, source: string): Record<st
21
21
  export declare function json(v: string | boolean | undefined, source?: string): Record<string, unknown> | undefined;
22
22
  export declare function readStdinText(): string;
23
23
  export declare function readInputText(path: string, flag: string): string;
24
+ /**
25
+ * Async UTF-8 read of a user-supplied path (BOM kept, like `readFileSync`).
26
+ * Stays on node:fs: Bun 1.4.3 `Bun.file(path)` reads of a missing path ended the
27
+ * process on Windows CI instead of rejecting with ENOENT.
28
+ */
29
+ export declare function readTextFile(path: string): Promise<string>;
24
30
  export declare function seenJsonInputCredentials(): readonly string[];
25
31
  export declare function jsonInput(flags: Record<string, string | boolean>): Record<string, unknown> | undefined;
26
32
  export declare function unknownJsonInput(flags: Record<string, string | boolean>): unknown;
@@ -1,4 +1,5 @@
1
1
  import { readFileSync, } from "node:fs";
2
+ import { readFile, } from "node:fs/promises";
2
3
  import { credentialValues, } from "../utils/secret-sanitize.js";
3
4
  import { UsageError, } from "./usage.js";
4
5
  function safeValuePreview(value) {
@@ -174,6 +175,14 @@ export function readInputText(path, flag) {
174
175
  throw new UsageError(`Could not read ${flag} file: ${path}`, "validation_failed", "Verify that the path names an existing, readable file.", { flag, path, ...(cause ? { cause, } : {}), });
175
176
  }
176
177
  }
178
+ /**
179
+ * Async UTF-8 read of a user-supplied path (BOM kept, like `readFileSync`).
180
+ * Stays on node:fs: Bun 1.4.3 `Bun.file(path)` reads of a missing path ended the
181
+ * process on Windows CI instead of rejecting with ENOENT.
182
+ */
183
+ export async function readTextFile(path) {
184
+ return readFile(path, "utf-8");
185
+ }
177
186
  /**
178
187
  * Credential values seen in this process's JSON inputs. DSS error text can
179
188
  * echo the request payload, so error reports redact these.
@@ -754,7 +754,7 @@ export const appCommands = withUsage("app", {
754
754
  const usage = commandUsage("app", "permissions-diff");
755
755
  requireNoArgs(_a, usage);
756
756
  const file = requiredStringFlag(f, "file", usage);
757
- const snapshot = readAppPermissionsSnapshot(file);
757
+ const snapshot = await readAppPermissionsSnapshot(file);
758
758
  const projectKey = c.resolveProjectKey(f["project-key"]);
759
759
  assertAppPermissionsSnapshotBinding(snapshot, projectKey, c.getBaseUrl());
760
760
  const currentProjectIncarnationHash = await requireProjectIncarnationHash(c, projectKey);
@@ -779,7 +779,7 @@ export const appCommands = withUsage("app", {
779
779
  const usage = commandUsage("app", "permissions-restore");
780
780
  requireNoArgs(_a, usage);
781
781
  const file = requiredStringFlag(f, "file", usage);
782
- const snapshot = readAppPermissionsSnapshot(file);
782
+ const snapshot = await readAppPermissionsSnapshot(file);
783
783
  const projectKey = c.resolveProjectKey(f["project-key"]);
784
784
  assertAppPermissionsSnapshotBinding(snapshot, projectKey, c.getBaseUrl());
785
785
  const currentProjectIncarnationHash = await requireProjectIncarnationHash(c, projectKey);
@@ -1,7 +1,6 @@
1
- import { readFileSync, writeFileSync, } from "node:fs";
2
1
  import { ClientValidationError, } from "../../errors.js";
3
2
  import { SHA256_HEX_PATTERN, stableHash, } from "../../utils/stable-hash.js";
4
- import { json, jsonInput, num, numFlag, parseBooleanOption, } from "../coerce.js";
3
+ import { json, jsonInput, num, numFlag, parseBooleanOption, readTextFile, } from "../coerce.js";
5
4
  import { executionMode, } from "../flags.js";
6
5
  import { enqueueCliWarning, isNotFoundError, readIfExists, skipResult, } from "../output.js";
7
6
  import { commandUsage, withUsage, } from "../syntax.js";
@@ -49,9 +48,9 @@ function splitPackageSpec(raw) {
49
48
  * (set-packages replaces specPackageList wholesale); only a call with no
50
49
  * package source flag at all is a usage error.
51
50
  */
52
- function codeEnvPackageList(flags) {
51
+ async function codeEnvPackageList(flags) {
53
52
  const file = flags["file"];
54
- const packages = typeof file === "string" ? splitPackageSpec(readFileSync(file, "utf-8")) : [];
53
+ const packages = typeof file === "string" ? splitPackageSpec(await readTextFile(file)) : [];
55
54
  if (typeof flags["packages"] === "string") {
56
55
  packages.push(...splitPackageSpec(flags["packages"]));
57
56
  }
@@ -122,7 +121,7 @@ export const codeEnvCommands = withUsage("code-env", {
122
121
  });
123
122
  const output = f["output"];
124
123
  if (typeof output === "string" && output.trim().length > 0) {
125
- writeFileSync(output, result.log, "utf-8");
124
+ await Bun.write(output, result.log);
126
125
  if (result.truncated || result.tailed) {
127
126
  const limits = [
128
127
  result.tailed ? `tail limited to ${maxLines ?? 500} lines` : undefined,
@@ -257,7 +256,7 @@ export const codeEnvCommands = withUsage("code-env", {
257
256
  const usage = commandUsage("code-env", "set-packages");
258
257
  requireArgs(a, 2, usage);
259
258
  const envLang = requireEnvLang(a[0], usage);
260
- const packages = codeEnvPackageList(f);
259
+ const packages = await codeEnvPackageList(f);
261
260
  const installCorePackages = parseBooleanOption(f["install-core-packages"], "--install-core-packages");
262
261
  const expectHash = validateExpectHash(f["expect-hash"]);
263
262
  if (executionMode(f).dryRun) {
@@ -115,19 +115,33 @@ export const codeCommands = withUsage("code", {
115
115
  maxLogBytes: run.maxLogBytes,
116
116
  cleanup: run.cleanup,
117
117
  ...(run.timedOut === true ? { timedOut: true, timeoutMs: run.timeoutMs, } : {}),
118
+ ...(run.outputTruncated === true ? { outputTruncated: true, } : {}),
119
+ ...(run.outputOmittedBytes !== undefined
120
+ ? { outputOmittedBytes: run.outputOmittedBytes, }
121
+ : {}),
118
122
  };
119
123
  if (f["full-log"] === true || run.output === undefined) {
120
124
  result.log = run.log;
121
125
  }
126
+ if (run.outputTruncated === true) {
127
+ enqueueCliWarning({
128
+ code: "code_run_output_truncated",
129
+ hint: run.outputOmittedBytes !== undefined
130
+ ? `Script output exceeded the capture budget; the first ${run.outputOmittedBytes} bytes were dropped and \`output\` holds the last part. Raise --max-log-bytes (the budget is a quarter of it) or print less.`
131
+ : "The run log exceeded --max-log-bytes, so the start of the script output was cut and `output` holds the last part. Raise --max-log-bytes or print less.",
132
+ ...(run.outputOmittedBytes !== undefined ? { omittedBytes: run.outputOmittedBytes, } : {}),
133
+ maxLogBytes: run.maxLogBytes,
134
+ });
135
+ }
122
136
  if (run.cleanup.status === "failed") {
123
137
  reportCleanupFailure({ scenarioId: run.scenarioId, error: run.cleanup.error ?? "unknown", });
124
138
  }
125
139
  return result;
126
140
  },
127
- description: "Run Python through a throwaway custom-python scenario and return captured output plus outcome. --timeout caps waiting; --max-log-bytes caps logs; --full-log returns the capped raw log. Exits 4 unless outcome is SUCCESS.",
141
+ description: "Run Python through a throwaway custom-python scenario and return captured output plus outcome. --timeout caps waiting; --max-log-bytes caps the log tail read (script output keeps its last quarter; a code_run_output_truncated warning reports cuts); --full-log returns the capped raw log. Exits 4 unless outcome is SUCCESS.",
128
142
  examples: [
129
143
  "dss code run --file inspect.py",
130
- "dss code run --file inspect.py --env py39_pandas",
144
+ "dss code run --file inspect.py --env py311_pandas",
131
145
  "cat snippet.py | dss code run --stdin",
132
146
  "dss code run --file inspect.py --full-log",
133
147
  ],
@@ -1,4 +1,3 @@
1
- import * as fs from "node:fs";
2
1
  import { ClientValidationError, } from "../../errors.js";
3
2
  import { deepMerge, } from "../../utils/deep-merge.js";
4
3
  import { jsonInput, parseBooleanOption, } from "../coerce.js";
@@ -73,7 +72,7 @@ export const dashboardCommands = withUsage("dashboard", {
73
72
  if (!isPdf) {
74
73
  throw new ClientValidationError("DSS dashboard export did not return a PDF.", "validation_failed", "Confirm DSS graphics export is configured and retry the export.");
75
74
  }
76
- fs.writeFileSync(outputPath, bytes);
75
+ await Bun.write(outputPath, bytes);
77
76
  return {
78
77
  path: outputPath,
79
78
  bytes: bytes.length,
@@ -275,7 +275,7 @@ export const flowZoneCommands = withUsage("flow-zone", {
275
275
  handler: async (c, _a, f) => {
276
276
  const usage = commandUsage("flow-zone", "organize");
277
277
  const pk = f["project-key"];
278
- const parsed = readFlowZoneOrganizePlan(f, usage);
278
+ const parsed = await readFlowZoneOrganizePlan(f, usage);
279
279
  const sync = f["sync"] === true;
280
280
  const validateObjects = f["validate-objects"] === true;
281
281
  const recreateOnMove = f["recreate-on-move"] === true;
@@ -1,9 +1,10 @@
1
- import { mkdir, writeFile, } from "node:fs/promises";
2
- import { dirname, resolve, } from "node:path";
3
- import { ClientValidationError, DataikuError, } from "../../errors.js";
1
+ import { resolve, } from "node:path";
2
+ import { ClientValidationError, DataikuError, dataikuErrorCode, } from "../../errors.js";
4
3
  import { asRecord, } from "../../utils/records.js";
5
4
  import { jobBuildTargetTypeFromFlags, jobLogFilterFromFlag, maxLogLinesFromFlags, num, } from "../coerce.js";
6
5
  import { executionMode, } from "../flags.js";
6
+ import { compactValue, } from "../list-output.js";
7
+ import { COMPACT_LIST_FIELDS, } from "../list-shapes.js";
7
8
  import { encodedProjectEndpoint, enqueueCliWarning, } from "../output.js";
8
9
  import { commandUsage, withUsage, } from "../syntax.js";
9
10
  import { requireArgs, UsageError, } from "../usage.js";
@@ -33,6 +34,24 @@ function tailJobLogForStdout(log, limit) {
33
34
  const tail = lines.slice(-shown).join("\n");
34
35
  return hasTrailingLineBreak ? `${tail}\n` : tail;
35
36
  }
37
+ const RESPONSE_TOO_LARGE_STATUS_TEXT = "Response Too Large";
38
+ /** POSIX-shell quoting for a resume command argument. */
39
+ function shellArg(value) {
40
+ return /^[\w.:@%+=,/-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
41
+ }
42
+ /** Command that resumes waiting for a job whose wait timed out. */
43
+ function jobWaitResumeCommand(jobId, projectKey) {
44
+ return `dss job wait ${shellArg(jobId)}${projectKey ? ` --project-key ${shellArg(projectKey)}` : ""}`;
45
+ }
46
+ /**
47
+ * A wait that timed out only stopped polling: the job is still running in DSS.
48
+ * Attach the command that resumes waiting; other results pass through.
49
+ */
50
+ function withWaitResume(result, projectKey) {
51
+ if (result.timedOut !== true)
52
+ return result;
53
+ return { ...result, resume: jobWaitResumeCommand(result.jobId, projectKey), };
54
+ }
36
55
  /**
37
56
  * With an empty output schema DSS ends a dataset build DONE after writing no
38
57
  * columns and no rows, without an error. Flag that after a successful build so
@@ -120,8 +139,9 @@ function jobSummaryType(job) {
120
139
  ?? "unknown";
121
140
  }
122
141
  function jobSummaryState(job) {
142
+ const listState = compactValue(job, COMPACT_LIST_FIELDS["job"]["state"]);
123
143
  return stringPath(job, ["baseStatus", "state",])
124
- ?? stringPath(job, ["state",])
144
+ ?? (typeof listState === "string" ? listState : undefined)
125
145
  ?? "unknown";
126
146
  }
127
147
  function filteredJobList(jobs, flags) {
@@ -202,7 +222,19 @@ function jobDurationMs(details) {
202
222
  }
203
223
  async function jobInspectionSummary(client, jobId, flags) {
204
224
  const projectKey = flags["project-key"];
205
- const details = await client.jobs.get(jobId, projectKey);
225
+ // A job-detail body over the buffered response cap must not sink the whole
226
+ // summary: keep the log part and report the details failure structurally.
227
+ let details = {};
228
+ let detailsError;
229
+ try {
230
+ details = await client.jobs.get(jobId, projectKey);
231
+ }
232
+ catch (error) {
233
+ if (!(error instanceof DataikuError && error.statusText === RESPONSE_TOO_LARGE_STATUS_TEXT)) {
234
+ throw error;
235
+ }
236
+ detailsError = { code: dataikuErrorCode(error.category), message: error.message, };
237
+ }
206
238
  let log;
207
239
  let logError;
208
240
  try {
@@ -233,6 +265,7 @@ async function jobInspectionSummary(client, jobId, flags) {
233
265
  jobId: jobSummaryId(details, jobId),
234
266
  state: jobSummaryState(details),
235
267
  type: jobSummaryType(details),
268
+ ...(detailsError ? { detailsError, } : {}),
236
269
  ...(durationMs !== undefined ? { durationMs, } : {}),
237
270
  ...(outputs !== undefined ? { outputs, } : {}),
238
271
  warnings: jobWarningSummary(details, log),
@@ -253,7 +286,7 @@ async function jobInspectionSummary(client, jobId, flags) {
253
286
  export const jobCommands = withUsage("job", {
254
287
  list: {
255
288
  handler: async (c, _a, f) => filteredJobList(await c.jobs.list(f["project-key"]), f),
256
- description: "List recent jobs, optionally filtered for automation.",
289
+ description: "List recent jobs, optionally filtered for automation. In-progress jobs, which DSS lists without a state, show RUNNING; `job get` has the exact phase.",
257
290
  examples: ["dss job list --state DONE --latest", "dss job list --contains WLM225S --limit 10",],
258
291
  },
259
292
  get: {
@@ -282,18 +315,34 @@ export const jobCommands = withUsage("job", {
282
315
  ?? f["output-file"];
283
316
  const requestedLines = maxLogLinesFromFlags(f);
284
317
  // Stdout is tailed here (not in the SDK) so the total can be reported.
285
- const log = await c.jobs.log(a[0], {
318
+ const details = await c.jobs.logDetails(a[0], {
286
319
  activity: f["activity"],
287
320
  logId: f["log-id"],
288
321
  logFilter,
289
322
  maxLogLines: outputFile ? requestedLines : -1,
290
323
  projectKey: f["project-key"],
291
324
  });
325
+ const log = details.text;
326
+ if (details.truncated) {
327
+ enqueueCliWarning({
328
+ code: "log_truncated",
329
+ reason: "byte_cap",
330
+ earlyLinesDropped: true,
331
+ maxLogBytes: details.maxLogBytes,
332
+ hint: "Only the most recent part of the log was retrieved; early lines (and any earlier errors) were dropped. Use `dss job get` for job-level and per-activity errors.",
333
+ });
334
+ }
335
+ if (logFilter === "errors" && log.length === 0) {
336
+ enqueueCliWarning({
337
+ code: "no_error_lines",
338
+ scannedLines: details.scannedLines,
339
+ hint: `No error lines matched in ${details.scannedLines} scanned log lines. Run \`dss job get ${a[0]}\` for the job-level and per-activity errors, or \`dss job log ${a[0]}\` without --errors-only for the full log.`,
340
+ });
341
+ }
292
342
  if (!outputFile)
293
343
  return tailJobLogForStdout(log, requestedLines ?? DEFAULT_STDOUT_LOG_LINES);
294
344
  const outputPath = resolve(outputFile);
295
- await mkdir(dirname(outputPath), { recursive: true, });
296
- await writeFile(outputPath, log.endsWith("\n") ? log : `${log}\n`, "utf-8");
345
+ await Bun.write(outputPath, log.endsWith("\n") ? log : `${log}\n`);
297
346
  return outputPath;
298
347
  },
299
348
  description: "Get public API job log output (last 100 lines by default; --max-lines N, 0 = all); find why a job failed. Use --errors-only for error/traceback lines and --output PATH to write it to a file (stdout returns the path). --log-id is accepted for UI parity; API keys cannot read browser-only cat-activity-log files.",
@@ -349,7 +398,7 @@ export const jobCommands = withUsage("job", {
349
398
  };
350
399
  }
351
400
  if (f["wait"] === true) {
352
- const result = await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, });
401
+ const result = withWaitResume(await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, }), pk);
353
402
  await warnIfBuiltDatasetHasNoColumns(c, a[0], options.targetType, result.success, pk);
354
403
  return result;
355
404
  }
@@ -389,7 +438,7 @@ export const jobCommands = withUsage("job", {
389
438
  method: "POST",
390
439
  };
391
440
  }
392
- const result = await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, });
441
+ const result = withWaitResume(await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, }), pk);
393
442
  await warnIfBuiltDatasetHasNoColumns(c, a[0], options.targetType, result.success, pk);
394
443
  return result;
395
444
  },
@@ -402,17 +451,19 @@ export const jobCommands = withUsage("job", {
402
451
  ],
403
452
  },
404
453
  wait: {
405
- handler: (c, a, f) => {
454
+ handler: async (c, a, f) => {
406
455
  requireArgs(a, 1, commandUsage("job", "wait"));
407
- return c.jobs.wait(a[0], {
456
+ const projectKey = f["project-key"];
457
+ const result = await c.jobs.wait(a[0], {
408
458
  includeLogs: f["include-logs"] === true,
409
459
  logFilter: jobLogFilterFromFlag(f["log-filter"]),
410
460
  maxLogLines: maxLogLinesFromFlags(f),
411
461
  pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
412
462
  timeoutMs: num(f["timeout"], "--timeout"),
413
463
  summary: f["summary"] === true,
414
- projectKey: f["project-key"],
464
+ projectKey,
415
465
  });
466
+ return withWaitResume(result, projectKey);
416
467
  },
417
468
  description: "Wait for an existing job to complete.",
418
469
  examples: [
@@ -432,7 +483,7 @@ export const jobCommands = withUsage("job", {
432
483
  summary: f["summary"] !== false,
433
484
  projectKey: f["project-key"],
434
485
  };
435
- const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
486
+ const jobs = await Promise.all(a.map(async (jobId) => withWaitResume(await c.jobs.wait(jobId, options), options.projectKey)));
436
487
  return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
437
488
  },
438
489
  description: "Monitor one or more existing jobs and summarize progress counters from logs.",
@@ -450,7 +501,7 @@ export const jobCommands = withUsage("job", {
450
501
  summary: true,
451
502
  projectKey: f["project-key"],
452
503
  };
453
- const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
504
+ const jobs = await Promise.all(a.map(async (jobId) => withWaitResume(await c.jobs.wait(jobId, options), options.projectKey)));
454
505
  return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
455
506
  },
456
507
  description: "Watch one or more existing jobs with progress extraction enabled.",
@@ -1,7 +1,7 @@
1
- import * as fs from "node:fs";
1
+ import { readFile, } from "node:fs/promises";
2
2
  import { validatedPluginGitUrl, validatePluginDestinationPath, validatePluginId, validatePluginName, validatePluginPath, } from "../../resources/plugins.js";
3
3
  import { writeResponseToFile, } from "../../utils/response-file.js";
4
- import { num, readStdinText, sha256Hex, } from "../coerce.js";
4
+ import { num, readStdinText, readTextFile, sha256Hex, } from "../coerce.js";
5
5
  import { executionMode, } from "../flags.js";
6
6
  import { planResult, } from "../output.js";
7
7
  import { commandUsage, withUsage, } from "../syntax.js";
@@ -281,7 +281,7 @@ export const pluginCommands = withUsage("plugin", {
281
281
  requireArgs(a, 1, SETTINGS_SET_USAGE);
282
282
  const pluginId = validatePluginId(a[0]);
283
283
  const mode = executionMode(f);
284
- const settings = readSettingsInput(f);
284
+ const settings = await readSettingsInput(f);
285
285
  if (mode.dryRun) {
286
286
  return pluginPlan("settings-set", {
287
287
  method: "POST",
@@ -330,7 +330,7 @@ export const pluginCommands = withUsage("plugin", {
330
330
  },
331
331
  description: "Create the code env of a plugin. Returns the DSS future (jobId); --wait polls it to completion.",
332
332
  examples: [
333
- "dss plugin code-env-create my-plugin --python-interpreter PYTHON36",
333
+ "dss plugin code-env-create my-plugin --python-interpreter PYTHON311",
334
334
  "dss plugin code-env-create my-plugin --conda --wait --timeout 300000",
335
335
  ],
336
336
  },
@@ -629,7 +629,7 @@ export const pluginCommands = withUsage("plugin", {
629
629
  });
630
630
  }
631
631
  const content = typeof f["file"] === "string"
632
- ? fs.readFileSync(f["file"])
632
+ ? await readFile(f["file"])
633
633
  : typeof f["content"] === "string"
634
634
  ? f["content"]
635
635
  : await readStdinText();
@@ -738,10 +738,10 @@ function normalizeCreationMode(value) {
738
738
  }
739
739
  throw new UsageError("--creation-mode must be one of EMPTY, GIT_CLONE, GIT_EXPORT.", "invalid_enum");
740
740
  }
741
- function readSettingsInput(flags) {
741
+ async function readSettingsInput(flags) {
742
742
  let text;
743
743
  if (typeof flags["file"] === "string") {
744
- text = fs.readFileSync(flags["file"], "utf-8");
744
+ text = await readTextFile(flags["file"]);
745
745
  }
746
746
  else if (typeof flags["content"] === "string") {
747
747
  text = flags["content"];
@@ -159,7 +159,7 @@ export const projectLibraryCommands = withUsage("project-library", {
159
159
  const content = typeof f["content"] === "string"
160
160
  ? f["content"]
161
161
  : typeof f["file"] === "string"
162
- ? fs.readFileSync(f["file"])
162
+ ? await fs.promises.readFile(f["file"])
163
163
  : await readStdinText();
164
164
  const result = await c.projectLibrary.addOrUpdateFile(a[0], content, pk, expectSha256 !== undefined ? { expectSha256, } : undefined);
165
165
  return {
@@ -184,7 +184,7 @@ export const projectLibraryCommands = withUsage("project-library", {
184
184
  const local = typeof f["content"] === "string"
185
185
  ? f["content"]
186
186
  : typeof f["file"] === "string"
187
- ? fs.readFileSync(f["file"])
187
+ ? await fs.promises.readFile(f["file"])
188
188
  : await readStdinText();
189
189
  return c.projectLibrary.diffFile(a[0], local, pk, maxLines !== undefined ? { maxLines, } : {});
190
190
  },
@@ -4,11 +4,13 @@ import { inspectProjectArchive, } from "../../utils/project-archive.js";
4
4
  import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
5
5
  import { jsonInput, num, requiredJsonInput, requiredStringFlag, } from "../coerce.js";
6
6
  import { executionMode, } from "../flags.js";
7
- import { CommandResultFailure, } from "../output.js";
7
+ import { CommandResultFailure, enqueueCliWarning, } from "../output.js";
8
8
  import { compactProjectMapOutput, } from "../project-map-output.js";
9
9
  import { commandUsage, withUsage, } from "../syntax.js";
10
10
  import { requireArgs, UsageError, } from "../usage.js";
11
11
  const INSPECT_ARCHIVE_USAGE = commandUsage("project", "inspect-archive");
12
+ /** Maximum credential inventory entries echoed in the export warning. */
13
+ const EXPORT_CREDENTIAL_INVENTORY_LIMIT = 100;
12
14
  function flowRenderFormat(value) {
13
15
  if (value === undefined)
14
16
  return undefined;
@@ -162,9 +164,24 @@ export const projectCommands = withUsage("project", {
162
164
  if (!out)
163
165
  throw new UsageError("--output PATH is required.", "missing_required_flag");
164
166
  await c.projects.exportArchive(a[0], out, jsonInput(f));
165
- return { path: out, };
167
+ const inspection = await inspectProjectArchive(out);
168
+ const findings = inspection.credentialFindings;
169
+ if (findings.length > 0) {
170
+ enqueueCliWarning({
171
+ code: "export_contains_credentials",
172
+ message: `The exported archive '${out}' contains ${findings.length} credential-bearing structure(s) `
173
+ + "(member paths and key names only; values are never printed). "
174
+ + "DSS exports credentials, API keys and secret-named variables verbatim.",
175
+ hint: "Sanitize the archive before sharing or uploading it. Overwriting an object in versioned storage does not erase its retained versions: "
176
+ + "once an unsanitized archive is uploaded, rotate the exposed credentials.",
177
+ total: findings.length,
178
+ shown: Math.min(findings.length, EXPORT_CREDENTIAL_INVENTORY_LIMIT),
179
+ findings: findings.slice(0, EXPORT_CREDENTIAL_INVENTORY_LIMIT),
180
+ });
181
+ }
182
+ return { path: out, credentialFindings: findings.length, };
166
183
  },
167
- description: "Export a project to a local archive (.zip).",
184
+ description: "Export a project to a local archive (.zip). The written archive is inspected locally; credential-bearing structures (apikeys.json, secret-named variables, apiKey/token/password fields) raise an export_contains_credentials warning listing member paths and key names only, and the result reports credentialFindings. The archive is not redacted.",
168
185
  examples: ["dss project export MY_PROJ --output ./my_proj.zip",],
169
186
  },
170
187
  import: {
@@ -202,7 +219,7 @@ export const projectCommands = withUsage("project", {
202
219
  "inspect-archive": {
203
220
  handler: async (_c, a) => inspectArchiveCommand(a),
204
221
  localHandler: inspectArchiveCommand,
205
- description: "Read-only local inspection of a project export archive: member names, sizes, manifest validity, archive issues, and the source project key. Never contacts DSS.",
222
+ description: "Read-only local inspection of a project export archive: member names, sizes, manifest validity, archive issues (including credential_structure and residue_scan_skipped warnings), a credentialFindings inventory of credential-bearing member paths and key names (values are never reported), and the source project key. Never contacts DSS.",
206
223
  examples: ["dss project inspect-archive ./my_proj.zip",],
207
224
  },
208
225
  "permissions-set": {
@@ -1,10 +1,8 @@
1
- import { readFileSync, } from "node:fs";
2
- import { writeFile, } from "node:fs/promises";
3
1
  import { join, } from "node:path";
4
2
  import { ClientValidationError, DataikuError, } from "../../errors.js";
5
3
  import { recipeCreateWarnings, recipeOutputSchemaIsComputable, schemaUpdateComputableId, schemaUpdateIsPending, } from "../../resources/recipe-create.js";
6
- import { deepMerge, } from "../../utils/deep-merge.js";
7
- import { jobLogFilterFromFlag, jsonInput, maxLogLinesFromFlags, normalizeLineEndings, num, recipeInputDatasetsFromFlags, requiredJsonInput, requiredStringFlag, rewritePairsFromFlags, sha256Hex, splitCsvFlag, stableHash, } from "../coerce.js";
4
+ import { buildRecipeUpdate, } from "../../resources/recipes.js";
5
+ import { jobLogFilterFromFlag, jsonInput, maxLogLinesFromFlags, normalizeLineEndings, num, readTextFile, recipeInputDatasetsFromFlags, requiredJsonInput, requiredStringFlag, rewritePairsFromFlags, sha256Hex, splitCsvFlag, stableHash, } from "../coerce.js";
8
6
  import { executionMode, } from "../flags.js";
9
7
  import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
10
8
  import { ensureRecipeBackupDir, readRecipeBackup, recipeBackupDocument, recipeBackupPath, recipeCodeEnv, recipeGraph, recipeInputItemRef, recipeRoleInputItems, recipeRunShouldWait, writeRecipeBackup, } from "../helpers/recipe.js";
@@ -12,9 +10,9 @@ import { encodedProjectEndpoint, enqueueCliWarning, readIfExists, skipResult, }
12
10
  import { commandUsage, withUsage, } from "../syntax.js";
13
11
  import { requireArgs, UsageError, } from "../usage.js";
14
12
  /** Read a local recipe file before any DSS request, mapping read failures to usage validation. */
15
- function readRecipeFile(filePath, flagName) {
13
+ async function readRecipeFile(filePath, flagName) {
16
14
  try {
17
- return readFileSync(filePath, "utf-8");
15
+ return await readTextFile(filePath);
18
16
  }
19
17
  catch (error) {
20
18
  const reason = error instanceof Error ? error.message : String(error);
@@ -507,7 +505,7 @@ export const recipeCommands = withUsage("recipe", {
507
505
  throw new UsageError(`--file is required. Usage: ${commandUsage("recipe", "diff")}`);
508
506
  }
509
507
  // Read and validate the local file before any DSS request.
510
- const localContent = readRecipeFile(filePath, "--file");
508
+ const localContent = await readRecipeFile(filePath, "--file");
511
509
  const result = await c.recipes.get(a[0], {
512
510
  includePayload: true,
513
511
  projectKey: f["project-key"],
@@ -530,23 +528,17 @@ export const recipeCommands = withUsage("recipe", {
530
528
  const pk = f["project-key"];
531
529
  if (executionMode(f).dryRun) {
532
530
  const current = await c.recipes.get(a[0], { projectKey: pk, includePayload: true, });
533
- const currentRecipe = current.recipe;
534
- const next = {
535
- ...current,
536
- ...data,
537
- recipe: deepMerge(currentRecipe, (data.recipe && typeof data.recipe === "object" && !Array.isArray(data.recipe))
538
- ? data.recipe
539
- : {}),
540
- };
531
+ const next = buildRecipeUpdate(current, data);
541
532
  return { dryRun: true, action: "update", resource: "recipe", name: a[0], current, next, };
542
533
  }
543
534
  await c.recipes.update(a[0], data, pk);
544
535
  return { updated: a[0], resource: "recipe", };
545
536
  },
546
- description: "Update recipe settings via JSON merge. Recipe definition fields must be nested under a top-level recipe key.",
537
+ description: "Update recipe settings via JSON merge. Recipe definition fields must be nested under a top-level recipe key. recipe.params.envSelection, when present, replaces the existing envSelection object (include envMode; no stale envName is kept) and is only accepted under recipe.params.",
547
538
  examples: [
548
539
  "dss recipe update compute_orders --data-file settings.json --dry-run",
549
- 'dss recipe update compute_orders --data \'{"recipe":{"params":{"envSelection":{"envMode":"EXPLICIT_ENV","envName":"python39"}}}}\'',
540
+ 'dss recipe update compute_orders --data \'{"recipe":{"params":{"envSelection":{"envMode":"EXPLICIT_ENV","envName":"py311_env"}}}}\'',
541
+ 'dss recipe update compute_orders --data \'{"recipe":{"params":{"envSelection":{"envMode":"INHERIT"}}}}\'',
550
542
  "cat settings.json | dss recipe update compute_orders --stdin",
551
543
  ],
552
544
  },
@@ -636,7 +628,7 @@ export const recipeCommands = withUsage("recipe", {
636
628
  projectKey: f["project-key"],
637
629
  });
638
630
  if (typeof f["output"] === "string") {
639
- await writeFile(f["output"], payload, "utf-8");
631
+ await Bun.write(f["output"], payload);
640
632
  return f["output"];
641
633
  }
642
634
  return payload;
@@ -663,7 +655,7 @@ export const recipeCommands = withUsage("recipe", {
663
655
  const filePath = f["file"];
664
656
  if (!filePath)
665
657
  throw new UsageError("--file is required.");
666
- const content = readRecipeFile(filePath, "--file");
658
+ const content = await readRecipeFile(filePath, "--file");
667
659
  const pk = f["project-key"];
668
660
  const shouldBackup = f["no-backup"] !== true;
669
661
  const backupDir = shouldBackup
@@ -233,11 +233,9 @@ export const scenarioCommands = withUsage("scenario", {
233
233
  const outputFile = f["output"]
234
234
  ?? f["output-file"];
235
235
  if (outputFile) {
236
- const { mkdir, writeFile, } = await import("node:fs/promises");
237
- const { dirname, resolve, } = await import("node:path");
236
+ const { resolve, } = await import("node:path");
238
237
  const outputPath = resolve(outputFile);
239
- await mkdir(dirname(outputPath), { recursive: true, });
240
- await writeFile(outputPath, result.text, "utf-8");
238
+ await Bun.write(outputPath, result.text);
241
239
  return { path: outputPath, truncated: result.truncated, };
242
240
  }
243
241
  return result.text;