dataiku-sdk 0.12.4 → 0.12.6
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 +119 -7
- package/dist/packages/types/src/index.d.ts +5 -0
- package/dist/packages/types/src/index.js +5 -0
- package/dist/src/cli/coerce.d.ts +1 -3
- package/dist/src/cli/coerce.js +1 -17
- package/dist/src/cli/commands/app.js +709 -14
- package/dist/src/cli/commands/code-env.js +2 -2
- package/dist/src/cli/commands/dashboard.js +1 -1
- package/dist/src/cli/commands/dataset.js +2 -2
- package/dist/src/cli/commands/folder.js +1 -1
- package/dist/src/cli/commands/insight.js +2 -2
- package/dist/src/cli/commands/notebook.js +2 -2
- package/dist/src/cli/commands/recipe.js +3 -3
- package/dist/src/cli/commands/scenario.js +1 -1
- package/dist/src/cli/commands/sql.js +17 -2
- package/dist/src/cli/commands/variable.js +1 -1
- package/dist/src/cli/commands/wiki.js +1 -1
- package/dist/src/cli/contract.d.ts +12 -3
- package/dist/src/cli/contract.js +805 -49
- package/dist/src/cli/flags.js +9 -0
- package/dist/src/cli/helpers/app-permissions.d.ts +63 -0
- package/dist/src/cli/helpers/app-permissions.js +239 -0
- package/dist/src/cli/helpers/app-successor.d.ts +106 -0
- package/dist/src/cli/helpers/app-successor.js +625 -0
- package/dist/src/cli/helpers/cleanup.js +46 -12
- package/dist/src/cli/output.d.ts +12 -1
- package/dist/src/cli/output.js +53 -6
- package/dist/src/cli/usage.d.ts +1 -1
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli.js +329 -32
- package/dist/src/client.d.ts +13 -1
- package/dist/src/client.js +46 -6
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/resources/applications.d.ts +191 -1
- package/dist/src/resources/applications.js +549 -10
- package/dist/src/resources/datasets.js +8 -0
- package/dist/src/resources/futures.js +7 -2
- package/dist/src/resources/sql.d.ts +5 -0
- package/dist/src/resources/sql.js +5 -4
- package/dist/src/skill.js +78 -19
- package/dist/src/utils/cleanup-ledger.d.ts +34 -1
- package/dist/src/utils/cleanup-ledger.js +118 -7
- package/dist/src/utils/dss-url.d.ts +6 -0
- package/dist/src/utils/dss-url.js +17 -0
- package/dist/src/utils/project-incarnation.d.ts +6 -0
- package/dist/src/utils/project-incarnation.js +27 -0
- package/dist/src/utils/stable-hash.d.ts +11 -0
- package/dist/src/utils/stable-hash.js +27 -0
- package/node_modules/@sinclair/typebox/build/cjs/compiler/compiler.js +26 -46
- package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/guard.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/cjs/parser/runtime/token.d.ts +2 -2
- package/node_modules/@sinclair/typebox/build/cjs/syntax/mapping.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.d.ts +5 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/evaluate.js +11 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/index.d.ts +1 -0
- package/node_modules/@sinclair/typebox/build/cjs/system/index.js +1 -0
- package/node_modules/@sinclair/typebox/build/cjs/type/object/object.d.ts +2 -2
- package/node_modules/@sinclair/typebox/build/cjs/type/object/object.js +18 -2
- package/node_modules/@sinclair/typebox/build/cjs/value/parse/parse.d.ts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/compiler/compiler.mjs +27 -47
- package/node_modules/@sinclair/typebox/build/esm/parser/runtime/guard.d.mts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/parser/runtime/token.d.mts +2 -2
- package/node_modules/@sinclair/typebox/build/esm/syntax/mapping.d.mts +1 -1
- package/node_modules/@sinclair/typebox/build/esm/system/evaluate.d.mts +5 -0
- package/node_modules/@sinclair/typebox/build/esm/system/evaluate.mjs +7 -0
- package/node_modules/@sinclair/typebox/build/esm/system/index.d.mts +1 -0
- package/node_modules/@sinclair/typebox/build/esm/system/index.mjs +1 -0
- package/node_modules/@sinclair/typebox/build/esm/type/composite/composite.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/const/const.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/instantiate/instantiate.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/mapped/mapped.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/module/compute.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/object/object.d.mts +2 -2
- package/node_modules/@sinclair/typebox/build/esm/type/object/object.mjs +18 -2
- package/node_modules/@sinclair/typebox/build/esm/type/omit/omit.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/partial/partial.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/pick/pick.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/record/record.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/required/required.mjs +3 -3
- package/node_modules/@sinclair/typebox/build/esm/type/type/json.mjs +2 -2
- package/node_modules/@sinclair/typebox/build/esm/value/delta/delta.mjs +4 -4
- package/node_modules/@sinclair/typebox/build/esm/value/parse/parse.d.mts +1 -1
- package/node_modules/@sinclair/typebox/package.json +1 -1
- package/package.json +9 -5
- package/packages/types/dist/index.d.ts +5 -0
- package/packages/types/dist/index.js +5 -0
package/dist/src/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { num, unknownJsonInput, } from "./cli/coerce.js";
|
|
3
3
|
import { commands, } from "./cli/commands/index.js";
|
|
4
|
-
import { AGENT_CONTRACT_USAGE, AUTH_ACTIONS, BATCH_EXAMPLE_PAYLOAD, BATCH_HINT, BATCH_USAGE, buildAgentContract, buildCommandRegistry, buildMutationPlan, CLEANUP_USAGE, COMMANDS_USAGE, inferRequiresProject, isAllowedCleanupAction, supportsCleanupLedger, } from "./cli/contract.js";
|
|
4
|
+
import { AGENT_CONTRACT_USAGE, AUTH_ACTIONS, BATCH_EXAMPLE_PAYLOAD, BATCH_HINT, BATCH_PLAN_EXIT_CODES, BATCH_USAGE, buildAgentContract, buildCommandRegistry, buildMutationPlan, CLEANUP_USAGE, COMMANDS_USAGE, inferRequiresProject, isAllowedCleanupAction, supportsCleanupLedger, } from "./cli/contract.js";
|
|
5
5
|
import { runDoctor, runFixtures, } from "./cli/doctor.js";
|
|
6
6
|
import { dataikuEnvironmentEnabled, loadEnvFile, } from "./cli/env.js";
|
|
7
|
-
import { FLAG_ALIASES, parseArgs, SHORT_FLAGS, VALUE_FLAGS, } from "./cli/flags.js";
|
|
7
|
+
import { FLAG_ALIASES, isNegativeNumberToken, parseArgs, SHORT_FLAGS, VALUE_FLAGS, } from "./cli/flags.js";
|
|
8
8
|
import { cleanupLedgerEntry, } from "./cli/helpers/cleanup.js";
|
|
9
|
-
import { commandFailureExitCode, CommandResultFailure, planResult, projectResultFields, setOutputFieldProjection, writeCommandResult, } from "./cli/output.js";
|
|
9
|
+
import { commandFailureExitCode, commandFailureMessage, CommandResultFailure, isFailedWaitResult, planResult, projectResultFields, setCompactJsonOutput, setOutputFieldProjection, writeCommandResult, } from "./cli/output.js";
|
|
10
10
|
import { currentCommandContext, resolveCredentials, resolveTlsSettings, } from "./cli/runtime.js";
|
|
11
11
|
import { COMMANDS_RUN_HINT, missingActionError, noCommandError, requireArgs, unknownActionError, unknownResourceError, UsageError, } from "./cli/usage.js";
|
|
12
12
|
import { cliVersionResult, } from "./cli/version.js";
|
|
@@ -14,7 +14,8 @@ import { DataikuClient, } from "./client.js";
|
|
|
14
14
|
import { getCredentialsPath, } from "./config.js";
|
|
15
15
|
import { canonicalStatusText, ClientValidationError, DataikuError, dataikuErrorCode, } from "./errors.js";
|
|
16
16
|
import { AGENTS, detectAgents, findWorkspaceRoot, installSkill, planSkillInstalls, } from "./skill.js";
|
|
17
|
-
import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-ledger.js";
|
|
17
|
+
import { appendCleanupLedgerEntry, findCleanupLedgerBindingViolation, preflightCleanupLedgerPath, readCleanupLedger, reserveCleanupLedgerDssUrl, } from "./utils/cleanup-ledger.js";
|
|
18
|
+
import { canonicalDssUrl, } from "./utils/dss-url.js";
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Arg parsing
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -30,6 +31,22 @@ import { appendCleanupLedgerEntry, readCleanupLedger, } from "./utils/cleanup-le
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
32
|
// Auth commands (run before client creation)
|
|
32
33
|
// ---------------------------------------------------------------------------
|
|
34
|
+
function findUnboundAppCleanup(entries) {
|
|
35
|
+
for (const [index, entry,] of entries.entries()) {
|
|
36
|
+
const parsed = parseArgs(entry.cleanup.argv);
|
|
37
|
+
const [resource, action,] = parsed.positional;
|
|
38
|
+
if (resource !== "app" || action !== "delete-instance")
|
|
39
|
+
continue;
|
|
40
|
+
if (parsed.flags["unconfirmed-creation"] === true)
|
|
41
|
+
continue;
|
|
42
|
+
const expected = parsed.flags["expect-project-incarnation"];
|
|
43
|
+
if (typeof expected !== "string")
|
|
44
|
+
return { index, reason: "missing", };
|
|
45
|
+
if (!/^[0-9a-f]{64}$/.test(expected))
|
|
46
|
+
return { index, reason: "invalid", };
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
33
50
|
async function runCleanup(flags) {
|
|
34
51
|
const filePath = flags["file"];
|
|
35
52
|
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
@@ -53,6 +70,7 @@ async function runCleanup(flags) {
|
|
|
53
70
|
name: entry.name,
|
|
54
71
|
path: entry.path,
|
|
55
72
|
projectKey: entry.projectKey,
|
|
73
|
+
dssUrl: typeof entry.dssUrl === "string" ? canonicalDssUrl(entry.dssUrl) : null,
|
|
56
74
|
cleanup: entry.cleanup,
|
|
57
75
|
}));
|
|
58
76
|
if (flags["apply"] !== true) {
|
|
@@ -77,6 +95,47 @@ async function runCleanup(flags) {
|
|
|
77
95
|
tlsRejectUnauthorized,
|
|
78
96
|
caCertPath,
|
|
79
97
|
});
|
|
98
|
+
// Refuse before any DSS call when the ledger is not entirely bound to this
|
|
99
|
+
// server: a mixed ledger must never partially apply.
|
|
100
|
+
const bindingViolation = findCleanupLedgerBindingViolation(ordered, client.getBaseUrl());
|
|
101
|
+
if (bindingViolation) {
|
|
102
|
+
return {
|
|
103
|
+
result: {
|
|
104
|
+
applied: false,
|
|
105
|
+
steps,
|
|
106
|
+
bindingError: {
|
|
107
|
+
entryIndex: bindingViolation.index,
|
|
108
|
+
resource: bindingViolation.resource,
|
|
109
|
+
action: bindingViolation.action,
|
|
110
|
+
reason: bindingViolation.reason,
|
|
111
|
+
expectedDssUrl: client.getBaseUrl(),
|
|
112
|
+
...(bindingViolation.found === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: { foundDssUrl: bindingViolation.found, }),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
exitCode: 2,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Lifecycle binding is as load-bearing as server binding. Validate the whole
|
|
121
|
+
// reversed plan before step one so a later legacy app entry cannot leave an
|
|
122
|
+
// earlier cleanup applied and then fail on project-key reuse protection.
|
|
123
|
+
const lifecycleViolation = findUnboundAppCleanup(ordered);
|
|
124
|
+
if (lifecycleViolation) {
|
|
125
|
+
return {
|
|
126
|
+
result: {
|
|
127
|
+
applied: false,
|
|
128
|
+
steps,
|
|
129
|
+
lifecycleError: {
|
|
130
|
+
entryIndex: lifecycleViolation.index,
|
|
131
|
+
resource: "app",
|
|
132
|
+
action: "delete-instance",
|
|
133
|
+
reason: lifecycleViolation.reason,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
exitCode: 2,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
80
139
|
const applied = [];
|
|
81
140
|
const failures = [];
|
|
82
141
|
for (const [index, entry,] of ordered.entries()) {
|
|
@@ -87,7 +146,29 @@ async function runCleanup(flags) {
|
|
|
87
146
|
|| !commands[resource]?.[action]) {
|
|
88
147
|
throw new UsageError(`Invalid cleanup argv: ${entry.cleanup.argv.join(" ")}`);
|
|
89
148
|
}
|
|
149
|
+
if (resource === "app"
|
|
150
|
+
&& action === "delete-instance"
|
|
151
|
+
&& parsed.flags["unconfirmed-creation"] !== true
|
|
152
|
+
&& typeof parsed.flags["expect-project-incarnation"] !== "string") {
|
|
153
|
+
throw new UsageError("App cleanup entry is not bound to a project incarnation. Refusing deletion after possible project-key reuse.", "validation_failed", "Capture a new cleanup entry from the current CLI. Legacy entries cannot safely delete app projects.");
|
|
154
|
+
}
|
|
90
155
|
const result = await commands[resource][action].handler(client, args, parsed.flags);
|
|
156
|
+
if (isFailedWaitResult(result)) {
|
|
157
|
+
const failure = {
|
|
158
|
+
index,
|
|
159
|
+
cleanup: entry.cleanup,
|
|
160
|
+
error: commandFailureMessage(result),
|
|
161
|
+
result,
|
|
162
|
+
};
|
|
163
|
+
failures.push(failure);
|
|
164
|
+
if (flags["continue-on-error"] !== true) {
|
|
165
|
+
return {
|
|
166
|
+
result: { applied: true, steps, results: applied, failures, },
|
|
167
|
+
exitCode: 2,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
91
172
|
applied.push({ index, cleanup: entry.cleanup, result, });
|
|
92
173
|
}
|
|
93
174
|
catch (error) {
|
|
@@ -170,6 +251,25 @@ function supportedCommandFlags(entry) {
|
|
|
170
251
|
}
|
|
171
252
|
return supported;
|
|
172
253
|
}
|
|
254
|
+
function scopedCommandDiscoveryHint(resource, action) {
|
|
255
|
+
return `Use \`dss commands run --fields ${resource}.${action} --json\` to list the flags this command supports.`;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Resolve the registry field selectors for `commands run --fields`. Omitting the flag
|
|
259
|
+
* selects the whole registry, but an explicitly supplied value carrying no selector is a
|
|
260
|
+
* usage error: an agent that asked for a scoped subset must never be handed the full
|
|
261
|
+
* registry instead.
|
|
262
|
+
*/
|
|
263
|
+
function commandRegistrySelectors(flags) {
|
|
264
|
+
const fieldsFlag = flags["fields"];
|
|
265
|
+
if (typeof fieldsFlag !== "string")
|
|
266
|
+
return [];
|
|
267
|
+
const selectors = fieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
|
|
268
|
+
if (selectors.length === 0) {
|
|
269
|
+
throw new UsageError("--fields requires at least one selector. Expected RESOURCE or RESOURCE.ACTION[.FIELD...].", "usage_error", COMMANDS_RUN_HINT, { fields: fieldsFlag, });
|
|
270
|
+
}
|
|
271
|
+
return selectors;
|
|
272
|
+
}
|
|
173
273
|
function validateSupportedCommandFlags(resource, action, flags) {
|
|
174
274
|
const entry = cachedCommandRegistry()[resource]?.[action];
|
|
175
275
|
if (!entry)
|
|
@@ -177,9 +277,14 @@ function validateSupportedCommandFlags(resource, action, flags) {
|
|
|
177
277
|
const supported = supportedCommandFlags(entry);
|
|
178
278
|
for (const flagName of Object.keys(flags)) {
|
|
179
279
|
if (supported[flagName] !== true) {
|
|
180
|
-
throw new UsageError(`Unknown flag --${flagName} for ${resource} ${action}`, "unknown_flag");
|
|
280
|
+
throw new UsageError(`Unknown flag --${flagName} for ${resource} ${action}`, "unknown_flag", scopedCommandDiscoveryHint(resource, action));
|
|
181
281
|
}
|
|
182
282
|
}
|
|
283
|
+
if (flags["dry-run"] === true && !entry.dryRun) {
|
|
284
|
+
throw new UsageError(`--dry-run is not supported for ${resource} ${action}.`, "unknown_flag", entry.flags.some((flag) => flag.name === "plan")
|
|
285
|
+
? "Remove --dry-run; use --plan to preview the mutation instead."
|
|
286
|
+
: `Remove --dry-run; ${resource} ${action} has no dry-run mode.`);
|
|
287
|
+
}
|
|
183
288
|
}
|
|
184
289
|
function stripOptionalUsageGroupsForValidation(usage) {
|
|
185
290
|
let stripped = "";
|
|
@@ -207,23 +312,30 @@ function requiredPositionalCount(usage) {
|
|
|
207
312
|
const positionalTokens = [...requiredUsage.matchAll(/<[^>]+>/g),];
|
|
208
313
|
return positionalTokens.length;
|
|
209
314
|
}
|
|
210
|
-
function flagIsProvided(flags, name) {
|
|
315
|
+
function flagIsProvided(flags, name, allowEmpty = false) {
|
|
211
316
|
const value = flags[name];
|
|
212
317
|
if (value === undefined || value === false)
|
|
213
318
|
return false;
|
|
214
|
-
|
|
319
|
+
if (typeof value !== "string")
|
|
320
|
+
return true;
|
|
321
|
+
return allowEmpty || value.length > 0;
|
|
215
322
|
}
|
|
216
323
|
function validateRequiredCommandInputs(resource, action, args, flags, entry) {
|
|
217
324
|
const argCount = requiredPositionalCount(entry.usage);
|
|
325
|
+
const allowEmptyFlags = {};
|
|
326
|
+
for (const flag of entry.flags) {
|
|
327
|
+
if (flag.allowEmptyValue === true)
|
|
328
|
+
allowEmptyFlags[flag.name] = true;
|
|
329
|
+
}
|
|
218
330
|
if (args.length < argCount)
|
|
219
331
|
requireArgs(args, argCount, entry.usage);
|
|
220
332
|
for (const flagName of entry.requiredFlags) {
|
|
221
|
-
if (!flagIsProvided(flags, flagName)) {
|
|
333
|
+
if (!flagIsProvided(flags, flagName, allowEmptyFlags[flagName] === true)) {
|
|
222
334
|
throw new UsageError(`--${flagName} is required. Usage: ${entry.usage}`, "missing_required_flag");
|
|
223
335
|
}
|
|
224
336
|
}
|
|
225
337
|
for (const choice of entry.requiredOneOf ?? []) {
|
|
226
|
-
const satisfied = choice.oneOf.some((alternative) => alternative.every((flagName) => flagIsProvided(flags, flagName)));
|
|
338
|
+
const satisfied = choice.oneOf.some((alternative) => alternative.every((flagName) => flagIsProvided(flags, flagName, allowEmptyFlags[flagName] === true)));
|
|
227
339
|
if (!satisfied) {
|
|
228
340
|
const alternatives = choice.oneOf.map((alternative) => alternative.map((flagName) => `--${flagName}`).join(" and ")).join(" or ");
|
|
229
341
|
throw new UsageError(`One of ${alternatives} is required for ${resource} ${action}. Usage: ${entry.usage}`, "missing_required_flag");
|
|
@@ -307,6 +419,7 @@ function validateMetaCommandInputs(resource, action, flags) {
|
|
|
307
419
|
throw missingActionError("commands", ["run",], COMMANDS_USAGE);
|
|
308
420
|
if (action !== "run")
|
|
309
421
|
throw unknownActionError("commands", action, ["run",]);
|
|
422
|
+
commandRegistrySelectors(flags);
|
|
310
423
|
return action;
|
|
311
424
|
}
|
|
312
425
|
if (resource === "version") {
|
|
@@ -365,7 +478,9 @@ async function validateBatchStep(argv) {
|
|
|
365
478
|
}
|
|
366
479
|
function batchStepNeedsClient(argv) {
|
|
367
480
|
try {
|
|
368
|
-
const { positional, } = parseArgs(argv);
|
|
481
|
+
const { positional, flags, } = parseArgs(argv);
|
|
482
|
+
if (flags["plan"] === true)
|
|
483
|
+
return false;
|
|
369
484
|
const resource = positional[0];
|
|
370
485
|
if (!resource)
|
|
371
486
|
return false;
|
|
@@ -401,6 +516,79 @@ function batchStepCommandContext(argv) {
|
|
|
401
516
|
}
|
|
402
517
|
return { resource: positionals[0], action: positionals[1], };
|
|
403
518
|
}
|
|
519
|
+
const SENSITIVE_ARGV_VALUE_FLAGS = new Set([
|
|
520
|
+
"api-key",
|
|
521
|
+
]);
|
|
522
|
+
function redactArgv(argv) {
|
|
523
|
+
const redacted = [];
|
|
524
|
+
for (let index = 0; index < argv.length; index++) {
|
|
525
|
+
const arg = argv[index];
|
|
526
|
+
if (arg === "--") {
|
|
527
|
+
redacted.push(...argv.slice(index));
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
if (arg.startsWith("--")) {
|
|
531
|
+
const eqIdx = arg.indexOf("=");
|
|
532
|
+
const rawFlagName = eqIdx === -1 ? arg.slice(2) : arg.slice(2, eqIdx);
|
|
533
|
+
const flagName = FLAG_ALIASES[rawFlagName] ?? rawFlagName;
|
|
534
|
+
if (SENSITIVE_ARGV_VALUE_FLAGS.has(flagName)) {
|
|
535
|
+
if (eqIdx !== -1) {
|
|
536
|
+
redacted.push(`${arg.slice(0, eqIdx + 1)}***`);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
redacted.push(arg);
|
|
540
|
+
const next = argv[index + 1];
|
|
541
|
+
if (next !== undefined
|
|
542
|
+
&& (next === "-" || !next.startsWith("-") || isNegativeNumberToken(next))) {
|
|
543
|
+
redacted.push("***");
|
|
544
|
+
index++;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
redacted.push(arg);
|
|
551
|
+
}
|
|
552
|
+
return redacted;
|
|
553
|
+
}
|
|
554
|
+
function validateBatchStepMode(argv, index) {
|
|
555
|
+
let positional;
|
|
556
|
+
let flags;
|
|
557
|
+
try {
|
|
558
|
+
({ positional, flags, } = parseArgs(argv));
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (flags["plan"] !== true && flags["dry-run"] !== true)
|
|
564
|
+
return;
|
|
565
|
+
const resource = positional[0];
|
|
566
|
+
const action = positional[1];
|
|
567
|
+
if (!resource)
|
|
568
|
+
return;
|
|
569
|
+
if (META_COMMAND_RESOURCES[resource] === true)
|
|
570
|
+
return;
|
|
571
|
+
const resourceActions = commands[resource];
|
|
572
|
+
if (!resourceActions)
|
|
573
|
+
return;
|
|
574
|
+
const meta = resourceActions[action ?? ""];
|
|
575
|
+
if (!meta)
|
|
576
|
+
return;
|
|
577
|
+
const entry = cachedCommandRegistry()[resource]?.[action ?? ""];
|
|
578
|
+
if (!entry)
|
|
579
|
+
return;
|
|
580
|
+
if (flags["plan"] === true && !entry.flags.some((flag) => flag.name === "plan")) {
|
|
581
|
+
throw new UsageError(`Batch step ${index} requests --plan, which is not supported for ${resource} ${action}.`, "unknown_flag", "Remove --plan from the step; only mutating commands support --plan.");
|
|
582
|
+
}
|
|
583
|
+
if (flags["dry-run"] === true && !entry.dryRun) {
|
|
584
|
+
throw new UsageError(`Batch step ${index} requests --dry-run, which is not supported for ${resource} ${action}.`, "unknown_flag", "Remove --dry-run from the step.");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function validateBatchStepModes(steps) {
|
|
588
|
+
for (let index = 0; index < steps.length; index++) {
|
|
589
|
+
validateBatchStepMode(steps[index], index);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
404
592
|
function runInstallSkill(flags) {
|
|
405
593
|
const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
|
|
406
594
|
const isGlobal = flags["global"] === true;
|
|
@@ -520,13 +708,13 @@ function batchPlan(flags) {
|
|
|
520
708
|
needsClient: steps.some((argv) => batchStepNeedsClient(argv)),
|
|
521
709
|
},
|
|
522
710
|
payload: {
|
|
523
|
-
steps,
|
|
711
|
+
steps: steps.map((argv) => redactArgv(argv)),
|
|
524
712
|
continueOnError: flags["continue-on-error"] === true,
|
|
525
713
|
dryRun: flags["dry-run"] === true,
|
|
526
714
|
},
|
|
527
715
|
idempotency: "none",
|
|
528
716
|
asyncKind: "none",
|
|
529
|
-
exitCodesOnFailure:
|
|
717
|
+
exitCodesOnFailure: BATCH_PLAN_EXIT_CODES,
|
|
530
718
|
plannedAndDryRun: flags["dry-run"] === true,
|
|
531
719
|
});
|
|
532
720
|
}
|
|
@@ -575,7 +763,22 @@ async function runMetaCommand(resource, action, flags) {
|
|
|
575
763
|
currentCommandContext.action = action;
|
|
576
764
|
if (action !== "run")
|
|
577
765
|
throw unknownActionError("commands", action, ["run",]);
|
|
578
|
-
|
|
766
|
+
const selectors = commandRegistrySelectors(flags);
|
|
767
|
+
const registry = buildCommandRegistry();
|
|
768
|
+
for (const selector of selectors) {
|
|
769
|
+
const selectorParts = selector.split(".");
|
|
770
|
+
if (selectorParts.some((part) => part.length === 0)) {
|
|
771
|
+
throw new UsageError(`Invalid --fields selector: ${selector}. Expected RESOURCE or RESOURCE.ACTION[.FIELD...].`, "usage_error", COMMANDS_USAGE, { selector, });
|
|
772
|
+
}
|
|
773
|
+
const [selectedResource, selectedAction,] = selectorParts;
|
|
774
|
+
const resourceActions = registry[selectedResource];
|
|
775
|
+
if (!resourceActions)
|
|
776
|
+
throw unknownResourceError(selectedResource);
|
|
777
|
+
if (selectedAction && !resourceActions[selectedAction]) {
|
|
778
|
+
throw unknownActionError(selectedResource, selectedAction, Object.keys(resourceActions));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return { action, result: registry, exitCode: 0, };
|
|
579
782
|
}
|
|
580
783
|
if (resource === "version") {
|
|
581
784
|
if (action !== undefined && action !== "run") {
|
|
@@ -615,6 +818,82 @@ async function runMetaCommand(resource, action, flags) {
|
|
|
615
818
|
}
|
|
616
819
|
return undefined;
|
|
617
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Cleanup-ledger support is checked before a handler runs so an unsupported
|
|
823
|
+
* `--record-cleanup` fails without mutating DSS. Direct dispatch and batch steps
|
|
824
|
+
* share this guard.
|
|
825
|
+
*/
|
|
826
|
+
function assertCleanupLedgerSupported(resource, action, flags) {
|
|
827
|
+
if (typeof flags["record-cleanup"] !== "string" || flags["dry-run"] === true)
|
|
828
|
+
return;
|
|
829
|
+
if (!supportsCleanupLedger(resource, action)) {
|
|
830
|
+
throw new UsageError(`--record-cleanup is not supported for ${resource} ${action}.`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Preflight a requested cleanup ledger path before any remote mutation so an
|
|
835
|
+
* unwritable (or unwritable-to-be) ledger fails fast with no DSS-side change.
|
|
836
|
+
* Shares assertCleanupLedgerSupported's placement: direct dispatch and batch
|
|
837
|
+
* steps both call it, and dry-run/preview runs never touch the ledger.
|
|
838
|
+
*/
|
|
839
|
+
async function preflightCleanupLedgerForFlags(client, flags) {
|
|
840
|
+
if (flags["dry-run"] === true)
|
|
841
|
+
return;
|
|
842
|
+
const ledgerPath = flags["record-cleanup"];
|
|
843
|
+
if (typeof ledgerPath !== "string")
|
|
844
|
+
return;
|
|
845
|
+
if (ledgerPath.trim().length === 0) {
|
|
846
|
+
throw new UsageError("--record-cleanup requires a non-empty file path.", "validation_failed");
|
|
847
|
+
}
|
|
848
|
+
const assertExistingEntriesBound = async () => {
|
|
849
|
+
let entries;
|
|
850
|
+
try {
|
|
851
|
+
entries = await readCleanupLedger(ledgerPath);
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
if (error.code === "ENOENT")
|
|
855
|
+
return;
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
const violation = findCleanupLedgerBindingViolation(entries, client.getBaseUrl());
|
|
859
|
+
if (violation) {
|
|
860
|
+
throw new UsageError(`Existing cleanup ledger entry ${violation.index + 1} is not bound to the configured DSS server.`, "validation_failed", "Use a new cleanup ledger path, or run the mutation against the DSS server already bound to this ledger.", {
|
|
861
|
+
entryIndex: violation.index,
|
|
862
|
+
resource: violation.resource,
|
|
863
|
+
action: violation.action,
|
|
864
|
+
reason: violation.reason,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
try {
|
|
869
|
+
await preflightCleanupLedgerPath(ledgerPath);
|
|
870
|
+
await assertExistingEntriesBound();
|
|
871
|
+
await reserveCleanupLedgerDssUrl(ledgerPath, client.getBaseUrl());
|
|
872
|
+
// Re-read after winning the atomic reservation. This catches an entry
|
|
873
|
+
// appended between the initial compatibility check and reservation.
|
|
874
|
+
await assertExistingEntriesBound();
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
if (error instanceof UsageError)
|
|
878
|
+
throw error;
|
|
879
|
+
throw new UsageError(`Could not preflight cleanup ledger: ${error instanceof Error ? error.message : String(error)}`, "validation_failed");
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Append the cleanup entry for a completed handler result. Callers must pass the
|
|
884
|
+
* raw, unprojected result and must call this before any failure exit code is
|
|
885
|
+
* raised: an indeterminate post-POST outcome stays addressable only when the
|
|
886
|
+
* ledger records it first. Eligibility (dry runs, skipped work, an explicit
|
|
887
|
+
* `cleanupEligible:false`) is decided by `cleanupLedgerEntry`, never here.
|
|
888
|
+
*/
|
|
889
|
+
async function recordCleanupLedgerEntry(client, resource, action, args, flags, result, projectKey) {
|
|
890
|
+
const ledgerPath = flags["record-cleanup"];
|
|
891
|
+
if (typeof ledgerPath !== "string" || flags["dry-run"] === true)
|
|
892
|
+
return;
|
|
893
|
+
const entry = cleanupLedgerEntry(resource, action, args, flags, result, projectKey);
|
|
894
|
+
if (entry)
|
|
895
|
+
await appendCleanupLedgerEntry(ledgerPath, entry, client.getBaseUrl());
|
|
896
|
+
}
|
|
618
897
|
async function runBatch(flags) {
|
|
619
898
|
const payload = unknownJsonInput(flags);
|
|
620
899
|
if (payload === undefined) {
|
|
@@ -631,6 +910,7 @@ async function runBatch(flags) {
|
|
|
631
910
|
const context = batchStepCommandContext(argv);
|
|
632
911
|
Object.assign(currentCommandContext, { ...context, projectKey: batchProjectKey, });
|
|
633
912
|
try {
|
|
913
|
+
validateBatchStepMode(argv, index);
|
|
634
914
|
const step = await validateBatchStep(argv);
|
|
635
915
|
Object.assign(currentCommandContext, {
|
|
636
916
|
resource: step.resource,
|
|
@@ -639,13 +919,19 @@ async function runBatch(flags) {
|
|
|
639
919
|
? step.flags["project-key"]
|
|
640
920
|
: batchProjectKey,
|
|
641
921
|
});
|
|
642
|
-
return {
|
|
922
|
+
return {
|
|
923
|
+
index,
|
|
924
|
+
args: redactArgv(argv),
|
|
925
|
+
resource: step.resource,
|
|
926
|
+
action: step.action,
|
|
927
|
+
runnable: true,
|
|
928
|
+
};
|
|
643
929
|
}
|
|
644
930
|
catch (error) {
|
|
645
931
|
const envelope = buildErrorReport(error);
|
|
646
932
|
return {
|
|
647
933
|
index,
|
|
648
|
-
args: argv,
|
|
934
|
+
args: redactArgv(argv),
|
|
649
935
|
resource: context.resource,
|
|
650
936
|
action: context.action,
|
|
651
937
|
runnable: false,
|
|
@@ -658,6 +944,7 @@ async function runBatch(flags) {
|
|
|
658
944
|
exitCode: planned.every((step) => step.runnable) ? 0 : 1,
|
|
659
945
|
};
|
|
660
946
|
}
|
|
947
|
+
validateBatchStepModes(steps);
|
|
661
948
|
let projectKey = typeof flags["project-key"] === "string"
|
|
662
949
|
? flags["project-key"]
|
|
663
950
|
: dataikuEnvironmentEnabled()
|
|
@@ -700,7 +987,7 @@ async function runBatch(flags) {
|
|
|
700
987
|
let resource = context.resource;
|
|
701
988
|
let action = context.action;
|
|
702
989
|
if (firstFailureExit !== undefined && !continueOnError) {
|
|
703
|
-
results.push({ index, args: argv, resource, action, ok: null, skipped: true, });
|
|
990
|
+
results.push({ index, args: redactArgv(argv), resource, action, ok: null, skipped: true, });
|
|
704
991
|
continue;
|
|
705
992
|
}
|
|
706
993
|
Object.assign(currentCommandContext, {
|
|
@@ -742,10 +1029,19 @@ async function runBatch(flags) {
|
|
|
742
1029
|
if (!meta)
|
|
743
1030
|
throw unknownActionError(resource, action, Object.keys(resourceActions));
|
|
744
1031
|
validateSupportedCommandFlags(resource, action, stepFlags);
|
|
745
|
-
if (
|
|
746
|
-
|
|
1032
|
+
if (stepFlags["plan"] === true) {
|
|
1033
|
+
result = buildMutationPlan(resource, action, meta, positional.slice(2), stepFlags);
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
if (!client) {
|
|
1037
|
+
throw new UsageError("Missing Dataiku URL.", "missing_required_flag", "Set DATAIKU_URL or pass --url.", { requiredFlags: ["url",], env: ["DATAIKU_URL",], });
|
|
1038
|
+
}
|
|
1039
|
+
assertCleanupLedgerSupported(resource, action, stepFlags);
|
|
1040
|
+
await preflightCleanupLedgerForFlags(client, stepFlags);
|
|
1041
|
+
const stepArgs = positional.slice(2);
|
|
1042
|
+
result = await meta.handler(client, stepArgs, stepFlags);
|
|
1043
|
+
await recordCleanupLedgerEntry(client, resource, action, stepArgs, stepFlags, result, projectKey);
|
|
747
1044
|
}
|
|
748
|
-
result = await meta.handler(client, positional.slice(2), stepFlags);
|
|
749
1045
|
const failureExitCode = commandFailureExitCode(result);
|
|
750
1046
|
if (failureExitCode !== undefined)
|
|
751
1047
|
throw new CommandResultFailure(result, failureExitCode);
|
|
@@ -755,11 +1051,18 @@ async function runBatch(flags) {
|
|
|
755
1051
|
? stepFieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0)
|
|
756
1052
|
: [];
|
|
757
1053
|
const stepResult = stepFields.length > 0 ? projectResultFields(result, stepFields) : result;
|
|
758
|
-
results.push({
|
|
1054
|
+
results.push({
|
|
1055
|
+
index,
|
|
1056
|
+
args: redactArgv(argv),
|
|
1057
|
+
resource,
|
|
1058
|
+
action,
|
|
1059
|
+
ok: true,
|
|
1060
|
+
result: stepResult,
|
|
1061
|
+
});
|
|
759
1062
|
}
|
|
760
1063
|
catch (error) {
|
|
761
1064
|
const envelope = buildErrorReport(error);
|
|
762
|
-
results.push({ index, args: argv, resource, action, ok: false, error: envelope, });
|
|
1065
|
+
results.push({ index, args: redactArgv(argv), resource, action, ok: false, error: envelope, });
|
|
763
1066
|
if (firstFailureExit === undefined)
|
|
764
1067
|
firstFailureExit = envelope.exitCode;
|
|
765
1068
|
}
|
|
@@ -901,7 +1204,7 @@ function buildErrorReport(err) {
|
|
|
901
1204
|
type: "error",
|
|
902
1205
|
ok: false,
|
|
903
1206
|
error: err.message,
|
|
904
|
-
code:
|
|
1207
|
+
code: err.code,
|
|
905
1208
|
category: "dss",
|
|
906
1209
|
exitCode: err.exitCode,
|
|
907
1210
|
details: { result: err.result, },
|
|
@@ -973,6 +1276,7 @@ function writeErrorReport(err) {
|
|
|
973
1276
|
async function main() {
|
|
974
1277
|
loadEnvFile();
|
|
975
1278
|
const { positional, flags, } = parseArgs(process.argv.slice(2));
|
|
1279
|
+
setCompactJsonOutput(flags["json"] === true);
|
|
976
1280
|
const fieldsFlag = flags["fields"];
|
|
977
1281
|
if (typeof fieldsFlag === "string") {
|
|
978
1282
|
const selected = fieldsFlag.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
|
|
@@ -1040,17 +1344,10 @@ async function main() {
|
|
|
1040
1344
|
tlsRejectUnauthorized,
|
|
1041
1345
|
caCertPath,
|
|
1042
1346
|
});
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
throw new UsageError(`--record-cleanup is not supported for ${resource} ${action}.`);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1347
|
+
assertCleanupLedgerSupported(resource, action, flags);
|
|
1348
|
+
await preflightCleanupLedgerForFlags(client, flags);
|
|
1048
1349
|
const result = await actionMeta.handler(client, args, flags);
|
|
1049
|
-
|
|
1050
|
-
const entry = cleanupLedgerEntry(resource, action, args, flags, result, projectKey);
|
|
1051
|
-
if (entry)
|
|
1052
|
-
await appendCleanupLedgerEntry(flags["record-cleanup"], entry);
|
|
1053
|
-
}
|
|
1350
|
+
await recordCleanupLedgerEntry(client, resource, action, args, flags, result, projectKey);
|
|
1054
1351
|
const failureExitCode = commandFailureExitCode(result);
|
|
1055
1352
|
if (failureExitCode !== undefined)
|
|
1056
1353
|
throw new CommandResultFailure(result, failureExitCode);
|
package/dist/src/client.d.ts
CHANGED
|
@@ -151,6 +151,12 @@ export declare class DataikuClient {
|
|
|
151
151
|
get savedModels(): SavedModelsResource;
|
|
152
152
|
get modelEvaluationStores(): ModelEvaluationStoresResource;
|
|
153
153
|
constructor(config?: DataikuClientConfig);
|
|
154
|
+
/**
|
|
155
|
+
* Canonical request base URL: the exact prefix every HTTP verb concatenates
|
|
156
|
+
* paths onto (trimmed, trailing slashes stripped). Callers that must bind a
|
|
157
|
+
* recorded artifact to its originating DSS server compare against this.
|
|
158
|
+
*/
|
|
159
|
+
getBaseUrl(): string;
|
|
154
160
|
getRequestTimeoutMs(): number;
|
|
155
161
|
resolveProjectKey(paramValue?: string): string;
|
|
156
162
|
get<T = unknown>(path: string): Promise<T>;
|
|
@@ -159,7 +165,13 @@ export declare class DataikuClient {
|
|
|
159
165
|
text: string;
|
|
160
166
|
truncated: boolean;
|
|
161
167
|
}>;
|
|
162
|
-
|
|
168
|
+
/**
|
|
169
|
+
* POST with optional transient retry. Only opt in when repeating the request
|
|
170
|
+
* is safe: the server may have accepted an attempt whose response was lost.
|
|
171
|
+
*/
|
|
172
|
+
post<T = unknown>(path: string, body?: unknown, options?: {
|
|
173
|
+
retryMaxAttempts?: number;
|
|
174
|
+
}): Promise<T>;
|
|
163
175
|
postText(path: string, body?: unknown): Promise<string>;
|
|
164
176
|
put<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
165
177
|
del(path: string): Promise<void>;
|