dataiku-sdk 3.9.0 → 4.1.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 (74) hide show
  1. package/README.md +4 -3
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +6 -0
  4. package/dist/packages/types/src/index.js +6 -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/command-syntax.json +216 -0
  8. package/dist/src/cli/commands/app.js +2 -2
  9. package/dist/src/cli/commands/code-env.js +10 -9
  10. package/dist/src/cli/commands/code.js +16 -2
  11. package/dist/src/cli/commands/dashboard.js +1 -2
  12. package/dist/src/cli/commands/dataset.js +6 -4
  13. package/dist/src/cli/commands/flow-zone.js +1 -1
  14. package/dist/src/cli/commands/job.js +70 -16
  15. package/dist/src/cli/commands/plugin.js +7 -7
  16. package/dist/src/cli/commands/project-library.js +2 -2
  17. package/dist/src/cli/commands/project.js +21 -4
  18. package/dist/src/cli/commands/recipe.js +34 -21
  19. package/dist/src/cli/commands/scenario.js +78 -12
  20. package/dist/src/cli/contract.js +30 -7
  21. package/dist/src/cli/env.d.ts +6 -0
  22. package/dist/src/cli/env.js +10 -3
  23. package/dist/src/cli/helpers/app-permissions.d.ts +1 -1
  24. package/dist/src/cli/helpers/app-permissions.js +2 -2
  25. package/dist/src/cli/helpers/cleanup.d.ts +2 -0
  26. package/dist/src/cli/helpers/cleanup.js +122 -8
  27. package/dist/src/cli/helpers/flow-zone.d.ts +1 -1
  28. package/dist/src/cli/helpers/flow-zone.js +3 -4
  29. package/dist/src/cli/list-output.d.ts +5 -0
  30. package/dist/src/cli/list-output.js +11 -2
  31. package/dist/src/cli/list-shapes.d.ts +6 -0
  32. package/dist/src/cli/list-shapes.js +7 -1
  33. package/dist/src/cli/output-schemas.d.ts +1 -1
  34. package/dist/src/cli/output-schemas.js +1 -1
  35. package/dist/src/cli/output.d.ts +2 -0
  36. package/dist/src/cli/output.js +31 -4
  37. package/dist/src/cli/read-shapes.js +90 -0
  38. package/dist/src/cli/result-shapes.d.ts +19 -1
  39. package/dist/src/cli/result-shapes.js +227 -4
  40. package/dist/src/cli/selector-suggestions.d.ts +14 -0
  41. package/dist/src/cli/selector-suggestions.js +56 -0
  42. package/dist/src/cli/usage.d.ts +1 -1
  43. package/dist/src/cli/usage.js +29 -4
  44. package/dist/src/cli/version.d.ts +2 -2
  45. package/dist/src/cli/version.js +4 -5
  46. package/dist/src/cli.js +128 -30
  47. package/dist/src/client.d.ts +15 -2
  48. package/dist/src/client.js +36 -11
  49. package/dist/src/errors.d.ts +2 -0
  50. package/dist/src/errors.js +12 -2
  51. package/dist/src/generated/action-output-schemas.json +232 -0
  52. package/dist/src/index.d.ts +1 -1
  53. package/dist/src/resources/datasets.d.ts +11 -1
  54. package/dist/src/resources/datasets.js +91 -30
  55. package/dist/src/resources/jobs.d.ts +35 -5
  56. package/dist/src/resources/jobs.js +68 -41
  57. package/dist/src/resources/projects.js +2 -2
  58. package/dist/src/resources/recipe-create.d.ts +4 -1
  59. package/dist/src/resources/recipe-create.js +11 -2
  60. package/dist/src/resources/recipes.d.ts +12 -1
  61. package/dist/src/resources/recipes.js +39 -12
  62. package/dist/src/resources/scenarios.d.ts +8 -1
  63. package/dist/src/resources/scenarios.js +51 -12
  64. package/dist/src/resources/sql.js +3 -3
  65. package/dist/src/resources/wiki.js +3 -7
  66. package/dist/src/skill.js +1 -2
  67. package/dist/src/utils/project-archive.d.ts +25 -0
  68. package/dist/src/utils/project-archive.js +216 -5
  69. package/dist/src/utils/response-file.js +15 -8
  70. package/package.json +12 -12
  71. package/plugin.json +1 -1
  72. package/skills/dataiku-dss/SKILL.md +15 -17
  73. package/skills/dataiku-dss/references/discovery.md +2 -2
  74. package/skills/dataiku-dss/references/troubleshooting.md +1 -0
@@ -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, RESPONSE_TOO_LARGE_STATUS_TEXT, } 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,27 @@ function tailJobLogForStdout(log, limit) {
33
34
  const tail = lines.slice(-shown).join("\n");
34
35
  return hasTrailingLineBreak ? `${tail}\n` : tail;
35
36
  }
37
+ /** POSIX-shell quoting for the display form of a resume command. */
38
+ function shellArg(value) {
39
+ return /^[\w.:@%+=,/-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
40
+ }
41
+ /**
42
+ * A wait that timed out only stopped polling: the job is still running in DSS.
43
+ * Attach the command that resumes waiting: `resumeArgv` (run it without a
44
+ * shell, on any OS) and `resume` (POSIX display form); other results pass through.
45
+ */
46
+ function withWaitResume(result, projectKey) {
47
+ if (result.timedOut !== true)
48
+ return result;
49
+ const resumeArgv = [
50
+ "dss",
51
+ "job",
52
+ "wait",
53
+ result.jobId,
54
+ ...(projectKey ? ["--project-key", projectKey,] : []),
55
+ ];
56
+ return { ...result, resume: resumeArgv.map(shellArg).join(" "), resumeArgv, };
57
+ }
36
58
  /**
37
59
  * With an empty output schema DSS ends a dataset build DONE after writing no
38
60
  * columns and no rows, without an error. Flag that after a successful build so
@@ -120,8 +142,9 @@ function jobSummaryType(job) {
120
142
  ?? "unknown";
121
143
  }
122
144
  function jobSummaryState(job) {
145
+ const listState = compactValue(job, COMPACT_LIST_FIELDS["job"]["state"]);
123
146
  return stringPath(job, ["baseStatus", "state",])
124
- ?? stringPath(job, ["state",])
147
+ ?? (typeof listState === "string" ? listState : undefined)
125
148
  ?? "unknown";
126
149
  }
127
150
  function filteredJobList(jobs, flags) {
@@ -202,7 +225,19 @@ function jobDurationMs(details) {
202
225
  }
203
226
  async function jobInspectionSummary(client, jobId, flags) {
204
227
  const projectKey = flags["project-key"];
205
- const details = await client.jobs.get(jobId, projectKey);
228
+ // A job-detail body over the buffered response cap must not sink the whole
229
+ // summary: keep the log part and report the details failure structurally.
230
+ let details = {};
231
+ let detailsError;
232
+ try {
233
+ details = await client.jobs.get(jobId, projectKey);
234
+ }
235
+ catch (error) {
236
+ if (!(error instanceof DataikuError && error.statusText === RESPONSE_TOO_LARGE_STATUS_TEXT)) {
237
+ throw error;
238
+ }
239
+ detailsError = { code: dataikuErrorCode(error.category), message: error.message, };
240
+ }
206
241
  let log;
207
242
  let logError;
208
243
  try {
@@ -233,6 +268,7 @@ async function jobInspectionSummary(client, jobId, flags) {
233
268
  jobId: jobSummaryId(details, jobId),
234
269
  state: jobSummaryState(details),
235
270
  type: jobSummaryType(details),
271
+ ...(detailsError ? { detailsError, } : {}),
236
272
  ...(durationMs !== undefined ? { durationMs, } : {}),
237
273
  ...(outputs !== undefined ? { outputs, } : {}),
238
274
  warnings: jobWarningSummary(details, log),
@@ -253,7 +289,7 @@ async function jobInspectionSummary(client, jobId, flags) {
253
289
  export const jobCommands = withUsage("job", {
254
290
  list: {
255
291
  handler: async (c, _a, f) => filteredJobList(await c.jobs.list(f["project-key"]), f),
256
- description: "List recent jobs, optionally filtered for automation.",
292
+ 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
293
  examples: ["dss job list --state DONE --latest", "dss job list --contains WLM225S --limit 10",],
258
294
  },
259
295
  get: {
@@ -282,18 +318,34 @@ export const jobCommands = withUsage("job", {
282
318
  ?? f["output-file"];
283
319
  const requestedLines = maxLogLinesFromFlags(f);
284
320
  // Stdout is tailed here (not in the SDK) so the total can be reported.
285
- const log = await c.jobs.log(a[0], {
321
+ const details = await c.jobs.logDetails(a[0], {
286
322
  activity: f["activity"],
287
323
  logId: f["log-id"],
288
324
  logFilter,
289
325
  maxLogLines: outputFile ? requestedLines : -1,
290
326
  projectKey: f["project-key"],
291
327
  });
328
+ const log = details.text;
329
+ if (details.truncated) {
330
+ enqueueCliWarning({
331
+ code: "log_truncated",
332
+ reason: "byte_cap",
333
+ earlyLinesDropped: true,
334
+ maxLogBytes: details.maxLogBytes,
335
+ 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.",
336
+ });
337
+ }
338
+ if (logFilter === "errors" && log.length === 0) {
339
+ enqueueCliWarning({
340
+ code: "no_error_lines",
341
+ scannedLines: details.scannedLines,
342
+ 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.`,
343
+ });
344
+ }
292
345
  if (!outputFile)
293
346
  return tailJobLogForStdout(log, requestedLines ?? DEFAULT_STDOUT_LOG_LINES);
294
347
  const outputPath = resolve(outputFile);
295
- await mkdir(dirname(outputPath), { recursive: true, });
296
- await writeFile(outputPath, log.endsWith("\n") ? log : `${log}\n`, "utf-8");
348
+ await Bun.write(outputPath, log.endsWith("\n") ? log : `${log}\n`);
297
349
  return outputPath;
298
350
  },
299
351
  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 +401,7 @@ export const jobCommands = withUsage("job", {
349
401
  };
350
402
  }
351
403
  if (f["wait"] === true) {
352
- const result = await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, });
404
+ const result = withWaitResume(await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, }), pk);
353
405
  await warnIfBuiltDatasetHasNoColumns(c, a[0], options.targetType, result.success, pk);
354
406
  return result;
355
407
  }
@@ -389,7 +441,7 @@ export const jobCommands = withUsage("job", {
389
441
  method: "POST",
390
442
  };
391
443
  }
392
- const result = await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, });
444
+ const result = withWaitResume(await c.jobs.buildAndWait(a[0], { ...options, projectKey: pk, }), pk);
393
445
  await warnIfBuiltDatasetHasNoColumns(c, a[0], options.targetType, result.success, pk);
394
446
  return result;
395
447
  },
@@ -402,17 +454,19 @@ export const jobCommands = withUsage("job", {
402
454
  ],
403
455
  },
404
456
  wait: {
405
- handler: (c, a, f) => {
457
+ handler: async (c, a, f) => {
406
458
  requireArgs(a, 1, commandUsage("job", "wait"));
407
- return c.jobs.wait(a[0], {
459
+ const projectKey = f["project-key"];
460
+ const result = await c.jobs.wait(a[0], {
408
461
  includeLogs: f["include-logs"] === true,
409
462
  logFilter: jobLogFilterFromFlag(f["log-filter"]),
410
463
  maxLogLines: maxLogLinesFromFlags(f),
411
464
  pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
412
465
  timeoutMs: num(f["timeout"], "--timeout"),
413
466
  summary: f["summary"] === true,
414
- projectKey: f["project-key"],
467
+ projectKey,
415
468
  });
469
+ return withWaitResume(result, projectKey);
416
470
  },
417
471
  description: "Wait for an existing job to complete.",
418
472
  examples: [
@@ -432,7 +486,7 @@ export const jobCommands = withUsage("job", {
432
486
  summary: f["summary"] !== false,
433
487
  projectKey: f["project-key"],
434
488
  };
435
- const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
489
+ const jobs = await Promise.all(a.map(async (jobId) => withWaitResume(await c.jobs.wait(jobId, options), options.projectKey)));
436
490
  return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
437
491
  },
438
492
  description: "Monitor one or more existing jobs and summarize progress counters from logs.",
@@ -450,7 +504,7 @@ export const jobCommands = withUsage("job", {
450
504
  summary: true,
451
505
  projectKey: f["project-key"],
452
506
  };
453
- const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
507
+ const jobs = await Promise.all(a.map(async (jobId) => withWaitResume(await c.jobs.wait(jobId, options), options.projectKey)));
454
508
  return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
455
509
  },
456
510
  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);
@@ -414,9 +412,30 @@ export const recipeCommands = withUsage("recipe", {
414
412
  hint: `Fix the recipe settings, then run dss recipe update-schema ${createdName}.`,
415
413
  });
416
414
  }
417
- return { created: createdName, resource: "recipe", ...created, ...moved, };
415
+ // A visual recipe whose payload came from flags is ready to run: point at the build
416
+ // (recursive, so outputs of recipes created just before are built too).
417
+ const output = f["output"];
418
+ const next = typeof output === "string" && (created.payloadConfigured || created.joinConfigured)
419
+ ? [[
420
+ "dss",
421
+ "job",
422
+ "build-and-wait",
423
+ output,
424
+ "--build-mode",
425
+ "RECURSIVE_BUILD",
426
+ "--summary",
427
+ ...(pk ? ["--project-key", pk,] : []),
428
+ ],]
429
+ : undefined;
430
+ return {
431
+ created: createdName,
432
+ resource: "recipe",
433
+ ...created,
434
+ ...moved,
435
+ ...(next ? { next, } : {}),
436
+ };
418
437
  },
419
- description: "Create a recipe with optional inputs and a dataset or managed-folder output. Missing outputs are created on --output-connection as DSS managed datasets. Join, grouping (--group-by/--aggregate) and prepare (--rename/--formula/--filter/...) payloads come from flags. Visual recipe outputs get the DSS-computed schema (outputSchemaUpdated).",
438
+ description: "Create a recipe; missing outputs become managed datasets on --output-connection. Join, grouping (--group-by/--aggregate: keys plus those aggregates, no implicit count) and prepare (--rename/--formula/--filter/...) payloads come from flags, then `next` is the build. Visual outputs get the DSS-computed schema.",
420
439
  examples: [
421
440
  "dss recipe create --type python --input raw_orders,lookup --output orders_clean",
422
441
  "dss recipe create --type grouping --input orders --output orders_by_cat --group-by category --aggregate amount:sum+avg,name:first --order-by day",
@@ -507,7 +526,7 @@ export const recipeCommands = withUsage("recipe", {
507
526
  throw new UsageError(`--file is required. Usage: ${commandUsage("recipe", "diff")}`);
508
527
  }
509
528
  // Read and validate the local file before any DSS request.
510
- const localContent = readRecipeFile(filePath, "--file");
529
+ const localContent = await readRecipeFile(filePath, "--file");
511
530
  const result = await c.recipes.get(a[0], {
512
531
  includePayload: true,
513
532
  projectKey: f["project-key"],
@@ -530,23 +549,17 @@ export const recipeCommands = withUsage("recipe", {
530
549
  const pk = f["project-key"];
531
550
  if (executionMode(f).dryRun) {
532
551
  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
- };
552
+ const next = buildRecipeUpdate(current, data);
541
553
  return { dryRun: true, action: "update", resource: "recipe", name: a[0], current, next, };
542
554
  }
543
555
  await c.recipes.update(a[0], data, pk);
544
556
  return { updated: a[0], resource: "recipe", };
545
557
  },
546
- description: "Update recipe settings via JSON merge. Recipe definition fields must be nested under a top-level recipe key.",
558
+ 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
559
  examples: [
548
560
  "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"}}}}\'',
561
+ 'dss recipe update compute_orders --data \'{"recipe":{"params":{"envSelection":{"envMode":"EXPLICIT_ENV","envName":"py311_env"}}}}\'',
562
+ 'dss recipe update compute_orders --data \'{"recipe":{"params":{"envSelection":{"envMode":"INHERIT"}}}}\'',
550
563
  "cat settings.json | dss recipe update compute_orders --stdin",
551
564
  ],
552
565
  },
@@ -636,7 +649,7 @@ export const recipeCommands = withUsage("recipe", {
636
649
  projectKey: f["project-key"],
637
650
  });
638
651
  if (typeof f["output"] === "string") {
639
- await writeFile(f["output"], payload, "utf-8");
652
+ await Bun.write(f["output"], payload);
640
653
  return f["output"];
641
654
  }
642
655
  return payload;
@@ -663,7 +676,7 @@ export const recipeCommands = withUsage("recipe", {
663
676
  const filePath = f["file"];
664
677
  if (!filePath)
665
678
  throw new UsageError("--file is required.");
666
- const content = readRecipeFile(filePath, "--file");
679
+ const content = await readRecipeFile(filePath, "--file");
667
680
  const pk = f["project-key"];
668
681
  const shouldBackup = f["no-backup"] !== true;
669
682
  const backupDir = shouldBackup
@@ -1,8 +1,69 @@
1
+ import { asRecord, } from "../../utils/records.js";
1
2
  import { jsonInput, num, parseBooleanOption, } from "../coerce.js";
2
3
  import { executionMode, } from "../flags.js";
3
4
  import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
5
+ import { scenarioRunDiagnosis, shapeCommandResult, } from "../result-shapes.js";
4
6
  import { commandUsage, withUsage, } from "../syntax.js";
5
7
  import { requireArgs, UsageError, } from "../usage.js";
8
+ /**
9
+ * The recipe a failed job was running and the datasets it read, from one
10
+ * `job get` (its FAILED activity). Undefined when the job cannot be read or
11
+ * names no failed recipe: the diagnosis then stays job-level.
12
+ */
13
+ async function failedActivity(c, jobId, projectKey) {
14
+ if (typeof jobId !== "string")
15
+ return undefined;
16
+ try {
17
+ const job = asRecord(await c.jobs.get(jobId, projectKey));
18
+ const activities = asRecord(asRecord(job?.["baseStatus"])?.["activities"]) ?? {};
19
+ for (const value of Object.values(activities)) {
20
+ const activity = asRecord(value);
21
+ if (activity?.["state"] !== "FAILED" || typeof activity["recipeName"] !== "string")
22
+ continue;
23
+ const sources = Array.isArray(activity["sources"]) ? activity["sources"] : [];
24
+ const inputs = sources.flatMap((source) => {
25
+ const id = asRecord(source)?.["id"];
26
+ if (typeof id !== "string")
27
+ return [];
28
+ // Same-project inputs are named bare; foreign ones keep PROJECT.name.
29
+ const dot = id.indexOf(".");
30
+ return [dot > 0 && id.slice(0, dot) === projectKey ? id.slice(dot + 1) : id,];
31
+ });
32
+ return { recipe: activity["recipeName"], inputs, };
33
+ }
34
+ }
35
+ catch {
36
+ // A diagnosis aid only: the run report itself is already in hand.
37
+ }
38
+ return undefined;
39
+ }
40
+ /** Diagnosis of a raw run report, located at the failed recipe when its job names one. */
41
+ async function locatedDiagnosis(c, details, projectKey) {
42
+ const plain = scenarioRunDiagnosis(details);
43
+ const runProject = asRecord(asRecord(asRecord(details)?.["scenarioRun"])?.["trigger"])?.["projectKey"];
44
+ const pk = projectKey ?? (typeof runProject === "string" ? runProject : undefined);
45
+ const activity = await failedActivity(c, plain.failure?.["jobId"], pk);
46
+ return activity ? scenarioRunDiagnosis(details, activity) : plain;
47
+ }
48
+ /**
49
+ * Run a scenario and wait; a failed (not timed-out) run gains the first
50
+ * failure, the recipe that failed, and the next commands. Extra reads happen
51
+ * only on failure; a read error leaves the wait result untouched.
52
+ */
53
+ async function runAndWaitWithFailure(c, scenarioId, options) {
54
+ const result = await c.scenarios.runAndWait(scenarioId, options);
55
+ if (result.success || result.timedOut === true || !result.runId)
56
+ return result;
57
+ try {
58
+ const details = await c.scenarios.getRunDetails(scenarioId, result.runId, {
59
+ projectKey: options.projectKey,
60
+ });
61
+ return { ...result, ...await locatedDiagnosis(c, details, options.projectKey), };
62
+ }
63
+ catch {
64
+ return result;
65
+ }
66
+ }
6
67
  export const scenarioCommands = withUsage("scenario", {
7
68
  list: {
8
69
  handler: (c, _a, f) => c.scenarios.list(f["project-key"]),
@@ -37,7 +98,7 @@ export const scenarioCommands = withUsage("scenario", {
37
98
  };
38
99
  }
39
100
  if (f["wait"] === true) {
40
- return c.scenarios.runAndWait(a[0], { ...options, projectKey: pk, });
101
+ return runAndWaitWithFailure(c, a[0], { ...options, projectKey: pk, });
41
102
  }
42
103
  return c.scenarios.run(a[0], pk);
43
104
  },
@@ -63,7 +124,7 @@ export const scenarioCommands = withUsage("scenario", {
63
124
  method: "POST",
64
125
  };
65
126
  }
66
- return c.scenarios.runAndWait(a[0], { ...options, projectKey: pk, });
127
+ return runAndWaitWithFailure(c, a[0], { ...options, projectKey: pk, });
67
128
  },
68
129
  description: "Run a scenario and wait for completion.",
69
130
  examples: [
@@ -209,17 +270,24 @@ export const scenarioCommands = withUsage("scenario", {
209
270
  projectKey: f["project-key"],
210
271
  });
211
272
  },
212
- description: "List the most recent runs of a scenario (run id, start/end, outcome).",
273
+ description: "List the most recent runs of a scenario (compact by default: run id, outcome, start/end, trigger, failure message; --full returns the DSS run objects).",
213
274
  examples: ["dss scenario last-runs my_scenario --limit 10",],
214
275
  },
215
276
  "get-run": {
216
- handler: (c, a, f) => {
277
+ handler: async (c, a, f) => {
217
278
  requireArgs(a, 2, commandUsage("scenario", "get-run"));
218
- return c.scenarios.getRunDetails(a[0], a[1], {
219
- projectKey: f["project-key"],
220
- });
279
+ const projectKey = f["project-key"];
280
+ const details = await c.scenarios.getRunDetails(a[0], a[1], { projectKey, });
281
+ if (f["full"] === true || typeof f["fields"] === "string")
282
+ return details;
283
+ // Compact view, located at the failed recipe (one extra job read, only on failure).
284
+ // The shaped object has no `scenarioRun`, so the dispatcher's shaping passes it through.
285
+ const shaped = asRecord(shapeCommandResult("scenario", "get-run", details, f));
286
+ if (!shaped?.["failure"])
287
+ return shaped ?? details;
288
+ return { ...shaped, ...await locatedDiagnosis(c, details, projectKey), };
221
289
  },
222
- description: "Get the details of a specific scenario run, including per-step outcomes.",
290
+ description: "Get a scenario run: outcome, timing, trigger, per-step datasets and jobs, plus `failure` {stepId, jobId, dataset, recipe, inputs, message} and `next` argv (recipe code, input schemas, job errors) for the first failure (compact by default; --full returns the raw DSS run report).",
223
291
  examples: ["dss scenario get-run my_scenario 2024-09-01-01-02-03-123",],
224
292
  },
225
293
  log: {
@@ -233,11 +301,9 @@ export const scenarioCommands = withUsage("scenario", {
233
301
  const outputFile = f["output"]
234
302
  ?? f["output-file"];
235
303
  if (outputFile) {
236
- const { mkdir, writeFile, } = await import("node:fs/promises");
237
- const { dirname, resolve, } = await import("node:path");
304
+ const { resolve, } = await import("node:path");
238
305
  const outputPath = resolve(outputFile);
239
- await mkdir(dirname(outputPath), { recursive: true, });
240
- await writeFile(outputPath, result.text, "utf-8");
306
+ await Bun.write(outputPath, result.text);
241
307
  return { path: outputPath, truncated: result.truncated, };
242
308
  }
243
309
  return result.text;
@@ -248,6 +248,13 @@ const ALLOWED_CLEANUP_ACTIONS = new Set([
248
248
  "project-library delete",
249
249
  "streaming-endpoint delete",
250
250
  "workspace delete",
251
+ "saved-model delete",
252
+ "project-folder delete",
253
+ "data-collection delete",
254
+ "plugin delete",
255
+ "connection delete",
256
+ "user delete",
257
+ "group delete",
251
258
  ]);
252
259
  export function isAllowedCleanupAction(resource, action) {
253
260
  return ALLOWED_CLEANUP_ACTIONS.has(`${resource} ${action}`);
@@ -391,7 +398,7 @@ function structuredExamples(examples, examplePayload) {
391
398
  * (`dss commands run`, `dss agent contract`) never loads the TypeBox graph.
392
399
  * The content matches `typeBoxCommandOutputSchemas()` exactly; parity and
393
400
  * freshness are enforced by tests/cli/discovery-output-schemas.test.ts and
394
- * `bun run check`.
401
+ * `bun run verify`.
395
402
  */
396
403
  const actionOutputSchemas = actionOutputSchemasMetadata
397
404
  .schemas;
@@ -764,6 +771,21 @@ const STRING_OUTPUT_ACTIONS = new Set([
764
771
  "log",
765
772
  "log-url",
766
773
  ]);
774
+ /**
775
+ * `resource.action` shapes that differ from the bare-action-name sets above:
776
+ * `contents`/`usages`/`project-timeline` are array-named elsewhere, and
777
+ * `download`/`diff` are string-named elsewhere, but these handlers return the
778
+ * opposite shape. Pinned by tests/cli/output-shapes.test.ts.
779
+ */
780
+ const SCOPED_OUTPUT_SHAPES = {
781
+ "folder.contents": "array",
782
+ "scenario.last-runs": "array",
783
+ "plugin.usages": "object",
784
+ "plugin.download": "object",
785
+ "dataset.download": "object",
786
+ "project-library.get": "string",
787
+ "project-library.diff": "object",
788
+ };
767
789
  function inferOutputShape(resource, action) {
768
790
  if (resource === "project-git") {
769
791
  if (action === "branches" || action === "tags" || action === "list-libraries")
@@ -777,14 +799,15 @@ function inferOutputShape(resource, action) {
777
799
  || resource === "version") {
778
800
  return "object";
779
801
  }
802
+ // Resource-scoped rules come first: the bare-name sets below classify by
803
+ // action alone and would otherwise misfile same-named actions elsewhere.
804
+ const scoped = SCOPED_OUTPUT_SHAPES[`${resource}.${action}`];
805
+ if (scoped !== undefined)
806
+ return scoped;
780
807
  if (/^list(-|$)/.test(action))
781
808
  return "array";
782
809
  if (ARRAY_OUTPUT_ACTIONS.has(action))
783
810
  return "array";
784
- if (resource === "dataset" && action === "download")
785
- return "object";
786
- if (resource === "project-library" && action === "get")
787
- return "string";
788
811
  if (STRING_OUTPUT_ACTIONS.has(action))
789
812
  return "string";
790
813
  return "object";
@@ -1496,7 +1519,7 @@ export function buildAgentContract() {
1496
1519
  "dss commands run --fields dataset",
1497
1520
  "dss commands run --fields dataset.create",
1498
1521
  ],
1499
- scopedDiscoveryHint: "Default: resource/action summary. --contains TEXT: top 5 actions by concept. --fields RESOURCE: all resource entries; RESOURCE.ACTION: one entry keyed by that path; append .FIELD for nested metadata. Comma-separate paths. --output PATH exports the full registry.",
1522
+ scopedDiscoveryHint: "Default: resource/action summary. --contains TEXT: top 5 actions by concept. --fields RESOURCE: each action's usage, description, and side effect; RESOURCE.ACTION: one full entry keyed by that path; append .FIELD for nested metadata. Comma-separate paths. --output PATH exports full entries (the full registry without --fields).",
1500
1523
  // Accepted by every command and absent from per-entry `flags`; the
1501
1524
  // connection flags matter only for commands that call DSS.
1502
1525
  globalFlags: [...GLOBAL_AGENT_FLAGS, ...AUTHENTICATED_AGENT_FLAGS,].map((name) => {
@@ -1523,7 +1546,7 @@ export function buildAgentContract() {
1523
1546
  failure: "structured-error-object",
1524
1547
  failureResultDetailLimitBytes: 65_536,
1525
1548
  fieldProjection: "Missing --fields paths: null on stdout; field_projection_missing on stderr.",
1526
- compactResults: "*.list, job get, dataset get/info, project get: compact; --full/--fields use DSS objects; lists take --contains/--limit (list_truncated warning).",
1549
+ compactResults: "*.list, job get, dataset get/info, project get, scenario get-run/last-runs: compact; --full/--fields use DSS objects; lists take --contains/--limit (list_truncated warning).",
1527
1550
  richFailureResults: "doctor/batch/cleanup failures: own compact {ok:false,...} result on stdout and command exit code; no wrapper.",
1528
1551
  },
1529
1552
  stderr: {
@@ -10,4 +10,10 @@ export declare function envFileProvenance(name: string): EnvFileProvenance | und
10
10
  export declare function dataikuEnvironmentEnabled(): boolean;
11
11
  /** DATAIKU_PROJECT_KEY from the environment, unless `DATAIKU_DISABLE_ENV=1`. */
12
12
  export declare function ambientProjectKey(): string | undefined;
13
+ /**
14
+ * `DSS_PRETTY=1` indents stdout JSON. Agent shells that cut long lines (omp at
15
+ * 768 bytes) truncate a one-line result; indented output keeps each line short.
16
+ * Read from the process environment only, never from a `.env` file.
17
+ */
18
+ export declare function prettyOutputEnabled(): boolean;
13
19
  export declare function loadEnvFile(): void;