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