dataiku-sdk 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -3,17 +3,15 @@ import { createHash, } from "node:crypto";
3
3
  import { readFileSync, } from "node:fs";
4
4
  import { mkdir, writeFile, } from "node:fs/promises";
5
5
  import { dirname, join, resolve, } from "node:path";
6
- import { createInterface, } from "node:readline";
7
- import { Writable, } from "node:stream";
8
6
  import { fileURLToPath, } from "node:url";
9
7
  import { validateCredentials, } from "./auth.js";
10
8
  import { DataikuClient, } from "./client.js";
11
- import { deleteCredentials, getCredentialsPath, loadCredentials, maskApiKey, saveCredentials, } from "./config.js";
9
+ import { getCredentialsPath, loadCredentials, saveCredentials, } from "./config.js";
12
10
  import { DataikuError, dataikuErrorCode, } from "./errors.js";
13
11
  import { buildDatasetCloneSettings, } from "./resources/datasets.js";
14
12
  import { parseJobLogProgress, } from "./resources/jobs.js";
15
13
  import { scenarioUpdatePreview, } from "./resources/scenarios.js";
16
- import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, } from "./skill.js";
14
+ import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, planSkillInstalls, } from "./skill.js";
17
15
  import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
18
16
  import { deepMerge, } from "./utils/deep-merge.js";
19
17
  import { sanitizeFileName, } from "./utils/sanitize.js";
@@ -73,10 +71,10 @@ function gitRevision(packageRoot) {
73
71
  }
74
72
  const PACKAGE_ROOT = findPackageRoot();
75
73
  const CLI_VERSION = packageVersion(PACKAGE_ROOT);
76
- const CLI_VERSION_LABEL = (() => {
77
- const revision = gitRevision(PACKAGE_ROOT);
78
- return revision ? `${CLI_VERSION}+g${revision}` : CLI_VERSION;
79
- })();
74
+ const CLI_GIT_REVISION = gitRevision(PACKAGE_ROOT);
75
+ function cliVersionResult() {
76
+ return { version: CLI_VERSION, gitRevision: CLI_GIT_REVISION ?? null, };
77
+ }
80
78
  function num(v) {
81
79
  if (typeof v !== "string")
82
80
  return undefined;
@@ -883,7 +881,24 @@ function json(v, source = "JSON flag") {
883
881
  return undefined;
884
882
  return parseJsonObject(v, source);
885
883
  }
886
- const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--output PATH|--output-file PATH] [--request-timeout MS] [--project-key KEY]";
884
+ const SQL_QUERY_USAGE = "dss sql query [SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin] (--connection CONN | --dataset FULL_NAME) [--database DB] [--output PATH|--output-file PATH] [--preview N] [--request-timeout MS] [--project-key KEY]";
885
+ const DEFAULT_SQL_PREVIEW_ROWS = 5;
886
+ /**
887
+ * Parse `--preview N` into a non-negative row count. Rejects non-integers,
888
+ * negatives, and empty values loudly so a bad flag never silently degrades to a
889
+ * default. `--preview 0` is valid and yields an empty preview (explicit opt-out).
890
+ */
891
+ function parseSqlPreviewCount(value) {
892
+ if (typeof value !== "string") {
893
+ throw new UsageError(`--preview requires an integer value. Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
894
+ }
895
+ const trimmed = value.trim();
896
+ const parsed = Number(trimmed);
897
+ if (trimmed.length === 0 || !Number.isInteger(parsed) || parsed < 0) {
898
+ throw new UsageError(`--preview must be a non-negative integer (got "${value}"). Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
899
+ }
900
+ return parsed;
901
+ }
887
902
  function readStdinText() {
888
903
  return readFileSync(0, "utf-8");
889
904
  }
@@ -1049,6 +1064,30 @@ function resolveSqlInput(args, flags) {
1049
1064
  }
1050
1065
  return query;
1051
1066
  }
1067
+ const CODE_RUN_USAGE = "dss code run (--file PATH | --stdin) [--env ENV] [--timeout MS] [--keep] [--full-log] [--project-key KEY]";
1068
+ function resolveCodeInput(args, flags) {
1069
+ if (args.length > 0) {
1070
+ throw new UsageError(`code run takes no positional arguments; pass the script via --file PATH or --stdin. Usage: ${CODE_RUN_USAGE}`);
1071
+ }
1072
+ const sources = [];
1073
+ if (typeof flags["file"] === "string") {
1074
+ sources.push({ label: "--file", read: () => readFileSync(flags["file"], "utf-8"), });
1075
+ }
1076
+ if (flags["stdin"] === true) {
1077
+ sources.push({ label: "--stdin", read: readStdinText, });
1078
+ }
1079
+ if (sources.length === 0) {
1080
+ throw new UsageError(`Python source is required: pass --file PATH or --stdin. Usage: ${CODE_RUN_USAGE}`);
1081
+ }
1082
+ if (sources.length > 1) {
1083
+ throw new UsageError(`Choose exactly one Python source: --file or --stdin. Usage: ${CODE_RUN_USAGE}`);
1084
+ }
1085
+ const script = stripUtf8Bom(sources[0].read());
1086
+ if (script.trim().length === 0) {
1087
+ throw new UsageError(`Python source from ${sources[0].label} must not be empty. Usage: ${CODE_RUN_USAGE}`);
1088
+ }
1089
+ return script;
1090
+ }
1052
1091
  async function resolveFolderId(client, nameOrId, flags) {
1053
1092
  return client.folders.resolveId(nameOrId, flags["project-key"]);
1054
1093
  }
@@ -1079,8 +1118,58 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
1079
1118
  }
1080
1119
  return lines.join("\n");
1081
1120
  }
1121
+ let outputFieldProjection;
1122
+ /**
1123
+ * Non-fatal diagnostics queued during a command and flushed to stderr as one
1124
+ * JSON envelope, so agents get a loud, machine-parseable signal (e.g. truncated
1125
+ * exports) without polluting the stdout result contract.
1126
+ */
1127
+ let pendingCliWarnings = [];
1128
+ function enqueueCliWarning(warning) {
1129
+ pendingCliWarnings.push(warning);
1130
+ }
1131
+ /** Emit queued warnings as a single `{ warnings: [...] }` object on stderr. Idempotent. */
1132
+ function flushCliWarnings() {
1133
+ if (pendingCliWarnings.length === 0)
1134
+ return;
1135
+ const warnings = pendingCliWarnings;
1136
+ pendingCliWarnings = [];
1137
+ process.stderr.write(`${JSON.stringify({ warnings, }, null, 2)}\n`);
1138
+ }
1139
+ function resolveFieldPath(source, field) {
1140
+ let current = source;
1141
+ for (const segment of field.split(".")) {
1142
+ if (current === null || typeof current !== "object" || Array.isArray(current))
1143
+ return null;
1144
+ current = current[segment];
1145
+ }
1146
+ return current ?? null;
1147
+ }
1148
+ function pickResultFields(item, fields) {
1149
+ if (!item || typeof item !== "object" || Array.isArray(item))
1150
+ return item;
1151
+ const source = item;
1152
+ const picked = {};
1153
+ for (const field of fields)
1154
+ picked[field] = resolveFieldPath(source, field);
1155
+ return picked;
1156
+ }
1157
+ /**
1158
+ * Project the top-level fields callers asked for via --fields. Arrays are mapped
1159
+ * element-wise; scalars and string results pass through untouched. Requested keys
1160
+ * that are absent become null so every row keeps a stable, predictable shape.
1161
+ */
1162
+ function projectResultFields(result, fields) {
1163
+ if (Array.isArray(result))
1164
+ return result.map((item) => pickResultFields(item, fields));
1165
+ return pickResultFields(result, fields);
1166
+ }
1082
1167
  function writeCommandResult(result) {
1083
- process.stdout.write(`${JSON.stringify(result ?? { ok: true, }, null, 2)}\n`);
1168
+ flushCliWarnings();
1169
+ const projected = outputFieldProjection
1170
+ ? projectResultFields(result, outputFieldProjection)
1171
+ : result;
1172
+ process.stdout.write(`${JSON.stringify(projected ?? { ok: true, }, null, 2)}\n`);
1084
1173
  }
1085
1174
  function transientBodyWithTargetContext(body, target, elapsedMs) {
1086
1175
  try {
@@ -1100,7 +1189,7 @@ function transientBodyWithTargetContext(body, target, elapsedMs) {
1100
1189
  }
1101
1190
  function addTransientTargetContext(error, target, elapsedMs) {
1102
1191
  if (error instanceof DataikuError && error.category === "transient") {
1103
- throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry);
1192
+ throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry, error.requestId);
1104
1193
  }
1105
1194
  throw error;
1106
1195
  }
@@ -1120,6 +1209,27 @@ function commandFailureExitCode(result) {
1120
1209
  return 4;
1121
1210
  return undefined;
1122
1211
  }
1212
+ class CommandResultFailure extends Error {
1213
+ result;
1214
+ exitCode;
1215
+ constructor(result, exitCode) {
1216
+ super(commandFailureMessage(result));
1217
+ this.name = "CommandResultFailure";
1218
+ this.result = result;
1219
+ this.exitCode = exitCode;
1220
+ }
1221
+ }
1222
+ function commandFailureMessage(result) {
1223
+ if (isFailedWaitResult(result)) {
1224
+ const record = result;
1225
+ const state = typeof record.state === "string" ? record.state : record.outcome;
1226
+ return `Command completed with failed long-running result${state ? `: ${state}` : ""}.`;
1227
+ }
1228
+ if (result && typeof result === "object" && result.unchanged === false) {
1229
+ return "Command completed with failed assertion result.";
1230
+ }
1231
+ return "Command completed with failed result.";
1232
+ }
1123
1233
  function isNotFoundError(error) {
1124
1234
  if (error instanceof DataikuError)
1125
1235
  return error.category === "not_found";
@@ -1140,6 +1250,22 @@ async function readIfExists(reader) {
1140
1250
  function skipResult(resource, id, reason, extra = {}) {
1141
1251
  return { skipped: id, reason, resource, ...extra, };
1142
1252
  }
1253
+ function recipeRoleInputItems(recipe, role) {
1254
+ const inputs = recipe["inputs"];
1255
+ if (!inputs || typeof inputs !== "object")
1256
+ return [];
1257
+ const roleEntry = inputs[role];
1258
+ if (!roleEntry || typeof roleEntry !== "object")
1259
+ return [];
1260
+ const items = roleEntry["items"];
1261
+ return Array.isArray(items) ? items : [];
1262
+ }
1263
+ function recipeInputItemRef(item) {
1264
+ if (!item || typeof item !== "object")
1265
+ return undefined;
1266
+ const ref = item["ref"];
1267
+ return typeof ref === "string" && ref.length > 0 ? ref : undefined;
1268
+ }
1143
1269
  function planResult(resource, action, options) {
1144
1270
  return {
1145
1271
  plan: true,
@@ -1337,7 +1463,6 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
1337
1463
  // Arg parsing
1338
1464
  // ---------------------------------------------------------------------------
1339
1465
  const BOOLEAN_FLAGS = new Set([
1340
- "help",
1341
1466
  "verbose",
1342
1467
  "version",
1343
1468
  "stdin",
@@ -1361,7 +1486,6 @@ const BOOLEAN_FLAGS = new Set([
1361
1486
  "if-not-exists",
1362
1487
  "if-exists",
1363
1488
  "json",
1364
- "report-json",
1365
1489
  "no-wait",
1366
1490
  "force-rebuild",
1367
1491
  "latest",
@@ -1372,9 +1496,11 @@ const BOOLEAN_FLAGS = new Set([
1372
1496
  "allow-same-path",
1373
1497
  "sync",
1374
1498
  "validate-objects",
1499
+ "errors-only",
1500
+ "keep",
1501
+ "full-log",
1375
1502
  ]);
1376
1503
  const SHORT_FLAGS = {
1377
- h: "help",
1378
1504
  v: "verbose",
1379
1505
  V: "version",
1380
1506
  o: "output",
@@ -1387,8 +1513,10 @@ const FLAG_ALIASES = {
1387
1513
  "extra-ca-certs": "ca-cert",
1388
1514
  explain: "plan",
1389
1515
  "zone-name": "zone",
1516
+ rows: "max-rows",
1390
1517
  };
1391
1518
  const VALUE_FLAGS = new Set([
1519
+ "fields",
1392
1520
  "activity",
1393
1521
  "agent",
1394
1522
  "api-key",
@@ -1412,6 +1540,7 @@ const VALUE_FLAGS = new Set([
1412
1540
  "database",
1413
1541
  "dataset",
1414
1542
  "file",
1543
+ "env",
1415
1544
  "install-core-packages",
1416
1545
  "folder",
1417
1546
  "input",
@@ -1447,6 +1576,7 @@ const VALUE_FLAGS = new Set([
1447
1576
  "partition",
1448
1577
  "parent",
1449
1578
  "path",
1579
+ "preview",
1450
1580
  "project-key",
1451
1581
  "recipe",
1452
1582
  "request-timeout",
@@ -1454,6 +1584,7 @@ const VALUE_FLAGS = new Set([
1454
1584
  "results-per-page",
1455
1585
  "record-cleanup",
1456
1586
  "rule-id",
1587
+ "role",
1457
1588
  "retries",
1458
1589
  "poll-interval",
1459
1590
  "python-interpreter",
@@ -1497,6 +1628,8 @@ const KNOWN_LONG_FLAGS = new Set([
1497
1628
  ...Object.values(FLAG_ALIASES),
1498
1629
  ]);
1499
1630
  function normalizeLongFlag(rawFlagName) {
1631
+ if (rawFlagName === "help")
1632
+ throw unsupportedHelpFlag();
1500
1633
  const flagName = FLAG_ALIASES[rawFlagName] ?? rawFlagName;
1501
1634
  if (!KNOWN_LONG_FLAGS.has(rawFlagName) && !KNOWN_LONG_FLAGS.has(flagName)) {
1502
1635
  throw new UsageError(`Unknown flag: --${rawFlagName}`, "unknown_flag");
@@ -1563,6 +1696,8 @@ function parseArgs(argv) {
1563
1696
  }
1564
1697
  }
1565
1698
  else {
1699
+ if (arg[1] === "h")
1700
+ throw unsupportedHelpFlag();
1566
1701
  throw new UsageError(`Unknown flag: -${arg[1]}`, "unknown_flag");
1567
1702
  }
1568
1703
  }
@@ -2621,9 +2756,9 @@ const commands = {
2621
2756
  timeoutMs: num(f["timeout"]),
2622
2757
  });
2623
2758
  },
2624
- usage: "dss dataset preview <name> [--max-rows N] [--project-key KEY] [--timeout MS]",
2625
- description: "Preview dataset rows.",
2626
- examples: ["dss dataset preview orders", "dss dataset preview orders --max-rows 5",],
2759
+ usage: "dss dataset preview <name> [--max-rows N|--rows N] [--project-key KEY] [--timeout MS]",
2760
+ description: "Preview dataset rows (--rows is an alias for --max-rows).",
2761
+ examples: ["dss dataset preview orders", "dss dataset preview orders --rows 5",],
2627
2762
  },
2628
2763
  metadata: {
2629
2764
  handler: (c, a, f) => {
@@ -2635,15 +2770,28 @@ const commands = {
2635
2770
  examples: ["dss dataset metadata orders",],
2636
2771
  },
2637
2772
  download: {
2638
- handler: (c, a, f) => {
2773
+ handler: async (c, a, f) => {
2639
2774
  requireArgs(a, 1, "dss dataset download <name>");
2640
- return c.datasets.download(a[0], {
2775
+ const result = await c.datasets.download(a[0], {
2641
2776
  outputPath: f["output"],
2642
2777
  projectKey: f["project-key"],
2778
+ limit: num(f["limit"]),
2643
2779
  });
2780
+ if (result.truncated) {
2781
+ enqueueCliWarning({
2782
+ code: "dataset_download_truncated",
2783
+ message: `Download of '${a[0]}' stopped at the ${result.limit}-row cap; the dataset has more rows. `
2784
+ + "Re-run with --limit N for more, or read inside a recipe (get_dataframe) for the full data.",
2785
+ dataset: a[0],
2786
+ rows: result.rows,
2787
+ limit: result.limit,
2788
+ path: result.path,
2789
+ });
2790
+ }
2791
+ return result;
2644
2792
  },
2645
- usage: "dss dataset download <name> [--output PATH] [--project-key KEY]",
2646
- description: "Download dataset contents as CSV.",
2793
+ usage: "dss dataset download <name> [--output PATH] [--limit N] [--project-key KEY]",
2794
+ description: "Download up to --limit rows (default 100k) as CSV; returns { path, rows, truncated, limit }. When truncated, a dataset_download_truncated warning is also written to stderr so the cap is never silent.",
2647
2795
  examples: ["dss dataset download orders", "dss dataset download orders --output ./data/",],
2648
2796
  },
2649
2797
  create: {
@@ -3096,6 +3244,87 @@ const commands = {
3096
3244
  "cat settings.json | dss recipe update compute_orders --stdin",
3097
3245
  ],
3098
3246
  },
3247
+ "add-input": {
3248
+ handler: async (c, a, f) => {
3249
+ requireArgs(a, 2, "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]");
3250
+ const role = f["role"] ?? "main";
3251
+ const pk = f["project-key"];
3252
+ const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
3253
+ const items = recipeRoleInputItems(recipe, role);
3254
+ const present = items.some((item) => recipeInputItemRef(item) === a[1]);
3255
+ if (present) {
3256
+ if (f["if-not-exists"] === true) {
3257
+ return skipResult("recipe", a[0], "exists", { dataset: a[1], role, });
3258
+ }
3259
+ throw new UsageError(`Dataset "${a[1]}" is already a "${role}" input of recipe "${a[0]}".`, "validation_failed");
3260
+ }
3261
+ const nextItems = [...items, { ref: a[1], deps: [], },];
3262
+ const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
3263
+ if (f["dry-run"] === true) {
3264
+ return {
3265
+ dryRun: true,
3266
+ action: "add-input",
3267
+ resource: "recipe",
3268
+ recipe: a[0],
3269
+ dataset: a[1],
3270
+ role,
3271
+ inputs,
3272
+ };
3273
+ }
3274
+ await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
3275
+ return { updated: a[0], resource: "recipe", action: "add-input", role, dataset: a[1], inputs, };
3276
+ },
3277
+ usage: "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]",
3278
+ description: "Add a dataset as a recipe input by appending one item to the current inputs (no need to resend the whole list).",
3279
+ examples: [
3280
+ "dss recipe add-input compute_orders extra_lookup",
3281
+ "dss recipe add-input compute_orders extra_lookup --if-not-exists --dry-run",
3282
+ ],
3283
+ },
3284
+ "remove-input": {
3285
+ handler: async (c, a, f) => {
3286
+ requireArgs(a, 2, "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]");
3287
+ const role = f["role"] ?? "main";
3288
+ const pk = f["project-key"];
3289
+ const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
3290
+ const items = recipeRoleInputItems(recipe, role);
3291
+ const present = items.some((item) => recipeInputItemRef(item) === a[1]);
3292
+ if (!present) {
3293
+ if (f["if-exists"] === true) {
3294
+ return skipResult("recipe", a[0], "missing", { dataset: a[1], role, });
3295
+ }
3296
+ throw new UsageError(`Dataset "${a[1]}" is not a "${role}" input of recipe "${a[0]}".`, "validation_failed");
3297
+ }
3298
+ const nextItems = items.filter((item) => recipeInputItemRef(item) !== a[1]);
3299
+ const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
3300
+ if (f["dry-run"] === true) {
3301
+ return {
3302
+ dryRun: true,
3303
+ action: "remove-input",
3304
+ resource: "recipe",
3305
+ recipe: a[0],
3306
+ dataset: a[1],
3307
+ role,
3308
+ inputs,
3309
+ };
3310
+ }
3311
+ await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
3312
+ return {
3313
+ updated: a[0],
3314
+ resource: "recipe",
3315
+ action: "remove-input",
3316
+ role,
3317
+ dataset: a[1],
3318
+ inputs,
3319
+ };
3320
+ },
3321
+ usage: "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]",
3322
+ description: "Remove a dataset from a recipe's inputs by dropping one item from the current inputs.",
3323
+ examples: [
3324
+ "dss recipe remove-input compute_orders stale_lookup",
3325
+ "dss recipe remove-input compute_orders stale_lookup --if-exists --dry-run",
3326
+ ],
3327
+ },
3099
3328
  "get-payload": {
3100
3329
  handler: async (c, a, f) => {
3101
3330
  requireArgs(a, 1, "dss recipe get-payload <name>");
@@ -3109,7 +3338,7 @@ const commands = {
3109
3338
  return payload;
3110
3339
  },
3111
3340
  usage: "dss recipe get-payload <name> [--raw] [--output PATH] [--project-key KEY]",
3112
- description: "Print the recipe code payload to stdout; use --raw for pipeable code bytes.",
3341
+ description: "Print the recipe code payload as JSON; use --raw for raw bytes, not JSON.",
3113
3342
  examples: [
3114
3343
  "dss recipe get-payload compute_orders --raw",
3115
3344
  "dss recipe get-payload compute_orders -o code.py",
@@ -3123,7 +3352,7 @@ const commands = {
3123
3352
  });
3124
3353
  },
3125
3354
  usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
3126
- description: "Print a recipe code payload; combine with --raw for shell pipes and diffs.",
3355
+ description: "Print a recipe code payload as JSON; use --raw for raw bytes, not JSON.",
3127
3356
  examples: ["dss recipe cat compute_orders --raw",],
3128
3357
  },
3129
3358
  "set-payload": {
@@ -3306,17 +3535,29 @@ const commands = {
3306
3535
  examples: ["dss job summary JOB_ID --max-log-lines 200",],
3307
3536
  },
3308
3537
  log: {
3309
- handler: (c, a, f) => {
3538
+ handler: async (c, a, f) => {
3310
3539
  requireArgs(a, 1, "dss job log <id>");
3311
- return c.jobs.log(a[0], {
3540
+ const logFilter = f["errors-only"] === true
3541
+ ? "errors"
3542
+ : jobLogFilterFromFlag(f["log-filter"]);
3543
+ const log = await c.jobs.log(a[0], {
3312
3544
  activity: f["activity"],
3313
3545
  logId: f["log-id"],
3546
+ logFilter,
3314
3547
  maxLogLines: maxLogLinesFromFlags(f),
3315
3548
  projectKey: f["project-key"],
3316
3549
  });
3550
+ const outputFile = f["output"]
3551
+ ?? f["output-file"];
3552
+ if (!outputFile)
3553
+ return log;
3554
+ const outputPath = resolve(outputFile);
3555
+ await mkdir(dirname(outputPath), { recursive: true, });
3556
+ await writeFile(outputPath, log.endsWith("\n") ? log : `${log}\n`, "utf-8");
3557
+ return outputPath;
3317
3558
  },
3318
- usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
3319
- description: "Get public API job log output. --log-id is accepted for UI parity but DSS API-key auth cannot select browser-only cat-activity-log files.",
3559
+ usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--log-filter stdout|stderr|user|errors] [--errors-only] [--max-lines N|--max-log-lines N] [--output PATH] [--project-key KEY]",
3560
+ description: "Get public API job log output. Use --errors-only (or --log-filter errors) to surface just error/traceback lines, and --output PATH to write the log to a file (stdout returns the path). --log-id is accepted for UI parity but DSS API-key auth cannot select browser-only cat-activity-log files.",
3320
3561
  examples: [
3321
3562
  "dss job log JOB_ID",
3322
3563
  "dss job log JOB_ID --activity main --max-log-lines 200",
@@ -4185,12 +4426,21 @@ const commands = {
4185
4426
  sql: {
4186
4427
  query: {
4187
4428
  handler: async (c, a, f) => {
4188
- const query = resolveSqlInput(a, f);
4189
4429
  const connection = f["connection"];
4190
4430
  const datasetFullName = f["dataset"];
4191
4431
  if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
4192
4432
  throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
4193
4433
  }
4434
+ const outputFile = f["output"]
4435
+ ?? f["output-file"];
4436
+ const previewProvided = f["preview"] !== undefined;
4437
+ if (previewProvided && !outputFile) {
4438
+ throw new UsageError(`--preview requires --output or --output-file. Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
4439
+ }
4440
+ const previewCount = previewProvided
4441
+ ? parseSqlPreviewCount(f["preview"])
4442
+ : DEFAULT_SQL_PREVIEW_ROWS;
4443
+ const query = resolveSqlInput(a, f);
4194
4444
  const result = await c.sql.query({
4195
4445
  query,
4196
4446
  connection,
@@ -4198,8 +4448,6 @@ const commands = {
4198
4448
  database: f["database"],
4199
4449
  projectKey: f["project-key"],
4200
4450
  });
4201
- const outputFile = f["output"]
4202
- ?? f["output-file"];
4203
4451
  if (!outputFile)
4204
4452
  return result;
4205
4453
  const outputPath = resolve(outputFile);
@@ -4210,6 +4458,7 @@ const commands = {
4210
4458
  schema: result.schema,
4211
4459
  columns: result.columns ?? result.schema,
4212
4460
  rowCount: result.rows.length,
4461
+ preview: result.rows.slice(0, previewCount),
4213
4462
  outputPath,
4214
4463
  written: outputPath,
4215
4464
  };
@@ -4221,6 +4470,40 @@ const commands = {
4221
4470
  "dss sql query --sql-file query.sql --connection my_pg",
4222
4471
  "echo 'SELECT 1' | dss sql query --stdin --dataset MYPROJ.orders",
4223
4472
  "dss sql query --sql-file query.sql --connection my_pg --output results.json --request-timeout 120000",
4473
+ "dss sql query --sql-file query.sql --connection my_pg --output results.json --preview 10",
4474
+ ],
4475
+ },
4476
+ },
4477
+ code: {
4478
+ run: {
4479
+ handler: async (c, a, f) => {
4480
+ const script = resolveCodeInput(a, f);
4481
+ const run = await c.scenarios.runScript(script, {
4482
+ envName: f["env"],
4483
+ projectKey: f["project-key"],
4484
+ timeoutMs: num(f["timeout"]),
4485
+ keepScenario: f["keep"] === true,
4486
+ });
4487
+ const result = {
4488
+ outcome: run.outcome,
4489
+ success: run.success,
4490
+ runId: run.runId,
4491
+ elapsedMs: run.elapsedMs,
4492
+ pollCount: run.pollCount,
4493
+ output: run.output ?? "",
4494
+ };
4495
+ if (f["full-log"] === true || run.output === undefined) {
4496
+ result.log = run.log;
4497
+ }
4498
+ return result;
4499
+ },
4500
+ usage: CODE_RUN_USAGE,
4501
+ description: "Run one-off Python in a DSS code env via a throwaway custom-python scenario; returns the script's captured output (stdout+stderr) plus outcome/success. Pass --full-log for the raw DSS run log. Exits 4 on a non-SUCCESS outcome.",
4502
+ examples: [
4503
+ "dss code run --file inspect.py",
4504
+ "dss code run --file inspect.py --env py39_pandas",
4505
+ "cat snippet.py | dss code run --stdin",
4506
+ "dss code run --file inspect.py --full-log",
4224
4507
  ],
4225
4508
  },
4226
4509
  },
@@ -4452,7 +4735,7 @@ const commands = {
4452
4735
  },
4453
4736
  };
4454
4737
  // ---------------------------------------------------------------------------
4455
- // Help
4738
+ // Agent-facing command inventory
4456
4739
  // ---------------------------------------------------------------------------
4457
4740
  const RESOURCE_NAMES = [
4458
4741
  ...Object.keys(commands),
@@ -4463,87 +4746,37 @@ const RESOURCE_NAMES = [
4463
4746
  "install-skill",
4464
4747
  ]
4465
4748
  .sort();
4466
- function printTopLevelHelp() {
4467
- const lines = [
4468
- "Usage: dss <resource> <action> [args...] [--flags]",
4469
- "",
4470
- "Global flags:",
4471
- " -h, --help Show help",
4472
- " -v, --verbose Log HTTP requests to stderr",
4473
- " -V, --version Show version",
4474
- " --json Emit JSON output (default)",
4475
- " -o, --output PATH Write output to file (recipe get-payload)",
4476
- " --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
4477
- " --api-key KEY API key (env: DATAIKU_API_KEY)",
4478
- " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
4479
- " --timeout MS Operation timeout (build-and-wait, run-and-wait, recipe run)",
4480
- " --request-timeout MS HTTP request timeout in ms (default: 30000)",
4481
- " --dry-run Preview destructive actions without executing",
4482
- " --if-not-exists Skip create if resource already exists",
4483
- " --if-exists Skip delete if resource is already missing",
4484
- " --insecure Disable TLS certificate verification",
4485
- " --ca-cert PATH Extra PEM CA bundle (env: NODE_EXTRA_CA_CERTS)",
4486
- "",
4487
- "Resources:",
4488
- ...RESOURCE_NAMES.map((r) => ` ${r}`),
4489
- "",
4490
- "Quick start:",
4491
- " dss auth login Save DSS credentials",
4492
- " dss auth status Verify connection",
4493
- " dss doctor Run JSON connectivity diagnostics",
4494
- " dss project list List accessible projects",
4495
- " dss dataset list List datasets in default project",
4496
- " dss dataset preview <name> Preview dataset rows as CSV",
4497
- " dss recipe get-payload <name> Print recipe code to stdout",
4498
- " dss recipe download-code <name> Download recipe code to a file",
4499
- " dss job log <id> View job log output",
4500
- " dss install-skill Install agent skill for coding agents",
4501
- ];
4502
- process.stderr.write(`${lines.join("\n")}\n`);
4503
- }
4504
- function printResourceHelp(resource) {
4505
- const actions = commands[resource];
4506
- if (!actions)
4507
- return;
4508
- const maxName = Math.max(...Object.keys(actions).map((n) => n.length));
4509
- const lines = [
4510
- `Usage: dss ${resource} <action> [args...] [--flags]`,
4511
- "",
4512
- "Actions:",
4513
- ...Object.entries(actions).map(([name, meta,]) => ` ${name.padEnd(maxName + 2)}${meta.description ?? meta.usage}`),
4514
- "",
4515
- `Run 'dss ${resource} <action> --help' for details and examples.`,
4516
- ];
4517
- process.stderr.write(`${lines.join("\n")}\n`);
4518
- }
4519
- function printActionHelp(resource, action) {
4520
- const meta = commands[resource]?.[action];
4521
- if (!meta)
4522
- return;
4523
- const lines = [];
4524
- if (meta.description)
4525
- lines.push(meta.description, "");
4526
- lines.push(`Usage: ${meta.usage}`);
4527
- if (meta.examples && meta.examples.length > 0) {
4528
- lines.push("", "Examples:");
4529
- for (const ex of meta.examples)
4530
- lines.push(` ${ex}`);
4531
- }
4532
- process.stderr.write(`${lines.join("\n")}\n`);
4533
- }
4534
4749
  // ---------------------------------------------------------------------------
4535
4750
  // Validation
4536
4751
  // ---------------------------------------------------------------------------
4537
4752
  class UsageError extends Error {
4538
4753
  code;
4539
4754
  hint;
4540
- constructor(message, code = "usage_error", hint) {
4755
+ details;
4756
+ constructor(message, code = "usage_error", hint, details) {
4541
4757
  super(message);
4542
4758
  this.name = "UsageError";
4543
4759
  this.code = code;
4544
4760
  this.hint = hint;
4761
+ this.details = details;
4545
4762
  }
4546
4763
  }
4764
+ const COMMANDS_RUN_HINT = "Use `dss commands run` for machine-readable command discovery.";
4765
+ function unsupportedHelpFlag() {
4766
+ return new UsageError("Help screens are not supported.", "usage_error", COMMANDS_RUN_HINT, { command: "dss commands run", });
4767
+ }
4768
+ function noCommandError() {
4769
+ return new UsageError("No command provided.", "usage_error", COMMANDS_RUN_HINT, { command: "dss commands run", resources: RESOURCE_NAMES, });
4770
+ }
4771
+ function missingActionError(resource, validActions, usage) {
4772
+ return new UsageError(`Missing action for ${resource}.`, "usage_error", usage ?? COMMANDS_RUN_HINT, { resource, validActions, });
4773
+ }
4774
+ function unknownResourceError(resource) {
4775
+ return new UsageError(`Unknown resource: ${resource}.`, "usage_error", COMMANDS_RUN_HINT, { resource, validResources: RESOURCE_NAMES, });
4776
+ }
4777
+ function unknownActionError(resource, action, validActions, hint) {
4778
+ return new UsageError(`Unknown action: ${resource} ${action ?? ""}`.trim(), "usage_error", hint ?? COMMANDS_RUN_HINT, { resource, action, validActions, });
4779
+ }
4547
4780
  function requireArgs(args, count, usage) {
4548
4781
  if (args.length < count) {
4549
4782
  throw new UsageError(`Expected ${count} argument(s), got ${args.length}.\nUsage: ${usage}`, "missing_required_arg");
@@ -4552,12 +4785,18 @@ function requireArgs(args, count, usage) {
4552
4785
  // ---------------------------------------------------------------------------
4553
4786
  // .env auto-loading
4554
4787
  // ---------------------------------------------------------------------------
4788
+ function dataikuEnvironmentEnabled() {
4789
+ return process.env.DATAIKU_DISABLE_ENV !== "1";
4790
+ }
4555
4791
  function loadEnvFile() {
4556
- if (process.env.DATAIKU_DISABLE_ENV === "1")
4792
+ if (!dataikuEnvironmentEnabled())
4557
4793
  return;
4794
+ // The invocation cwd takes precedence over the CLI install/root directory, so a
4795
+ // project-local .env where `dss` is invoked overrides defaults shipped beside the
4796
+ // CLI. First writer wins below, so cwd must be listed first.
4558
4797
  const dirs = [
4559
- resolve(dirname(fileURLToPath(import.meta.url)), ".."),
4560
4798
  process.cwd(),
4799
+ resolve(dirname(fileURLToPath(import.meta.url)), ".."),
4561
4800
  ];
4562
4801
  for (const dir of dirs) {
4563
4802
  try {
@@ -4587,81 +4826,42 @@ const AUTH_ACTIONS = {
4587
4826
  login: {
4588
4827
  handler: async (flags) => {
4589
4828
  const tlsSettings = resolveTlsSettings(flags);
4590
- let { url, apiKey, projectKey, } = resolveCredentials(flags);
4829
+ const useEnv = dataikuEnvironmentEnabled();
4830
+ const url = typeof flags["url"] === "string"
4831
+ ? flags["url"]
4832
+ : useEnv
4833
+ ? process.env.DATAIKU_URL ?? ""
4834
+ : "";
4835
+ const apiKey = typeof flags["api-key"] === "string"
4836
+ ? flags["api-key"]
4837
+ : useEnv
4838
+ ? process.env.DATAIKU_API_KEY ?? ""
4839
+ : "";
4840
+ const projectKey = typeof flags["project-key"] === "string"
4841
+ ? flags["project-key"]
4842
+ : useEnv
4843
+ ? process.env.DATAIKU_PROJECT_KEY
4844
+ : undefined;
4591
4845
  if (!url || !apiKey) {
4592
- if (!process.stdin.isTTY) {
4593
- throw new UsageError("Missing --url and/or --api-key. Provide them as flags or run interactively.");
4594
- }
4595
- if (!url)
4596
- url = await promptLine("DSS URL: ");
4597
- if (!apiKey)
4598
- apiKey = await promptSecret("API key: ");
4599
- if (!projectKey)
4600
- projectKey = (await promptLine("Project key (optional): ")) || undefined;
4846
+ throw new UsageError("Missing --url and/or --api-key for auth login.", "missing_required_flag", "Pass --url and --api-key, or set DATAIKU_URL and DATAIKU_API_KEY.", { requiredFlags: ["url", "api-key",], env: ["DATAIKU_URL", "DATAIKU_API_KEY",], });
4601
4847
  }
4602
- if (!url)
4603
- throw new UsageError("URL is required.");
4604
- if (!apiKey)
4605
- throw new UsageError("API key is required.");
4606
- process.stderr.write("Validating credentials... ");
4607
4848
  const result = await validateCredentials(url, apiKey, tlsSettings);
4608
4849
  if (!result.valid) {
4609
- process.stderr.write("Failed\n");
4610
4850
  if (result.dataikuError)
4611
4851
  throw result.dataikuError;
4612
4852
  throw new DataikuError(0, "Authentication Failed", result.error ?? "Credential validation failed");
4613
4853
  }
4614
- process.stderr.write("Connected\n");
4854
+ const path = getCredentialsPath();
4615
4855
  saveCredentials({ url, apiKey, projectKey, ...tlsSettings, });
4616
- process.stderr.write(`Credentials saved to ${getCredentialsPath()}\n`);
4856
+ return { saved: true, path, };
4617
4857
  },
4618
- usage: "dss auth login [--url URL] [--api-key KEY] [--project-key KEY] [--insecure] [--ca-cert PATH]",
4619
- description: "Save DSS credentials (interactive or via flags).",
4858
+ usage: "dss auth login --url URL --api-key KEY [--project-key KEY] [--insecure] [--ca-cert PATH]",
4859
+ description: "Validate and save DSS credentials from flags or environment variables.",
4620
4860
  examples: [
4621
4861
  "dss auth login --url https://dss.example.com --api-key YOUR_KEY",
4622
4862
  "dss auth login --url https://dss.example.com --api-key YOUR_KEY --project-key MYPROJ",
4623
4863
  ],
4624
- },
4625
- status: {
4626
- handler: async (flags) => {
4627
- const creds = loadCredentials();
4628
- if (!creds) {
4629
- process.stderr.write("No saved credentials. Run: dss auth login\n");
4630
- process.exit(1);
4631
- }
4632
- const tlsSettings = resolveTlsSettings(flags, creds);
4633
- const lines = [
4634
- `URL: ${creds.url}`,
4635
- `API key: ${maskApiKey(creds.apiKey)}`,
4636
- `Project key: ${creds.projectKey ?? "(not set)"}`,
4637
- `TLS verify: ${tlsSettings.tlsRejectUnauthorized === false ? "disabled" : "strict"}`,
4638
- `CA cert: ${tlsSettings.caCertPath ?? "(default trust store)"}`,
4639
- ];
4640
- for (const line of lines)
4641
- process.stderr.write(`${line}\n`);
4642
- const result = await validateCredentials(creds.url, creds.apiKey, tlsSettings);
4643
- if (result.valid) {
4644
- process.stderr.write("Connection: valid\n");
4645
- }
4646
- else {
4647
- process.stderr.write(`Connection: failed (${result.error ?? "unknown error"})\n`);
4648
- process.stderr.write(`Config: ${getCredentialsPath()}\n`);
4649
- process.exit(1);
4650
- }
4651
- process.stderr.write(`Config: ${getCredentialsPath()}\n`);
4652
- },
4653
- usage: "dss auth status [--insecure] [--ca-cert PATH]",
4654
- description: "Show saved credentials and verify the connection.",
4655
- examples: ["dss auth status",],
4656
- },
4657
- logout: {
4658
- handler: async (_flags) => {
4659
- deleteCredentials();
4660
- process.stderr.write("Credentials removed.\n");
4661
- },
4662
- usage: "dss auth logout",
4663
- description: "Remove saved credentials.",
4664
- examples: ["dss auth logout",],
4864
+ requiredFlags: ["url", "api-key",],
4665
4865
  },
4666
4866
  };
4667
4867
  function errorDetails(error) {
@@ -4913,7 +5113,7 @@ async function runDoctor(flags) {
4913
5113
  ok: credentialsOk,
4914
5114
  message: credentialsOk
4915
5115
  ? "Dataiku URL and API key are configured."
4916
- : "Missing Dataiku URL and/or API key. Set DATAIKU_URL/DATAIKU_API_KEY, pass flags, or run dss auth login.",
5116
+ : "Missing Dataiku URL and/or API key. Set DATAIKU_URL/DATAIKU_API_KEY or pass --url/--api-key.",
4917
5117
  });
4918
5118
  let accessibleProjects;
4919
5119
  if (credentialsOk) {
@@ -4992,13 +5192,13 @@ async function runDoctor(flags) {
4992
5192
  async function runFixtures(flags) {
4993
5193
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
4994
5194
  if (!url) {
4995
- throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
5195
+ throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL or pass --url.", "missing_required_flag");
4996
5196
  }
4997
5197
  if (!apiKey) {
4998
- throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
5198
+ throw new UsageError("Missing API key. Set DATAIKU_API_KEY or pass --api-key.", "missing_required_flag");
4999
5199
  }
5000
5200
  if (!projectKey) {
5001
- throw new UsageError("Missing project key. Set DATAIKU_PROJECT_KEY, pass --project-key, or run: dss auth login", "missing_required_flag");
5201
+ throw new UsageError("Missing project key. Set DATAIKU_PROJECT_KEY or pass --project-key.", "missing_required_flag");
5002
5202
  }
5003
5203
  currentCommandContext.projectKey = projectKey;
5004
5204
  const requestTimeoutMs = num(flags["request-timeout"]);
@@ -5072,7 +5272,7 @@ const PROJECT_SCOPED_RESOURCES = new Set([
5072
5272
  "variable",
5073
5273
  "wiki",
5074
5274
  ]);
5075
- const GLOBAL_AGENT_FLAGS = ["help", "json", "report-json", "verbose",];
5275
+ const GLOBAL_AGENT_FLAGS = ["json", "verbose", "fields",];
5076
5276
  const AUTHENTICATED_AGENT_FLAGS = [
5077
5277
  "url",
5078
5278
  "api-key",
@@ -5081,9 +5281,12 @@ const AUTHENTICATED_AGENT_FLAGS = [
5081
5281
  "insecure",
5082
5282
  "ca-cert",
5083
5283
  ];
5084
- const COMMANDS_USAGE = "dss commands [--json]";
5284
+ const COMMANDS_USAGE = "dss commands run [--json]";
5085
5285
  const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
5086
- const COMMANDS_EXAMPLES = ["dss commands", "dss commands --json",];
5286
+ const COMMANDS_EXAMPLES = ["dss commands run", "dss commands run --json",];
5287
+ const VERSION_USAGE = "dss version";
5288
+ const VERSION_DESCRIPTION = "Print the CLI version and git revision as JSON.";
5289
+ const VERSION_EXAMPLES = ["dss version", "dss --version",];
5087
5290
  const INSTALL_SKILL_USAGE = "dss install-skill [--global] [--agent NAME] [--target PATH] [--list-agents] [--dry-run] [--plan]";
5088
5291
  const INSTALL_SKILL_DESCRIPTION = "Install the dataiku-dss agent skill for detected coding agents.";
5089
5292
  const INSTALL_SKILL_EXAMPLES = [
@@ -5102,6 +5305,22 @@ const FIXTURES_EXAMPLES = [
5102
5305
  "dss fixtures --json",
5103
5306
  "dss fixtures --json --allow-types Filesystem,Inline",
5104
5307
  ];
5308
+ const ALLOWED_CLEANUP_ACTIONS = new Set([
5309
+ // Must mirror every cleanup.argv shape emitted by cleanupLedgerEntry().
5310
+ "dataset delete",
5311
+ "recipe delete",
5312
+ "scenario delete",
5313
+ "flow-zone delete",
5314
+ "wiki delete",
5315
+ "dashboard delete",
5316
+ "insight delete",
5317
+ "data-quality delete-rule",
5318
+ "code-env delete",
5319
+ "folder delete-file",
5320
+ ]);
5321
+ function isAllowedCleanupAction(resource, action) {
5322
+ return ALLOWED_CLEANUP_ACTIONS.has(`${resource} ${action}`);
5323
+ }
5105
5324
  function uniqueStrings(values) {
5106
5325
  return [...new Set(values),];
5107
5326
  }
@@ -5166,26 +5385,33 @@ function extractPositionals(usage) {
5166
5385
  function inferSideEffect(resource, action) {
5167
5386
  if (resource === "auth")
5168
5387
  return "auth";
5169
- if (resource === "doctor" || resource === "commands" || resource === "fixtures")
5388
+ if (resource === "doctor" || resource === "commands" || resource === "fixtures"
5389
+ || resource === "version") {
5170
5390
  return "read";
5391
+ }
5171
5392
  if (resource === "install-skill")
5172
5393
  return "write";
5173
5394
  if (resource === "data-quality" && action === "compute")
5174
5395
  return "write";
5175
5396
  if (READ_ACTIONS.has(action))
5176
5397
  return "read";
5177
- if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
5398
+ if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove)/
5178
5399
  .test(action)) {
5179
5400
  return "write";
5180
5401
  }
5181
5402
  return "read";
5182
5403
  }
5183
5404
  function inferRequiresAuth(resource) {
5184
- return resource !== "auth" && resource !== "commands" && resource !== "install-skill";
5405
+ return resource !== "auth"
5406
+ && resource !== "commands"
5407
+ && resource !== "install-skill"
5408
+ && resource !== "version";
5185
5409
  }
5186
5410
  function inferRequiresProject(resource, action, usage) {
5187
- if (resource === "doctor" || resource === "commands" || resource === "install-skill")
5411
+ if (resource === "auth" || resource === "doctor" || resource === "commands"
5412
+ || resource === "install-skill" || resource === "version") {
5188
5413
  return false;
5414
+ }
5189
5415
  if (PROJECT_SCOPED_RESOURCES.has(resource))
5190
5416
  return true;
5191
5417
  if (resource === "project" && action !== "list")
@@ -5213,13 +5439,16 @@ const STRING_OUTPUT_ACTIONS = new Set([
5213
5439
  "cat",
5214
5440
  "log",
5215
5441
  "log-url",
5216
- "preview",
5217
5442
  ]);
5218
5443
  function inferOutputShape(resource, action) {
5219
- if (resource === "auth" || resource === "install-skill")
5220
- return "void";
5444
+ if (resource === "auth" || resource === "commands" || resource === "install-skill"
5445
+ || resource === "version") {
5446
+ return "object";
5447
+ }
5221
5448
  if (ARRAY_OUTPUT_ACTIONS.has(action))
5222
5449
  return "array";
5450
+ if (resource === "dataset" && action === "download")
5451
+ return "object";
5223
5452
  if (STRING_OUTPUT_ACTIONS.has(action))
5224
5453
  return "string";
5225
5454
  return "object";
@@ -5234,8 +5463,107 @@ function inferInputContract(usage) {
5234
5463
  function stripOptionalUsageGroups(usage) {
5235
5464
  return usage.replace(/\[[^\]]*\]/g, " ");
5236
5465
  }
5237
- function extractRequiredUsageFlags(usage) {
5238
- return extractUsageFlags(stripOptionalUsageGroups(usage));
5466
+ function stripAllUsageGroups(usage) {
5467
+ return usage.replace(/\[[^\]]*\]/g, " ").replace(/\([^)]*\)/g, " ");
5468
+ }
5469
+ function topLevelParenGroups(usage) {
5470
+ const groups = [];
5471
+ let depth = 0;
5472
+ let current = "";
5473
+ for (const char of usage) {
5474
+ if (char === "(") {
5475
+ if (depth > 0)
5476
+ current += char;
5477
+ else
5478
+ current = "";
5479
+ depth++;
5480
+ }
5481
+ else if (char === ")") {
5482
+ depth--;
5483
+ if (depth === 0)
5484
+ groups.push(current);
5485
+ else
5486
+ current += char;
5487
+ }
5488
+ else if (depth > 0) {
5489
+ current += char;
5490
+ }
5491
+ }
5492
+ return groups;
5493
+ }
5494
+ function splitTopLevelChoices(group) {
5495
+ const parts = [];
5496
+ let depth = 0;
5497
+ let current = "";
5498
+ for (const char of group) {
5499
+ if (char === "[" || char === "(")
5500
+ depth++;
5501
+ else if (char === "]" || char === ")")
5502
+ depth--;
5503
+ if (char === "|" && depth === 0) {
5504
+ parts.push(current);
5505
+ current = "";
5506
+ }
5507
+ else {
5508
+ current += char;
5509
+ }
5510
+ }
5511
+ parts.push(current);
5512
+ return parts;
5513
+ }
5514
+ function flagsInUsageFragment(fragment) {
5515
+ return extractUsageFlags(fragment.replace(/\[[^\]]*\]/g, " "));
5516
+ }
5517
+ /**
5518
+ * Split required usage flags into unconditional flags and mutually-exclusive
5519
+ * choice groups. A required `(--a X | --b Y)` group becomes a requiredOneOf entry
5520
+ * (pick exactly one alternative; an alternative listing several flags must be
5521
+ * supplied together) instead of marking every flag as unconditionally required.
5522
+ */
5523
+ function deriveRequiredUsage(usage) {
5524
+ const requiredFlags = extractUsageFlags(stripAllUsageGroups(usage));
5525
+ const requiredOneOf = [];
5526
+ for (const group of topLevelParenGroups(usage)) {
5527
+ const alternatives = splitTopLevelChoices(group);
5528
+ if (alternatives.length <= 1) {
5529
+ requiredFlags.push(...flagsInUsageFragment(group));
5530
+ continue;
5531
+ }
5532
+ const oneOf = alternatives
5533
+ .map((alternative) => flagsInUsageFragment(alternative))
5534
+ .filter((alternativeFlags) => alternativeFlags.length > 0);
5535
+ if (oneOf.length > 1)
5536
+ requiredOneOf.push({ oneOf, });
5537
+ else if (oneOf.length === 1)
5538
+ requiredFlags.push(...oneOf[0]);
5539
+ }
5540
+ return { requiredFlags: uniqueStrings(requiredFlags), requiredOneOf, };
5541
+ }
5542
+ const GLOBAL_FLAG_VALUE_HINTS = {
5543
+ url: { valueType: "URL", },
5544
+ fields: { valueType: "CSV", },
5545
+ "api-key": { valueType: "KEY", },
5546
+ "request-timeout": { valueType: "MS", },
5547
+ retries: { valueType: "N", },
5548
+ "ca-cert": { valueType: "PATH", },
5549
+ "project-key": { valueType: "KEY", },
5550
+ "record-cleanup": { valueType: "PATH", },
5551
+ };
5552
+ /** Derive a value placeholder (and enum members) for each value flag from its usage token. */
5553
+ function extractFlagValueHints(usage) {
5554
+ const hints = new Map();
5555
+ for (const match of usage.matchAll(/--([a-z0-9-]+)\s+([a-z]+(?:\|[a-z]+)+)/g)) {
5556
+ const flag = FLAG_ALIASES[match[1]] ?? match[1];
5557
+ if (!hints.has(flag)) {
5558
+ hints.set(flag, { valueType: "enum", enumValues: match[2].split("|"), });
5559
+ }
5560
+ }
5561
+ for (const match of usage.matchAll(/--([a-z0-9-]+)\s+(<[^>]+>|[A-Z][A-Za-z0-9_]*)/g)) {
5562
+ const flag = FLAG_ALIASES[match[1]] ?? match[1];
5563
+ if (!hints.has(flag))
5564
+ hints.set(flag, { valueType: match[2], });
5565
+ }
5566
+ return hints;
5239
5567
  }
5240
5568
  function inferPayloadSchema(inputContract) {
5241
5569
  if (!inputContract.stdin && !inputContract.dataFlag && !inputContract.dataFileFlag) {
@@ -5288,6 +5616,8 @@ function inferAsyncKind(resource, action) {
5288
5616
  }
5289
5617
  if (resource === "data-quality" && action === "compute")
5290
5618
  return "future";
5619
+ if (resource === "code" && action === "run")
5620
+ return "future";
5291
5621
  return "none";
5292
5622
  }
5293
5623
  function inferIdempotency(sideEffect, action, usage) {
@@ -5297,6 +5627,8 @@ function inferIdempotency(sideEffect, action, usage) {
5297
5627
  return "if-not-exists";
5298
5628
  if (action.startsWith("delete") && usage.includes("--if-exists"))
5299
5629
  return "if-exists";
5630
+ if (/^(clear|refresh|set|save)/.test(action))
5631
+ return "convergent";
5300
5632
  return "none";
5301
5633
  }
5302
5634
  function inferCleanupHint(resource, action) {
@@ -5327,12 +5659,18 @@ function buildRegistryEntry(resource, action, meta) {
5327
5659
  ...(requiresAuth ? AUTHENTICATED_AGENT_FLAGS : []),
5328
5660
  ...(requiresProject ? ["project-key",] : []),
5329
5661
  ]);
5662
+ const derivedRequired = deriveRequiredUsage(meta.usage);
5330
5663
  const requiredFlags = meta.requiredFlags
5331
5664
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.requiredFlags
5332
- ?? extractRequiredUsageFlags(meta.usage);
5665
+ ?? derivedRequired.requiredFlags;
5666
+ const requiredOneOf = meta.requiredOneOf
5667
+ ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.requiredOneOf
5668
+ ?? derivedRequired.requiredOneOf;
5669
+ const oneOfFlags = new Set(requiredOneOf.flatMap((choice) => choice.oneOf.flat()));
5333
5670
  const optionalFlags = meta.optionalFlags
5334
5671
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.optionalFlags
5335
- ?? flags.filter((flag) => !requiredFlags.includes(flag));
5672
+ ?? flags.filter((flag) => !requiredFlags.includes(flag) && !oneOfFlags.has(flag));
5673
+ const valueHints = extractFlagValueHints(meta.usage);
5336
5674
  const inputContract = inferInputContract(meta.usage);
5337
5675
  const cleanupHint = inferCleanupHint(resource, action);
5338
5676
  const payloadSchema = meta.payloadSchema
@@ -5349,7 +5687,20 @@ function buildRegistryEntry(resource, action, meta) {
5349
5687
  usage: meta.usage,
5350
5688
  description: meta.description,
5351
5689
  examples: meta.examples,
5352
- flags: flags.map((name) => ({ name, kind: flagKind(name), })),
5690
+ flags: flags.map((name) => {
5691
+ const kind = flagKind(name);
5692
+ if (kind === "boolean")
5693
+ return { name, kind, };
5694
+ const hint = valueHints.get(name) ?? GLOBAL_FLAG_VALUE_HINTS[name];
5695
+ if (!hint)
5696
+ return { name, kind, };
5697
+ return {
5698
+ name,
5699
+ kind,
5700
+ valueType: hint.valueType,
5701
+ ...(hint.enumValues ? { enumValues: hint.enumValues, } : {}),
5702
+ };
5703
+ }),
5353
5704
  positionals: extractPositionals(meta.usage),
5354
5705
  sideEffect,
5355
5706
  requiresAuth,
@@ -5365,6 +5716,7 @@ function buildRegistryEntry(resource, action, meta) {
5365
5716
  dryRun: meta.usage.includes("--dry-run"),
5366
5717
  requiredFlags: uniqueStrings(requiredFlags),
5367
5718
  optionalFlags: uniqueStrings(optionalFlags),
5719
+ ...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
5368
5720
  ...(payloadSchema ? { payloadSchema, } : {}),
5369
5721
  ...(examplePayload !== undefined ? { examplePayload, } : {}),
5370
5722
  ...(cleanupCommand ? { cleanupCommand, } : {}),
@@ -5388,6 +5740,14 @@ function buildCommandRegistry() {
5388
5740
  examples: COMMANDS_EXAMPLES,
5389
5741
  }),
5390
5742
  };
5743
+ registry.version = {
5744
+ run: buildRegistryEntry("version", "run", {
5745
+ handler: async () => undefined,
5746
+ usage: VERSION_USAGE,
5747
+ description: VERSION_DESCRIPTION,
5748
+ examples: VERSION_EXAMPLES,
5749
+ }),
5750
+ };
5391
5751
  registry["install-skill"] = {
5392
5752
  run: buildRegistryEntry("install-skill", "run", {
5393
5753
  handler: async () => undefined,
@@ -5412,6 +5772,16 @@ function buildCommandRegistry() {
5412
5772
  examples: FIXTURES_EXAMPLES,
5413
5773
  }),
5414
5774
  };
5775
+ registry.batch = {
5776
+ run: buildRegistryEntry("batch", "run", {
5777
+ handler: async () => undefined,
5778
+ usage: BATCH_USAGE,
5779
+ description: BATCH_DESCRIPTION,
5780
+ examples: BATCH_EXAMPLES,
5781
+ examplePayload: BATCH_EXAMPLE_PAYLOAD,
5782
+ payloadSchema: { stdin: true, dataFlag: true, dataFileFlag: true, jsonShape: "array", },
5783
+ }),
5784
+ };
5415
5785
  registry.auth = {};
5416
5786
  for (const [action, meta,] of Object.entries(AUTH_ACTIONS)) {
5417
5787
  registry.auth[action] = buildRegistryEntry("auth", action, {
@@ -5419,6 +5789,7 @@ function buildCommandRegistry() {
5419
5789
  usage: meta.usage,
5420
5790
  description: meta.description,
5421
5791
  examples: meta.examples,
5792
+ requiredFlags: meta.requiredFlags,
5422
5793
  });
5423
5794
  }
5424
5795
  return registry;
@@ -6041,10 +6412,10 @@ async function runCleanup(flags) {
6041
6412
  }
6042
6413
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
6043
6414
  if (!url) {
6044
- throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
6415
+ throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL or pass --url.", "missing_required_flag");
6045
6416
  }
6046
6417
  if (!apiKey) {
6047
- throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
6418
+ throw new UsageError("Missing API key. Set DATAIKU_API_KEY or pass --api-key.", "missing_required_flag");
6048
6419
  }
6049
6420
  const requestTimeoutMs = num(flags["request-timeout"]);
6050
6421
  const retryMaxAttempts = num(flags["retries"]);
@@ -6064,7 +6435,8 @@ async function runCleanup(flags) {
6064
6435
  try {
6065
6436
  const parsed = parseArgs(entry.cleanup.argv);
6066
6437
  const [resource, action, ...args] = parsed.positional;
6067
- if (!resource || !action || !commands[resource]?.[action]) {
6438
+ if (!resource || !action || !isAllowedCleanupAction(resource, action)
6439
+ || !commands[resource]?.[action]) {
6068
6440
  throw new UsageError(`Invalid cleanup argv: ${entry.cleanup.argv.join(" ")}`);
6069
6441
  }
6070
6442
  const result = await commands[resource][action].handler(client, args, parsed.flags);
@@ -6090,35 +6462,128 @@ async function runCleanup(flags) {
6090
6462
  exitCode: failures.length > 0 ? 2 : 0,
6091
6463
  };
6092
6464
  }
6093
- // ---------------------------------------------------------------------------
6094
- // Interactive prompts
6095
- // ---------------------------------------------------------------------------
6096
- function promptLine(label) {
6097
- return new Promise((res, rej) => {
6098
- const rl = createInterface({ input: process.stdin, output: process.stderr, });
6099
- rl.on("close", () => rej(new UsageError("Input closed before a value was provided.")));
6100
- rl.question(label, (answer) => {
6101
- rl.close();
6102
- res(answer.trim());
6103
- });
6465
+ const BATCH_USAGE = "dss batch (--data JSON|--data-file PATH|--stdin) [--continue-on-error] [--dry-run]";
6466
+ const BATCH_DESCRIPTION = "Run a sequence of dss commands from a JSON array of argv arrays. Fail-fast by default; returns one envelope with a per-step ok/result/error and exits non-zero if any step failed.";
6467
+ const BATCH_HINT = 'Pass a JSON array of argv arrays, e.g. [["dataset","list"],["recipe","update","r","--data-file","p.json"]].';
6468
+ const BATCH_EXAMPLE_PAYLOAD = [
6469
+ ["recipe", "set-payload", "compute_orders", "--file", "code.py", "--no-backup",],
6470
+ ["recipe", "update", "compute_orders", "--data-file", "env.json",],
6471
+ ["dataset", "update", "orders", "--data-file", "ds.json",],
6472
+ ];
6473
+ const BATCH_EXAMPLES = [
6474
+ "dss batch --data-file steps.json",
6475
+ "dss batch --stdin --continue-on-error",
6476
+ ];
6477
+ function parseBatchSteps(payload) {
6478
+ if (!Array.isArray(payload)) {
6479
+ throw new UsageError("Batch payload must be a JSON array of command-argument arrays.", "validation_failed", BATCH_HINT, { example: BATCH_EXAMPLE_PAYLOAD, });
6480
+ }
6481
+ return payload.map((step, index) => {
6482
+ if (!Array.isArray(step) || !step.every((token) => typeof token === "string")) {
6483
+ throw new UsageError(`Batch step ${index} must be an array of string arguments.`, "validation_failed", BATCH_HINT);
6484
+ }
6485
+ return step;
6104
6486
  });
6105
6487
  }
6106
- function promptSecret(label) {
6107
- return new Promise((res, rej) => {
6108
- const muted = new Writable({
6109
- write(_chunk, _encoding, cb) {
6110
- cb();
6111
- },
6488
+ async function runBatch(flags) {
6489
+ const payload = unknownJsonInput(flags);
6490
+ if (payload === undefined) {
6491
+ throw new UsageError(`Provide steps via --data, --data-file, or --stdin. Usage: ${BATCH_USAGE}`, "missing_required_flag", BATCH_HINT);
6492
+ }
6493
+ const steps = parseBatchSteps(payload);
6494
+ if (flags["dry-run"] === true) {
6495
+ const planned = steps.map((argv, index) => {
6496
+ const { positional, } = parseArgs(argv);
6497
+ const resource = positional[0];
6498
+ const action = positional[1];
6499
+ const runnable = Boolean(resource && action && commands[resource]?.[action]);
6500
+ return { index, args: argv, resource, action, runnable, };
6112
6501
  });
6113
- const rl = createInterface({ input: process.stdin, output: muted, terminal: true, });
6114
- rl.on("close", () => rej(new UsageError("Input closed before a value was provided.")));
6115
- process.stderr.write(label);
6116
- rl.question("", (answer) => {
6117
- rl.close();
6118
- process.stderr.write("\n");
6119
- res(answer.trim());
6502
+ return {
6503
+ result: { dryRun: true, total: steps.length, steps: planned, },
6504
+ exitCode: planned.every((step) => step.runnable) ? 0 : 1,
6505
+ };
6506
+ }
6507
+ const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
6508
+ if (!url) {
6509
+ throw new UsageError("Missing Dataiku URL.", "missing_required_flag", "Set DATAIKU_URL or pass --url.", {
6510
+ requiredFlags: ["url",],
6511
+ env: ["DATAIKU_URL",],
6120
6512
  });
6513
+ }
6514
+ if (!apiKey) {
6515
+ throw new UsageError("Missing API key.", "missing_required_flag", "Set DATAIKU_API_KEY or pass --api-key.", {
6516
+ requiredFlags: ["api-key",],
6517
+ env: ["DATAIKU_API_KEY",],
6518
+ });
6519
+ }
6520
+ const client = new DataikuClient({
6521
+ url,
6522
+ apiKey,
6523
+ projectKey,
6524
+ verbose: flags["verbose"] === true,
6525
+ requestTimeoutMs: num(flags["request-timeout"]),
6526
+ retryMaxAttempts: num(flags["retries"]),
6527
+ tlsRejectUnauthorized,
6528
+ caCertPath,
6121
6529
  });
6530
+ const continueOnError = flags["continue-on-error"] === true;
6531
+ const results = [];
6532
+ let firstFailureExit;
6533
+ for (let index = 0; index < steps.length; index++) {
6534
+ const argv = steps[index];
6535
+ const { positional, flags: stepFlags, } = parseArgs(argv);
6536
+ const resource = positional[0];
6537
+ const action = positional[1];
6538
+ if (firstFailureExit !== undefined && !continueOnError) {
6539
+ results.push({ index, args: argv, resource, action, ok: null, skipped: true, });
6540
+ continue;
6541
+ }
6542
+ currentCommandContext = {
6543
+ resource,
6544
+ action,
6545
+ projectKey: typeof stepFlags["project-key"] === "string" ? stepFlags["project-key"] : projectKey,
6546
+ };
6547
+ try {
6548
+ if (!resource)
6549
+ throw noCommandError();
6550
+ const resourceActions = commands[resource];
6551
+ if (!resourceActions)
6552
+ throw unknownResourceError(resource);
6553
+ if (!action) {
6554
+ throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action>`);
6555
+ }
6556
+ const meta = resourceActions[action];
6557
+ if (!meta)
6558
+ throw unknownActionError(resource, action, Object.keys(resourceActions));
6559
+ const result = await meta.handler(client, positional.slice(2), stepFlags);
6560
+ const failureExitCode = commandFailureExitCode(result);
6561
+ if (failureExitCode !== undefined)
6562
+ throw new CommandResultFailure(result, failureExitCode);
6563
+ const stepFieldsFlag = stepFlags["fields"];
6564
+ const stepFields = typeof stepFieldsFlag === "string"
6565
+ ? stepFieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0)
6566
+ : [];
6567
+ const stepResult = stepFields.length > 0 ? projectResultFields(result, stepFields) : result;
6568
+ results.push({ index, args: argv, resource, action, ok: true, result: stepResult, });
6569
+ }
6570
+ catch (error) {
6571
+ const envelope = buildErrorReport(error);
6572
+ results.push({ index, args: argv, resource, action, ok: false, error: envelope, });
6573
+ if (firstFailureExit === undefined)
6574
+ firstFailureExit = envelope.exitCode;
6575
+ }
6576
+ }
6577
+ const ok = firstFailureExit === undefined;
6578
+ return {
6579
+ result: {
6580
+ ok,
6581
+ total: steps.length,
6582
+ completed: results.filter((step) => step.ok !== null).length,
6583
+ steps: results,
6584
+ },
6585
+ exitCode: ok ? 0 : firstFailureExit ?? 2,
6586
+ };
6122
6587
  }
6123
6588
  // ---------------------------------------------------------------------------
6124
6589
  // Credential resolution
@@ -6131,12 +6596,15 @@ function resolveCredentials(flags) {
6131
6596
  let apiKey = hasApiKeyFlag ? flags["api-key"] : undefined;
6132
6597
  let projectKey = hasProjectKeyFlag ? flags["project-key"] : undefined;
6133
6598
  const saved = loadCredentials();
6134
- if (!hasUrlFlag)
6135
- url ??= process.env.DATAIKU_URL;
6136
- if (!hasApiKeyFlag)
6137
- apiKey ??= process.env.DATAIKU_API_KEY;
6138
- if (!hasProjectKeyFlag)
6139
- projectKey ??= process.env.DATAIKU_PROJECT_KEY;
6599
+ const useEnv = dataikuEnvironmentEnabled();
6600
+ if (useEnv) {
6601
+ if (!hasUrlFlag)
6602
+ url ??= process.env.DATAIKU_URL;
6603
+ if (!hasApiKeyFlag)
6604
+ apiKey ??= process.env.DATAIKU_API_KEY;
6605
+ if (!hasProjectKeyFlag)
6606
+ projectKey ??= process.env.DATAIKU_PROJECT_KEY;
6607
+ }
6140
6608
  if (saved) {
6141
6609
  if (!hasUrlFlag)
6142
6610
  url ??= saved.url;
@@ -6153,10 +6621,6 @@ function resolveCredentials(flags) {
6153
6621
  };
6154
6622
  }
6155
6623
  let currentCommandContext = {};
6156
- function isReportJsonRequested() {
6157
- return process.env.DSS_REPORT_JSON === "1"
6158
- || process.argv.slice(2).some((arg) => arg === "--report-json" || arg.startsWith("--report-json="));
6159
- }
6160
6624
  function rawFlagValue(argv, flagName) {
6161
6625
  const longFlag = `--${flagName}`;
6162
6626
  for (let index = 0; index < argv.length; index++) {
@@ -6170,6 +6634,12 @@ function rawFlagValue(argv, flagName) {
6170
6634
  }
6171
6635
  return undefined;
6172
6636
  }
6637
+ function commandIsProjectScoped(resource, action) {
6638
+ if (!resource)
6639
+ return false;
6640
+ const usage = commands[resource]?.[action ?? ""]?.usage ?? "";
6641
+ return inferRequiresProject(resource, action ?? "", usage);
6642
+ }
6173
6643
  function rawCommandContext() {
6174
6644
  const argv = process.argv.slice(2);
6175
6645
  const positionals = [];
@@ -6194,13 +6664,19 @@ function rawCommandContext() {
6194
6664
  }
6195
6665
  positionals.push(arg);
6196
6666
  }
6667
+ const resource = currentCommandContext.resource ?? positionals[0];
6668
+ const action = currentCommandContext.action ?? positionals[1];
6669
+ const explicitProjectKey = rawFlagValue(argv, "project-key") ?? rawFlagValue(argv, "project");
6670
+ const ambientProjectKey = dataikuEnvironmentEnabled()
6671
+ ? process.env.DATAIKU_PROJECT_KEY
6672
+ : undefined;
6197
6673
  return {
6198
- resource: currentCommandContext.resource ?? positionals[0],
6199
- action: currentCommandContext.action ?? positionals[1],
6200
- projectKey: currentCommandContext.projectKey
6201
- ?? rawFlagValue(argv, "project-key")
6202
- ?? rawFlagValue(argv, "project")
6203
- ?? process.env.DATAIKU_PROJECT_KEY,
6674
+ resource,
6675
+ action,
6676
+ projectKey: explicitProjectKey
6677
+ ?? (commandIsProjectScoped(resource, action)
6678
+ ? currentCommandContext.projectKey ?? ambientProjectKey
6679
+ : undefined),
6204
6680
  };
6205
6681
  }
6206
6682
  function requestIdFromBody(body) {
@@ -6213,26 +6689,52 @@ function requestIdFromBody(body) {
6213
6689
  return undefined;
6214
6690
  }
6215
6691
  }
6692
+ function errorExitCode(err) {
6693
+ if (err instanceof CommandResultFailure)
6694
+ return err.exitCode;
6695
+ if (err instanceof UsageError)
6696
+ return 1;
6697
+ if (err instanceof DataikuError)
6698
+ return err.category === "transient" ? 3 : 2;
6699
+ return 2;
6700
+ }
6216
6701
  function buildErrorReport(err) {
6217
6702
  const context = rawCommandContext();
6703
+ const exitCode = errorExitCode(err);
6218
6704
  if (err instanceof UsageError) {
6219
6705
  return {
6706
+ ok: false,
6707
+ error: err.message,
6220
6708
  code: err.code,
6221
6709
  category: "usage",
6222
- message: err.message,
6710
+ exitCode,
6223
6711
  ...(err.hint ? { hint: err.hint, } : {}),
6712
+ ...(err.details ? { details: err.details, } : {}),
6713
+ ...context,
6714
+ };
6715
+ }
6716
+ if (err instanceof CommandResultFailure) {
6717
+ return {
6718
+ ok: false,
6719
+ error: err.message,
6720
+ code: "long_running_failure",
6721
+ category: "dss",
6722
+ exitCode: err.exitCode,
6723
+ details: { result: err.result, },
6224
6724
  ...context,
6225
6725
  };
6226
6726
  }
6227
6727
  if (err instanceof DataikuError) {
6228
6728
  return {
6729
+ ok: false,
6730
+ error: err.message,
6229
6731
  code: dataikuErrorCode(err.category),
6230
6732
  category: "dss",
6231
- message: err.message,
6733
+ exitCode,
6232
6734
  hint: err.retryHint,
6233
6735
  status: err.status,
6234
6736
  retryable: err.retryable,
6235
- requestId: requestIdFromBody(err.body),
6737
+ requestId: err.requestId ?? requestIdFromBody(err.body),
6236
6738
  details: {
6237
6739
  dssCategory: err.category,
6238
6740
  statusText: err.statusText,
@@ -6244,190 +6746,110 @@ function buildErrorReport(err) {
6244
6746
  }
6245
6747
  const message = err instanceof Error ? err.message : String(err);
6246
6748
  return {
6749
+ ok: false,
6750
+ error: message,
6247
6751
  code: "internal_error",
6248
6752
  category: "internal",
6249
- message,
6753
+ exitCode,
6250
6754
  ...context,
6251
6755
  };
6252
6756
  }
6253
6757
  function writeErrorReport(err) {
6254
6758
  process.stderr.write(`${JSON.stringify(buildErrorReport(err), null, 2)}\n`);
6255
6759
  }
6256
- function commandRegistryEntry(resource, action) {
6257
- return buildCommandRegistry()[resource]?.[action];
6258
- }
6259
- function writeReportHelp(resource, action) {
6260
- const entry = commandRegistryEntry(resource, action);
6261
- if (entry) {
6262
- process.stderr.write(`${JSON.stringify(entry, null, 2)}\n`);
6263
- return;
6264
- }
6265
- process.stderr.write(`${JSON.stringify({
6266
- code: "usage_error",
6267
- category: "usage",
6268
- message: `No registry entry for ${resource} ${action}.`,
6269
- resource,
6270
- action,
6271
- }, null, 2)}\n`);
6272
- }
6273
6760
  // ---------------------------------------------------------------------------
6274
6761
  // Main
6275
6762
  // ---------------------------------------------------------------------------
6276
6763
  async function main() {
6277
6764
  loadEnvFile();
6278
6765
  const { positional, flags, } = parseArgs(process.argv.slice(2));
6279
- // --version
6280
- if (flags["version"] === true) {
6281
- process.stdout.write(`${CLI_VERSION_LABEL}\n`);
6282
- process.exit(0);
6766
+ const fieldsFlag = flags["fields"];
6767
+ if (typeof fieldsFlag === "string") {
6768
+ const selected = fieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
6769
+ if (selected.length > 0)
6770
+ outputFieldProjection = selected;
6283
6771
  }
6284
- // Top-level help
6285
- if (positional.length === 0 || (positional.length === 0 && flags["help"])) {
6286
- printTopLevelHelp();
6287
- if (flags["help"])
6288
- process.exit(0);
6289
- process.exit(1);
6772
+ if (flags["version"] === true) {
6773
+ writeCommandResult(cliVersionResult());
6774
+ return;
6290
6775
  }
6776
+ if (positional.length === 0)
6777
+ throw noCommandError();
6291
6778
  const resource = positional[0];
6292
6779
  currentCommandContext = {
6293
6780
  resource,
6294
6781
  action: positional[1],
6295
6782
  projectKey: typeof flags["project-key"] === "string"
6296
6783
  ? flags["project-key"]
6297
- : process.env.DATAIKU_PROJECT_KEY,
6784
+ : dataikuEnvironmentEnabled()
6785
+ ? process.env.DATAIKU_PROJECT_KEY
6786
+ : undefined,
6298
6787
  };
6299
6788
  if (resource === "doctor") {
6300
6789
  const action = positional[1];
6301
- if (flags["help"] === true) {
6302
- if (flags["report-json"] === true)
6303
- writeReportHelp("doctor", "run");
6304
- else
6305
- printActionHelp("doctor", "run");
6306
- process.exit(0);
6307
- }
6790
+ currentCommandContext.action = action ?? "run";
6308
6791
  if (action !== undefined && action !== "run") {
6309
- throw new UsageError("Usage: dss doctor [--project-key KEY] [--capabilities] [--fast]");
6792
+ throw unknownActionError("doctor", action, ["run",]);
6310
6793
  }
6311
6794
  const { result, exitCode, } = await runDoctor(flags);
6312
6795
  writeCommandResult(result);
6313
- process.exit(exitCode);
6796
+ if (exitCode !== 0)
6797
+ process.exit(exitCode);
6798
+ return;
6314
6799
  }
6315
- // Auth commands — dispatched before client creation
6316
6800
  if (resource === "auth") {
6317
6801
  const action = positional[1];
6802
+ const validActions = Object.keys(AUTH_ACTIONS);
6318
6803
  if (!action) {
6319
- const maxName = Math.max(...Object.keys(AUTH_ACTIONS).map((n) => n.length));
6320
- const lines = [
6321
- "Usage: dss auth <action> [--flags]",
6322
- "",
6323
- "Actions:",
6324
- ...Object.entries(AUTH_ACTIONS).map(([name, meta,]) => ` ${name.padEnd(maxName + 2)}${meta.description ?? meta.usage}`),
6325
- "",
6326
- "Run 'dss auth <action> --help' for details and examples.",
6327
- ];
6328
- process.stderr.write(`${lines.join("\n")}\n`);
6329
- process.exit(flags["help"] === true ? 0 : 1);
6804
+ throw missingActionError("auth", validActions, "dss auth login --url URL --api-key KEY");
6330
6805
  }
6806
+ currentCommandContext.action = action;
6331
6807
  const authMeta = AUTH_ACTIONS[action];
6332
6808
  if (!authMeta) {
6333
- if (flags["report-json"] === true) {
6334
- throw new UsageError(`Unknown action: auth ${action}. Available: ${Object.keys(AUTH_ACTIONS).join(", ")}`);
6335
- }
6336
- process.stderr.write(`Unknown action: auth ${action}\nAvailable: ${Object.keys(AUTH_ACTIONS).join(", ")}\n`);
6337
- process.exit(1);
6809
+ throw unknownActionError("auth", action, validActions, "auth only supports 'login'. To check credentials/connectivity, run 'dss doctor'.");
6338
6810
  }
6339
- if (flags["help"] === true) {
6340
- if (flags["report-json"] === true) {
6341
- writeReportHelp("auth", action);
6342
- }
6343
- else {
6344
- const lines = [];
6345
- if (authMeta.description)
6346
- lines.push(authMeta.description, "");
6347
- lines.push(`Usage: ${authMeta.usage}`);
6348
- if (authMeta.examples && authMeta.examples.length > 0) {
6349
- lines.push("", "Examples:");
6350
- for (const ex of authMeta.examples)
6351
- lines.push(` ${ex}`);
6352
- }
6353
- process.stderr.write(`${lines.join("\n")}\n`);
6354
- }
6355
- process.exit(0);
6356
- }
6357
- await authMeta.handler(flags);
6811
+ const result = await authMeta.handler(flags);
6812
+ writeCommandResult(result);
6358
6813
  return;
6359
6814
  }
6360
- // install-skill — dispatched before client creation
6361
6815
  if (resource === "install-skill") {
6362
- const installSkillAction = positional[1];
6363
- if (flags["help"] === true) {
6364
- if (flags["report-json"] === true) {
6365
- writeReportHelp("install-skill", "run");
6366
- }
6367
- else {
6368
- const lines = [
6369
- `Usage: ${INSTALL_SKILL_USAGE}`,
6370
- "",
6371
- INSTALL_SKILL_DESCRIPTION,
6372
- "",
6373
- "Flags:",
6374
- " --global Install to user-level global scope (default: project)",
6375
- " --agent NAME Target a specific agent: claude, codex, cursor, pi, omp",
6376
- " --target PATH Project directory to install into (default: workspace root)",
6377
- " --list-agents Print detected agents and exit",
6378
- " --dry-run Print planned skill installs without writing files",
6379
- " --plan Print planned skill installs without writing files",
6380
- ];
6381
- process.stderr.write(`${lines.join("\n")}\n`);
6382
- }
6383
- process.exit(0);
6384
- }
6385
- if (installSkillAction !== undefined && installSkillAction !== "run") {
6386
- throw new UsageError(`Usage: ${INSTALL_SKILL_USAGE}`);
6816
+ const action = positional[1];
6817
+ currentCommandContext.action = action ?? "run";
6818
+ if (action !== undefined && action !== "run") {
6819
+ throw unknownActionError("install-skill", action, ["run",]);
6387
6820
  }
6388
- const listOnly = flags["list-agents"] === true;
6389
6821
  const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
6390
6822
  const isGlobal = flags["global"] === true;
6391
6823
  const targetDir = typeof flags["target"] === "string" ? flags["target"] : undefined;
6392
- // Resolve target agents
6393
- let targets;
6394
- if (agentFilter) {
6824
+ const targets = (() => {
6825
+ if (!agentFilter)
6826
+ return detectAgents();
6395
6827
  const def = AGENTS[agentFilter];
6396
6828
  if (!def) {
6397
- throw new UsageError(`Unknown agent: ${agentFilter}. Available: ${Object.keys(AGENTS).join(", ")}`);
6398
- }
6399
- targets = [{ id: agentFilter, def, via: "flag", },];
6400
- }
6401
- else {
6402
- targets = detectAgents();
6403
- }
6404
- if (listOnly) {
6405
- if (targets.length === 0) {
6406
- process.stderr.write("No coding agents detected.\n");
6829
+ throw new UsageError(`Unknown agent: ${agentFilter}.`, "usage_error", COMMANDS_RUN_HINT, { agent: agentFilter, validAgents: Object.keys(AGENTS), });
6407
6830
  }
6408
- else {
6409
- process.stderr.write("Detected agents:\n");
6410
- for (const t of targets) {
6411
- process.stderr.write(` ${t.id} (${t.def.name}, via ${t.via})\n`);
6412
- }
6413
- }
6414
- process.exit(0);
6831
+ return [{ id: agentFilter, def, via: "flag", },];
6832
+ })();
6833
+ if (flags["list-agents"] === true) {
6834
+ writeCommandResult({
6835
+ agents: targets.map((target) => ({
6836
+ id: target.id,
6837
+ name: target.def.name,
6838
+ via: target.via,
6839
+ })),
6840
+ });
6841
+ return;
6415
6842
  }
6416
6843
  if (targets.length === 0) {
6417
- throw new UsageError("No coding agents detected. Install one (claude, codex, cursor, pi, omp) or use --agent NAME.");
6844
+ throw new UsageError("No coding agents detected.", "usage_error", "Use --agent NAME to choose one of the supported agents.", { validAgents: Object.keys(AGENTS), });
6418
6845
  }
6419
6846
  const scope = isGlobal ? "global" : "project";
6420
6847
  const cwd = targetDir ?? (isGlobal ? process.cwd() : findWorkspaceRoot(process.cwd()));
6848
+ const installed = planSkillInstalls(targets, { global: isGlobal, cwd, });
6421
6849
  if (flags["plan"] === true) {
6422
6850
  writeCommandResult(planResult("install-skill", "run", {
6423
6851
  identifiers: { scope, target: cwd, },
6424
- payload: {
6425
- agents: targets.map((target) => ({
6426
- id: target.id,
6427
- name: target.def.name,
6428
- via: target.via,
6429
- })),
6430
- },
6852
+ payload: { installed, },
6431
6853
  idempotency: "none",
6432
6854
  asyncKind: "none",
6433
6855
  exitCodesOnFailure: { usage: 1, error: 2, transient: 3, },
@@ -6435,163 +6857,93 @@ async function main() {
6435
6857
  }));
6436
6858
  return;
6437
6859
  }
6438
- if (flags["dry-run"] === true) {
6439
- writeCommandResult({
6440
- dryRun: true,
6441
- action: "install-skill",
6442
- resource: "install-skill",
6443
- scope,
6444
- target: cwd,
6445
- agents: targets.map((target) => ({
6446
- id: target.id,
6447
- name: target.def.name,
6448
- via: target.via,
6449
- })),
6450
- });
6451
- return;
6452
- }
6453
- process.stderr.write(`Installing dataiku-dss skill (${scope} scope):\n`);
6454
- const results = installSkill(targets, { global: isGlobal, cwd, });
6455
- for (const r of results) {
6456
- process.stderr.write(` ${r.agent} -> ${r.path}\n`);
6457
- }
6458
- if (results.length > 0) {
6459
- process.stderr.write(`\nDone. ${results.length} skill(s) installed.\n`);
6460
- }
6860
+ writeCommandResult({
6861
+ scope,
6862
+ target: cwd,
6863
+ installed: flags["dry-run"] === true
6864
+ ? installed
6865
+ : installSkill(targets, { global: isGlobal, cwd, }),
6866
+ ...(flags["dry-run"] === true ? { dryRun: true, } : {}),
6867
+ });
6461
6868
  return;
6462
6869
  }
6463
- // commands — machine-readable introspection (no auth needed)
6464
6870
  if (resource === "commands") {
6465
6871
  const action = positional[1];
6466
- if (flags["help"] === true) {
6467
- if (flags["report-json"] === true) {
6468
- writeReportHelp("commands", "run");
6469
- }
6470
- else {
6471
- const lines = [
6472
- `Usage: ${COMMANDS_USAGE}`,
6473
- "",
6474
- COMMANDS_DESCRIPTION,
6475
- "",
6476
- "Examples:",
6477
- ...COMMANDS_EXAMPLES.map((example) => ` ${example}`),
6478
- ];
6479
- process.stderr.write(`${lines.join("\n")}\n`);
6480
- }
6481
- process.exit(0);
6482
- }
6872
+ if (!action)
6873
+ throw missingActionError("commands", ["run",], COMMANDS_USAGE);
6874
+ currentCommandContext.action = action;
6875
+ if (action !== "run")
6876
+ throw unknownActionError("commands", action, ["run",]);
6877
+ writeCommandResult(buildCommandRegistry());
6878
+ return;
6879
+ }
6880
+ if (resource === "version") {
6881
+ const action = positional[1];
6882
+ currentCommandContext.action = action ?? "run";
6483
6883
  if (action !== undefined && action !== "run") {
6484
- throw new UsageError(`Usage: ${COMMANDS_USAGE}`);
6884
+ throw unknownActionError("version", action, ["run",]);
6485
6885
  }
6486
- writeCommandResult(buildCommandRegistry());
6886
+ writeCommandResult(cliVersionResult());
6487
6887
  return;
6488
6888
  }
6489
6889
  if (resource === "cleanup") {
6490
6890
  const action = positional[1];
6491
- if (flags["help"] === true) {
6492
- if (flags["report-json"] === true) {
6493
- writeReportHelp("cleanup", "run");
6494
- }
6495
- else {
6496
- const lines = [
6497
- `Usage: ${CLEANUP_USAGE}`,
6498
- "",
6499
- CLEANUP_DESCRIPTION,
6500
- "",
6501
- "Examples:",
6502
- ...CLEANUP_EXAMPLES.map((example) => ` ${example}`),
6503
- ];
6504
- process.stderr.write(`${lines.join("\n")}\n`);
6505
- }
6506
- process.exit(0);
6507
- }
6891
+ currentCommandContext.action = action ?? "run";
6508
6892
  if (action !== undefined && action !== "run") {
6509
- throw new UsageError(`Usage: ${CLEANUP_USAGE}`);
6893
+ throw unknownActionError("cleanup", action, ["run",]);
6510
6894
  }
6511
6895
  const { result, exitCode, } = await runCleanup(flags);
6512
6896
  writeCommandResult(result);
6513
- process.exit(exitCode);
6897
+ if (exitCode !== 0)
6898
+ process.exit(exitCode);
6899
+ return;
6514
6900
  }
6515
6901
  if (resource === "fixtures") {
6516
6902
  const action = positional[1];
6517
- currentCommandContext.action = "run";
6518
- if (flags["help"] === true) {
6519
- if (flags["report-json"] === true) {
6520
- writeReportHelp("fixtures", "run");
6521
- }
6522
- else {
6523
- const lines = [
6524
- `Usage: ${FIXTURES_USAGE}`,
6525
- "",
6526
- FIXTURES_DESCRIPTION,
6527
- "",
6528
- "Examples:",
6529
- ...FIXTURES_EXAMPLES.map((example) => ` ${example}`),
6530
- ];
6531
- process.stderr.write(`${lines.join("\n")}\n`);
6532
- }
6533
- process.exit(0);
6534
- }
6903
+ currentCommandContext.action = action ?? "run";
6535
6904
  if (action !== undefined && action !== "run") {
6536
- throw new UsageError(`Usage: ${FIXTURES_USAGE}`);
6905
+ throw unknownActionError("fixtures", action, ["run",]);
6537
6906
  }
6538
6907
  const result = await runFixtures(flags);
6539
6908
  writeCommandResult(result);
6540
6909
  return;
6541
6910
  }
6542
- // Unknown resource
6543
- if (!commands[resource]) {
6544
- if (flags["help"]) {
6545
- printTopLevelHelp();
6546
- process.exit(0);
6547
- }
6548
- if (flags["report-json"] === true) {
6549
- throw new UsageError(`Unknown resource: ${resource}. Available: ${RESOURCE_NAMES.join(", ")}`);
6550
- }
6551
- process.stderr.write(`Unknown resource: ${resource} \nAvailable: ${RESOURCE_NAMES.join(", ")} \n`);
6552
- process.exit(1);
6553
- }
6554
- // Resource-level help
6555
- if (positional.length === 1 || flags["help"] === true) {
6556
- if (positional.length === 1) {
6557
- printResourceHelp(resource);
6558
- if (flags["help"])
6559
- process.exit(0);
6560
- process.exit(1);
6561
- }
6562
- }
6563
- const action = positional[1];
6564
- const actionMeta = commands[resource][action];
6565
- // Unknown action
6566
- if (!actionMeta) {
6567
- if (flags["report-json"] === true) {
6568
- throw new UsageError(`Unknown action: ${resource} ${action}. Available actions for ${resource}: ${Object.keys(commands[resource]).join(", ")}`);
6911
+ if (resource === "batch") {
6912
+ const action = positional[1];
6913
+ currentCommandContext.action = action ?? "run";
6914
+ if (action !== undefined && action !== "run") {
6915
+ throw unknownActionError("batch", action, ["run",]);
6569
6916
  }
6570
- process.stderr.write(`Unknown action: ${resource} ${action} \nAvailable actions for ${resource}: ${Object.keys(commands[resource]).join(", ")} \n`);
6571
- process.exit(1);
6917
+ const { result, exitCode, } = await runBatch(flags);
6918
+ writeCommandResult(result);
6919
+ if (exitCode !== 0)
6920
+ process.exit(exitCode);
6921
+ return;
6572
6922
  }
6573
- // Action-level help
6574
- if (flags["help"] === true) {
6575
- if (flags["report-json"] === true)
6576
- writeReportHelp(resource, action);
6577
- else
6578
- printActionHelp(resource, action);
6579
- process.exit(0);
6923
+ if (!commands[resource])
6924
+ throw unknownResourceError(resource);
6925
+ const resourceActions = commands[resource];
6926
+ if (positional.length === 1) {
6927
+ throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action> [args...]`);
6580
6928
  }
6929
+ const action = positional[1];
6930
+ currentCommandContext.action = action;
6931
+ const actionMeta = resourceActions[action];
6932
+ if (!actionMeta)
6933
+ throw unknownActionError(resource, action, Object.keys(resourceActions));
6581
6934
  const args = positional.slice(2);
6582
6935
  if (flags["plan"] === true) {
6583
6936
  const plan = buildMutationPlan(resource, action, actionMeta, args, flags);
6584
6937
  writeCommandResult(plan);
6585
6938
  return;
6586
6939
  }
6587
- // Resolve credentials: flags > env > saved > .env
6588
6940
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
6589
6941
  currentCommandContext.projectKey = projectKey;
6590
6942
  if (!url) {
6591
- throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
6943
+ throw new UsageError("Missing Dataiku URL.", "missing_required_flag", "Set DATAIKU_URL or pass --url.", { requiredFlags: ["url",], env: ["DATAIKU_URL",], });
6592
6944
  }
6593
6945
  if (!apiKey) {
6594
- throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
6946
+ throw new UsageError("Missing API key.", "missing_required_flag", "Set DATAIKU_API_KEY or pass --api-key.", { requiredFlags: ["api-key",], env: ["DATAIKU_API_KEY",], });
6595
6947
  }
6596
6948
  const requestTimeoutMs = num(flags["request-timeout"]);
6597
6949
  const retryMaxAttempts = num(flags["retries"]);
@@ -6616,41 +6968,17 @@ async function main() {
6616
6968
  if (entry)
6617
6969
  await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
6618
6970
  }
6619
- if (flags["raw"] === true && typeof result === "string") {
6971
+ const failureExitCode = commandFailureExitCode(result);
6972
+ if (failureExitCode !== undefined)
6973
+ throw new CommandResultFailure(result, failureExitCode);
6974
+ if (flags["raw"] === true && typeof result === "string" && typeof flags["output"] !== "string") {
6620
6975
  process.stdout.write(result);
6621
6976
  }
6622
6977
  else {
6623
6978
  writeCommandResult(result);
6624
6979
  }
6625
- const failureExitCode = commandFailureExitCode(result);
6626
- if (failureExitCode !== undefined)
6627
- process.exit(failureExitCode);
6628
6980
  }
6629
6981
  main().catch((err) => {
6630
- if (isReportJsonRequested()) {
6631
- writeErrorReport(err);
6632
- if (err instanceof UsageError)
6633
- process.exit(1);
6634
- if (err instanceof DataikuError)
6635
- process.exit(err.category === "transient" ? 3 : 2);
6636
- process.exit(2);
6637
- }
6638
- if (err instanceof UsageError) {
6639
- process.stderr.write(`${JSON.stringify({ error: err.message, code: "usage", }, null, 2)}\n`);
6640
- process.exit(1);
6641
- }
6642
- if (err instanceof DataikuError) {
6643
- const payload = {
6644
- error: err.message,
6645
- category: err.category,
6646
- retryable: err.retryable,
6647
- };
6648
- if (err.retryHint)
6649
- payload.retryHint = err.retryHint;
6650
- process.stderr.write(`${JSON.stringify(payload, null, 2)} \n`);
6651
- process.exit(err.category === "transient" ? 3 : 2);
6652
- }
6653
- const message = err instanceof Error ? err.message : String(err);
6654
- process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)} \n`);
6655
- process.exit(1);
6982
+ writeErrorReport(err);
6983
+ process.exit(errorExitCode(err));
6656
6984
  });