dataiku-sdk 0.12.4 → 0.12.5
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 +118 -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 +1 -1
- 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 +8 -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 +6 -0
- package/dist/src/client.js +28 -1
- 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/skill.js +76 -18
- 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 +7 -5
- package/packages/types/dist/index.d.ts +5 -0
- package/packages/types/dist/index.js +5 -0
package/dist/src/cli/contract.js
CHANGED
|
@@ -3,14 +3,15 @@ import * as path from "node:path";
|
|
|
3
3
|
import { validateCredentials, } from "../auth.js";
|
|
4
4
|
import { getCredentialsPath, saveCredentials, } from "../config.js";
|
|
5
5
|
import { DataikuError, } from "../errors.js";
|
|
6
|
-
import {
|
|
6
|
+
import { APP_MANIFEST_CONCURRENCY_CONTROL, } from "../resources/applications.js";
|
|
7
|
+
import { jobBuildTargetTypeFromFlags, json, jsonInput, num, parseBooleanOption, parseJsonObject, requiredJsonInput, rewritePairsFromFlags, schemaColumnsInput, stringField, textInput, } from "./coerce.js";
|
|
7
8
|
import { commands, } from "./commands/index.js";
|
|
8
9
|
import { dataikuEnvironmentEnabled, } from "./env.js";
|
|
9
|
-
import { BOOLEAN_FLAGS, FLAG_ALIASES, KNOWN_LONG_FLAGS, } from "./flags.js";
|
|
10
|
+
import { BOOLEAN_FLAGS, FLAG_ALIASES, KNOWN_LONG_FLAGS, SHORT_FLAGS, } from "./flags.js";
|
|
10
11
|
import { flowZoneColor, flowZoneMoveItems, flowZoneName, } from "./helpers/flow-zone.js";
|
|
11
12
|
import { recipeBackupPath, recipeRunShouldWait, } from "./helpers/recipe.js";
|
|
12
13
|
import { encodedProjectEndpointForPlan, planResult, } from "./output.js";
|
|
13
|
-
import {
|
|
14
|
+
import { resolveTlsSettings, } from "./runtime.js";
|
|
14
15
|
import { requireArgs, UsageError, } from "./usage.js";
|
|
15
16
|
import { AGENT_CONTRACT_SCHEMA_ID, AGENT_CONTRACT_VERSION, cliVersionResult, JSON_SCHEMA_DRAFT, } from "./version.js";
|
|
16
17
|
function codeEnvWait(flags) {
|
|
@@ -109,6 +110,7 @@ const READ_ACTIONS = new Set([
|
|
|
109
110
|
"list-sql",
|
|
110
111
|
"log",
|
|
111
112
|
"log-url",
|
|
113
|
+
"manifest-version",
|
|
112
114
|
"map",
|
|
113
115
|
"metadata",
|
|
114
116
|
"peek",
|
|
@@ -127,6 +129,7 @@ const READ_ACTIONS = new Set([
|
|
|
127
129
|
"settings-get",
|
|
128
130
|
"status-by-partition",
|
|
129
131
|
"usages",
|
|
132
|
+
"verify-instance",
|
|
130
133
|
]);
|
|
131
134
|
const PROJECT_SCOPED_RESOURCES = new Set([
|
|
132
135
|
"analysis",
|
|
@@ -157,12 +160,20 @@ const AUTHENTICATED_AGENT_FLAGS = [
|
|
|
157
160
|
"insecure",
|
|
158
161
|
"ca-cert",
|
|
159
162
|
];
|
|
160
|
-
export const COMMANDS_USAGE = "dss commands run [--json]";
|
|
161
|
-
const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning.";
|
|
162
|
-
const COMMANDS_EXAMPLES = [
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
export const COMMANDS_USAGE = "dss commands run [--fields PATHS] [--json]";
|
|
164
|
+
const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning; scope it with --fields RESOURCE, RESOURCE.ACTION, or RESOURCE.ACTION.FIELD instead of reading the whole registry.";
|
|
165
|
+
const COMMANDS_EXAMPLES = [
|
|
166
|
+
"dss commands run --fields dataset --json",
|
|
167
|
+
"dss commands run --fields dataset.create --json",
|
|
168
|
+
"dss commands run --fields dataset.create.usage,dataset.create.description,dataset.create.flags,dataset.create.examples --json",
|
|
169
|
+
];
|
|
170
|
+
export const AGENT_CONTRACT_COMMAND = "dss agent contract";
|
|
171
|
+
export const AGENT_CONTRACT_USAGE = "dss agent contract [--fields PATHS] [--json]";
|
|
172
|
+
const AGENT_CONTRACT_DESCRIPTION = "Print the versioned JSON agent contract; scope bootstrap fields, use commands.actions to enumerate the surface, and read schemas only when needed.";
|
|
173
|
+
const AGENT_CONTRACT_EXAMPLES = [
|
|
174
|
+
"dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility --json",
|
|
175
|
+
"dss agent contract --fields commands.actions --json",
|
|
176
|
+
];
|
|
166
177
|
const VERSION_USAGE = "dss version";
|
|
167
178
|
const VERSION_DESCRIPTION = "Print the CLI version and git revision as JSON.";
|
|
168
179
|
const VERSION_EXAMPLES = ["dss version", "dss --version",];
|
|
@@ -186,7 +197,6 @@ const FIXTURES_EXAMPLES = [
|
|
|
186
197
|
];
|
|
187
198
|
const ALLOWED_CLEANUP_ACTIONS = new Set([
|
|
188
199
|
// Must mirror every cleanup.argv shape emitted by cleanupLedgerEntry().
|
|
189
|
-
"project delete",
|
|
190
200
|
"analysis delete",
|
|
191
201
|
"model-evaluation-store delete",
|
|
192
202
|
"ml-task delete",
|
|
@@ -201,6 +211,7 @@ const ALLOWED_CLEANUP_ACTIONS = new Set([
|
|
|
201
211
|
"code-env delete",
|
|
202
212
|
"folder delete",
|
|
203
213
|
"folder delete-file",
|
|
214
|
+
"app delete-instance",
|
|
204
215
|
]);
|
|
205
216
|
export function isAllowedCleanupAction(resource, action) {
|
|
206
217
|
return ALLOWED_CLEANUP_ACTIONS.has(`${resource} ${action}`);
|
|
@@ -258,7 +269,71 @@ function exampleArgv(example) {
|
|
|
258
269
|
const tokens = splitShellLike(example);
|
|
259
270
|
if (!tokens || tokens[0] !== "dss")
|
|
260
271
|
return undefined;
|
|
261
|
-
|
|
272
|
+
const argv = canonicalizeArgv(tokens.slice(1));
|
|
273
|
+
// Global flag aliases like `dss --version` have no command path to emit.
|
|
274
|
+
return argv[0]?.startsWith("--") ? undefined : argv;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Fold a parsed shell argv into the machine-canonical flag form the argv
|
|
278
|
+
* schemas advertise: value flags become one `--flag=VALUE` token and boolean
|
|
279
|
+
* flags stay standalone, so structured examples always validate against their
|
|
280
|
+
* own generated argv schema. Alias flags are rewritten to their canonical
|
|
281
|
+
* name. Positional tokens and tokens after the `--` separator pass through
|
|
282
|
+
* unchanged; the human-facing `shell` examples are never rewritten.
|
|
283
|
+
*/
|
|
284
|
+
function canonicalizeArgv(argv) {
|
|
285
|
+
const canonical = [];
|
|
286
|
+
let afterSeparator = false;
|
|
287
|
+
for (let index = 0; index < argv.length; index++) {
|
|
288
|
+
const token = argv[index];
|
|
289
|
+
if (afterSeparator || token === "--") {
|
|
290
|
+
canonical.push(token);
|
|
291
|
+
afterSeparator = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (token.length === 2 && token[0] === "-" && token[1] !== "-") {
|
|
295
|
+
const flagName = SHORT_FLAGS[token[1]];
|
|
296
|
+
if (!flagName) {
|
|
297
|
+
canonical.push(token);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (BOOLEAN_FLAGS.has(flagName)) {
|
|
301
|
+
canonical.push(`--${flagName}`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const next = argv[index + 1];
|
|
305
|
+
if (next === undefined || (next !== "-" && next.startsWith("-"))) {
|
|
306
|
+
canonical.push(token);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
canonical.push(`--${flagName}=${next}`);
|
|
310
|
+
index++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!token.startsWith("--")) {
|
|
314
|
+
canonical.push(token);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const eqIdx = token.indexOf("=");
|
|
318
|
+
const rawFlagName = eqIdx === -1 ? token.slice(2) : token.slice(2, eqIdx);
|
|
319
|
+
const flagName = FLAG_ALIASES[rawFlagName] ?? rawFlagName;
|
|
320
|
+
if (eqIdx !== -1) {
|
|
321
|
+
canonical.push(BOOLEAN_FLAGS.has(flagName) ? `--${flagName}` : `--${flagName}=${token.slice(eqIdx + 1)}`);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (BOOLEAN_FLAGS.has(flagName)) {
|
|
325
|
+
canonical.push(`--${flagName}`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const next = argv[index + 1];
|
|
329
|
+
if (next === undefined || next.startsWith("--")) {
|
|
330
|
+
canonical.push(`--${flagName}`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
canonical.push(`--${flagName}=${next}`);
|
|
334
|
+
index++;
|
|
335
|
+
}
|
|
336
|
+
return canonical;
|
|
262
337
|
}
|
|
263
338
|
function structuredExamples(examples, examplePayload) {
|
|
264
339
|
return (examples ?? []).map((shell) => {
|
|
@@ -286,13 +361,121 @@ function payloadJsonSchema(payloadSchema) {
|
|
|
286
361
|
? { type: "array", items: true, }
|
|
287
362
|
: { type: "object", additionalProperties: true, };
|
|
288
363
|
}
|
|
289
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Value flags the runtime meaningfully accepts with an empty value. Only flags
|
|
366
|
+
* with proven clear semantics belong here: `--version-notes ""` clears saved
|
|
367
|
+
* notes, while `--content ""` clears text payloads such as wiki articles,
|
|
368
|
+
* insights, and project-library files. Canonical argv patterns emit `*` for
|
|
369
|
+
* these and `+` for every other value flag, so `--flag=` alone can never
|
|
370
|
+
* validate for a required non-empty flag.
|
|
371
|
+
*/
|
|
372
|
+
const ALLOW_EMPTY_VALUE_FLAGS = {
|
|
373
|
+
content: true,
|
|
374
|
+
"version-notes": true,
|
|
375
|
+
};
|
|
376
|
+
function flagValueTokenPattern(flag) {
|
|
377
|
+
const names = [flag.name, ...(flag.aliases ?? []),];
|
|
378
|
+
const quantifier = flag.allowEmptyValue === true ? "*" : "+";
|
|
379
|
+
return `^--(${names.join("|")})=[\\s\\S]${quantifier}$`;
|
|
380
|
+
}
|
|
381
|
+
function booleanFlagPattern(names) {
|
|
382
|
+
return `^--(${names.join("|")})$`;
|
|
383
|
+
}
|
|
384
|
+
function shortFlagsFor(names) {
|
|
385
|
+
return Object.entries(SHORT_FLAGS)
|
|
386
|
+
.filter(([, canonical,]) => names.includes(canonical))
|
|
387
|
+
.map(([short,]) => short);
|
|
388
|
+
}
|
|
389
|
+
function shortFlagPattern(flags) {
|
|
390
|
+
const shortChars = shortFlagsFor(flags.filter((flag) => flag.kind === "boolean").map((flag) => flag.name));
|
|
391
|
+
return shortChars.length > 0 ? `^-(${shortChars.join("|")})$` : undefined;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Machine-canonical argv prefix for a registry entry: the public usage tokens
|
|
395
|
+
* between `dss` and the first positional. Synthetic single-action commands
|
|
396
|
+
* whose usage omits their action token (e.g. `dss batch ...`, `dss version`)
|
|
397
|
+
* use an actionless prefix, while real command paths that spell `run` in their
|
|
398
|
+
* usage (e.g. `dss commands run [--json]`) keep the action token.
|
|
399
|
+
*/
|
|
400
|
+
function argvPrefix(resource, action, usage) {
|
|
401
|
+
const usageTokens = usage.split(/\s+/).filter((token) => token.length > 0);
|
|
402
|
+
const head = usageTokens.slice(1);
|
|
403
|
+
return head[0] === resource && head[1] === action ? [resource, action,] : [resource,];
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Describe one invocation as a canonical argv token array. Agents emit the
|
|
407
|
+
* usage-derived prefix, then the required `<...>` positionals first; flags and
|
|
408
|
+
* values follow, so `prefixItems` pins the head of the array and the tail is
|
|
409
|
+
* validated against the canonical flag token alphabet. Constraints mirror the
|
|
410
|
+
* runtime checks in `validateRegistryCommandInputs`: required positional arity,
|
|
411
|
+
* required flags, required choice groups, and rejection of unknown long and
|
|
412
|
+
* short flags. Value flags only validate in `--flag=VALUE` form, so a bare
|
|
413
|
+
* value flag can never pass without its value; boolean flags stay standalone.
|
|
414
|
+
* Tradeoff: a positional/value token that itself begins with `-` (e.g. a
|
|
415
|
+
* negative number) is rejected unless it is the literal `-` stdin marker or a
|
|
416
|
+
* supported flag; the runtime additionally accepts such tokens via its
|
|
417
|
+
* negative-number and `--` separator rules.
|
|
418
|
+
*/
|
|
419
|
+
function argvJsonSchema(resource, action, flags, usage, requiredFlags, requiredOneOf) {
|
|
420
|
+
const prefix = argvPrefix(resource, action, usage);
|
|
421
|
+
const aliases = {};
|
|
422
|
+
for (const flag of flags) {
|
|
423
|
+
aliases[flag.name] = flag.aliases ?? [];
|
|
424
|
+
}
|
|
425
|
+
const booleanNames = flags.filter((flag) => flag.kind === "boolean")
|
|
426
|
+
.flatMap((flag) => [flag.name, ...(flag.aliases ?? []),]);
|
|
427
|
+
const valueFlags = flags.filter((flag) => flag.kind === "value");
|
|
428
|
+
const requiredPositionals = requiredPlanPositionals(usage);
|
|
429
|
+
const itemSchemas = [
|
|
430
|
+
{ not: { pattern: "^-", }, },
|
|
431
|
+
{ const: "-", },
|
|
432
|
+
{ pattern: "^--$", },
|
|
433
|
+
];
|
|
434
|
+
if (booleanNames.length > 0)
|
|
435
|
+
itemSchemas.push({ pattern: booleanFlagPattern(booleanNames), });
|
|
436
|
+
if (valueFlags.length > 0) {
|
|
437
|
+
itemSchemas.push({ pattern: valueFlags.map(flagValueTokenPattern).join("|"), });
|
|
438
|
+
}
|
|
439
|
+
const shortPattern = shortFlagPattern(flags);
|
|
440
|
+
if (shortPattern)
|
|
441
|
+
itemSchemas.push({ pattern: shortPattern, });
|
|
442
|
+
const contains = (names) => {
|
|
443
|
+
const requiredValuePatterns = flags
|
|
444
|
+
.filter((flag) => flag.kind === "value" && names.includes(flag.name))
|
|
445
|
+
.map(flagValueTokenPattern);
|
|
446
|
+
const pattern = requiredValuePatterns.length > 0
|
|
447
|
+
? requiredValuePatterns.join("|")
|
|
448
|
+
: booleanFlagPattern(names);
|
|
449
|
+
return { contains: { pattern, }, minContains: 1, };
|
|
450
|
+
};
|
|
451
|
+
const constraints = requiredFlags.map((name) => contains([name, ...(aliases[name] ?? []),]));
|
|
452
|
+
for (const choice of requiredOneOf) {
|
|
453
|
+
const alternativeSchema = (alternative) => {
|
|
454
|
+
const required = alternative.map((name) => contains([name, ...(aliases[name] ?? []),]));
|
|
455
|
+
return required.length === 1 ? required[0] : { allOf: required, };
|
|
456
|
+
};
|
|
457
|
+
constraints.push({ anyOf: choice.oneOf.map(alternativeSchema), });
|
|
458
|
+
}
|
|
290
459
|
return {
|
|
460
|
+
$schema: JSON_SCHEMA_DRAFT,
|
|
291
461
|
type: "object",
|
|
292
462
|
additionalProperties: true,
|
|
293
463
|
required: ["argv",],
|
|
294
464
|
properties: {
|
|
295
|
-
argv: {
|
|
465
|
+
argv: {
|
|
466
|
+
type: "array",
|
|
467
|
+
prefixItems: [
|
|
468
|
+
...prefix.map((token) => ({ const: token, })),
|
|
469
|
+
...requiredPositionals.map((name) => ({
|
|
470
|
+
type: "string",
|
|
471
|
+
title: name.replace(/\.\.\.$/, ""),
|
|
472
|
+
not: { pattern: "^-", },
|
|
473
|
+
})),
|
|
474
|
+
],
|
|
475
|
+
items: { type: "string", anyOf: itemSchemas, },
|
|
476
|
+
minItems: prefix.length + requiredPositionals.length,
|
|
477
|
+
...(constraints.length > 0 ? { allOf: constraints, } : {}),
|
|
478
|
+
},
|
|
296
479
|
resource: { const: resource, },
|
|
297
480
|
action: { const: action, },
|
|
298
481
|
},
|
|
@@ -311,17 +494,25 @@ function unsafeOutputs(resource, action, flags, producesLocalFile) {
|
|
|
311
494
|
});
|
|
312
495
|
}
|
|
313
496
|
if (producesLocalFile) {
|
|
497
|
+
const sensitivePermissions = resource === "app" && action === "permissions-snapshot";
|
|
314
498
|
outputs.push({
|
|
315
499
|
condition: "--output or --output-file",
|
|
316
500
|
kind: "local-file",
|
|
317
|
-
detail:
|
|
501
|
+
detail: sensitivePermissions
|
|
502
|
+
? "Command writes access-control identities and permissions to an owner-only local file."
|
|
503
|
+
: "Command writes bytes to a local path and returns JSON metadata on stdout.",
|
|
504
|
+
...(sensitivePermissions
|
|
505
|
+
? {
|
|
506
|
+
safeAlternative: "Keep the file owner-only and outside version control unless repository policy explicitly permits committing access-control data.",
|
|
507
|
+
}
|
|
508
|
+
: {}),
|
|
318
509
|
});
|
|
319
510
|
}
|
|
320
511
|
return outputs.length > 0 ? outputs : undefined;
|
|
321
512
|
}
|
|
322
|
-
export function buildCommandSchemas(resource, action, flags,
|
|
513
|
+
export function buildCommandSchemas(resource, action, flags, requiredFlags, requiredOneOf, payloadSchema, outputShape, usage) {
|
|
323
514
|
return {
|
|
324
|
-
argv: argvJsonSchema(resource, action, flags,
|
|
515
|
+
argv: argvJsonSchema(resource, action, flags, usage, requiredFlags, requiredOneOf),
|
|
325
516
|
...(payloadSchema ? { input: payloadJsonSchema(payloadSchema), } : {}),
|
|
326
517
|
output: outputJsonSchema(outputShape),
|
|
327
518
|
};
|
|
@@ -364,6 +555,10 @@ const EXPLICIT_REGISTRY_OVERRIDES = {
|
|
|
364
555
|
"scenario.update": {
|
|
365
556
|
examplePayload: { active: false, },
|
|
366
557
|
},
|
|
558
|
+
"variable.set": {
|
|
559
|
+
requiredFlags: [],
|
|
560
|
+
requiredOneOf: [{ oneOf: [["standard",], ["local",],], },],
|
|
561
|
+
},
|
|
367
562
|
"wiki.update": {
|
|
368
563
|
examplePayload: { article: { name: "Updated article", }, },
|
|
369
564
|
},
|
|
@@ -388,6 +583,11 @@ function inferSideEffect(resource, action) {
|
|
|
388
583
|
}
|
|
389
584
|
if (resource === "install-skill")
|
|
390
585
|
return "write";
|
|
586
|
+
// `project export` streams an archive over the API and writes it to a local
|
|
587
|
+
// path; the DSS-side project is untouched, so it reads remotely and only
|
|
588
|
+
// mutates the local filesystem.
|
|
589
|
+
if (resource === "project" && action === "export")
|
|
590
|
+
return "read";
|
|
391
591
|
if (resource === "data-quality" && action === "compute")
|
|
392
592
|
return "write";
|
|
393
593
|
if (READ_ACTIONS.has(action))
|
|
@@ -395,7 +595,7 @@ function inferSideEffect(resource, action) {
|
|
|
395
595
|
if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove|publish|activate|deploy|import|export|preload|upgrade|start|stop|restart|duplicate|put|rename|reply|compute|organize)/
|
|
396
596
|
.test(action)
|
|
397
597
|
// Compound actions whose mutating verb is a suffix (e.g. permissions-set, dataset-compute).
|
|
398
|
-
|| /-(set|compute)$/.test(action)) {
|
|
598
|
+
|| /-(set|compute|restore)$/.test(action)) {
|
|
399
599
|
return "write";
|
|
400
600
|
}
|
|
401
601
|
return "read";
|
|
@@ -499,12 +699,14 @@ function splitTopLevelChoices(group) {
|
|
|
499
699
|
const parts = [];
|
|
500
700
|
let depth = 0;
|
|
501
701
|
let current = "";
|
|
502
|
-
for (
|
|
702
|
+
for (let index = 0; index < group.length; index++) {
|
|
703
|
+
const char = group[index];
|
|
503
704
|
if (char === "[" || char === "(")
|
|
504
705
|
depth++;
|
|
505
706
|
else if (char === "]" || char === ")")
|
|
506
707
|
depth--;
|
|
507
|
-
|
|
708
|
+
const nextAlternative = group.slice(index + 1).trimStart();
|
|
709
|
+
if (char === "|" && depth === 0 && nextAlternative.startsWith("--")) {
|
|
508
710
|
parts.push(current);
|
|
509
711
|
current = "";
|
|
510
712
|
}
|
|
@@ -519,9 +721,9 @@ function flagsInUsageFragment(fragment) {
|
|
|
519
721
|
return extractUsageFlags(fragment.replace(/\[[^\]]*\]/g, " "));
|
|
520
722
|
}
|
|
521
723
|
/**
|
|
522
|
-
* Split required usage flags into unconditional flags and
|
|
523
|
-
*
|
|
524
|
-
* (
|
|
724
|
+
* Split required usage flags into unconditional flags and required choice
|
|
725
|
+
* groups. A required `(--a X | --b Y)` group becomes a requiredOneOf entry
|
|
726
|
+
* (at least one alternative; an alternative listing several flags must be
|
|
525
727
|
* supplied together) instead of marking every flag as unconditionally required.
|
|
526
728
|
*/
|
|
527
729
|
function deriveRequiredUsage(usage) {
|
|
@@ -536,6 +738,14 @@ function deriveRequiredUsage(usage) {
|
|
|
536
738
|
const oneOf = alternatives
|
|
537
739
|
.map((alternative) => flagsInUsageFragment(alternative))
|
|
538
740
|
.filter((alternativeFlags) => alternativeFlags.length > 0);
|
|
741
|
+
// A group mixing a positional alternative with flags (e.g. sql query's
|
|
742
|
+
// `(SQL | --sql QUERY | --sql-file PATH | --sql - | --stdin)`) cannot be
|
|
743
|
+
// expressed as a flag-only choice: filtering out the flagless alternative
|
|
744
|
+
// would falsely require one of the flags. Omit the group from the flag
|
|
745
|
+
// contract entirely; the handler stays the runtime authority.
|
|
746
|
+
if (alternatives.some((alternative) => flagsInUsageFragment(alternative).length === 0)) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
539
749
|
if (oneOf.length > 1)
|
|
540
750
|
requiredOneOf.push({ oneOf, });
|
|
541
751
|
else if (oneOf.length === 1)
|
|
@@ -556,7 +766,7 @@ const GLOBAL_FLAG_VALUE_HINTS = {
|
|
|
556
766
|
/** Derive a value placeholder (and enum members) for each value flag from its usage token. */
|
|
557
767
|
function extractFlagValueHints(usage) {
|
|
558
768
|
const hints = new Map();
|
|
559
|
-
for (const match of usage.matchAll(/--([a-z0-9-]+)\s+([
|
|
769
|
+
for (const match of usage.matchAll(/--([a-z0-9-]+)\s+([A-Za-z][A-Za-z0-9_-]*(?:\|[A-Za-z][A-Za-z0-9_-]*)+)(?![A-Za-z0-9_=-])/g)) {
|
|
560
770
|
const flag = FLAG_ALIASES[match[1]] ?? match[1];
|
|
561
771
|
if (!hints.has(flag)) {
|
|
562
772
|
hints.set(flag, { valueType: "enum", enumValues: match[2].split("|"), });
|
|
@@ -575,20 +785,40 @@ function inferPayloadSchema(inputContract) {
|
|
|
575
785
|
}
|
|
576
786
|
return { ...inputContract, jsonShape: "object", };
|
|
577
787
|
}
|
|
578
|
-
|
|
788
|
+
/**
|
|
789
|
+
* Long-running commands surface exit 4 when the remote work itself fails.
|
|
790
|
+
* `batch run` is synchronous, yet any step it dispatches may be a long-running
|
|
791
|
+
* lifecycle command (app instance create/delete, job build, scenario run), so a
|
|
792
|
+
* batch can end on 4 as well and must advertise it.
|
|
793
|
+
* `recipe assert-unchanged` is synchronous: a drift check exits 4 with the
|
|
794
|
+
* stable code `assertion_failed`, advertised as `assertionFailure` and never
|
|
795
|
+
* as a long-running outcome.
|
|
796
|
+
*/
|
|
797
|
+
function inferExitCodes(resource, action, asyncKind) {
|
|
798
|
+
const longRunning = asyncKind !== "none" || `${resource}.${action}` === "batch.run";
|
|
799
|
+
const assertion = `${resource}.${action}` === "recipe.assert-unchanged"
|
|
800
|
+
|| `${resource}.${action}` === "batch.run";
|
|
579
801
|
return {
|
|
580
802
|
ok: 0,
|
|
581
803
|
usage: 1,
|
|
582
804
|
error: 2,
|
|
583
805
|
transient: 3,
|
|
584
|
-
...(
|
|
806
|
+
...(longRunning ? { longRunningFailure: 4, } : {}),
|
|
807
|
+
...(assertion ? { assertionFailure: 4, } : {}),
|
|
585
808
|
};
|
|
586
809
|
}
|
|
587
810
|
function cleanupCommandFromDeleteUsage(resource, action) {
|
|
811
|
+
if (resource === "project" && (action === "create" || action === "duplicate")) {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
588
814
|
if (!(action.startsWith("create") || action === "clone" || action === "duplicate")) {
|
|
589
815
|
return undefined;
|
|
590
816
|
}
|
|
591
|
-
const deleteAction = action === "create-rule"
|
|
817
|
+
const deleteAction = action === "create-rule"
|
|
818
|
+
? "delete-rule"
|
|
819
|
+
: action === "create-instance" || action === "create-successor-instance"
|
|
820
|
+
? "delete-instance"
|
|
821
|
+
: "delete";
|
|
592
822
|
const deleteUsage = commands[resource]?.[deleteAction]?.usage;
|
|
593
823
|
if (!deleteUsage)
|
|
594
824
|
return undefined;
|
|
@@ -623,23 +853,54 @@ function inferAsyncKind(resource, action) {
|
|
|
623
853
|
return "future";
|
|
624
854
|
if (resource === "code" && action === "run")
|
|
625
855
|
return "future";
|
|
856
|
+
if (resource === "app"
|
|
857
|
+
&& ["create-instance", "create-successor-instance", "delete-instance",].includes(action)) {
|
|
858
|
+
return "future";
|
|
859
|
+
}
|
|
626
860
|
return "none";
|
|
627
861
|
}
|
|
628
|
-
function inferIdempotency(sideEffect, action, usage) {
|
|
862
|
+
function inferIdempotency(resource, sideEffect, action, usage) {
|
|
629
863
|
if (sideEffect === "read")
|
|
630
864
|
return "safe";
|
|
631
865
|
if (action.startsWith("create") && usage.includes("--if-not-exists"))
|
|
632
866
|
return "if-not-exists";
|
|
633
867
|
if (action.startsWith("delete") && usage.includes("--if-exists"))
|
|
634
868
|
return "if-exists";
|
|
869
|
+
if (`${resource}.${action}` === "app.set-manifest-version")
|
|
870
|
+
return "none";
|
|
871
|
+
// `app delete-instance` converges without an `--if-exists` flag: an absent
|
|
872
|
+
// target project is reported as an already-absent success instead of an
|
|
873
|
+
// error, so replaying the delete cannot fail on absence alone.
|
|
874
|
+
if (`${resource}.${action}` === "app.delete-instance")
|
|
875
|
+
return "convergent";
|
|
635
876
|
if (/^(clear|refresh|set|save)/.test(action))
|
|
636
877
|
return "convergent";
|
|
637
878
|
return "none";
|
|
638
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* `app create-instance` and `app create-successor-instance` hand back a DSS
|
|
882
|
+
* creation future, and the created project only becomes safe to delete once
|
|
883
|
+
* that future is terminal. A directly runnable delete command cannot express
|
|
884
|
+
* that gate, so these commands advertise no `cleanupCommand` at all: recovery
|
|
885
|
+
* goes through the recorded cleanup ledger, whose replay carries the creation
|
|
886
|
+
* future ID and refuses to delete while creation is unconfirmed.
|
|
887
|
+
*/
|
|
888
|
+
const LEDGER_ONLY_CLEANUP_KEYS = {
|
|
889
|
+
"app.create-instance": true,
|
|
890
|
+
"app.create-successor-instance": true,
|
|
891
|
+
};
|
|
892
|
+
const LEDGER_ONLY_CLEANUP_HINT = "Pass `--record-cleanup PATH` when creating, then recover with `dss cleanup --file PATH --apply`. Do not issue direct instance deletion as cleanup: it bypasses the creation-future gate and can delete the project while DSS is still creating it.";
|
|
639
893
|
export function inferCleanupHint(resource, action) {
|
|
640
894
|
if (!(action.startsWith("create") || action === "clone"))
|
|
641
895
|
return undefined;
|
|
642
|
-
|
|
896
|
+
if (LEDGER_ONLY_CLEANUP_KEYS[`${resource}.${action}`] === true) {
|
|
897
|
+
return LEDGER_ONLY_CLEANUP_HINT;
|
|
898
|
+
}
|
|
899
|
+
const deleteAction = action === "create-rule"
|
|
900
|
+
? "delete-rule"
|
|
901
|
+
: action === "create-instance" || action === "create-successor-instance"
|
|
902
|
+
? "delete-instance"
|
|
903
|
+
: "delete";
|
|
643
904
|
const deleteUsage = commands[resource]?.[deleteAction]?.usage;
|
|
644
905
|
const ifExists = deleteUsage?.includes("--if-exists") ? " --if-exists" : "";
|
|
645
906
|
if (resource === "code-env") {
|
|
@@ -687,9 +948,12 @@ function buildRegistryEntry(resource, action, meta) {
|
|
|
687
948
|
?? inferPayloadSchema(inputContract);
|
|
688
949
|
const examplePayload = meta.examplePayload
|
|
689
950
|
?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.examplePayload;
|
|
690
|
-
const
|
|
951
|
+
const inferredCleanupCommand = meta.cleanupCommand
|
|
691
952
|
?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.cleanupCommand
|
|
692
953
|
?? cleanupCommandFromDeleteUsage(resource, action);
|
|
954
|
+
const cleanupCommand = LEDGER_ONLY_CLEANUP_KEYS[`${resource}.${action}`] === true
|
|
955
|
+
? undefined
|
|
956
|
+
: inferredCleanupCommand;
|
|
693
957
|
const flagMetadata = flags.map((name) => {
|
|
694
958
|
const aliases = Object.entries(FLAG_ALIASES)
|
|
695
959
|
.filter(([raw, canonical,]) => canonical === name && new RegExp(`--${raw}(?![a-z0-9-])`).test(meta.usage))
|
|
@@ -698,15 +962,17 @@ function buildRegistryEntry(resource, action, meta) {
|
|
|
698
962
|
const kind = flagKind(name);
|
|
699
963
|
if (kind === "boolean")
|
|
700
964
|
return { name, kind, ...aliasPart, };
|
|
965
|
+
const allowEmptyPart = ALLOW_EMPTY_VALUE_FLAGS[name] === true ? { allowEmptyValue: true, } : {};
|
|
701
966
|
const hint = valueHints.get(name) ?? GLOBAL_FLAG_VALUE_HINTS[name];
|
|
702
967
|
if (!hint)
|
|
703
|
-
return { name, kind, ...aliasPart, };
|
|
968
|
+
return { name, kind, ...aliasPart, ...allowEmptyPart, };
|
|
704
969
|
return {
|
|
705
970
|
name,
|
|
706
971
|
kind,
|
|
707
972
|
valueType: hint.valueType,
|
|
708
973
|
...(hint.enumValues ? { enumValues: hint.enumValues, } : {}),
|
|
709
974
|
...aliasPart,
|
|
975
|
+
...allowEmptyPart,
|
|
710
976
|
};
|
|
711
977
|
});
|
|
712
978
|
const positionals = extractPositionals(meta.usage);
|
|
@@ -734,17 +1000,17 @@ function buildRegistryEntry(resource, action, meta) {
|
|
|
734
1000
|
producesLocalFile,
|
|
735
1001
|
mutatesDss,
|
|
736
1002
|
async: asyncKind,
|
|
737
|
-
idempotency: inferIdempotency(sideEffect, action, meta.usage),
|
|
1003
|
+
idempotency: inferIdempotency(resource, sideEffect, action, meta.usage),
|
|
738
1004
|
dryRun: meta.usage.includes("--dry-run"),
|
|
739
1005
|
requiredFlags: uniqueRequiredFlags,
|
|
740
1006
|
optionalFlags: uniqueOptionalFlags,
|
|
741
1007
|
...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
|
|
742
1008
|
...(payloadSchema ? { payloadSchema, } : {}),
|
|
743
|
-
schemas: buildCommandSchemas(resource, action, flagMetadata,
|
|
1009
|
+
schemas: buildCommandSchemas(resource, action, flagMetadata, uniqueRequiredFlags, requiredOneOf, payloadSchema, outputShape, meta.usage),
|
|
744
1010
|
...(unsafe ? { unsafeOutputs: unsafe, } : {}),
|
|
745
1011
|
...(examplePayload !== undefined ? { examplePayload, } : {}),
|
|
746
1012
|
...(cleanupCommand ? { cleanupCommand, } : {}),
|
|
747
|
-
exitCodes: inferExitCodes(asyncKind),
|
|
1013
|
+
exitCodes: inferExitCodes(resource, action, asyncKind),
|
|
748
1014
|
...(cleanupHint ? { cleanupHint, } : {}),
|
|
749
1015
|
agentContractVersion: AGENT_CONTRACT_VERSION,
|
|
750
1016
|
};
|
|
@@ -838,6 +1104,7 @@ function commandFlagJsonSchema() {
|
|
|
838
1104
|
valueType: { type: "string", },
|
|
839
1105
|
enumValues: { type: "array", items: { type: "string", }, },
|
|
840
1106
|
aliases: { type: "array", items: { type: "string", }, },
|
|
1107
|
+
allowEmptyValue: { type: "boolean", },
|
|
841
1108
|
},
|
|
842
1109
|
};
|
|
843
1110
|
}
|
|
@@ -978,6 +1245,15 @@ export function buildAgentContract() {
|
|
|
978
1245
|
cli: cliVersionResult(),
|
|
979
1246
|
commands: {
|
|
980
1247
|
discoveryCommand: "dss commands run",
|
|
1248
|
+
scopedDiscoveryCommand: "dss commands run --fields RESOURCE[.ACTION[.FIELD...]] --json",
|
|
1249
|
+
actionIndexCommand: "dss agent contract --fields commands.actions --json",
|
|
1250
|
+
compactOutputFlag: "--json",
|
|
1251
|
+
compactOutputHint: "Pass --json to emit compact single-line JSON and reduce agent context usage.",
|
|
1252
|
+
scopedDiscoveryExamples: [
|
|
1253
|
+
"dss commands run --fields dataset --json",
|
|
1254
|
+
"dss commands run --fields dataset.create --json",
|
|
1255
|
+
],
|
|
1256
|
+
scopedDiscoveryHint: "--fields RESOURCE returns every action of one resource; --fields RESOURCE.ACTION returns a single registry entry keyed by the dotted path; append .FIELD paths to project nested metadata. Comma-separate paths to select several.",
|
|
981
1257
|
actions: commandActionSummary(registry),
|
|
982
1258
|
},
|
|
983
1259
|
schemas: {
|
|
@@ -1000,11 +1276,18 @@ export function buildAgentContract() {
|
|
|
1000
1276
|
format: "jsonl",
|
|
1001
1277
|
events: ["warning", "trace", "error",],
|
|
1002
1278
|
error: "single-final-error-event-on-nonzero-exit",
|
|
1279
|
+
failureRouting: {
|
|
1280
|
+
commandFailure: "doctor/batch/cleanup failures: single JSON stdout; empty stderr",
|
|
1281
|
+
dispatchFailure: "usage/unknown/transport/internal failures: empty stdout; single JSON error stderr",
|
|
1282
|
+
},
|
|
1003
1283
|
},
|
|
1004
1284
|
},
|
|
1005
1285
|
planning: {
|
|
1006
1286
|
discoveryCommand: "dss commands run",
|
|
1007
|
-
contractCommand:
|
|
1287
|
+
contractCommand: AGENT_CONTRACT_COMMAND,
|
|
1288
|
+
bootstrapCommand: "dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility --json",
|
|
1289
|
+
preferredDiscoveryCommand: "dss commands run --fields RESOURCE.ACTION --json",
|
|
1290
|
+
actionIndexCommand: "dss agent contract --fields commands.actions --json",
|
|
1008
1291
|
mutatingCommandsAdvertisePlan: true,
|
|
1009
1292
|
},
|
|
1010
1293
|
compatibility: {
|
|
@@ -1021,20 +1304,54 @@ function exitCodesOnFailure(entry) {
|
|
|
1021
1304
|
...(entry.exitCodes.longRunningFailure !== undefined
|
|
1022
1305
|
? { longRunningFailure: entry.exitCodes.longRunningFailure, }
|
|
1023
1306
|
: {}),
|
|
1307
|
+
...(entry.exitCodes.assertionFailure !== undefined
|
|
1308
|
+
? { assertionFailure: entry.exitCodes.assertionFailure, }
|
|
1309
|
+
: {}),
|
|
1024
1310
|
};
|
|
1025
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Plan-local project key resolution. `--plan` is a purely local preview, so it
|
|
1314
|
+
* resolves the target project from the explicit `--project-key` flag and the
|
|
1315
|
+
* documented `DATAIKU_PROJECT_KEY` environment variable only. The saved
|
|
1316
|
+
* credentials file is never opened and no API key is ever resolved, so planning
|
|
1317
|
+
* can neither depend on nor leak stored secrets.
|
|
1318
|
+
*/
|
|
1319
|
+
function planProjectKeyFromArgs(flags) {
|
|
1320
|
+
const fromFlag = flags["project-key"];
|
|
1321
|
+
if (typeof fromFlag === "string" && fromFlag.trim().length > 0)
|
|
1322
|
+
return fromFlag.trim();
|
|
1323
|
+
if (!dataikuEnvironmentEnabled())
|
|
1324
|
+
return undefined;
|
|
1325
|
+
const fromEnv = process.env.DATAIKU_PROJECT_KEY;
|
|
1326
|
+
return fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv.trim() : undefined;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Commands whose plan must pin the target with an explicit `--project-key`
|
|
1330
|
+
* instead of falling back to ambient `DATAIKU_PROJECT_KEY`, so a cleanup plan
|
|
1331
|
+
* can never silently retarget a project the caller did not name.
|
|
1332
|
+
*/
|
|
1333
|
+
const EXPLICIT_PLAN_PROJECT_KEY = { "app.delete-instance": true, };
|
|
1334
|
+
/** Plan-local project key, or the canonical `--project-key` usage error. */
|
|
1335
|
+
function requiredPlanProjectKey(flags, usage) {
|
|
1336
|
+
return planProjectKeyFromArgs(flags) ?? requiredPlanFlag(flags, "project-key", usage);
|
|
1337
|
+
}
|
|
1026
1338
|
function projectKeyForPlan(entry, flags) {
|
|
1027
1339
|
if (!entry.requiresProject)
|
|
1028
1340
|
return undefined;
|
|
1029
|
-
|
|
1341
|
+
if (EXPLICIT_PLAN_PROJECT_KEY[`${entry.resource}.${entry.action}`] === true) {
|
|
1342
|
+
return requiredPlanFlag(flags, "project-key", entry.usage);
|
|
1343
|
+
}
|
|
1344
|
+
const projectKey = planProjectKeyFromArgs(flags);
|
|
1030
1345
|
if (projectKey)
|
|
1031
1346
|
return projectKey;
|
|
1032
1347
|
throw new UsageError(`Missing project key. Pass --project-key or set DATAIKU_PROJECT_KEY before planning ${entry.resource} ${entry.action}.`);
|
|
1033
1348
|
}
|
|
1034
1349
|
function requiredPlanFlag(flags, name, usage) {
|
|
1035
1350
|
const value = flags[name];
|
|
1351
|
+
// Return trimmed like the runtime's requiredStringFlag so plans advertise
|
|
1352
|
+
// exactly the identifier the command will act on.
|
|
1036
1353
|
if (typeof value === "string" && value.trim().length > 0)
|
|
1037
|
-
return value;
|
|
1354
|
+
return value.trim();
|
|
1038
1355
|
throw new UsageError(`--${name} is required. Usage: ${usage}`);
|
|
1039
1356
|
}
|
|
1040
1357
|
function optionalJsonFlag(flags, name) {
|
|
@@ -1289,6 +1606,31 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
1289
1606
|
payload: { datasetName: name, connection, dsType, projectKey, },
|
|
1290
1607
|
};
|
|
1291
1608
|
}
|
|
1609
|
+
case "dataset.clone": {
|
|
1610
|
+
const source = args[0];
|
|
1611
|
+
const target = args[1];
|
|
1612
|
+
return {
|
|
1613
|
+
method: "POST",
|
|
1614
|
+
endpoint: projectEndpoint("/datasets/"),
|
|
1615
|
+
identifiers: { source, target, },
|
|
1616
|
+
payload: {
|
|
1617
|
+
sourceDataset: source,
|
|
1618
|
+
targetDataset: target,
|
|
1619
|
+
path: flags["path"],
|
|
1620
|
+
table: flags["table"],
|
|
1621
|
+
metastoreTableName: flags["metastore-table"],
|
|
1622
|
+
allowSamePath: flags["allow-same-path"] === true,
|
|
1623
|
+
projectKey,
|
|
1624
|
+
},
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
case "dataset.rename":
|
|
1628
|
+
return {
|
|
1629
|
+
method: "POST",
|
|
1630
|
+
endpoint: projectEndpoint("/actions/renameDataset"),
|
|
1631
|
+
identifiers: { oldName: args[0], newName: args[1], },
|
|
1632
|
+
payload: { oldName: args[0], newName: args[1], },
|
|
1633
|
+
};
|
|
1292
1634
|
case "dataset.delete":
|
|
1293
1635
|
return {
|
|
1294
1636
|
method: "DELETE",
|
|
@@ -1311,6 +1653,55 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
1311
1653
|
identifiers: { name: id, },
|
|
1312
1654
|
payload: requiredPlanJsonInput(flags, entry.usage),
|
|
1313
1655
|
};
|
|
1656
|
+
case "recipe.add-input":
|
|
1657
|
+
case "recipe.remove-input": {
|
|
1658
|
+
const role = flags["role"] ?? "main";
|
|
1659
|
+
return {
|
|
1660
|
+
method: "PUT",
|
|
1661
|
+
endpoint: projectEndpoint(`/recipes/${encodeURIComponent(args[0])}`),
|
|
1662
|
+
identifiers: { recipe: args[0], dataset: args[1], role, },
|
|
1663
|
+
payload: {
|
|
1664
|
+
operation: action === "add-input" ? "append" : "remove",
|
|
1665
|
+
dataset: args[1],
|
|
1666
|
+
role,
|
|
1667
|
+
projectKey,
|
|
1668
|
+
},
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
case "recipe.clone": {
|
|
1672
|
+
const positionalSource = args[0];
|
|
1673
|
+
const fromFlag = typeof flags["from"] === "string" ? flags["from"].trim() : "";
|
|
1674
|
+
const source = positionalSource ?? fromFlag;
|
|
1675
|
+
if (!source) {
|
|
1676
|
+
throw new UsageError(`Source recipe is required. Usage: ${entry.usage}`, "missing_required_flag");
|
|
1677
|
+
}
|
|
1678
|
+
if (positionalSource && fromFlag && positionalSource !== fromFlag) {
|
|
1679
|
+
throw new UsageError("Positional source and --from must match when both are provided.", "invalid_enum");
|
|
1680
|
+
}
|
|
1681
|
+
const toFlag = typeof flags["to"] === "string" ? flags["to"].trim() : "";
|
|
1682
|
+
const nameFlag = typeof flags["name"] === "string" ? flags["name"].trim() : "";
|
|
1683
|
+
const target = toFlag || nameFlag;
|
|
1684
|
+
if (!target) {
|
|
1685
|
+
throw new UsageError(`--name or --to is required. Usage: ${entry.usage}`, "missing_required_flag");
|
|
1686
|
+
}
|
|
1687
|
+
return {
|
|
1688
|
+
method: "POST",
|
|
1689
|
+
endpoint: projectEndpoint("/recipes/"),
|
|
1690
|
+
identifiers: { source, target, },
|
|
1691
|
+
payload: {
|
|
1692
|
+
sourceRecipe: source,
|
|
1693
|
+
targetRecipe: target,
|
|
1694
|
+
inputRewrites: rewritePairsFromFlags(flags, "replace-input"),
|
|
1695
|
+
outputRewrites: rewritePairsFromFlags(flags, "replace-output"),
|
|
1696
|
+
payloadTextRewrites: rewritePairsFromFlags(flags, "replace-payload-text"),
|
|
1697
|
+
outputDataset: flags["output"],
|
|
1698
|
+
copyOutputSettings: flags["copy-output-settings"] === true,
|
|
1699
|
+
outputPath: flags["path"],
|
|
1700
|
+
metastoreTableName: flags["metastore-table"],
|
|
1701
|
+
projectKey,
|
|
1702
|
+
},
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1314
1705
|
case "recipe.delete":
|
|
1315
1706
|
return {
|
|
1316
1707
|
method: "DELETE",
|
|
@@ -1698,13 +2089,376 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
1698
2089
|
identifiers: { filePath: id, },
|
|
1699
2090
|
payload: uploadPayload(id),
|
|
1700
2091
|
};
|
|
1701
|
-
case "app.
|
|
2092
|
+
case "app.save-instance-manifest": {
|
|
2093
|
+
const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
|
|
2094
|
+
return {
|
|
2095
|
+
method: "PUT",
|
|
2096
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`,
|
|
2097
|
+
identifiers: { projectKey: targetProjectKey, },
|
|
2098
|
+
payload: requiredPlanJsonInput(flags, entry.usage),
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
case "app.create-instance": {
|
|
2102
|
+
const payload = requiredPlanJsonInput(flags, entry.usage);
|
|
2103
|
+
const rawTargetProjectKey = stringField(payload, ["targetProjectKey",]);
|
|
2104
|
+
if (!rawTargetProjectKey || rawTargetProjectKey.trim() === "") {
|
|
2105
|
+
throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${entry.usage}`);
|
|
2106
|
+
}
|
|
2107
|
+
// The runtime trims and rewrites body.targetProjectKey; the plan
|
|
2108
|
+
// advertises the normalized identifier and payload.
|
|
2109
|
+
const targetProjectKey = rawTargetProjectKey.trim();
|
|
2110
|
+
const normalizedPayload = { ...payload, targetProjectKey, };
|
|
1702
2111
|
return {
|
|
1703
2112
|
method: "POST",
|
|
1704
2113
|
endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
|
|
1705
|
-
identifiers: {
|
|
1706
|
-
|
|
2114
|
+
identifiers: {
|
|
2115
|
+
appId: id,
|
|
2116
|
+
targetProjectKey,
|
|
2117
|
+
incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
|
|
2118
|
+
incarnationObservationRequests: [
|
|
2119
|
+
{
|
|
2120
|
+
method: "GET",
|
|
2121
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
|
|
2122
|
+
when: flags["wait"] === true
|
|
2123
|
+
? "after-terminal-future-target"
|
|
2124
|
+
: "conditional-inline-hasResult-target",
|
|
2125
|
+
intent: "After DSS reports inline or terminal creation success for the requested key, observe creationTag for later cleanup binding.",
|
|
2126
|
+
},
|
|
2127
|
+
],
|
|
2128
|
+
preflightRequests: [
|
|
2129
|
+
{
|
|
2130
|
+
method: "GET",
|
|
2131
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
|
|
2132
|
+
when: "before-create",
|
|
2133
|
+
intent: "Require the target project to be absent before creating the instance.",
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
method: "GET",
|
|
2137
|
+
endpoint: "/public/api/projects/",
|
|
2138
|
+
when: "conditional",
|
|
2139
|
+
intent: "Fallback list probe issued only when the direct project GET is forbidden (403) to confirm the target is absent; an unconfirmed target rejects creation.",
|
|
2140
|
+
},
|
|
2141
|
+
],
|
|
2142
|
+
note: "Strict preflight: the instance POST runs only after the payload targetProjectKey is confirmed absent. Creation never writes into an existing or unprovable project.",
|
|
2143
|
+
},
|
|
2144
|
+
payload: normalizedPayload,
|
|
2145
|
+
wait: flags["wait"] === true,
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
case "app.set-manifest-version": {
|
|
2149
|
+
const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
|
|
2150
|
+
const payloadPatch = {};
|
|
2151
|
+
const version = flags["manifest-version"];
|
|
2152
|
+
if (version !== undefined) {
|
|
2153
|
+
if (version.trim() === "") {
|
|
2154
|
+
throw new UsageError("App manifest version must be a non-empty string.", "validation_failed", `Usage: ${entry.usage}`);
|
|
2155
|
+
}
|
|
2156
|
+
payloadPatch.version = version;
|
|
2157
|
+
}
|
|
2158
|
+
const versionNotes = flags["version-notes"];
|
|
2159
|
+
if (versionNotes !== undefined)
|
|
2160
|
+
payloadPatch.versionNotes = versionNotes;
|
|
2161
|
+
if (version === undefined && versionNotes === undefined) {
|
|
2162
|
+
throw new UsageError("At least one of --manifest-version or --version-notes is required.", "usage_error", `Usage: ${entry.usage}`);
|
|
2163
|
+
}
|
|
2164
|
+
const expectHash = flags["expect-hash"];
|
|
2165
|
+
if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
|
|
2166
|
+
throw new UsageError("Expected manifest hash must be a 64-character SHA-256 hex digest.", "validation_failed", `Usage: ${entry.usage}`);
|
|
2167
|
+
}
|
|
2168
|
+
return {
|
|
2169
|
+
method: "PUT",
|
|
2170
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`,
|
|
2171
|
+
identifiers: {
|
|
2172
|
+
projectKey: targetProjectKey,
|
|
2173
|
+
payloadPatch,
|
|
2174
|
+
...(expectHash !== undefined ? { expectHash: expectHash.toLowerCase(), } : {}),
|
|
2175
|
+
concurrencyControl: APP_MANIFEST_CONCURRENCY_CONTROL,
|
|
2176
|
+
staleReadCheck: expectHash === undefined
|
|
2177
|
+
? "none"
|
|
2178
|
+
: "client-side-expect-hash-compare-before-put",
|
|
2179
|
+
note: expectHash === undefined
|
|
2180
|
+
? "Unconditional PUT: no stale-read check is armed because --expect-hash was not supplied."
|
|
2181
|
+
: "The hash is compared client-side against a fresh read; the PUT itself stays unconditional, so this command can overwrite a writer that commits between that read and this PUT without detecting the lost update.",
|
|
2182
|
+
},
|
|
1707
2183
|
};
|
|
2184
|
+
}
|
|
2185
|
+
case "app.create-successor-instance": {
|
|
2186
|
+
const sourceProjectKey = requiredPlanFlag(flags, "from", entry.usage);
|
|
2187
|
+
const targetProjectKey = requiredPlanFlag(flags, "to", entry.usage);
|
|
2188
|
+
if (sourceProjectKey === targetProjectKey) {
|
|
2189
|
+
throw new UsageError("--from and --to must be different project keys.", "validation_failed", `Usage: ${entry.usage}`);
|
|
2190
|
+
}
|
|
2191
|
+
const targetProjectName = flags["name"];
|
|
2192
|
+
const copyPermissions = parseBooleanOption(flags["copy-permissions"], "--copy-permissions")
|
|
2193
|
+
?? false;
|
|
2194
|
+
return {
|
|
2195
|
+
method: "POST",
|
|
2196
|
+
endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
|
|
2197
|
+
identifiers: {
|
|
2198
|
+
appId: id,
|
|
2199
|
+
sourceProjectKey,
|
|
2200
|
+
targetProjectKey,
|
|
2201
|
+
...(targetProjectName !== undefined ? { targetProjectName, } : {}),
|
|
2202
|
+
copyPermissions,
|
|
2203
|
+
incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
|
|
2204
|
+
postFutureRequests: [
|
|
2205
|
+
{
|
|
2206
|
+
method: "GET",
|
|
2207
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
|
|
2208
|
+
intent: "After the terminal future names the successor key, observe creationTag and bind later target checks and cleanup to that hash.",
|
|
2209
|
+
},
|
|
2210
|
+
],
|
|
2211
|
+
...(copyPermissions
|
|
2212
|
+
? {
|
|
2213
|
+
permissionConcurrencyControl: "client-side-non-atomic-stale-identity-and-hash-checks",
|
|
2214
|
+
}
|
|
2215
|
+
: {}),
|
|
2216
|
+
preflightRequests: [
|
|
2217
|
+
{
|
|
2218
|
+
method: "GET",
|
|
2219
|
+
endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
|
|
2220
|
+
when: "before-create",
|
|
2221
|
+
intent: "Verify the --from project is a registered instance of the app.",
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
method: "GET",
|
|
2225
|
+
endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/app-manifest`,
|
|
2226
|
+
when: "before-create",
|
|
2227
|
+
intent: "Verify the --from project is an APP_INSTANCE project.",
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
method: "GET",
|
|
2231
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
|
|
2232
|
+
when: "before-create",
|
|
2233
|
+
intent: "Require the --to target project to be absent before creating the successor.",
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
method: "GET",
|
|
2237
|
+
endpoint: "/public/api/projects/",
|
|
2238
|
+
when: "conditional",
|
|
2239
|
+
intent: "Fallback list probe issued only when the direct project GET is forbidden (403) to confirm the target is absent; an unconfirmed target rejects creation.",
|
|
2240
|
+
},
|
|
2241
|
+
],
|
|
2242
|
+
...(copyPermissions
|
|
2243
|
+
? {
|
|
2244
|
+
permissionRequests: [
|
|
2245
|
+
{
|
|
2246
|
+
method: "GET",
|
|
2247
|
+
endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
|
|
2248
|
+
intent: "Snapshot the predecessor instance ACL before creation.",
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
method: "GET",
|
|
2252
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
|
|
2253
|
+
intent: "Read the successor ACL to decide whether the copy is a no-op.",
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
method: "GET",
|
|
2257
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
|
|
2258
|
+
intent: "Recheck successor creationTag after reading its ACL; stop if the project key was reused.",
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
method: "GET",
|
|
2262
|
+
endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
|
|
2263
|
+
intent: "Recheck the predecessor ACL immediately before the write and stop if its hash drifted.",
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
method: "GET",
|
|
2267
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
|
|
2268
|
+
intent: "Recheck successor creationTag immediately before the unconditional permission PUT.",
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
method: "PUT",
|
|
2272
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
|
|
2273
|
+
intent: "Apply the predecessor ACL snapshot to the successor instance.",
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
method: "GET",
|
|
2277
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
|
|
2278
|
+
intent: "Verify the successor ACL hash equals the predecessor snapshot hash.",
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
method: "GET",
|
|
2282
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
|
|
2283
|
+
intent: "Detect successor project-key reuse across the permission write and verification read.",
|
|
2284
|
+
},
|
|
2285
|
+
],
|
|
2286
|
+
}
|
|
2287
|
+
: {}),
|
|
2288
|
+
note: "Additive, non-transactional: strict preflight verifies the predecessor instance and the --to target absence before the single instance POST, then waits on the DSS future. The predecessor is never modified or deleted; cleanup targets only the successor key. DSS exposes no immutable future target ID, conditional DELETE, or conditional permission PUT: later creationTag and ACL checks narrow and detect races but cannot atomically join creation provenance or serialize writes.",
|
|
2289
|
+
},
|
|
2290
|
+
payload: {
|
|
2291
|
+
targetProjectKey,
|
|
2292
|
+
targetProjectName: targetProjectName ?? targetProjectKey,
|
|
2293
|
+
},
|
|
2294
|
+
wait: true,
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
case "app.delete-instance": {
|
|
2298
|
+
const targetProjectKey = requiredPlanFlag(flags, "project-key", entry.usage);
|
|
2299
|
+
const futureId = flags["future-id"] === undefined
|
|
2300
|
+
? undefined
|
|
2301
|
+
: requiredPlanFlag(flags, "future-id", entry.usage);
|
|
2302
|
+
const expectedProjectIncarnation = flags["expect-project-incarnation"] === undefined
|
|
2303
|
+
? undefined
|
|
2304
|
+
: requiredPlanFlag(flags, "expect-project-incarnation", entry.usage);
|
|
2305
|
+
if (expectedProjectIncarnation !== undefined
|
|
2306
|
+
&& !/^[0-9a-f]{64}$/.test(expectedProjectIncarnation)) {
|
|
2307
|
+
throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
|
|
2308
|
+
}
|
|
2309
|
+
const unconfirmedCreation = parseBooleanOption(flags["unconfirmed-creation"], "--unconfirmed-creation") ?? false;
|
|
2310
|
+
const manifestProbe = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`;
|
|
2311
|
+
const projectDetailsProbe = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`;
|
|
2312
|
+
const typeValidationRequests = [
|
|
2313
|
+
{
|
|
2314
|
+
method: "GET",
|
|
2315
|
+
endpoint: manifestProbe,
|
|
2316
|
+
when: "before-delete",
|
|
2317
|
+
intent: "Verify the target is an APP_INSTANCE project before deleting.",
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
method: "GET",
|
|
2321
|
+
endpoint: projectDetailsProbe,
|
|
2322
|
+
when: expectedProjectIncarnation === undefined
|
|
2323
|
+
? "conditional-type-check-before-delete"
|
|
2324
|
+
: "incarnation-and-conditional-type-check-before-delete",
|
|
2325
|
+
intent: expectedProjectIncarnation === undefined
|
|
2326
|
+
? "Fallback probe issued only when the app-manifest response omits projectAppType (live DSS does)."
|
|
2327
|
+
: "Recompute the current project-incarnation hash from creationTag after the manifest probe; the same response supplies projectAppType when the manifest omits it.",
|
|
2328
|
+
},
|
|
2329
|
+
];
|
|
2330
|
+
const preflightRequests = typeValidationRequests;
|
|
2331
|
+
if (unconfirmedCreation) {
|
|
2332
|
+
return {
|
|
2333
|
+
identifiers: {
|
|
2334
|
+
projectKey: targetProjectKey,
|
|
2335
|
+
...(futureId !== undefined ? { futureId, } : {}),
|
|
2336
|
+
unconfirmedCreation: true,
|
|
2337
|
+
note: "Indeterminate creation without a DSS future ID: no DSS request is issued. The command reports an unresolved cleanup failure (exit 4, cleanupResolved false) without deleting, because creation may still be running.",
|
|
2338
|
+
},
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
if (futureId === undefined) {
|
|
2342
|
+
return {
|
|
2343
|
+
method: "DELETE",
|
|
2344
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}`,
|
|
2345
|
+
identifiers: {
|
|
2346
|
+
projectKey: targetProjectKey,
|
|
2347
|
+
...(expectedProjectIncarnation === undefined
|
|
2348
|
+
? {}
|
|
2349
|
+
: {
|
|
2350
|
+
projectIncarnationGate: {
|
|
2351
|
+
required: false,
|
|
2352
|
+
provided: true,
|
|
2353
|
+
expectedHash: expectedProjectIncarnation,
|
|
2354
|
+
},
|
|
2355
|
+
incarnationControl: "client-side-non-atomic-stale-identity-check",
|
|
2356
|
+
}),
|
|
2357
|
+
preflightRequests,
|
|
2358
|
+
note: expectedProjectIncarnation === undefined
|
|
2359
|
+
? "Convergent direct delete: the manifest preflight rejects non-instance targets, an absent target (404) is an already-absent success issued without any DELETE, and only a verified instance target receives the project DELETE. No project-incarnation binding was requested."
|
|
2360
|
+
: "Incarnation-bound direct delete: after verifying APP_INSTANCE type, a project GET must match the expected creationTag hash before an unconditional DELETE. DSS exposes no immutable project ID or conditional DELETE, so this client-side check narrows but cannot serialize against project-key reuse after the GET.",
|
|
2361
|
+
},
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
return {
|
|
2365
|
+
method: "DELETE",
|
|
2366
|
+
endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}`,
|
|
2367
|
+
identifiers: {
|
|
2368
|
+
projectKey: targetProjectKey,
|
|
2369
|
+
futureId,
|
|
2370
|
+
projectIncarnationGate: {
|
|
2371
|
+
required: true,
|
|
2372
|
+
provided: expectedProjectIncarnation !== undefined,
|
|
2373
|
+
...(expectedProjectIncarnation === undefined
|
|
2374
|
+
? {}
|
|
2375
|
+
: { expectedHash: expectedProjectIncarnation, }),
|
|
2376
|
+
},
|
|
2377
|
+
incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
|
|
2378
|
+
preflightRequests: [
|
|
2379
|
+
{
|
|
2380
|
+
method: "GET",
|
|
2381
|
+
endpoint: manifestProbe,
|
|
2382
|
+
when: "before-future-wait",
|
|
2383
|
+
intent: "Verify the target is an APP_INSTANCE project before the supplied future is touched, so an invalid target cannot affect it.",
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
method: "GET",
|
|
2387
|
+
endpoint: projectDetailsProbe,
|
|
2388
|
+
when: "conditional-before-wait",
|
|
2389
|
+
intent: "Fallback probe issued only when the pre-wait app-manifest response omits projectAppType (live DSS does).",
|
|
2390
|
+
},
|
|
2391
|
+
],
|
|
2392
|
+
futureGate: [
|
|
2393
|
+
{
|
|
2394
|
+
method: "GET",
|
|
2395
|
+
endpoint: `/public/api/futures/${encodeURIComponent(futureId)}?peek=false`,
|
|
2396
|
+
intent: "Wait for the supplied creation future to settle or time out; never abort it, and repeat this read-only GET while it is live.",
|
|
2397
|
+
},
|
|
2398
|
+
],
|
|
2399
|
+
postFutureValidationRequests: expectedProjectIncarnation === undefined
|
|
2400
|
+
? []
|
|
2401
|
+
: typeValidationRequests,
|
|
2402
|
+
note: expectedProjectIncarnation === undefined
|
|
2403
|
+
? "The target type is verified before the future is touched. Waiting never aborts the future, but a terminal target match still cannot authorize deletion without --expect-project-incarnation; the command then exits with validation_failed and issues no DELETE."
|
|
2404
|
+
: "The target type is verified before the future is touched. The DELETE runs only after the terminal future reports the requested target, then a later project GET still matches the recorded creationTag hash and the target is re-verified as APP_INSTANCE. DSS exposes no immutable future target ID or conditional DELETE: the future target and creationTag are independent, non-atomic observations that narrow but cannot eliminate a project-key-reuse race.",
|
|
2405
|
+
},
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
case "app.permissions-restore": {
|
|
2409
|
+
const file = requiredPlanFlag(flags, "file", entry.usage);
|
|
2410
|
+
const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
|
|
2411
|
+
const permissionsEndpoint = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`;
|
|
2412
|
+
const projectDetailsEndpoint = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`;
|
|
2413
|
+
return {
|
|
2414
|
+
method: "PUT",
|
|
2415
|
+
endpoint: permissionsEndpoint,
|
|
2416
|
+
identifiers: {
|
|
2417
|
+
file,
|
|
2418
|
+
projectKey: targetProjectKey,
|
|
2419
|
+
localPreflight: [
|
|
2420
|
+
"Read and hash-verify the owner-only snapshot file.",
|
|
2421
|
+
"Require its project key and canonical DSS URL to match this invocation.",
|
|
2422
|
+
],
|
|
2423
|
+
preflightRequests: [
|
|
2424
|
+
{
|
|
2425
|
+
method: "GET",
|
|
2426
|
+
endpoint: projectDetailsEndpoint,
|
|
2427
|
+
intent: "Require the snapshot project-incarnation hash to match creationTag.",
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
method: "GET",
|
|
2431
|
+
endpoint: permissionsEndpoint,
|
|
2432
|
+
intent: "Read current permissions; an equal hash makes the PUT unnecessary.",
|
|
2433
|
+
},
|
|
2434
|
+
{
|
|
2435
|
+
method: "GET",
|
|
2436
|
+
endpoint: projectDetailsEndpoint,
|
|
2437
|
+
intent: "Recheck the project incarnation after the permission read and immediately before any PUT.",
|
|
2438
|
+
},
|
|
2439
|
+
],
|
|
2440
|
+
conditionalWrite: {
|
|
2441
|
+
method: "PUT",
|
|
2442
|
+
endpoint: permissionsEndpoint,
|
|
2443
|
+
when: "permissions-differ-and-dry-run-is-false",
|
|
2444
|
+
},
|
|
2445
|
+
verificationRequests: [
|
|
2446
|
+
{
|
|
2447
|
+
method: "GET",
|
|
2448
|
+
endpoint: permissionsEndpoint,
|
|
2449
|
+
intent: "Verify DSS persisted the desired permission hash.",
|
|
2450
|
+
},
|
|
2451
|
+
{
|
|
2452
|
+
method: "GET",
|
|
2453
|
+
endpoint: projectDetailsEndpoint,
|
|
2454
|
+
intent: "Detect project-key reuse across the permission write.",
|
|
2455
|
+
},
|
|
2456
|
+
],
|
|
2457
|
+
incarnationControl: "client-side-non-atomic-stale-identity-check",
|
|
2458
|
+
note: "DSS exposes no conditional permission PUT or immutable project ID. The repeated creationTag checks narrow and detect key-reuse races but cannot serialize the check with the PUT.",
|
|
2459
|
+
},
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
1708
2462
|
case "statistics.create-worksheet":
|
|
1709
2463
|
return {
|
|
1710
2464
|
method: "POST",
|
|
@@ -1940,16 +2694,6 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
|
|
|
1940
2694
|
},
|
|
1941
2695
|
};
|
|
1942
2696
|
}
|
|
1943
|
-
case "project.export": {
|
|
1944
|
-
const output = requiredPlanFlag(flags, "output", entry.usage);
|
|
1945
|
-
return {
|
|
1946
|
-
method: "POST",
|
|
1947
|
-
endpoint: `/public/api/projects/${encodeURIComponent(id)}/export`,
|
|
1948
|
-
identifiers: { projectKey: id, output, },
|
|
1949
|
-
payload: jsonInput(flags) ?? {},
|
|
1950
|
-
localWrites: [{ path: output, source: "remote project archive", after: "POST", },],
|
|
1951
|
-
};
|
|
1952
|
-
}
|
|
1953
2697
|
case "project.import":
|
|
1954
2698
|
return {
|
|
1955
2699
|
method: "POST",
|
|
@@ -2027,6 +2771,18 @@ export function buildMutationPlan(resource, action, meta, args, flags) {
|
|
|
2027
2771
|
plannedAndDryRun: flags["dry-run"] === true,
|
|
2028
2772
|
});
|
|
2029
2773
|
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Batch plans may surface exit 4: any dispatched step can be a long-running
|
|
2776
|
+
* lifecycle command whose remote work fails, or a synchronous assertion
|
|
2777
|
+
* mismatch (`recipe assert-unchanged`). Batch itself stays async "none".
|
|
2778
|
+
*/
|
|
2779
|
+
export const BATCH_PLAN_EXIT_CODES = {
|
|
2780
|
+
usage: 1,
|
|
2781
|
+
error: 2,
|
|
2782
|
+
transient: 3,
|
|
2783
|
+
longRunningFailure: 4,
|
|
2784
|
+
assertionFailure: 4,
|
|
2785
|
+
};
|
|
2030
2786
|
export const BATCH_USAGE = "dss batch (--data JSON|--data-file PATH|--stdin) [--continue-on-error] [--dry-run]";
|
|
2031
2787
|
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.";
|
|
2032
2788
|
export const BATCH_HINT = 'Pass a JSON array of argv arrays, e.g. [["dataset","list"],["recipe","update","r","--data-file","p.json"]].';
|