dataiku-sdk 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +14 -6
  2. package/bin/bun-version.js +14 -0
  3. package/bin/dss.js +23 -12
  4. package/dist/build-metadata.json +1 -1
  5. package/dist/packages/types/src/index.d.ts +74 -1
  6. package/dist/packages/types/src/index.js +53 -1
  7. package/dist/src/cli/coerce.d.ts +1 -0
  8. package/dist/src/cli/coerce.js +19 -15
  9. package/dist/src/cli/command-syntax.json +3521 -416
  10. package/dist/src/cli/commands/analysis.js +19 -1
  11. package/dist/src/cli/commands/bundle.js +22 -0
  12. package/dist/src/cli/commands/connection.js +26 -2
  13. package/dist/src/cli/commands/data-quality.js +33 -1
  14. package/dist/src/cli/commands/dataset.js +35 -4
  15. package/dist/src/cli/commands/discussion.js +15 -0
  16. package/dist/src/cli/commands/doctor.js +1 -1
  17. package/dist/src/cli/commands/flow.d.ts +5 -0
  18. package/dist/src/cli/commands/flow.js +57 -0
  19. package/dist/src/cli/commands/future.js +8 -0
  20. package/dist/src/cli/commands/index.js +6 -0
  21. package/dist/src/cli/commands/job.js +29 -6
  22. package/dist/src/cli/commands/metrics.js +21 -1
  23. package/dist/src/cli/commands/ml-task.d.ts +5 -0
  24. package/dist/src/cli/commands/ml-task.js +86 -16
  25. package/dist/src/cli/commands/mlflow.d.ts +11 -0
  26. package/dist/src/cli/commands/mlflow.js +121 -0
  27. package/dist/src/cli/commands/plugin.js +5 -11
  28. package/dist/src/cli/commands/project-deployer.js +70 -0
  29. package/dist/src/cli/commands/project-library.js +2 -5
  30. package/dist/src/cli/commands/project.js +8 -0
  31. package/dist/src/cli/commands/recipe.d.ts +9 -0
  32. package/dist/src/cli/commands/recipe.js +93 -7
  33. package/dist/src/cli/commands/saved-model.d.ts +1 -1
  34. package/dist/src/cli/commands/saved-model.js +9 -76
  35. package/dist/src/cli/commands/streaming-endpoint.js +30 -1
  36. package/dist/src/cli/commands/webapp.js +8 -0
  37. package/dist/src/cli/commands/wiki.js +22 -0
  38. package/dist/src/cli/commands/workspace.js +8 -0
  39. package/dist/src/cli/contract.d.ts +16 -15
  40. package/dist/src/cli/contract.js +139 -3404
  41. package/dist/src/cli/doctor.d.ts +13 -0
  42. package/dist/src/cli/doctor.js +41 -1
  43. package/dist/src/cli/flags.d.ts +7 -1
  44. package/dist/src/cli/flags.js +78 -6
  45. package/dist/src/cli/helpers/app-successor.js +1 -2
  46. package/dist/src/cli/helpers/documentation.d.ts +21 -0
  47. package/dist/src/cli/helpers/documentation.js +53 -0
  48. package/dist/src/cli/helpers/trained-model.d.ts +26 -0
  49. package/dist/src/cli/helpers/trained-model.js +144 -0
  50. package/dist/src/cli/list-output.d.ts +7 -0
  51. package/dist/src/cli/list-output.js +77 -0
  52. package/dist/src/cli/list-shapes.d.ts +21 -0
  53. package/dist/src/cli/list-shapes.js +111 -0
  54. package/dist/src/cli/output-schemas.d.ts +0 -14
  55. package/dist/src/cli/output-schemas.js +4 -7
  56. package/dist/src/cli/output.d.ts +0 -1
  57. package/dist/src/cli/output.js +27 -19
  58. package/dist/src/cli/plans.d.ts +14 -0
  59. package/dist/src/cli/plans.js +3684 -0
  60. package/dist/src/cli/usage.d.ts +7 -0
  61. package/dist/src/cli/usage.js +20 -1
  62. package/dist/src/cli/version.d.ts +2 -2
  63. package/dist/src/cli/version.js +2 -2
  64. package/dist/src/cli.js +127 -113
  65. package/dist/src/client.d.ts +52 -43
  66. package/dist/src/client.js +267 -96
  67. package/dist/src/errors.d.ts +11 -1
  68. package/dist/src/errors.js +31 -47
  69. package/dist/src/generated/action-output-schemas.json +345 -89
  70. package/dist/src/index.d.ts +5 -1
  71. package/dist/src/index.js +3 -0
  72. package/dist/src/resources/analyses.d.ts +8 -0
  73. package/dist/src/resources/analyses.js +13 -1
  74. package/dist/src/resources/bundles.d.ts +34 -0
  75. package/dist/src/resources/bundles.js +67 -0
  76. package/dist/src/resources/connections.d.ts +9 -0
  77. package/dist/src/resources/connections.js +15 -0
  78. package/dist/src/resources/data-quality.d.ts +20 -0
  79. package/dist/src/resources/data-quality.js +30 -0
  80. package/dist/src/resources/dataset-create.d.ts +19 -0
  81. package/dist/src/resources/dataset-create.js +66 -0
  82. package/dist/src/resources/datasets.d.ts +25 -15
  83. package/dist/src/resources/datasets.js +59 -111
  84. package/dist/src/resources/discussions.d.ts +5 -0
  85. package/dist/src/resources/discussions.js +7 -0
  86. package/dist/src/resources/documentation.d.ts +25 -0
  87. package/dist/src/resources/documentation.js +16 -0
  88. package/dist/src/resources/flow.d.ts +48 -0
  89. package/dist/src/resources/flow.js +65 -0
  90. package/dist/src/resources/futures.d.ts +23 -0
  91. package/dist/src/resources/futures.js +32 -0
  92. package/dist/src/resources/jobs.js +102 -2
  93. package/dist/src/resources/metrics.d.ts +10 -0
  94. package/dist/src/resources/metrics.js +10 -0
  95. package/dist/src/resources/ml-tasks.d.ts +65 -2
  96. package/dist/src/resources/ml-tasks.js +102 -28
  97. package/dist/src/resources/mlflow-extension.d.ts +73 -0
  98. package/dist/src/resources/mlflow-extension.js +105 -0
  99. package/dist/src/resources/projects.d.ts +6 -0
  100. package/dist/src/resources/projects.js +8 -0
  101. package/dist/src/resources/recipe-create.d.ts +65 -0
  102. package/dist/src/resources/recipe-create.js +338 -0
  103. package/dist/src/resources/recipes.d.ts +28 -29
  104. package/dist/src/resources/recipes.js +126 -245
  105. package/dist/src/resources/saved-models.d.ts +10 -6
  106. package/dist/src/resources/saved-models.js +16 -14
  107. package/dist/src/resources/scenarios.js +8 -14
  108. package/dist/src/resources/streaming-endpoints.d.ts +12 -0
  109. package/dist/src/resources/streaming-endpoints.js +15 -0
  110. package/dist/src/resources/trained-model.d.ts +60 -0
  111. package/dist/src/resources/trained-model.js +98 -0
  112. package/dist/src/resources/webapps.d.ts +8 -0
  113. package/dist/src/resources/webapps.js +10 -0
  114. package/dist/src/resources/wiki.d.ts +5 -0
  115. package/dist/src/resources/wiki.js +7 -0
  116. package/dist/src/resources/workspaces.d.ts +2 -0
  117. package/dist/src/resources/workspaces.js +4 -0
  118. package/dist/src/utils/response-file.js +20 -9
  119. package/dist/src/utils/secret-sanitize.d.ts +7 -0
  120. package/dist/src/utils/secret-sanitize.js +46 -0
  121. package/package.json +25 -26
  122. package/plugin.json +1 -1
  123. package/skills/dataiku-dss/SKILL.md +1 -1
  124. package/skills/dataiku-dss/references/authentication.md +3 -1
  125. package/skills/dataiku-dss/references/coding.md +1 -0
  126. package/skills/dataiku-dss/references/discovery.md +4 -6
  127. package/skills/dataiku-dss/references/troubleshooting.md +3 -2
@@ -0,0 +1,3684 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { APP_MANIFEST_CONCURRENCY_CONTROL, } from "../resources/applications.js";
4
+ import { assertDatasetTypeCreatable, buildDatasetCreateBody, } from "../resources/dataset-create.js";
5
+ import { schemaPropagationBody, } from "../resources/flow.js";
6
+ import { mlTaskCreateBody, mlTaskReguessQuery, } from "../resources/ml-tasks.js";
7
+ import { deployRunQuery, experimentsDatasetBody, inferenceInfoBody, } from "../resources/mlflow-extension.js";
8
+ import { validatePluginDestinationPath, validatePluginPath, } from "../resources/plugins.js";
9
+ import { encodeLibraryPath, PROJECT_LIBRARY_CONCURRENCY_CONTROL, validateLibraryDestinationPath, validateLibraryName, validateLibraryPath, } from "../resources/project-library.js";
10
+ import { buildRecipeCreateRequest, } from "../resources/recipe-create.js";
11
+ import { modelDiagnosticsBody, } from "../resources/trained-model.js";
12
+ import { encodeGitReferencePath, validateGitReferencePath, } from "../utils/git-reference.js";
13
+ import { jobBuildTargetTypeFromFlags, json, jsonInput, num, parseBooleanOption, parseJsonObject, requiredJsonInput, rewritePairsFromFlags, schemaColumnsInput, sha256Hex, stableHash, stringField, textInput, unknownJsonInput, } from "./coerce.js";
14
+ import { parseCodeRunIntegerFlag, resolveCodeInputWithSource, } from "./commands/code.js";
15
+ import { schemaPropagationOptionsFromFlags, } from "./commands/flow.js";
16
+ import { mlTaskFieldsFromFlags, mlTaskReguessOptionsFromFlags, } from "./commands/ml-task.js";
17
+ import { deployRunOptionsFromFlags, experimentsDatasetOptionsFromFlags, inferenceInfoFromFlags, } from "./commands/mlflow.js";
18
+ import { projectLibraryPutPayload, } from "./commands/project-library.js";
19
+ import { recipeCreateOptionsFromFlags, } from "./commands/recipe.js";
20
+ import { resolveSqlQueryInvocation, } from "./commands/sql.js";
21
+ import { buildRegistryEntry, splitPackageSpec, } from "./contract.js";
22
+ import { ambientProjectKey, } from "./env.js";
23
+ import { executionMode, } from "./flags.js";
24
+ import { flowZoneColor, flowZoneMoveItems, flowZoneName, } from "./helpers/flow-zone.js";
25
+ import { recipeBackupPath, recipeRunShouldWait, } from "./helpers/recipe.js";
26
+ import { diagnosticsComputationFromFlags, } from "./helpers/trained-model.js";
27
+ import { encodedProjectEndpointForPlan, planResult, } from "./output.js";
28
+ import { commandUsage, } from "./syntax.js";
29
+ import { requireArgs, UsageError, } from "./usage.js";
30
+ function codeEnvWait(flags) {
31
+ return flags["no-wait"] !== true;
32
+ }
33
+ function codeEnvLang(value, usage) {
34
+ if (value !== "PYTHON" && value !== "R") {
35
+ throw new UsageError(`Invalid code environment language ${JSON.stringify(value)}. Usage: ${usage}`);
36
+ }
37
+ return value;
38
+ }
39
+ function codeEnvParams(flags) {
40
+ const params = json(flags["params"]) ?? jsonInput(flags) ?? {};
41
+ if (typeof flags["python-interpreter"] === "string") {
42
+ params.pythonInterpreter = flags["python-interpreter"];
43
+ }
44
+ return params;
45
+ }
46
+ /**
47
+ * Resolve requested package specs from --file/--packages/--package. An
48
+ * explicit source that resolves to zero specs is a legitimate clear
49
+ * (set-packages replaces specPackageList wholesale); only a call with no
50
+ * package source flag at all is a usage error.
51
+ */
52
+ function codeEnvPackageList(flags) {
53
+ const file = flags["file"];
54
+ const packages = typeof file === "string"
55
+ ? splitPackageSpec(fs.readFileSync(file, "utf-8"))
56
+ : [];
57
+ if (typeof flags["packages"] === "string") {
58
+ packages.push(...splitPackageSpec(flags["packages"]));
59
+ }
60
+ if (typeof flags["package"] === "string") {
61
+ packages.push(...splitPackageSpec(flags["package"]));
62
+ }
63
+ if (typeof file !== "string"
64
+ && typeof flags["packages"] !== "string"
65
+ && typeof flags["package"] !== "string") {
66
+ throw new UsageError("--packages, --package, or --file is required. Use newline-separated package specs for version constraints; an explicitly empty value (or empty file) clears the requested package list.");
67
+ }
68
+ return packages;
69
+ }
70
+ function exitCodesOnFailure(entry) {
71
+ return {
72
+ usage: entry.exitCodes.usage,
73
+ error: entry.exitCodes.error,
74
+ transient: entry.exitCodes.transient,
75
+ ...(entry.exitCodes.longRunningFailure !== undefined
76
+ ? { longRunningFailure: entry.exitCodes.longRunningFailure, }
77
+ : {}),
78
+ ...(entry.exitCodes.assertionFailure !== undefined
79
+ ? { assertionFailure: entry.exitCodes.assertionFailure, }
80
+ : {}),
81
+ };
82
+ }
83
+ /**
84
+ * Plan-local project key resolution. `--plan` is a purely local preview, so it
85
+ * resolves the target project from the explicit `--project-key` flag and the
86
+ * documented `DATAIKU_PROJECT_KEY` environment variable only. The saved
87
+ * credentials file is never opened and no API key is ever resolved, so planning
88
+ * can neither depend on nor leak stored secrets.
89
+ */
90
+ function planProjectKeyFromArgs(flags) {
91
+ const fromFlag = flags["project-key"];
92
+ if (typeof fromFlag === "string" && fromFlag.trim().length > 0)
93
+ return fromFlag.trim();
94
+ const fromEnv = ambientProjectKey();
95
+ return fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv.trim() : undefined;
96
+ }
97
+ /**
98
+ * Commands whose plan must pin the target with an explicit `--project-key`
99
+ * instead of falling back to ambient `DATAIKU_PROJECT_KEY`, so a cleanup plan
100
+ * can never silently retarget a project the caller did not name.
101
+ */
102
+ const EXPLICIT_PLAN_PROJECT_KEY = { "app.delete-instance": true, };
103
+ /** Plan-local project key, or the canonical `--project-key` usage error. */
104
+ function requiredPlanProjectKey(flags, usage) {
105
+ return planProjectKeyFromArgs(flags) ?? requiredPlanFlag(flags, "project-key", usage);
106
+ }
107
+ function projectKeyForPlan(entry, flags) {
108
+ if (!entry.requiresProject)
109
+ return undefined;
110
+ if (EXPLICIT_PLAN_PROJECT_KEY[`${entry.resource}.${entry.action}`] === true) {
111
+ return requiredPlanFlag(flags, "project-key", entry.usage);
112
+ }
113
+ const projectKey = planProjectKeyFromArgs(flags);
114
+ if (projectKey)
115
+ return projectKey;
116
+ throw new UsageError(`Missing project key. Pass --project-key or set DATAIKU_PROJECT_KEY before planning ${entry.resource} ${entry.action}.`);
117
+ }
118
+ function requiredPlanFlag(flags, name, usage) {
119
+ const value = flags[name];
120
+ // Return trimmed like the runtime's requiredStringFlag so plans advertise
121
+ // exactly the identifier the command will act on.
122
+ if (typeof value === "string" && value.trim().length > 0)
123
+ return value.trim();
124
+ throw new UsageError(`--${name} is required. Usage: ${usage}`);
125
+ }
126
+ /**
127
+ * Optional caller-chosen identifier. Absent selects the command's generated
128
+ * mode; a present-but-empty value is an input error, never a silent omission.
129
+ */
130
+ function optionalPlanFlag(flags, name, usage) {
131
+ const value = flags[name];
132
+ if (value === undefined)
133
+ return undefined;
134
+ if (typeof value !== "string" || value.trim().length === 0) {
135
+ throw new UsageError(`--${name} must not be empty when supplied; omit --${name} entirely to generate the successor key during apply. Usage: ${usage}`, "validation_failed");
136
+ }
137
+ return value.trim();
138
+ }
139
+ function optionalJsonFlag(flags, name) {
140
+ const value = flags[name];
141
+ return typeof value === "string" ? parseJsonObject(value, `--${name}`) : undefined;
142
+ }
143
+ function requiredPlanJsonInput(flags, usage) {
144
+ return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
145
+ }
146
+ function dataQualityEndpoint(projectKey, datasetName, suffix) {
147
+ return encodedProjectEndpointForPlan(projectKey, `/datasets/${encodeURIComponent(datasetName)}/data-quality${suffix}`);
148
+ }
149
+ function querySuffix(params) {
150
+ const search = new URLSearchParams();
151
+ for (const [key, value,] of Object.entries(params)) {
152
+ if (value !== undefined)
153
+ search.set(key, String(value));
154
+ }
155
+ const raw = search.toString();
156
+ return raw ? `?${raw}` : "";
157
+ }
158
+ function projectFolderEndpoint(folderId) {
159
+ return `/public/api/project-folders/${encodeURIComponent(folderId)}`;
160
+ }
161
+ /**
162
+ * Git and future API mount. The official Python client mounts these routes on
163
+ * `/dip/publicapi`, which is also what the project-git resource uses; do not
164
+ * normalize them to the repo-wide `/public/api`.
165
+ */
166
+ const PROJECT_GIT_API_ROOT = "/dip/publicapi";
167
+ function projectGitEndpoint(projectKey, suffix) {
168
+ if (!projectKey)
169
+ throw new UsageError("--project-key is required for project-git mutations.");
170
+ return `${PROJECT_GIT_API_ROOT}/projects/${encodeURIComponent(projectKey)}/git${suffix}`;
171
+ }
172
+ function projectGitFutureEndpoint(jobId) {
173
+ return `${PROJECT_GIT_API_ROOT}/futures/${encodeURIComponent(jobId)}`;
174
+ }
175
+ /**
176
+ * `--plan` bypasses the SDK boundary guard, so HTTP(S) URLs with embedded
177
+ * userinfo are rejected here before any plan is printed. SSH/scp-style URLs
178
+ * (`git@host:org/repo.git`, `ssh://...`) remain valid.
179
+ */
180
+ function validatedPlanRepositoryUrl(url, flag, usage) {
181
+ const candidate = url.trim();
182
+ if (/[\u0000-\u001f\u007f]/u.test(candidate)) {
183
+ throw new UsageError(`--${flag} must not contain control characters. Usage: ${usage}`);
184
+ }
185
+ if (/^https?:/i.test(candidate)) {
186
+ if (candidate.includes("\\")) {
187
+ throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
188
+ }
189
+ if (!/^https?:\/\//i.test(candidate)) {
190
+ throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
191
+ }
192
+ let parsed;
193
+ try {
194
+ parsed = new URL(candidate);
195
+ }
196
+ catch {
197
+ throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
198
+ }
199
+ if (parsed.username !== "" || parsed.password !== "") {
200
+ throw new UsageError(`--${flag} must not contain embedded credentials (userinfo). Usage: ${usage}`);
201
+ }
202
+ }
203
+ return candidate;
204
+ }
205
+ function pluginRootEndpoint(pluginId, suffix) {
206
+ return `/public/api/plugins/${encodeURIComponent(pluginId)}${suffix}`;
207
+ }
208
+ function pluginActionEndpoint(pluginId, action) {
209
+ return pluginRootEndpoint(pluginId, `/actions/${action}`);
210
+ }
211
+ function pluginContentsEndpoint(pluginId, contentPath) {
212
+ return `${pluginRootEndpoint(pluginId, "/contents/")}${encodePluginSegmentsForPlan(contentPath)}`;
213
+ }
214
+ function pluginFoldersEndpoint(pluginId, contentPath) {
215
+ return `${pluginRootEndpoint(pluginId, "/folders/")}${encodePluginSegmentsForPlan(contentPath)}`;
216
+ }
217
+ /** Encodes a plugin content path per segment for plan-time endpoints. */
218
+ function encodePluginSegmentsForPlan(contentPath) {
219
+ return contentPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
220
+ }
221
+ /**
222
+ * Plan payload for plugin git install/update bodies. The repository URL is
223
+ * validated (embedded userinfo rejected) and secrets are never echoed: the
224
+ * plan carries the URL only, never embedded credentials.
225
+ */
226
+ function pluginGitPlanPayload(flags) {
227
+ const repository = requiredPlanFlag(flags, "repository", "dss plugin install-from-git --repository URL");
228
+ // Mirrors PluginsResource install/update bodies: absent options are sent as null.
229
+ return {
230
+ gitRepositoryUrl: validatedPlanRepositoryUrl(repository, "repository", "--repository URL"),
231
+ gitCheckout: typeof flags["checkout"] === "string" ? flags["checkout"] : null,
232
+ gitSubpath: typeof flags["path-in-repository"] === "string" ? flags["path-in-repository"] : null,
233
+ };
234
+ }
235
+ function optionalPlanProjectScope(flags) {
236
+ const projectKey = flags["project-key"];
237
+ if (typeof projectKey !== "string" || projectKey.trim() === "")
238
+ return "";
239
+ return `?projectKey=${encodeURIComponent(projectKey.trim())}`;
240
+ }
241
+ function settingsConfigKeys(flags) {
242
+ const content = flags["content"];
243
+ if (typeof content !== "string")
244
+ return [];
245
+ try {
246
+ const parsed = JSON.parse(content);
247
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
248
+ return [];
249
+ return Object.keys(parsed);
250
+ }
251
+ catch {
252
+ return [];
253
+ }
254
+ }
255
+ function contentSourceKind(flags) {
256
+ if (flags["stdin"] === true)
257
+ return "stdin";
258
+ if (typeof flags["file"] === "string")
259
+ return "file";
260
+ if (typeof flags["content"] === "string")
261
+ return "content";
262
+ return "unknown";
263
+ }
264
+ /**
265
+ * Plan-safe view of a user create/update body: password values are replaced by
266
+ * a fixed marker so secrets never appear in --plan output.
267
+ */
268
+ function redactedUserPayload(payload) {
269
+ if (payload["password"] === undefined)
270
+ return payload;
271
+ return { ...payload, password: "<omitted>", };
272
+ }
273
+ /**
274
+ * Plan-safe view of a connection create/update body: the connection-type
275
+ * specific `params` object may carry credentials (passwords, keys, tokens), so
276
+ * the plan carries only the marker, never the raw params.
277
+ */
278
+ function redactedConnectionPayload(payload) {
279
+ if (payload["params"] === undefined)
280
+ return payload;
281
+ return { ...payload, params: "<omitted; may include credentials>", };
282
+ }
283
+ /** Required CSV flag for plans (comma-separated logins list). */
284
+ function requiredPlanCsv(flags, name, usage) {
285
+ const raw = requiredPlanFlag(flags, name, usage);
286
+ return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
287
+ }
288
+ function requiredPlanRepositoryUrl(flags, flag, usage) {
289
+ const url = requiredPlanFlag(flags, flag, usage);
290
+ return validatedPlanRepositoryUrl(url, flag, usage);
291
+ }
292
+ /** Wait procedure advertised for the library calls that return a job id. */
293
+ function projectGitFutureWait() {
294
+ return {
295
+ when: "after-dispatch",
296
+ endpoint: `${PROJECT_GIT_API_ROOT}/futures/{jobId}?peek=false`,
297
+ description: "Poll the returned job id until the future reports a result; never abort it.",
298
+ };
299
+ }
300
+ function jobBuildPayload(target, projectKey, flags) {
301
+ const targetType = jobBuildTargetTypeFromFlags(flags);
302
+ const partition = flags["partition"];
303
+ const output = { projectKey, id: target, type: targetType, };
304
+ if (targetType === "DATASET") {
305
+ if (partition !== undefined)
306
+ output.partition = partition;
307
+ }
308
+ else {
309
+ output.targetManagedFolderProjectKey = projectKey;
310
+ output.targetManagedFolder = target;
311
+ output.targetPartition = partition ?? "NP";
312
+ }
313
+ const payload = {
314
+ outputs: [output,],
315
+ type: flags["build-mode"] ?? "NON_RECURSIVE_FORCED_BUILD",
316
+ };
317
+ if (flags["auto-update-schema"] === true && targetType === "DATASET") {
318
+ payload.autoUpdateSchemaBeforeEachRecipeRun = true;
319
+ }
320
+ return payload;
321
+ }
322
+ function uploadPayload(filePath) {
323
+ return {
324
+ contentType: "multipart/form-data",
325
+ fileField: "file",
326
+ filePath,
327
+ fileName: path.basename(filePath),
328
+ };
329
+ }
330
+ export function commandPlanShape(resource, action, args, flags, entry, projectKey) {
331
+ /** Route and ids of the trained model an `ml-task` (3 ids) or `saved-model` (2 ids) action targets. */
332
+ const trainedModelEndpoint = () => {
333
+ requireArgs(args, resource === "ml-task" ? 3 : 2, entry.usage);
334
+ const [first, second, third,] = args.map((arg) => encodeURIComponent(arg));
335
+ return resource === "ml-task"
336
+ ? {
337
+ path: projectEndpoint(`/models/lab/${first}/${second}/models/${third}`),
338
+ identifiers: { analysisId: args[0], mlTaskId: args[1], modelId: args[2], },
339
+ }
340
+ : {
341
+ path: projectEndpoint(`/savedmodels/${first}/versions/${second}`),
342
+ identifiers: { savedModelId: args[0], versionId: args[1], },
343
+ };
344
+ };
345
+ const requiredProjectKey = () => {
346
+ if (!projectKey)
347
+ throw new UsageError(`Missing project key for ${resource} ${action}.`);
348
+ return projectKey;
349
+ };
350
+ const projectEndpoint = (suffix) => encodedProjectEndpointForPlan(requiredProjectKey(), suffix);
351
+ const id = args[0];
352
+ const codeEnvEndpoint = (suffix = "") => `/public/api/admin/code-envs/${encodeURIComponent(codeEnvLang(args[0], entry.usage))}/${encodeURIComponent(args[1])}${suffix}`;
353
+ const statisticsWorksheetsEndpoint = (datasetName) => projectEndpoint(`/datasets/${encodeURIComponent(datasetName)}/statistics/worksheets/`);
354
+ const statisticsWorksheetEndpoint = (datasetName, worksheetId) => `${statisticsWorksheetsEndpoint(datasetName)}${encodeURIComponent(worksheetId)}`;
355
+ switch (`${resource}.${action}`) {
356
+ case "sql.query": {
357
+ const payload = resolveSqlQueryInvocation(args, flags, planProjectKeyFromArgs(flags));
358
+ return {
359
+ method: "POST",
360
+ endpoint: "/public/api/sql/queries/",
361
+ identifiers: {
362
+ connection: payload.connection,
363
+ dataset: payload.datasetFullName,
364
+ },
365
+ payload,
366
+ };
367
+ }
368
+ case "ml-task.train":
369
+ return {
370
+ method: "POST",
371
+ endpoint: projectEndpoint(`/models/lab/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}/train`),
372
+ identifiers: { analysisId: args[0], mlTaskId: args[1], },
373
+ payload: {
374
+ sessionName: flags["session-name"],
375
+ runQueue: false,
376
+ },
377
+ wait: flags["wait"] === true,
378
+ };
379
+ case "wiki.create": {
380
+ const name = requiredPlanFlag(flags, "name", entry.usage);
381
+ const content = textInput(flags);
382
+ const create = {
383
+ method: "POST",
384
+ endpoint: projectEndpoint("/wiki/"),
385
+ payload: { projectKey, name, parent: flags["parent"] ?? null, },
386
+ };
387
+ return {
388
+ ...create,
389
+ identifiers: { name, },
390
+ // Content is not accepted by the create endpoint; it lands in a follow-up update.
391
+ ...(content === undefined ? {} : {
392
+ requests: [
393
+ { sequence: 1, ...create, },
394
+ { sequence: 2, method: "GET", endpoint: projectEndpoint("/wiki/{createdArticleId}"), },
395
+ {
396
+ sequence: 3,
397
+ method: "PUT",
398
+ endpoint: projectEndpoint("/wiki/{createdArticleId}"),
399
+ payload: { payload: content, },
400
+ },
401
+ ],
402
+ }),
403
+ };
404
+ }
405
+ case "wiki.update":
406
+ return {
407
+ method: "PUT",
408
+ endpoint: projectEndpoint(`/wiki/${encodeURIComponent(id)}`),
409
+ identifiers: { article: id, },
410
+ payload: {
411
+ ...jsonInput(flags),
412
+ name: flags["name"],
413
+ content: textInput(flags),
414
+ },
415
+ };
416
+ case "wiki.delete":
417
+ return {
418
+ method: "DELETE",
419
+ endpoint: projectEndpoint(`/wiki/${encodeURIComponent(id)}`),
420
+ identifiers: { article: id, },
421
+ };
422
+ case "dashboard.create": {
423
+ const data = jsonInput(flags);
424
+ const flagName = flags["name"];
425
+ const dataName = data?.["name"];
426
+ const name = flagName ?? (typeof dataName === "string" ? dataName : undefined);
427
+ if (!name) {
428
+ throw new UsageError(`--name or dashboard settings containing a string name are required. Usage: ${commandUsage("dashboard", "create")}`);
429
+ }
430
+ const listed = parseBooleanOption(flags["listed"], "--listed");
431
+ const payload = { ...(data ?? { pages: [], }), name, };
432
+ if (listed !== undefined)
433
+ payload.listed = listed;
434
+ return {
435
+ method: "POST",
436
+ endpoint: projectEndpoint("/dashboards/"),
437
+ identifiers: { name, },
438
+ payload,
439
+ };
440
+ }
441
+ case "dashboard.update": {
442
+ const payload = { ...jsonInput(flags), };
443
+ if (typeof flags["name"] === "string")
444
+ payload.name = flags["name"];
445
+ const listed = parseBooleanOption(flags["listed"], "--listed");
446
+ if (listed !== undefined)
447
+ payload.listed = listed;
448
+ return {
449
+ method: "PUT",
450
+ endpoint: projectEndpoint(`/dashboards/${encodeURIComponent(id)}/`),
451
+ identifiers: { id, },
452
+ payload,
453
+ };
454
+ }
455
+ case "dashboard.delete":
456
+ return {
457
+ method: "DELETE",
458
+ endpoint: projectEndpoint(`/dashboards/${encodeURIComponent(id)}/`),
459
+ identifiers: { id, },
460
+ };
461
+ case "insight.create": {
462
+ const data = jsonInput(flags);
463
+ const name = flags["name"];
464
+ const type = flags["type"];
465
+ if (!data && (!name || !type)) {
466
+ throw new UsageError(`--data or both --name and --type are required. Usage: ${commandUsage("insight", "create")}`);
467
+ }
468
+ const prototype = { ...data, };
469
+ if (name !== undefined)
470
+ prototype.name = name;
471
+ if (type !== undefined)
472
+ prototype.type = type;
473
+ const listed = parseBooleanOption(flags["listed"], "--listed");
474
+ if (listed !== undefined)
475
+ prototype.listed = listed;
476
+ const params = optionalJsonFlag(flags, "params");
477
+ if (params !== undefined)
478
+ prototype.params = params;
479
+ return {
480
+ method: "POST",
481
+ endpoint: projectEndpoint("/insights/"),
482
+ identifiers: { name, type, },
483
+ payload: {
484
+ insightPrototype: prototype,
485
+ contentType: flags["content-type"],
486
+ payload: textInput(flags),
487
+ },
488
+ };
489
+ }
490
+ case "insight.update":
491
+ return {
492
+ method: "POST",
493
+ endpoint: projectEndpoint(`/insights/${encodeURIComponent(id)}/`),
494
+ identifiers: { id, },
495
+ payload: {
496
+ insight: {
497
+ ...jsonInput(flags),
498
+ name: flags["name"],
499
+ listed: parseBooleanOption(flags["listed"], "--listed"),
500
+ params: optionalJsonFlag(flags, "params"),
501
+ },
502
+ contentType: flags["content-type"],
503
+ payload: textInput(flags),
504
+ },
505
+ };
506
+ case "insight.delete":
507
+ return {
508
+ method: "DELETE",
509
+ endpoint: projectEndpoint(`/insights/${encodeURIComponent(id)}/`),
510
+ identifiers: { id, },
511
+ };
512
+ case "data-quality.create-rule":
513
+ return {
514
+ method: "POST",
515
+ endpoint: dataQualityEndpoint(projectKey, args[0], "/rules"),
516
+ identifiers: { dataset: args[0], },
517
+ payload: requiredPlanJsonInput(flags, entry.usage),
518
+ };
519
+ case "data-quality.update-rule":
520
+ return {
521
+ method: "PUT",
522
+ endpoint: dataQualityEndpoint(projectKey, args[0], `/rules/${encodeURIComponent(args[1])}`),
523
+ identifiers: { dataset: args[0], ruleId: args[1], },
524
+ payload: requiredPlanJsonInput(flags, entry.usage),
525
+ };
526
+ case "data-quality.delete-rule":
527
+ return {
528
+ method: "DELETE",
529
+ endpoint: dataQualityEndpoint(projectKey, args[0], `/rules/${encodeURIComponent(args[1])}${querySuffix({ ruleId: args[1], })}`),
530
+ identifiers: { dataset: args[0], ruleId: args[1], },
531
+ };
532
+ case "data-quality.compute":
533
+ return {
534
+ method: "POST",
535
+ endpoint: dataQualityEndpoint(projectKey, args[0], `/actions/compute-rules${querySuffix({
536
+ partition: flags["partition"] ?? "NP",
537
+ ruleId: flags["rule-id"],
538
+ })}`),
539
+ identifiers: { dataset: args[0], ruleId: flags["rule-id"], },
540
+ wait: flags["wait"] === true,
541
+ };
542
+ case "data-quality.delete-history": {
543
+ const partition = flags["partition"] ?? "NP";
544
+ return {
545
+ method: "DELETE",
546
+ endpoint: dataQualityEndpoint(projectKey, args[0], `/history/${encodeURIComponent(partition)}`),
547
+ identifiers: { dataset: args[0], partition, },
548
+ };
549
+ }
550
+ case "dataset.sync-hive-metastore":
551
+ return {
552
+ method: "POST",
553
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/actions/synchronizeHiveMetastore`),
554
+ identifiers: { name: id, },
555
+ };
556
+ case "dataset.update-from-hive":
557
+ return {
558
+ method: "POST",
559
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/actions/updateFromHive`),
560
+ identifiers: { name: id, },
561
+ };
562
+ case "metrics.dataset-run-checks": {
563
+ const partitions = flags["partitions"];
564
+ const data = unknownJsonInput(flags);
565
+ return {
566
+ method: "POST",
567
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/actions/runChecks/${partitions ? `?partitions=${encodeURIComponent(partitions)}` : ""}`),
568
+ identifiers: { dataset: id, },
569
+ ...(data !== undefined ? { payload: data, } : {}),
570
+ };
571
+ }
572
+ case "workspace.remove-object":
573
+ return {
574
+ method: "DELETE",
575
+ endpoint: `/public/api/workspaces/${encodeURIComponent(args[0])}/objects/${encodeURIComponent(args[1])}`,
576
+ identifiers: { workspaceKey: args[0], workspaceObjectId: args[1], },
577
+ };
578
+ case "discussion.update": {
579
+ const endpoint = projectEndpoint(`/discussions/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}/${encodeURIComponent(args[2])}`);
580
+ const body = jsonInput(flags);
581
+ return body
582
+ ? { method: "PUT", endpoint, identifiers: { discussionId: args[2], }, payload: body, }
583
+ : {
584
+ exact: false,
585
+ reason: "--topic edits the current discussion: DSS returns it first (GET), then the edited object is PUT.",
586
+ method: "PUT",
587
+ endpoint,
588
+ identifiers: { discussionId: args[2], },
589
+ requests: [
590
+ { method: "GET", endpoint, },
591
+ { method: "PUT", endpoint, payloadEdits: { topic: flags["topic"], }, },
592
+ ],
593
+ };
594
+ }
595
+ case "wiki.update-settings":
596
+ return {
597
+ method: "PUT",
598
+ endpoint: projectEndpoint("/wiki/"),
599
+ identifiers: {},
600
+ payload: requiredPlanJsonInput(flags, entry.usage),
601
+ };
602
+ case "streaming-endpoint.create-managed":
603
+ return {
604
+ method: "POST",
605
+ endpoint: projectEndpoint("/streamingendpoints/managed"),
606
+ identifiers: { id, },
607
+ payload: {
608
+ id,
609
+ creationSettings: {
610
+ connectionId: requiredPlanFlag(flags, "connection", entry.usage),
611
+ ...(typeof flags["format-option"] === "string"
612
+ ? { formatOptionId: flags["format-option"], }
613
+ : {}),
614
+ },
615
+ },
616
+ };
617
+ case "streaming-endpoint.set-schema":
618
+ return {
619
+ method: "PUT",
620
+ endpoint: projectEndpoint(`/streamingendpoints/${encodeURIComponent(id)}/schema`),
621
+ identifiers: { id, },
622
+ payload: requiredPlanJsonInput(flags, entry.usage),
623
+ };
624
+ case "webapp.trust":
625
+ return {
626
+ method: "POST",
627
+ endpoint: projectEndpoint(`/webapps/${encodeURIComponent(id)}/actions/trust${flags["for-everybody"] === true ? "?trustForEverybody=true" : ""}`),
628
+ identifiers: { id, },
629
+ };
630
+ case "project.push-to-git-remote":
631
+ return {
632
+ method: "POST",
633
+ endpoint: projectEndpoint(`/actions/push-to-git-remote?remote=${encodeURIComponent(id)}`),
634
+ identifiers: { remote: id, },
635
+ };
636
+ case "future.abort":
637
+ return {
638
+ method: "DELETE",
639
+ endpoint: `/public/api/futures/${encodeURIComponent(id)}`,
640
+ identifiers: { id, },
641
+ };
642
+ case "mlflow.set-inference-info":
643
+ return {
644
+ method: "POST",
645
+ endpoint: "/public/api/api/2.0/mlflow/extension/set-run-inference-info",
646
+ identifiers: { runId: id, trackingProjectKey: requiredProjectKey(), },
647
+ payload: inferenceInfoBody(inferenceInfoFromFlags(id, flags)),
648
+ };
649
+ case "mlflow.deploy-run": {
650
+ const options = deployRunOptionsFromFlags(id, args[1], flags);
651
+ const query = deployRunQuery({ ...options, versionId: options.versionId ?? "<timestamp>", }, requiredProjectKey());
652
+ return {
653
+ ...(options.versionId === undefined
654
+ ? {
655
+ exact: false,
656
+ reason: "Without --version-id the SDK names the version after the current time.",
657
+ }
658
+ : {}),
659
+ method: "POST",
660
+ endpoint: `/public/api/api/2.0/mlflow/extension/deploy-run?${query}`,
661
+ identifiers: { runId: id, savedModelId: args[1], versionId: options.versionId, },
662
+ };
663
+ }
664
+ case "mlflow.create-experiments-dataset":
665
+ return {
666
+ method: "POST",
667
+ endpoint: "/public/api/api/2.0/mlflow/extension/create-project-experiments-dataset",
668
+ identifiers: { dataset: id, trackingProjectKey: requiredProjectKey(), },
669
+ payload: experimentsDatasetBody(experimentsDatasetOptionsFromFlags(id, flags)),
670
+ };
671
+ case "mlflow.garbage-collect":
672
+ return {
673
+ method: "POST",
674
+ endpoint: "/public/api/api/2.0/mlflow/extension/garbage-collect",
675
+ identifiers: { trackingProjectKey: requiredProjectKey(), },
676
+ };
677
+ case "mlflow.clean-db":
678
+ return {
679
+ method: "DELETE",
680
+ endpoint: `/public/api/api/2.0/mlflow/extension/clean-db/${encodeURIComponent(requiredProjectKey())}`,
681
+ identifiers: { projectKey, },
682
+ };
683
+ case "flow.propagate-schema":
684
+ return {
685
+ method: "POST",
686
+ endpoint: projectEndpoint("/flow/tools/propagate-schema/"),
687
+ identifiers: { dataset: args[0], },
688
+ payload: schemaPropagationBody(projectKey, args[0], schemaPropagationOptionsFromFlags(flags)),
689
+ wait: flags["wait"] === true,
690
+ };
691
+ case "flow-zone.create": {
692
+ const name = flowZoneName(flags["name"]);
693
+ const payload = { name, color: flowZoneColor(flags["color"]) ?? "#2ab1ac", };
694
+ return {
695
+ method: "POST",
696
+ endpoint: projectEndpoint("/flow/zones"),
697
+ identifiers: { name, },
698
+ payload,
699
+ };
700
+ }
701
+ case "flow-zone.update":
702
+ return {
703
+ method: "PUT",
704
+ endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}`),
705
+ identifiers: { id, },
706
+ payload: {
707
+ name: typeof flags["name"] === "string" ? flowZoneName(flags["name"]) : undefined,
708
+ color: flowZoneColor(flags["color"]),
709
+ projectKey,
710
+ },
711
+ };
712
+ case "flow-zone.delete":
713
+ return {
714
+ method: "DELETE",
715
+ endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}`),
716
+ identifiers: { id, },
717
+ };
718
+ case "flow-zone.move":
719
+ return {
720
+ method: "POST",
721
+ endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/add-items`),
722
+ identifiers: { id, },
723
+ payload: flowZoneMoveItems(flags),
724
+ };
725
+ case "dataset.create": {
726
+ const name = requiredPlanFlag(flags, "name", entry.usage);
727
+ const connection = flags["connection"];
728
+ const dsType = requiredPlanFlag(flags, "type", entry.usage);
729
+ assertDatasetTypeCreatable(dsType);
730
+ if (!connection && dsType.toLowerCase() !== "uploadedfiles") {
731
+ throw new UsageError("--connection is required unless --type is UploadedFiles.");
732
+ }
733
+ const endpoint = projectEndpoint("/datasets/"); // throws without a project key
734
+ return {
735
+ method: "POST",
736
+ endpoint,
737
+ identifiers: { name, },
738
+ payload: buildDatasetCreateBody({
739
+ projectKey: projectKey,
740
+ datasetName: name,
741
+ connection,
742
+ dsType,
743
+ }),
744
+ };
745
+ }
746
+ case "dataset.clone": {
747
+ const source = args[0];
748
+ const target = args[1];
749
+ return {
750
+ exact: false,
751
+ reason: "Apply GETs the source dataset and copies its current connection, format, and schema into the new dataset body.",
752
+ method: "POST",
753
+ endpoint: projectEndpoint("/datasets/"),
754
+ identifiers: { source, target, },
755
+ payload: {
756
+ sourceDataset: source,
757
+ targetDataset: target,
758
+ path: flags["path"],
759
+ table: flags["table"],
760
+ metastoreTableName: flags["metastore-table"],
761
+ allowSamePath: flags["allow-same-path"] === true,
762
+ projectKey,
763
+ },
764
+ };
765
+ }
766
+ case "dataset.rename":
767
+ return {
768
+ method: "POST",
769
+ endpoint: projectEndpoint("/actions/renameDataset"),
770
+ identifiers: { oldName: args[0], newName: args[1], },
771
+ payload: { oldName: args[0], newName: args[1], },
772
+ };
773
+ case "dataset.upload-file": {
774
+ const fileName = requiredPlanFlag(flags, "file-name", entry.usage);
775
+ const datasetEndpoint = projectEndpoint(`/datasets/${encodeURIComponent(args[0])}`);
776
+ const filesEndpoint = `${datasetEndpoint}/uploaded/files`;
777
+ const upload = {
778
+ method: "POST",
779
+ endpoint: filesEndpoint,
780
+ payload: { ...uploadPayload(args[1]), fileName, },
781
+ };
782
+ return {
783
+ ...upload,
784
+ identifiers: {
785
+ datasetName: args[0],
786
+ localPath: args[1],
787
+ fileName,
788
+ },
789
+ requests: [
790
+ { sequence: 1, method: "GET", endpoint: datasetEndpoint, },
791
+ { sequence: 2, method: "GET", endpoint: filesEndpoint, },
792
+ { sequence: 3, ...upload, },
793
+ {
794
+ sequence: 4,
795
+ method: "GET",
796
+ endpoint: filesEndpoint,
797
+ assert: {
798
+ filename: fileName,
799
+ length: "local file byte length",
800
+ },
801
+ },
802
+ ],
803
+ };
804
+ }
805
+ case "dataset.delete":
806
+ return {
807
+ method: "DELETE",
808
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
809
+ identifiers: { name: id, },
810
+ };
811
+ case "dataset.refresh-schema": {
812
+ const columns = schemaColumnsInput(flags, entry.usage);
813
+ return {
814
+ method: "PUT",
815
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/schema`),
816
+ identifiers: { name: id, },
817
+ payload: { columns, },
818
+ };
819
+ }
820
+ case "dataset.update":
821
+ return {
822
+ method: "PUT",
823
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
824
+ identifiers: { name: id, },
825
+ };
826
+ case "dataset.metadata-set":
827
+ return {
828
+ method: "PUT",
829
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/metadata`),
830
+ identifiers: { name: id, },
831
+ payload: requiredPlanJsonInput(flags, entry.usage),
832
+ };
833
+ case "dataset.create-managed": {
834
+ const name = requiredPlanFlag(flags, "name", entry.usage);
835
+ const connection = requiredPlanFlag(flags, "connection", entry.usage);
836
+ const creationSettings = { connectionId: connection, };
837
+ const specificSettings = {};
838
+ if (typeof flags["type-option-id"] === "string") {
839
+ creationSettings.typeOptionId = flags["type-option-id"];
840
+ }
841
+ if (typeof flags["format-option-id"] === "string") {
842
+ specificSettings.formatOptionId = flags["format-option-id"];
843
+ }
844
+ if (typeof flags["copy-partitioning-from"] === "string") {
845
+ specificSettings.partitioningOptionId = `copy:${flags["partitioning-folder"] === true ? "folder" : "dataset"}:${flags["copy-partitioning-from"]}`;
846
+ }
847
+ if (Object.keys(specificSettings).length > 0) {
848
+ creationSettings.specificSettings = specificSettings;
849
+ }
850
+ return {
851
+ method: "POST",
852
+ endpoint: projectEndpoint("/datasets/managed"),
853
+ identifiers: { name, },
854
+ payload: { name, creationSettings, },
855
+ };
856
+ }
857
+ case "recipe.add-input":
858
+ case "recipe.remove-input": {
859
+ const role = flags["role"] ?? "main";
860
+ return {
861
+ method: "PUT",
862
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(args[0])}`),
863
+ identifiers: { recipe: args[0], dataset: args[1], role, },
864
+ payload: {
865
+ operation: action === "add-input" ? "append" : "remove",
866
+ dataset: args[1],
867
+ role,
868
+ projectKey,
869
+ },
870
+ };
871
+ }
872
+ case "recipe.clone": {
873
+ const positionalSource = args[0];
874
+ const fromFlag = typeof flags["from"] === "string" ? flags["from"].trim() : "";
875
+ const source = positionalSource ?? fromFlag;
876
+ if (!source) {
877
+ throw new UsageError(`Source recipe is required. Usage: ${entry.usage}`, "missing_required_flag");
878
+ }
879
+ if (positionalSource && fromFlag && positionalSource !== fromFlag) {
880
+ throw new UsageError("Positional source and --from must match when both are provided.", "invalid_enum");
881
+ }
882
+ const toFlag = typeof flags["to"] === "string" ? flags["to"].trim() : "";
883
+ const nameFlag = typeof flags["name"] === "string" ? flags["name"].trim() : "";
884
+ const target = toFlag || nameFlag;
885
+ if (!target) {
886
+ throw new UsageError(`--name or --to is required. Usage: ${entry.usage}`, "missing_required_flag");
887
+ }
888
+ return {
889
+ method: "POST",
890
+ endpoint: projectEndpoint("/recipes/"),
891
+ identifiers: { source, target, },
892
+ payload: {
893
+ sourceRecipe: source,
894
+ targetRecipe: target,
895
+ inputRewrites: rewritePairsFromFlags(flags, "replace-input"),
896
+ outputRewrites: rewritePairsFromFlags(flags, "replace-output"),
897
+ payloadTextRewrites: rewritePairsFromFlags(flags, "replace-payload-text"),
898
+ outputDataset: flags["output"],
899
+ copyOutputSettings: flags["copy-output-settings"] === true,
900
+ outputPath: flags["path"],
901
+ metastoreTableName: flags["metastore-table"],
902
+ projectKey,
903
+ },
904
+ };
905
+ }
906
+ case "recipe.delete":
907
+ return {
908
+ method: "DELETE",
909
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
910
+ identifiers: { name: id, },
911
+ };
912
+ case "recipe.create": {
913
+ requiredPlanFlag(flags, "type", entry.usage);
914
+ const outputDataset = flags["output"];
915
+ const outputFolder = flags["output-folder"];
916
+ if (outputDataset && outputFolder) {
917
+ throw new UsageError("--output and --output-folder are mutually exclusive.");
918
+ }
919
+ if (!outputDataset && !outputFolder) {
920
+ throw new UsageError("--output or --output-folder is required.");
921
+ }
922
+ if (outputFolder && !flags["output-connection"]) {
923
+ throw new UsageError("--output-connection is required when using --output-folder.");
924
+ }
925
+ const endpoint = projectEndpoint("/recipes/"); // throws without a project key
926
+ // Same flag mapping and body construction as the handler and RecipesResource.create.
927
+ const { recipePrototype, creationSettings, grouping, prepareSteps, } = buildRecipeCreateRequest(recipeCreateOptionsFromFlags(flags), projectKey);
928
+ const edits = grouping
929
+ ? { grouping, }
930
+ : prepareSteps.length > 0
931
+ ? { steps: prepareSteps, }
932
+ : undefined;
933
+ const recipePath = projectEndpoint(`/recipes/${encodeURIComponent(recipePrototype.name)}`);
934
+ return {
935
+ method: "POST",
936
+ endpoint,
937
+ identifiers: { name: recipePrototype.name, },
938
+ payload: { recipePrototype, creationSettings, },
939
+ // Grouping keys/aggregates and prepare steps are written into the payload DSS creates.
940
+ ...(edits
941
+ ? {
942
+ requests: [
943
+ { method: "POST", endpoint, payload: { recipePrototype, creationSettings, }, },
944
+ { method: "GET", endpoint: recipePath, },
945
+ { method: "PUT", endpoint: recipePath, payloadEdits: edits, },
946
+ ],
947
+ }
948
+ : {}),
949
+ };
950
+ }
951
+ case "recipe.run":
952
+ return {
953
+ exact: false,
954
+ reason: "The job endpoint is exact, but DSS reads the recipe and its outputs before constructing the job payload; use recipe run --dry-run for a resolved payload.",
955
+ method: "POST",
956
+ endpoint: projectEndpoint("/jobs/"),
957
+ identifiers: { recipe: id, },
958
+ wait: recipeRunShouldWait(flags),
959
+ };
960
+ case "code.run": {
961
+ const { script, source, } = resolveCodeInputWithSource(args, flags);
962
+ const sourceSha256 = sha256Hex(script);
963
+ const scenarioBase = projectEndpoint("/scenarios/{generatedScenarioId}");
964
+ const envName = flags["env"];
965
+ const keepScenario = flags["keep"] === true;
966
+ const timeoutMs = parseCodeRunIntegerFlag(flags["timeout"], "--timeout") ?? 120_000;
967
+ const maxLogBytes = flags["full-log"] === true
968
+ ? 0
969
+ : parseCodeRunIntegerFlag(flags["max-log-bytes"], "--max-log-bytes") ?? 1_048_576;
970
+ return {
971
+ method: "POST",
972
+ endpoint: projectEndpoint("/scenarios/"),
973
+ identifiers: {
974
+ source,
975
+ sourceSha256,
976
+ sourceBytes: Buffer.byteLength(script),
977
+ },
978
+ requests: [
979
+ {
980
+ method: "POST",
981
+ endpoint: projectEndpoint("/scenarios/"),
982
+ payload: {
983
+ id: "{generatedScenarioId}",
984
+ name: "dss code run ({generatedScenarioId})",
985
+ projectKey,
986
+ type: "custom_python",
987
+ params: {
988
+ envSelection: envName
989
+ ? { envMode: "EXPLICIT_ENV", envName, }
990
+ : { envMode: "INHERIT", },
991
+ },
992
+ },
993
+ },
994
+ {
995
+ method: "PUT",
996
+ endpoint: `${scenarioBase}/payload`,
997
+ payload: { extension: "py", script: "<omitted>", },
998
+ redactedFields: ["payload.script",],
999
+ },
1000
+ { method: "POST", endpoint: `${scenarioBase}/run/`, payload: {}, },
1001
+ {
1002
+ method: "GET",
1003
+ endpoint: `${scenarioBase}/get-run-for-trigger?triggerId={triggerId}&triggerRunId={triggerRunId}`,
1004
+ repeat: "until scenarioRun.result.outcome or timeout",
1005
+ },
1006
+ {
1007
+ method: "GET",
1008
+ endpoint: `${scenarioBase}/{runId}/log`,
1009
+ boundedBytes: maxLogBytes,
1010
+ },
1011
+ ...keepScenario ? [] : [{ method: "DELETE", endpoint: scenarioBase, cleanup: true, },],
1012
+ ],
1013
+ wait: {
1014
+ timeoutMs,
1015
+ pollEndpoint: `${scenarioBase}/get-run-for-trigger`,
1016
+ },
1017
+ };
1018
+ }
1019
+ case "recipe.update":
1020
+ return {
1021
+ method: "PUT",
1022
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
1023
+ identifiers: { name: id, },
1024
+ payload: requiredPlanJsonInput(flags, entry.usage),
1025
+ };
1026
+ case "recipe.update-schema": {
1027
+ const recipePath = `/recipes/${encodeURIComponent(id)}`;
1028
+ return {
1029
+ exact: false,
1030
+ reason: "DSS computes the output schemas from the live recipe; one POST is sent per output whose computed schema differs. Use recipe update-schema --dry-run to list them.",
1031
+ method: "POST",
1032
+ endpoint: projectEndpoint(`${recipePath}/actions/updateOutputSchema`),
1033
+ identifiers: { name: id, },
1034
+ requests: [
1035
+ { method: "GET", endpoint: projectEndpoint(recipePath), },
1036
+ { method: "GET", endpoint: projectEndpoint(`${recipePath}/schema-update`), },
1037
+ {
1038
+ method: "POST",
1039
+ endpoint: projectEndpoint(`${recipePath}/actions/updateOutputSchema`),
1040
+ payload: { computableType: "{type}", computableId: "{id}", newSchema: "{newSchema}", },
1041
+ repeat: "per output whose computed schema differs",
1042
+ },
1043
+ ],
1044
+ };
1045
+ }
1046
+ case "recipe.set-payload": {
1047
+ const file = requiredPlanFlag(flags, "file", entry.usage);
1048
+ const backupDir = flags["no-backup"] === true
1049
+ ? undefined
1050
+ : flags["backup-dir"]
1051
+ ?? path.join(process.cwd(), ".dss-backups", "recipes");
1052
+ const backupPath = backupDir ? recipeBackupPath(id, backupDir) : undefined;
1053
+ return {
1054
+ method: "PUT",
1055
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
1056
+ identifiers: { name: id, },
1057
+ payload: {
1058
+ file,
1059
+ content: textInput(flags),
1060
+ ...(backupPath ? { backupPath, } : {}),
1061
+ },
1062
+ ...(backupPath
1063
+ ? { localWrites: [{ path: backupPath, source: "remote recipe backup", before: "PUT", },], }
1064
+ : {}),
1065
+ };
1066
+ }
1067
+ case "job.build":
1068
+ case "job.build-and-wait":
1069
+ return {
1070
+ method: "POST",
1071
+ endpoint: projectEndpoint("/jobs/"),
1072
+ identifiers: { target: id, },
1073
+ payload: jobBuildPayload(id, projectKey, flags),
1074
+ wait: action === "build-and-wait" || flags["wait"] === true,
1075
+ };
1076
+ case "scenario.abort":
1077
+ return {
1078
+ method: "POST",
1079
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/abort`),
1080
+ identifiers: { id, },
1081
+ };
1082
+ case "scenario.payload-set":
1083
+ return {
1084
+ method: "PUT",
1085
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/payload`),
1086
+ identifiers: { id, },
1087
+ payload: requiredPlanJsonInput(flags, entry.usage),
1088
+ };
1089
+ case "scenario.active-set": {
1090
+ const activeRaw = args[1];
1091
+ if (activeRaw !== "true" && activeRaw !== "false") {
1092
+ throw new UsageError(`Usage: ${entry.usage}`);
1093
+ }
1094
+ const lightEndpoint = projectEndpoint(`/scenarios/${encodeURIComponent(id)}/light`);
1095
+ const active = activeRaw === "true";
1096
+ // DSS parses the light PUT as a full Scenario, so the command echoes the
1097
+ // current light status with `active` overridden (see ScenariosResource.setActive).
1098
+ return {
1099
+ method: "PUT",
1100
+ endpoint: lightEndpoint,
1101
+ identifiers: { id, },
1102
+ payload: { active, },
1103
+ requests: [
1104
+ { sequence: 1, method: "GET", endpoint: lightEndpoint, },
1105
+ {
1106
+ sequence: 2,
1107
+ method: "PUT",
1108
+ endpoint: lightEndpoint,
1109
+ payload: { "...current": true, active, },
1110
+ },
1111
+ { sequence: 3, method: "GET", endpoint: lightEndpoint, },
1112
+ ],
1113
+ };
1114
+ }
1115
+ case "folder.create": {
1116
+ const name = requiredPlanFlag(flags, "name", entry.usage);
1117
+ const type = flags["type"];
1118
+ const connection = flags["connection"];
1119
+ const pathFlag = flags["path"];
1120
+ const body = {
1121
+ name,
1122
+ projectKey,
1123
+ type: type ?? null,
1124
+ params: { connection, path: pathFlag?.trim() || "/${projectKey}/${odbId}", },
1125
+ };
1126
+ // Mirrors FoldersResource.create.
1127
+ if (connection === undefined) {
1128
+ return {
1129
+ exact: false,
1130
+ reason: "Without --connection, apply reads DSS admin settings to pick the managed-folder connection (falling back to filesystem_folders); params.connection is set then.",
1131
+ method: "POST",
1132
+ endpoint: projectEndpoint("/managedfolders/"),
1133
+ identifiers: { name, },
1134
+ payload: body,
1135
+ };
1136
+ }
1137
+ return {
1138
+ method: "POST",
1139
+ endpoint: projectEndpoint("/managedfolders/"),
1140
+ identifiers: { name, },
1141
+ payload: body,
1142
+ };
1143
+ }
1144
+ case "job.abort":
1145
+ return {
1146
+ method: "POST",
1147
+ endpoint: projectEndpoint(`/jobs/${encodeURIComponent(id)}/abort/`),
1148
+ identifiers: { id, },
1149
+ };
1150
+ case "scenario.run":
1151
+ case "scenario.run-and-wait":
1152
+ return {
1153
+ method: "POST",
1154
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/run/`),
1155
+ identifiers: { id, },
1156
+ payload: {},
1157
+ wait: action === "run-and-wait" || flags["wait"] === true,
1158
+ };
1159
+ case "scenario.delete":
1160
+ return {
1161
+ method: "DELETE",
1162
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/`),
1163
+ identifiers: { id, },
1164
+ };
1165
+ case "scenario.create": {
1166
+ const type = flags["type"] ?? "step_based";
1167
+ return {
1168
+ method: "POST",
1169
+ endpoint: projectEndpoint("/scenarios/"),
1170
+ identifiers: { id: args[0], name: args[1], },
1171
+ // Mirrors ScenariosResource.create's default params for step-based scenarios.
1172
+ payload: {
1173
+ id: args[0],
1174
+ name: args[1],
1175
+ projectKey,
1176
+ type,
1177
+ params: type === "step_based" ? { steps: [], triggers: [], reporters: [], } : {},
1178
+ },
1179
+ };
1180
+ }
1181
+ case "scenario.update":
1182
+ return {
1183
+ method: "PUT",
1184
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/`),
1185
+ identifiers: { id, },
1186
+ payload: requiredPlanJsonInput(flags, entry.usage),
1187
+ };
1188
+ case "folder.update":
1189
+ return {
1190
+ method: "PUT",
1191
+ endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(id)}`),
1192
+ identifiers: { folder: id, },
1193
+ payload: requiredPlanJsonInput(flags, entry.usage),
1194
+ };
1195
+ case "folder.delete":
1196
+ return {
1197
+ method: "DELETE",
1198
+ endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(id)}`),
1199
+ identifiers: { folder: id, },
1200
+ };
1201
+ case "folder.upload":
1202
+ return {
1203
+ method: "POST",
1204
+ endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(args[0])}/contents/${encodeURIComponent(args[1])}`),
1205
+ identifiers: { folder: args[0], path: args[1], localPath: args[2], },
1206
+ };
1207
+ case "folder.delete-file":
1208
+ return {
1209
+ method: "DELETE",
1210
+ endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(args[0])}/contents/${encodeURIComponent(args[1])}`),
1211
+ identifiers: { folder: args[0], path: args[1], },
1212
+ };
1213
+ case "variable.set":
1214
+ return {
1215
+ exact: false,
1216
+ reason: "The payload lists the requested changes; apply PUTs the full {standard, local} object (merged with the current variables unless --replace).",
1217
+ method: "PUT",
1218
+ endpoint: projectEndpoint("/variables/"),
1219
+ payload: {
1220
+ standard: optionalJsonFlag(flags, "standard"),
1221
+ local: optionalJsonFlag(flags, "local"),
1222
+ replace: flags["replace"] === true,
1223
+ },
1224
+ };
1225
+ case "project-deployer.create-project": {
1226
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1227
+ return {
1228
+ method: "POST",
1229
+ endpoint: "/public/api/project-deployer/projects",
1230
+ identifiers: { projectKey: payload.projectKey, id: payload.id, },
1231
+ payload,
1232
+ };
1233
+ }
1234
+ case "project-deployer.upload-bundle":
1235
+ return {
1236
+ method: "POST",
1237
+ endpoint: "/public/api/project-deployer/projects/bundles",
1238
+ identifiers: { filePath: id, },
1239
+ payload: uploadPayload(id),
1240
+ };
1241
+ case "project-deployer.create-deployment": {
1242
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1243
+ return {
1244
+ method: "POST",
1245
+ endpoint: "/public/api/project-deployer/deployments",
1246
+ identifiers: { deploymentId: payload.deploymentId ?? payload.id, },
1247
+ payload,
1248
+ };
1249
+ }
1250
+ case "project-deployer.save-deployment-settings":
1251
+ return {
1252
+ method: "PUT",
1253
+ endpoint: `/public/api/project-deployer/deployments/${encodeURIComponent(id)}/settings`,
1254
+ identifiers: { deploymentId: id, },
1255
+ payload: requiredPlanJsonInput(flags, entry.usage),
1256
+ };
1257
+ case "project-deployer.deploy":
1258
+ return {
1259
+ method: "POST",
1260
+ endpoint: `/public/api/project-deployer/deployments/${encodeURIComponent(id)}/actions/update`,
1261
+ identifiers: { deploymentId: id, },
1262
+ payload: {},
1263
+ };
1264
+ case "project-deployer.delete-deployment":
1265
+ return {
1266
+ method: "DELETE",
1267
+ endpoint: `/public/api/project-deployer/deployments/${encodeURIComponent(id)}`,
1268
+ identifiers: { deploymentId: id, },
1269
+ };
1270
+ case "project-deployer.create-infra": {
1271
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1272
+ return {
1273
+ method: "POST",
1274
+ endpoint: "/public/api/project-deployer/infras",
1275
+ identifiers: { infraId: payload.id, },
1276
+ payload,
1277
+ };
1278
+ }
1279
+ case "project-deployer.save-infra-settings":
1280
+ return {
1281
+ method: "PUT",
1282
+ endpoint: `/public/api/project-deployer/infras/${encodeURIComponent(id)}/settings`,
1283
+ identifiers: { infraId: id, },
1284
+ payload: requiredPlanJsonInput(flags, entry.usage),
1285
+ };
1286
+ case "project-deployer.delete-infra":
1287
+ return {
1288
+ method: "DELETE",
1289
+ endpoint: `/public/api/project-deployer/infras/${encodeURIComponent(id)}`,
1290
+ identifiers: { infraId: id, },
1291
+ };
1292
+ case "project-deployer.delete-project":
1293
+ return {
1294
+ method: "DELETE",
1295
+ endpoint: `/public/api/project-deployer/projects/${encodeURIComponent(id)}`,
1296
+ identifiers: { publishedProjectKey: id, },
1297
+ };
1298
+ case "project-deployer.save-project-settings":
1299
+ return {
1300
+ method: "PUT",
1301
+ endpoint: `/public/api/project-deployer/projects/${encodeURIComponent(id)}/settings`,
1302
+ identifiers: { publishedProjectKey: id, },
1303
+ payload: requiredPlanJsonInput(flags, entry.usage),
1304
+ };
1305
+ case "project-deployer.delete-bundle":
1306
+ return {
1307
+ method: "DELETE",
1308
+ endpoint: `/public/api/project-deployer/projects/${encodeURIComponent(id)}/bundles/${encodeURIComponent(args[1])}`,
1309
+ identifiers: { publishedProjectKey: id, bundleId: args[1], },
1310
+ };
1311
+ case "bundle.create-project": {
1312
+ const archivePath = flags["archive-path"];
1313
+ const query = querySuffix({
1314
+ archivePath: typeof archivePath === "string" ? archivePath : undefined,
1315
+ projectFolderId: flags["project-folder"],
1316
+ });
1317
+ const file = flags["file"];
1318
+ return typeof file === "string"
1319
+ ? {
1320
+ method: "POST",
1321
+ endpoint: `/public/api/projectsFromBundle/${query}`,
1322
+ identifiers: { filePath: file, },
1323
+ payload: uploadPayload(file),
1324
+ }
1325
+ : {
1326
+ method: "POST",
1327
+ endpoint: `/public/api/projectsFromBundle/fromArchive${query}`,
1328
+ identifiers: { archivePath, },
1329
+ };
1330
+ }
1331
+ case "api-deployer.create-infra": {
1332
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1333
+ return {
1334
+ method: "POST",
1335
+ endpoint: "/public/api/api-deployer/infras",
1336
+ identifiers: { infraId: payload.id, },
1337
+ payload,
1338
+ };
1339
+ }
1340
+ case "api-deployer.delete-infra":
1341
+ return {
1342
+ method: "DELETE",
1343
+ endpoint: `/public/api/api-deployer/infras/${encodeURIComponent(id)}`,
1344
+ identifiers: { infraId: id, },
1345
+ };
1346
+ case "api-deployer.create-service": {
1347
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1348
+ return {
1349
+ method: "POST",
1350
+ endpoint: "/public/api/api-deployer/services",
1351
+ identifiers: { serviceId: payload.id ?? payload.publishedServiceId, },
1352
+ payload,
1353
+ };
1354
+ }
1355
+ case "api-deployer.delete-service":
1356
+ return {
1357
+ method: "DELETE",
1358
+ endpoint: `/public/api/api-deployer/services/${encodeURIComponent(id)}`,
1359
+ identifiers: { serviceId: id, },
1360
+ };
1361
+ case "api-deployer.publish-version":
1362
+ return {
1363
+ method: "POST",
1364
+ endpoint: `/public/api/api-deployer/services/${encodeURIComponent(args[0])}/versions`,
1365
+ identifiers: { serviceId: args[0], filePath: args[1], },
1366
+ payload: uploadPayload(args[1]),
1367
+ };
1368
+ case "api-deployer.delete-version":
1369
+ return {
1370
+ method: "DELETE",
1371
+ endpoint: `/public/api/api-deployer/services/${encodeURIComponent(args[0])}/versions/${encodeURIComponent(args[1])}`,
1372
+ identifiers: { serviceId: args[0], version: args[1], },
1373
+ };
1374
+ case "api-deployer.create-deployment": {
1375
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1376
+ return {
1377
+ method: "POST",
1378
+ endpoint: "/public/api/api-deployer/deployments",
1379
+ identifiers: { deploymentId: payload.deploymentId ?? payload.id, },
1380
+ payload,
1381
+ };
1382
+ }
1383
+ case "api-deployer.save-deployment-settings":
1384
+ return {
1385
+ method: "PUT",
1386
+ endpoint: `/public/api/api-deployer/deployments/${encodeURIComponent(id)}/settings`,
1387
+ identifiers: { deploymentId: id, },
1388
+ payload: requiredPlanJsonInput(flags, entry.usage),
1389
+ };
1390
+ case "api-deployer.deploy":
1391
+ return {
1392
+ method: "POST",
1393
+ endpoint: `/public/api/api-deployer/deployments/${encodeURIComponent(id)}/actions/update`,
1394
+ identifiers: { deploymentId: id, },
1395
+ payload: {},
1396
+ };
1397
+ case "api-deployer.delete-deployment":
1398
+ return {
1399
+ method: "DELETE",
1400
+ endpoint: `/public/api/api-deployer/deployments/${encodeURIComponent(id)}`,
1401
+ identifiers: { deploymentId: id, },
1402
+ };
1403
+ case "workspace.create": {
1404
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1405
+ return {
1406
+ method: "POST",
1407
+ endpoint: "/public/api/workspaces/",
1408
+ identifiers: { workspaceKey: payload.workspaceKey, },
1409
+ payload,
1410
+ };
1411
+ }
1412
+ case "workspace.update-settings":
1413
+ return {
1414
+ method: "PUT",
1415
+ endpoint: `/public/api/workspaces/${encodeURIComponent(id)}`,
1416
+ identifiers: { workspaceKey: id, },
1417
+ payload: requiredPlanJsonInput(flags, entry.usage),
1418
+ };
1419
+ case "workspace.delete":
1420
+ return {
1421
+ method: "DELETE",
1422
+ endpoint: `/public/api/workspaces/${encodeURIComponent(id)}`,
1423
+ identifiers: { workspaceKey: id, },
1424
+ };
1425
+ case "workspace.add-object":
1426
+ return {
1427
+ method: "POST",
1428
+ endpoint: `/public/api/workspaces/${encodeURIComponent(id)}/objects`,
1429
+ identifiers: { workspaceKey: id, },
1430
+ payload: requiredPlanJsonInput(flags, entry.usage),
1431
+ };
1432
+ case "meaning.create": {
1433
+ const body = jsonInput(flags) ?? {};
1434
+ const payload = {
1435
+ ...body,
1436
+ id: args[0],
1437
+ label: args[1],
1438
+ type: args[2],
1439
+ description: body.description ?? null,
1440
+ entries: body.entries ?? null,
1441
+ mappings: body.mappings ?? null,
1442
+ pattern: body.pattern ?? null,
1443
+ normalizationMode: body.normalizationMode ?? null,
1444
+ detectable: body.detectable ?? false,
1445
+ };
1446
+ return {
1447
+ method: "POST",
1448
+ endpoint: "/public/api/meanings/",
1449
+ identifiers: { id: args[0], },
1450
+ payload,
1451
+ };
1452
+ }
1453
+ case "meaning.update":
1454
+ return {
1455
+ method: "PUT",
1456
+ endpoint: `/public/api/meanings/${encodeURIComponent(id)}`,
1457
+ identifiers: { id, },
1458
+ payload: requiredPlanJsonInput(flags, entry.usage),
1459
+ };
1460
+ case "meaning.delete":
1461
+ return {
1462
+ method: "DELETE",
1463
+ endpoint: `/public/api/meanings/${encodeURIComponent(id)}`,
1464
+ identifiers: { id, },
1465
+ };
1466
+ case "llm.list": {
1467
+ const params = new URLSearchParams();
1468
+ const purpose = flags["purpose"];
1469
+ if (typeof purpose === "string" && purpose.trim() !== "") {
1470
+ params.set("purpose", purpose.trim());
1471
+ }
1472
+ const query = params.size > 0 ? `?${params.toString()}` : "";
1473
+ return {
1474
+ method: "GET",
1475
+ endpoint: projectEndpoint(`/llms/${query}`),
1476
+ };
1477
+ }
1478
+ case "llm.completions":
1479
+ case "llm.embeddings": {
1480
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1481
+ const llmId = stringField(payload, ["llmId",]);
1482
+ if (llmId === undefined || llmId.trim() === "") {
1483
+ throw new UsageError(`llmId is required and must be a non-empty string (e.g. "openai:openai1:gpt-4").\nUsage: ${entry.usage}`, "invalid_flag_value");
1484
+ }
1485
+ const queries = payload["queries"];
1486
+ if (!Array.isArray(queries) || queries.length === 0) {
1487
+ throw new UsageError(`queries is required and must be a non-empty array of query objects.\nUsage: ${entry.usage}`, "invalid_flag_value");
1488
+ }
1489
+ return {
1490
+ method: "POST",
1491
+ endpoint: projectEndpoint(action === "completions" ? "/llms/completions" : "/llms/embeddings"),
1492
+ identifiers: { llmId, },
1493
+ payload,
1494
+ };
1495
+ }
1496
+ case "knowledge-bank.search": {
1497
+ if (typeof id !== "string" || id.trim() === "") {
1498
+ throw new UsageError(`knowledgeBankId must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
1499
+ }
1500
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1501
+ const query = stringField(payload, ["query",]);
1502
+ if (query === undefined || query.trim() === "") {
1503
+ throw new UsageError(`query is required and must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
1504
+ }
1505
+ return {
1506
+ method: "POST",
1507
+ endpoint: projectEndpoint(`/knowledge-banks/${encodeURIComponent(id)}/search`),
1508
+ identifiers: { knowledgeBankId: id, },
1509
+ payload,
1510
+ };
1511
+ }
1512
+ case "knowledge-bank.clear":
1513
+ if (typeof id !== "string" || id.trim() === "") {
1514
+ throw new UsageError(`knowledgeBankId must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
1515
+ }
1516
+ return {
1517
+ method: "POST",
1518
+ endpoint: projectEndpoint(`/knowledge-banks/${encodeURIComponent(id)}/clear`),
1519
+ identifiers: { knowledgeBankId: id, },
1520
+ };
1521
+ case "data-collection.create":
1522
+ return {
1523
+ method: "POST",
1524
+ endpoint: "/public/api/data-collections/",
1525
+ payload: requiredPlanJsonInput(flags, entry.usage),
1526
+ };
1527
+ case "data-collection.settings-set":
1528
+ return {
1529
+ method: "PUT",
1530
+ endpoint: `/public/api/data-collections/${encodeURIComponent(id)}`,
1531
+ identifiers: { dataCollectionId: id, },
1532
+ payload: requiredPlanJsonInput(flags, entry.usage),
1533
+ };
1534
+ case "data-collection.delete":
1535
+ return {
1536
+ method: "DELETE",
1537
+ endpoint: `/public/api/data-collections/${encodeURIComponent(id)}`,
1538
+ identifiers: { dataCollectionId: id, },
1539
+ };
1540
+ case "data-collection.add-object": {
1541
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1542
+ const cid = id
1543
+ ?? (typeof payload["dataCollectionId"] === "string"
1544
+ ? payload["dataCollectionId"]
1545
+ : undefined);
1546
+ if (!cid)
1547
+ throw new UsageError(`Usage: ${entry.usage}`);
1548
+ const reference = { ...payload, };
1549
+ delete reference["dataCollectionId"];
1550
+ return {
1551
+ method: "POST",
1552
+ endpoint: `/public/api/data-collections/${encodeURIComponent(cid)}/objects`,
1553
+ identifiers: { dataCollectionId: cid, },
1554
+ payload: reference,
1555
+ };
1556
+ }
1557
+ case "data-collection.remove-dataset": {
1558
+ const projectKeyArg = args[1];
1559
+ const datasetName = args[2];
1560
+ if (!projectKeyArg || !datasetName) {
1561
+ throw new UsageError(`Usage: ${entry.usage}`);
1562
+ }
1563
+ return {
1564
+ method: "DELETE",
1565
+ endpoint: `/public/api/data-collections/${encodeURIComponent(id)}/objects/dataset/${encodeURIComponent(projectKeyArg)}/${encodeURIComponent(datasetName)}`,
1566
+ identifiers: { dataCollectionId: id, projectKey: projectKeyArg, datasetName, },
1567
+ };
1568
+ }
1569
+ case "project.tags-set":
1570
+ return {
1571
+ method: "PUT",
1572
+ endpoint: encodedProjectEndpointForPlan(projectKey ?? requiredPlanProjectKey(flags, entry.usage), "/tags"),
1573
+ identifiers: { projectKey: projectKey ?? requiredPlanProjectKey(flags, entry.usage), },
1574
+ payload: requiredPlanJsonInput(flags, entry.usage),
1575
+ };
1576
+ case "project.metadata-set":
1577
+ return {
1578
+ method: "PUT",
1579
+ endpoint: encodedProjectEndpointForPlan(projectKey ?? requiredPlanProjectKey(flags, entry.usage), "/metadata"),
1580
+ identifiers: { projectKey: projectKey ?? requiredPlanProjectKey(flags, entry.usage), },
1581
+ payload: requiredPlanJsonInput(flags, entry.usage),
1582
+ };
1583
+ case "macro.run":
1584
+ case "macro.run-and-wait": {
1585
+ // The plan payload mirrors the live POST body exactly: params and
1586
+ // adminParams when provided via --data, {} otherwise. Validation is
1587
+ // identical to the live path (JSON object shape), zero network.
1588
+ const data = optionalJsonFlag(flags, "data");
1589
+ const runPayload = {};
1590
+ if (data && typeof data === "object" && !Array.isArray(data)) {
1591
+ if (data["params"] !== undefined)
1592
+ runPayload["params"] = data["params"];
1593
+ if (data["adminParams"] !== undefined)
1594
+ runPayload["adminParams"] = data["adminParams"];
1595
+ }
1596
+ return {
1597
+ method: "POST",
1598
+ endpoint: projectEndpoint(`/runnables/${encodeURIComponent(id)}?wait=false`),
1599
+ identifiers: { id, },
1600
+ payload: runPayload,
1601
+ wait: action === "macro.run-and-wait" || flags["wait"] === true,
1602
+ };
1603
+ }
1604
+ case "macro.abort": {
1605
+ const runId = args[1];
1606
+ if (!runId)
1607
+ throw new UsageError(`Usage: ${entry.usage}`);
1608
+ return {
1609
+ method: "POST",
1610
+ endpoint: projectEndpoint(`/runnables/${encodeURIComponent(id)}/abort/${encodeURIComponent(runId)}`),
1611
+ identifiers: { id, runId, },
1612
+ };
1613
+ }
1614
+ case "recipe.metadata-set":
1615
+ return {
1616
+ method: "PUT",
1617
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}/metadata`),
1618
+ identifiers: { recipeName: id, },
1619
+ payload: requiredPlanJsonInput(flags, entry.usage),
1620
+ };
1621
+ case "user.create":
1622
+ return {
1623
+ method: "POST",
1624
+ endpoint: "/public/api/admin/users",
1625
+ payload: redactedUserPayload(requiredPlanJsonInput(flags, entry.usage)),
1626
+ };
1627
+ case "group.create":
1628
+ return {
1629
+ method: "POST",
1630
+ endpoint: "/public/api/admin/groups",
1631
+ payload: requiredPlanJsonInput(flags, entry.usage),
1632
+ };
1633
+ case "saved-model.create-external": {
1634
+ const config = optionalJsonFlag(flags, "configuration") ?? optionalJsonFlag(flags, "data");
1635
+ return {
1636
+ method: "POST",
1637
+ endpoint: projectEndpoint("/savedmodels/"),
1638
+ identifiers: { name: id, },
1639
+ payload: {
1640
+ savedModelType: requiredPlanFlag(flags, "type", entry.usage),
1641
+ name: id,
1642
+ ...(typeof flags["prediction-type"] === "string"
1643
+ ? { predictionType: flags["prediction-type"], }
1644
+ : {}),
1645
+ ...(config ? { proxyModelConfiguration: config, } : {}),
1646
+ },
1647
+ };
1648
+ }
1649
+ case "saved-model.update-settings":
1650
+ return {
1651
+ method: "PUT",
1652
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}`),
1653
+ identifiers: { savedModelId: id, },
1654
+ payload: requiredPlanJsonInput(flags, entry.usage),
1655
+ };
1656
+ case "saved-model.delete-versions": {
1657
+ const versionsRaw = args[1];
1658
+ if (typeof versionsRaw !== "string" || versionsRaw.trim() === "") {
1659
+ throw new UsageError(`Usage: ${entry.usage}`);
1660
+ }
1661
+ return {
1662
+ method: "POST",
1663
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/actions/delete-versions`),
1664
+ identifiers: { savedModelId: id, },
1665
+ payload: {
1666
+ versions: versionsRaw.split(",").map((v) => v.trim()).filter((v) => v.length > 0),
1667
+ removeIntermediate: parseBooleanOption(flags["remove-intermediate"], "--remove-intermediate") ?? true,
1668
+ },
1669
+ };
1670
+ }
1671
+ case "saved-model.import-mlflow-version":
1672
+ return {
1673
+ method: "POST",
1674
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}`),
1675
+ identifiers: { savedModelId: id, versionId: args[1], },
1676
+ payload: {
1677
+ source: { kind: "local-archive", archive: requiredPlanFlag(flags, "archive", entry.usage), },
1678
+ ...(typeof flags["code-env"] === "string"
1679
+ ? { codeEnvName: flags["code-env"], }
1680
+ : {}),
1681
+ // Execute always sends the query parameter with the
1682
+ // external-caller default NONE; the plan mirrors it.
1683
+ containerExecConfigName: typeof flags["container-exec-config"] === "string"
1684
+ ? flags["container-exec-config"]
1685
+ : "NONE",
1686
+ setActive: parseBooleanOption(flags["set-active"], "--set-active") ?? true,
1687
+ ...(flags["binary-classification-threshold"] !== undefined
1688
+ ? {
1689
+ binaryClassificationThreshold: num(flags["binary-classification-threshold"], "--binary-classification-threshold"),
1690
+ }
1691
+ : {}),
1692
+ },
1693
+ };
1694
+ case "saved-model.import-mlflow-version-from-folder":
1695
+ return {
1696
+ exact: false,
1697
+ reason: "Apply sends these values as query parameters (with codeEnvName and binaryClassificationThreshold defaults) on an empty multipart body; the payload shows them structured.",
1698
+ method: "POST",
1699
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}`),
1700
+ identifiers: { savedModelId: id, versionId: args[1], },
1701
+ payload: {
1702
+ source: {
1703
+ kind: "managed-folder",
1704
+ folderRef: requiredPlanFlag(flags, "folder", entry.usage),
1705
+ path: flags["path"],
1706
+ },
1707
+ ...(typeof flags["code-env"] === "string" ? { codeEnvName: flags["code-env"], } : {}),
1708
+ // Execute always sends the query parameter with the
1709
+ // external-caller default NONE; the plan mirrors it.
1710
+ containerExecConfigName: typeof flags["container-exec-config"] === "string"
1711
+ ? flags["container-exec-config"]
1712
+ : "NONE",
1713
+ setActive: parseBooleanOption(flags["set-active"], "--set-active") ?? true,
1714
+ },
1715
+ };
1716
+ case "saved-model.external-metadata-put": {
1717
+ // DSS requires containerExecConfigName on this endpoint; for an
1718
+ // external API caller it resolves as LOCAL-CONFIG -> NONE, so the
1719
+ // plan mirrors the NONE default unless --container-exec-config
1720
+ // overrides it. Same contract as execute.
1721
+ const containerExecConfigName = typeof flags["container-exec-config"] === "string"
1722
+ ? flags["container-exec-config"]
1723
+ : "NONE";
1724
+ return {
1725
+ method: "PUT",
1726
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}/external-ml/metadata?containerExecConfigName=${encodeURIComponent(containerExecConfigName)}`),
1727
+ identifiers: { savedModelId: id, versionId: args[1], },
1728
+ payload: requiredPlanJsonInput(flags, entry.usage),
1729
+ };
1730
+ }
1731
+ case "saved-model.evaluate-version": {
1732
+ const payload = {
1733
+ datasetRef: requiredPlanFlag(flags, "dataset", entry.usage),
1734
+ // Execute always sends an explicit containerExecConfigName
1735
+ // (official external-caller semantics: LOCAL-CONFIG -> NONE);
1736
+ // the plan mirrors that default and any explicit override.
1737
+ containerExecConfigName: typeof flags["container-exec-config"] === "string"
1738
+ ? flags["container-exec-config"]
1739
+ : "NONE",
1740
+ };
1741
+ // --sampling is a JSON flag value; the plan carries the parsed
1742
+ // object under the same key as the execute body (samplingParam).
1743
+ if (flags["sampling"] !== undefined) {
1744
+ payload.samplingParam = json(flags["sampling"], "--sampling");
1745
+ }
1746
+ return {
1747
+ method: "POST",
1748
+ endpoint: `${projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}/external-ml/actions/evaluate`)}?useOptimalThreshold=${parseBooleanOption(flags["use-optimal-threshold"], "--use-optimal-threshold") ?? true}&skipExpensiveReports=${parseBooleanOption(flags["skip-expensive-reports"], "--skip-expensive-reports") ?? true}`,
1749
+ identifiers: { savedModelId: id, versionId: args[1], },
1750
+ payload,
1751
+ };
1752
+ }
1753
+ case "ml-task.set-user-meta":
1754
+ case "saved-model.set-user-meta": {
1755
+ const modelEndpoint = trainedModelEndpoint();
1756
+ return {
1757
+ method: "PUT",
1758
+ endpoint: `${modelEndpoint.path}/user-meta`,
1759
+ identifiers: modelEndpoint.identifiers,
1760
+ payload: requiredPlanJsonInput(flags, entry.usage),
1761
+ };
1762
+ }
1763
+ case "ml-task.compute-diagnostics":
1764
+ case "saved-model.compute-diagnostics": {
1765
+ const modelEndpoint = trainedModelEndpoint();
1766
+ const { kind, features, computation, } = diagnosticsComputationFromFlags(flags);
1767
+ const payload = modelDiagnosticsBody(kind, features, computation);
1768
+ return {
1769
+ method: "POST",
1770
+ endpoint: `${modelEndpoint.path}/${kind}`,
1771
+ identifiers: modelEndpoint.identifiers,
1772
+ ...(payload !== undefined ? { payload, } : {}),
1773
+ wait: flags["wait"] === true,
1774
+ };
1775
+ }
1776
+ case "ml-task.reguess": {
1777
+ const query = mlTaskReguessQuery(mlTaskReguessOptionsFromFlags(flags));
1778
+ return {
1779
+ method: "POST",
1780
+ endpoint: projectEndpoint(`/models/lab/${encodeURIComponent(id)}/${encodeURIComponent(args[1])}/guess${query.size > 0 ? `?${query}` : ""}`),
1781
+ identifiers: { analysisId: id, mlTaskId: args[1], },
1782
+ };
1783
+ }
1784
+ case "ml-task.reguess-forecasting":
1785
+ return {
1786
+ method: "POST",
1787
+ endpoint: projectEndpoint(`/models/lab/${encodeURIComponent(id)}/${encodeURIComponent(args[1])}/reguess-with-forecasting-params`),
1788
+ identifiers: { analysisId: id, mlTaskId: args[1], },
1789
+ payload: requiredPlanJsonInput(flags, entry.usage),
1790
+ };
1791
+ case "ml-task.create-for-dataset":
1792
+ return {
1793
+ method: "POST",
1794
+ endpoint: projectEndpoint("/models/lab/"),
1795
+ identifiers: { inputDataset: id, },
1796
+ payload: {
1797
+ inputDataset: id,
1798
+ ...mlTaskCreateBody(mlTaskFieldsFromFlags(flags, entry.usage)),
1799
+ },
1800
+ };
1801
+ case "analysis.update":
1802
+ return {
1803
+ method: "PUT",
1804
+ endpoint: projectEndpoint(`/lab/${encodeURIComponent(id)}/`),
1805
+ identifiers: { analysisId: id, },
1806
+ payload: requiredPlanJsonInput(flags, entry.usage),
1807
+ };
1808
+ case "user.update":
1809
+ return {
1810
+ method: "PUT",
1811
+ endpoint: `/public/api/admin/users/${encodeURIComponent(id)}`,
1812
+ identifiers: { login: id, },
1813
+ payload: redactedUserPayload(requiredPlanJsonInput(flags, entry.usage)),
1814
+ };
1815
+ case "user.delete":
1816
+ case "group.delete":
1817
+ return {
1818
+ method: "DELETE",
1819
+ endpoint: resource === "user"
1820
+ ? `/public/api/admin/users/${encodeURIComponent(id)}`
1821
+ : `/public/api/admin/groups/${encodeURIComponent(id)}`,
1822
+ identifiers: resource === "user" ? { login: id, } : { name: id, },
1823
+ };
1824
+ case "user.resync":
1825
+ return {
1826
+ method: "POST",
1827
+ endpoint: `/public/api/admin/users/${encodeURIComponent(id)}/actions/resync`,
1828
+ identifiers: { login: id, },
1829
+ };
1830
+ case "user.resync-multi": {
1831
+ const logins = requiredPlanCsv(flags, "logins", entry.usage);
1832
+ return {
1833
+ method: "POST",
1834
+ endpoint: "/public/api/admin/users/actions/resync-multi",
1835
+ identifiers: { count: logins.length, },
1836
+ payload: logins,
1837
+ };
1838
+ }
1839
+ case "user.external-users":
1840
+ return {
1841
+ method: "GET",
1842
+ endpoint: "/public/api/admin/users/actions/external-users",
1843
+ };
1844
+ case "user.external-groups":
1845
+ return {
1846
+ method: "GET",
1847
+ endpoint: "/public/api/admin/users/actions/external-groups",
1848
+ };
1849
+ case "connection.test":
1850
+ return {
1851
+ method: "GET",
1852
+ endpoint: `/public/api/connections/${encodeURIComponent(id)}/test`,
1853
+ identifiers: { connectionName: id, },
1854
+ };
1855
+ case "user.provision":
1856
+ return {
1857
+ method: "POST",
1858
+ endpoint: "/public/api/admin/users/actions/provision",
1859
+ payload: requiredPlanJsonInput(flags, entry.usage),
1860
+ };
1861
+ case "group.update":
1862
+ return {
1863
+ method: "PUT",
1864
+ endpoint: `/public/api/admin/groups/${encodeURIComponent(id)}`,
1865
+ identifiers: { name: id, },
1866
+ payload: requiredPlanJsonInput(flags, entry.usage),
1867
+ };
1868
+ case "connection.create":
1869
+ return {
1870
+ method: "POST",
1871
+ endpoint: "/public/api/admin/connections",
1872
+ payload: redactedConnectionPayload(requiredPlanJsonInput(flags, entry.usage)),
1873
+ };
1874
+ case "connection.update":
1875
+ return {
1876
+ method: "PUT",
1877
+ endpoint: `/public/api/admin/connections/${encodeURIComponent(id)}`,
1878
+ identifiers: { connectionName: id, },
1879
+ payload: redactedConnectionPayload(requiredPlanJsonInput(flags, entry.usage)),
1880
+ };
1881
+ case "connection.delete":
1882
+ return {
1883
+ method: "DELETE",
1884
+ endpoint: `/public/api/admin/connections/${encodeURIComponent(id)}`,
1885
+ identifiers: { connectionName: id, },
1886
+ };
1887
+ case "connection.prepare-import": {
1888
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1889
+ if (!projectKey)
1890
+ throw new UsageError(`Missing project key for ${resource} ${action}.`);
1891
+ return {
1892
+ method: "POST",
1893
+ endpoint: encodedProjectEndpointForPlan(projectKey, "/datasets/tables-import/actions/prepare-from-keys"),
1894
+ identifiers: { projectKey, },
1895
+ payload,
1896
+ };
1897
+ }
1898
+ case "connection.execute-import": {
1899
+ const payload = requiredPlanJsonInput(flags, entry.usage);
1900
+ if (!projectKey)
1901
+ throw new UsageError(`Missing project key for ${resource} ${action}.`);
1902
+ return {
1903
+ method: "POST",
1904
+ endpoint: encodedProjectEndpointForPlan(projectKey, "/datasets/tables-import/actions/execute-from-candidates"),
1905
+ identifiers: { projectKey, },
1906
+ payload,
1907
+ };
1908
+ }
1909
+ case "plugin.install-from-zip":
1910
+ return {
1911
+ method: "POST",
1912
+ endpoint: "/public/api/plugins/actions/installFromZip",
1913
+ payload: { file: requiredPlanFlag(flags, "file", entry.usage), upload: "multipart", },
1914
+ };
1915
+ case "plugin.install-from-store":
1916
+ return {
1917
+ method: "POST",
1918
+ endpoint: "/public/api/plugins/actions/installFromStore",
1919
+ identifiers: { pluginId: id, },
1920
+ payload: { pluginId: id, },
1921
+ };
1922
+ case "plugin.install-from-git": {
1923
+ const payload = pluginGitPlanPayload(flags);
1924
+ return {
1925
+ method: "POST",
1926
+ endpoint: "/public/api/plugins/actions/installFromGit",
1927
+ payload,
1928
+ };
1929
+ }
1930
+ case "plugin.update-from-zip":
1931
+ return {
1932
+ method: "POST",
1933
+ endpoint: pluginActionEndpoint(id, "updateFromZip"),
1934
+ identifiers: { pluginId: id, },
1935
+ payload: { file: requiredPlanFlag(flags, "file", entry.usage), upload: "multipart", },
1936
+ };
1937
+ case "plugin.update-from-store":
1938
+ return {
1939
+ method: "POST",
1940
+ endpoint: pluginActionEndpoint(id, "updateFromStore"),
1941
+ identifiers: { pluginId: id, },
1942
+ };
1943
+ case "plugin.update-from-git":
1944
+ return {
1945
+ method: "POST",
1946
+ endpoint: pluginActionEndpoint(id, "updateFromGit"),
1947
+ identifiers: { pluginId: id, },
1948
+ payload: pluginGitPlanPayload(flags),
1949
+ };
1950
+ case "plugin.settings-set": {
1951
+ const scope = optionalPlanProjectScope(flags);
1952
+ return {
1953
+ method: "POST",
1954
+ endpoint: `${pluginRootEndpoint(id, "/settings")}${scope}`,
1955
+ identifiers: { pluginId: id, },
1956
+ payload: { configKeys: settingsConfigKeys(flags), },
1957
+ };
1958
+ }
1959
+ case "plugin.code-env-create": {
1960
+ const conda = parseBooleanOption(flags["conda"], "--conda") ?? false;
1961
+ return {
1962
+ method: "POST",
1963
+ endpoint: `${pluginRootEndpoint(id, "/code-env/actions/create")}`,
1964
+ identifiers: { pluginId: id, },
1965
+ payload: {
1966
+ deploymentMode: "PLUGIN_MANAGED",
1967
+ conda,
1968
+ pythonInterpreter: flags["python-interpreter"] ?? null,
1969
+ },
1970
+ wait: flags["wait"] === true,
1971
+ };
1972
+ }
1973
+ case "plugin.code-env-update":
1974
+ return {
1975
+ method: "POST",
1976
+ endpoint: `${pluginRootEndpoint(id, "/code-env/actions/update")}`,
1977
+ identifiers: { pluginId: id, },
1978
+ wait: flags["wait"] === true,
1979
+ };
1980
+ case "plugin.move-to-dev":
1981
+ return {
1982
+ method: "POST",
1983
+ endpoint: pluginActionEndpoint(id, "moveToDev"),
1984
+ identifiers: { pluginId: id, },
1985
+ };
1986
+ case "plugin.delete":
1987
+ return {
1988
+ method: "POST",
1989
+ endpoint: pluginActionEndpoint(id, "delete"),
1990
+ identifiers: { pluginId: id, },
1991
+ payload: { force: parseBooleanOption(flags["force"], "--force") ?? false, },
1992
+ };
1993
+ case "plugin.create-dev": {
1994
+ const creationMode = requiredPlanFlag(flags, "creation-mode", entry.usage);
1995
+ const needsGit = creationMode !== "EMPTY";
1996
+ // Mirrors PluginsResource.createDev: git fields are always present, null when unused.
1997
+ const payload = {
1998
+ pluginId: id,
1999
+ creationMode,
2000
+ gitRepository: needsGit && typeof flags["repository"] === "string"
2001
+ ? validatedPlanRepositoryUrl(flags["repository"], "repository", "--repository URL")
2002
+ : null,
2003
+ gitCheckout: needsGit && typeof flags["checkout"] === "string" ? flags["checkout"] : null,
2004
+ gitSubpath: creationMode === "GIT_EXPORT" && typeof flags["path-in-repository"] === "string"
2005
+ ? flags["path-in-repository"]
2006
+ : null,
2007
+ };
2008
+ return {
2009
+ method: "POST",
2010
+ endpoint: "/public/api/plugins/actions/createDev",
2011
+ identifiers: { pluginId: id, creationMode, },
2012
+ payload,
2013
+ };
2014
+ }
2015
+ case "plugin.set-git-remote": {
2016
+ const repository = requiredPlanFlag(flags, "repository", entry.usage);
2017
+ return {
2018
+ method: "POST",
2019
+ endpoint: `${pluginRootEndpoint(id, "/gitRemote")}`,
2020
+ identifiers: { pluginId: id, },
2021
+ payload: { repositoryUrl: validatedPlanRepositoryUrl(repository, "repository", entry.usage), },
2022
+ };
2023
+ }
2024
+ case "plugin.delete-git-remote":
2025
+ return {
2026
+ method: "DELETE",
2027
+ endpoint: pluginRootEndpoint(id, "/gitRemote"),
2028
+ identifiers: { pluginId: id, },
2029
+ };
2030
+ case "plugin.push":
2031
+ case "plugin.pull":
2032
+ case "plugin.fetch":
2033
+ case "plugin.reset-local":
2034
+ case "plugin.reset-remote": {
2035
+ const actionMap = {
2036
+ "plugin.push": "push",
2037
+ "plugin.pull": "pullRebase",
2038
+ "plugin.fetch": "fetch",
2039
+ "plugin.reset-local": "resetToLocalHeadState",
2040
+ "plugin.reset-remote": "resetToRemoteHeadState",
2041
+ };
2042
+ return {
2043
+ method: "POST",
2044
+ endpoint: pluginActionEndpoint(id, actionMap[`${resource}.${action}`]),
2045
+ identifiers: { pluginId: id, },
2046
+ };
2047
+ }
2048
+ case "plugin.contents-put":
2049
+ return {
2050
+ method: "POST",
2051
+ endpoint: pluginContentsEndpoint(id, args[1] ?? ""),
2052
+ identifiers: { pluginId: id, path: args[1], },
2053
+ payload: { contentSource: contentSourceKind(flags), },
2054
+ };
2055
+ case "plugin.contents-delete":
2056
+ return {
2057
+ method: "DELETE",
2058
+ endpoint: pluginContentsEndpoint(id, args[1] ?? ""),
2059
+ identifiers: { pluginId: id, path: args[1], },
2060
+ };
2061
+ case "plugin.folder-add":
2062
+ return {
2063
+ method: "POST",
2064
+ endpoint: pluginFoldersEndpoint(id, args[1] ?? ""),
2065
+ identifiers: { pluginId: id, path: args[1], },
2066
+ };
2067
+ case "plugin.rename": {
2068
+ if (!args[2])
2069
+ throw new UsageError(`Usage: ${entry.usage}`);
2070
+ return {
2071
+ method: "POST",
2072
+ endpoint: `${pluginRootEndpoint(id, "/contents-actions/rename")}`,
2073
+ identifiers: { pluginId: id, path: args[1], },
2074
+ payload: { oldPath: `/${validatePluginPath(args[1] ?? "")}`, newName: args[2], },
2075
+ };
2076
+ }
2077
+ case "plugin.move": {
2078
+ const destination = args[2];
2079
+ if (!destination)
2080
+ throw new UsageError(`Usage: ${entry.usage}`);
2081
+ return {
2082
+ method: "POST",
2083
+ endpoint: `${pluginRootEndpoint(id, "/contents-actions/move")}`,
2084
+ identifiers: { pluginId: id, path: args[1], },
2085
+ payload: {
2086
+ oldPath: `/${validatePluginPath(args[1] ?? "")}`,
2087
+ newPath: validatePluginDestinationPath(destination),
2088
+ },
2089
+ };
2090
+ }
2091
+ case "business-app.save-settings":
2092
+ return {
2093
+ method: "PUT",
2094
+ endpoint: `/public/api/business-apps/${encodeURIComponent(id)}/settings`,
2095
+ identifiers: { id, },
2096
+ payload: requiredPlanJsonInput(flags, entry.usage),
2097
+ };
2098
+ case "business-app.create-instance":
2099
+ return {
2100
+ method: "POST",
2101
+ endpoint: `/public/api/business-apps/${encodeURIComponent(id)}/instances`,
2102
+ identifiers: { id, },
2103
+ payload: requiredPlanJsonInput(flags, entry.usage),
2104
+ };
2105
+ case "business-app.upgrade-instance":
2106
+ return {
2107
+ method: "POST",
2108
+ endpoint: `/public/api/business-apps/${encodeURIComponent(args[0])}/instances/${encodeURIComponent(args[1])}/upgrade`,
2109
+ identifiers: { id: args[0], projectKey: args[1], },
2110
+ payload: {},
2111
+ };
2112
+ case "business-app.install-from-archive":
2113
+ return {
2114
+ method: "POST",
2115
+ endpoint: "/public/api/business-apps/install-from-archive",
2116
+ identifiers: { filePath: id, },
2117
+ payload: uploadPayload(id),
2118
+ };
2119
+ case "app.save-instance-manifest": {
2120
+ const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
2121
+ return {
2122
+ method: "PUT",
2123
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`,
2124
+ identifiers: { projectKey: targetProjectKey, },
2125
+ payload: requiredPlanJsonInput(flags, entry.usage),
2126
+ };
2127
+ }
2128
+ case "app.create-instance": {
2129
+ const payload = requiredPlanJsonInput(flags, entry.usage);
2130
+ if (payload["targetProjectKey"] === undefined) {
2131
+ // Generated-at-apply mode: --plan never allocates or reserves a
2132
+ // random key. The payload stays exactly as supplied, the runtime
2133
+ // generates the key (and defaults the display name to it) during
2134
+ // apply, and no absence probe runs for a generated key, so the
2135
+ // plan carries no preflight requests and no fake concrete GET
2136
+ // path: `{targetProjectKey}` resolves at apply time.
2137
+ return {
2138
+ exact: false,
2139
+ reason: "targetProjectKey (and targetProjectName when omitted) are generated during apply; the sent body adds them.",
2140
+ method: "POST",
2141
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
2142
+ identifiers: {
2143
+ appId: id,
2144
+ targetProjectKeyGeneratedDuringApply: true,
2145
+ ...(payload["targetProjectName"] === undefined
2146
+ ? { targetProjectNameGeneratedDuringApply: true, }
2147
+ : {}),
2148
+ preflightExecuted: false,
2149
+ preflightWillRunDuringApply: false,
2150
+ preflightRequests: [],
2151
+ incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
2152
+ incarnationObservationRequests: [
2153
+ {
2154
+ method: "GET",
2155
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
2156
+ when: flags["wait"] === true
2157
+ ? "after-terminal-future-target"
2158
+ : "conditional-inline-hasResult-target",
2159
+ intent: "After DSS reports inline or terminal creation success for the generated key, observe creationTag for later cleanup binding.",
2160
+ },
2161
+ ],
2162
+ note: "Generated-at-apply: a random APP_ targetProjectKey is generated client-side during apply (never at plan time); targetProjectName defaults to it when omitted. No absent-project preflight runs for a generated key.",
2163
+ },
2164
+ payload,
2165
+ wait: flags["wait"] === true,
2166
+ };
2167
+ }
2168
+ const rawTargetProjectKey = stringField(payload, ["targetProjectKey",]);
2169
+ if (!rawTargetProjectKey || rawTargetProjectKey.trim() === "") {
2170
+ throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${entry.usage}`);
2171
+ }
2172
+ // The runtime trims and rewrites body.targetProjectKey; the plan
2173
+ // advertises the normalized identifier and payload.
2174
+ const targetProjectKey = rawTargetProjectKey.trim();
2175
+ const normalizedPayload = { ...payload, targetProjectKey, };
2176
+ return {
2177
+ method: "POST",
2178
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
2179
+ identifiers: {
2180
+ appId: id,
2181
+ targetProjectKey,
2182
+ preflightExecuted: false,
2183
+ preflightWillRunDuringApply: true,
2184
+ incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
2185
+ incarnationObservationRequests: [
2186
+ {
2187
+ method: "GET",
2188
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
2189
+ when: flags["wait"] === true
2190
+ ? "after-terminal-future-target"
2191
+ : "conditional-inline-hasResult-target",
2192
+ intent: "After DSS reports inline or terminal creation success for the requested key, observe creationTag for later cleanup binding.",
2193
+ },
2194
+ ],
2195
+ preflightRequests: [
2196
+ {
2197
+ method: "GET",
2198
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
2199
+ when: "before-create",
2200
+ intent: "Require the target project to be absent before creating the instance.",
2201
+ },
2202
+ {
2203
+ method: "GET",
2204
+ endpoint: "/public/api/projects/",
2205
+ when: "conditional",
2206
+ intent: "Fallback list probe issued only when the direct project GET is forbidden (403). Presence proves a collision; absence cannot prove availability.",
2207
+ },
2208
+ {
2209
+ method: "GET",
2210
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
2211
+ when: "conditional",
2212
+ intent: "Fallback app-instance list probe issued when the direct project GET is forbidden (403). Presence proves a collision; absence still rejects creation as unverifiable.",
2213
+ },
2214
+ ],
2215
+ note: "Strict preflight: the instance POST runs only after the payload targetProjectKey is confirmed absent. Creation never writes into an existing or unprovable project.",
2216
+ },
2217
+ payload: normalizedPayload,
2218
+ wait: flags["wait"] === true,
2219
+ };
2220
+ }
2221
+ case "app.set-manifest-version": {
2222
+ const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
2223
+ const payloadPatch = {};
2224
+ const version = flags["manifest-version"];
2225
+ if (version !== undefined) {
2226
+ if (version.trim() === "") {
2227
+ throw new UsageError("App manifest version must be a non-empty string.", "validation_failed", `Usage: ${entry.usage}`);
2228
+ }
2229
+ payloadPatch.version = version;
2230
+ }
2231
+ const versionNotes = flags["version-notes"];
2232
+ if (versionNotes !== undefined)
2233
+ payloadPatch.versionNotes = versionNotes;
2234
+ if (version === undefined && versionNotes === undefined) {
2235
+ throw new UsageError("At least one of --manifest-version or --version-notes is required.", "usage_error", `Usage: ${entry.usage}`);
2236
+ }
2237
+ const expectHash = flags["expect-hash"];
2238
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
2239
+ throw new UsageError("Expected manifest hash must be a 64-character SHA-256 hex digest.", "validation_failed", `Usage: ${entry.usage}`);
2240
+ }
2241
+ return {
2242
+ method: "PUT",
2243
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`,
2244
+ identifiers: {
2245
+ projectKey: targetProjectKey,
2246
+ payloadPatch,
2247
+ ...(expectHash !== undefined ? { expectHash: expectHash.toLowerCase(), } : {}),
2248
+ concurrencyControl: APP_MANIFEST_CONCURRENCY_CONTROL,
2249
+ staleReadCheck: expectHash === undefined
2250
+ ? "none"
2251
+ : "client-side-expect-hash-compare-before-put",
2252
+ note: expectHash === undefined
2253
+ ? "Unconditional PUT: no stale-read check is armed because --expect-hash was not supplied."
2254
+ : "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.",
2255
+ },
2256
+ };
2257
+ }
2258
+ case "app.create-successor-instance": {
2259
+ const sourceProjectKey = requiredPlanFlag(flags, "from", entry.usage);
2260
+ const targetProjectKey = optionalPlanFlag(flags, "to", entry.usage);
2261
+ if (targetProjectKey !== undefined && sourceProjectKey === targetProjectKey) {
2262
+ throw new UsageError("--from and --to must be different project keys.", "validation_failed", `Usage: ${entry.usage}`);
2263
+ }
2264
+ const targetProjectName = flags["name"];
2265
+ const copyPermissions = parseBooleanOption(flags["copy-permissions"], "--copy-permissions")
2266
+ ?? false;
2267
+ if (targetProjectKey === undefined) {
2268
+ // Generated-at-apply mode: --plan never allocates a key and no
2269
+ // target exists yet, so the plan makes no target-absence claim
2270
+ // and advertises no target probe. The key is generated once
2271
+ // during apply, immediately before the single instance POST;
2272
+ // `{targetProjectKey}` resolves only after the terminal future
2273
+ // names it. Source and template gates still run before the POST.
2274
+ return {
2275
+ method: "POST",
2276
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
2277
+ identifiers: {
2278
+ appId: id,
2279
+ sourceProjectKey,
2280
+ targetProjectKeyGeneratedDuringApply: true,
2281
+ targetPreflight: "not-applicable-generated-key",
2282
+ preflightExecuted: false,
2283
+ preflightWillRunDuringApply: true,
2284
+ ...(targetProjectName !== undefined
2285
+ ? { targetProjectName, }
2286
+ : { targetProjectNameGeneratedDuringApply: true, }),
2287
+ copyPermissions,
2288
+ incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
2289
+ incarnationObservationRequests: [
2290
+ {
2291
+ method: "GET",
2292
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
2293
+ when: "after-terminal-future-target",
2294
+ intent: "After the terminal future names the generated successor key, observe creationTag and bind later target checks and cleanup to that hash.",
2295
+ },
2296
+ ],
2297
+ preflightRequests: [
2298
+ {
2299
+ method: "GET",
2300
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
2301
+ when: "before-create",
2302
+ intent: "Verify the --from project is a registered instance of the app.",
2303
+ },
2304
+ {
2305
+ method: "GET",
2306
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/app-manifest`,
2307
+ when: "before-create",
2308
+ intent: "Verify the --from project is an APP_INSTANCE project.",
2309
+ },
2310
+ ],
2311
+ ...(copyPermissions
2312
+ ? {
2313
+ permissionConcurrencyControl: "client-side-non-atomic-stale-identity-and-hash-checks",
2314
+ permissionRequests: [
2315
+ {
2316
+ method: "GET",
2317
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
2318
+ intent: "Snapshot the predecessor instance ACL before creation.",
2319
+ },
2320
+ {
2321
+ method: "GET",
2322
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
2323
+ intent: "Read the generated successor ACL to decide whether the copy is a no-op.",
2324
+ },
2325
+ {
2326
+ method: "GET",
2327
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
2328
+ intent: "Recheck generated successor creationTag after reading its ACL; stop if the project key was reused.",
2329
+ },
2330
+ {
2331
+ method: "GET",
2332
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
2333
+ intent: "Recheck the predecessor ACL immediately before the write and stop if its hash drifted.",
2334
+ },
2335
+ {
2336
+ method: "GET",
2337
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
2338
+ intent: "Recheck generated successor creationTag immediately before the unconditional permission PUT.",
2339
+ },
2340
+ {
2341
+ method: "PUT",
2342
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
2343
+ intent: "Apply the predecessor ACL snapshot to the generated successor instance.",
2344
+ },
2345
+ {
2346
+ method: "GET",
2347
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
2348
+ intent: "Verify the generated successor ACL hash equals the predecessor snapshot hash.",
2349
+ },
2350
+ {
2351
+ method: "GET",
2352
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
2353
+ intent: "Detect generated successor project-key reuse across the permission write and verification read.",
2354
+ },
2355
+ ],
2356
+ }
2357
+ : {}),
2358
+ note: "Generated-at-apply: the successor key is generated once during apply (never at plan time) and no absent-project preflight runs for a generated key, so the plan claims no target absence. Source and template gates run before the single instance POST, then the terminal future's own result names the successor key. The predecessor is never modified or deleted; cleanup targets the generated successor key only.",
2359
+ },
2360
+ payload: targetProjectName !== undefined ? { targetProjectName, } : {},
2361
+ wait: true,
2362
+ };
2363
+ }
2364
+ return {
2365
+ method: "POST",
2366
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
2367
+ identifiers: {
2368
+ appId: id,
2369
+ sourceProjectKey,
2370
+ targetProjectKey,
2371
+ preflightExecuted: false,
2372
+ preflightWillRunDuringApply: true,
2373
+ ...(targetProjectName !== undefined ? { targetProjectName, } : {}),
2374
+ copyPermissions,
2375
+ incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
2376
+ postFutureRequests: [
2377
+ {
2378
+ method: "GET",
2379
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
2380
+ intent: "After the terminal future names the successor key, observe creationTag and bind later target checks and cleanup to that hash.",
2381
+ },
2382
+ ],
2383
+ ...(copyPermissions
2384
+ ? {
2385
+ permissionConcurrencyControl: "client-side-non-atomic-stale-identity-and-hash-checks",
2386
+ }
2387
+ : {}),
2388
+ preflightRequests: [
2389
+ {
2390
+ method: "GET",
2391
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
2392
+ when: "before-create",
2393
+ intent: "Verify the --from project is a registered instance of the app.",
2394
+ },
2395
+ {
2396
+ method: "GET",
2397
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/app-manifest`,
2398
+ when: "before-create",
2399
+ intent: "Verify the --from project is an APP_INSTANCE project.",
2400
+ },
2401
+ {
2402
+ method: "GET",
2403
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
2404
+ when: "before-create",
2405
+ intent: "Require the --to target project to be absent before creating the successor.",
2406
+ },
2407
+ {
2408
+ method: "GET",
2409
+ endpoint: "/public/api/projects/",
2410
+ when: "conditional",
2411
+ intent: "Fallback list probe issued only when the direct project GET is forbidden (403). Presence proves a collision; absence cannot prove availability and rejects creation.",
2412
+ },
2413
+ ],
2414
+ ...(copyPermissions
2415
+ ? {
2416
+ permissionRequests: [
2417
+ {
2418
+ method: "GET",
2419
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
2420
+ intent: "Snapshot the predecessor instance ACL before creation.",
2421
+ },
2422
+ {
2423
+ method: "GET",
2424
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
2425
+ intent: "Read the successor ACL to decide whether the copy is a no-op.",
2426
+ },
2427
+ {
2428
+ method: "GET",
2429
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
2430
+ intent: "Recheck successor creationTag after reading its ACL; stop if the project key was reused.",
2431
+ },
2432
+ {
2433
+ method: "GET",
2434
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
2435
+ intent: "Recheck the predecessor ACL immediately before the write and stop if its hash drifted.",
2436
+ },
2437
+ {
2438
+ method: "GET",
2439
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
2440
+ intent: "Recheck successor creationTag immediately before the unconditional permission PUT.",
2441
+ },
2442
+ {
2443
+ method: "PUT",
2444
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
2445
+ intent: "Apply the predecessor ACL snapshot to the successor instance.",
2446
+ },
2447
+ {
2448
+ method: "GET",
2449
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
2450
+ intent: "Verify the successor ACL hash equals the predecessor snapshot hash.",
2451
+ },
2452
+ {
2453
+ method: "GET",
2454
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
2455
+ intent: "Detect successor project-key reuse across the permission write and verification read.",
2456
+ },
2457
+ ],
2458
+ }
2459
+ : {}),
2460
+ 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.",
2461
+ },
2462
+ payload: {
2463
+ targetProjectKey,
2464
+ targetProjectName: targetProjectName ?? targetProjectKey,
2465
+ },
2466
+ wait: true,
2467
+ };
2468
+ }
2469
+ case "app.delete-instance": {
2470
+ const targetProjectKey = requiredPlanFlag(flags, "project-key", entry.usage);
2471
+ const futureId = flags["future-id"] === undefined
2472
+ ? undefined
2473
+ : requiredPlanFlag(flags, "future-id", entry.usage);
2474
+ const expectedProjectIncarnation = flags["expect-project-incarnation"] === undefined
2475
+ ? undefined
2476
+ : requiredPlanFlag(flags, "expect-project-incarnation", entry.usage);
2477
+ if (expectedProjectIncarnation !== undefined
2478
+ && !/^[0-9a-f]{64}$/.test(expectedProjectIncarnation)) {
2479
+ throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
2480
+ }
2481
+ const unconfirmedCreation = parseBooleanOption(flags["unconfirmed-creation"], "--unconfirmed-creation") ?? false;
2482
+ const manifestProbe = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`;
2483
+ const projectDetailsProbe = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`;
2484
+ const typeValidationRequests = [
2485
+ {
2486
+ method: "GET",
2487
+ endpoint: manifestProbe,
2488
+ when: "before-delete",
2489
+ intent: "Verify the target is an APP_INSTANCE project before deleting.",
2490
+ },
2491
+ {
2492
+ method: "GET",
2493
+ endpoint: projectDetailsProbe,
2494
+ when: expectedProjectIncarnation === undefined
2495
+ ? "conditional-type-check-before-delete"
2496
+ : "incarnation-and-conditional-type-check-before-delete",
2497
+ intent: expectedProjectIncarnation === undefined
2498
+ ? "Fallback probe issued only when the app-manifest response omits projectAppType (live DSS does)."
2499
+ : "Recompute the current project-incarnation hash from creationTag after the manifest probe; the same response supplies projectAppType when the manifest omits it.",
2500
+ },
2501
+ ];
2502
+ const preflightRequests = typeValidationRequests;
2503
+ if (unconfirmedCreation) {
2504
+ return {
2505
+ identifiers: {
2506
+ projectKey: targetProjectKey,
2507
+ ...(futureId !== undefined ? { futureId, } : {}),
2508
+ unconfirmedCreation: true,
2509
+ 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.",
2510
+ },
2511
+ };
2512
+ }
2513
+ if (futureId === undefined) {
2514
+ return {
2515
+ method: "DELETE",
2516
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}`,
2517
+ identifiers: {
2518
+ projectKey: targetProjectKey,
2519
+ ...(expectedProjectIncarnation === undefined
2520
+ ? {}
2521
+ : {
2522
+ projectIncarnationGate: {
2523
+ required: false,
2524
+ provided: true,
2525
+ expectedHash: expectedProjectIncarnation,
2526
+ },
2527
+ incarnationControl: "client-side-non-atomic-stale-identity-check",
2528
+ }),
2529
+ preflightRequests,
2530
+ note: expectedProjectIncarnation === undefined
2531
+ ? "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."
2532
+ : "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.",
2533
+ },
2534
+ };
2535
+ }
2536
+ return {
2537
+ method: "DELETE",
2538
+ endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}`,
2539
+ identifiers: {
2540
+ projectKey: targetProjectKey,
2541
+ futureId,
2542
+ projectIncarnationGate: {
2543
+ required: true,
2544
+ provided: expectedProjectIncarnation !== undefined,
2545
+ ...(expectedProjectIncarnation === undefined
2546
+ ? {}
2547
+ : { expectedHash: expectedProjectIncarnation, }),
2548
+ },
2549
+ incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
2550
+ preflightRequests: [
2551
+ {
2552
+ method: "GET",
2553
+ endpoint: manifestProbe,
2554
+ when: "before-future-wait",
2555
+ intent: "Verify the target is an APP_INSTANCE project before the supplied future is touched, so an invalid target cannot affect it.",
2556
+ },
2557
+ {
2558
+ method: "GET",
2559
+ endpoint: projectDetailsProbe,
2560
+ when: "conditional-before-wait",
2561
+ intent: "Fallback probe issued only when the pre-wait app-manifest response omits projectAppType (live DSS does).",
2562
+ },
2563
+ ],
2564
+ futureGate: [
2565
+ {
2566
+ method: "GET",
2567
+ endpoint: `/public/api/futures/${encodeURIComponent(futureId)}?peek=false`,
2568
+ 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.",
2569
+ },
2570
+ ],
2571
+ postFutureValidationRequests: expectedProjectIncarnation === undefined
2572
+ ? []
2573
+ : typeValidationRequests,
2574
+ note: expectedProjectIncarnation === undefined
2575
+ ? "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."
2576
+ : "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.",
2577
+ },
2578
+ };
2579
+ }
2580
+ case "app.permissions-restore": {
2581
+ const file = requiredPlanFlag(flags, "file", entry.usage);
2582
+ const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
2583
+ const permissionsEndpoint = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`;
2584
+ const projectDetailsEndpoint = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`;
2585
+ return {
2586
+ method: "PUT",
2587
+ endpoint: permissionsEndpoint,
2588
+ identifiers: {
2589
+ file,
2590
+ projectKey: targetProjectKey,
2591
+ localPreflight: [
2592
+ "Read and hash-verify the owner-only snapshot file.",
2593
+ "Require its project key and canonical DSS URL to match this invocation.",
2594
+ ],
2595
+ preflightRequests: [
2596
+ {
2597
+ method: "GET",
2598
+ endpoint: projectDetailsEndpoint,
2599
+ intent: "Require the snapshot project-incarnation hash to match creationTag.",
2600
+ },
2601
+ {
2602
+ method: "GET",
2603
+ endpoint: permissionsEndpoint,
2604
+ intent: "Read current permissions; an equal hash makes the PUT unnecessary.",
2605
+ },
2606
+ {
2607
+ method: "GET",
2608
+ endpoint: projectDetailsEndpoint,
2609
+ intent: "Recheck the project incarnation after the permission read and immediately before any PUT.",
2610
+ },
2611
+ ],
2612
+ conditionalWrite: {
2613
+ method: "PUT",
2614
+ endpoint: permissionsEndpoint,
2615
+ when: "permissions-differ-and-dry-run-is-false",
2616
+ },
2617
+ verificationRequests: [
2618
+ {
2619
+ method: "GET",
2620
+ endpoint: permissionsEndpoint,
2621
+ intent: "Verify DSS persisted the desired permission hash.",
2622
+ },
2623
+ {
2624
+ method: "GET",
2625
+ endpoint: projectDetailsEndpoint,
2626
+ intent: "Detect project-key reuse across the permission write.",
2627
+ },
2628
+ ],
2629
+ incarnationControl: "client-side-non-atomic-stale-identity-check",
2630
+ 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.",
2631
+ },
2632
+ };
2633
+ }
2634
+ case "statistics.create-worksheet":
2635
+ return {
2636
+ method: "POST",
2637
+ endpoint: statisticsWorksheetsEndpoint(args[0]),
2638
+ identifiers: { dataset: args[0], },
2639
+ payload: requiredPlanJsonInput(flags, entry.usage),
2640
+ };
2641
+ case "statistics.update-worksheet":
2642
+ return {
2643
+ method: "PUT",
2644
+ endpoint: statisticsWorksheetEndpoint(args[0], args[1]),
2645
+ identifiers: { dataset: args[0], worksheetId: args[1], },
2646
+ payload: requiredPlanJsonInput(flags, entry.usage),
2647
+ };
2648
+ case "statistics.delete-worksheet":
2649
+ return {
2650
+ method: "DELETE",
2651
+ endpoint: statisticsWorksheetEndpoint(args[0], args[1]),
2652
+ identifiers: { dataset: args[0], worksheetId: args[1], },
2653
+ };
2654
+ case "statistics.run-worksheet":
2655
+ return {
2656
+ method: "POST",
2657
+ endpoint: `${statisticsWorksheetEndpoint(args[0], args[1])}/actions/run-card`,
2658
+ identifiers: { dataset: args[0], worksheetId: args[1], },
2659
+ };
2660
+ case "statistics.run-card":
2661
+ return {
2662
+ method: "POST",
2663
+ endpoint: `${statisticsWorksheetEndpoint(args[0], args[1])}/actions/run-card`,
2664
+ identifiers: { dataset: args[0], worksheetId: args[1], },
2665
+ payload: requiredPlanJsonInput(flags, entry.usage),
2666
+ };
2667
+ case "statistics.run-computation":
2668
+ return {
2669
+ method: "POST",
2670
+ endpoint: `${statisticsWorksheetEndpoint(args[0], args[1])}/actions/run-computation`,
2671
+ identifiers: { dataset: args[0], worksheetId: args[1], },
2672
+ payload: requiredPlanJsonInput(flags, entry.usage),
2673
+ };
2674
+ case "webapp.create":
2675
+ return {
2676
+ method: "POST",
2677
+ endpoint: projectEndpoint("/webapps/"),
2678
+ payload: requiredPlanJsonInput(flags, entry.usage),
2679
+ };
2680
+ case "webapp.update-settings": {
2681
+ const patch = requiredPlanJsonInput(flags, entry.usage);
2682
+ const expectHash = flags["expect-hash"];
2683
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
2684
+ throw new UsageError("Expected webapp hash must be a 64-character SHA-256 hex digest.");
2685
+ }
2686
+ const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}`);
2687
+ return {
2688
+ exact: false,
2689
+ reason: "Apply GETs the current settings, deep-merges the patch, then PUTs the full object.",
2690
+ method: "PUT",
2691
+ endpoint,
2692
+ identifiers: {
2693
+ webappId: id,
2694
+ patchHash: stableHash(patch),
2695
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
2696
+ },
2697
+ payload: { patch: "<omitted>", patchHash: stableHash(patch), },
2698
+ requests: [
2699
+ { method: "GET", endpoint, purpose: "read full settings and verify expectHash", },
2700
+ {
2701
+ method: "PUT",
2702
+ endpoint,
2703
+ condition: "settings read and expected hash matched",
2704
+ payload: "<merged full settings>",
2705
+ },
2706
+ ],
2707
+ };
2708
+ }
2709
+ case "webapp.stop-backend":
2710
+ return {
2711
+ method: "PUT",
2712
+ endpoint: projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/stop`),
2713
+ identifiers: { webappId: id, },
2714
+ payload: {},
2715
+ };
2716
+ case "webapp.restart-backend": {
2717
+ const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/restart`);
2718
+ return {
2719
+ method: "PUT",
2720
+ endpoint,
2721
+ identifiers: { webappId: id, },
2722
+ payload: {},
2723
+ wait: {
2724
+ requested: flags["wait"] === true,
2725
+ timeoutMs: num(flags["timeout"], "--timeout"),
2726
+ pollIntervalMs: num(flags["poll-interval"], "--poll-interval"),
2727
+ },
2728
+ };
2729
+ }
2730
+ case "api-service.create":
2731
+ return {
2732
+ method: "POST",
2733
+ endpoint: projectEndpoint(`/apiservices/${encodeURIComponent(id)}`),
2734
+ identifiers: { serviceId: id, },
2735
+ payload: {},
2736
+ };
2737
+ case "api-service.save-settings": {
2738
+ const settings = requiredPlanJsonInput(flags, entry.usage);
2739
+ const expectHash = flags["expect-hash"];
2740
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
2741
+ throw new UsageError("Expected API service settings hash must be a 64-character SHA-256 hex digest.");
2742
+ }
2743
+ const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
2744
+ const payload = {
2745
+ settings: "<omitted>",
2746
+ settingsHash: stableHash(settings),
2747
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
2748
+ };
2749
+ return {
2750
+ method: "PUT",
2751
+ endpoint,
2752
+ identifiers: { serviceId: id, },
2753
+ payload,
2754
+ requests: [
2755
+ ...(expectHash
2756
+ ? [{ method: "GET", endpoint, purpose: "verify expectHash", },]
2757
+ : []),
2758
+ {
2759
+ method: "PUT",
2760
+ endpoint,
2761
+ condition: expectHash ? "hash matched" : "unconditional",
2762
+ payload,
2763
+ },
2764
+ ],
2765
+ };
2766
+ }
2767
+ case "api-service.add-prediction-endpoint": {
2768
+ const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
2769
+ return {
2770
+ exact: false,
2771
+ reason: "Apply GETs the full settings, appends one endpoint, then PUTs the full object.",
2772
+ method: "PUT",
2773
+ endpoint,
2774
+ identifiers: { serviceId: id, endpointId: args[1], savedModelId: args[2], },
2775
+ payload: { id: args[1], type: "STD_PREDICTION", modelRef: args[2], },
2776
+ requests: [
2777
+ { method: "GET", endpoint, purpose: "read full settings", },
2778
+ { method: "PUT", endpoint, payload: "<merged full settings>", },
2779
+ ],
2780
+ };
2781
+ }
2782
+ case "api-service.create-package":
2783
+ case "api-service.delete-package":
2784
+ case "api-service.publish-package": {
2785
+ const serviceId = args[0];
2786
+ const packageId = args[1];
2787
+ const packageEndpoint = projectEndpoint(`/apiservices/${encodeURIComponent(serviceId)}/packages/${encodeURIComponent(packageId)}`);
2788
+ if (action === "delete-package") {
2789
+ return {
2790
+ method: "DELETE",
2791
+ endpoint: packageEndpoint,
2792
+ identifiers: { serviceId, packageId, },
2793
+ };
2794
+ }
2795
+ if (action === "publish-package") {
2796
+ return {
2797
+ method: "POST",
2798
+ endpoint: `${packageEndpoint}/publish${querySuffix({
2799
+ publishedServiceId: flags["published-service-id"],
2800
+ })}`,
2801
+ identifiers: { serviceId, packageId, },
2802
+ };
2803
+ }
2804
+ return {
2805
+ method: "POST",
2806
+ endpoint: packageEndpoint + querySuffix({
2807
+ releaseNotes: flags["release-notes"],
2808
+ }),
2809
+ identifiers: { serviceId, packageId, },
2810
+ };
2811
+ }
2812
+ case "bundle.export": {
2813
+ const evaluateProjectStandardsChecks = parseBooleanOption(flags["evaluate-standards-checks"], "--evaluate-standards-checks") ?? true;
2814
+ return {
2815
+ method: "PUT",
2816
+ endpoint: projectEndpoint("/bundles/exported/" + encodeURIComponent(id)) + querySuffix({
2817
+ releaseNotes: flags["release-notes"],
2818
+ evaluateProjectStandardsChecks,
2819
+ }),
2820
+ identifiers: { bundleId: id, },
2821
+ };
2822
+ }
2823
+ case "bundle.publish":
2824
+ return {
2825
+ method: "POST",
2826
+ endpoint: projectEndpoint("/bundles/" + encodeURIComponent(id) + "/publish") + querySuffix({
2827
+ publishedProjectKey: flags["published-project-key"],
2828
+ }),
2829
+ identifiers: { bundleId: id, },
2830
+ payload: {},
2831
+ };
2832
+ case "bundle.activate": {
2833
+ const rawScenarios = flags["scenarios"];
2834
+ let scenariosToEnable;
2835
+ if (rawScenarios !== undefined && rawScenarios !== false) {
2836
+ const parsed = json(rawScenarios);
2837
+ if (typeof parsed !== "object"
2838
+ || parsed === null
2839
+ || Array.isArray(parsed)
2840
+ || !Object.values(parsed).every((value) => typeof value === "boolean")) {
2841
+ throw new UsageError("--scenarios must be a JSON object mapping scenario IDs to true|false.");
2842
+ }
2843
+ scenariosToEnable = parsed;
2844
+ }
2845
+ return {
2846
+ method: "POST",
2847
+ endpoint: projectEndpoint(`/bundles/imported/${encodeURIComponent(id)}/actions/activate`),
2848
+ identifiers: { bundleId: id, },
2849
+ payload: scenariosToEnable === undefined
2850
+ ? {}
2851
+ : { scenariosActiveOnActivation: scenariosToEnable, },
2852
+ };
2853
+ }
2854
+ case "bundle.preload":
2855
+ return {
2856
+ method: "POST",
2857
+ endpoint: projectEndpoint(`/bundles/imported/${encodeURIComponent(id)}/actions/preload`),
2858
+ identifiers: { bundleId: id, },
2859
+ payload: {},
2860
+ };
2861
+ case "project-library.create-file":
2862
+ case "project-library.create-folder": {
2863
+ const libraryPath = validateLibraryPath(id);
2864
+ const kind = action === "create-file" ? "contents" : "folders";
2865
+ return {
2866
+ exact: false,
2867
+ reason: "Apply performs a live absence check before POST; --if-not-exists may stop after that read, and create never overwrites an existing item.",
2868
+ method: "POST",
2869
+ endpoint: projectEndpoint(`/libraries/${kind}/${encodeLibraryPath(libraryPath)}`),
2870
+ identifiers: { path: libraryPath, },
2871
+ };
2872
+ }
2873
+ case "project-library.put": {
2874
+ const libraryPath = validateLibraryPath(id);
2875
+ const endpoint = projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`);
2876
+ const payload = projectLibraryPutPayload(flags);
2877
+ return {
2878
+ method: "POST",
2879
+ endpoint,
2880
+ identifiers: {
2881
+ path: libraryPath,
2882
+ concurrencyControl: payload.expectSha256
2883
+ ? PROJECT_LIBRARY_CONCURRENCY_CONTROL
2884
+ : "none",
2885
+ },
2886
+ payload,
2887
+ ...(payload.expectSha256
2888
+ ? {
2889
+ requests: [
2890
+ {
2891
+ method: "GET",
2892
+ endpoint: `${endpoint}?dataEncoding=base64`,
2893
+ purpose: "verify expectSha256",
2894
+ },
2895
+ { method: "POST", endpoint, condition: "hash matched", payload, },
2896
+ ],
2897
+ }
2898
+ : {}),
2899
+ };
2900
+ }
2901
+ case "project-library.delete": {
2902
+ const libraryPath = validateLibraryPath(id);
2903
+ return {
2904
+ method: "DELETE",
2905
+ endpoint: projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`),
2906
+ identifiers: { path: libraryPath, },
2907
+ };
2908
+ }
2909
+ case "project-library.rename": {
2910
+ const libraryPath = validateLibraryPath(id);
2911
+ const newName = validateLibraryName(args[1]);
2912
+ return {
2913
+ method: "POST",
2914
+ endpoint: projectEndpoint("/libraries/contents-actions/rename/"),
2915
+ identifiers: { path: libraryPath, newName, },
2916
+ payload: { oldPath: `/${libraryPath}`, newName, },
2917
+ };
2918
+ }
2919
+ case "project-library.move": {
2920
+ const libraryPath = validateLibraryPath(id);
2921
+ const destinationFolder = validateLibraryDestinationPath(args[1]);
2922
+ return {
2923
+ method: "POST",
2924
+ endpoint: projectEndpoint("/libraries/contents-actions/move"),
2925
+ identifiers: { path: libraryPath, destinationFolder, },
2926
+ payload: { oldPath: `/${libraryPath}`, newPath: destinationFolder, },
2927
+ };
2928
+ }
2929
+ case "code-env.create": {
2930
+ const wait = codeEnvWait(flags);
2931
+ return {
2932
+ method: "POST",
2933
+ endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
2934
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
2935
+ payload: {
2936
+ ...codeEnvParams(flags),
2937
+ deploymentMode: requiredPlanFlag(flags, "deployment-mode", entry.usage),
2938
+ },
2939
+ wait,
2940
+ };
2941
+ }
2942
+ case "code-env.set-definition": {
2943
+ const definition = requiredPlanJsonInput(flags, entry.usage);
2944
+ const expectHash = flags["expect-hash"];
2945
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
2946
+ throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
2947
+ }
2948
+ const endpoint = codeEnvEndpoint();
2949
+ const definitionHash = stableHash(definition);
2950
+ const normalizedExpectHash = expectHash?.toLowerCase();
2951
+ return {
2952
+ exact: false,
2953
+ reason: "Apply PUTs the supplied definition object unchanged; this plan omits that body and reports only its hash.",
2954
+ method: "PUT",
2955
+ endpoint,
2956
+ identifiers: {
2957
+ lang: codeEnvLang(args[0], entry.usage),
2958
+ name: args[1],
2959
+ definitionHash,
2960
+ ...(normalizedExpectHash ? { expectHash: normalizedExpectHash, } : {}),
2961
+ },
2962
+ payload: "<omitted>",
2963
+ ...(normalizedExpectHash
2964
+ ? {
2965
+ requests: [
2966
+ { method: "GET", endpoint, purpose: "verify expectHash", },
2967
+ {
2968
+ method: "PUT",
2969
+ endpoint,
2970
+ condition: "hash matched",
2971
+ payload: "<omitted>",
2972
+ redactedFields: ["payload",],
2973
+ },
2974
+ ],
2975
+ }
2976
+ : {}),
2977
+ };
2978
+ }
2979
+ case "code-env.set-packages": {
2980
+ const installCorePackages = parseBooleanOption(flags["install-core-packages"], "--install-core-packages");
2981
+ const expectHash = flags["expect-hash"];
2982
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
2983
+ throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
2984
+ }
2985
+ const endpoint = codeEnvEndpoint();
2986
+ return {
2987
+ exact: false,
2988
+ reason: "Apply GETs the full definition, merges only package fields, then PUTs the full result.",
2989
+ method: "PUT",
2990
+ endpoint,
2991
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
2992
+ payload: {
2993
+ packages: codeEnvPackageList(flags),
2994
+ ...(installCorePackages !== undefined ? { installCorePackages, } : {}),
2995
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
2996
+ },
2997
+ requests: [
2998
+ { method: "GET", endpoint, purpose: "read full definition and verify expectHash", },
2999
+ {
3000
+ method: "PUT",
3001
+ endpoint,
3002
+ condition: "definition read and hash matched",
3003
+ payload: "<merged full definition>",
3004
+ },
3005
+ ],
3006
+ };
3007
+ }
3008
+ case "code-env.update-packages": {
3009
+ const versionToUpdate = typeof flags["env-version"] === "string"
3010
+ ? flags["env-version"]
3011
+ : typeof flags["version"] === "string"
3012
+ ? flags["version"]
3013
+ : undefined;
3014
+ const wait = codeEnvWait(flags);
3015
+ return {
3016
+ method: "POST",
3017
+ endpoint: `${codeEnvEndpoint("/packages")}${querySuffix({
3018
+ forceRebuildEnv: flags["force-rebuild"] === true,
3019
+ versionToUpdate,
3020
+ wait,
3021
+ })}`,
3022
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3023
+ wait,
3024
+ };
3025
+ }
3026
+ case "code-env.update-images": {
3027
+ const wait = codeEnvWait(flags);
3028
+ return {
3029
+ method: "POST",
3030
+ endpoint: `${codeEnvEndpoint("/images")}${querySuffix({ envVersion: flags["env-version"], wait, })}`,
3031
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3032
+ wait,
3033
+ };
3034
+ }
3035
+ case "code-env.set-jupyter": {
3036
+ const active = parseBooleanOption(flags["active"], "--active");
3037
+ if (active === undefined) {
3038
+ throw new UsageError(`--active is required. Usage: ${commandUsage("code-env", "set-jupyter")}`);
3039
+ }
3040
+ const wait = codeEnvWait(flags);
3041
+ return {
3042
+ method: "POST",
3043
+ endpoint: `${codeEnvEndpoint("/jupyter")}${querySuffix({ active, wait, })}`,
3044
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3045
+ wait,
3046
+ };
3047
+ }
3048
+ case "code-env.delete": {
3049
+ const wait = codeEnvWait(flags);
3050
+ return {
3051
+ method: "DELETE",
3052
+ endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
3053
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3054
+ wait,
3055
+ };
3056
+ }
3057
+ case "notebook.save-jupyter": {
3058
+ const content = requiredPlanJsonInput(flags, entry.usage);
3059
+ const expectHash = flags["expect-hash"];
3060
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3061
+ throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
3062
+ }
3063
+ const endpoint = projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`);
3064
+ return {
3065
+ exact: false,
3066
+ reason: "A fresh GET selects POST when absent or PUT when present; a confirming GET supplies the persisted hash.",
3067
+ endpoint,
3068
+ identifiers: {
3069
+ name: id,
3070
+ contentHash: stableHash(content),
3071
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3072
+ },
3073
+ requests: [
3074
+ { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
3075
+ {
3076
+ method: "POST|PUT",
3077
+ endpoint,
3078
+ condition: "POST after not_found; otherwise PUT",
3079
+ payload: "<omitted>",
3080
+ redactedFields: ["payload",],
3081
+ },
3082
+ { method: "GET", endpoint, purpose: "confirm persisted hash", },
3083
+ ],
3084
+ };
3085
+ }
3086
+ case "notebook.delete-jupyter":
3087
+ return {
3088
+ method: "DELETE",
3089
+ endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`),
3090
+ identifiers: { name: id, },
3091
+ };
3092
+ case "notebook.clear-jupyter-outputs":
3093
+ return {
3094
+ method: "DELETE",
3095
+ endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}/outputs`),
3096
+ identifiers: { name: id, },
3097
+ };
3098
+ case "notebook.unload-jupyter":
3099
+ if (flags["all"] === true) {
3100
+ if (args.length > 0)
3101
+ throw new UsageError(entry.usage);
3102
+ return {
3103
+ exact: false,
3104
+ reason: "DSS has no unload-all endpoint; apply lists active notebooks and sessions, then issues one verified session DELETE per result.",
3105
+ identifiers: { all: true, },
3106
+ requests: [
3107
+ { method: "GET", endpoint: projectEndpoint("/jupyter-notebooks/?active=true"), },
3108
+ {
3109
+ method: "GET",
3110
+ endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions"),
3111
+ forEach: "active notebook",
3112
+ },
3113
+ {
3114
+ method: "DELETE",
3115
+ endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions/{sessionId}"),
3116
+ forEach: "listed session",
3117
+ },
3118
+ ],
3119
+ };
3120
+ }
3121
+ requireArgs(args, 2, entry.usage);
3122
+ return {
3123
+ method: "DELETE",
3124
+ endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(args[0])}/sessions/${encodeURIComponent(args[1])}`),
3125
+ identifiers: { name: args[0], sessionId: args[1], },
3126
+ };
3127
+ case "notebook.save-sql": {
3128
+ const content = requiredPlanJsonInput(flags, entry.usage);
3129
+ const expectHash = flags["expect-hash"];
3130
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3131
+ throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
3132
+ }
3133
+ const endpoint = projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`);
3134
+ return {
3135
+ exact: false,
3136
+ reason: "A fresh GET selects collection POST when absent or entity PUT when present; a confirming GET supplies the persisted hash.",
3137
+ endpoint,
3138
+ identifiers: {
3139
+ id,
3140
+ contentHash: stableHash(content),
3141
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3142
+ },
3143
+ requests: [
3144
+ { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
3145
+ {
3146
+ method: "POST",
3147
+ endpoint: projectEndpoint("/sql-notebooks/"),
3148
+ condition: "after not_found",
3149
+ payload: "<omitted; includes id and projectKey>",
3150
+ redactedFields: ["payload",],
3151
+ },
3152
+ {
3153
+ method: "PUT",
3154
+ endpoint,
3155
+ condition: "when present",
3156
+ payload: "<omitted>",
3157
+ redactedFields: ["payload",],
3158
+ },
3159
+ { method: "GET", endpoint, purpose: "confirm persisted hash", },
3160
+ ],
3161
+ };
3162
+ }
3163
+ case "notebook.delete-sql":
3164
+ return {
3165
+ method: "DELETE",
3166
+ endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`),
3167
+ identifiers: { id, },
3168
+ };
3169
+ case "notebook.clear-sql-history":
3170
+ return {
3171
+ method: "POST",
3172
+ endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}/history/clear`),
3173
+ identifiers: { id, },
3174
+ payload: {
3175
+ cellId: flags["cell-id"],
3176
+ numRunsToRetain: num(flags["retain"], "--retain"),
3177
+ },
3178
+ };
3179
+ case "project.create": {
3180
+ const settings = jsonInput(flags) ?? null;
3181
+ return {
3182
+ method: "POST",
3183
+ endpoint: "/public/api/projects/",
3184
+ identifiers: { projectKey: args[0], name: args[1], },
3185
+ payload: {
3186
+ projectKey: args[0],
3187
+ name: args[1],
3188
+ owner: flags["owner"] ?? null,
3189
+ settings,
3190
+ description: null,
3191
+ permissions: [],
3192
+ tags: [],
3193
+ },
3194
+ };
3195
+ }
3196
+ case "project.delete": {
3197
+ const expectedProjectIncarnation = flags["expect-project-incarnation"] === undefined
3198
+ ? undefined
3199
+ : requiredPlanFlag(flags, "expect-project-incarnation", entry.usage);
3200
+ if (expectedProjectIncarnation !== undefined
3201
+ && !/^[0-9a-f]{64}$/.test(expectedProjectIncarnation)) {
3202
+ throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
3203
+ }
3204
+ const endpoint = `/public/api/projects/${encodeURIComponent(id)}${querySuffix({
3205
+ clearManagedDatasets: flags["drop-data"] === true,
3206
+ clearOutputManagedFolders: false,
3207
+ clearJobAndScenarioLogs: true,
3208
+ wait: true,
3209
+ })}`;
3210
+ const guarded = flags["if-exists"] === true
3211
+ || executionMode(flags).dryRun
3212
+ || expectedProjectIncarnation !== undefined;
3213
+ if (!guarded) {
3214
+ return {
3215
+ method: "DELETE",
3216
+ endpoint,
3217
+ identifiers: { projectKey: id, },
3218
+ };
3219
+ }
3220
+ const projectProbe = `/public/api/projects/${encodeURIComponent(id)}/`;
3221
+ const incarnationGate = expectedProjectIncarnation === undefined
3222
+ ? { required: false, provided: false, }
3223
+ : {
3224
+ required: true,
3225
+ provided: true,
3226
+ expectedHash: expectedProjectIncarnation,
3227
+ };
3228
+ if (executionMode(flags).dryRun) {
3229
+ return {
3230
+ method: "GET",
3231
+ endpoint: projectProbe,
3232
+ identifiers: {
3233
+ projectKey: id,
3234
+ dryRun: true,
3235
+ ifExists: flags["if-exists"] === true,
3236
+ projectIncarnationGate: incarnationGate,
3237
+ note: "Read-only guarded-delete preflight. No DELETE is issued; a supplied creationTag hash must match.",
3238
+ },
3239
+ };
3240
+ }
3241
+ return {
3242
+ method: "DELETE",
3243
+ endpoint,
3244
+ identifiers: {
3245
+ projectKey: id,
3246
+ ifExists: flags["if-exists"] === true,
3247
+ projectIncarnationGate: incarnationGate,
3248
+ incarnationControl: expectedProjectIncarnation === undefined
3249
+ ? "none"
3250
+ : "client-side-non-atomic-stale-identity-check",
3251
+ preflightRequests: [{
3252
+ method: "GET",
3253
+ endpoint: projectProbe,
3254
+ intent: expectedProjectIncarnation === undefined
3255
+ ? "Confirm existence before a convergent delete."
3256
+ : "Recompute the current project-incarnation hash from creationTag before DELETE.",
3257
+ },],
3258
+ note: expectedProjectIncarnation === undefined
3259
+ ? "Convergent delete: a 404 preflight is an already-absent success only with --if-exists."
3260
+ : "Incarnation-bound delete: the GET must match the expected creationTag hash before an unconditional DELETE. DSS exposes no conditional project DELETE, so the client-side gate cannot serialize against key reuse after the GET.",
3261
+ },
3262
+ };
3263
+ }
3264
+ case "project.duplicate": {
3265
+ const options = jsonInput(flags);
3266
+ return {
3267
+ method: "POST",
3268
+ endpoint: `/public/api/projects/${encodeURIComponent(args[0])}/duplicate/`,
3269
+ identifiers: { sourceKey: args[0], targetKey: args[1], targetName: args[2], },
3270
+ payload: {
3271
+ targetProjectName: args[2],
3272
+ targetProjectKey: args[1],
3273
+ duplicationMode: options?.duplicationMode ?? "MINIMAL",
3274
+ exportAnalysisModels: options?.exportAnalysisModels ?? true,
3275
+ exportSavedModels: options?.exportSavedModels ?? true,
3276
+ exportGitRepository: options?.exportGitRepository ?? null,
3277
+ exportInsightsData: options?.exportInsightsData ?? true,
3278
+ remapping: options?.remapping ?? {},
3279
+ ...(options?.targetProjectFolderId !== undefined
3280
+ ? { targetProjectFolderId: options.targetProjectFolderId, }
3281
+ : {}),
3282
+ },
3283
+ };
3284
+ }
3285
+ case "project.import": {
3286
+ const settings = jsonInput(flags) ?? {};
3287
+ const rawTarget = flags["target-project-key"];
3288
+ const targetProjectKey = rawTarget?.trim();
3289
+ if (rawTarget !== undefined && targetProjectKey === "") {
3290
+ throw new UsageError(`--target-project-key must not be empty. Usage: ${entry.usage}`);
3291
+ }
3292
+ const settingsTarget = settings.targetProjectKey;
3293
+ if (settingsTarget !== undefined
3294
+ && (typeof settingsTarget !== "string" || settingsTarget.trim() === "")) {
3295
+ throw new UsageError(`targetProjectKey in import settings must be a non-empty string. Usage: ${entry.usage}`);
3296
+ }
3297
+ if (targetProjectKey !== undefined
3298
+ && settingsTarget !== undefined
3299
+ && targetProjectKey !== settingsTarget.trim()) {
3300
+ throw new UsageError(`--target-project-key conflicts with targetProjectKey in import settings. Usage: ${entry.usage}`);
3301
+ }
3302
+ const processPayload = targetProjectKey === undefined
3303
+ ? settings
3304
+ : { ...settings, targetProjectKey, };
3305
+ const finalizedPayload = Object.keys(processPayload).length === 0
3306
+ ? { _: "_", }
3307
+ : processPayload;
3308
+ const upload = {
3309
+ method: "POST",
3310
+ endpoint: "/public/api/projects/import/upload",
3311
+ payload: {
3312
+ contentType: "multipart/form-data",
3313
+ fileField: "file",
3314
+ filePath: id,
3315
+ fileName: "tmp-import.zip",
3316
+ },
3317
+ };
3318
+ return {
3319
+ ...upload,
3320
+ identifiers: {
3321
+ filePath: id,
3322
+ ...(targetProjectKey ? { targetProjectKey, } : {}),
3323
+ archivePreflight: {
3324
+ local: true,
3325
+ required: true,
3326
+ checks: [
3327
+ "zip-integrity",
3328
+ "safe-unique-members",
3329
+ "manifest",
3330
+ "flow-references",
3331
+ "orphan-members",
3332
+ ],
3333
+ },
3334
+ successVerification: {
3335
+ usedProjectKeyRequired: true,
3336
+ projectReadRequired: true,
3337
+ projectIncarnationRequired: true,
3338
+ remappingReported: true,
3339
+ },
3340
+ ...(flags["record-cleanup"] === undefined
3341
+ ? {}
3342
+ : {
3343
+ cleanupBinding: "actual used project key plus verified creationTag incarnation hash",
3344
+ }),
3345
+ },
3346
+ requests: [
3347
+ { sequence: 1, ...upload, },
3348
+ {
3349
+ sequence: 2,
3350
+ method: "POST",
3351
+ endpoint: "/public/api/projects/import/{importId}/process",
3352
+ pathBindings: { importId: "requests[0].response.id", },
3353
+ payload: finalizedPayload,
3354
+ },
3355
+ ],
3356
+ };
3357
+ }
3358
+ case "project.settings-set":
3359
+ return {
3360
+ method: "PUT",
3361
+ endpoint: projectEndpoint("/settings"),
3362
+ payload: requiredPlanJsonInput(flags, entry.usage),
3363
+ };
3364
+ case "project.permissions-set":
3365
+ return {
3366
+ method: "PUT",
3367
+ endpoint: projectEndpoint("/permissions"),
3368
+ payload: requiredPlanJsonInput(flags, entry.usage),
3369
+ };
3370
+ case "continuous-activity.start":
3371
+ return {
3372
+ method: "POST",
3373
+ endpoint: projectEndpoint(`/continuous-activities/${encodeURIComponent(id)}/start`),
3374
+ identifiers: { recipeId: id, },
3375
+ payload: jsonInput(flags) ?? {},
3376
+ };
3377
+ case "continuous-activity.stop":
3378
+ return {
3379
+ method: "POST",
3380
+ endpoint: projectEndpoint(`/continuous-activities/${encodeURIComponent(id)}/stop`),
3381
+ identifiers: { recipeId: id, },
3382
+ };
3383
+ case "metrics.dataset-compute":
3384
+ return {
3385
+ method: "POST",
3386
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/actions/computeMetrics?partition=`),
3387
+ identifiers: { dataset: id, },
3388
+ };
3389
+ case "flow-zone.organize":
3390
+ return {
3391
+ method: "POST",
3392
+ endpoint: projectEndpoint("/flow/zones"),
3393
+ payload: jsonInput(flags),
3394
+ };
3395
+ /* ---- project-git: mutations only; reads are never planned ---- */
3396
+ case "project-git.set-remote": {
3397
+ const remote = flags["name"] ?? "origin";
3398
+ return {
3399
+ method: "POST",
3400
+ endpoint: projectGitEndpoint(projectKey, `/remotes/${encodeURIComponent(remote)}`),
3401
+ identifiers: { remote, },
3402
+ payload: { url: requiredPlanRepositoryUrl(flags, "repository", entry.usage), },
3403
+ };
3404
+ }
3405
+ case "project-git.remove-remote": {
3406
+ const remote = flags["name"] ?? "origin";
3407
+ return {
3408
+ method: "DELETE",
3409
+ endpoint: projectGitEndpoint(projectKey, `/remotes/${encodeURIComponent(remote)}`),
3410
+ identifiers: { remote, },
3411
+ };
3412
+ }
3413
+ case "project-git.create-branch":
3414
+ return {
3415
+ method: "POST",
3416
+ endpoint: projectGitEndpoint(projectKey, "/branches/"),
3417
+ identifiers: { name: id, },
3418
+ payload: {
3419
+ name: id,
3420
+ commit: flags["commit"] ?? null,
3421
+ duplicateProject: parseBooleanOption(flags["duplicate-project"], "--duplicate-project") ?? false,
3422
+ targetProjectKey: flags["target-project-key"] ?? null,
3423
+ targetProjectFolderId: flags["target-project-folder-id"] ?? null,
3424
+ },
3425
+ };
3426
+ case "project-git.delete-branch": {
3427
+ const remote = parseBooleanOption(flags["remote"], "--remote") ?? false;
3428
+ return {
3429
+ method: "POST",
3430
+ endpoint: projectGitEndpoint(projectKey, "/actions/deleteBranch"),
3431
+ identifiers: { name: id, remote, },
3432
+ payload: {
3433
+ name: id,
3434
+ remote,
3435
+ deleteRemotely: parseBooleanOption(flags["delete-remotely"], "--delete-remotely") ?? false,
3436
+ forceDelete: parseBooleanOption(flags["force-delete"], "--force-delete") ?? false,
3437
+ },
3438
+ };
3439
+ }
3440
+ case "project-git.switch":
3441
+ return {
3442
+ method: "POST",
3443
+ endpoint: projectGitEndpoint(projectKey, `/actions/switchBranch${querySuffix({ branchName: id, })}`),
3444
+ identifiers: { branch: id, },
3445
+ };
3446
+ case "project-git.create-tag":
3447
+ return {
3448
+ method: "POST",
3449
+ endpoint: projectGitEndpoint(projectKey, "/tags/"),
3450
+ identifiers: { name: id, },
3451
+ payload: {
3452
+ name: id,
3453
+ reference: flags["reference"] ?? "HEAD",
3454
+ message: flags["message"] ?? "",
3455
+ },
3456
+ };
3457
+ case "project-git.delete-tag":
3458
+ return {
3459
+ method: "POST",
3460
+ endpoint: projectGitEndpoint(projectKey, "/actions/deleteTag"),
3461
+ identifiers: { name: id, },
3462
+ payload: { name: id, },
3463
+ };
3464
+ case "project-git.fetch":
3465
+ return {
3466
+ method: "POST",
3467
+ endpoint: projectGitEndpoint(projectKey, "/actions/fetch"),
3468
+ };
3469
+ case "project-git.pull":
3470
+ return {
3471
+ method: "POST",
3472
+ endpoint: projectGitEndpoint(projectKey, `/actions/pullRebase${querySuffix({
3473
+ branchName: flags["branch"],
3474
+ })}`),
3475
+ ...(flags["branch"] !== undefined
3476
+ ? { identifiers: { branch: flags["branch"], }, }
3477
+ : {}),
3478
+ };
3479
+ case "project-git.push":
3480
+ return {
3481
+ method: "POST",
3482
+ endpoint: projectGitEndpoint(projectKey, `/actions/push${querySuffix({
3483
+ branchName: flags["branch"],
3484
+ })}`),
3485
+ ...(flags["branch"] !== undefined
3486
+ ? { identifiers: { branch: flags["branch"], }, }
3487
+ : {}),
3488
+ };
3489
+ case "project-git.commit": {
3490
+ const message = requiredPlanFlag(flags, "message", entry.usage);
3491
+ return {
3492
+ method: "POST",
3493
+ endpoint: projectGitEndpoint(projectKey, "/actions/commit"),
3494
+ identifiers: { message, },
3495
+ payload: { message, },
3496
+ };
3497
+ }
3498
+ case "project-git.revert-to-revision":
3499
+ return {
3500
+ method: "POST",
3501
+ endpoint: projectGitEndpoint(projectKey, `/actions/revertToRevision${querySuffix({ commit: id, })}`),
3502
+ identifiers: { commit: id, },
3503
+ };
3504
+ case "project-git.revert-commit":
3505
+ return {
3506
+ method: "POST",
3507
+ endpoint: projectGitEndpoint(projectKey, `/actions/revertCommit${querySuffix({ commit: id, })}`),
3508
+ identifiers: { commit: id, },
3509
+ };
3510
+ case "project-git.reset-to-head":
3511
+ return {
3512
+ method: "POST",
3513
+ endpoint: projectGitEndpoint(projectKey, "/actions/resetToLocalHeadState"),
3514
+ };
3515
+ case "project-git.reset-to-upstream":
3516
+ return {
3517
+ method: "POST",
3518
+ endpoint: projectGitEndpoint(projectKey, "/actions/resetToRemoteHeadState"),
3519
+ };
3520
+ case "project-git.drop-and-rebuild": {
3521
+ if (parseBooleanOption(flags["i-know-what-i-am-doing"], "--i-know-what-i-am-doing") !== true) {
3522
+ throw new UsageError(`--i-know-what-i-am-doing is required to acknowledge the irreversible Git history loss. Usage: ${entry.usage}`);
3523
+ }
3524
+ return {
3525
+ method: "POST",
3526
+ endpoint: projectGitEndpoint(projectKey, `/actions/dropAndRebuild${querySuffix({ iKnowWhatIAmDoing: true, })}`),
3527
+ };
3528
+ }
3529
+ case "project-git.add-library": {
3530
+ const targetPath = id;
3531
+ return {
3532
+ method: "POST",
3533
+ endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/"),
3534
+ identifiers: { localTargetPath: targetPath, },
3535
+ payload: {
3536
+ repository: requiredPlanRepositoryUrl(flags, "repository", entry.usage),
3537
+ login: flags["login"] ?? null,
3538
+ password: flags["password-env"] !== undefined ? "***" : null,
3539
+ pathInGitRepository: flags["path-in-repository"] ?? "",
3540
+ localTargetPath: targetPath,
3541
+ checkout: requiredPlanFlag(flags, "checkout", entry.usage),
3542
+ addToPythonPath: parseBooleanOption(flags["no-add-to-python-path"], "--no-add-to-python-path") !== true,
3543
+ },
3544
+ wait: projectGitFutureWait(),
3545
+ };
3546
+ }
3547
+ case "project-git.set-library": {
3548
+ const targetPath = validateGitReferencePath(id);
3549
+ return {
3550
+ method: "PUT",
3551
+ endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}`),
3552
+ identifiers: { library: targetPath, },
3553
+ payload: {
3554
+ repository: requiredPlanRepositoryUrl(flags, "repository", entry.usage),
3555
+ login: flags["login"] ?? null,
3556
+ password: flags["password-env"] !== undefined ? "***" : null,
3557
+ pathInGitRepository: flags["path-in-repository"] ?? "",
3558
+ checkout: requiredPlanFlag(flags, "checkout", entry.usage),
3559
+ },
3560
+ };
3561
+ }
3562
+ case "project-git.remove-library": {
3563
+ const targetPath = validateGitReferencePath(id);
3564
+ return {
3565
+ method: "DELETE",
3566
+ endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}${querySuffix({
3567
+ deleteDirectory: parseBooleanOption(flags["delete-directory"], "--delete-directory") ?? false,
3568
+ })}`),
3569
+ identifiers: { library: targetPath, },
3570
+ };
3571
+ }
3572
+ case "project-git.reset-library": {
3573
+ const targetPath = validateGitReferencePath(id);
3574
+ return {
3575
+ method: "POST",
3576
+ endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/reset"),
3577
+ identifiers: { library: targetPath, },
3578
+ payload: { gitRef: targetPath, },
3579
+ wait: projectGitFutureWait(),
3580
+ };
3581
+ }
3582
+ case "project-git.push-library": {
3583
+ const targetPath = validateGitReferencePath(id);
3584
+ const message = requiredPlanFlag(flags, "message", entry.usage);
3585
+ return {
3586
+ method: "POST",
3587
+ endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/push"),
3588
+ identifiers: { library: targetPath, message, },
3589
+ payload: { gitRef: targetPath, commitMessage: message, },
3590
+ wait: projectGitFutureWait(),
3591
+ };
3592
+ }
3593
+ case "project-git.push-all-libraries": {
3594
+ const message = requiredPlanFlag(flags, "message", entry.usage);
3595
+ return {
3596
+ method: "POST",
3597
+ endpoint: projectGitEndpoint(projectKey, "/actions/git-refs/push-all"),
3598
+ identifiers: { message, },
3599
+ payload: { commitMessage: message, },
3600
+ wait: projectGitFutureWait(),
3601
+ };
3602
+ }
3603
+ case "project-git.reset-all-libraries":
3604
+ return {
3605
+ method: "POST",
3606
+ endpoint: projectGitEndpoint(projectKey, "/actions/git-refs/reset-all"),
3607
+ wait: projectGitFutureWait(),
3608
+ };
3609
+ case "project-git.future-abort":
3610
+ return {
3611
+ method: "DELETE",
3612
+ endpoint: projectGitFutureEndpoint(id),
3613
+ identifiers: { jobId: id, },
3614
+ };
3615
+ case "project-folder.settings-set": {
3616
+ const payload = requiredPlanJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${entry.usage}`);
3617
+ return {
3618
+ method: "PUT",
3619
+ endpoint: `${projectFolderEndpoint(id)}/settings`,
3620
+ identifiers: { folderId: id, },
3621
+ payload,
3622
+ };
3623
+ }
3624
+ case "project-folder.move": {
3625
+ const destination = args[1];
3626
+ if (!destination)
3627
+ throw new UsageError(`Usage: ${entry.usage}`);
3628
+ return {
3629
+ method: "POST",
3630
+ endpoint: `${projectFolderEndpoint(id)}/move${querySuffix({ destination, })}`,
3631
+ identifiers: { folderId: id, destination, },
3632
+ };
3633
+ }
3634
+ case "project-folder.delete":
3635
+ return {
3636
+ method: "DELETE",
3637
+ endpoint: projectFolderEndpoint(id),
3638
+ identifiers: { folderId: id, },
3639
+ };
3640
+ case "project-folder.create-child": {
3641
+ const name = requiredPlanFlag(flags, "name", entry.usage);
3642
+ return {
3643
+ method: "POST",
3644
+ endpoint: `${projectFolderEndpoint(id)}/children${querySuffix({ name, })}`,
3645
+ identifiers: { parentFolderId: id, name, },
3646
+ };
3647
+ }
3648
+ case "project-folder.move-project": {
3649
+ const projectKeyArg = args[1];
3650
+ const destination = args[2];
3651
+ if (!projectKeyArg || !destination) {
3652
+ throw new UsageError(`Usage: ${entry.usage}`);
3653
+ }
3654
+ return {
3655
+ method: "POST",
3656
+ endpoint: `${projectFolderEndpoint(id)}/projects/${encodeURIComponent(projectKeyArg)}/move${querySuffix({ destination, })}`,
3657
+ identifiers: { folderId: id, projectKey: projectKeyArg, destination, },
3658
+ };
3659
+ }
3660
+ default:
3661
+ return {
3662
+ exact: false,
3663
+ reason: `No exact offline request shape is defined for ${resource}.${action}; no endpoint was guessed.`,
3664
+ identifiers: id ? { id, } : undefined,
3665
+ };
3666
+ }
3667
+ }
3668
+ export function buildMutationPlan(resource, action, meta, args, flags) {
3669
+ const entry = buildRegistryEntry(resource, action, meta);
3670
+ if (!entry.mutatesDss && entry.sideEffect !== "write") {
3671
+ throw new UsageError(`--plan is only supported for mutating commands. Usage: ${meta.usage}`);
3672
+ }
3673
+ const requiredPositionals = entry.positionalArguments.filter((positional) => positional.required);
3674
+ requireArgs(args, requiredPositionals.length, meta.usage);
3675
+ const projectKey = projectKeyForPlan(entry, flags);
3676
+ const shape = commandPlanShape(resource, action, args, flags, entry, projectKey);
3677
+ return planResult(resource, action, {
3678
+ ...shape,
3679
+ asyncKind: entry.async,
3680
+ exitCodesOnFailure: exitCodesOnFailure(entry),
3681
+ idempotency: entry.idempotency,
3682
+ plannedAndDryRun: executionMode(flags).dryRun,
3683
+ });
3684
+ }