dataiku-sdk 4.0.0 → 4.2.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 (66) hide show
  1. package/README.md +1 -0
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +5 -1
  4. package/dist/packages/types/src/index.js +5 -1
  5. package/dist/src/cli/command-syntax.json +4136 -2756
  6. package/dist/src/cli/commands/code-env.js +5 -3
  7. package/dist/src/cli/commands/dataset.js +6 -4
  8. package/dist/src/cli/commands/future.js +7 -2
  9. package/dist/src/cli/commands/index.js +4 -0
  10. package/dist/src/cli/commands/job.js +12 -9
  11. package/dist/src/cli/commands/messaging-channel.d.ts +17 -0
  12. package/dist/src/cli/commands/messaging-channel.js +385 -0
  13. package/dist/src/cli/commands/project-deployer.js +33 -0
  14. package/dist/src/cli/commands/project.js +30 -0
  15. package/dist/src/cli/commands/recipe.js +23 -2
  16. package/dist/src/cli/commands/scenario.d.ts +6 -0
  17. package/dist/src/cli/commands/scenario.js +495 -17
  18. package/dist/src/cli/contract.js +20 -4
  19. package/dist/src/cli/env.d.ts +6 -0
  20. package/dist/src/cli/env.js +8 -0
  21. package/dist/src/cli/flags.js +34 -0
  22. package/dist/src/cli/helpers/cleanup.js +11 -0
  23. package/dist/src/cli/list-shapes.js +5 -0
  24. package/dist/src/cli/output.d.ts +2 -0
  25. package/dist/src/cli/output.js +24 -4
  26. package/dist/src/cli/plans.js +94 -2
  27. package/dist/src/cli/read-shapes.js +92 -1
  28. package/dist/src/cli/result-shapes.d.ts +19 -1
  29. package/dist/src/cli/result-shapes.js +228 -4
  30. package/dist/src/cli/selector-suggestions.d.ts +14 -0
  31. package/dist/src/cli/selector-suggestions.js +56 -0
  32. package/dist/src/cli/usage.d.ts +1 -1
  33. package/dist/src/cli/usage.js +36 -4
  34. package/dist/src/cli/version.d.ts +2 -2
  35. package/dist/src/cli/version.js +2 -2
  36. package/dist/src/cli.js +62 -13
  37. package/dist/src/client.d.ts +16 -4
  38. package/dist/src/client.js +45 -9
  39. package/dist/src/errors.d.ts +2 -0
  40. package/dist/src/errors.js +12 -2
  41. package/dist/src/generated/action-output-schemas.json +296 -0
  42. package/dist/src/index.d.ts +1 -0
  43. package/dist/src/index.js +1 -0
  44. package/dist/src/resources/bundles.d.ts +18 -0
  45. package/dist/src/resources/bundles.js +26 -0
  46. package/dist/src/resources/futures.d.ts +3 -0
  47. package/dist/src/resources/futures.js +5 -1
  48. package/dist/src/resources/messaging-channels.d.ts +83 -0
  49. package/dist/src/resources/messaging-channels.js +108 -0
  50. package/dist/src/resources/projects.d.ts +24 -0
  51. package/dist/src/resources/projects.js +36 -0
  52. package/dist/src/resources/recipe-create.d.ts +4 -1
  53. package/dist/src/resources/recipe-create.js +11 -2
  54. package/dist/src/resources/scenario-authoring.d.ts +52 -0
  55. package/dist/src/resources/scenario-authoring.js +216 -0
  56. package/dist/src/resources/scenarios.d.ts +128 -11
  57. package/dist/src/resources/scenarios.js +273 -43
  58. package/dist/src/resources/sql.js +3 -3
  59. package/dist/src/utils/messaging-channel-secrets.d.ts +17 -0
  60. package/dist/src/utils/messaging-channel-secrets.js +114 -0
  61. package/package.json +2 -1
  62. package/plugin.json +1 -1
  63. package/skills/dataiku-dss/SKILL.md +15 -16
  64. package/skills/dataiku-dss/references/discovery.md +2 -2
  65. package/skills/dataiku-dss/references/notifications.md +29 -0
  66. package/skills/dataiku-dss/references/troubleshooting.md +1 -0
@@ -1,8 +1,287 @@
1
- import { jsonInput, num, parseBooleanOption, } from "../coerce.js";
1
+ import { resolve, } from "node:path";
2
+ import { mailReporter, SCENARIO_TRIGGER_FREQUENCIES, selectScenarioListIndex, temporalTrigger, } from "../../resources/scenario-authoring.js";
3
+ import { messagingChannelSecretsFromBody, sanitizeMessagingChannelError, sanitizeMessagingChannelSecrets, } from "../../utils/messaging-channel-secrets.js";
4
+ import { asRecord, } from "../../utils/records.js";
5
+ import { json, jsonInput, num, parseBooleanOption, parseJsonObject, readInputText, splitCsvFlag, } from "../coerce.js";
2
6
  import { executionMode, } from "../flags.js";
3
- import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
7
+ import { encodedProjectEndpoint, enqueueCliWarning, readIfExists, skipResult, } from "../output.js";
8
+ import { scenarioRunDiagnosis, shapeCommandResult, } from "../result-shapes.js";
4
9
  import { commandUsage, withUsage, } from "../syntax.js";
5
10
  import { requireArgs, UsageError, } from "../usage.js";
11
+ /**
12
+ * The recipe a failed job was running and the datasets it read, from one
13
+ * `job get` (its FAILED activity). Undefined when the job cannot be read or
14
+ * names no failed recipe: the diagnosis then stays job-level.
15
+ */
16
+ async function failedActivity(c, jobId, projectKey) {
17
+ if (typeof jobId !== "string")
18
+ return undefined;
19
+ try {
20
+ const job = asRecord(await c.jobs.get(jobId, projectKey));
21
+ const activities = asRecord(asRecord(job?.["baseStatus"])?.["activities"]) ?? {};
22
+ for (const value of Object.values(activities)) {
23
+ const activity = asRecord(value);
24
+ if (activity?.["state"] !== "FAILED" || typeof activity["recipeName"] !== "string")
25
+ continue;
26
+ const sources = Array.isArray(activity["sources"]) ? activity["sources"] : [];
27
+ const inputs = sources.flatMap((source) => {
28
+ const id = asRecord(source)?.["id"];
29
+ if (typeof id !== "string")
30
+ return [];
31
+ // Same-project inputs are named bare; foreign ones keep PROJECT.name.
32
+ const dot = id.indexOf(".");
33
+ return [dot > 0 && id.slice(0, dot) === projectKey ? id.slice(dot + 1) : id,];
34
+ });
35
+ return { recipe: activity["recipeName"], inputs, };
36
+ }
37
+ }
38
+ catch {
39
+ // A diagnosis aid only: the run report itself is already in hand.
40
+ }
41
+ return undefined;
42
+ }
43
+ /** Diagnosis of a raw run report, located at the failed recipe when its job names one. */
44
+ async function locatedDiagnosis(c, details, projectKey) {
45
+ const plain = scenarioRunDiagnosis(details);
46
+ const runProject = asRecord(asRecord(asRecord(details)?.["scenarioRun"])?.["trigger"])?.["projectKey"];
47
+ const pk = projectKey ?? (typeof runProject === "string" ? runProject : undefined);
48
+ const activity = await failedActivity(c, plain.failure?.["jobId"], pk);
49
+ return activity ? scenarioRunDiagnosis(details, activity) : plain;
50
+ }
51
+ /**
52
+ * Run a scenario and wait; a failed (not timed-out) run gains the first
53
+ * failure, the recipe that failed, and the next commands. Extra reads happen
54
+ * only on failure; a read error leaves the wait result untouched. A cancelled
55
+ * trigger fire has no run to diagnose: it says why instead.
56
+ */
57
+ async function runAndWaitWithFailure(c, scenarioId, options) {
58
+ const result = await c.scenarios.runAndWait(scenarioId, options);
59
+ if (result.cancelled === true) {
60
+ return {
61
+ ...result,
62
+ failure: {
63
+ message: `DSS cancelled this run before it started (trigger run ${result.triggerRunId}): the scenario was already running, or another trigger fire replaced this one. No scenario run exists; wait for or abort the current run (scenario status, scenario abort), then run again.`,
64
+ },
65
+ next: [["dss", "scenario", "status", scenarioId, ...projectKeyArgs(options.projectKey),],],
66
+ };
67
+ }
68
+ if (result.success || result.timedOut === true || !result.runId)
69
+ return result;
70
+ try {
71
+ const details = await c.scenarios.getRunDetails(scenarioId, result.runId, {
72
+ projectKey: options.projectKey,
73
+ });
74
+ return { ...result, ...await locatedDiagnosis(c, details, options.projectKey), };
75
+ }
76
+ catch {
77
+ return result;
78
+ }
79
+ }
80
+ function projectKeyArgs(projectKey) {
81
+ return projectKey === undefined ? [] : ["--project-key", projectKey,];
82
+ }
83
+ /** `scenario run` result: the trigger fire, with the explicit pointer to the scenario run it will start. */
84
+ function triggerRunResult(fire, scenarioId, projectKey) {
85
+ if (fire.cancelled) {
86
+ return {
87
+ ...fire,
88
+ hint: "DSS cancelled this trigger fire: the scenario was already running (or another fire replaced it), so no scenario run exists for this triggerRunId.",
89
+ next: [["dss", "scenario", "status", scenarioId, ...projectKeyArgs(projectKey),],],
90
+ };
91
+ }
92
+ return {
93
+ ...fire,
94
+ hint: "triggerRunId identifies this trigger firing, not a scenario run: get-run and log need the scenario run id that get-run-for-trigger returns (or use --wait).",
95
+ next: [[
96
+ "dss",
97
+ "scenario",
98
+ "get-run-for-trigger",
99
+ scenarioId,
100
+ fire.triggerRunId,
101
+ ...(fire.triggerId === "manual" ? [] : ["--trigger-id", fire.triggerId,]),
102
+ ...projectKeyArgs(projectKey),
103
+ ],],
104
+ };
105
+ }
106
+ /** `scenario get-run-for-trigger` result: the fire's state and the commands that follow from it. */
107
+ function triggerRunStateResult(state, scenarioId, projectKey) {
108
+ const pk = projectKeyArgs(projectKey);
109
+ const next = state.runId !== undefined
110
+ ? [
111
+ ["dss", "scenario", "get-run", scenarioId, state.runId, ...pk,],
112
+ ["dss", "scenario", "log", scenarioId, state.runId, ...pk,],
113
+ ]
114
+ : state.state === "PENDING"
115
+ ? [[
116
+ "dss",
117
+ "scenario",
118
+ "get-run-for-trigger",
119
+ scenarioId,
120
+ state.triggerRunId,
121
+ ...(state.triggerId === "manual" ? [] : ["--trigger-id", state.triggerId,]),
122
+ ...pk,
123
+ ],]
124
+ : [["dss", "scenario", "status", scenarioId, ...pk,],];
125
+ return {
126
+ ...state,
127
+ ...(state.cancelled
128
+ ? {
129
+ hint: "DSS cancelled this trigger fire (the scenario was already running, or another fire replaced it); no scenario run will exist.",
130
+ }
131
+ : {}),
132
+ next,
133
+ };
134
+ }
135
+ /** Run parameters from `--params JSON` or `--params-file PATH` (the POST /run body). */
136
+ export function scenarioRunParams(flags) {
137
+ const file = flags["params-file"];
138
+ if (flags["params"] !== undefined && file !== undefined) {
139
+ throw new UsageError("Pass --params or --params-file, not both.");
140
+ }
141
+ return typeof file === "string"
142
+ ? parseJsonObject(readInputText(file, "--params-file"), "--params-file")
143
+ : json(flags["params"], "--params");
144
+ }
145
+ /** The trigger `trigger-add` appends: a raw `--data` object, or a schedule built from the --frequency flags. */
146
+ export function scenarioTriggerFromFlags(flags) {
147
+ const usage = commandUsage("scenario", "trigger-add");
148
+ const raw = jsonInput(flags);
149
+ if (raw !== undefined) {
150
+ if (flags["frequency"] !== undefined) {
151
+ throw new UsageError(`--frequency builds a schedule; pass it or --data/--data-file/--stdin, not both. Usage: ${usage}`);
152
+ }
153
+ return raw;
154
+ }
155
+ const frequency = SCENARIO_TRIGGER_FREQUENCIES.find((candidate) => candidate === flags["frequency"]);
156
+ if (frequency === undefined) {
157
+ throw new UsageError(`--frequency ${SCENARIO_TRIGGER_FREQUENCIES.join("|")} (or a raw trigger via --data) is required. Usage: ${usage}`);
158
+ }
159
+ const text = (name) => typeof flags[name] === "string" ? flags[name] : undefined;
160
+ return temporalTrigger({
161
+ frequency,
162
+ every: num(flags["every"], "--every"),
163
+ hour: num(flags["hour"], "--hour"),
164
+ minute: num(flags["minute"], "--minute"),
165
+ days: flags["days"] === undefined ? undefined : splitCsvFlag(flags["days"]),
166
+ dayOfMonth: num(flags["day-of-month"], "--day-of-month"),
167
+ runOn: text("run-on"),
168
+ startDate: text("start-date"),
169
+ timezone: text("timezone"),
170
+ name: text("name"),
171
+ active: parseBooleanOption(flags["active"], "--active"),
172
+ });
173
+ }
174
+ /** The reporter `reporter-add` appends: a raw `--data` object, or a mail reporter built from the --channel flags. */
175
+ export function scenarioReporterFromFlags(flags) {
176
+ const usage = commandUsage("scenario", "reporter-add");
177
+ const raw = jsonInput(flags);
178
+ const mailFlags = ["channel", "recipient", "subject", "message", "condition", "phase",];
179
+ if (raw !== undefined) {
180
+ const clash = mailFlags.find((name) => flags[name] !== undefined);
181
+ if (clash !== undefined) {
182
+ throw new UsageError(`--${clash} builds a mail reporter; pass it or --data/--data-file/--stdin, not both. Usage: ${usage}`);
183
+ }
184
+ return raw;
185
+ }
186
+ const text = (name) => typeof flags[name] === "string" ? flags[name] : undefined;
187
+ const [channel, recipient, subject, message,] = ["channel", "recipient", "subject", "message",]
188
+ .map((name) => {
189
+ const value = text(name);
190
+ if (value === undefined) {
191
+ throw new UsageError(`--${name} is required (or pass a raw reporter via --data). Usage: ${usage}`);
192
+ }
193
+ return value;
194
+ });
195
+ return mailReporter({
196
+ channel,
197
+ recipient,
198
+ subject,
199
+ message,
200
+ condition: text("condition"),
201
+ phase: text("phase"),
202
+ name: text("name"),
203
+ active: parseBooleanOption(flags["active"], "--active"),
204
+ });
205
+ }
206
+ /** Reporter reduced to what a list needs: identity, phase, condition, channel, recipient, subject. */
207
+ function compactReporter(reporter, index) {
208
+ const fields = asRecord(reporter);
209
+ const messaging = asRecord(fields?.["messaging"]);
210
+ const configuration = asRecord(messaging?.["configuration"]);
211
+ return {
212
+ index,
213
+ id: fields?.["id"],
214
+ name: fields?.["name"],
215
+ active: fields?.["active"],
216
+ phase: fields?.["phase"],
217
+ runCondition: fields?.["runConditionEnabled"] === true ? fields["runCondition"] : undefined,
218
+ type: messaging?.["type"],
219
+ channel: configuration?.["channelId"],
220
+ recipient: configuration?.["recipient"],
221
+ subject: configuration?.["subject"],
222
+ };
223
+ }
224
+ /** Shared `trigger-add`/`reporter-add`: dry-run previews the appended element, otherwise it is written and verified. */
225
+ async function addScenarioItem(c, kind, scenarioId, item, flags) {
226
+ const projectKey = flags["project-key"];
227
+ const listAction = `${kind}-list`;
228
+ if (executionMode(flags).dryRun) {
229
+ const items = await c.scenarios.listItems(kind, scenarioId, { projectKey, });
230
+ return {
231
+ dryRun: true,
232
+ action: `${kind}-add`,
233
+ resource: "scenario",
234
+ id: scenarioId,
235
+ [kind]: sanitizeMessagingChannelSecrets(item),
236
+ index: items.length,
237
+ count: items.length + 1,
238
+ };
239
+ }
240
+ // A raw reporter can carry a webhook URL or token that DSS may echo back in an error.
241
+ const secrets = messagingChannelSecretsFromBody(item);
242
+ const result = await c.scenarios.addItem(kind, scenarioId, item, { projectKey, }).catch((error) => {
243
+ throw sanitizeMessagingChannelError(error, secrets);
244
+ });
245
+ return {
246
+ added: kind,
247
+ resource: "scenario",
248
+ id: scenarioId,
249
+ index: result.index,
250
+ [kind]: sanitizeMessagingChannelSecrets(result.item),
251
+ count: result.count,
252
+ next: [["dss", "scenario", listAction, scenarioId, ...projectKeyArgs(projectKey),],],
253
+ };
254
+ }
255
+ /** Shared `trigger-remove`/`reporter-remove`: the element is chosen by --index or --name; the others stay. */
256
+ async function removeScenarioItem(c, kind, scenarioId, flags) {
257
+ const projectKey = flags["project-key"];
258
+ const selector = {
259
+ index: num(flags["index"], "--index"),
260
+ name: flags["name"],
261
+ };
262
+ if (executionMode(flags).dryRun) {
263
+ const items = await c.scenarios.listItems(kind, scenarioId, { projectKey, });
264
+ const index = selectScenarioListIndex(items, selector, kind);
265
+ return {
266
+ dryRun: true,
267
+ action: `${kind}-remove`,
268
+ resource: "scenario",
269
+ id: scenarioId,
270
+ index,
271
+ [kind]: sanitizeMessagingChannelSecrets(items[index]),
272
+ count: items.length - 1,
273
+ };
274
+ }
275
+ const result = await c.scenarios.removeItem(kind, scenarioId, selector, { projectKey, });
276
+ return {
277
+ removed: kind,
278
+ resource: "scenario",
279
+ id: scenarioId,
280
+ index: result.index,
281
+ [kind]: sanitizeMessagingChannelSecrets(result.item),
282
+ count: result.count,
283
+ };
284
+ }
6
285
  export const scenarioCommands = withUsage("scenario", {
7
286
  list: {
8
287
  handler: (c, _a, f) => c.scenarios.list(f["project-key"]),
@@ -21,6 +300,7 @@ export const scenarioCommands = withUsage("scenario", {
21
300
  handler: async (c, a, f) => {
22
301
  requireArgs(a, 1, commandUsage("scenario", "run"));
23
302
  const pk = f["project-key"];
303
+ const params = scenarioRunParams(f);
24
304
  const options = {
25
305
  pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
26
306
  timeoutMs: num(f["timeout"], "--timeout"),
@@ -32,22 +312,28 @@ export const scenarioCommands = withUsage("scenario", {
32
312
  resource: "scenario",
33
313
  id: a[0],
34
314
  ...options,
315
+ ...(params === undefined ? {} : { params, }),
35
316
  endpoint: encodedProjectEndpoint(c, pk, `/scenarios/${encodeURIComponent(a[0])}/run/`),
36
317
  method: "POST",
37
318
  };
38
319
  }
39
320
  if (f["wait"] === true) {
40
- return c.scenarios.runAndWait(a[0], { ...options, projectKey: pk, });
321
+ return runAndWaitWithFailure(c, a[0], { ...options, params, projectKey: pk, });
41
322
  }
42
- return c.scenarios.run(a[0], pk);
323
+ return triggerRunResult(await c.scenarios.run(a[0], { params, projectKey: pk, }), a[0], pk);
43
324
  },
44
- description: "Trigger a scenario run, optionally waiting for completion.",
45
- examples: ["dss scenario run my_scenario", "dss scenario run my_scenario --wait",],
325
+ description: "Trigger a scenario run; --wait waits for it. --params JSON/--params-file PATH are the run parameters. Without --wait the result is the trigger fire: triggerRunId is NOT a scenario run id, resolve it with get-run-for-trigger (see `next`). cancelled: true means the scenario was already running; no run starts.",
326
+ examples: [
327
+ "dss scenario run my_scenario",
328
+ "dss scenario run my_scenario --wait",
329
+ 'dss scenario run my_scenario --params \'{"region":"EU"}\' --wait',
330
+ ],
46
331
  },
47
332
  "run-and-wait": {
48
333
  handler: async (c, a, f) => {
49
334
  requireArgs(a, 1, commandUsage("scenario", "run-and-wait"));
50
335
  const pk = f["project-key"];
336
+ const params = scenarioRunParams(f);
51
337
  const options = {
52
338
  pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
53
339
  timeoutMs: num(f["timeout"], "--timeout"),
@@ -59,16 +345,18 @@ export const scenarioCommands = withUsage("scenario", {
59
345
  resource: "scenario",
60
346
  id: a[0],
61
347
  ...options,
348
+ ...(params === undefined ? {} : { params, }),
62
349
  endpoint: encodedProjectEndpoint(c, pk, `/scenarios/${encodeURIComponent(a[0])}/run/`),
63
350
  method: "POST",
64
351
  };
65
352
  }
66
- return c.scenarios.runAndWait(a[0], { ...options, projectKey: pk, });
353
+ return runAndWaitWithFailure(c, a[0], { ...options, params, projectKey: pk, });
67
354
  },
68
- description: "Run a scenario and wait for completion.",
355
+ description: "Run a scenario and wait for completion (--params JSON / --params-file PATH as run parameters). Outcome CANCELLED (exit 4) means DSS cancelled the trigger fire because the scenario was already running; TIMEOUT means the wait ran out while the run continues.",
69
356
  examples: [
70
357
  "dss scenario run-and-wait my_scenario",
71
358
  "dss scenario run-and-wait my_scenario --timeout 300000",
359
+ "dss scenario run-and-wait my_scenario --params-file params.json",
72
360
  ],
73
361
  },
74
362
  status: {
@@ -101,11 +389,13 @@ export const scenarioCommands = withUsage("scenario", {
101
389
  handler: async (c, a, f) => {
102
390
  requireArgs(a, 2, commandUsage("scenario", "create"));
103
391
  const pk = f["project-key"];
392
+ const data = jsonInput(f);
104
393
  const payload = {
105
394
  scenarioId: a[0],
106
395
  name: a[1],
107
396
  scenarioType: f["type"],
108
397
  projectKey: pk,
398
+ ...(data === undefined ? {} : { data, }),
109
399
  };
110
400
  if (f["if-not-exists"] === true || executionMode(f).dryRun) {
111
401
  const list = await c.scenarios.list(pk);
@@ -126,14 +416,16 @@ export const scenarioCommands = withUsage("scenario", {
126
416
  }
127
417
  await c.scenarios.create(a[0], a[1], {
128
418
  scenarioType: payload.scenarioType,
419
+ data,
129
420
  projectKey: pk,
130
421
  });
131
422
  return { created: a[0], name: a[1], resource: "scenario", };
132
423
  },
133
- description: "Create a new scenario.",
424
+ description: "Create a new scenario. --data/--data-file/--stdin merge extra top-level fields into the create body (e.g. triggers, reporters, params.steps, runAsUser).",
134
425
  examples: [
135
426
  'dss scenario create my_scenario "My Scenario"',
136
427
  'dss scenario create my_scenario "My Scenario" --type custom_python --dry-run',
428
+ 'dss scenario create nightly "Nightly" --data \'{"triggers":[{"active":true,"type":"temporal","params":{"frequency":"Daily","hour":2,"minute":0,"startingFrom":"2026-01-01T00:00:00.000+0000","repeatFrequency":1,"timezone":"SERVER"}}]}\' --dry-run',
137
429
  ],
138
430
  },
139
431
  update: {
@@ -206,20 +498,31 @@ export const scenarioCommands = withUsage("scenario", {
206
498
  requireArgs(a, 1, commandUsage("scenario", "last-runs"));
207
499
  return c.scenarios.getLastRuns(a[0], {
208
500
  limit: num(f["limit"], "--limit"),
501
+ onlyFinished: f["only-finished"] === true,
209
502
  projectKey: f["project-key"],
210
503
  });
211
504
  },
212
- description: "List the most recent runs of a scenario (run id, start/end, outcome).",
213
- examples: ["dss scenario last-runs my_scenario --limit 10",],
505
+ description: "List a scenario's most recent runs, newest first (compact: run id, outcome, timing, trigger, failure; --full for DSS objects). --only-finished skips in-progress (and, observed live, aborted) runs: `--only-finished --limit 1` is the last finished run; read its outcome.",
506
+ examples: [
507
+ "dss scenario last-runs my_scenario --limit 10",
508
+ "dss scenario last-runs my_scenario --only-finished --limit 1",
509
+ ],
214
510
  },
215
511
  "get-run": {
216
- handler: (c, a, f) => {
512
+ handler: async (c, a, f) => {
217
513
  requireArgs(a, 2, commandUsage("scenario", "get-run"));
218
- return c.scenarios.getRunDetails(a[0], a[1], {
219
- projectKey: f["project-key"],
220
- });
514
+ const projectKey = f["project-key"];
515
+ const details = await c.scenarios.getRunDetails(a[0], a[1], { projectKey, });
516
+ if (f["full"] === true || typeof f["fields"] === "string")
517
+ return details;
518
+ // Compact view, located at the failed recipe (one extra job read, only on failure).
519
+ // The shaped object has no `scenarioRun`, so the dispatcher's shaping passes it through.
520
+ const shaped = asRecord(shapeCommandResult("scenario", "get-run", details, f));
521
+ if (!shaped?.["failure"])
522
+ return shaped ?? details;
523
+ return { ...shaped, ...await locatedDiagnosis(c, details, projectKey), };
221
524
  },
222
- description: "Get the details of a specific scenario run, including per-step outcomes.",
525
+ 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
526
  examples: ["dss scenario get-run my_scenario 2024-09-01-01-02-03-123",],
224
527
  },
225
528
  log: {
@@ -233,7 +536,6 @@ export const scenarioCommands = withUsage("scenario", {
233
536
  const outputFile = f["output"]
234
537
  ?? f["output-file"];
235
538
  if (outputFile) {
236
- const { resolve, } = await import("node:path");
237
539
  const outputPath = resolve(outputFile);
238
540
  await Bun.write(outputPath, result.text);
239
541
  return { path: outputPath, truncated: result.truncated, };
@@ -309,4 +611,180 @@ export const scenarioCommands = withUsage("scenario", {
309
611
  "dss scenario active-set my_scenario true --dry-run",
310
612
  ],
311
613
  },
614
+ "get-run-for-trigger": {
615
+ handler: async (c, a, f) => {
616
+ requireArgs(a, 2, commandUsage("scenario", "get-run-for-trigger"));
617
+ const pk = f["project-key"];
618
+ const state = await c.scenarios.getRunForTrigger(a[0], a[1], {
619
+ triggerId: f["trigger-id"],
620
+ projectKey: pk,
621
+ });
622
+ return triggerRunStateResult(state, a[0], pk);
623
+ },
624
+ description: "Resolve a trigger run (the triggerRunId `scenario run` returns) to its scenario run: state PENDING (no run yet; repeat), RUNNING, FINISHED (outcome), or CANCELLED (DSS cancelled the fire because the scenario was already running; no run will exist). `runId` is what get-run and log take.",
625
+ examples: ["dss scenario get-run-for-trigger my_scenario 2024-09-01-01-02-03-123",],
626
+ },
627
+ "runs-by-date": {
628
+ handler: (c, a, f) => {
629
+ requireArgs(a, 1, commandUsage("scenario", "runs-by-date"));
630
+ const from = f["from"];
631
+ if (typeof from !== "string") {
632
+ throw new UsageError(`--from is required. Usage: ${commandUsage("scenario", "runs-by-date")}`);
633
+ }
634
+ return c.scenarios.getRunsByDate(a[0], {
635
+ from,
636
+ to: f["to"],
637
+ projectKey: f["project-key"],
638
+ });
639
+ },
640
+ description: "List the runs of a scenario that started from --from (inclusive) to --to (exclusive: DSS compares the start of that day, so --from D --to D is empty), each YYYY-MM-DD or an ISO date-time (time dropped). --to defaults to tomorrow, i.e. up to now. Compact like last-runs; --full returns the DSS run objects.",
641
+ examples: ["dss scenario runs-by-date my_scenario --from 2026-10-01 --to 2026-10-31",],
642
+ },
643
+ "run-report": {
644
+ handler: async (c, a, f) => {
645
+ requireArgs(a, 2, commandUsage("scenario", "run-report"));
646
+ const result = await c.scenarios.getRunReport(a[0], a[1], {
647
+ stepId: f["step-id"],
648
+ maxBytes: num(f["max-bytes"], "--max-bytes"),
649
+ projectKey: f["project-key"],
650
+ });
651
+ const outputFile = f["output"]
652
+ ?? f["output-file"];
653
+ if (outputFile) {
654
+ const outputPath = resolve(outputFile);
655
+ await Bun.write(outputPath, result.text);
656
+ return { path: outputPath, truncated: result.truncated, };
657
+ }
658
+ return result.text;
659
+ },
660
+ description: "Get the JUnit XML report of a test scenario run (or of one step via --step-id). Only scenarios marked as test (markedAsTest) have reports. Byte-bounded at --max-bytes (default 1 MiB); --output PATH writes it to a file (stdout returns the path).",
661
+ examples: [
662
+ "dss scenario run-report my_test_scenario RUN_ID --output report.xml",
663
+ "dss scenario run-report my_test_scenario RUN_ID --step-id run_tests",
664
+ ],
665
+ },
666
+ "trigger-list": {
667
+ handler: async (c, a, f) => {
668
+ requireArgs(a, 1, commandUsage("scenario", "trigger-list"));
669
+ const triggers = await c.scenarios.listItems("trigger", a[0], {
670
+ projectKey: f["project-key"],
671
+ });
672
+ return triggers.map((trigger, index) => ({ index, ...asRecord(trigger), }));
673
+ },
674
+ description: "List the automatic triggers of a scenario with their 0-based index (the --index of trigger-remove). Triggers only fire while the scenario is active (scenario active-set).",
675
+ examples: ["dss scenario trigger-list my_scenario",],
676
+ },
677
+ "trigger-add": {
678
+ handler: (c, a, f) => {
679
+ requireArgs(a, 1, commandUsage("scenario", "trigger-add"));
680
+ return addScenarioItem(c, "trigger", a[0], scenarioTriggerFromFlags(f), f);
681
+ },
682
+ description: "Append a trigger, keeping the others. Schedule: --frequency (dataikuapi builders) with --every, --hour, --minute, --days (weekly), --day-of-month/--run-on (monthly), --start-date, --timezone (default SERVER), --name, --active. Other trigger types: JSON via --data.",
683
+ examples: [
684
+ "dss scenario trigger-add my_scenario --frequency daily --hour 6 --minute 30",
685
+ "dss scenario trigger-add my_scenario --frequency weekly --days Monday,Friday --hour 8",
686
+ "dss scenario trigger-add my_scenario --frequency monthly --day-of-month 5 --run-on ON_THE_DAY --dry-run",
687
+ 'dss scenario trigger-add my_scenario --data \'{"type":"temporal","active":true,"params":{"frequency":"Minutely","repeatFrequency":15}}\'',
688
+ ],
689
+ },
690
+ "trigger-remove": {
691
+ handler: (c, a, f) => {
692
+ requireArgs(a, 1, commandUsage("scenario", "trigger-remove"));
693
+ return removeScenarioItem(c, "trigger", a[0], f);
694
+ },
695
+ description: "Remove one trigger, selected by --index (from trigger-list) or --name, keeping the others.",
696
+ examples: [
697
+ "dss scenario trigger-remove my_scenario --index 0",
698
+ "dss scenario trigger-remove my_scenario --name nightly --dry-run",
699
+ ],
700
+ },
701
+ "reporter-list": {
702
+ handler: async (c, a, f) => {
703
+ requireArgs(a, 1, commandUsage("scenario", "reporter-list"));
704
+ const reporters = await c.scenarios.listItems("reporter", a[0], {
705
+ projectKey: f["project-key"],
706
+ });
707
+ // Channel credentials live on the channel, but reporters of other types may carry their own.
708
+ return reporters.map((reporter, index) => f["full"] === true
709
+ ? { index, ...asRecord(sanitizeMessagingChannelSecrets(reporter)), }
710
+ : compactReporter(reporter, index));
711
+ },
712
+ description: "List the reporters (notifications) of a scenario with their 0-based index: id, phase, active, channel, recipient, subject (compact by default; --full returns the DSS objects with credential-looking values redacted).",
713
+ examples: ["dss scenario reporter-list my_scenario",],
714
+ },
715
+ "reporter-add": {
716
+ handler: async (c, a, f) => {
717
+ requireArgs(a, 1, commandUsage("scenario", "reporter-add"));
718
+ const reporter = scenarioReporterFromFlags(f);
719
+ // DSS saves a reporter with any channel id; an unknown one never sends. Warn, don't fail:
720
+ // a channel may be hidden from this key yet usable by the scenario's run-as user.
721
+ const channel = f["channel"];
722
+ if (typeof channel === "string") {
723
+ const known = (await c.messagingChannels.list()).map((entry) => entry.id);
724
+ if (!known.includes(channel)) {
725
+ enqueueCliWarning({
726
+ code: "reporter_channel_unknown",
727
+ channel,
728
+ knownChannels: known,
729
+ hint: "No messaging channel with this id is visible: the reporter will not send. Check `dss messaging-channel list`.",
730
+ });
731
+ }
732
+ }
733
+ return addScenarioItem(c, "reporter", a[0], reporter, f);
734
+ },
735
+ description: "Append a reporter, keeping the others. Mail: --channel ID (messaging-channel list), --recipient, --subject, --message (required), --condition EXPR, --phase START|END (default END). Slack, Teams and other channel types: pass the reporter JSON with --data.",
736
+ examples: [
737
+ 'dss scenario reporter-add my_scenario --channel smtp --recipient ops@example.com --subject "Scenario done" --message "See DSS"',
738
+ "dss scenario reporter-add my_scenario --channel smtp --recipient ops@example.com --subject Failed --message Failed --condition \"outcome == 'FAILED'\" --dry-run",
739
+ ],
740
+ },
741
+ "reporter-remove": {
742
+ handler: (c, a, f) => {
743
+ requireArgs(a, 1, commandUsage("scenario", "reporter-remove"));
744
+ return removeScenarioItem(c, "reporter", a[0], f);
745
+ },
746
+ description: "Remove one reporter, selected by --index (from reporter-list) or --name, keeping the others.",
747
+ examples: ["dss scenario reporter-remove my_scenario --index 0",],
748
+ },
749
+ "run-as-set": {
750
+ handler: async (c, a, f) => {
751
+ requireArgs(a, 1, commandUsage("scenario", "run-as-set"));
752
+ const user = f["user"];
753
+ const clear = f["clear"] === true;
754
+ if ((typeof user === "string") === clear) {
755
+ throw new UsageError(`Pass exactly one of --user LOGIN or --clear. Usage: ${commandUsage("scenario", "run-as-set")}`);
756
+ }
757
+ const runAsUser = typeof user === "string" ? user : null;
758
+ const pk = f["project-key"];
759
+ if (executionMode(f).dryRun) {
760
+ const current = asRecord(await c.scenarios.get(a[0], { projectKey: pk, }));
761
+ return {
762
+ dryRun: true,
763
+ action: "run-as-set",
764
+ resource: "scenario",
765
+ id: a[0],
766
+ runAsUser,
767
+ currentRunAsUser: current?.["runAsUser"] ?? null,
768
+ endpoint: encodedProjectEndpoint(c, pk, `/scenarios/${encodeURIComponent(a[0])}/`),
769
+ method: "PUT",
770
+ };
771
+ }
772
+ const result = await c.scenarios.setRunAs(a[0], runAsUser, { projectKey: pk, });
773
+ return {
774
+ updated: a[0],
775
+ resource: "scenario",
776
+ part: "runAsUser",
777
+ runAsUser: result.runAsUser,
778
+ effectiveRunAs: result.effectiveRunAs,
779
+ ...(result.previousRunAsUser === undefined
780
+ ? {}
781
+ : { previousRunAsUser: result.previousRunAsUser, }),
782
+ };
783
+ },
784
+ description: "Set the user a scenario runs as (--user LOGIN, administrators only) or clear it (--clear: it then runs as its last editor). The result reports the effective run-as; scenario status shows runAsUser.",
785
+ examples: [
786
+ "dss scenario run-as-set my_scenario --user service_account",
787
+ "dss scenario run-as-set my_scenario --clear --dry-run",
788
+ ],
789
+ },
312
790
  });