dataiku-sdk 0.6.1 → 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;
@@ -340,6 +338,336 @@ function flowZoneDetailSummary(zone) {
340
338
  items: flowZoneItems(zone),
341
339
  };
342
340
  }
341
+ function optionalStringField(record, keys) {
342
+ for (const key of keys) {
343
+ const value = record[key];
344
+ if (typeof value === "string" && value.trim().length > 0)
345
+ return value.trim();
346
+ }
347
+ return undefined;
348
+ }
349
+ function requiredStringArray(value, source) {
350
+ if (!Array.isArray(value)) {
351
+ throw new UsageError(`${source} must be an array of strings.`, "validation_failed");
352
+ }
353
+ return value.map((item, index) => {
354
+ if (typeof item !== "string" || item.trim().length === 0) {
355
+ throw new UsageError(`${source}[${index}] must be a non-empty string.`, "validation_failed");
356
+ }
357
+ return item.trim();
358
+ });
359
+ }
360
+ function finiteNumberField(record, key, source) {
361
+ const value = record[key];
362
+ if (typeof value !== "number" || !Number.isFinite(value)) {
363
+ throw new UsageError(`${source}.${key} must be a finite number.`, "validation_failed");
364
+ }
365
+ return value;
366
+ }
367
+ function flowZonePlanColor(value, source) {
368
+ if (value === undefined)
369
+ return undefined;
370
+ if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value.trim())) {
371
+ throw new UsageError(`${source} must be a hex color like #2ab1ac.`, "validation_failed");
372
+ }
373
+ return value.trim();
374
+ }
375
+ function flowZonePlanPosition(value, source) {
376
+ if (value === undefined)
377
+ return undefined;
378
+ const record = plainRecord(value);
379
+ if (!record) {
380
+ throw new UsageError(`${source} must be an object with x and y.`, "validation_failed");
381
+ }
382
+ return {
383
+ x: finiteNumberField(record, "x", source),
384
+ y: finiteNumberField(record, "y", source),
385
+ };
386
+ }
387
+ function flowZoneCurrentPosition(zone) {
388
+ const record = zone;
389
+ const position = plainRecord(record.position);
390
+ if (!position)
391
+ return undefined;
392
+ const x = position.x;
393
+ const y = position.y;
394
+ return typeof x === "number" && Number.isFinite(x) && typeof y === "number" && Number.isFinite(y)
395
+ ? { x, y, }
396
+ : undefined;
397
+ }
398
+ function flowZoneSamePosition(a, b) {
399
+ if (a === undefined || b === undefined)
400
+ return a === b;
401
+ return a.x === b.x && a.y === b.y;
402
+ }
403
+ function parseFlowZonePlanItem(value, source) {
404
+ if (typeof value === "string")
405
+ return parseFlowZoneObject(value);
406
+ const record = plainRecord(value);
407
+ if (!record) {
408
+ throw new UsageError(`${source} must be TYPE:ID or an object.`, "validation_failed");
409
+ }
410
+ const object = optionalStringField(record, ["object",]);
411
+ if (object)
412
+ return parseFlowZoneObject(object);
413
+ const objectType = optionalStringField(record, ["objectType", "type",]);
414
+ const objectId = optionalStringField(record, ["objectId", "id", "name",]);
415
+ if (!objectType || !objectId) {
416
+ throw new UsageError(`${source} must include objectType/type and objectId/id, or object as TYPE:ID.`, "validation_failed");
417
+ }
418
+ const projectKey = optionalStringField(record, ["projectKey", "project",]);
419
+ return {
420
+ objectType: flowZoneObjectType(objectType),
421
+ objectId,
422
+ ...(projectKey ? { projectKey, } : {}),
423
+ };
424
+ }
425
+ function addFlowZonePlanTypedItems(items, record, key, objectType, source) {
426
+ if (record[key] === undefined)
427
+ return;
428
+ for (const objectId of requiredStringArray(record[key], `${source}.${key}`)) {
429
+ items.push({ objectType, objectId, });
430
+ }
431
+ }
432
+ function flowZoneItemKey(item) {
433
+ return `${item.projectKey ?? ""}\0${item.objectType}\0${item.objectId}`;
434
+ }
435
+ function flowZonePlanLabel(plan) {
436
+ return plan.id ?? plan.name ?? "<unknown>";
437
+ }
438
+ function dedupeFlowZonePlanItems(items) {
439
+ const seen = new Set();
440
+ const result = [];
441
+ for (const item of items) {
442
+ const key = flowZoneItemKey(item);
443
+ if (seen.has(key))
444
+ continue;
445
+ seen.add(key);
446
+ result.push(item);
447
+ }
448
+ return result;
449
+ }
450
+ function flowZonePlanItemKeys(plan) {
451
+ const keys = new Set();
452
+ for (const zone of plan.zones) {
453
+ for (const item of zone.items)
454
+ keys.add(flowZoneItemKey(item));
455
+ }
456
+ return keys;
457
+ }
458
+ function validateUniqueFlowZoneAssignments(plan) {
459
+ const seen = new Map();
460
+ for (const zone of plan.zones) {
461
+ const label = flowZonePlanLabel(zone);
462
+ for (const item of zone.items) {
463
+ const key = flowZoneItemKey(item);
464
+ const previous = seen.get(key);
465
+ if (previous) {
466
+ throw new UsageError(`Flow object ${item.objectType}:${item.objectId} is assigned to both "${previous}" and "${label}".`, "validation_failed");
467
+ }
468
+ seen.set(key, label);
469
+ }
470
+ }
471
+ }
472
+ function parseFlowZoneOrganizePlan(input) {
473
+ const zones = input.zones;
474
+ if (!Array.isArray(zones) || zones.length === 0) {
475
+ throw new UsageError("Flow zone organize plan must include a non-empty zones array.", "validation_failed");
476
+ }
477
+ const plan = {
478
+ zones: zones.map((value, index) => {
479
+ const source = `zones[${index}]`;
480
+ const record = plainRecord(value);
481
+ if (!record)
482
+ throw new UsageError(`${source} must be an object.`, "validation_failed");
483
+ const id = optionalStringField(record, ["id", "zoneId",]);
484
+ const name = optionalStringField(record, ["name",]);
485
+ if (!id && !name) {
486
+ throw new UsageError(`${source} must include name or id.`, "validation_failed");
487
+ }
488
+ const items = [];
489
+ const rawItems = record.items ?? record.objects;
490
+ if (rawItems !== undefined) {
491
+ if (!Array.isArray(rawItems)) {
492
+ throw new UsageError(`${source}.items must be an array.`, "validation_failed");
493
+ }
494
+ rawItems.forEach((item, itemIndex) => {
495
+ items.push(parseFlowZonePlanItem(item, `${source}.items[${itemIndex}]`));
496
+ });
497
+ }
498
+ addFlowZonePlanTypedItems(items, record, "datasets", "DATASET", source);
499
+ addFlowZonePlanTypedItems(items, record, "recipes", "RECIPE", source);
500
+ addFlowZonePlanTypedItems(items, record, "folders", "MANAGED_FOLDER", source);
501
+ addFlowZonePlanTypedItems(items, record, "savedModels", "SAVED_MODEL", source);
502
+ addFlowZonePlanTypedItems(items, record, "modelEvaluationStores", "MODEL_EVALUATION_STORE", source);
503
+ addFlowZonePlanTypedItems(items, record, "streamingEndpoints", "STREAMING_ENDPOINT", source);
504
+ addFlowZonePlanTypedItems(items, record, "labelingTasks", "LABELING_TASK", source);
505
+ addFlowZonePlanTypedItems(items, record, "knowledgeBanks", "RETRIEVABLE_KNOWLEDGE", source);
506
+ return {
507
+ ...(id ? { id, } : {}),
508
+ ...(name ? { name, } : {}),
509
+ ...(record.color !== undefined
510
+ ? { color: flowZonePlanColor(record.color, `${source}.color`), }
511
+ : {}),
512
+ ...(record.position !== undefined
513
+ ? { position: flowZonePlanPosition(record.position, `${source}.position`), }
514
+ : {}),
515
+ items: dedupeFlowZonePlanItems(items),
516
+ };
517
+ }),
518
+ };
519
+ validateUniqueFlowZoneAssignments(plan);
520
+ return plan;
521
+ }
522
+ function readFlowZoneOrganizePlan(flags, usage) {
523
+ const data = typeof flags["file"] === "string"
524
+ ? parseJsonObject(readFileSync(flags["file"], "utf-8"), flags["file"])
525
+ : jsonInput(flags);
526
+ if (!data) {
527
+ throw new UsageError(`--data, --data-file, --file, or --stdin is required. Usage: ${usage}`, "missing_required_flag");
528
+ }
529
+ return parseFlowZoneOrganizePlan(data);
530
+ }
531
+ function findFlowZoneForPlan(zones, plan) {
532
+ if (plan.id) {
533
+ const byId = zones.find((zone) => zone.id === plan.id);
534
+ if (byId)
535
+ return byId;
536
+ }
537
+ if (!plan.name)
538
+ return undefined;
539
+ const byName = zones.filter((zone) => zone.name === plan.name);
540
+ if (byName.length > 1) {
541
+ throw new UsageError(`Multiple flow zones named "${plan.name}" exist; use id.`, "validation_failed");
542
+ }
543
+ return byName[0];
544
+ }
545
+ function ensureFlowZonePlanTarget(plan, existing) {
546
+ if (existing || plan.name)
547
+ return;
548
+ throw new UsageError(`Flow zone ${plan.id ?? "<unknown>"} was not found and cannot be created without name.`, "validation_failed");
549
+ }
550
+ function flowZoneExplicitItems(zone) {
551
+ return (zone.items ?? []).map((item) => ({
552
+ objectId: item.objectId,
553
+ objectType: item.objectType,
554
+ ...(item.projectKey ? { projectKey: item.projectKey, } : {}),
555
+ }));
556
+ }
557
+ function flowZonePruneItems(existing, plannedItemKeys) {
558
+ if (!existing)
559
+ return [];
560
+ return flowZoneExplicitItems(existing).filter((item) => !plannedItemKeys.has(flowZoneItemKey(item)));
561
+ }
562
+ function flowZoneOrganizeStep(plan, existing, sync, plannedItemKeys) {
563
+ ensureFlowZonePlanTarget(plan, existing);
564
+ const update = {};
565
+ if (existing && plan.name && plan.name !== existing.name)
566
+ update.name = plan.name;
567
+ if (existing && plan.color && plan.color !== existing.color)
568
+ update.color = plan.color;
569
+ if (existing && plan.position !== undefined
570
+ && !flowZoneSamePosition(flowZoneCurrentPosition(existing), plan.position)) {
571
+ update.position = plan.position;
572
+ }
573
+ const pruneItems = sync ? flowZonePruneItems(existing, plannedItemKeys) : [];
574
+ return {
575
+ target: {
576
+ ...(plan.id ? { id: plan.id, } : {}),
577
+ ...(plan.name ? { name: plan.name, } : {}),
578
+ ...(plan.color ? { color: plan.color, } : {}),
579
+ ...(plan.position ? { position: plan.position, } : {}),
580
+ },
581
+ ...(existing ? { existing: flowZoneSummary(existing), } : { create: true, }),
582
+ ...(Object.keys(update).length > 0 ? { update, } : {}),
583
+ moveItems: plan.items,
584
+ ...(pruneItems.length > 0 ? { pruneItems, } : {}),
585
+ };
586
+ }
587
+ function flowZoneValidationBucket(index, objectType) {
588
+ switch (objectType) {
589
+ case "DATASET":
590
+ return index.datasets;
591
+ case "RECIPE":
592
+ return index.recipes;
593
+ case "MANAGED_FOLDER":
594
+ return index.folders;
595
+ case "SAVED_MODEL":
596
+ case "MODEL_EVALUATION_STORE":
597
+ case "STREAMING_ENDPOINT":
598
+ case "LABELING_TASK":
599
+ case "RETRIEVABLE_KNOWLEDGE":
600
+ return index.all;
601
+ }
602
+ }
603
+ async function flowZoneValidationIndex(client, projectKey) {
604
+ const result = await client.projects.map({
605
+ projectKey,
606
+ maxNodes: 100_000,
607
+ maxEdges: 100_000,
608
+ });
609
+ const index = {
610
+ projectKey: result.map.projectKey,
611
+ all: new Set(),
612
+ datasets: new Set(),
613
+ recipes: new Set(),
614
+ folders: new Set(),
615
+ };
616
+ for (const node of result.map.nodes) {
617
+ index.all.add(node.id);
618
+ switch (node.kind) {
619
+ case "dataset":
620
+ index.datasets.add(node.id);
621
+ break;
622
+ case "recipe":
623
+ index.recipes.add(node.id);
624
+ break;
625
+ case "folder":
626
+ index.folders.add(node.id);
627
+ break;
628
+ case "other":
629
+ break;
630
+ }
631
+ }
632
+ return index;
633
+ }
634
+ async function validateFlowZoneOrganizeObjects(client, plan, projectKey) {
635
+ const indexes = new Map();
636
+ const missing = [];
637
+ const getIndex = async (itemProjectKey) => {
638
+ const requestedProjectKey = itemProjectKey ?? projectKey;
639
+ const cacheKey = requestedProjectKey ?? "";
640
+ const cached = indexes.get(cacheKey);
641
+ if (cached)
642
+ return cached;
643
+ const index = await flowZoneValidationIndex(client, requestedProjectKey);
644
+ indexes.set(cacheKey, index);
645
+ return index;
646
+ };
647
+ for (const zone of plan.zones) {
648
+ for (const item of zone.items) {
649
+ const index = await getIndex(item.projectKey);
650
+ const bucket = flowZoneValidationBucket(index, item.objectType);
651
+ if (bucket.has(item.objectId))
652
+ continue;
653
+ missing.push({
654
+ zone: flowZonePlanLabel(zone),
655
+ objectId: item.objectId,
656
+ objectType: item.objectType,
657
+ ...(item.projectKey ? { projectKey: item.projectKey, } : {}),
658
+ reason: `Object not found in project ${item.projectKey ?? index.projectKey}.`,
659
+ });
660
+ }
661
+ }
662
+ return { valid: missing.length === 0, missing, };
663
+ }
664
+ function throwFlowZoneValidationError(validation) {
665
+ if (validation.valid)
666
+ return;
667
+ const first = validation.missing[0];
668
+ const suffix = validation.missing.length > 1 ? ` and ${validation.missing.length - 1} more` : "";
669
+ throw new UsageError(`Flow zone organize validation failed: ${first?.objectType}:${first?.objectId} in zone "${first?.zone}" was not found${suffix}.`, "validation_failed");
670
+ }
343
671
  async function resolveFlowZoneIdFromFlags(client, flags, projectKey) {
344
672
  const zoneId = typeof flags["zone-id"] === "string" ? flags["zone-id"].trim() : "";
345
673
  if (zoneId)
@@ -553,7 +881,24 @@ function json(v, source = "JSON flag") {
553
881
  return undefined;
554
882
  return parseJsonObject(v, source);
555
883
  }
556
- 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
+ }
557
902
  function readStdinText() {
558
903
  return readFileSync(0, "utf-8");
559
904
  }
@@ -713,12 +1058,36 @@ function resolveSqlInput(args, flags) {
713
1058
  if (sources.length > 1) {
714
1059
  throw new UsageError(`Choose exactly one SQL input source: --sql, --sql-file, --stdin, or one positional SQL argument. Usage: ${SQL_QUERY_USAGE}`);
715
1060
  }
716
- const query = sources[0].read();
1061
+ const query = stripUtf8Bom(sources[0].read());
717
1062
  if (query.trim().length === 0) {
718
1063
  throw new UsageError(`SQL input from ${sources[0].label} must not be empty. Usage: ${SQL_QUERY_USAGE}`);
719
1064
  }
720
1065
  return query;
721
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
+ }
722
1091
  async function resolveFolderId(client, nameOrId, flags) {
723
1092
  return client.folders.resolveId(nameOrId, flags["project-key"]);
724
1093
  }
@@ -749,8 +1118,40 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
749
1118
  }
750
1119
  return lines.join("\n");
751
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
+ }
752
1150
  function writeCommandResult(result) {
753
- 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`);
754
1155
  }
755
1156
  function transientBodyWithTargetContext(body, target, elapsedMs) {
756
1157
  try {
@@ -770,7 +1171,7 @@ function transientBodyWithTargetContext(body, target, elapsedMs) {
770
1171
  }
771
1172
  function addTransientTargetContext(error, target, elapsedMs) {
772
1173
  if (error instanceof DataikuError && error.category === "transient") {
773
- 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);
774
1175
  }
775
1176
  throw error;
776
1177
  }
@@ -790,6 +1191,27 @@ function commandFailureExitCode(result) {
790
1191
  return 4;
791
1192
  return undefined;
792
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
+ }
793
1215
  function isNotFoundError(error) {
794
1216
  if (error instanceof DataikuError)
795
1217
  return error.category === "not_found";
@@ -810,6 +1232,22 @@ async function readIfExists(reader) {
810
1232
  function skipResult(resource, id, reason, extra = {}) {
811
1233
  return { skipped: id, reason, resource, ...extra, };
812
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
+ }
813
1251
  function planResult(resource, action, options) {
814
1252
  return {
815
1253
  plan: true,
@@ -1007,7 +1445,6 @@ function cleanupLedgerEntry(resource, action, args, flags, result, projectKey) {
1007
1445
  // Arg parsing
1008
1446
  // ---------------------------------------------------------------------------
1009
1447
  const BOOLEAN_FLAGS = new Set([
1010
- "help",
1011
1448
  "verbose",
1012
1449
  "version",
1013
1450
  "stdin",
@@ -1031,7 +1468,6 @@ const BOOLEAN_FLAGS = new Set([
1031
1468
  "if-not-exists",
1032
1469
  "if-exists",
1033
1470
  "json",
1034
- "report-json",
1035
1471
  "no-wait",
1036
1472
  "force-rebuild",
1037
1473
  "latest",
@@ -1040,9 +1476,13 @@ const BOOLEAN_FLAGS = new Set([
1040
1476
  "no-backup",
1041
1477
  "payload-only",
1042
1478
  "allow-same-path",
1479
+ "sync",
1480
+ "validate-objects",
1481
+ "errors-only",
1482
+ "keep",
1483
+ "full-log",
1043
1484
  ]);
1044
1485
  const SHORT_FLAGS = {
1045
- h: "help",
1046
1486
  v: "verbose",
1047
1487
  V: "version",
1048
1488
  o: "output",
@@ -1057,6 +1497,7 @@ const FLAG_ALIASES = {
1057
1497
  "zone-name": "zone",
1058
1498
  };
1059
1499
  const VALUE_FLAGS = new Set([
1500
+ "fields",
1060
1501
  "activity",
1061
1502
  "agent",
1062
1503
  "api-key",
@@ -1080,6 +1521,7 @@ const VALUE_FLAGS = new Set([
1080
1521
  "database",
1081
1522
  "dataset",
1082
1523
  "file",
1524
+ "env",
1083
1525
  "install-core-packages",
1084
1526
  "folder",
1085
1527
  "input",
@@ -1115,6 +1557,7 @@ const VALUE_FLAGS = new Set([
1115
1557
  "partition",
1116
1558
  "parent",
1117
1559
  "path",
1560
+ "preview",
1118
1561
  "project-key",
1119
1562
  "recipe",
1120
1563
  "request-timeout",
@@ -1122,6 +1565,7 @@ const VALUE_FLAGS = new Set([
1122
1565
  "results-per-page",
1123
1566
  "record-cleanup",
1124
1567
  "rule-id",
1568
+ "role",
1125
1569
  "retries",
1126
1570
  "poll-interval",
1127
1571
  "python-interpreter",
@@ -1165,6 +1609,8 @@ const KNOWN_LONG_FLAGS = new Set([
1165
1609
  ...Object.values(FLAG_ALIASES),
1166
1610
  ]);
1167
1611
  function normalizeLongFlag(rawFlagName) {
1612
+ if (rawFlagName === "help")
1613
+ throw unsupportedHelpFlag();
1168
1614
  const flagName = FLAG_ALIASES[rawFlagName] ?? rawFlagName;
1169
1615
  if (!KNOWN_LONG_FLAGS.has(rawFlagName) && !KNOWN_LONG_FLAGS.has(flagName)) {
1170
1616
  throw new UsageError(`Unknown flag: --${rawFlagName}`, "unknown_flag");
@@ -1231,6 +1677,8 @@ function parseArgs(argv) {
1231
1677
  }
1232
1678
  }
1233
1679
  else {
1680
+ if (arg[1] === "h")
1681
+ throw unsupportedHelpFlag();
1234
1682
  throw new UsageError(`Unknown flag: -${arg[1]}`, "unknown_flag");
1235
1683
  }
1236
1684
  }
@@ -2097,6 +2545,110 @@ const commands = {
2097
2545
  "dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
2098
2546
  ],
2099
2547
  },
2548
+ organize: {
2549
+ handler: async (c, _a, f) => {
2550
+ const usage = "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]";
2551
+ const pk = f["project-key"];
2552
+ const plan = readFlowZoneOrganizePlan(f, usage);
2553
+ const sync = f["sync"] === true;
2554
+ const validateObjects = f["validate-objects"] === true;
2555
+ const zones = await c.flowZones.list(pk);
2556
+ const plannedItemKeys = flowZonePlanItemKeys(plan);
2557
+ const planned = plan.zones.map((zonePlan) => flowZoneOrganizeStep(zonePlan, findFlowZoneForPlan(zones, zonePlan), sync, plannedItemKeys));
2558
+ const validation = validateObjects
2559
+ ? await validateFlowZoneOrganizeObjects(c, plan, pk)
2560
+ : undefined;
2561
+ if (validation)
2562
+ throwFlowZoneValidationError(validation);
2563
+ const itemCount = plan.zones.reduce((count, zonePlan) => count + zonePlan.items.length, 0);
2564
+ const pruneItemCount = planned.reduce((count, step) => {
2565
+ const pruneItems = Array.isArray(step.pruneItems) ? step.pruneItems : [];
2566
+ return count + pruneItems.length;
2567
+ }, 0);
2568
+ if (f["dry-run"] === true) {
2569
+ return {
2570
+ dryRun: true,
2571
+ action: "organize",
2572
+ resource: "flow-zone",
2573
+ projectKey: pk,
2574
+ sync,
2575
+ validateObjects,
2576
+ zoneCount: plan.zones.length,
2577
+ itemCount,
2578
+ pruneItemCount,
2579
+ ...(validation ? { validation, } : {}),
2580
+ planned,
2581
+ };
2582
+ }
2583
+ const currentZones = [...zones,];
2584
+ const created = [];
2585
+ const updated = [];
2586
+ const moved = [];
2587
+ const pruned = [];
2588
+ for (const zonePlan of plan.zones) {
2589
+ let zone = findFlowZoneForPlan(currentZones, zonePlan);
2590
+ ensureFlowZonePlanTarget(zonePlan, zone);
2591
+ const pruneItems = sync ? flowZonePruneItems(zone, plannedItemKeys) : [];
2592
+ if (!zone) {
2593
+ zone = await c.flowZones.create({
2594
+ name: zonePlan.name,
2595
+ color: zonePlan.color,
2596
+ position: zonePlan.position,
2597
+ projectKey: pk,
2598
+ });
2599
+ currentZones.push(zone);
2600
+ created.push(zone);
2601
+ }
2602
+ else {
2603
+ const patch = {
2604
+ ...(zonePlan.name && zonePlan.name !== zone.name ? { name: zonePlan.name, } : {}),
2605
+ ...(zonePlan.color && zonePlan.color !== zone.color ? { color: zonePlan.color, } : {}),
2606
+ ...(zonePlan.position !== undefined
2607
+ && !flowZoneSamePosition(flowZoneCurrentPosition(zone), zonePlan.position)
2608
+ ? { position: zonePlan.position, }
2609
+ : {}),
2610
+ projectKey: pk,
2611
+ };
2612
+ if (patch.name !== undefined || patch.color !== undefined || patch.position !== undefined) {
2613
+ zone = await c.flowZones.update(zone.id, patch);
2614
+ const index = currentZones.findIndex((candidate) => candidate.id === zone.id);
2615
+ if (index !== -1)
2616
+ currentZones[index] = zone;
2617
+ updated.push(zone);
2618
+ }
2619
+ }
2620
+ if (zonePlan.items.length > 0) {
2621
+ await c.flowZones.moveItems(zone.id, zonePlan.items, pk);
2622
+ moved.push({ zoneId: zone.id, name: zone.name, items: zonePlan.items, });
2623
+ }
2624
+ if (pruneItems.length > 0) {
2625
+ await c.flowZones.moveItems("default", pruneItems, pk);
2626
+ pruned.push({ zoneId: "default", fromZoneId: zone.id, name: zone.name, items: pruneItems, });
2627
+ }
2628
+ }
2629
+ return {
2630
+ organized: true,
2631
+ action: "organize",
2632
+ resource: "flow-zone",
2633
+ projectKey: pk,
2634
+ sync,
2635
+ validateObjects,
2636
+ zoneCount: plan.zones.length,
2637
+ itemCount,
2638
+ pruneItemCount,
2639
+ created,
2640
+ updated,
2641
+ moved,
2642
+ pruned,
2643
+ };
2644
+ },
2645
+ usage: "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]",
2646
+ description: "Create/update flow zones and move objects from a declarative visual organization plan.",
2647
+ examples: [
2648
+ "dss flow-zone organize --file flow-zones.json --dry-run",
2649
+ `dss flow-zone organize --data '{"zones":[{"name":"Raw","color":"#64748b","datasets":["raw_orders"]}]}'`,
2650
+ ],
2651
+ },
2100
2652
  graph: {
2101
2653
  handler: (c, a, f) => {
2102
2654
  requireArgs(a, 1, "dss flow-zone graph <id>");
@@ -2204,10 +2756,11 @@ const commands = {
2204
2756
  return c.datasets.download(a[0], {
2205
2757
  outputPath: f["output"],
2206
2758
  projectKey: f["project-key"],
2759
+ limit: num(f["limit"]),
2207
2760
  });
2208
2761
  },
2209
- usage: "dss dataset download <name> [--output PATH] [--project-key KEY]",
2210
- 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.",
2211
2764
  examples: ["dss dataset download orders", "dss dataset download orders --output ./data/",],
2212
2765
  },
2213
2766
  create: {
@@ -2660,6 +3213,87 @@ const commands = {
2660
3213
  "cat settings.json | dss recipe update compute_orders --stdin",
2661
3214
  ],
2662
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
+ },
2663
3297
  "get-payload": {
2664
3298
  handler: async (c, a, f) => {
2665
3299
  requireArgs(a, 1, "dss recipe get-payload <name>");
@@ -2673,7 +3307,7 @@ const commands = {
2673
3307
  return payload;
2674
3308
  },
2675
3309
  usage: "dss recipe get-payload <name> [--raw] [--output PATH] [--project-key KEY]",
2676
- 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.",
2677
3311
  examples: [
2678
3312
  "dss recipe get-payload compute_orders --raw",
2679
3313
  "dss recipe get-payload compute_orders -o code.py",
@@ -2687,7 +3321,7 @@ const commands = {
2687
3321
  });
2688
3322
  },
2689
3323
  usage: "dss recipe cat <name> [--raw] [--project-key KEY]",
2690
- 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.",
2691
3325
  examples: ["dss recipe cat compute_orders --raw",],
2692
3326
  },
2693
3327
  "set-payload": {
@@ -2870,17 +3504,29 @@ const commands = {
2870
3504
  examples: ["dss job summary JOB_ID --max-log-lines 200",],
2871
3505
  },
2872
3506
  log: {
2873
- handler: (c, a, f) => {
3507
+ handler: async (c, a, f) => {
2874
3508
  requireArgs(a, 1, "dss job log <id>");
2875
- 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], {
2876
3513
  activity: f["activity"],
2877
3514
  logId: f["log-id"],
3515
+ logFilter,
2878
3516
  maxLogLines: maxLogLinesFromFlags(f),
2879
3517
  projectKey: f["project-key"],
2880
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;
2881
3527
  },
2882
- usage: "dss job log <id> [--activity ACTIVITY_ID] [--log-id LOG_ID] [--max-lines N|--max-log-lines N] [--project-key KEY]",
2883
- 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.",
2884
3530
  examples: [
2885
3531
  "dss job log JOB_ID",
2886
3532
  "dss job log JOB_ID --activity main --max-log-lines 200",
@@ -3749,12 +4395,21 @@ const commands = {
3749
4395
  sql: {
3750
4396
  query: {
3751
4397
  handler: async (c, a, f) => {
3752
- const query = resolveSqlInput(a, f);
3753
4398
  const connection = f["connection"];
3754
4399
  const datasetFullName = f["dataset"];
3755
4400
  if ((connection ? 1 : 0) + (datasetFullName ? 1 : 0) !== 1) {
3756
4401
  throw new UsageError(`Pass exactly one of --connection or --dataset. Usage: ${SQL_QUERY_USAGE}`);
3757
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);
3758
4413
  const result = await c.sql.query({
3759
4414
  query,
3760
4415
  connection,
@@ -3762,8 +4417,6 @@ const commands = {
3762
4417
  database: f["database"],
3763
4418
  projectKey: f["project-key"],
3764
4419
  });
3765
- const outputFile = f["output"]
3766
- ?? f["output-file"];
3767
4420
  if (!outputFile)
3768
4421
  return result;
3769
4422
  const outputPath = resolve(outputFile);
@@ -3774,6 +4427,7 @@ const commands = {
3774
4427
  schema: result.schema,
3775
4428
  columns: result.columns ?? result.schema,
3776
4429
  rowCount: result.rows.length,
4430
+ preview: result.rows.slice(0, previewCount),
3777
4431
  outputPath,
3778
4432
  written: outputPath,
3779
4433
  };
@@ -3785,6 +4439,40 @@ const commands = {
3785
4439
  "dss sql query --sql-file query.sql --connection my_pg",
3786
4440
  "echo 'SELECT 1' | dss sql query --stdin --dataset MYPROJ.orders",
3787
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",
3788
4476
  ],
3789
4477
  },
3790
4478
  },
@@ -4016,7 +4704,7 @@ const commands = {
4016
4704
  },
4017
4705
  };
4018
4706
  // ---------------------------------------------------------------------------
4019
- // Help
4707
+ // Agent-facing command inventory
4020
4708
  // ---------------------------------------------------------------------------
4021
4709
  const RESOURCE_NAMES = [
4022
4710
  ...Object.keys(commands),
@@ -4027,87 +4715,37 @@ const RESOURCE_NAMES = [
4027
4715
  "install-skill",
4028
4716
  ]
4029
4717
  .sort();
4030
- function printTopLevelHelp() {
4031
- const lines = [
4032
- "Usage: dss <resource> <action> [args...] [--flags]",
4033
- "",
4034
- "Global flags:",
4035
- " -h, --help Show help",
4036
- " -v, --verbose Log HTTP requests to stderr",
4037
- " -V, --version Show version",
4038
- " --json Emit JSON output (default)",
4039
- " -o, --output PATH Write output to file (recipe get-payload)",
4040
- " --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
4041
- " --api-key KEY API key (env: DATAIKU_API_KEY)",
4042
- " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
4043
- " --timeout MS Operation timeout (build-and-wait, run-and-wait, recipe run)",
4044
- " --request-timeout MS HTTP request timeout in ms (default: 30000)",
4045
- " --dry-run Preview destructive actions without executing",
4046
- " --if-not-exists Skip create if resource already exists",
4047
- " --if-exists Skip delete if resource is already missing",
4048
- " --insecure Disable TLS certificate verification",
4049
- " --ca-cert PATH Extra PEM CA bundle (env: NODE_EXTRA_CA_CERTS)",
4050
- "",
4051
- "Resources:",
4052
- ...RESOURCE_NAMES.map((r) => ` ${r}`),
4053
- "",
4054
- "Quick start:",
4055
- " dss auth login Save DSS credentials",
4056
- " dss auth status Verify connection",
4057
- " dss doctor Run JSON connectivity diagnostics",
4058
- " dss project list List accessible projects",
4059
- " dss dataset list List datasets in default project",
4060
- " dss dataset preview <name> Preview dataset rows as CSV",
4061
- " dss recipe get-payload <name> Print recipe code to stdout",
4062
- " dss recipe download-code <name> Download recipe code to a file",
4063
- " dss job log <id> View job log output",
4064
- " dss install-skill Install agent skill for coding agents",
4065
- ];
4066
- process.stderr.write(`${lines.join("\n")}\n`);
4067
- }
4068
- function printResourceHelp(resource) {
4069
- const actions = commands[resource];
4070
- if (!actions)
4071
- return;
4072
- const maxName = Math.max(...Object.keys(actions).map((n) => n.length));
4073
- const lines = [
4074
- `Usage: dss ${resource} <action> [args...] [--flags]`,
4075
- "",
4076
- "Actions:",
4077
- ...Object.entries(actions).map(([name, meta,]) => ` ${name.padEnd(maxName + 2)}${meta.description ?? meta.usage}`),
4078
- "",
4079
- `Run 'dss ${resource} <action> --help' for details and examples.`,
4080
- ];
4081
- process.stderr.write(`${lines.join("\n")}\n`);
4082
- }
4083
- function printActionHelp(resource, action) {
4084
- const meta = commands[resource]?.[action];
4085
- if (!meta)
4086
- return;
4087
- const lines = [];
4088
- if (meta.description)
4089
- lines.push(meta.description, "");
4090
- lines.push(`Usage: ${meta.usage}`);
4091
- if (meta.examples && meta.examples.length > 0) {
4092
- lines.push("", "Examples:");
4093
- for (const ex of meta.examples)
4094
- lines.push(` ${ex}`);
4095
- }
4096
- process.stderr.write(`${lines.join("\n")}\n`);
4097
- }
4098
4718
  // ---------------------------------------------------------------------------
4099
4719
  // Validation
4100
4720
  // ---------------------------------------------------------------------------
4101
4721
  class UsageError extends Error {
4102
4722
  code;
4103
4723
  hint;
4104
- constructor(message, code = "usage_error", hint) {
4724
+ details;
4725
+ constructor(message, code = "usage_error", hint, details) {
4105
4726
  super(message);
4106
4727
  this.name = "UsageError";
4107
4728
  this.code = code;
4108
4729
  this.hint = hint;
4730
+ this.details = details;
4109
4731
  }
4110
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
+ }
4111
4749
  function requireArgs(args, count, usage) {
4112
4750
  if (args.length < count) {
4113
4751
  throw new UsageError(`Expected ${count} argument(s), got ${args.length}.\nUsage: ${usage}`, "missing_required_arg");
@@ -4116,12 +4754,18 @@ function requireArgs(args, count, usage) {
4116
4754
  // ---------------------------------------------------------------------------
4117
4755
  // .env auto-loading
4118
4756
  // ---------------------------------------------------------------------------
4757
+ function dataikuEnvironmentEnabled() {
4758
+ return process.env.DATAIKU_DISABLE_ENV !== "1";
4759
+ }
4119
4760
  function loadEnvFile() {
4120
- if (process.env.DATAIKU_DISABLE_ENV === "1")
4761
+ if (!dataikuEnvironmentEnabled())
4121
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.
4122
4766
  const dirs = [
4123
- resolve(dirname(fileURLToPath(import.meta.url)), ".."),
4124
4767
  process.cwd(),
4768
+ resolve(dirname(fileURLToPath(import.meta.url)), ".."),
4125
4769
  ];
4126
4770
  for (const dir of dirs) {
4127
4771
  try {
@@ -4151,81 +4795,42 @@ const AUTH_ACTIONS = {
4151
4795
  login: {
4152
4796
  handler: async (flags) => {
4153
4797
  const tlsSettings = resolveTlsSettings(flags);
4154
- 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;
4155
4814
  if (!url || !apiKey) {
4156
- if (!process.stdin.isTTY) {
4157
- throw new UsageError("Missing --url and/or --api-key. Provide them as flags or run interactively.");
4158
- }
4159
- if (!url)
4160
- url = await promptLine("DSS URL: ");
4161
- if (!apiKey)
4162
- apiKey = await promptSecret("API key: ");
4163
- if (!projectKey)
4164
- 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",], });
4165
4816
  }
4166
- if (!url)
4167
- throw new UsageError("URL is required.");
4168
- if (!apiKey)
4169
- throw new UsageError("API key is required.");
4170
- process.stderr.write("Validating credentials... ");
4171
4817
  const result = await validateCredentials(url, apiKey, tlsSettings);
4172
4818
  if (!result.valid) {
4173
- process.stderr.write("Failed\n");
4174
4819
  if (result.dataikuError)
4175
4820
  throw result.dataikuError;
4176
4821
  throw new DataikuError(0, "Authentication Failed", result.error ?? "Credential validation failed");
4177
4822
  }
4178
- process.stderr.write("Connected\n");
4823
+ const path = getCredentialsPath();
4179
4824
  saveCredentials({ url, apiKey, projectKey, ...tlsSettings, });
4180
- process.stderr.write(`Credentials saved to ${getCredentialsPath()}\n`);
4825
+ return { saved: true, path, };
4181
4826
  },
4182
- usage: "dss auth login [--url URL] [--api-key KEY] [--project-key KEY] [--insecure] [--ca-cert PATH]",
4183
- 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.",
4184
4829
  examples: [
4185
4830
  "dss auth login --url https://dss.example.com --api-key YOUR_KEY",
4186
4831
  "dss auth login --url https://dss.example.com --api-key YOUR_KEY --project-key MYPROJ",
4187
4832
  ],
4188
- },
4189
- status: {
4190
- handler: async (flags) => {
4191
- const creds = loadCredentials();
4192
- if (!creds) {
4193
- process.stderr.write("No saved credentials. Run: dss auth login\n");
4194
- process.exit(1);
4195
- }
4196
- const tlsSettings = resolveTlsSettings(flags, creds);
4197
- const lines = [
4198
- `URL: ${creds.url}`,
4199
- `API key: ${maskApiKey(creds.apiKey)}`,
4200
- `Project key: ${creds.projectKey ?? "(not set)"}`,
4201
- `TLS verify: ${tlsSettings.tlsRejectUnauthorized === false ? "disabled" : "strict"}`,
4202
- `CA cert: ${tlsSettings.caCertPath ?? "(default trust store)"}`,
4203
- ];
4204
- for (const line of lines)
4205
- process.stderr.write(`${line}\n`);
4206
- const result = await validateCredentials(creds.url, creds.apiKey, tlsSettings);
4207
- if (result.valid) {
4208
- process.stderr.write("Connection: valid\n");
4209
- }
4210
- else {
4211
- process.stderr.write(`Connection: failed (${result.error ?? "unknown error"})\n`);
4212
- process.stderr.write(`Config: ${getCredentialsPath()}\n`);
4213
- process.exit(1);
4214
- }
4215
- process.stderr.write(`Config: ${getCredentialsPath()}\n`);
4216
- },
4217
- usage: "dss auth status [--insecure] [--ca-cert PATH]",
4218
- description: "Show saved credentials and verify the connection.",
4219
- examples: ["dss auth status",],
4220
- },
4221
- logout: {
4222
- handler: async (_flags) => {
4223
- deleteCredentials();
4224
- process.stderr.write("Credentials removed.\n");
4225
- },
4226
- usage: "dss auth logout",
4227
- description: "Remove saved credentials.",
4228
- examples: ["dss auth logout",],
4833
+ requiredFlags: ["url", "api-key",],
4229
4834
  },
4230
4835
  };
4231
4836
  function errorDetails(error) {
@@ -4477,7 +5082,7 @@ async function runDoctor(flags) {
4477
5082
  ok: credentialsOk,
4478
5083
  message: credentialsOk
4479
5084
  ? "Dataiku URL and API key are configured."
4480
- : "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.",
4481
5086
  });
4482
5087
  let accessibleProjects;
4483
5088
  if (credentialsOk) {
@@ -4556,13 +5161,13 @@ async function runDoctor(flags) {
4556
5161
  async function runFixtures(flags) {
4557
5162
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
4558
5163
  if (!url) {
4559
- 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");
4560
5165
  }
4561
5166
  if (!apiKey) {
4562
- 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");
4563
5168
  }
4564
5169
  if (!projectKey) {
4565
- 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");
4566
5171
  }
4567
5172
  currentCommandContext.projectKey = projectKey;
4568
5173
  const requestTimeoutMs = num(flags["request-timeout"]);
@@ -4636,7 +5241,7 @@ const PROJECT_SCOPED_RESOURCES = new Set([
4636
5241
  "variable",
4637
5242
  "wiki",
4638
5243
  ]);
4639
- const GLOBAL_AGENT_FLAGS = ["help", "json", "report-json", "verbose",];
5244
+ const GLOBAL_AGENT_FLAGS = ["json", "verbose", "fields",];
4640
5245
  const AUTHENTICATED_AGENT_FLAGS = [
4641
5246
  "url",
4642
5247
  "api-key",
@@ -4645,9 +5250,12 @@ const AUTHENTICATED_AGENT_FLAGS = [
4645
5250
  "insecure",
4646
5251
  "ca-cert",
4647
5252
  ];
4648
- const COMMANDS_USAGE = "dss commands [--json]";
5253
+ const COMMANDS_USAGE = "dss commands run [--json]";
4649
5254
  const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
4650
- 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",];
4651
5259
  const INSTALL_SKILL_USAGE = "dss install-skill [--global] [--agent NAME] [--target PATH] [--list-agents] [--dry-run] [--plan]";
4652
5260
  const INSTALL_SKILL_DESCRIPTION = "Install the dataiku-dss agent skill for detected coding agents.";
4653
5261
  const INSTALL_SKILL_EXAMPLES = [
@@ -4666,6 +5274,22 @@ const FIXTURES_EXAMPLES = [
4666
5274
  "dss fixtures --json",
4667
5275
  "dss fixtures --json --allow-types Filesystem,Inline",
4668
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
+ }
4669
5293
  function uniqueStrings(values) {
4670
5294
  return [...new Set(values),];
4671
5295
  }
@@ -4730,26 +5354,33 @@ function extractPositionals(usage) {
4730
5354
  function inferSideEffect(resource, action) {
4731
5355
  if (resource === "auth")
4732
5356
  return "auth";
4733
- if (resource === "doctor" || resource === "commands" || resource === "fixtures")
5357
+ if (resource === "doctor" || resource === "commands" || resource === "fixtures"
5358
+ || resource === "version") {
4734
5359
  return "read";
5360
+ }
4735
5361
  if (resource === "install-skill")
4736
5362
  return "write";
4737
5363
  if (resource === "data-quality" && action === "compute")
4738
5364
  return "write";
4739
5365
  if (READ_ACTIONS.has(action))
4740
5366
  return "read";
4741
- 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)/
4742
5368
  .test(action)) {
4743
5369
  return "write";
4744
5370
  }
4745
5371
  return "read";
4746
5372
  }
4747
5373
  function inferRequiresAuth(resource) {
4748
- return resource !== "auth" && resource !== "commands" && resource !== "install-skill";
5374
+ return resource !== "auth"
5375
+ && resource !== "commands"
5376
+ && resource !== "install-skill"
5377
+ && resource !== "version";
4749
5378
  }
4750
5379
  function inferRequiresProject(resource, action, usage) {
4751
- if (resource === "doctor" || resource === "commands" || resource === "install-skill")
5380
+ if (resource === "auth" || resource === "doctor" || resource === "commands"
5381
+ || resource === "install-skill" || resource === "version") {
4752
5382
  return false;
5383
+ }
4753
5384
  if (PROJECT_SCOPED_RESOURCES.has(resource))
4754
5385
  return true;
4755
5386
  if (resource === "project" && action !== "list")
@@ -4777,13 +5408,16 @@ const STRING_OUTPUT_ACTIONS = new Set([
4777
5408
  "cat",
4778
5409
  "log",
4779
5410
  "log-url",
4780
- "preview",
4781
5411
  ]);
4782
5412
  function inferOutputShape(resource, action) {
4783
- if (resource === "auth" || resource === "install-skill")
4784
- return "void";
5413
+ if (resource === "auth" || resource === "commands" || resource === "install-skill"
5414
+ || resource === "version") {
5415
+ return "object";
5416
+ }
4785
5417
  if (ARRAY_OUTPUT_ACTIONS.has(action))
4786
5418
  return "array";
5419
+ if (resource === "dataset" && action === "download")
5420
+ return "object";
4787
5421
  if (STRING_OUTPUT_ACTIONS.has(action))
4788
5422
  return "string";
4789
5423
  return "object";
@@ -4798,8 +5432,107 @@ function inferInputContract(usage) {
4798
5432
  function stripOptionalUsageGroups(usage) {
4799
5433
  return usage.replace(/\[[^\]]*\]/g, " ");
4800
5434
  }
4801
- function extractRequiredUsageFlags(usage) {
4802
- 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;
4803
5536
  }
4804
5537
  function inferPayloadSchema(inputContract) {
4805
5538
  if (!inputContract.stdin && !inputContract.dataFlag && !inputContract.dataFileFlag) {
@@ -4852,6 +5585,8 @@ function inferAsyncKind(resource, action) {
4852
5585
  }
4853
5586
  if (resource === "data-quality" && action === "compute")
4854
5587
  return "future";
5588
+ if (resource === "code" && action === "run")
5589
+ return "future";
4855
5590
  return "none";
4856
5591
  }
4857
5592
  function inferIdempotency(sideEffect, action, usage) {
@@ -4861,6 +5596,8 @@ function inferIdempotency(sideEffect, action, usage) {
4861
5596
  return "if-not-exists";
4862
5597
  if (action.startsWith("delete") && usage.includes("--if-exists"))
4863
5598
  return "if-exists";
5599
+ if (/^(clear|refresh|set|save)/.test(action))
5600
+ return "convergent";
4864
5601
  return "none";
4865
5602
  }
4866
5603
  function inferCleanupHint(resource, action) {
@@ -4891,12 +5628,18 @@ function buildRegistryEntry(resource, action, meta) {
4891
5628
  ...(requiresAuth ? AUTHENTICATED_AGENT_FLAGS : []),
4892
5629
  ...(requiresProject ? ["project-key",] : []),
4893
5630
  ]);
5631
+ const derivedRequired = deriveRequiredUsage(meta.usage);
4894
5632
  const requiredFlags = meta.requiredFlags
4895
5633
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.requiredFlags
4896
- ?? 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()));
4897
5639
  const optionalFlags = meta.optionalFlags
4898
5640
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.optionalFlags
4899
- ?? flags.filter((flag) => !requiredFlags.includes(flag));
5641
+ ?? flags.filter((flag) => !requiredFlags.includes(flag) && !oneOfFlags.has(flag));
5642
+ const valueHints = extractFlagValueHints(meta.usage);
4900
5643
  const inputContract = inferInputContract(meta.usage);
4901
5644
  const cleanupHint = inferCleanupHint(resource, action);
4902
5645
  const payloadSchema = meta.payloadSchema
@@ -4913,7 +5656,20 @@ function buildRegistryEntry(resource, action, meta) {
4913
5656
  usage: meta.usage,
4914
5657
  description: meta.description,
4915
5658
  examples: meta.examples,
4916
- 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
+ }),
4917
5673
  positionals: extractPositionals(meta.usage),
4918
5674
  sideEffect,
4919
5675
  requiresAuth,
@@ -4929,6 +5685,7 @@ function buildRegistryEntry(resource, action, meta) {
4929
5685
  dryRun: meta.usage.includes("--dry-run"),
4930
5686
  requiredFlags: uniqueStrings(requiredFlags),
4931
5687
  optionalFlags: uniqueStrings(optionalFlags),
5688
+ ...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
4932
5689
  ...(payloadSchema ? { payloadSchema, } : {}),
4933
5690
  ...(examplePayload !== undefined ? { examplePayload, } : {}),
4934
5691
  ...(cleanupCommand ? { cleanupCommand, } : {}),
@@ -4952,6 +5709,14 @@ function buildCommandRegistry() {
4952
5709
  examples: COMMANDS_EXAMPLES,
4953
5710
  }),
4954
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
+ };
4955
5720
  registry["install-skill"] = {
4956
5721
  run: buildRegistryEntry("install-skill", "run", {
4957
5722
  handler: async () => undefined,
@@ -4976,6 +5741,16 @@ function buildCommandRegistry() {
4976
5741
  examples: FIXTURES_EXAMPLES,
4977
5742
  }),
4978
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
+ };
4979
5754
  registry.auth = {};
4980
5755
  for (const [action, meta,] of Object.entries(AUTH_ACTIONS)) {
4981
5756
  registry.auth[action] = buildRegistryEntry("auth", action, {
@@ -4983,6 +5758,7 @@ function buildCommandRegistry() {
4983
5758
  usage: meta.usage,
4984
5759
  description: meta.description,
4985
5760
  examples: meta.examples,
5761
+ requiredFlags: meta.requiredFlags,
4986
5762
  });
4987
5763
  }
4988
5764
  return registry;
@@ -5605,10 +6381,10 @@ async function runCleanup(flags) {
5605
6381
  }
5606
6382
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
5607
6383
  if (!url) {
5608
- 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");
5609
6385
  }
5610
6386
  if (!apiKey) {
5611
- 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");
5612
6388
  }
5613
6389
  const requestTimeoutMs = num(flags["request-timeout"]);
5614
6390
  const retryMaxAttempts = num(flags["retries"]);
@@ -5628,7 +6404,8 @@ async function runCleanup(flags) {
5628
6404
  try {
5629
6405
  const parsed = parseArgs(entry.cleanup.argv);
5630
6406
  const [resource, action, ...args] = parsed.positional;
5631
- if (!resource || !action || !commands[resource]?.[action]) {
6407
+ if (!resource || !action || !isAllowedCleanupAction(resource, action)
6408
+ || !commands[resource]?.[action]) {
5632
6409
  throw new UsageError(`Invalid cleanup argv: ${entry.cleanup.argv.join(" ")}`);
5633
6410
  }
5634
6411
  const result = await commands[resource][action].handler(client, args, parsed.flags);
@@ -5654,35 +6431,128 @@ async function runCleanup(flags) {
5654
6431
  exitCode: failures.length > 0 ? 2 : 0,
5655
6432
  };
5656
6433
  }
5657
- // ---------------------------------------------------------------------------
5658
- // Interactive prompts
5659
- // ---------------------------------------------------------------------------
5660
- function promptLine(label) {
5661
- return new Promise((res, rej) => {
5662
- const rl = createInterface({ input: process.stdin, output: process.stderr, });
5663
- rl.on("close", () => rej(new UsageError("Input closed before a value was provided.")));
5664
- rl.question(label, (answer) => {
5665
- rl.close();
5666
- res(answer.trim());
5667
- });
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;
5668
6455
  });
5669
6456
  }
5670
- function promptSecret(label) {
5671
- return new Promise((res, rej) => {
5672
- const muted = new Writable({
5673
- write(_chunk, _encoding, cb) {
5674
- cb();
5675
- },
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, };
6470
+ });
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",],
5676
6481
  });
5677
- const rl = createInterface({ input: process.stdin, output: muted, terminal: true, });
5678
- rl.on("close", () => rej(new UsageError("Input closed before a value was provided.")));
5679
- process.stderr.write(label);
5680
- rl.question("", (answer) => {
5681
- rl.close();
5682
- process.stderr.write("\n");
5683
- res(answer.trim());
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",],
5684
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,
5685
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
+ };
5686
6556
  }
5687
6557
  // ---------------------------------------------------------------------------
5688
6558
  // Credential resolution
@@ -5695,12 +6565,15 @@ function resolveCredentials(flags) {
5695
6565
  let apiKey = hasApiKeyFlag ? flags["api-key"] : undefined;
5696
6566
  let projectKey = hasProjectKeyFlag ? flags["project-key"] : undefined;
5697
6567
  const saved = loadCredentials();
5698
- if (!hasUrlFlag)
5699
- url ??= process.env.DATAIKU_URL;
5700
- if (!hasApiKeyFlag)
5701
- apiKey ??= process.env.DATAIKU_API_KEY;
5702
- if (!hasProjectKeyFlag)
5703
- 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
+ }
5704
6577
  if (saved) {
5705
6578
  if (!hasUrlFlag)
5706
6579
  url ??= saved.url;
@@ -5717,10 +6590,6 @@ function resolveCredentials(flags) {
5717
6590
  };
5718
6591
  }
5719
6592
  let currentCommandContext = {};
5720
- function isReportJsonRequested() {
5721
- return process.env.DSS_REPORT_JSON === "1"
5722
- || process.argv.slice(2).some((arg) => arg === "--report-json" || arg.startsWith("--report-json="));
5723
- }
5724
6593
  function rawFlagValue(argv, flagName) {
5725
6594
  const longFlag = `--${flagName}`;
5726
6595
  for (let index = 0; index < argv.length; index++) {
@@ -5734,6 +6603,12 @@ function rawFlagValue(argv, flagName) {
5734
6603
  }
5735
6604
  return undefined;
5736
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
+ }
5737
6612
  function rawCommandContext() {
5738
6613
  const argv = process.argv.slice(2);
5739
6614
  const positionals = [];
@@ -5758,13 +6633,19 @@ function rawCommandContext() {
5758
6633
  }
5759
6634
  positionals.push(arg);
5760
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;
5761
6642
  return {
5762
- resource: currentCommandContext.resource ?? positionals[0],
5763
- action: currentCommandContext.action ?? positionals[1],
5764
- projectKey: currentCommandContext.projectKey
5765
- ?? rawFlagValue(argv, "project-key")
5766
- ?? rawFlagValue(argv, "project")
5767
- ?? process.env.DATAIKU_PROJECT_KEY,
6643
+ resource,
6644
+ action,
6645
+ projectKey: explicitProjectKey
6646
+ ?? (commandIsProjectScoped(resource, action)
6647
+ ? currentCommandContext.projectKey ?? ambientProjectKey
6648
+ : undefined),
5768
6649
  };
5769
6650
  }
5770
6651
  function requestIdFromBody(body) {
@@ -5777,26 +6658,52 @@ function requestIdFromBody(body) {
5777
6658
  return undefined;
5778
6659
  }
5779
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
+ }
5780
6670
  function buildErrorReport(err) {
5781
6671
  const context = rawCommandContext();
6672
+ const exitCode = errorExitCode(err);
5782
6673
  if (err instanceof UsageError) {
5783
6674
  return {
6675
+ ok: false,
6676
+ error: err.message,
5784
6677
  code: err.code,
5785
6678
  category: "usage",
5786
- message: err.message,
6679
+ exitCode,
5787
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, },
5788
6693
  ...context,
5789
6694
  };
5790
6695
  }
5791
6696
  if (err instanceof DataikuError) {
5792
6697
  return {
6698
+ ok: false,
6699
+ error: err.message,
5793
6700
  code: dataikuErrorCode(err.category),
5794
6701
  category: "dss",
5795
- message: err.message,
6702
+ exitCode,
5796
6703
  hint: err.retryHint,
5797
6704
  status: err.status,
5798
6705
  retryable: err.retryable,
5799
- requestId: requestIdFromBody(err.body),
6706
+ requestId: err.requestId ?? requestIdFromBody(err.body),
5800
6707
  details: {
5801
6708
  dssCategory: err.category,
5802
6709
  statusText: err.statusText,
@@ -5808,190 +6715,109 @@ function buildErrorReport(err) {
5808
6715
  }
5809
6716
  const message = err instanceof Error ? err.message : String(err);
5810
6717
  return {
6718
+ ok: false,
6719
+ error: message,
5811
6720
  code: "internal_error",
5812
6721
  category: "internal",
5813
- message,
6722
+ exitCode,
5814
6723
  ...context,
5815
6724
  };
5816
6725
  }
5817
6726
  function writeErrorReport(err) {
5818
6727
  process.stderr.write(`${JSON.stringify(buildErrorReport(err), null, 2)}\n`);
5819
6728
  }
5820
- function commandRegistryEntry(resource, action) {
5821
- return buildCommandRegistry()[resource]?.[action];
5822
- }
5823
- function writeReportHelp(resource, action) {
5824
- const entry = commandRegistryEntry(resource, action);
5825
- if (entry) {
5826
- process.stderr.write(`${JSON.stringify(entry, null, 2)}\n`);
5827
- return;
5828
- }
5829
- process.stderr.write(`${JSON.stringify({
5830
- code: "usage_error",
5831
- category: "usage",
5832
- message: `No registry entry for ${resource} ${action}.`,
5833
- resource,
5834
- action,
5835
- }, null, 2)}\n`);
5836
- }
5837
6729
  // ---------------------------------------------------------------------------
5838
6730
  // Main
5839
6731
  // ---------------------------------------------------------------------------
5840
6732
  async function main() {
5841
6733
  loadEnvFile();
5842
6734
  const { positional, flags, } = parseArgs(process.argv.slice(2));
5843
- // --version
5844
- if (flags["version"] === true) {
5845
- process.stdout.write(`${CLI_VERSION_LABEL}\n`);
5846
- 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;
5847
6740
  }
5848
- // Top-level help
5849
- if (positional.length === 0 || (positional.length === 0 && flags["help"])) {
5850
- printTopLevelHelp();
5851
- if (flags["help"])
5852
- process.exit(0);
5853
- process.exit(1);
6741
+ if (flags["version"] === true) {
6742
+ writeCommandResult(cliVersionResult());
6743
+ return;
5854
6744
  }
6745
+ if (positional.length === 0)
6746
+ throw noCommandError();
5855
6747
  const resource = positional[0];
5856
6748
  currentCommandContext = {
5857
6749
  resource,
5858
6750
  action: positional[1],
5859
6751
  projectKey: typeof flags["project-key"] === "string"
5860
6752
  ? flags["project-key"]
5861
- : process.env.DATAIKU_PROJECT_KEY,
6753
+ : dataikuEnvironmentEnabled()
6754
+ ? process.env.DATAIKU_PROJECT_KEY
6755
+ : undefined,
5862
6756
  };
5863
6757
  if (resource === "doctor") {
5864
6758
  const action = positional[1];
5865
- if (flags["help"] === true) {
5866
- if (flags["report-json"] === true)
5867
- writeReportHelp("doctor", "run");
5868
- else
5869
- printActionHelp("doctor", "run");
5870
- process.exit(0);
5871
- }
6759
+ currentCommandContext.action = action ?? "run";
5872
6760
  if (action !== undefined && action !== "run") {
5873
- throw new UsageError("Usage: dss doctor [--project-key KEY] [--capabilities] [--fast]");
6761
+ throw unknownActionError("doctor", action, ["run",]);
5874
6762
  }
5875
6763
  const { result, exitCode, } = await runDoctor(flags);
5876
6764
  writeCommandResult(result);
5877
- process.exit(exitCode);
6765
+ if (exitCode !== 0)
6766
+ process.exit(exitCode);
6767
+ return;
5878
6768
  }
5879
- // Auth commands — dispatched before client creation
5880
6769
  if (resource === "auth") {
5881
6770
  const action = positional[1];
6771
+ const validActions = Object.keys(AUTH_ACTIONS);
5882
6772
  if (!action) {
5883
- const maxName = Math.max(...Object.keys(AUTH_ACTIONS).map((n) => n.length));
5884
- const lines = [
5885
- "Usage: dss auth <action> [--flags]",
5886
- "",
5887
- "Actions:",
5888
- ...Object.entries(AUTH_ACTIONS).map(([name, meta,]) => ` ${name.padEnd(maxName + 2)}${meta.description ?? meta.usage}`),
5889
- "",
5890
- "Run 'dss auth <action> --help' for details and examples.",
5891
- ];
5892
- process.stderr.write(`${lines.join("\n")}\n`);
5893
- process.exit(flags["help"] === true ? 0 : 1);
6773
+ throw missingActionError("auth", validActions, "dss auth login --url URL --api-key KEY");
5894
6774
  }
6775
+ currentCommandContext.action = action;
5895
6776
  const authMeta = AUTH_ACTIONS[action];
5896
- if (!authMeta) {
5897
- if (flags["report-json"] === true) {
5898
- throw new UsageError(`Unknown action: auth ${action}. Available: ${Object.keys(AUTH_ACTIONS).join(", ")}`);
5899
- }
5900
- process.stderr.write(`Unknown action: auth ${action}\nAvailable: ${Object.keys(AUTH_ACTIONS).join(", ")}\n`);
5901
- process.exit(1);
5902
- }
5903
- if (flags["help"] === true) {
5904
- if (flags["report-json"] === true) {
5905
- writeReportHelp("auth", action);
5906
- }
5907
- else {
5908
- const lines = [];
5909
- if (authMeta.description)
5910
- lines.push(authMeta.description, "");
5911
- lines.push(`Usage: ${authMeta.usage}`);
5912
- if (authMeta.examples && authMeta.examples.length > 0) {
5913
- lines.push("", "Examples:");
5914
- for (const ex of authMeta.examples)
5915
- lines.push(` ${ex}`);
5916
- }
5917
- process.stderr.write(`${lines.join("\n")}\n`);
5918
- }
5919
- process.exit(0);
5920
- }
5921
- await authMeta.handler(flags);
6777
+ if (!authMeta)
6778
+ throw unknownActionError("auth", action, validActions);
6779
+ const result = await authMeta.handler(flags);
6780
+ writeCommandResult(result);
5922
6781
  return;
5923
6782
  }
5924
- // install-skill — dispatched before client creation
5925
6783
  if (resource === "install-skill") {
5926
- const installSkillAction = positional[1];
5927
- if (flags["help"] === true) {
5928
- if (flags["report-json"] === true) {
5929
- writeReportHelp("install-skill", "run");
5930
- }
5931
- else {
5932
- const lines = [
5933
- `Usage: ${INSTALL_SKILL_USAGE}`,
5934
- "",
5935
- INSTALL_SKILL_DESCRIPTION,
5936
- "",
5937
- "Flags:",
5938
- " --global Install to user-level global scope (default: project)",
5939
- " --agent NAME Target a specific agent: claude, codex, cursor, pi, omp",
5940
- " --target PATH Project directory to install into (default: workspace root)",
5941
- " --list-agents Print detected agents and exit",
5942
- " --dry-run Print planned skill installs without writing files",
5943
- " --plan Print planned skill installs without writing files",
5944
- ];
5945
- process.stderr.write(`${lines.join("\n")}\n`);
5946
- }
5947
- process.exit(0);
5948
- }
5949
- if (installSkillAction !== undefined && installSkillAction !== "run") {
5950
- 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",]);
5951
6788
  }
5952
- const listOnly = flags["list-agents"] === true;
5953
6789
  const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
5954
6790
  const isGlobal = flags["global"] === true;
5955
6791
  const targetDir = typeof flags["target"] === "string" ? flags["target"] : undefined;
5956
- // Resolve target agents
5957
- let targets;
5958
- if (agentFilter) {
6792
+ const targets = (() => {
6793
+ if (!agentFilter)
6794
+ return detectAgents();
5959
6795
  const def = AGENTS[agentFilter];
5960
6796
  if (!def) {
5961
- 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), });
5962
6798
  }
5963
- targets = [{ id: agentFilter, def, via: "flag", },];
5964
- }
5965
- else {
5966
- targets = detectAgents();
5967
- }
5968
- if (listOnly) {
5969
- if (targets.length === 0) {
5970
- process.stderr.write("No coding agents detected.\n");
5971
- }
5972
- else {
5973
- process.stderr.write("Detected agents:\n");
5974
- for (const t of targets) {
5975
- process.stderr.write(` ${t.id} (${t.def.name}, via ${t.via})\n`);
5976
- }
5977
- }
5978
- 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;
5979
6810
  }
5980
6811
  if (targets.length === 0) {
5981
- 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), });
5982
6813
  }
5983
6814
  const scope = isGlobal ? "global" : "project";
5984
6815
  const cwd = targetDir ?? (isGlobal ? process.cwd() : findWorkspaceRoot(process.cwd()));
6816
+ const installed = planSkillInstalls(targets, { global: isGlobal, cwd, });
5985
6817
  if (flags["plan"] === true) {
5986
6818
  writeCommandResult(planResult("install-skill", "run", {
5987
6819
  identifiers: { scope, target: cwd, },
5988
- payload: {
5989
- agents: targets.map((target) => ({
5990
- id: target.id,
5991
- name: target.def.name,
5992
- via: target.via,
5993
- })),
5994
- },
6820
+ payload: { installed, },
5995
6821
  idempotency: "none",
5996
6822
  asyncKind: "none",
5997
6823
  exitCodesOnFailure: { usage: 1, error: 2, transient: 3, },
@@ -5999,163 +6825,93 @@ async function main() {
5999
6825
  }));
6000
6826
  return;
6001
6827
  }
6002
- if (flags["dry-run"] === true) {
6003
- writeCommandResult({
6004
- dryRun: true,
6005
- action: "install-skill",
6006
- resource: "install-skill",
6007
- scope,
6008
- target: cwd,
6009
- agents: targets.map((target) => ({
6010
- id: target.id,
6011
- name: target.def.name,
6012
- via: target.via,
6013
- })),
6014
- });
6015
- return;
6016
- }
6017
- process.stderr.write(`Installing dataiku-dss skill (${scope} scope):\n`);
6018
- const results = installSkill(targets, { global: isGlobal, cwd, });
6019
- for (const r of results) {
6020
- process.stderr.write(` ${r.agent} -> ${r.path}\n`);
6021
- }
6022
- if (results.length > 0) {
6023
- process.stderr.write(`\nDone. ${results.length} skill(s) installed.\n`);
6024
- }
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
+ });
6025
6836
  return;
6026
6837
  }
6027
- // commands — machine-readable introspection (no auth needed)
6028
6838
  if (resource === "commands") {
6029
6839
  const action = positional[1];
6030
- if (flags["help"] === true) {
6031
- if (flags["report-json"] === true) {
6032
- writeReportHelp("commands", "run");
6033
- }
6034
- else {
6035
- const lines = [
6036
- `Usage: ${COMMANDS_USAGE}`,
6037
- "",
6038
- COMMANDS_DESCRIPTION,
6039
- "",
6040
- "Examples:",
6041
- ...COMMANDS_EXAMPLES.map((example) => ` ${example}`),
6042
- ];
6043
- process.stderr.write(`${lines.join("\n")}\n`);
6044
- }
6045
- process.exit(0);
6046
- }
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";
6047
6851
  if (action !== undefined && action !== "run") {
6048
- throw new UsageError(`Usage: ${COMMANDS_USAGE}`);
6852
+ throw unknownActionError("version", action, ["run",]);
6049
6853
  }
6050
- writeCommandResult(buildCommandRegistry());
6854
+ writeCommandResult(cliVersionResult());
6051
6855
  return;
6052
6856
  }
6053
6857
  if (resource === "cleanup") {
6054
6858
  const action = positional[1];
6055
- if (flags["help"] === true) {
6056
- if (flags["report-json"] === true) {
6057
- writeReportHelp("cleanup", "run");
6058
- }
6059
- else {
6060
- const lines = [
6061
- `Usage: ${CLEANUP_USAGE}`,
6062
- "",
6063
- CLEANUP_DESCRIPTION,
6064
- "",
6065
- "Examples:",
6066
- ...CLEANUP_EXAMPLES.map((example) => ` ${example}`),
6067
- ];
6068
- process.stderr.write(`${lines.join("\n")}\n`);
6069
- }
6070
- process.exit(0);
6071
- }
6859
+ currentCommandContext.action = action ?? "run";
6072
6860
  if (action !== undefined && action !== "run") {
6073
- throw new UsageError(`Usage: ${CLEANUP_USAGE}`);
6861
+ throw unknownActionError("cleanup", action, ["run",]);
6074
6862
  }
6075
6863
  const { result, exitCode, } = await runCleanup(flags);
6076
6864
  writeCommandResult(result);
6077
- process.exit(exitCode);
6865
+ if (exitCode !== 0)
6866
+ process.exit(exitCode);
6867
+ return;
6078
6868
  }
6079
6869
  if (resource === "fixtures") {
6080
6870
  const action = positional[1];
6081
- currentCommandContext.action = "run";
6082
- if (flags["help"] === true) {
6083
- if (flags["report-json"] === true) {
6084
- writeReportHelp("fixtures", "run");
6085
- }
6086
- else {
6087
- const lines = [
6088
- `Usage: ${FIXTURES_USAGE}`,
6089
- "",
6090
- FIXTURES_DESCRIPTION,
6091
- "",
6092
- "Examples:",
6093
- ...FIXTURES_EXAMPLES.map((example) => ` ${example}`),
6094
- ];
6095
- process.stderr.write(`${lines.join("\n")}\n`);
6096
- }
6097
- process.exit(0);
6098
- }
6871
+ currentCommandContext.action = action ?? "run";
6099
6872
  if (action !== undefined && action !== "run") {
6100
- throw new UsageError(`Usage: ${FIXTURES_USAGE}`);
6873
+ throw unknownActionError("fixtures", action, ["run",]);
6101
6874
  }
6102
6875
  const result = await runFixtures(flags);
6103
6876
  writeCommandResult(result);
6104
6877
  return;
6105
6878
  }
6106
- // Unknown resource
6107
- if (!commands[resource]) {
6108
- if (flags["help"]) {
6109
- printTopLevelHelp();
6110
- process.exit(0);
6111
- }
6112
- if (flags["report-json"] === true) {
6113
- throw new UsageError(`Unknown resource: ${resource}. Available: ${RESOURCE_NAMES.join(", ")}`);
6114
- }
6115
- process.stderr.write(`Unknown resource: ${resource} \nAvailable: ${RESOURCE_NAMES.join(", ")} \n`);
6116
- process.exit(1);
6117
- }
6118
- // Resource-level help
6119
- if (positional.length === 1 || flags["help"] === true) {
6120
- if (positional.length === 1) {
6121
- printResourceHelp(resource);
6122
- if (flags["help"])
6123
- process.exit(0);
6124
- process.exit(1);
6125
- }
6126
- }
6127
- const action = positional[1];
6128
- const actionMeta = commands[resource][action];
6129
- // Unknown action
6130
- if (!actionMeta) {
6131
- if (flags["report-json"] === true) {
6132
- 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",]);
6133
6884
  }
6134
- process.stderr.write(`Unknown action: ${resource} ${action} \nAvailable actions for ${resource}: ${Object.keys(commands[resource]).join(", ")} \n`);
6135
- process.exit(1);
6885
+ const { result, exitCode, } = await runBatch(flags);
6886
+ writeCommandResult(result);
6887
+ if (exitCode !== 0)
6888
+ process.exit(exitCode);
6889
+ return;
6136
6890
  }
6137
- // Action-level help
6138
- if (flags["help"] === true) {
6139
- if (flags["report-json"] === true)
6140
- writeReportHelp(resource, action);
6141
- else
6142
- printActionHelp(resource, action);
6143
- 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...]`);
6144
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));
6145
6902
  const args = positional.slice(2);
6146
6903
  if (flags["plan"] === true) {
6147
6904
  const plan = buildMutationPlan(resource, action, actionMeta, args, flags);
6148
6905
  writeCommandResult(plan);
6149
6906
  return;
6150
6907
  }
6151
- // Resolve credentials: flags > env > saved > .env
6152
6908
  const { url, apiKey, projectKey, tlsRejectUnauthorized, caCertPath, } = resolveCredentials(flags);
6153
6909
  currentCommandContext.projectKey = projectKey;
6154
6910
  if (!url) {
6155
- 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",], });
6156
6912
  }
6157
6913
  if (!apiKey) {
6158
- 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",], });
6159
6915
  }
6160
6916
  const requestTimeoutMs = num(flags["request-timeout"]);
6161
6917
  const retryMaxAttempts = num(flags["retries"]);
@@ -6180,41 +6936,17 @@ async function main() {
6180
6936
  if (entry)
6181
6937
  await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
6182
6938
  }
6183
- 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") {
6184
6943
  process.stdout.write(result);
6185
6944
  }
6186
6945
  else {
6187
6946
  writeCommandResult(result);
6188
6947
  }
6189
- const failureExitCode = commandFailureExitCode(result);
6190
- if (failureExitCode !== undefined)
6191
- process.exit(failureExitCode);
6192
6948
  }
6193
6949
  main().catch((err) => {
6194
- if (isReportJsonRequested()) {
6195
- writeErrorReport(err);
6196
- if (err instanceof UsageError)
6197
- process.exit(1);
6198
- if (err instanceof DataikuError)
6199
- process.exit(err.category === "transient" ? 3 : 2);
6200
- process.exit(2);
6201
- }
6202
- if (err instanceof UsageError) {
6203
- process.stderr.write(`${JSON.stringify({ error: err.message, code: "usage", }, null, 2)}\n`);
6204
- process.exit(1);
6205
- }
6206
- if (err instanceof DataikuError) {
6207
- const payload = {
6208
- error: err.message,
6209
- category: err.category,
6210
- retryable: err.retryable,
6211
- };
6212
- if (err.retryHint)
6213
- payload.retryHint = err.retryHint;
6214
- process.stderr.write(`${JSON.stringify(payload, null, 2)} \n`);
6215
- process.exit(err.category === "transient" ? 3 : 2);
6216
- }
6217
- const message = err instanceof Error ? err.message : String(err);
6218
- process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)} \n`);
6219
- process.exit(1);
6950
+ writeErrorReport(err);
6951
+ process.exit(errorExitCode(err));
6220
6952
  });