dataiku-sdk 4.0.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 (38) 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 +3 -1
  4. package/dist/packages/types/src/index.js +3 -1
  5. package/dist/src/cli/command-syntax.json +216 -0
  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/job.js +12 -9
  9. package/dist/src/cli/commands/recipe.js +23 -2
  10. package/dist/src/cli/commands/scenario.js +76 -8
  11. package/dist/src/cli/contract.js +3 -2
  12. package/dist/src/cli/env.d.ts +6 -0
  13. package/dist/src/cli/env.js +8 -0
  14. package/dist/src/cli/output.d.ts +2 -0
  15. package/dist/src/cli/output.js +24 -4
  16. package/dist/src/cli/read-shapes.js +90 -0
  17. package/dist/src/cli/result-shapes.d.ts +19 -1
  18. package/dist/src/cli/result-shapes.js +227 -4
  19. package/dist/src/cli/selector-suggestions.d.ts +14 -0
  20. package/dist/src/cli/selector-suggestions.js +56 -0
  21. package/dist/src/cli/usage.d.ts +1 -1
  22. package/dist/src/cli/usage.js +29 -4
  23. package/dist/src/cli/version.d.ts +2 -2
  24. package/dist/src/cli/version.js +2 -2
  25. package/dist/src/cli.js +62 -13
  26. package/dist/src/client.d.ts +5 -4
  27. package/dist/src/client.js +24 -7
  28. package/dist/src/errors.d.ts +2 -0
  29. package/dist/src/errors.js +12 -2
  30. package/dist/src/generated/action-output-schemas.json +214 -0
  31. package/dist/src/resources/recipe-create.d.ts +4 -1
  32. package/dist/src/resources/recipe-create.js +11 -2
  33. package/dist/src/resources/sql.js +3 -3
  34. package/package.json +2 -1
  35. package/plugin.json +1 -1
  36. package/skills/dataiku-dss/SKILL.md +14 -16
  37. package/skills/dataiku-dss/references/discovery.md +2 -2
  38. package/skills/dataiku-dss/references/troubleshooting.md +1 -0
@@ -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;
@@ -23,6 +23,14 @@ export function dataikuEnvironmentEnabled() {
23
23
  export function ambientProjectKey() {
24
24
  return dataikuEnvironmentEnabled() ? process.env.DATAIKU_PROJECT_KEY : undefined;
25
25
  }
26
+ /**
27
+ * `DSS_PRETTY=1` indents stdout JSON. Agent shells that cut long lines (omp at
28
+ * 768 bytes) truncate a one-line result; indented output keeps each line short.
29
+ * Read from the process environment only, never from a `.env` file.
30
+ */
31
+ export function prettyOutputEnabled() {
32
+ return process.env.DSS_PRETTY === "1";
33
+ }
26
34
  const allowedEnvKeys = new Set(["NODE_EXTRA_CA_CERTS", "NODE_TLS_REJECT_UNAUTHORIZED",]);
27
35
  /**
28
36
  * `.env` files are untrusted input: only the CLI's own `DATAIKU_*` variables and
@@ -11,6 +11,8 @@ export declare function pickResultFields(item: unknown, fields: string[]): unkno
11
11
  * that are absent become null so every row keeps a stable, predictable shape.
12
12
  */
13
13
  export declare function projectResultFields(result: unknown, fields: string[], availableFieldNames?: () => string[]): unknown;
14
+ /** One JSON value plus newline for stdout; indented when `DSS_PRETTY=1`. */
15
+ export declare function stdoutJson(value: unknown): string;
14
16
  export declare function writeCommandResult(result: unknown): void;
15
17
  export declare function addTransientTargetContext(error: unknown, target: string, elapsedMs: number): never;
16
18
  export declare function isFailedWaitResult(result: unknown): boolean;
@@ -1,4 +1,5 @@
1
1
  import { DataikuError, } from "../errors.js";
2
+ import { prettyOutputEnabled, } from "./env.js";
2
3
  let outputFieldProjection;
3
4
  export function setOutputFieldProjection(fields) {
4
5
  outputFieldProjection = fields;
@@ -98,12 +99,16 @@ export function projectResultFields(result, fields, availableFieldNames) {
98
99
  return result.map((item) => pickResultFields(item, fields));
99
100
  return pickResultFields(result, fields);
100
101
  }
102
+ /** One JSON value plus newline for stdout; indented when `DSS_PRETTY=1`. */
103
+ export function stdoutJson(value) {
104
+ return `${JSON.stringify(value, null, prettyOutputEnabled() ? 2 : undefined)}\n`;
105
+ }
101
106
  export function writeCommandResult(result) {
102
107
  const projected = outputFieldProjection
103
108
  ? projectResultFields(result, outputFieldProjection)
104
109
  : result;
105
110
  flushCliWarnings();
106
- process.stdout.write(`${JSON.stringify(projected ?? { ok: true, })}\n`);
111
+ process.stdout.write(stdoutJson(projected ?? { ok: true, }));
107
112
  }
108
113
  export function addTransientTargetContext(error, target, elapsedMs) {
109
114
  if (error instanceof DataikuError && error.category === "transient") {
@@ -200,13 +205,28 @@ export function commandFailureMessage(result) {
200
205
  const resume = typeof record.resume === "string" ? ` Resume with: ${record.resume}` : "";
201
206
  return `Timed out waiting for job ${record.jobId} after ${record.elapsedMs}ms (last known state: ${record.state}); the job is still running in DSS and was not cancelled.${resume}`;
202
207
  }
208
+ if (record.timedOut === true) {
209
+ // ML training, scenario runs, futures, code run: the client stopped waiting. Only a
210
+ // result that says `cancelled: false` proves nothing was stopped; `code run` cleans up
211
+ // its throwaway scenario, so its `cleanup` status is what tells.
212
+ const known = typeof record.state === "string"
213
+ ? record.state
214
+ : typeof record.outcome === "string"
215
+ ? record.outcome
216
+ : undefined;
217
+ const elapsed = typeof record.elapsedMs === "number" ? ` after ${record.elapsedMs}ms` : "";
218
+ const after = record.cancelled === false
219
+ ? "the operation was not cancelled and may still be running in DSS."
220
+ : record.cleanup !== undefined
221
+ ? "the client stopped waiting; see `cleanup` for what happened to the run."
222
+ : "the client stopped waiting; the operation may still be running in DSS.";
223
+ return `Timed out waiting${elapsed}${known ? ` (last known state: ${known})` : ""}; ${after}`;
224
+ }
203
225
  const state = typeof record.state === "string"
204
226
  ? record.state
205
227
  : typeof record.outcome === "string"
206
228
  ? record.outcome
207
- : typeof record.timedOut === "boolean"
208
- ? "timed out"
209
- : undefined;
229
+ : undefined;
210
230
  return `Command completed with failed long-running result${state ? `: ${state}` : ""}.`;
211
231
  }
212
232
  if (isAssertionFailureResult(result)) {
@@ -32,6 +32,28 @@ export function compactReadOutputSchemas() {
32
32
  additionalProperties: false,
33
33
  properties: { type: STRING, message: STRING, },
34
34
  };
35
+ const triggerSchema = {
36
+ type: "object",
37
+ additionalProperties: false,
38
+ properties: { type: STRING, name: STRING, },
39
+ };
40
+ const failureSchema = {
41
+ type: "object",
42
+ additionalProperties: false,
43
+ properties: {
44
+ stepId: STRING,
45
+ jobId: STRING,
46
+ dataset: STRING,
47
+ recipe: STRING,
48
+ inputs: { type: "array", items: { type: "string", }, },
49
+ message: STRING,
50
+ },
51
+ };
52
+ const nextSchema = {
53
+ type: "array",
54
+ description: "Next commands to run, each as an argv array.",
55
+ items: { type: "array", items: STRING, },
56
+ };
35
57
  return {
36
58
  "job.get": {
37
59
  type: "object",
@@ -110,5 +132,73 @@ export function compactReadOutputSchemas() {
110
132
  lastModifiedBy: STRING,
111
133
  },
112
134
  },
135
+ "scenario.get-run": {
136
+ type: "object",
137
+ additionalProperties: false,
138
+ required: ["runId", "steps",],
139
+ properties: {
140
+ runId: STRING,
141
+ scenarioId: STRING,
142
+ projectKey: STRING,
143
+ outcome: STRING,
144
+ start: NUMBER,
145
+ end: NUMBER,
146
+ durationMs: NUMBER,
147
+ trigger: triggerSchema,
148
+ steps: {
149
+ type: "array",
150
+ items: {
151
+ type: "object",
152
+ additionalProperties: false,
153
+ properties: {
154
+ id: STRING,
155
+ name: STRING,
156
+ type: STRING,
157
+ outcome: STRING,
158
+ message: STRING,
159
+ datasets: {
160
+ type: "array",
161
+ items: {
162
+ type: "object",
163
+ additionalProperties: false,
164
+ properties: { dataset: STRING, projectKey: STRING, partition: STRING, outcome: STRING, },
165
+ },
166
+ },
167
+ jobs: {
168
+ type: "array",
169
+ items: {
170
+ type: "object",
171
+ additionalProperties: false,
172
+ properties: { jobId: STRING, outcome: STRING, message: STRING, },
173
+ },
174
+ },
175
+ },
176
+ },
177
+ },
178
+ failure: failureSchema,
179
+ next: nextSchema,
180
+ hint: {
181
+ type: "string",
182
+ description: "Edit-and-rerun commands when the failed recipe is known.",
183
+ },
184
+ },
185
+ },
186
+ "scenario.last-runs": {
187
+ type: "array",
188
+ items: {
189
+ type: "object",
190
+ additionalProperties: false,
191
+ required: ["runId",],
192
+ properties: {
193
+ runId: STRING,
194
+ outcome: STRING,
195
+ start: NUMBER,
196
+ end: NUMBER,
197
+ trigger: triggerSchema,
198
+ failure: { type: "object", additionalProperties: false, properties: { message: STRING, }, },
199
+ next: nextSchema,
200
+ },
201
+ },
202
+ },
113
203
  };
114
204
  }
@@ -1,6 +1,24 @@
1
+ type Item = Record<string, unknown>;
2
+ /** The failed activity of a job: the recipe that failed and the datasets it read. */
3
+ export interface FailedActivity {
4
+ recipe: string;
5
+ inputs: string[];
6
+ }
7
+ /**
8
+ * Failure diagnosis of a raw `scenario get-run` report: the first failure
9
+ * (`{ stepId, jobId, dataset, message }`) and the next command(s) to run as
10
+ * argv arrays. With the failed recipe known, `hint` names the edit-and-rerun
11
+ * loop, so the agent does not look up the write commands. Empty when the run did not fail.
12
+ */
13
+ export declare function scenarioRunDiagnosis(details: unknown, activity?: FailedActivity): {
14
+ failure?: Item;
15
+ next?: string[][];
16
+ hint?: string;
17
+ };
1
18
  /**
2
19
  * Compact a command result for agents: `*.list` items (filter, cap, compact
3
- * fields) and the four large read commands. `--full` and `--fields` bypass
20
+ * fields) and the large read commands. `--full` and `--fields` bypass
4
21
  * the read shapers so the DSS object (or a projection of it) is returned.
5
22
  */
6
23
  export declare function shapeCommandResult(resource: string, action: string, result: unknown, flags: Record<string, string | boolean>): unknown;
24
+ export {};
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Compact default results of the four large read commands (`job get`,
3
- * `dataset info`, `dataset get`, `project get`), plus the single dispatch
4
- * point for compacting command results (`shapeCommandResult`).
2
+ * Compact default results of the large read commands (`job get`,
3
+ * `dataset info`, `dataset get`, `project get`, `scenario get-run`,
4
+ * `scenario last-runs`), plus the single dispatch point for compacting
5
+ * command results (`shapeCommandResult`).
5
6
  *
6
7
  * The SDK resources keep returning the raw DSS objects; only the CLI compacts.
7
8
  * `--full` returns the DSS object unchanged, and `--fields` projects DSS paths
@@ -213,16 +214,238 @@ function shapeProjectGet(result) {
213
214
  const pruned = record(pruneEmpty(settings));
214
215
  return { ...pruned, deniedPermissions, };
215
216
  }
217
+ /** Scenario/step/job outcomes that count as a failure worth diagnosing. */
218
+ const FAILED_OUTCOMES = { FAILED: true, ABORTED: true, };
219
+ function isFailedOutcome(value) {
220
+ return typeof value === "string" && FAILED_OUTCOMES[value] === true;
221
+ }
222
+ /** Message of a DSS `thrown` exception object (`{ clazz, message, ... }`). */
223
+ function thrownMessage(value) {
224
+ const thrown = record(value);
225
+ return nonEmptyString(thrown["message"]) ?? nonEmptyString(thrown["detailedMessage"]);
226
+ }
227
+ function reportItems(stepRun, type) {
228
+ const items = stepRun["additionalReportItems"];
229
+ return Array.isArray(items)
230
+ ? items.filter((item) => isRecord(item) && item["type"] === type)
231
+ : [];
232
+ }
233
+ /**
234
+ * One `stepRuns[]` entry: the step identity and outcome, the datasets it built
235
+ * (`BUILT_DATASET` report items) and the jobs it ran (`JOB_EXECUTED`, with the
236
+ * thrown message). The job log tail DSS embeds in the report is dropped.
237
+ */
238
+ function shapeScenarioStep(stepRun, runProjectKey) {
239
+ const fields = record(stepRun);
240
+ const step = record(fields["step"]);
241
+ const result = record(fields["result"]);
242
+ const datasets = reportItems(fields, "BUILT_DATASET").map((item) => {
243
+ const target = record(item["target"]);
244
+ const partition = target["partition"];
245
+ return defined({
246
+ dataset: target["datasetName"],
247
+ projectKey: typeof target["projectKey"] === "string" && target["projectKey"] !== runProjectKey
248
+ ? target["projectKey"]
249
+ : undefined,
250
+ partition: typeof partition === "string" && partition !== "NP" ? partition : undefined,
251
+ outcome: item["outcome"],
252
+ });
253
+ });
254
+ const jobs = reportItems(fields, "JOB_EXECUTED").map((item) => defined({
255
+ jobId: item["jobId"],
256
+ outcome: item["outcome"],
257
+ message: thrownMessage(item["thrown"]),
258
+ }));
259
+ return defined({
260
+ id: step["id"],
261
+ name: step["name"],
262
+ type: step["type"],
263
+ outcome: result["outcome"],
264
+ message: thrownMessage(result["thrown"]),
265
+ datasets: datasets.length > 0 ? datasets : undefined,
266
+ jobs: jobs.length > 0 ? jobs : undefined,
267
+ });
268
+ }
269
+ /**
270
+ * First failure of a shaped run: the failed step, its failed job and dataset,
271
+ * and the most specific message (job, then step, then scenario).
272
+ */
273
+ function firstScenarioFailure(steps, runThrown) {
274
+ for (const step of steps) {
275
+ const jobs = step["jobs"] ?? [];
276
+ const job = jobs.find((entry) => isFailedOutcome(entry["outcome"]));
277
+ if (!isFailedOutcome(step["outcome"]) && !job)
278
+ continue;
279
+ const datasets = step["datasets"] ?? [];
280
+ const dataset = datasets.find((entry) => isFailedOutcome(entry["outcome"]));
281
+ return defined({
282
+ stepId: step["id"],
283
+ jobId: job?.["jobId"],
284
+ dataset: dataset?.["dataset"],
285
+ message: nonEmptyString(job?.["message"]) ?? nonEmptyString(step["message"])
286
+ ?? thrownMessage(runThrown),
287
+ });
288
+ }
289
+ const message = thrownMessage(runThrown);
290
+ return message === undefined ? undefined : { message, };
291
+ }
292
+ function projectKeyArgs(projectKey) {
293
+ return typeof projectKey === "string" && projectKey.length > 0
294
+ ? ["--project-key", projectKey,]
295
+ : [];
296
+ }
297
+ /**
298
+ * Next-step argv for a failure. With the failed recipe known: its code and the
299
+ * schemas of its inputs (what a "column not found" or type error needs), then
300
+ * the job's error log; else the job's error log, else the scenario run log.
301
+ */
302
+ function failureNextSteps(failure, scenarioId, runId, projectKey, activity) {
303
+ const pk = projectKeyArgs(projectKey);
304
+ const inspect = activity
305
+ ? [
306
+ ["dss", "recipe", "cat", activity.recipe, ...pk,],
307
+ ...activity.inputs.slice(0, 3).map((input) => ["dss", "dataset", "schema", input, ...pk,]),
308
+ ]
309
+ : [];
310
+ if (typeof failure["jobId"] === "string") {
311
+ return [...inspect, ["dss", "job", "log", failure["jobId"], "--errors-only", ...pk,],];
312
+ }
313
+ if (typeof scenarioId !== "string" || typeof runId !== "string")
314
+ return undefined;
315
+ const stepArgs = typeof failure["stepId"] === "string" ? ["--step-id", failure["stepId"],] : [];
316
+ return [["dss", "scenario", "log", scenarioId, runId, ...stepArgs, ...pk,],];
317
+ }
318
+ /** Scenario identity of a run object (`scenarioRun` or a last-runs item). */
319
+ function runScenarioIdentity(run) {
320
+ const scenario = record(run["scenario"]);
321
+ const target = record(record(run["result"])["target"]);
322
+ return {
323
+ scenarioId: scenario["id"] ?? target["scenarioId"],
324
+ projectKey: scenario["projectKey"] ?? target["projectKey"],
325
+ };
326
+ }
327
+ function runTrigger(run) {
328
+ const trigger = record(record(run["trigger"])["trigger"]);
329
+ const shaped = defined({
330
+ type: nonEmptyString(trigger["type"]),
331
+ name: nonEmptyString(trigger["name"]),
332
+ });
333
+ return Object.keys(shaped).length > 0 ? shaped : undefined;
334
+ }
335
+ /**
336
+ * Failure diagnosis of a raw `scenario get-run` report: the first failure
337
+ * (`{ stepId, jobId, dataset, message }`) and the next command(s) to run as
338
+ * argv arrays. With the failed recipe known, `hint` names the edit-and-rerun
339
+ * loop, so the agent does not look up the write commands. Empty when the run did not fail.
340
+ */
341
+ export function scenarioRunDiagnosis(details, activity) {
342
+ const fields = record(details);
343
+ const run = record(fields["scenarioRun"]);
344
+ const result = record(run["result"]);
345
+ const { scenarioId, projectKey, } = runScenarioIdentity(run);
346
+ const steps = Array.isArray(fields["stepRuns"])
347
+ ? fields["stepRuns"].map((stepRun) => shapeScenarioStep(stepRun, projectKey))
348
+ : [];
349
+ const failed = isFailedOutcome(result["outcome"])
350
+ || steps.some((step) => isFailedOutcome(step["outcome"]));
351
+ const failure = failed ? firstScenarioFailure(steps, result["thrown"]) : undefined;
352
+ if (!failure)
353
+ return {};
354
+ if (!activity) {
355
+ const next = failureNextSteps(failure, scenarioId, run["runId"], projectKey, undefined);
356
+ return next ? { failure, next, } : { failure, };
357
+ }
358
+ const located = { ...failure, recipe: activity.recipe, inputs: activity.inputs, };
359
+ const next = failureNextSteps(located, scenarioId, run["runId"], projectKey, activity);
360
+ const pk = projectKeyArgs(projectKey).join(" ");
361
+ const suffix = pk === "" ? "" : ` ${pk}`;
362
+ const rerun = typeof scenarioId === "string"
363
+ ? `; rerun: dss scenario run-and-wait ${scenarioId}${suffix}`
364
+ : "";
365
+ const hint = `To fix: dss recipe get-payload ${activity.recipe} --output FILE${suffix}, edit FILE, `
366
+ + `dss recipe set-payload ${activity.recipe} --file FILE${suffix}${rerun}.`;
367
+ return next ? { failure: located, next, hint, } : { failure: located, hint, };
368
+ }
369
+ /**
370
+ * `scenario get-run`: run identity, outcome, timing, trigger, per-step
371
+ * datasets/jobs, the first failure, and next-step argv. Never the trigger
372
+ * auth context, run-as user, API key metadata, or job log tails.
373
+ */
374
+ function shapeScenarioGetRun(result) {
375
+ if (!isRecord(result) || !isRecord(result["scenarioRun"]))
376
+ return result;
377
+ const run = result["scenarioRun"];
378
+ const runResult = record(run["result"]);
379
+ const { scenarioId, projectKey, } = runScenarioIdentity(run);
380
+ const start = typeof run["start"] === "number" ? run["start"] : undefined;
381
+ const end = typeof run["end"] === "number" ? run["end"] : undefined;
382
+ const steps = Array.isArray(result["stepRuns"])
383
+ ? result["stepRuns"].map((stepRun) => shapeScenarioStep(stepRun, projectKey))
384
+ : [];
385
+ const { failure, next, } = scenarioRunDiagnosis(result);
386
+ return defined({
387
+ runId: run["runId"],
388
+ scenarioId,
389
+ projectKey,
390
+ outcome: runResult["outcome"],
391
+ start,
392
+ end,
393
+ durationMs: start !== undefined && end !== undefined && end >= start ? end - start : undefined,
394
+ trigger: runTrigger(run),
395
+ steps,
396
+ failure,
397
+ next,
398
+ });
399
+ }
400
+ /**
401
+ * One `scenario last-runs` item: identity, outcome, timing, trigger, failure
402
+ * message, and for a failed run the `scenario get-run` argv that shows why.
403
+ */
404
+ function shapeScenarioRunSummary(item) {
405
+ if (!isRecord(item))
406
+ return item;
407
+ const result = record(item["result"]);
408
+ const message = thrownMessage(result["thrown"]);
409
+ const { scenarioId, projectKey, } = runScenarioIdentity(item);
410
+ const next = isFailedOutcome(result["outcome"]) && typeof scenarioId === "string"
411
+ && typeof item["runId"] === "string"
412
+ ? [[
413
+ "dss",
414
+ "scenario",
415
+ "get-run",
416
+ scenarioId,
417
+ item["runId"],
418
+ ...(typeof projectKey === "string" ? ["--project-key", projectKey,] : []),
419
+ ],]
420
+ : undefined;
421
+ // DSS lists an in-progress run with `end: 0` and no result outcome.
422
+ const running = result["outcome"] === undefined && item["end"] === 0;
423
+ return defined({
424
+ runId: item["runId"],
425
+ outcome: running ? "RUNNING" : result["outcome"],
426
+ start: item["start"] ?? result["start"],
427
+ end: item["end"] ?? result["end"],
428
+ trigger: runTrigger(item),
429
+ failure: message === undefined ? undefined : { message, },
430
+ next,
431
+ });
432
+ }
433
+ /** `scenario last-runs`: one small object per run, newest first as DSS returns them. */
434
+ function shapeScenarioLastRuns(result) {
435
+ return Array.isArray(result) ? result.map(shapeScenarioRunSummary) : result;
436
+ }
216
437
  /** Compact read shapers by `resource.action`. */
217
438
  const READ_SHAPERS = {
218
439
  "job.get": shapeJobGet,
219
440
  "dataset.info": shapeDatasetInfo,
220
441
  "dataset.get": shapeDatasetGet,
221
442
  "project.get": shapeProjectGet,
443
+ "scenario.get-run": shapeScenarioGetRun,
444
+ "scenario.last-runs": shapeScenarioLastRuns,
222
445
  };
223
446
  /**
224
447
  * Compact a command result for agents: `*.list` items (filter, cap, compact
225
- * fields) and the four large read commands. `--full` and `--fields` bypass
448
+ * fields) and the large read commands. `--full` and `--fields` bypass
226
449
  * the read shapers so the DSS object (or a projection of it) is returned.
227
450
  */
228
451
  export function shapeCommandResult(resource, action, result, flags) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Did-you-mean suggestions for `commands run --fields RESOURCE[.ACTION[.FIELD...]]`
3
+ * selectors that name no resource or action. Pure data in, strings out.
4
+ */
5
+ /**
6
+ * Selectors worth trying instead of an unknown `resource` / `resource.action`
7
+ * selector, keeping any trailing `.field` path. For a known resource: the same
8
+ * action name under other resources (`flow.map.usage` -> `project.map.usage`),
9
+ * then close action names of that resource. For an unknown resource: close
10
+ * resource names (keeping the action), else the action under other resources.
11
+ *
12
+ * @param actionsByResource resource -> action names (`commandActionSummary()`).
13
+ */
14
+ export declare function suggestSelectors(selector: string, actionsByResource: Record<string, string[]>): string[];
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Did-you-mean suggestions for `commands run --fields RESOURCE[.ACTION[.FIELD...]]`
3
+ * selectors that name no resource or action. Pure data in, strings out.
4
+ */
5
+ const MAX_SUGGESTIONS = 4;
6
+ /** Levenshtein distance with a single rolling row. */
7
+ function editDistance(a, b) {
8
+ let previous = Array.from({ length: b.length + 1, }, (_, index) => index);
9
+ for (let i = 1; i <= a.length; i++) {
10
+ const current = [i,];
11
+ for (let j = 1; j <= b.length; j++) {
12
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
13
+ current.push(Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost));
14
+ }
15
+ previous = current;
16
+ }
17
+ return previous[b.length];
18
+ }
19
+ /** Names close to `name`: a typo (distance <= 2, <= 1 for short names) or one containing it. */
20
+ function closeNames(name, candidates) {
21
+ const maxDistance = name.length >= 5 ? 2 : 1;
22
+ return candidates.filter((candidate) => candidate !== name
23
+ && (editDistance(name, candidate) <= maxDistance
24
+ || (name.length >= 3 && candidate.includes(name))));
25
+ }
26
+ /**
27
+ * Selectors worth trying instead of an unknown `resource` / `resource.action`
28
+ * selector, keeping any trailing `.field` path. For a known resource: the same
29
+ * action name under other resources (`flow.map.usage` -> `project.map.usage`),
30
+ * then close action names of that resource. For an unknown resource: close
31
+ * resource names (keeping the action), else the action under other resources.
32
+ *
33
+ * @param actionsByResource resource -> action names (`commandActionSummary()`).
34
+ */
35
+ export function suggestSelectors(selector, actionsByResource) {
36
+ const [resource = "", action, ...rest] = selector.split(".");
37
+ const suffix = rest.length > 0 ? `.${rest.join(".")}` : "";
38
+ const resources = Object.keys(actionsByResource);
39
+ const sameActionElsewhere = action === undefined
40
+ ? []
41
+ : resources.filter((other) => other !== resource && actionsByResource[other].includes(action))
42
+ .map((other) => `${other}.${action}${suffix}`);
43
+ let suggestions;
44
+ if (Object.hasOwn(actionsByResource, resource)) {
45
+ const closeActions = action === undefined
46
+ ? []
47
+ : closeNames(action, actionsByResource[resource]).map((close) => `${resource}.${close}${suffix}`);
48
+ suggestions = [...sameActionElsewhere, ...closeActions,];
49
+ }
50
+ else {
51
+ const tail = action === undefined ? "" : `.${[action, ...rest,].join(".")}`;
52
+ const closeResources = closeNames(resource, resources).map((close) => `${close}${tail}`);
53
+ suggestions = closeResources.length > 0 ? closeResources : sameActionElsewhere;
54
+ }
55
+ return [...new Set(suggestions),].slice(0, MAX_SUGGESTIONS);
56
+ }
@@ -11,7 +11,7 @@ export declare const COMMANDS_RUN_HINT = "Use `dss commands run --fields RESOURC
11
11
  export declare function unsupportedHelpFlag(): UsageError;
12
12
  export declare function noCommandError(): UsageError;
13
13
  export declare function missingActionError(resource: string, validActions: string[], usage?: string): UsageError;
14
- export declare function unknownResourceError(resource: string): UsageError;
14
+ export declare function unknownResourceError(resource: string, action?: string): UsageError;
15
15
  /**
16
16
  * Agents rarely mistype; they reuse plausible flags from other commands or
17
17
  * CLIs. When the command is known, the hint is its usage line, which lists
@@ -1,4 +1,23 @@
1
- import { commandSyntaxTree, renderNodes, syntaxHasFlag, } from "./syntax.js";
1
+ import { suggestSelectors, } from "./selector-suggestions.js";
2
+ import { commandSyntaxKeys, commandSyntaxTree, renderNodes, syntaxHasFlag, } from "./syntax.js";
3
+ /**
4
+ * `dss resource action` commands close to a mistyped one (`flow map` -> `project map`,
5
+ * `recipes list` -> `recipe list`), from the syntax tree, so the error names the fix.
6
+ */
7
+ function commandSuggestions(resource, action) {
8
+ const actionsByResource = {};
9
+ for (const key of commandSyntaxKeys()) {
10
+ const dot = key.indexOf(".");
11
+ (actionsByResource[key.slice(0, dot)] ??= []).push(key.slice(dot + 1));
12
+ }
13
+ return suggestSelectors(action ? `${resource}.${action}` : resource, actionsByResource)
14
+ .map((selector) => `dss ${selector.replace(".", " ")}`);
15
+ }
16
+ function suggestionHint(suggestions) {
17
+ return suggestions.length > 0
18
+ ? `Did you mean ${suggestions.map((command) => `\`${command}\``).join(" or ")}? ${COMMANDS_RUN_HINT}`
19
+ : COMMANDS_RUN_HINT;
20
+ }
2
21
  export const RESOURCE_NAMES = [
3
22
  "agent",
4
23
  "app",
@@ -122,8 +141,13 @@ export function noCommandError() {
122
141
  export function missingActionError(resource, validActions, usage) {
123
142
  return new UsageError(`Missing action for ${resource}.`, "usage_error", usage ?? COMMANDS_RUN_HINT, { resource, validActions, });
124
143
  }
125
- export function unknownResourceError(resource) {
126
- return new UsageError(`Unknown resource: ${resource}.`, "usage_error", COMMANDS_RUN_HINT, { resource, validResources: RESOURCE_NAMES, });
144
+ export function unknownResourceError(resource, action) {
145
+ const suggestions = commandSuggestions(resource, action);
146
+ return new UsageError(`Unknown resource: ${resource}.`, "usage_error", suggestionHint(suggestions), {
147
+ resource,
148
+ validResources: RESOURCE_NAMES,
149
+ ...(suggestions.length > 0 ? { suggestions, } : {}),
150
+ });
127
151
  }
128
152
  /**
129
153
  * Agents rarely mistype; they reuse plausible flags from other commands or
@@ -141,7 +165,8 @@ export function unknownFlagError(flagLabel, resource, action) {
141
165
  return new UsageError(message, "unknown_flag", command ? `Usage: ${renderNodes(command)}` : undefined);
142
166
  }
143
167
  export function unknownActionError(resource, action, validActions, hint) {
144
- return new UsageError(`Unknown action: ${resource} ${action ?? ""}`.trim(), "usage_error", hint ?? COMMANDS_RUN_HINT, { resource, action, validActions, });
168
+ const suggestions = commandSuggestions(resource, action);
169
+ return new UsageError(`Unknown action: ${resource} ${action ?? ""}`.trim(), "usage_error", hint ?? suggestionHint(suggestions), { resource, action, validActions, ...(suggestions.length > 0 ? { suggestions, } : {}), });
145
170
  }
146
171
  export function requireArgs(args, count, usage) {
147
172
  if (args.length < count) {
@@ -49,7 +49,7 @@ export declare function buildVersionPayload(input: {
49
49
  export declare const PACKAGE_ROOT: string | undefined;
50
50
  export declare const CLI_VERSION: string;
51
51
  export declare const CLI_GIT_REVISION: string | undefined;
52
- export declare const AGENT_CONTRACT_VERSION = 5;
52
+ export declare const AGENT_CONTRACT_VERSION = 6;
53
53
  export declare const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
54
- export declare const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v5.json";
54
+ export declare const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v6.json";
55
55
  export declare function cliVersionResult(): CliVersionPayload;
@@ -203,9 +203,9 @@ export function buildVersionPayload(input) {
203
203
  export const PACKAGE_ROOT = findPackageRoot();
204
204
  export const CLI_VERSION = packageVersion(PACKAGE_ROOT);
205
205
  export const CLI_GIT_REVISION = gitRevision(PACKAGE_ROOT);
206
- export const AGENT_CONTRACT_VERSION = 5;
206
+ export const AGENT_CONTRACT_VERSION = 6;
207
207
  export const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
208
- export const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v5.json";
208
+ export const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v6.json";
209
209
  export function cliVersionResult() {
210
210
  // Only code actually loaded from dist/ may report dist provenance; an
211
211
  // inherited DSS_LOAD_SOURCE on a source run is ignored.