dataiku-sdk 0.6.2 → 0.7.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,40 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
1079
1118
  }
1080
1119
  return lines.join("\n");
1081
1120
  }
1121
+ let outputFieldProjection;
1122
+ function resolveFieldPath(source, field) {
1123
+ let current = source;
1124
+ for (const segment of field.split(".")) {
1125
+ if (current === null || typeof current !== "object" || Array.isArray(current))
1126
+ return null;
1127
+ current = current[segment];
1128
+ }
1129
+ return current ?? null;
1130
+ }
1131
+ function pickResultFields(item, fields) {
1132
+ if (!item || typeof item !== "object" || Array.isArray(item))
1133
+ return item;
1134
+ const source = item;
1135
+ const picked = {};
1136
+ for (const field of fields)
1137
+ picked[field] = resolveFieldPath(source, field);
1138
+ return picked;
1139
+ }
1140
+ /**
1141
+ * Project the top-level fields callers asked for via --fields. Arrays are mapped
1142
+ * element-wise; scalars and string results pass through untouched. Requested keys
1143
+ * that are absent become null so every row keeps a stable, predictable shape.
1144
+ */
1145
+ function projectResultFields(result, fields) {
1146
+ if (Array.isArray(result))
1147
+ return result.map((item) => pickResultFields(item, fields));
1148
+ return pickResultFields(result, fields);
1149
+ }
1082
1150
  function writeCommandResult(result) {
1083
- process.stdout.write(`${JSON.stringify(result ?? { ok: true, }, null, 2)}\n`);
1151
+ const projected = outputFieldProjection
1152
+ ? projectResultFields(result, outputFieldProjection)
1153
+ : result;
1154
+ process.stdout.write(`${JSON.stringify(projected ?? { ok: true, }, null, 2)}\n`);
1084
1155
  }
1085
1156
  function transientBodyWithTargetContext(body, target, elapsedMs) {
1086
1157
  try {
@@ -1100,7 +1171,7 @@ function transientBodyWithTargetContext(body, target, elapsedMs) {
1100
1171
  }
1101
1172
  function addTransientTargetContext(error, target, elapsedMs) {
1102
1173
  if (error instanceof DataikuError && error.category === "transient") {
1103
- throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry);
1174
+ throw new DataikuError(error.status, error.statusText, transientBodyWithTargetContext(error.body, target, elapsedMs), error.retry, error.requestId);
1104
1175
  }
1105
1176
  throw error;
1106
1177
  }
@@ -1120,6 +1191,27 @@ function commandFailureExitCode(result) {
1120
1191
  return 4;
1121
1192
  return undefined;
1122
1193
  }
1194
+ class CommandResultFailure extends Error {
1195
+ result;
1196
+ exitCode;
1197
+ constructor(result, exitCode) {
1198
+ super(commandFailureMessage(result));
1199
+ this.name = "CommandResultFailure";
1200
+ this.result = result;
1201
+ this.exitCode = exitCode;
1202
+ }
1203
+ }
1204
+ function commandFailureMessage(result) {
1205
+ if (isFailedWaitResult(result)) {
1206
+ const record = result;
1207
+ const state = typeof record.state === "string" ? record.state : record.outcome;
1208
+ return `Command completed with failed long-running result${state ? `: ${state}` : ""}.`;
1209
+ }
1210
+ if (result && typeof result === "object" && result.unchanged === false) {
1211
+ return "Command completed with failed assertion result.";
1212
+ }
1213
+ return "Command completed with failed result.";
1214
+ }
1123
1215
  function isNotFoundError(error) {
1124
1216
  if (error instanceof DataikuError)
1125
1217
  return error.category === "not_found";
@@ -1140,6 +1232,22 @@ async function readIfExists(reader) {
1140
1232
  function skipResult(resource, id, reason, extra = {}) {
1141
1233
  return { skipped: id, reason, resource, ...extra, };
1142
1234
  }
1235
+ function recipeRoleInputItems(recipe, role) {
1236
+ const inputs = recipe["inputs"];
1237
+ if (!inputs || typeof inputs !== "object")
1238
+ return [];
1239
+ const roleEntry = inputs[role];
1240
+ if (!roleEntry || typeof roleEntry !== "object")
1241
+ return [];
1242
+ const items = roleEntry["items"];
1243
+ return Array.isArray(items) ? items : [];
1244
+ }
1245
+ function recipeInputItemRef(item) {
1246
+ if (!item || typeof item !== "object")
1247
+ return undefined;
1248
+ const ref = item["ref"];
1249
+ return typeof ref === "string" && ref.length > 0 ? ref : undefined;
1250
+ }
1143
1251
  function planResult(resource, action, options) {
1144
1252
  return {
1145
1253
  plan: true,
@@ -1337,7 +1445,6 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
1337
1445
  // Arg parsing
1338
1446
  // ---------------------------------------------------------------------------
1339
1447
  const BOOLEAN_FLAGS = new Set([
1340
- "help",
1341
1448
  "verbose",
1342
1449
  "version",
1343
1450
  "stdin",
@@ -1361,7 +1468,6 @@ const BOOLEAN_FLAGS = new Set([
1361
1468
  "if-not-exists",
1362
1469
  "if-exists",
1363
1470
  "json",
1364
- "report-json",
1365
1471
  "no-wait",
1366
1472
  "force-rebuild",
1367
1473
  "latest",
@@ -1372,9 +1478,11 @@ const BOOLEAN_FLAGS = new Set([
1372
1478
  "allow-same-path",
1373
1479
  "sync",
1374
1480
  "validate-objects",
1481
+ "errors-only",
1482
+ "keep",
1483
+ "full-log",
1375
1484
  ]);
1376
1485
  const SHORT_FLAGS = {
1377
- h: "help",
1378
1486
  v: "verbose",
1379
1487
  V: "version",
1380
1488
  o: "output",
@@ -1389,6 +1497,7 @@ const FLAG_ALIASES = {
1389
1497
  "zone-name": "zone",
1390
1498
  };
1391
1499
  const VALUE_FLAGS = new Set([
1500
+ "fields",
1392
1501
  "activity",
1393
1502
  "agent",
1394
1503
  "api-key",
@@ -1412,6 +1521,7 @@ const VALUE_FLAGS = new Set([
1412
1521
  "database",
1413
1522
  "dataset",
1414
1523
  "file",
1524
+ "env",
1415
1525
  "install-core-packages",
1416
1526
  "folder",
1417
1527
  "input",
@@ -1447,6 +1557,7 @@ const VALUE_FLAGS = new Set([
1447
1557
  "partition",
1448
1558
  "parent",
1449
1559
  "path",
1560
+ "preview",
1450
1561
  "project-key",
1451
1562
  "recipe",
1452
1563
  "request-timeout",
@@ -1454,6 +1565,7 @@ const VALUE_FLAGS = new Set([
1454
1565
  "results-per-page",
1455
1566
  "record-cleanup",
1456
1567
  "rule-id",
1568
+ "role",
1457
1569
  "retries",
1458
1570
  "poll-interval",
1459
1571
  "python-interpreter",
@@ -1497,6 +1609,8 @@ const KNOWN_LONG_FLAGS = new Set([
1497
1609
  ...Object.values(FLAG_ALIASES),
1498
1610
  ]);
1499
1611
  function normalizeLongFlag(rawFlagName) {
1612
+ if (rawFlagName === "help")
1613
+ throw unsupportedHelpFlag();
1500
1614
  const flagName = FLAG_ALIASES[rawFlagName] ?? rawFlagName;
1501
1615
  if (!KNOWN_LONG_FLAGS.has(rawFlagName) && !KNOWN_LONG_FLAGS.has(flagName)) {
1502
1616
  throw new UsageError(`Unknown flag: --${rawFlagName}`, "unknown_flag");
@@ -1563,6 +1677,8 @@ function parseArgs(argv) {
1563
1677
  }
1564
1678
  }
1565
1679
  else {
1680
+ if (arg[1] === "h")
1681
+ throw unsupportedHelpFlag();
1566
1682
  throw new UsageError(`Unknown flag: -${arg[1]}`, "unknown_flag");
1567
1683
  }
1568
1684
  }
@@ -2640,10 +2756,11 @@ const commands = {
2640
2756
  return c.datasets.download(a[0], {
2641
2757
  outputPath: f["output"],
2642
2758
  projectKey: f["project-key"],
2759
+ limit: num(f["limit"]),
2643
2760
  });
2644
2761
  },
2645
- usage: "dss dataset download <name> [--output PATH] [--project-key KEY]",
2646
- description: "Download dataset contents as CSV.",
2762
+ usage: "dss dataset download <name> [--output PATH] [--limit N] [--project-key KEY]",
2763
+ description: "Download up to --limit rows (default 100k) as CSV; returns { path, rows, truncated, limit } so truncation is visible.",
2647
2764
  examples: ["dss dataset download orders", "dss dataset download orders --output ./data/",],
2648
2765
  },
2649
2766
  create: {
@@ -3096,6 +3213,87 @@ const commands = {
3096
3213
  "cat settings.json | dss recipe update compute_orders --stdin",
3097
3214
  ],
3098
3215
  },
3216
+ "add-input": {
3217
+ handler: async (c, a, f) => {
3218
+ requireArgs(a, 2, "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]");
3219
+ const role = f["role"] ?? "main";
3220
+ const pk = f["project-key"];
3221
+ const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
3222
+ const items = recipeRoleInputItems(recipe, role);
3223
+ const present = items.some((item) => recipeInputItemRef(item) === a[1]);
3224
+ if (present) {
3225
+ if (f["if-not-exists"] === true) {
3226
+ return skipResult("recipe", a[0], "exists", { dataset: a[1], role, });
3227
+ }
3228
+ throw new UsageError(`Dataset "${a[1]}" is already a "${role}" input of recipe "${a[0]}".`, "validation_failed");
3229
+ }
3230
+ const nextItems = [...items, { ref: a[1], deps: [], },];
3231
+ const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
3232
+ if (f["dry-run"] === true) {
3233
+ return {
3234
+ dryRun: true,
3235
+ action: "add-input",
3236
+ resource: "recipe",
3237
+ recipe: a[0],
3238
+ dataset: a[1],
3239
+ role,
3240
+ inputs,
3241
+ };
3242
+ }
3243
+ await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
3244
+ return { updated: a[0], resource: "recipe", action: "add-input", role, dataset: a[1], inputs, };
3245
+ },
3246
+ usage: "dss recipe add-input <recipe> <dataset> [--role ROLE] [--if-not-exists] [--dry-run] [--project-key KEY]",
3247
+ description: "Add a dataset as a recipe input by appending one item to the current inputs (no need to resend the whole list).",
3248
+ examples: [
3249
+ "dss recipe add-input compute_orders extra_lookup",
3250
+ "dss recipe add-input compute_orders extra_lookup --if-not-exists --dry-run",
3251
+ ],
3252
+ },
3253
+ "remove-input": {
3254
+ handler: async (c, a, f) => {
3255
+ requireArgs(a, 2, "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]");
3256
+ const role = f["role"] ?? "main";
3257
+ const pk = f["project-key"];
3258
+ const { recipe, } = await c.recipes.get(a[0], { projectKey: pk, });
3259
+ const items = recipeRoleInputItems(recipe, role);
3260
+ const present = items.some((item) => recipeInputItemRef(item) === a[1]);
3261
+ if (!present) {
3262
+ if (f["if-exists"] === true) {
3263
+ return skipResult("recipe", a[0], "missing", { dataset: a[1], role, });
3264
+ }
3265
+ throw new UsageError(`Dataset "${a[1]}" is not a "${role}" input of recipe "${a[0]}".`, "validation_failed");
3266
+ }
3267
+ const nextItems = items.filter((item) => recipeInputItemRef(item) !== a[1]);
3268
+ const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
3269
+ if (f["dry-run"] === true) {
3270
+ return {
3271
+ dryRun: true,
3272
+ action: "remove-input",
3273
+ resource: "recipe",
3274
+ recipe: a[0],
3275
+ dataset: a[1],
3276
+ role,
3277
+ inputs,
3278
+ };
3279
+ }
3280
+ await c.recipes.update(a[0], { recipe: { inputs: { [role]: { items: nextItems, }, }, }, }, pk);
3281
+ return {
3282
+ updated: a[0],
3283
+ resource: "recipe",
3284
+ action: "remove-input",
3285
+ role,
3286
+ dataset: a[1],
3287
+ inputs,
3288
+ };
3289
+ },
3290
+ usage: "dss recipe remove-input <recipe> <dataset> [--role ROLE] [--if-exists] [--dry-run] [--project-key KEY]",
3291
+ description: "Remove a dataset from a recipe's inputs by dropping one item from the current inputs.",
3292
+ examples: [
3293
+ "dss recipe remove-input compute_orders stale_lookup",
3294
+ "dss recipe remove-input compute_orders stale_lookup --if-exists --dry-run",
3295
+ ],
3296
+ },
3099
3297
  "get-payload": {
3100
3298
  handler: async (c, a, f) => {
3101
3299
  requireArgs(a, 1, "dss recipe get-payload <name>");
@@ -3109,7 +3307,7 @@ const commands = {
3109
3307
  return payload;
3110
3308
  },
3111
3309
  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.",
3310
+ description: "Print the recipe code payload as JSON; use --raw for raw bytes, not JSON.",
3113
3311
  examples: [
3114
3312
  "dss recipe get-payload compute_orders --raw",
3115
3313
  "dss recipe get-payload compute_orders -o code.py",
@@ -3123,7 +3321,7 @@ const commands = {
3123
3321
  });
3124
3322
  },
3125
3323
  usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
3126
- description: "Print a recipe code payload; combine with --raw for shell pipes and diffs.",
3324
+ description: "Print a recipe code payload as JSON; use --raw for raw bytes, not JSON.",
3127
3325
  examples: ["dss recipe cat compute_orders --raw",],
3128
3326
  },
3129
3327
  "set-payload": {
@@ -3306,17 +3504,29 @@ const commands = {
3306
3504
  examples: ["dss job summary JOB_ID --max-log-lines 200",],
3307
3505
  },
3308
3506
  log: {
3309
- handler: (c, a, f) => {
3507
+ handler: async (c, a, f) => {
3310
3508
  requireArgs(a, 1, "dss job log <id>");
3311
- return c.jobs.log(a[0], {
3509
+ const logFilter = f["errors-only"] === true
3510
+ ? "errors"
3511
+ : jobLogFilterFromFlag(f["log-filter"]);
3512
+ const log = await c.jobs.log(a[0], {
3312
3513
  activity: f["activity"],
3313
3514
  logId: f["log-id"],
3515
+ logFilter,
3314
3516
  maxLogLines: maxLogLinesFromFlags(f),
3315
3517
  projectKey: f["project-key"],
3316
3518
  });
3519
+ const outputFile = f["output"]
3520
+ ?? f["output-file"];
3521
+ if (!outputFile)
3522
+ return log;
3523
+ const outputPath = resolve(outputFile);
3524
+ await mkdir(dirname(outputPath), { recursive: true, });
3525
+ await writeFile(outputPath, log.endsWith("\n") ? log : `${log}\n`, "utf-8");
3526
+ return outputPath;
3317
3527
  },
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.",
3528
+ 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]",
3529
+ 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
3530
  examples: [
3321
3531
  "dss job log JOB_ID",
3322
3532
  "dss job log JOB_ID --activity main --max-log-lines 200",
@@ -4185,12 +4395,21 @@ const commands = {
4185
4395
  sql: {
4186
4396
  query: {
4187
4397
  handler: async (c, a, f) => {
4188
- const query = resolveSqlInput(a, f);
4189
4398
  const connection = f["connection"];
4190
4399
  const datasetFullName = f["dataset"];
4191
4400
  if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
4192
4401
  throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
4193
4402
  }
4403
+ const outputFile = f["output"]
4404
+ ?? f["output-file"];
4405
+ const previewProvided = f["preview"] !== undefined;
4406
+ if (previewProvided && !outputFile) {
4407
+ throw new UsageError(`--preview requires --output or --output-file. Usage: ${SQL_QUERY_USAGE}`, "validation_failed");
4408
+ }
4409
+ const previewCount = previewProvided
4410
+ ? parseSqlPreviewCount(f["preview"])
4411
+ : DEFAULT_SQL_PREVIEW_ROWS;
4412
+ const query = resolveSqlInput(a, f);
4194
4413
  const result = await c.sql.query({
4195
4414
  query,
4196
4415
  connection,
@@ -4198,8 +4417,6 @@ const commands = {
4198
4417
  database: f["database"],
4199
4418
  projectKey: f["project-key"],
4200
4419
  });
4201
- const outputFile = f["output"]
4202
- ?? f["output-file"];
4203
4420
  if (!outputFile)
4204
4421
  return result;
4205
4422
  const outputPath = resolve(outputFile);
@@ -4210,6 +4427,7 @@ const commands = {
4210
4427
  schema: result.schema,
4211
4428
  columns: result.columns ?? result.schema,
4212
4429
  rowCount: result.rows.length,
4430
+ preview: result.rows.slice(0, previewCount),
4213
4431
  outputPath,
4214
4432
  written: outputPath,
4215
4433
  };
@@ -4221,6 +4439,40 @@ const commands = {
4221
4439
  "dss sql query --sql-file query.sql --connection my_pg",
4222
4440
  "echo 'SELECT 1' | dss sql query --stdin --dataset MYPROJ.orders",
4223
4441
  "dss sql query --sql-file query.sql --connection my_pg --output results.json --request-timeout 120000",
4442
+ "dss sql query --sql-file query.sql --connection my_pg --output results.json --preview 10",
4443
+ ],
4444
+ },
4445
+ },
4446
+ code: {
4447
+ run: {
4448
+ handler: async (c, a, f) => {
4449
+ const script = resolveCodeInput(a, f);
4450
+ const run = await c.scenarios.runScript(script, {
4451
+ envName: f["env"],
4452
+ projectKey: f["project-key"],
4453
+ timeoutMs: num(f["timeout"]),
4454
+ keepScenario: f["keep"] === true,
4455
+ });
4456
+ const result = {
4457
+ outcome: run.outcome,
4458
+ success: run.success,
4459
+ runId: run.runId,
4460
+ elapsedMs: run.elapsedMs,
4461
+ pollCount: run.pollCount,
4462
+ output: run.output ?? "",
4463
+ };
4464
+ if (f["full-log"] === true || run.output === undefined) {
4465
+ result.log = run.log;
4466
+ }
4467
+ return result;
4468
+ },
4469
+ usage: CODE_RUN_USAGE,
4470
+ 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.",
4471
+ examples: [
4472
+ "dss code run --file inspect.py",
4473
+ "dss code run --file inspect.py --env py39_pandas",
4474
+ "cat snippet.py | dss code run --stdin",
4475
+ "dss code run --file inspect.py --full-log",
4224
4476
  ],
4225
4477
  },
4226
4478
  },
@@ -4452,7 +4704,7 @@ const commands = {
4452
4704
  },
4453
4705
  };
4454
4706
  // ---------------------------------------------------------------------------
4455
- // Help
4707
+ // Agent-facing command inventory
4456
4708
  // ---------------------------------------------------------------------------
4457
4709
  const RESOURCE_NAMES = [
4458
4710
  ...Object.keys(commands),
@@ -4463,87 +4715,37 @@ const RESOURCE_NAMES = [
4463
4715
  "install-skill",
4464
4716
  ]
4465
4717
  .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
4718
  // ---------------------------------------------------------------------------
4535
4719
  // Validation
4536
4720
  // ---------------------------------------------------------------------------
4537
4721
  class UsageError extends Error {
4538
4722
  code;
4539
4723
  hint;
4540
- constructor(message, code = "usage_error", hint) {
4724
+ details;
4725
+ constructor(message, code = "usage_error", hint, details) {
4541
4726
  super(message);
4542
4727
  this.name = "UsageError";
4543
4728
  this.code = code;
4544
4729
  this.hint = hint;
4730
+ this.details = details;
4545
4731
  }
4546
4732
  }
4733
+ const COMMANDS_RUN_HINT = "Use `dss commands run` for machine-readable command discovery.";
4734
+ function unsupportedHelpFlag() {
4735
+ return new UsageError("Help screens are not supported.", "usage_error", COMMANDS_RUN_HINT, { command: "dss commands run", });
4736
+ }
4737
+ function noCommandError() {
4738
+ return new UsageError("No command provided.", "usage_error", COMMANDS_RUN_HINT, { command: "dss commands run", resources: RESOURCE_NAMES, });
4739
+ }
4740
+ function missingActionError(resource, validActions, usage) {
4741
+ return new UsageError(`Missing action for ${resource}.`, "usage_error", usage ?? COMMANDS_RUN_HINT, { resource, validActions, });
4742
+ }
4743
+ function unknownResourceError(resource) {
4744
+ return new UsageError(`Unknown resource: ${resource}.`, "usage_error", COMMANDS_RUN_HINT, { resource, validResources: RESOURCE_NAMES, });
4745
+ }
4746
+ function unknownActionError(resource, action, validActions) {
4747
+ return new UsageError(`Unknown action: ${resource} ${action ?? ""}`.trim(), "usage_error", COMMANDS_RUN_HINT, { resource, action, validActions, });
4748
+ }
4547
4749
  function requireArgs(args, count, usage) {
4548
4750
  if (args.length < count) {
4549
4751
  throw new UsageError(`Expected ${count} argument(s), got ${args.length}.\nUsage: ${usage}`, "missing_required_arg");
@@ -4552,12 +4754,18 @@ function requireArgs(args, count, usage) {
4552
4754
  // ---------------------------------------------------------------------------
4553
4755
  // .env auto-loading
4554
4756
  // ---------------------------------------------------------------------------
4757
+ function dataikuEnvironmentEnabled() {
4758
+ return process.env.DATAIKU_DISABLE_ENV !== "1";
4759
+ }
4555
4760
  function loadEnvFile() {
4556
- if (process.env.DATAIKU_DISABLE_ENV === "1")
4761
+ if (!dataikuEnvironmentEnabled())
4557
4762
  return;
4763
+ // The invocation cwd takes precedence over the CLI install/root directory, so a
4764
+ // project-local .env where `dss` is invoked overrides defaults shipped beside the
4765
+ // CLI. First writer wins below, so cwd must be listed first.
4558
4766
  const dirs = [
4559
- resolve(dirname(fileURLToPath(import.meta.url)), ".."),
4560
4767
  process.cwd(),
4768
+ resolve(dirname(fileURLToPath(import.meta.url)), ".."),
4561
4769
  ];
4562
4770
  for (const dir of dirs) {
4563
4771
  try {
@@ -4587,81 +4795,42 @@ const AUTH_ACTIONS = {
4587
4795
  login: {
4588
4796
  handler: async (flags) => {
4589
4797
  const tlsSettings = resolveTlsSettings(flags);
4590
- let { url, apiKey, projectKey, } = resolveCredentials(flags);
4798
+ const useEnv = dataikuEnvironmentEnabled();
4799
+ const url = typeof flags["url"] === "string"
4800
+ ? flags["url"]
4801
+ : useEnv
4802
+ ? process.env.DATAIKU_URL ?? ""
4803
+ : "";
4804
+ const apiKey = typeof flags["api-key"] === "string"
4805
+ ? flags["api-key"]
4806
+ : useEnv
4807
+ ? process.env.DATAIKU_API_KEY ?? ""
4808
+ : "";
4809
+ const projectKey = typeof flags["project-key"] === "string"
4810
+ ? flags["project-key"]
4811
+ : useEnv
4812
+ ? process.env.DATAIKU_PROJECT_KEY
4813
+ : undefined;
4591
4814
  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;
4815
+ 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
4816
  }
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
4817
  const result = await validateCredentials(url, apiKey, tlsSettings);
4608
4818
  if (!result.valid) {
4609
- process.stderr.write("Failed\n");
4610
4819
  if (result.dataikuError)
4611
4820
  throw result.dataikuError;
4612
4821
  throw new DataikuError(0, "Authentication Failed", result.error ?? "Credential validation failed");
4613
4822
  }
4614
- process.stderr.write("Connected\n");
4823
+ const path = getCredentialsPath();
4615
4824
  saveCredentials({ url, apiKey, projectKey, ...tlsSettings, });
4616
- process.stderr.write(`Credentials saved to ${getCredentialsPath()}\n`);
4825
+ return { saved: true, path, };
4617
4826
  },
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).",
4827
+ usage: "dss auth login --url URL --api-key KEY [--project-key KEY] [--insecure] [--ca-cert PATH]",
4828
+ description: "Validate and save DSS credentials from flags or environment variables.",
4620
4829
  examples: [
4621
4830
  "dss auth login --url https://dss.example.com --api-key YOUR_KEY",
4622
4831
  "dss auth login --url https://dss.example.com --api-key YOUR_KEY --project-key MYPROJ",
4623
4832
  ],
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",],
4833
+ requiredFlags: ["url", "api-key",],
4665
4834
  },
4666
4835
  };
4667
4836
  function errorDetails(error) {
@@ -4913,7 +5082,7 @@ async function runDoctor(flags) {
4913
5082
  ok: credentialsOk,
4914
5083
  message: credentialsOk
4915
5084
  ? "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.",
5085
+ : "Missing Dataiku URL and/or API key. Set DATAIKU_URL/DATAIKU_API_KEY or pass --url/--api-key.",
4917
5086
  });
4918
5087
  let accessibleProjects;
4919
5088
  if (credentialsOk) {
@@ -4992,13 +5161,13 @@ async function runDoctor(flags) {
4992
5161
  async function runFixtures(flags) {
4993
5162
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
4994
5163
  if (!url) {
4995
- throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
5164
+ throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL or pass --url.", "missing_required_flag");
4996
5165
  }
4997
5166
  if (!apiKey) {
4998
- throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
5167
+ throw new UsageError("Missing API key. Set DATAIKU_API_KEY or pass --api-key.", "missing_required_flag");
4999
5168
  }
5000
5169
  if (!projectKey) {
5001
- throw new UsageError("Missing project key. Set DATAIKU_PROJECT_KEY, pass --project-key, or run: dss auth login", "missing_required_flag");
5170
+ throw new UsageError("Missing project key. Set DATAIKU_PROJECT_KEY or pass --project-key.", "missing_required_flag");
5002
5171
  }
5003
5172
  currentCommandContext.projectKey = projectKey;
5004
5173
  const requestTimeoutMs = num(flags["request-timeout"]);
@@ -5072,7 +5241,7 @@ const PROJECT_SCOPED_RESOURCES = new Set([
5072
5241
  "variable",
5073
5242
  "wiki",
5074
5243
  ]);
5075
- const GLOBAL_AGENT_FLAGS = ["help", "json", "report-json", "verbose",];
5244
+ const GLOBAL_AGENT_FLAGS = ["json", "verbose", "fields",];
5076
5245
  const AUTHENTICATED_AGENT_FLAGS = [
5077
5246
  "url",
5078
5247
  "api-key",
@@ -5081,9 +5250,12 @@ const AUTHENTICATED_AGENT_FLAGS = [
5081
5250
  "insecure",
5082
5251
  "ca-cert",
5083
5252
  ];
5084
- const COMMANDS_USAGE = "dss commands [--json]";
5253
+ const COMMANDS_USAGE = "dss commands run [--json]";
5085
5254
  const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
5086
- const COMMANDS_EXAMPLES = ["dss commands", "dss commands --json",];
5255
+ const COMMANDS_EXAMPLES = ["dss commands run", "dss commands run --json",];
5256
+ const VERSION_USAGE = "dss version";
5257
+ const VERSION_DESCRIPTION = "Print the CLI version and git revision as JSON.";
5258
+ const VERSION_EXAMPLES = ["dss version", "dss --version",];
5087
5259
  const INSTALL_SKILL_USAGE = "dss install-skill [--global] [--agent NAME] [--target PATH] [--list-agents] [--dry-run] [--plan]";
5088
5260
  const INSTALL_SKILL_DESCRIPTION = "Install the dataiku-dss agent skill for detected coding agents.";
5089
5261
  const INSTALL_SKILL_EXAMPLES = [
@@ -5102,6 +5274,22 @@ const FIXTURES_EXAMPLES = [
5102
5274
  "dss fixtures --json",
5103
5275
  "dss fixtures --json --allow-types Filesystem,Inline",
5104
5276
  ];
5277
+ const ALLOWED_CLEANUP_ACTIONS = new Set([
5278
+ // Must mirror every cleanup.argv shape emitted by cleanupLedgerEntry().
5279
+ "dataset delete",
5280
+ "recipe delete",
5281
+ "scenario delete",
5282
+ "flow-zone delete",
5283
+ "wiki delete",
5284
+ "dashboard delete",
5285
+ "insight delete",
5286
+ "data-quality delete-rule",
5287
+ "code-env delete",
5288
+ "folder delete-file",
5289
+ ]);
5290
+ function isAllowedCleanupAction(resource, action) {
5291
+ return ALLOWED_CLEANUP_ACTIONS.has(`${resource} ${action}`);
5292
+ }
5105
5293
  function uniqueStrings(values) {
5106
5294
  return [...new Set(values),];
5107
5295
  }
@@ -5166,26 +5354,33 @@ function extractPositionals(usage) {
5166
5354
  function inferSideEffect(resource, action) {
5167
5355
  if (resource === "auth")
5168
5356
  return "auth";
5169
- if (resource === "doctor" || resource === "commands" || resource === "fixtures")
5357
+ if (resource === "doctor" || resource === "commands" || resource === "fixtures"
5358
+ || resource === "version") {
5170
5359
  return "read";
5360
+ }
5171
5361
  if (resource === "install-skill")
5172
5362
  return "write";
5173
5363
  if (resource === "data-quality" && action === "compute")
5174
5364
  return "write";
5175
5365
  if (READ_ACTIONS.has(action))
5176
5366
  return "read";
5177
- if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout)/
5367
+ if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove)/
5178
5368
  .test(action)) {
5179
5369
  return "write";
5180
5370
  }
5181
5371
  return "read";
5182
5372
  }
5183
5373
  function inferRequiresAuth(resource) {
5184
- return resource !== "auth" && resource !== "commands" && resource !== "install-skill";
5374
+ return resource !== "auth"
5375
+ && resource !== "commands"
5376
+ && resource !== "install-skill"
5377
+ && resource !== "version";
5185
5378
  }
5186
5379
  function inferRequiresProject(resource, action, usage) {
5187
- if (resource === "doctor" || resource === "commands" || resource === "install-skill")
5380
+ if (resource === "auth" || resource === "doctor" || resource === "commands"
5381
+ || resource === "install-skill" || resource === "version") {
5188
5382
  return false;
5383
+ }
5189
5384
  if (PROJECT_SCOPED_RESOURCES.has(resource))
5190
5385
  return true;
5191
5386
  if (resource === "project" && action !== "list")
@@ -5213,13 +5408,16 @@ const STRING_OUTPUT_ACTIONS = new Set([
5213
5408
  "cat",
5214
5409
  "log",
5215
5410
  "log-url",
5216
- "preview",
5217
5411
  ]);
5218
5412
  function inferOutputShape(resource, action) {
5219
- if (resource === "auth" || resource === "install-skill")
5220
- return "void";
5413
+ if (resource === "auth" || resource === "commands" || resource === "install-skill"
5414
+ || resource === "version") {
5415
+ return "object";
5416
+ }
5221
5417
  if (ARRAY_OUTPUT_ACTIONS.has(action))
5222
5418
  return "array";
5419
+ if (resource === "dataset" && action === "download")
5420
+ return "object";
5223
5421
  if (STRING_OUTPUT_ACTIONS.has(action))
5224
5422
  return "string";
5225
5423
  return "object";
@@ -5234,8 +5432,107 @@ function inferInputContract(usage) {
5234
5432
  function stripOptionalUsageGroups(usage) {
5235
5433
  return usage.replace(/\[[^\]]*\]/g, " ");
5236
5434
  }
5237
- function extractRequiredUsageFlags(usage) {
5238
- return extractUsageFlags(stripOptionalUsageGroups(usage));
5435
+ function stripAllUsageGroups(usage) {
5436
+ return usage.replace(/\[[^\]]*\]/g, " ").replace(/\([^)]*\)/g, " ");
5437
+ }
5438
+ function topLevelParenGroups(usage) {
5439
+ const groups = [];
5440
+ let depth = 0;
5441
+ let current = "";
5442
+ for (const char of usage) {
5443
+ if (char === "(") {
5444
+ if (depth > 0)
5445
+ current += char;
5446
+ else
5447
+ current = "";
5448
+ depth++;
5449
+ }
5450
+ else if (char === ")") {
5451
+ depth--;
5452
+ if (depth === 0)
5453
+ groups.push(current);
5454
+ else
5455
+ current += char;
5456
+ }
5457
+ else if (depth > 0) {
5458
+ current += char;
5459
+ }
5460
+ }
5461
+ return groups;
5462
+ }
5463
+ function splitTopLevelChoices(group) {
5464
+ const parts = [];
5465
+ let depth = 0;
5466
+ let current = "";
5467
+ for (const char of group) {
5468
+ if (char === "[" || char === "(")
5469
+ depth++;
5470
+ else if (char === "]" || char === ")")
5471
+ depth--;
5472
+ if (char === "|" && depth === 0) {
5473
+ parts.push(current);
5474
+ current = "";
5475
+ }
5476
+ else {
5477
+ current += char;
5478
+ }
5479
+ }
5480
+ parts.push(current);
5481
+ return parts;
5482
+ }
5483
+ function flagsInUsageFragment(fragment) {
5484
+ return extractUsageFlags(fragment.replace(/\[[^\]]*\]/g, " "));
5485
+ }
5486
+ /**
5487
+ * Split required usage flags into unconditional flags and mutually-exclusive
5488
+ * choice groups. A required `(--a X | --b Y)` group becomes a requiredOneOf entry
5489
+ * (pick exactly one alternative; an alternative listing several flags must be
5490
+ * supplied together) instead of marking every flag as unconditionally required.
5491
+ */
5492
+ function deriveRequiredUsage(usage) {
5493
+ const requiredFlags = extractUsageFlags(stripAllUsageGroups(usage));
5494
+ const requiredOneOf = [];
5495
+ for (const group of topLevelParenGroups(usage)) {
5496
+ const alternatives = splitTopLevelChoices(group);
5497
+ if (alternatives.length <= 1) {
5498
+ requiredFlags.push(...flagsInUsageFragment(group));
5499
+ continue;
5500
+ }
5501
+ const oneOf = alternatives
5502
+ .map((alternative) => flagsInUsageFragment(alternative))
5503
+ .filter((alternativeFlags) => alternativeFlags.length > 0);
5504
+ if (oneOf.length > 1)
5505
+ requiredOneOf.push({ oneOf, });
5506
+ else if (oneOf.length === 1)
5507
+ requiredFlags.push(...oneOf[0]);
5508
+ }
5509
+ return { requiredFlags: uniqueStrings(requiredFlags), requiredOneOf, };
5510
+ }
5511
+ const GLOBAL_FLAG_VALUE_HINTS = {
5512
+ url: { valueType: "URL", },
5513
+ fields: { valueType: "CSV", },
5514
+ "api-key": { valueType: "KEY", },
5515
+ "request-timeout": { valueType: "MS", },
5516
+ retries: { valueType: "N", },
5517
+ "ca-cert": { valueType: "PATH", },
5518
+ "project-key": { valueType: "KEY", },
5519
+ "record-cleanup": { valueType: "PATH", },
5520
+ };
5521
+ /** Derive a value placeholder (and enum members) for each value flag from its usage token. */
5522
+ function extractFlagValueHints(usage) {
5523
+ const hints = new Map();
5524
+ for (const match of usage.matchAll(/--([a-z0-9-]+)\s+([a-z]+(?:\|[a-z]+)+)/g)) {
5525
+ const flag = FLAG_ALIASES[match[1]] ?? match[1];
5526
+ if (!hints.has(flag)) {
5527
+ hints.set(flag, { valueType: "enum", enumValues: match[2].split("|"), });
5528
+ }
5529
+ }
5530
+ for (const match of usage.matchAll(/--([a-z0-9-]+)\s+(<[^>]+>|[A-Z][A-Za-z0-9_]*)/g)) {
5531
+ const flag = FLAG_ALIASES[match[1]] ?? match[1];
5532
+ if (!hints.has(flag))
5533
+ hints.set(flag, { valueType: match[2], });
5534
+ }
5535
+ return hints;
5239
5536
  }
5240
5537
  function inferPayloadSchema(inputContract) {
5241
5538
  if (!inputContract.stdin && !inputContract.dataFlag && !inputContract.dataFileFlag) {
@@ -5288,6 +5585,8 @@ function inferAsyncKind(resource, action) {
5288
5585
  }
5289
5586
  if (resource === "data-quality" && action === "compute")
5290
5587
  return "future";
5588
+ if (resource === "code" && action === "run")
5589
+ return "future";
5291
5590
  return "none";
5292
5591
  }
5293
5592
  function inferIdempotency(sideEffect, action, usage) {
@@ -5297,6 +5596,8 @@ function inferIdempotency(sideEffect, action, usage) {
5297
5596
  return "if-not-exists";
5298
5597
  if (action.startsWith("delete") && usage.includes("--if-exists"))
5299
5598
  return "if-exists";
5599
+ if (/^(clear|refresh|set|save)/.test(action))
5600
+ return "convergent";
5300
5601
  return "none";
5301
5602
  }
5302
5603
  function inferCleanupHint(resource, action) {
@@ -5327,12 +5628,18 @@ function buildRegistryEntry(resource, action, meta) {
5327
5628
  ...(requiresAuth ? AUTHENTICATED_AGENT_FLAGS : []),
5328
5629
  ...(requiresProject ? ["project-key",] : []),
5329
5630
  ]);
5631
+ const derivedRequired = deriveRequiredUsage(meta.usage);
5330
5632
  const requiredFlags = meta.requiredFlags
5331
5633
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.requiredFlags
5332
- ?? extractRequiredUsageFlags(meta.usage);
5634
+ ?? derivedRequired.requiredFlags;
5635
+ const requiredOneOf = meta.requiredOneOf
5636
+ ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.requiredOneOf
5637
+ ?? derivedRequired.requiredOneOf;
5638
+ const oneOfFlags = new Set(requiredOneOf.flatMap((choice) => choice.oneOf.flat()));
5333
5639
  const optionalFlags = meta.optionalFlags
5334
5640
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.optionalFlags
5335
- ?? flags.filter((flag) => !requiredFlags.includes(flag));
5641
+ ?? flags.filter((flag) => !requiredFlags.includes(flag) && !oneOfFlags.has(flag));
5642
+ const valueHints = extractFlagValueHints(meta.usage);
5336
5643
  const inputContract = inferInputContract(meta.usage);
5337
5644
  const cleanupHint = inferCleanupHint(resource, action);
5338
5645
  const payloadSchema = meta.payloadSchema
@@ -5349,7 +5656,20 @@ function buildRegistryEntry(resource, action, meta) {
5349
5656
  usage: meta.usage,
5350
5657
  description: meta.description,
5351
5658
  examples: meta.examples,
5352
- flags: flags.map((name) => ({ name, kind: flagKind(name), })),
5659
+ flags: flags.map((name) => {
5660
+ const kind = flagKind(name);
5661
+ if (kind === "boolean")
5662
+ return { name, kind, };
5663
+ const hint = valueHints.get(name) ?? GLOBAL_FLAG_VALUE_HINTS[name];
5664
+ if (!hint)
5665
+ return { name, kind, };
5666
+ return {
5667
+ name,
5668
+ kind,
5669
+ valueType: hint.valueType,
5670
+ ...(hint.enumValues ? { enumValues: hint.enumValues, } : {}),
5671
+ };
5672
+ }),
5353
5673
  positionals: extractPositionals(meta.usage),
5354
5674
  sideEffect,
5355
5675
  requiresAuth,
@@ -5365,6 +5685,7 @@ function buildRegistryEntry(resource, action, meta) {
5365
5685
  dryRun: meta.usage.includes("--dry-run"),
5366
5686
  requiredFlags: uniqueStrings(requiredFlags),
5367
5687
  optionalFlags: uniqueStrings(optionalFlags),
5688
+ ...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
5368
5689
  ...(payloadSchema ? { payloadSchema, } : {}),
5369
5690
  ...(examplePayload !== undefined ? { examplePayload, } : {}),
5370
5691
  ...(cleanupCommand ? { cleanupCommand, } : {}),
@@ -5388,6 +5709,14 @@ function buildCommandRegistry() {
5388
5709
  examples: COMMANDS_EXAMPLES,
5389
5710
  }),
5390
5711
  };
5712
+ registry.version = {
5713
+ run: buildRegistryEntry("version", "run", {
5714
+ handler: async () => undefined,
5715
+ usage: VERSION_USAGE,
5716
+ description: VERSION_DESCRIPTION,
5717
+ examples: VERSION_EXAMPLES,
5718
+ }),
5719
+ };
5391
5720
  registry["install-skill"] = {
5392
5721
  run: buildRegistryEntry("install-skill", "run", {
5393
5722
  handler: async () => undefined,
@@ -5412,6 +5741,16 @@ function buildCommandRegistry() {
5412
5741
  examples: FIXTURES_EXAMPLES,
5413
5742
  }),
5414
5743
  };
5744
+ registry.batch = {
5745
+ run: buildRegistryEntry("batch", "run", {
5746
+ handler: async () => undefined,
5747
+ usage: BATCH_USAGE,
5748
+ description: BATCH_DESCRIPTION,
5749
+ examples: BATCH_EXAMPLES,
5750
+ examplePayload: BATCH_EXAMPLE_PAYLOAD,
5751
+ payloadSchema: { stdin: true, dataFlag: true, dataFileFlag: true, jsonShape: "array", },
5752
+ }),
5753
+ };
5415
5754
  registry.auth = {};
5416
5755
  for (const [action, meta,] of Object.entries(AUTH_ACTIONS)) {
5417
5756
  registry.auth[action] = buildRegistryEntry("auth", action, {
@@ -5419,6 +5758,7 @@ function buildCommandRegistry() {
5419
5758
  usage: meta.usage,
5420
5759
  description: meta.description,
5421
5760
  examples: meta.examples,
5761
+ requiredFlags: meta.requiredFlags,
5422
5762
  });
5423
5763
  }
5424
5764
  return registry;
@@ -6041,10 +6381,10 @@ async function runCleanup(flags) {
6041
6381
  }
6042
6382
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
6043
6383
  if (!url) {
6044
- throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
6384
+ throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL or pass --url.", "missing_required_flag");
6045
6385
  }
6046
6386
  if (!apiKey) {
6047
- throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
6387
+ throw new UsageError("Missing API key. Set DATAIKU_API_KEY or pass --api-key.", "missing_required_flag");
6048
6388
  }
6049
6389
  const requestTimeoutMs = num(flags["request-timeout"]);
6050
6390
  const retryMaxAttempts = num(flags["retries"]);
@@ -6064,7 +6404,8 @@ async function runCleanup(flags) {
6064
6404
  try {
6065
6405
  const parsed = parseArgs(entry.cleanup.argv);
6066
6406
  const [resource, action, ...args] = parsed.positional;
6067
- if (!resource || !action || !commands[resource]?.[action]) {
6407
+ if (!resource || !action || !isAllowedCleanupAction(resource, action)
6408
+ || !commands[resource]?.[action]) {
6068
6409
  throw new UsageError(`Invalid cleanup argv: ${entry.cleanup.argv.join(" ")}`);
6069
6410
  }
6070
6411
  const result = await commands[resource][action].handler(client, args, parsed.flags);
@@ -6090,35 +6431,128 @@ async function runCleanup(flags) {
6090
6431
  exitCode: failures.length > 0 ? 2 : 0,
6091
6432
  };
6092
6433
  }
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
- });
6434
+ const BATCH_USAGE = "dss batch (--data JSON|--data-file PATH|--stdin) [--continue-on-error] [--dry-run]";
6435
+ 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.";
6436
+ const BATCH_HINT = 'Pass a JSON array of argv arrays, e.g. [["dataset","list"],["recipe","update","r","--data-file","p.json"]].';
6437
+ const BATCH_EXAMPLE_PAYLOAD = [
6438
+ ["recipe", "set-payload", "compute_orders", "--file", "code.py", "--no-backup",],
6439
+ ["recipe", "update", "compute_orders", "--data-file", "env.json",],
6440
+ ["dataset", "update", "orders", "--data-file", "ds.json",],
6441
+ ];
6442
+ const BATCH_EXAMPLES = [
6443
+ "dss batch --data-file steps.json",
6444
+ "dss batch --stdin --continue-on-error",
6445
+ ];
6446
+ function parseBatchSteps(payload) {
6447
+ if (!Array.isArray(payload)) {
6448
+ throw new UsageError("Batch payload must be a JSON array of command-argument arrays.", "validation_failed", BATCH_HINT, { example: BATCH_EXAMPLE_PAYLOAD, });
6449
+ }
6450
+ return payload.map((step, index) => {
6451
+ if (!Array.isArray(step) || !step.every((token) => typeof token === "string")) {
6452
+ throw new UsageError(`Batch step ${index} must be an array of string arguments.`, "validation_failed", BATCH_HINT);
6453
+ }
6454
+ return step;
6104
6455
  });
6105
6456
  }
6106
- function promptSecret(label) {
6107
- return new Promise((res, rej) => {
6108
- const muted = new Writable({
6109
- write(_chunk, _encoding, cb) {
6110
- cb();
6111
- },
6457
+ async function runBatch(flags) {
6458
+ const payload = unknownJsonInput(flags);
6459
+ if (payload === undefined) {
6460
+ throw new UsageError(`Provide steps via --data, --data-file, or --stdin. Usage: ${BATCH_USAGE}`, "missing_required_flag", BATCH_HINT);
6461
+ }
6462
+ const steps = parseBatchSteps(payload);
6463
+ if (flags["dry-run"] === true) {
6464
+ const planned = steps.map((argv, index) => {
6465
+ const { positional, } = parseArgs(argv);
6466
+ const resource = positional[0];
6467
+ const action = positional[1];
6468
+ const runnable = Boolean(resource && action && commands[resource]?.[action]);
6469
+ return { index, args: argv, resource, action, runnable, };
6112
6470
  });
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());
6471
+ return {
6472
+ result: { dryRun: true, total: steps.length, steps: planned, },
6473
+ exitCode: planned.every((step) => step.runnable) ? 0 : 1,
6474
+ };
6475
+ }
6476
+ const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
6477
+ if (!url) {
6478
+ throw new UsageError("Missing Dataiku URL.", "missing_required_flag", "Set DATAIKU_URL or pass --url.", {
6479
+ requiredFlags: ["url",],
6480
+ env: ["DATAIKU_URL",],
6120
6481
  });
6482
+ }
6483
+ if (!apiKey) {
6484
+ throw new UsageError("Missing API key.", "missing_required_flag", "Set DATAIKU_API_KEY or pass --api-key.", {
6485
+ requiredFlags: ["api-key",],
6486
+ env: ["DATAIKU_API_KEY",],
6487
+ });
6488
+ }
6489
+ const client = new DataikuClient({
6490
+ url,
6491
+ apiKey,
6492
+ projectKey,
6493
+ verbose: flags["verbose"] === true,
6494
+ requestTimeoutMs: num(flags["request-timeout"]),
6495
+ retryMaxAttempts: num(flags["retries"]),
6496
+ tlsRejectUnauthorized,
6497
+ caCertPath,
6121
6498
  });
6499
+ const continueOnError = flags["continue-on-error"] === true;
6500
+ const results = [];
6501
+ let firstFailureExit;
6502
+ for (let index = 0; index < steps.length; index++) {
6503
+ const argv = steps[index];
6504
+ const { positional, flags: stepFlags, } = parseArgs(argv);
6505
+ const resource = positional[0];
6506
+ const action = positional[1];
6507
+ if (firstFailureExit !== undefined && !continueOnError) {
6508
+ results.push({ index, args: argv, resource, action, ok: null, skipped: true, });
6509
+ continue;
6510
+ }
6511
+ currentCommandContext = {
6512
+ resource,
6513
+ action,
6514
+ projectKey: typeof stepFlags["project-key"] === "string" ? stepFlags["project-key"] : projectKey,
6515
+ };
6516
+ try {
6517
+ if (!resource)
6518
+ throw noCommandError();
6519
+ const resourceActions = commands[resource];
6520
+ if (!resourceActions)
6521
+ throw unknownResourceError(resource);
6522
+ if (!action) {
6523
+ throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action>`);
6524
+ }
6525
+ const meta = resourceActions[action];
6526
+ if (!meta)
6527
+ throw unknownActionError(resource, action, Object.keys(resourceActions));
6528
+ const result = await meta.handler(client, positional.slice(2), stepFlags);
6529
+ const failureExitCode = commandFailureExitCode(result);
6530
+ if (failureExitCode !== undefined)
6531
+ throw new CommandResultFailure(result, failureExitCode);
6532
+ const stepFieldsFlag = stepFlags["fields"];
6533
+ const stepFields = typeof stepFieldsFlag === "string"
6534
+ ? stepFieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0)
6535
+ : [];
6536
+ const stepResult = stepFields.length > 0 ? projectResultFields(result, stepFields) : result;
6537
+ results.push({ index, args: argv, resource, action, ok: true, result: stepResult, });
6538
+ }
6539
+ catch (error) {
6540
+ const envelope = buildErrorReport(error);
6541
+ results.push({ index, args: argv, resource, action, ok: false, error: envelope, });
6542
+ if (firstFailureExit === undefined)
6543
+ firstFailureExit = envelope.exitCode;
6544
+ }
6545
+ }
6546
+ const ok = firstFailureExit === undefined;
6547
+ return {
6548
+ result: {
6549
+ ok,
6550
+ total: steps.length,
6551
+ completed: results.filter((step) => step.ok !== null).length,
6552
+ steps: results,
6553
+ },
6554
+ exitCode: ok ? 0 : firstFailureExit ?? 2,
6555
+ };
6122
6556
  }
6123
6557
  // ---------------------------------------------------------------------------
6124
6558
  // Credential resolution
@@ -6131,12 +6565,15 @@ function resolveCredentials(flags) {
6131
6565
  let apiKey = hasApiKeyFlag ? flags["api-key"] : undefined;
6132
6566
  let projectKey = hasProjectKeyFlag ? flags["project-key"] : undefined;
6133
6567
  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;
6568
+ const useEnv = dataikuEnvironmentEnabled();
6569
+ if (useEnv) {
6570
+ if (!hasUrlFlag)
6571
+ url ??= process.env.DATAIKU_URL;
6572
+ if (!hasApiKeyFlag)
6573
+ apiKey ??= process.env.DATAIKU_API_KEY;
6574
+ if (!hasProjectKeyFlag)
6575
+ projectKey ??= process.env.DATAIKU_PROJECT_KEY;
6576
+ }
6140
6577
  if (saved) {
6141
6578
  if (!hasUrlFlag)
6142
6579
  url ??= saved.url;
@@ -6153,10 +6590,6 @@ function resolveCredentials(flags) {
6153
6590
  };
6154
6591
  }
6155
6592
  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
6593
  function rawFlagValue(argv, flagName) {
6161
6594
  const longFlag = `--${flagName}`;
6162
6595
  for (let index = 0; index < argv.length; index++) {
@@ -6170,6 +6603,12 @@ function rawFlagValue(argv, flagName) {
6170
6603
  }
6171
6604
  return undefined;
6172
6605
  }
6606
+ function commandIsProjectScoped(resource, action) {
6607
+ if (!resource)
6608
+ return false;
6609
+ const usage = commands[resource]?.[action ?? ""]?.usage ?? "";
6610
+ return inferRequiresProject(resource, action ?? "", usage);
6611
+ }
6173
6612
  function rawCommandContext() {
6174
6613
  const argv = process.argv.slice(2);
6175
6614
  const positionals = [];
@@ -6194,13 +6633,19 @@ function rawCommandContext() {
6194
6633
  }
6195
6634
  positionals.push(arg);
6196
6635
  }
6636
+ const resource = currentCommandContext.resource ?? positionals[0];
6637
+ const action = currentCommandContext.action ?? positionals[1];
6638
+ const explicitProjectKey = rawFlagValue(argv, "project-key") ?? rawFlagValue(argv, "project");
6639
+ const ambientProjectKey = dataikuEnvironmentEnabled()
6640
+ ? process.env.DATAIKU_PROJECT_KEY
6641
+ : undefined;
6197
6642
  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,
6643
+ resource,
6644
+ action,
6645
+ projectKey: explicitProjectKey
6646
+ ?? (commandIsProjectScoped(resource, action)
6647
+ ? currentCommandContext.projectKey ?? ambientProjectKey
6648
+ : undefined),
6204
6649
  };
6205
6650
  }
6206
6651
  function requestIdFromBody(body) {
@@ -6213,26 +6658,52 @@ function requestIdFromBody(body) {
6213
6658
  return undefined;
6214
6659
  }
6215
6660
  }
6661
+ function errorExitCode(err) {
6662
+ if (err instanceof CommandResultFailure)
6663
+ return err.exitCode;
6664
+ if (err instanceof UsageError)
6665
+ return 1;
6666
+ if (err instanceof DataikuError)
6667
+ return err.category === "transient" ? 3 : 2;
6668
+ return 2;
6669
+ }
6216
6670
  function buildErrorReport(err) {
6217
6671
  const context = rawCommandContext();
6672
+ const exitCode = errorExitCode(err);
6218
6673
  if (err instanceof UsageError) {
6219
6674
  return {
6675
+ ok: false,
6676
+ error: err.message,
6220
6677
  code: err.code,
6221
6678
  category: "usage",
6222
- message: err.message,
6679
+ exitCode,
6223
6680
  ...(err.hint ? { hint: err.hint, } : {}),
6681
+ ...(err.details ? { details: err.details, } : {}),
6682
+ ...context,
6683
+ };
6684
+ }
6685
+ if (err instanceof CommandResultFailure) {
6686
+ return {
6687
+ ok: false,
6688
+ error: err.message,
6689
+ code: "long_running_failure",
6690
+ category: "dss",
6691
+ exitCode: err.exitCode,
6692
+ details: { result: err.result, },
6224
6693
  ...context,
6225
6694
  };
6226
6695
  }
6227
6696
  if (err instanceof DataikuError) {
6228
6697
  return {
6698
+ ok: false,
6699
+ error: err.message,
6229
6700
  code: dataikuErrorCode(err.category),
6230
6701
  category: "dss",
6231
- message: err.message,
6702
+ exitCode,
6232
6703
  hint: err.retryHint,
6233
6704
  status: err.status,
6234
6705
  retryable: err.retryable,
6235
- requestId: requestIdFromBody(err.body),
6706
+ requestId: err.requestId ?? requestIdFromBody(err.body),
6236
6707
  details: {
6237
6708
  dssCategory: err.category,
6238
6709
  statusText: err.statusText,
@@ -6244,190 +6715,109 @@ function buildErrorReport(err) {
6244
6715
  }
6245
6716
  const message = err instanceof Error ? err.message : String(err);
6246
6717
  return {
6718
+ ok: false,
6719
+ error: message,
6247
6720
  code: "internal_error",
6248
6721
  category: "internal",
6249
- message,
6722
+ exitCode,
6250
6723
  ...context,
6251
6724
  };
6252
6725
  }
6253
6726
  function writeErrorReport(err) {
6254
6727
  process.stderr.write(`${JSON.stringify(buildErrorReport(err), null, 2)}\n`);
6255
6728
  }
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
6729
  // ---------------------------------------------------------------------------
6274
6730
  // Main
6275
6731
  // ---------------------------------------------------------------------------
6276
6732
  async function main() {
6277
6733
  loadEnvFile();
6278
6734
  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);
6735
+ const fieldsFlag = flags["fields"];
6736
+ if (typeof fieldsFlag === "string") {
6737
+ const selected = fieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
6738
+ if (selected.length > 0)
6739
+ outputFieldProjection = selected;
6283
6740
  }
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);
6741
+ if (flags["version"] === true) {
6742
+ writeCommandResult(cliVersionResult());
6743
+ return;
6290
6744
  }
6745
+ if (positional.length === 0)
6746
+ throw noCommandError();
6291
6747
  const resource = positional[0];
6292
6748
  currentCommandContext = {
6293
6749
  resource,
6294
6750
  action: positional[1],
6295
6751
  projectKey: typeof flags["project-key"] === "string"
6296
6752
  ? flags["project-key"]
6297
- : process.env.DATAIKU_PROJECT_KEY,
6753
+ : dataikuEnvironmentEnabled()
6754
+ ? process.env.DATAIKU_PROJECT_KEY
6755
+ : undefined,
6298
6756
  };
6299
6757
  if (resource === "doctor") {
6300
6758
  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
- }
6759
+ currentCommandContext.action = action ?? "run";
6308
6760
  if (action !== undefined && action !== "run") {
6309
- throw new UsageError("Usage: dss doctor [--project-key KEY] [--capabilities] [--fast]");
6761
+ throw unknownActionError("doctor", action, ["run",]);
6310
6762
  }
6311
6763
  const { result, exitCode, } = await runDoctor(flags);
6312
6764
  writeCommandResult(result);
6313
- process.exit(exitCode);
6765
+ if (exitCode !== 0)
6766
+ process.exit(exitCode);
6767
+ return;
6314
6768
  }
6315
- // Auth commands — dispatched before client creation
6316
6769
  if (resource === "auth") {
6317
6770
  const action = positional[1];
6771
+ const validActions = Object.keys(AUTH_ACTIONS);
6318
6772
  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);
6773
+ throw missingActionError("auth", validActions, "dss auth login --url URL --api-key KEY");
6330
6774
  }
6775
+ currentCommandContext.action = action;
6331
6776
  const authMeta = AUTH_ACTIONS[action];
6332
- 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);
6338
- }
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);
6777
+ if (!authMeta)
6778
+ throw unknownActionError("auth", action, validActions);
6779
+ const result = await authMeta.handler(flags);
6780
+ writeCommandResult(result);
6358
6781
  return;
6359
6782
  }
6360
- // install-skill — dispatched before client creation
6361
6783
  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}`);
6784
+ const action = positional[1];
6785
+ currentCommandContext.action = action ?? "run";
6786
+ if (action !== undefined && action !== "run") {
6787
+ throw unknownActionError("install-skill", action, ["run",]);
6387
6788
  }
6388
- const listOnly = flags["list-agents"] === true;
6389
6789
  const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
6390
6790
  const isGlobal = flags["global"] === true;
6391
6791
  const targetDir = typeof flags["target"] === "string" ? flags["target"] : undefined;
6392
- // Resolve target agents
6393
- let targets;
6394
- if (agentFilter) {
6792
+ const targets = (() => {
6793
+ if (!agentFilter)
6794
+ return detectAgents();
6395
6795
  const def = AGENTS[agentFilter];
6396
6796
  if (!def) {
6397
- throw new UsageError(`Unknown agent: ${agentFilter}. Available: ${Object.keys(AGENTS).join(", ")}`);
6797
+ throw new UsageError(`Unknown agent: ${agentFilter}.`, "usage_error", COMMANDS_RUN_HINT, { agent: agentFilter, validAgents: Object.keys(AGENTS), });
6398
6798
  }
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");
6407
- }
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);
6799
+ return [{ id: agentFilter, def, via: "flag", },];
6800
+ })();
6801
+ if (flags["list-agents"] === true) {
6802
+ writeCommandResult({
6803
+ agents: targets.map((target) => ({
6804
+ id: target.id,
6805
+ name: target.def.name,
6806
+ via: target.via,
6807
+ })),
6808
+ });
6809
+ return;
6415
6810
  }
6416
6811
  if (targets.length === 0) {
6417
- throw new UsageError("No coding agents detected. Install one (claude, codex, cursor, pi, omp) or use --agent NAME.");
6812
+ throw new UsageError("No coding agents detected.", "usage_error", "Use --agent NAME to choose one of the supported agents.", { validAgents: Object.keys(AGENTS), });
6418
6813
  }
6419
6814
  const scope = isGlobal ? "global" : "project";
6420
6815
  const cwd = targetDir ?? (isGlobal ? process.cwd() : findWorkspaceRoot(process.cwd()));
6816
+ const installed = planSkillInstalls(targets, { global: isGlobal, cwd, });
6421
6817
  if (flags["plan"] === true) {
6422
6818
  writeCommandResult(planResult("install-skill", "run", {
6423
6819
  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
- },
6820
+ payload: { installed, },
6431
6821
  idempotency: "none",
6432
6822
  asyncKind: "none",
6433
6823
  exitCodesOnFailure: { usage: 1, error: 2, transient: 3, },
@@ -6435,163 +6825,93 @@ async function main() {
6435
6825
  }));
6436
6826
  return;
6437
6827
  }
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
- }
6828
+ writeCommandResult({
6829
+ scope,
6830
+ target: cwd,
6831
+ installed: flags["dry-run"] === true
6832
+ ? installed
6833
+ : installSkill(targets, { global: isGlobal, cwd, }),
6834
+ ...(flags["dry-run"] === true ? { dryRun: true, } : {}),
6835
+ });
6461
6836
  return;
6462
6837
  }
6463
- // commands — machine-readable introspection (no auth needed)
6464
6838
  if (resource === "commands") {
6465
6839
  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
- }
6840
+ if (!action)
6841
+ throw missingActionError("commands", ["run",], COMMANDS_USAGE);
6842
+ currentCommandContext.action = action;
6843
+ if (action !== "run")
6844
+ throw unknownActionError("commands", action, ["run",]);
6845
+ writeCommandResult(buildCommandRegistry());
6846
+ return;
6847
+ }
6848
+ if (resource === "version") {
6849
+ const action = positional[1];
6850
+ currentCommandContext.action = action ?? "run";
6483
6851
  if (action !== undefined && action !== "run") {
6484
- throw new UsageError(`Usage: ${COMMANDS_USAGE}`);
6852
+ throw unknownActionError("version", action, ["run",]);
6485
6853
  }
6486
- writeCommandResult(buildCommandRegistry());
6854
+ writeCommandResult(cliVersionResult());
6487
6855
  return;
6488
6856
  }
6489
6857
  if (resource === "cleanup") {
6490
6858
  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
- }
6859
+ currentCommandContext.action = action ?? "run";
6508
6860
  if (action !== undefined && action !== "run") {
6509
- throw new UsageError(`Usage: ${CLEANUP_USAGE}`);
6861
+ throw unknownActionError("cleanup", action, ["run",]);
6510
6862
  }
6511
6863
  const { result, exitCode, } = await runCleanup(flags);
6512
6864
  writeCommandResult(result);
6513
- process.exit(exitCode);
6865
+ if (exitCode !== 0)
6866
+ process.exit(exitCode);
6867
+ return;
6514
6868
  }
6515
6869
  if (resource === "fixtures") {
6516
6870
  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
- }
6871
+ currentCommandContext.action = action ?? "run";
6535
6872
  if (action !== undefined && action !== "run") {
6536
- throw new UsageError(`Usage: ${FIXTURES_USAGE}`);
6873
+ throw unknownActionError("fixtures", action, ["run",]);
6537
6874
  }
6538
6875
  const result = await runFixtures(flags);
6539
6876
  writeCommandResult(result);
6540
6877
  return;
6541
6878
  }
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(", ")}`);
6879
+ if (resource === "batch") {
6880
+ const action = positional[1];
6881
+ currentCommandContext.action = action ?? "run";
6882
+ if (action !== undefined && action !== "run") {
6883
+ throw unknownActionError("batch", action, ["run",]);
6569
6884
  }
6570
- process.stderr.write(`Unknown action: ${resource} ${action} \nAvailable actions for ${resource}: ${Object.keys(commands[resource]).join(", ")} \n`);
6571
- process.exit(1);
6885
+ const { result, exitCode, } = await runBatch(flags);
6886
+ writeCommandResult(result);
6887
+ if (exitCode !== 0)
6888
+ process.exit(exitCode);
6889
+ return;
6572
6890
  }
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);
6891
+ if (!commands[resource])
6892
+ throw unknownResourceError(resource);
6893
+ const resourceActions = commands[resource];
6894
+ if (positional.length === 1) {
6895
+ throw missingActionError(resource, Object.keys(resourceActions), `dss ${resource} <action> [args...]`);
6580
6896
  }
6897
+ const action = positional[1];
6898
+ currentCommandContext.action = action;
6899
+ const actionMeta = resourceActions[action];
6900
+ if (!actionMeta)
6901
+ throw unknownActionError(resource, action, Object.keys(resourceActions));
6581
6902
  const args = positional.slice(2);
6582
6903
  if (flags["plan"] === true) {
6583
6904
  const plan = buildMutationPlan(resource, action, actionMeta, args, flags);
6584
6905
  writeCommandResult(plan);
6585
6906
  return;
6586
6907
  }
6587
- // Resolve credentials: flags > env > saved > .env
6588
6908
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
6589
6909
  currentCommandContext.projectKey = projectKey;
6590
6910
  if (!url) {
6591
- throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
6911
+ throw new UsageError("Missing Dataiku URL.", "missing_required_flag", "Set DATAIKU_URL or pass --url.", { requiredFlags: ["url",], env: ["DATAIKU_URL",], });
6592
6912
  }
6593
6913
  if (!apiKey) {
6594
- throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
6914
+ 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
6915
  }
6596
6916
  const requestTimeoutMs = num(flags["request-timeout"]);
6597
6917
  const retryMaxAttempts = num(flags["retries"]);
@@ -6616,41 +6936,17 @@ async function main() {
6616
6936
  if (entry)
6617
6937
  await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
6618
6938
  }
6619
- if (flags["raw"] === true && typeof result === "string") {
6939
+ const failureExitCode = commandFailureExitCode(result);
6940
+ if (failureExitCode !== undefined)
6941
+ throw new CommandResultFailure(result, failureExitCode);
6942
+ if (flags["raw"] === true && typeof result === "string" && typeof flags["output"] !== "string") {
6620
6943
  process.stdout.write(result);
6621
6944
  }
6622
6945
  else {
6623
6946
  writeCommandResult(result);
6624
6947
  }
6625
- const failureExitCode = commandFailureExitCode(result);
6626
- if (failureExitCode !== undefined)
6627
- process.exit(failureExitCode);
6628
6948
  }
6629
6949
  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);
6950
+ writeErrorReport(err);
6951
+ process.exit(errorExitCode(err));
6656
6952
  });