dataiku-sdk 3.6.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +14 -6
  2. package/bin/bun-version.js +14 -0
  3. package/bin/dss.js +11 -15
  4. package/dist/build-metadata.json +1 -1
  5. package/dist/packages/types/src/index.d.ts +114 -5
  6. package/dist/packages/types/src/index.js +75 -2
  7. package/dist/src/cli/coerce.d.ts +2 -0
  8. package/dist/src/cli/coerce.js +20 -16
  9. package/dist/src/cli/command-syntax.json +3369 -144
  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 +76 -9
  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-zone.js +241 -46
  18. package/dist/src/cli/commands/flow.d.ts +5 -0
  19. package/dist/src/cli/commands/flow.js +57 -0
  20. package/dist/src/cli/commands/future.js +8 -0
  21. package/dist/src/cli/commands/index.js +6 -0
  22. package/dist/src/cli/commands/job.js +44 -6
  23. package/dist/src/cli/commands/metrics.js +21 -1
  24. package/dist/src/cli/commands/ml-task.d.ts +5 -0
  25. package/dist/src/cli/commands/ml-task.js +86 -16
  26. package/dist/src/cli/commands/mlflow.d.ts +11 -0
  27. package/dist/src/cli/commands/mlflow.js +121 -0
  28. package/dist/src/cli/commands/plugin.js +5 -11
  29. package/dist/src/cli/commands/project-deployer.js +70 -0
  30. package/dist/src/cli/commands/project-library.js +2 -5
  31. package/dist/src/cli/commands/project.js +8 -0
  32. package/dist/src/cli/commands/recipe.d.ts +13 -0
  33. package/dist/src/cli/commands/recipe.js +130 -7
  34. package/dist/src/cli/commands/saved-model.d.ts +1 -1
  35. package/dist/src/cli/commands/saved-model.js +9 -76
  36. package/dist/src/cli/commands/streaming-endpoint.js +30 -1
  37. package/dist/src/cli/commands/webapp.js +8 -0
  38. package/dist/src/cli/commands/wiki.js +78 -0
  39. package/dist/src/cli/commands/workspace.js +8 -0
  40. package/dist/src/cli/contract.d.ts +16 -15
  41. package/dist/src/cli/contract.js +150 -3407
  42. package/dist/src/cli/doctor.d.ts +13 -0
  43. package/dist/src/cli/doctor.js +43 -1
  44. package/dist/src/cli/flags.d.ts +14 -1
  45. package/dist/src/cli/flags.js +106 -8
  46. package/dist/src/cli/helpers/app-successor.js +1 -2
  47. package/dist/src/cli/helpers/dataset.d.ts +12 -0
  48. package/dist/src/cli/helpers/dataset.js +36 -0
  49. package/dist/src/cli/helpers/documentation.d.ts +21 -0
  50. package/dist/src/cli/helpers/documentation.js +53 -0
  51. package/dist/src/cli/helpers/flow-zone.d.ts +21 -0
  52. package/dist/src/cli/helpers/flow-zone.js +144 -4
  53. package/dist/src/cli/helpers/trained-model.d.ts +26 -0
  54. package/dist/src/cli/helpers/trained-model.js +144 -0
  55. package/dist/src/cli/list-output.d.ts +7 -0
  56. package/dist/src/cli/list-output.js +77 -0
  57. package/dist/src/cli/list-shapes.d.ts +21 -0
  58. package/dist/src/cli/list-shapes.js +111 -0
  59. package/dist/src/cli/output-schemas.d.ts +0 -14
  60. package/dist/src/cli/output-schemas.js +4 -7
  61. package/dist/src/cli/output.d.ts +0 -1
  62. package/dist/src/cli/output.js +33 -19
  63. package/dist/src/cli/plans.d.ts +14 -0
  64. package/dist/src/cli/plans.js +3740 -0
  65. package/dist/src/cli/types.d.ts +2 -0
  66. package/dist/src/cli/usage.d.ts +7 -0
  67. package/dist/src/cli/usage.js +20 -1
  68. package/dist/src/cli/version.d.ts +2 -2
  69. package/dist/src/cli/version.js +2 -2
  70. package/dist/src/cli.js +137 -115
  71. package/dist/src/client.d.ts +52 -43
  72. package/dist/src/client.js +267 -96
  73. package/dist/src/errors.d.ts +22 -2
  74. package/dist/src/errors.js +49 -50
  75. package/dist/src/generated/action-output-schemas.json +345 -89
  76. package/dist/src/index.d.ts +5 -1
  77. package/dist/src/index.js +3 -0
  78. package/dist/src/resources/analyses.d.ts +8 -0
  79. package/dist/src/resources/analyses.js +13 -1
  80. package/dist/src/resources/bundles.d.ts +34 -0
  81. package/dist/src/resources/bundles.js +67 -0
  82. package/dist/src/resources/connections.d.ts +9 -0
  83. package/dist/src/resources/connections.js +15 -0
  84. package/dist/src/resources/data-quality.d.ts +20 -0
  85. package/dist/src/resources/data-quality.js +30 -0
  86. package/dist/src/resources/dataset-create.d.ts +38 -0
  87. package/dist/src/resources/dataset-create.js +135 -0
  88. package/dist/src/resources/datasets.d.ts +46 -15
  89. package/dist/src/resources/datasets.js +120 -112
  90. package/dist/src/resources/discussions.d.ts +5 -0
  91. package/dist/src/resources/discussions.js +7 -0
  92. package/dist/src/resources/documentation.d.ts +25 -0
  93. package/dist/src/resources/documentation.js +16 -0
  94. package/dist/src/resources/flow-zones.d.ts +29 -3
  95. package/dist/src/resources/flow-zones.js +53 -3
  96. package/dist/src/resources/flow.d.ts +48 -0
  97. package/dist/src/resources/flow.js +65 -0
  98. package/dist/src/resources/futures.d.ts +23 -0
  99. package/dist/src/resources/futures.js +32 -0
  100. package/dist/src/resources/jobs.d.ts +34 -0
  101. package/dist/src/resources/jobs.js +170 -3
  102. package/dist/src/resources/metrics.d.ts +10 -0
  103. package/dist/src/resources/metrics.js +10 -0
  104. package/dist/src/resources/ml-tasks.d.ts +65 -2
  105. package/dist/src/resources/ml-tasks.js +102 -28
  106. package/dist/src/resources/mlflow-extension.d.ts +73 -0
  107. package/dist/src/resources/mlflow-extension.js +105 -0
  108. package/dist/src/resources/projects.d.ts +15 -0
  109. package/dist/src/resources/projects.js +18 -3
  110. package/dist/src/resources/recipe-create.d.ts +79 -0
  111. package/dist/src/resources/recipe-create.js +454 -0
  112. package/dist/src/resources/recipes.d.ts +28 -29
  113. package/dist/src/resources/recipes.js +201 -267
  114. package/dist/src/resources/saved-models.d.ts +10 -6
  115. package/dist/src/resources/saved-models.js +16 -14
  116. package/dist/src/resources/scenarios.js +8 -14
  117. package/dist/src/resources/sql.js +52 -13
  118. package/dist/src/resources/streaming-endpoints.d.ts +12 -0
  119. package/dist/src/resources/streaming-endpoints.js +15 -0
  120. package/dist/src/resources/trained-model.d.ts +60 -0
  121. package/dist/src/resources/trained-model.js +98 -0
  122. package/dist/src/resources/webapps.d.ts +8 -0
  123. package/dist/src/resources/webapps.js +10 -0
  124. package/dist/src/resources/wiki.d.ts +52 -1
  125. package/dist/src/resources/wiki.js +96 -0
  126. package/dist/src/resources/workspaces.d.ts +2 -0
  127. package/dist/src/resources/workspaces.js +4 -0
  128. package/dist/src/utils/flow-analysis.js +4 -0
  129. package/dist/src/utils/response-file.js +20 -9
  130. package/dist/src/utils/secret-sanitize.d.ts +7 -0
  131. package/dist/src/utils/secret-sanitize.js +46 -0
  132. package/package.json +25 -26
  133. package/plugin.json +1 -1
  134. package/skills/dataiku-dss/SKILL.md +1 -1
  135. package/skills/dataiku-dss/references/authentication.md +3 -1
  136. package/skills/dataiku-dss/references/coding.md +1 -0
  137. package/skills/dataiku-dss/references/discovery.md +4 -6
  138. package/skills/dataiku-dss/references/flow-maps.md +2 -2
  139. package/skills/dataiku-dss/references/troubleshooting.md +3 -2
@@ -1,74 +1,17 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
1
  import { validateCredentials, } from "../auth.js";
4
2
  import { getCredentialsPath, saveCredentials, } from "../config.js";
5
3
  import { DataikuError, } from "../errors.js";
6
4
  import actionOutputSchemasMetadata from "../generated/action-output-schemas.json" with { type: "json"
7
5
  };
8
- import { APP_MANIFEST_CONCURRENCY_CONTROL, } from "../resources/applications.js";
9
- import { buildDatasetCreateBody, } from "../resources/datasets.js";
10
- import { validatePluginDestinationPath, validatePluginPath, } from "../resources/plugins.js";
11
- import { encodeLibraryPath, PROJECT_LIBRARY_CONCURRENCY_CONTROL, validateLibraryDestinationPath, validateLibraryName, validateLibraryPath, } from "../resources/project-library.js";
12
- import { buildRecipeCreateRequest, } from "../resources/recipes.js";
13
- import { encodeGitReferencePath, validateGitReferencePath, } from "../utils/git-reference.js";
14
- import { jobBuildTargetTypeFromFlags, json, jsonInput, num, parseBooleanOption, parseJsonObject, requiredJsonInput, rewritePairsFromFlags, schemaColumnsInput, sha256Hex, stableHash, stringField, textInput, } from "./coerce.js";
15
- import { parseCodeRunIntegerFlag, resolveCodeInputWithSource, } from "./commands/code.js";
16
6
  import { commands, } from "./commands/index.js";
17
- import { projectLibraryPutPayload, } from "./commands/project-library.js";
18
- import { recipeCreateOptionsFromFlags, } from "./commands/recipe.js";
19
- import { resolveSqlQueryInvocation, } from "./commands/sql.js";
20
- import { ambientProjectKey, } from "./env.js";
21
- import { BOOLEAN_FLAGS, executionMode, FLAG_ALIASES, SHORT_FLAGS, } from "./flags.js";
22
- import { flowZoneColor, flowZoneMoveItems, flowZoneName, } from "./helpers/flow-zone.js";
23
- import { recipeBackupPath, recipeRunShouldWait, } from "./helpers/recipe.js";
24
- import { encodedProjectEndpointForPlan, planResult, } from "./output.js";
7
+ import { BOOLEAN_FLAGS, FLAG_ALIASES, SHORT_FLAGS, } from "./flags.js";
25
8
  import { resolveLoginCredentials, } from "./runtime.js";
26
- import { commandSyntax, commandSyntaxTree, commandUsage, renderNodes, syntaxCommandWords, syntaxHasFlag, } from "./syntax.js";
27
- import { inferRequiresProject, requireArgs, UsageError, } from "./usage.js";
9
+ import { commandSyntax, commandSyntaxKeys, commandSyntaxTree, commandUsage, renderNodes, syntaxCommandWords, syntaxHasFlag, } from "./syntax.js";
10
+ import { inferRequiresProject, UsageError, } from "./usage.js";
28
11
  import { AGENT_CONTRACT_SCHEMA_ID, AGENT_CONTRACT_VERSION, cliVersionResult, JSON_SCHEMA_DRAFT, } from "./version.js";
29
- function codeEnvWait(flags) {
30
- return flags["no-wait"] !== true;
31
- }
32
- function codeEnvLang(value, usage) {
33
- if (value !== "PYTHON" && value !== "R") {
34
- throw new UsageError(`Invalid code environment language ${JSON.stringify(value)}. Usage: ${usage}`);
35
- }
36
- return value;
37
- }
38
- function codeEnvParams(flags) {
39
- const params = json(flags["params"]) ?? jsonInput(flags) ?? {};
40
- if (typeof flags["python-interpreter"] === "string") {
41
- params.pythonInterpreter = flags["python-interpreter"];
42
- }
43
- return params;
44
- }
45
- function splitPackageSpec(raw) {
12
+ export function splitPackageSpec(raw) {
46
13
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
47
14
  }
48
- /**
49
- * Resolve requested package specs from --file/--packages/--package. An
50
- * explicit source that resolves to zero specs is a legitimate clear
51
- * (set-packages replaces specPackageList wholesale); only a call with no
52
- * package source flag at all is a usage error.
53
- */
54
- function codeEnvPackageList(flags) {
55
- const file = flags["file"];
56
- const packages = typeof file === "string"
57
- ? splitPackageSpec(fs.readFileSync(file, "utf-8"))
58
- : [];
59
- if (typeof flags["packages"] === "string") {
60
- packages.push(...splitPackageSpec(flags["packages"]));
61
- }
62
- if (typeof flags["package"] === "string") {
63
- packages.push(...splitPackageSpec(flags["package"]));
64
- }
65
- if (typeof file !== "string"
66
- && typeof flags["packages"] !== "string"
67
- && typeof flags["package"] !== "string") {
68
- 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.");
69
- }
70
- return packages;
71
- }
72
15
  export const AUTH_ACTIONS = {
73
16
  login: {
74
17
  handler: async (flags) => {
@@ -242,10 +185,12 @@ const AUTHENTICATED_AGENT_FLAGS = [
242
185
  "insecure",
243
186
  "ca-cert",
244
187
  ];
188
+ const GLOBAL_FLAG_NAMES = Object.fromEntries([...GLOBAL_AGENT_FLAGS, ...AUTHENTICATED_AGENT_FLAGS,].map((flag) => [flag, true,]));
245
189
  export const COMMANDS_USAGE = commandUsage("commands", "run");
246
- const COMMANDS_DESCRIPTION = "Print a compact resource/action summary by default; use --fields for scoped command metadata or --output PATH to export the full registry without sending it through stdout.";
190
+ const COMMANDS_DESCRIPTION = "Print a compact resource/action summary by default; --contains TEXT finds commands by concept (top 5 of id/usage/description matches), --fields gives scoped command metadata, and --output PATH exports the full registry without sending it through stdout.";
247
191
  const COMMANDS_EXAMPLES = [
248
192
  "dss commands run",
193
+ 'dss commands run --contains "propagate schema"',
249
194
  "dss commands run --fields dataset",
250
195
  "dss commands run --fields dataset.create",
251
196
  "dss commands run --fields dataset.create.usage,dataset.create.description,dataset.create.flags,dataset.create.examples",
@@ -426,16 +371,18 @@ function canonicalizeArgv(argv) {
426
371
  }
427
372
  return canonical;
428
373
  }
374
+ /** Machine forms of `examples`: canonical argv arrays, plus the example payload for --data examples. */
429
375
  function structuredExamples(examples, examplePayload) {
430
- return (examples ?? []).map((shell) => {
376
+ return (examples ?? []).flatMap((shell) => {
431
377
  const argv = exampleArgv(shell);
432
- return {
433
- shell,
434
- ...(argv ? { argv, } : {}),
435
- ...(examplePayload !== undefined && shell.includes("--data")
436
- ? { payload: examplePayload, }
437
- : {}),
438
- };
378
+ return argv
379
+ ? [{
380
+ argv,
381
+ ...(examplePayload !== undefined && shell.includes("--data")
382
+ ? { payload: examplePayload, }
383
+ : {}),
384
+ },]
385
+ : [];
439
386
  });
440
387
  }
441
388
  /**
@@ -464,6 +411,8 @@ function payloadJsonSchema(payloadSchema) {
464
411
  if (payloadSchema.contentType === "text/plain") {
465
412
  return { type: "string", contentMediaType: "text/plain", };
466
413
  }
414
+ if (payloadSchema.jsonSchema)
415
+ return payloadSchema.jsonSchema;
467
416
  return payloadSchema.jsonShape === "array"
468
417
  ? { type: "array", items: true, }
469
418
  : { type: "object", additionalProperties: true, };
@@ -528,8 +477,7 @@ function argvJsonSchema(resource, action, flags, usage, requiredFlags, requiredO
528
477
  for (const flag of flags) {
529
478
  aliases[flag.name] = flag.aliases ?? [];
530
479
  }
531
- const booleanNames = flags.filter((flag) => flag.kind === "boolean")
532
- .flatMap((flag) => [flag.name, ...(flag.aliases ?? []),]);
480
+ const booleanNames = flags.flatMap((flag) => flag.kind === "boolean" ? [flag.name, ...(flag.aliases ?? []),] : []);
533
481
  const valueFlags = flags.filter((flag) => flag.kind === "value");
534
482
  const requiredPositionals = commandSyntax(resource, action).positionalArguments
535
483
  .filter((positional) => positional.required).map((positional) => positional.name);
@@ -731,6 +679,26 @@ function inferSideEffect(resource, action) {
731
679
  // Saved-model evaluation triggers scoring work on DSS side (cost-bearing).
732
680
  if (resource === "saved-model" && action === "evaluate-version")
733
681
  return "write";
682
+ // Flow-wide schema propagation rewrites downstream schemas and may rebuild
683
+ // datasets; `propagate` matches no mutating verb pattern.
684
+ if (resource === "flow" && action === "propagate-schema")
685
+ return "write";
686
+ // Hive syncs rewrite the Hive table or the dataset; running checks records outcomes.
687
+ if (resource === "dataset" && action === "sync-hive-metastore")
688
+ return "write";
689
+ if (resource === "metrics" && action === "dataset-run-checks")
690
+ return "write";
691
+ if (resource === "webapp" && action === "trust")
692
+ return "write";
693
+ if (resource === "project" && action === "push-to-git-remote")
694
+ return "write";
695
+ if (resource === "mlflow" && (action === "garbage-collect" || action === "clean-db")) {
696
+ return "write";
697
+ }
698
+ // Re-guessing rewrites the ML task settings.
699
+ if (resource === "ml-task" && (action === "reguess" || action === "reguess-forecasting")) {
700
+ return "write";
701
+ }
734
702
  // LLM Mesh completions/embeddings invoke the LLM provider (cost-bearing,
735
703
  // arbitrary-prompt execution); the verb shapes match no mutating pattern
736
704
  // and would fall through to read, wrongly rejecting --plan. Knowledge-bank
@@ -756,7 +724,7 @@ function inferSideEffect(resource, action) {
756
724
  }
757
725
  if (READ_ACTIONS.has(action))
758
726
  return "read";
759
- if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove|publish|activate|deploy|import|export|preload|upgrade|start|stop|restart|duplicate|put|rename|reply|compute|organize)/
727
+ if (/^(create|clone|restore|update|delete|set|save|upload|run|build|abort|move|refresh|clear|unload|install|login|logout|add|remove|publish|activate|deploy|import|export|preload|upgrade|start|stop|restart|duplicate|put|rename|reply|compute|organize|attach|detach)/
760
728
  .test(action)
761
729
  // Compound actions whose mutating verb is a suffix (e.g. permissions-set,
762
730
  // dataset-compute, saved-model external-metadata-put).
@@ -910,6 +878,8 @@ export function supportsCleanupLedger(resource, action) {
910
878
  const EXPLICIT_DESTRUCTIVE_KEYS = {
911
879
  "dataset.upload-file": true,
912
880
  "sql.query": true,
881
+ // Detaching drops the attachment from the article; no public route restores it.
882
+ "wiki.detach": true,
913
883
  // Plugin dev-Git and content deletions discard work with no undo route.
914
884
  "plugin.delete": true,
915
885
  "plugin.delete-git-remote": true,
@@ -924,6 +894,9 @@ const EXPLICIT_DESTRUCTIVE_KEYS = {
924
894
  "macro.run-and-wait": true,
925
895
  // Saved-model version deletion removes model versions permanently.
926
896
  "saved-model.delete-versions": true,
897
+ // MLflow cleanup permanently erases experiment-tracking data.
898
+ "mlflow.garbage-collect": true,
899
+ "mlflow.clean-db": true,
927
900
  };
928
901
  function inferDestructiveLevel(resource, sideEffect, action) {
929
902
  if (sideEffect !== "write")
@@ -967,6 +940,14 @@ function inferAsyncKind(resource, action) {
967
940
  return "future";
968
941
  if (resource === "webapp" && action === "restart-backend")
969
942
  return "future";
943
+ if (resource === "flow" && (action === "propagate-schema" || action === "generate-documentation")) {
944
+ return "future";
945
+ }
946
+ // Trained-model diagnostics and documentation start DSS futures.
947
+ if ((resource === "ml-task" || resource === "saved-model")
948
+ && (action === "compute-diagnostics" || action === "generate-documentation")) {
949
+ return "future";
950
+ }
970
951
  if (resource === "code-env"
971
952
  && ["create", "update-packages", "update-images", "set-jupyter", "delete",].includes(action))
972
953
  return "future";
@@ -984,8 +965,10 @@ function inferIdempotency(resource, sideEffect, action, syntax) {
984
965
  if (action.startsWith("create") && syntaxHasFlag(syntax, "if-not-exists")) {
985
966
  return "if-not-exists";
986
967
  }
987
- if (action.startsWith("delete") && syntaxHasFlag(syntax, "if-exists"))
968
+ if ((action.startsWith("delete") || `${resource}.${action}` === "wiki.detach")
969
+ && syntaxHasFlag(syntax, "if-exists")) {
988
970
  return "if-exists";
971
+ }
989
972
  if (`${resource}.${action}` === "app.set-manifest-version")
990
973
  return "none";
991
974
  // `app delete-instance` converges without an `--if-exists` flag: an absent
@@ -1048,7 +1031,7 @@ export function inferCleanupHint(resource, action) {
1048
1031
  }
1049
1032
  return `For disposable creates: \`dss ${resource} delete <id>${ifExists}\`.`;
1050
1033
  }
1051
- function buildRegistryEntry(resource, action, meta) {
1034
+ export function buildRegistryEntry(resource, action, meta) {
1052
1035
  const requiresAuth = meta.localHandler === undefined && inferRequiresAuth(resource);
1053
1036
  const requiresProject = inferRequiresProject(resource, action);
1054
1037
  const sideEffect = inferSideEffect(resource, action);
@@ -1085,6 +1068,8 @@ function buildRegistryEntry(resource, action, meta) {
1085
1068
  const payloadSchema = meta.payloadSchema
1086
1069
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.payloadSchema
1087
1070
  ?? inferPayloadSchema(inputContract);
1071
+ // The precise JSON Schema is published once, as `schemas.input`.
1072
+ const { jsonSchema: _jsonSchema, ...payloadSummary } = payloadSchema ?? {};
1088
1073
  const examplePayload = meta.examplePayload
1089
1074
  ?? EXPLICIT_REGISTRY_OVERRIDES[registryKey(resource, action)]?.examplePayload;
1090
1075
  const inferredCleanupCommand = meta.cleanupCommand
@@ -1113,12 +1098,16 @@ function buildRegistryEntry(resource, action, meta) {
1113
1098
  };
1114
1099
  });
1115
1100
  const positionalArguments = syntax.positionalArguments;
1116
- const positionals = positionalArguments.map((positional) => positional.name);
1101
+ // Implicit global flags are listed once in the agent contract
1102
+ // (`commands.globalFlags`); entries keep the flags their usage names. The
1103
+ // argv schema still accepts both.
1104
+ const implicitGlobalFlags = new Set(flags.filter((name) => GLOBAL_FLAG_NAMES[name] === true && !usageFlags.includes(name)));
1105
+ const commandFlagMetadata = flagMetadata.filter((flag) => !implicitGlobalFlags.has(flag.name));
1117
1106
  const inputGroups = requiredInputGroups(resource, action);
1118
1107
  const outputShape = inferOutputShape(resource, action);
1119
1108
  const producesLocalFile = syntax.producesLocalFile;
1120
1109
  const uniqueRequiredFlags = uniqueStrings(requiredFlags);
1121
- const uniqueOptionalFlags = uniqueStrings(optionalFlags);
1110
+ const uniqueOptionalFlags = uniqueStrings(optionalFlags).filter((flag) => !implicitGlobalFlags.has(flag));
1122
1111
  const unsafe = unsafeOutputs(resource, action, producesLocalFile);
1123
1112
  let schemas;
1124
1113
  return {
@@ -1128,8 +1117,7 @@ function buildRegistryEntry(resource, action, meta) {
1128
1117
  description: meta.description,
1129
1118
  examples: meta.examples,
1130
1119
  structuredExamples: structuredExamples(meta.examples, examplePayload),
1131
- flags: flagMetadata,
1132
- positionals,
1120
+ flags: commandFlagMetadata,
1133
1121
  positionalArguments,
1134
1122
  sideEffect,
1135
1123
  requiresAuth,
@@ -1146,7 +1134,7 @@ function buildRegistryEntry(resource, action, meta) {
1146
1134
  optionalFlags: uniqueOptionalFlags,
1147
1135
  ...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
1148
1136
  ...(inputGroups.length > 0 ? { requiredInputGroups: inputGroups, } : {}),
1149
- ...(payloadSchema ? { payloadSchema, } : {}),
1137
+ ...(payloadSchema ? { payloadSchema: payloadSummary, } : {}),
1150
1138
  get schemas() {
1151
1139
  return schemas ??= buildCommandSchemas(resource, action, flagMetadata, uniqueRequiredFlags, requiredOneOf, payloadSchema, outputShape, meta.usage);
1152
1140
  },
@@ -1163,7 +1151,8 @@ function commandDefinitions(resourceFilter) {
1163
1151
  for (const resource of Object.keys(commands)) {
1164
1152
  if (resourceFilter !== undefined && resource !== resourceFilter)
1165
1153
  continue;
1166
- registry[resource] = commands[resource];
1154
+ // Forward the lazy lookup: enumerating resources must not load command modules.
1155
+ Object.defineProperty(registry, resource, { enumerable: true, get: () => commands[resource], });
1167
1156
  }
1168
1157
  if (resourceFilter === undefined || resourceFilter === "commands") {
1169
1158
  registry.commands = {
@@ -1266,6 +1255,58 @@ export function buildCommandRegistry(resourceFilter) {
1266
1255
  }
1267
1256
  return registry;
1268
1257
  }
1258
+ /** Query words that match nearly every description and carry no intent. */
1259
+ const SEARCH_STOPWORDS = {
1260
+ the: true,
1261
+ and: true,
1262
+ for: true,
1263
+ how: true,
1264
+ what: true,
1265
+ why: true,
1266
+ did: true,
1267
+ does: true,
1268
+ who: true,
1269
+ can: true,
1270
+ with: true,
1271
+ from: true,
1272
+ into: true,
1273
+ this: true,
1274
+ that: true,
1275
+ dss: true,
1276
+ };
1277
+ /**
1278
+ * Find actions by concept: every query word of three or more letters is
1279
+ * matched (case-insensitively, trailing plural `s` ignored) against the action
1280
+ * id, usage, and description. When some actions match every word, only those
1281
+ * are returned; otherwise actions matching more words rank first. Matches in
1282
+ * the id break ties. Reads command definitions only, never builds entries.
1283
+ */
1284
+ export function searchCommands(query) {
1285
+ const words = [...new Set(query.toLowerCase().split(/[^a-z0-9]+/)),]
1286
+ .filter((word) => word.length >= 3 && SEARCH_STOPWORDS[word] !== true)
1287
+ .map((word) => word.length > 4 && word.endsWith("s") ? word.slice(0, -1) : word);
1288
+ if (words.length === 0)
1289
+ return [];
1290
+ const scored = [];
1291
+ for (const [resource, actions,] of Object.entries(commandDefinitions())) {
1292
+ for (const [action, meta,] of Object.entries(actions)) {
1293
+ const id = `${resource}.${action}`;
1294
+ const usage = meta.usage ?? commandUsage(resource, action);
1295
+ const description = meta.description ?? "";
1296
+ const text = `${id} ${usage} ${description}`.toLowerCase();
1297
+ const matched = words.filter((word) => text.includes(word));
1298
+ if (matched.length === 0)
1299
+ continue;
1300
+ const inId = matched.filter((word) => id.includes(word)).length;
1301
+ scored.push({ id, usage, description, matched: matched.length, inId, });
1302
+ }
1303
+ }
1304
+ const best = Math.max(0, ...scored.map((match) => match.matched));
1305
+ return scored
1306
+ .filter((match) => best < words.length || match.matched === words.length)
1307
+ .sort((a, b) => b.matched - a.matched || b.inId - a.inId || a.id.localeCompare(b.id))
1308
+ .map(({ id, usage, description, }) => ({ id, usage, description, }));
1309
+ }
1269
1310
  function commandFlagJsonSchema() {
1270
1311
  return {
1271
1312
  type: "object",
@@ -1289,7 +1330,6 @@ function commandRegistryEntryJsonSchema() {
1289
1330
  "action",
1290
1331
  "usage",
1291
1332
  "flags",
1292
- "positionals",
1293
1333
  "positionalArguments",
1294
1334
  "sideEffect",
1295
1335
  "requiresAuth",
@@ -1307,9 +1347,16 @@ function commandRegistryEntryJsonSchema() {
1307
1347
  usage: { type: "string", },
1308
1348
  description: { type: "string", },
1309
1349
  examples: { type: "array", items: { type: "string", }, },
1310
- structuredExamples: { type: "array", items: { type: "object", additionalProperties: true, }, },
1350
+ structuredExamples: {
1351
+ type: "array",
1352
+ items: {
1353
+ type: "object",
1354
+ required: ["argv",],
1355
+ additionalProperties: false,
1356
+ properties: { argv: { type: "array", items: { type: "string", }, }, payload: {}, },
1357
+ },
1358
+ },
1311
1359
  flags: { type: "array", items: commandFlagJsonSchema(), },
1312
- positionals: { type: "array", items: { type: "string", }, },
1313
1360
  positionalArguments: {
1314
1361
  type: "array",
1315
1362
  items: {
@@ -1420,9 +1467,17 @@ export function agentContractJsonSchema() {
1420
1467
  };
1421
1468
  }
1422
1469
  export function commandActionSummary() {
1470
+ // command-syntax.json lists exactly the registered commands (pinned by
1471
+ // tests/cli/command-syntax.test.ts), so action names come from it and the
1472
+ // summary loads no command module.
1473
+ const actionsByResource = {};
1474
+ for (const key of commandSyntaxKeys()) {
1475
+ const dot = key.indexOf(".");
1476
+ (actionsByResource[key.slice(0, dot)] ??= []).push(key.slice(dot + 1));
1477
+ }
1423
1478
  const summary = {};
1424
- for (const [resource, actions,] of Object.entries(commandDefinitions())) {
1425
- summary[resource] = Object.keys(actions).sort();
1479
+ for (const resource of Object.keys(commandDefinitions())) {
1480
+ summary[resource] = (actionsByResource[resource] ?? []).sort();
1426
1481
  }
1427
1482
  return summary;
1428
1483
  }
@@ -1439,11 +1494,18 @@ export function buildAgentContract() {
1439
1494
  fullRegistryExportCommand: "dss commands run --output PATH",
1440
1495
  scopedDiscoveryCommand: "dss commands run --fields RESOURCE[.ACTION[.FIELD...]]",
1441
1496
  actionIndexCommand: "dss agent contract --fields commands.actions",
1497
+ searchCommand: "dss commands run --contains TEXT",
1442
1498
  scopedDiscoveryExamples: [
1443
1499
  "dss commands run --fields dataset",
1444
1500
  "dss commands run --fields dataset.create",
1445
1501
  ],
1446
- scopedDiscoveryHint: "Default: resource/action summary. --fields RESOURCE: all resource entries; RESOURCE.ACTION: one entry keyed by that path; append .FIELD for nested metadata. Comma-separate paths. --output PATH exports the full registry.",
1502
+ scopedDiscoveryHint: "Default: resource/action summary. --contains TEXT: top 5 actions by concept. --fields RESOURCE: all resource entries; RESOURCE.ACTION: one entry keyed by that path; append .FIELD for nested metadata. Comma-separate paths. --output PATH exports the full registry.",
1503
+ // Accepted by every command and absent from per-entry `flags`; the
1504
+ // connection flags matter only for commands that call DSS.
1505
+ globalFlags: [...GLOBAL_AGENT_FLAGS, ...AUTHENTICATED_AGENT_FLAGS,].map((name) => {
1506
+ const hint = GLOBAL_FLAG_VALUE_HINTS[name];
1507
+ return { name, kind: flagKind(name), ...(hint ? { valueType: hint.valueType, } : {}), };
1508
+ }),
1447
1509
  actions: commandActionSummary(),
1448
1510
  };
1449
1511
  },
@@ -1464,6 +1526,7 @@ export function buildAgentContract() {
1464
1526
  failure: "structured-error-object",
1465
1527
  failureResultDetailLimitBytes: 65_536,
1466
1528
  fieldProjection: "Missing --fields paths: null on stdout; field_projection_missing on stderr.",
1529
+ lists: "*.list: compact items; --full/--fields use DSS objects; --contains/--limit filter (list_truncated warning).",
1467
1530
  richFailureResults: "doctor/batch/cleanup failures: own compact {ok:false,...} result on stdout and command exit code; no wrapper.",
1468
1531
  },
1469
1532
  stderr: {
@@ -1485,3326 +1548,6 @@ export function buildAgentContract() {
1485
1548
  },
1486
1549
  };
1487
1550
  }
1488
- function exitCodesOnFailure(entry) {
1489
- return {
1490
- usage: entry.exitCodes.usage,
1491
- error: entry.exitCodes.error,
1492
- transient: entry.exitCodes.transient,
1493
- ...(entry.exitCodes.longRunningFailure !== undefined
1494
- ? { longRunningFailure: entry.exitCodes.longRunningFailure, }
1495
- : {}),
1496
- ...(entry.exitCodes.assertionFailure !== undefined
1497
- ? { assertionFailure: entry.exitCodes.assertionFailure, }
1498
- : {}),
1499
- };
1500
- }
1501
- /**
1502
- * Plan-local project key resolution. `--plan` is a purely local preview, so it
1503
- * resolves the target project from the explicit `--project-key` flag and the
1504
- * documented `DATAIKU_PROJECT_KEY` environment variable only. The saved
1505
- * credentials file is never opened and no API key is ever resolved, so planning
1506
- * can neither depend on nor leak stored secrets.
1507
- */
1508
- function planProjectKeyFromArgs(flags) {
1509
- const fromFlag = flags["project-key"];
1510
- if (typeof fromFlag === "string" && fromFlag.trim().length > 0)
1511
- return fromFlag.trim();
1512
- const fromEnv = ambientProjectKey();
1513
- return fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv.trim() : undefined;
1514
- }
1515
- /**
1516
- * Commands whose plan must pin the target with an explicit `--project-key`
1517
- * instead of falling back to ambient `DATAIKU_PROJECT_KEY`, so a cleanup plan
1518
- * can never silently retarget a project the caller did not name.
1519
- */
1520
- const EXPLICIT_PLAN_PROJECT_KEY = { "app.delete-instance": true, };
1521
- /** Plan-local project key, or the canonical `--project-key` usage error. */
1522
- function requiredPlanProjectKey(flags, usage) {
1523
- return planProjectKeyFromArgs(flags) ?? requiredPlanFlag(flags, "project-key", usage);
1524
- }
1525
- function projectKeyForPlan(entry, flags) {
1526
- if (!entry.requiresProject)
1527
- return undefined;
1528
- if (EXPLICIT_PLAN_PROJECT_KEY[`${entry.resource}.${entry.action}`] === true) {
1529
- return requiredPlanFlag(flags, "project-key", entry.usage);
1530
- }
1531
- const projectKey = planProjectKeyFromArgs(flags);
1532
- if (projectKey)
1533
- return projectKey;
1534
- throw new UsageError(`Missing project key. Pass --project-key or set DATAIKU_PROJECT_KEY before planning ${entry.resource} ${entry.action}.`);
1535
- }
1536
- function requiredPlanFlag(flags, name, usage) {
1537
- const value = flags[name];
1538
- // Return trimmed like the runtime's requiredStringFlag so plans advertise
1539
- // exactly the identifier the command will act on.
1540
- if (typeof value === "string" && value.trim().length > 0)
1541
- return value.trim();
1542
- throw new UsageError(`--${name} is required. Usage: ${usage}`);
1543
- }
1544
- /**
1545
- * Optional caller-chosen identifier. Absent selects the command's generated
1546
- * mode; a present-but-empty value is an input error, never a silent omission.
1547
- */
1548
- function optionalPlanFlag(flags, name, usage) {
1549
- const value = flags[name];
1550
- if (value === undefined)
1551
- return undefined;
1552
- if (typeof value !== "string" || value.trim().length === 0) {
1553
- throw new UsageError(`--${name} must not be empty when supplied; omit --${name} entirely to generate the successor key during apply. Usage: ${usage}`, "validation_failed");
1554
- }
1555
- return value.trim();
1556
- }
1557
- function optionalJsonFlag(flags, name) {
1558
- const value = flags[name];
1559
- return typeof value === "string" ? parseJsonObject(value, `--${name}`) : undefined;
1560
- }
1561
- function requiredPlanJsonInput(flags, usage) {
1562
- return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
1563
- }
1564
- function dataQualityEndpoint(projectKey, datasetName, suffix) {
1565
- return encodedProjectEndpointForPlan(projectKey, `/datasets/${encodeURIComponent(datasetName)}/data-quality${suffix}`);
1566
- }
1567
- function querySuffix(params) {
1568
- const search = new URLSearchParams();
1569
- for (const [key, value,] of Object.entries(params)) {
1570
- if (value !== undefined)
1571
- search.set(key, String(value));
1572
- }
1573
- const raw = search.toString();
1574
- return raw ? `?${raw}` : "";
1575
- }
1576
- function projectFolderEndpoint(folderId) {
1577
- return `/public/api/project-folders/${encodeURIComponent(folderId)}`;
1578
- }
1579
- /**
1580
- * Git and future API mount. The official Python client mounts these routes on
1581
- * `/dip/publicapi`, which is also what the project-git resource uses; do not
1582
- * normalize them to the repo-wide `/public/api`.
1583
- */
1584
- const PROJECT_GIT_API_ROOT = "/dip/publicapi";
1585
- function projectGitEndpoint(projectKey, suffix) {
1586
- if (!projectKey)
1587
- throw new UsageError("--project-key is required for project-git mutations.");
1588
- return `${PROJECT_GIT_API_ROOT}/projects/${encodeURIComponent(projectKey)}/git${suffix}`;
1589
- }
1590
- function projectGitFutureEndpoint(jobId) {
1591
- return `${PROJECT_GIT_API_ROOT}/futures/${encodeURIComponent(jobId)}`;
1592
- }
1593
- /**
1594
- * `--plan` bypasses the SDK boundary guard, so HTTP(S) URLs with embedded
1595
- * userinfo are rejected here before any plan is printed. SSH/scp-style URLs
1596
- * (`git@host:org/repo.git`, `ssh://...`) remain valid.
1597
- */
1598
- function validatedPlanRepositoryUrl(url, flag, usage) {
1599
- const candidate = url.trim();
1600
- if (/[\u0000-\u001f\u007f]/u.test(candidate)) {
1601
- throw new UsageError(`--${flag} must not contain control characters. Usage: ${usage}`);
1602
- }
1603
- if (/^https?:/i.test(candidate)) {
1604
- if (candidate.includes("\\")) {
1605
- throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
1606
- }
1607
- if (!/^https?:\/\//i.test(candidate)) {
1608
- throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
1609
- }
1610
- let parsed;
1611
- try {
1612
- parsed = new URL(candidate);
1613
- }
1614
- catch {
1615
- throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
1616
- }
1617
- if (parsed.username !== "" || parsed.password !== "") {
1618
- throw new UsageError(`--${flag} must not contain embedded credentials (userinfo). Usage: ${usage}`);
1619
- }
1620
- }
1621
- return candidate;
1622
- }
1623
- function pluginRootEndpoint(pluginId, suffix) {
1624
- return `/public/api/plugins/${encodeURIComponent(pluginId)}${suffix}`;
1625
- }
1626
- function pluginActionEndpoint(pluginId, action) {
1627
- return pluginRootEndpoint(pluginId, `/actions/${action}`);
1628
- }
1629
- function pluginContentsEndpoint(pluginId, contentPath) {
1630
- return `${pluginRootEndpoint(pluginId, "/contents/")}${encodePluginSegmentsForPlan(contentPath)}`;
1631
- }
1632
- function pluginFoldersEndpoint(pluginId, contentPath) {
1633
- return `${pluginRootEndpoint(pluginId, "/folders/")}${encodePluginSegmentsForPlan(contentPath)}`;
1634
- }
1635
- /** Encodes a plugin content path per segment for plan-time endpoints. */
1636
- function encodePluginSegmentsForPlan(contentPath) {
1637
- return contentPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1638
- }
1639
- /**
1640
- * Plan payload for plugin git install/update bodies. The repository URL is
1641
- * validated (embedded userinfo rejected) and secrets are never echoed: the
1642
- * plan carries the URL only, never embedded credentials.
1643
- */
1644
- function pluginGitPlanPayload(flags) {
1645
- const repository = requiredPlanFlag(flags, "repository", "dss plugin install-from-git --repository URL");
1646
- // Mirrors PluginsResource install/update bodies: absent options are sent as null.
1647
- return {
1648
- gitRepositoryUrl: validatedPlanRepositoryUrl(repository, "repository", "--repository URL"),
1649
- gitCheckout: typeof flags["checkout"] === "string" ? flags["checkout"] : null,
1650
- gitSubpath: typeof flags["path-in-repository"] === "string" ? flags["path-in-repository"] : null,
1651
- };
1652
- }
1653
- function optionalPlanProjectScope(flags) {
1654
- const projectKey = flags["project-key"];
1655
- if (typeof projectKey !== "string" || projectKey.trim() === "")
1656
- return "";
1657
- return `?projectKey=${encodeURIComponent(projectKey.trim())}`;
1658
- }
1659
- function settingsConfigKeys(flags) {
1660
- const content = flags["content"];
1661
- if (typeof content !== "string")
1662
- return [];
1663
- try {
1664
- const parsed = JSON.parse(content);
1665
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1666
- return [];
1667
- return Object.keys(parsed);
1668
- }
1669
- catch {
1670
- return [];
1671
- }
1672
- }
1673
- function contentSourceKind(flags) {
1674
- if (flags["stdin"] === true)
1675
- return "stdin";
1676
- if (typeof flags["file"] === "string")
1677
- return "file";
1678
- if (typeof flags["content"] === "string")
1679
- return "content";
1680
- return "unknown";
1681
- }
1682
- /**
1683
- * Plan-safe view of a user create/update body: password values are replaced by
1684
- * a fixed marker so secrets never appear in --plan output.
1685
- */
1686
- function redactedUserPayload(payload) {
1687
- if (payload["password"] === undefined)
1688
- return payload;
1689
- return { ...payload, password: "<omitted>", };
1690
- }
1691
- /**
1692
- * Plan-safe view of a connection create/update body: the connection-type
1693
- * specific `params` object may carry credentials (passwords, keys, tokens), so
1694
- * the plan carries only the marker, never the raw params.
1695
- */
1696
- function redactedConnectionPayload(payload) {
1697
- if (payload["params"] === undefined)
1698
- return payload;
1699
- return { ...payload, params: "<omitted; may include credentials>", };
1700
- }
1701
- /** Required CSV flag for plans (comma-separated logins list). */
1702
- function requiredPlanCsv(flags, name, usage) {
1703
- const raw = requiredPlanFlag(flags, name, usage);
1704
- return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1705
- }
1706
- function requiredPlanRepositoryUrl(flags, flag, usage) {
1707
- const url = requiredPlanFlag(flags, flag, usage);
1708
- return validatedPlanRepositoryUrl(url, flag, usage);
1709
- }
1710
- /** Wait procedure advertised for the library calls that return a job id. */
1711
- function projectGitFutureWait() {
1712
- return {
1713
- when: "after-dispatch",
1714
- endpoint: `${PROJECT_GIT_API_ROOT}/futures/{jobId}?peek=false`,
1715
- description: "Poll the returned job id until the future reports a result; never abort it.",
1716
- };
1717
- }
1718
- function jobBuildPayload(target, projectKey, flags) {
1719
- const targetType = jobBuildTargetTypeFromFlags(flags);
1720
- const partition = flags["partition"];
1721
- const output = { projectKey, id: target, type: targetType, };
1722
- if (targetType === "DATASET") {
1723
- if (partition !== undefined)
1724
- output.partition = partition;
1725
- }
1726
- else {
1727
- output.targetManagedFolderProjectKey = projectKey;
1728
- output.targetManagedFolder = target;
1729
- output.targetPartition = partition ?? "NP";
1730
- }
1731
- const payload = {
1732
- outputs: [output,],
1733
- type: flags["build-mode"] ?? "NON_RECURSIVE_FORCED_BUILD",
1734
- };
1735
- if (flags["force-rebuild"] === true && targetType === "DATASET") {
1736
- payload.autoUpdateSchemaBeforeEachRecipeRun = true;
1737
- }
1738
- return payload;
1739
- }
1740
- function uploadPayload(filePath) {
1741
- return {
1742
- contentType: "multipart/form-data",
1743
- fileField: "file",
1744
- filePath,
1745
- fileName: path.basename(filePath),
1746
- };
1747
- }
1748
- export function commandPlanShape(resource, action, args, flags, entry, projectKey) {
1749
- const projectEndpoint = (suffix) => {
1750
- if (!projectKey)
1751
- throw new UsageError(`Missing project key for ${resource} ${action}.`);
1752
- return encodedProjectEndpointForPlan(projectKey, suffix);
1753
- };
1754
- const id = args[0];
1755
- const codeEnvEndpoint = (suffix = "") => `/public/api/admin/code-envs/${encodeURIComponent(codeEnvLang(args[0], entry.usage))}/${encodeURIComponent(args[1])}${suffix}`;
1756
- const statisticsWorksheetsEndpoint = (datasetName) => projectEndpoint(`/datasets/${encodeURIComponent(datasetName)}/statistics/worksheets/`);
1757
- const statisticsWorksheetEndpoint = (datasetName, worksheetId) => `${statisticsWorksheetsEndpoint(datasetName)}${encodeURIComponent(worksheetId)}`;
1758
- switch (`${resource}.${action}`) {
1759
- case "sql.query": {
1760
- const payload = resolveSqlQueryInvocation(args, flags, planProjectKeyFromArgs(flags));
1761
- return {
1762
- method: "POST",
1763
- endpoint: "/public/api/sql/queries/",
1764
- identifiers: {
1765
- connection: payload.connection,
1766
- dataset: payload.datasetFullName,
1767
- },
1768
- payload,
1769
- };
1770
- }
1771
- case "ml-task.train":
1772
- return {
1773
- method: "POST",
1774
- endpoint: projectEndpoint(`/models/lab/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}/train`),
1775
- identifiers: { analysisId: args[0], mlTaskId: args[1], },
1776
- payload: {
1777
- sessionName: flags["session-name"],
1778
- runQueue: false,
1779
- },
1780
- wait: flags["wait"] === true,
1781
- };
1782
- case "wiki.create": {
1783
- const name = requiredPlanFlag(flags, "name", entry.usage);
1784
- const content = textInput(flags);
1785
- const create = {
1786
- method: "POST",
1787
- endpoint: projectEndpoint("/wiki/"),
1788
- payload: { projectKey, name, parent: flags["parent"] ?? null, },
1789
- };
1790
- return {
1791
- ...create,
1792
- identifiers: { name, },
1793
- // Content is not accepted by the create endpoint; it lands in a follow-up update.
1794
- ...(content === undefined ? {} : {
1795
- requests: [
1796
- { sequence: 1, ...create, },
1797
- { sequence: 2, method: "GET", endpoint: projectEndpoint("/wiki/{createdArticleId}"), },
1798
- {
1799
- sequence: 3,
1800
- method: "PUT",
1801
- endpoint: projectEndpoint("/wiki/{createdArticleId}"),
1802
- payload: { payload: content, },
1803
- },
1804
- ],
1805
- }),
1806
- };
1807
- }
1808
- case "wiki.update":
1809
- return {
1810
- method: "PUT",
1811
- endpoint: projectEndpoint(`/wiki/${encodeURIComponent(id)}`),
1812
- identifiers: { article: id, },
1813
- payload: {
1814
- ...jsonInput(flags),
1815
- name: flags["name"],
1816
- content: textInput(flags),
1817
- },
1818
- };
1819
- case "wiki.delete":
1820
- return {
1821
- method: "DELETE",
1822
- endpoint: projectEndpoint(`/wiki/${encodeURIComponent(id)}`),
1823
- identifiers: { article: id, },
1824
- };
1825
- case "dashboard.create": {
1826
- const data = jsonInput(flags);
1827
- const flagName = flags["name"];
1828
- const dataName = data?.["name"];
1829
- const name = flagName ?? (typeof dataName === "string" ? dataName : undefined);
1830
- if (!name) {
1831
- throw new UsageError(`--name or dashboard settings containing a string name are required. Usage: ${commandUsage("dashboard", "create")}`);
1832
- }
1833
- const listed = parseBooleanOption(flags["listed"], "--listed");
1834
- const payload = { ...(data ?? { pages: [], }), name, };
1835
- if (listed !== undefined)
1836
- payload.listed = listed;
1837
- return {
1838
- method: "POST",
1839
- endpoint: projectEndpoint("/dashboards/"),
1840
- identifiers: { name, },
1841
- payload,
1842
- };
1843
- }
1844
- case "dashboard.update": {
1845
- const payload = { ...jsonInput(flags), };
1846
- if (typeof flags["name"] === "string")
1847
- payload.name = flags["name"];
1848
- const listed = parseBooleanOption(flags["listed"], "--listed");
1849
- if (listed !== undefined)
1850
- payload.listed = listed;
1851
- return {
1852
- method: "PUT",
1853
- endpoint: projectEndpoint(`/dashboards/${encodeURIComponent(id)}/`),
1854
- identifiers: { id, },
1855
- payload,
1856
- };
1857
- }
1858
- case "dashboard.delete":
1859
- return {
1860
- method: "DELETE",
1861
- endpoint: projectEndpoint(`/dashboards/${encodeURIComponent(id)}/`),
1862
- identifiers: { id, },
1863
- };
1864
- case "insight.create": {
1865
- const data = jsonInput(flags);
1866
- const name = flags["name"];
1867
- const type = flags["type"];
1868
- if (!data && (!name || !type)) {
1869
- throw new UsageError(`--data or both --name and --type are required. Usage: ${commandUsage("insight", "create")}`);
1870
- }
1871
- const prototype = { ...data, };
1872
- if (name !== undefined)
1873
- prototype.name = name;
1874
- if (type !== undefined)
1875
- prototype.type = type;
1876
- const listed = parseBooleanOption(flags["listed"], "--listed");
1877
- if (listed !== undefined)
1878
- prototype.listed = listed;
1879
- const params = optionalJsonFlag(flags, "params");
1880
- if (params !== undefined)
1881
- prototype.params = params;
1882
- return {
1883
- method: "POST",
1884
- endpoint: projectEndpoint("/insights/"),
1885
- identifiers: { name, type, },
1886
- payload: {
1887
- insightPrototype: prototype,
1888
- contentType: flags["content-type"],
1889
- payload: textInput(flags),
1890
- },
1891
- };
1892
- }
1893
- case "insight.update":
1894
- return {
1895
- method: "POST",
1896
- endpoint: projectEndpoint(`/insights/${encodeURIComponent(id)}/`),
1897
- identifiers: { id, },
1898
- payload: {
1899
- insight: {
1900
- ...jsonInput(flags),
1901
- name: flags["name"],
1902
- listed: parseBooleanOption(flags["listed"], "--listed"),
1903
- params: optionalJsonFlag(flags, "params"),
1904
- },
1905
- contentType: flags["content-type"],
1906
- payload: textInput(flags),
1907
- },
1908
- };
1909
- case "insight.delete":
1910
- return {
1911
- method: "DELETE",
1912
- endpoint: projectEndpoint(`/insights/${encodeURIComponent(id)}/`),
1913
- identifiers: { id, },
1914
- };
1915
- case "data-quality.create-rule":
1916
- return {
1917
- method: "POST",
1918
- endpoint: dataQualityEndpoint(projectKey, args[0], "/rules"),
1919
- identifiers: { dataset: args[0], },
1920
- payload: requiredPlanJsonInput(flags, entry.usage),
1921
- };
1922
- case "data-quality.update-rule":
1923
- return {
1924
- method: "PUT",
1925
- endpoint: dataQualityEndpoint(projectKey, args[0], `/rules/${encodeURIComponent(args[1])}`),
1926
- identifiers: { dataset: args[0], ruleId: args[1], },
1927
- payload: requiredPlanJsonInput(flags, entry.usage),
1928
- };
1929
- case "data-quality.delete-rule":
1930
- return {
1931
- method: "DELETE",
1932
- endpoint: dataQualityEndpoint(projectKey, args[0], `/rules/${encodeURIComponent(args[1])}${querySuffix({ ruleId: args[1], })}`),
1933
- identifiers: { dataset: args[0], ruleId: args[1], },
1934
- };
1935
- case "data-quality.compute":
1936
- return {
1937
- method: "POST",
1938
- endpoint: dataQualityEndpoint(projectKey, args[0], `/actions/compute-rules${querySuffix({
1939
- partition: flags["partition"] ?? "NP",
1940
- ruleId: flags["rule-id"],
1941
- })}`),
1942
- identifiers: { dataset: args[0], ruleId: flags["rule-id"], },
1943
- wait: flags["wait"] === true,
1944
- };
1945
- case "future.abort":
1946
- return {
1947
- method: "DELETE",
1948
- endpoint: `/public/api/futures/${encodeURIComponent(id)}`,
1949
- identifiers: { id, },
1950
- };
1951
- case "flow-zone.create": {
1952
- const name = flowZoneName(flags["name"]);
1953
- const payload = { name, color: flowZoneColor(flags["color"]) ?? "#2ab1ac", };
1954
- return {
1955
- method: "POST",
1956
- endpoint: projectEndpoint("/flow/zones"),
1957
- identifiers: { name, },
1958
- payload,
1959
- };
1960
- }
1961
- case "flow-zone.update":
1962
- return {
1963
- method: "PUT",
1964
- endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}`),
1965
- identifiers: { id, },
1966
- payload: {
1967
- name: typeof flags["name"] === "string" ? flowZoneName(flags["name"]) : undefined,
1968
- color: flowZoneColor(flags["color"]),
1969
- projectKey,
1970
- },
1971
- };
1972
- case "flow-zone.delete":
1973
- return {
1974
- method: "DELETE",
1975
- endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}`),
1976
- identifiers: { id, },
1977
- };
1978
- case "flow-zone.move":
1979
- return {
1980
- method: "POST",
1981
- endpoint: projectEndpoint(`/flow/zones/${encodeURIComponent(id)}/add-items`),
1982
- identifiers: { id, },
1983
- payload: flowZoneMoveItems(flags),
1984
- };
1985
- case "dataset.create": {
1986
- const name = requiredPlanFlag(flags, "name", entry.usage);
1987
- const connection = flags["connection"];
1988
- const dsType = requiredPlanFlag(flags, "type", entry.usage);
1989
- if (!connection && dsType.toLowerCase() !== "uploadedfiles") {
1990
- throw new UsageError("--connection is required unless --type is UploadedFiles.");
1991
- }
1992
- const endpoint = projectEndpoint("/datasets/"); // throws without a project key
1993
- return {
1994
- method: "POST",
1995
- endpoint,
1996
- identifiers: { name, },
1997
- payload: buildDatasetCreateBody({
1998
- projectKey: projectKey,
1999
- datasetName: name,
2000
- connection,
2001
- dsType,
2002
- }),
2003
- };
2004
- }
2005
- case "dataset.clone": {
2006
- const source = args[0];
2007
- const target = args[1];
2008
- return {
2009
- exact: false,
2010
- reason: "Apply GETs the source dataset and copies its current connection, format, and schema into the new dataset body.",
2011
- method: "POST",
2012
- endpoint: projectEndpoint("/datasets/"),
2013
- identifiers: { source, target, },
2014
- payload: {
2015
- sourceDataset: source,
2016
- targetDataset: target,
2017
- path: flags["path"],
2018
- table: flags["table"],
2019
- metastoreTableName: flags["metastore-table"],
2020
- allowSamePath: flags["allow-same-path"] === true,
2021
- projectKey,
2022
- },
2023
- };
2024
- }
2025
- case "dataset.rename":
2026
- return {
2027
- method: "POST",
2028
- endpoint: projectEndpoint("/actions/renameDataset"),
2029
- identifiers: { oldName: args[0], newName: args[1], },
2030
- payload: { oldName: args[0], newName: args[1], },
2031
- };
2032
- case "dataset.upload-file": {
2033
- const fileName = requiredPlanFlag(flags, "file-name", entry.usage);
2034
- const datasetEndpoint = projectEndpoint(`/datasets/${encodeURIComponent(args[0])}`);
2035
- const filesEndpoint = `${datasetEndpoint}/uploaded/files`;
2036
- const upload = {
2037
- method: "POST",
2038
- endpoint: filesEndpoint,
2039
- payload: { ...uploadPayload(args[1]), fileName, },
2040
- };
2041
- return {
2042
- ...upload,
2043
- identifiers: {
2044
- datasetName: args[0],
2045
- localPath: args[1],
2046
- fileName,
2047
- },
2048
- requests: [
2049
- { sequence: 1, method: "GET", endpoint: datasetEndpoint, },
2050
- { sequence: 2, method: "GET", endpoint: filesEndpoint, },
2051
- { sequence: 3, ...upload, },
2052
- {
2053
- sequence: 4,
2054
- method: "GET",
2055
- endpoint: filesEndpoint,
2056
- assert: {
2057
- filename: fileName,
2058
- length: "local file byte length",
2059
- },
2060
- },
2061
- ],
2062
- };
2063
- }
2064
- case "dataset.delete":
2065
- return {
2066
- method: "DELETE",
2067
- endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
2068
- identifiers: { name: id, },
2069
- };
2070
- case "dataset.refresh-schema": {
2071
- const columns = schemaColumnsInput(flags, entry.usage);
2072
- return {
2073
- method: "PUT",
2074
- endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/schema`),
2075
- identifiers: { name: id, },
2076
- payload: { columns, },
2077
- };
2078
- }
2079
- case "dataset.update":
2080
- return {
2081
- method: "PUT",
2082
- endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
2083
- identifiers: { name: id, },
2084
- };
2085
- case "dataset.metadata-set":
2086
- return {
2087
- method: "PUT",
2088
- endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/metadata`),
2089
- identifiers: { name: id, },
2090
- payload: requiredPlanJsonInput(flags, entry.usage),
2091
- };
2092
- case "dataset.create-managed": {
2093
- const name = requiredPlanFlag(flags, "name", entry.usage);
2094
- const connection = requiredPlanFlag(flags, "connection", entry.usage);
2095
- const creationSettings = { connectionId: connection, };
2096
- const specificSettings = {};
2097
- if (typeof flags["type-option-id"] === "string") {
2098
- creationSettings.typeOptionId = flags["type-option-id"];
2099
- }
2100
- if (typeof flags["format-option-id"] === "string") {
2101
- specificSettings.formatOptionId = flags["format-option-id"];
2102
- }
2103
- if (typeof flags["copy-partitioning-from"] === "string") {
2104
- specificSettings.partitioningOptionId = `copy:${flags["partitioning-folder"] === true ? "folder" : "dataset"}:${flags["copy-partitioning-from"]}`;
2105
- }
2106
- if (Object.keys(specificSettings).length > 0) {
2107
- creationSettings.specificSettings = specificSettings;
2108
- }
2109
- return {
2110
- method: "POST",
2111
- endpoint: projectEndpoint("/datasets/managed"),
2112
- identifiers: { name, },
2113
- payload: { name, creationSettings, },
2114
- };
2115
- }
2116
- case "recipe.add-input":
2117
- case "recipe.remove-input": {
2118
- const role = flags["role"] ?? "main";
2119
- return {
2120
- method: "PUT",
2121
- endpoint: projectEndpoint(`/recipes/${encodeURIComponent(args[0])}`),
2122
- identifiers: { recipe: args[0], dataset: args[1], role, },
2123
- payload: {
2124
- operation: action === "add-input" ? "append" : "remove",
2125
- dataset: args[1],
2126
- role,
2127
- projectKey,
2128
- },
2129
- };
2130
- }
2131
- case "recipe.clone": {
2132
- const positionalSource = args[0];
2133
- const fromFlag = typeof flags["from"] === "string" ? flags["from"].trim() : "";
2134
- const source = positionalSource ?? fromFlag;
2135
- if (!source) {
2136
- throw new UsageError(`Source recipe is required. Usage: ${entry.usage}`, "missing_required_flag");
2137
- }
2138
- if (positionalSource && fromFlag && positionalSource !== fromFlag) {
2139
- throw new UsageError("Positional source and --from must match when both are provided.", "invalid_enum");
2140
- }
2141
- const toFlag = typeof flags["to"] === "string" ? flags["to"].trim() : "";
2142
- const nameFlag = typeof flags["name"] === "string" ? flags["name"].trim() : "";
2143
- const target = toFlag || nameFlag;
2144
- if (!target) {
2145
- throw new UsageError(`--name or --to is required. Usage: ${entry.usage}`, "missing_required_flag");
2146
- }
2147
- return {
2148
- method: "POST",
2149
- endpoint: projectEndpoint("/recipes/"),
2150
- identifiers: { source, target, },
2151
- payload: {
2152
- sourceRecipe: source,
2153
- targetRecipe: target,
2154
- inputRewrites: rewritePairsFromFlags(flags, "replace-input"),
2155
- outputRewrites: rewritePairsFromFlags(flags, "replace-output"),
2156
- payloadTextRewrites: rewritePairsFromFlags(flags, "replace-payload-text"),
2157
- outputDataset: flags["output"],
2158
- copyOutputSettings: flags["copy-output-settings"] === true,
2159
- outputPath: flags["path"],
2160
- metastoreTableName: flags["metastore-table"],
2161
- projectKey,
2162
- },
2163
- };
2164
- }
2165
- case "recipe.delete":
2166
- return {
2167
- method: "DELETE",
2168
- endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
2169
- identifiers: { name: id, },
2170
- };
2171
- case "recipe.create": {
2172
- requiredPlanFlag(flags, "type", entry.usage);
2173
- const outputDataset = flags["output"];
2174
- const outputFolder = flags["output-folder"];
2175
- if (outputDataset && outputFolder) {
2176
- throw new UsageError("--output and --output-folder are mutually exclusive.");
2177
- }
2178
- if (!outputDataset && !outputFolder) {
2179
- throw new UsageError("--output or --output-folder is required.");
2180
- }
2181
- if (outputFolder && !flags["output-connection"]) {
2182
- throw new UsageError("--output-connection is required when using --output-folder.");
2183
- }
2184
- const endpoint = projectEndpoint("/recipes/"); // throws without a project key
2185
- // Same flag mapping and body construction as the handler and RecipesResource.create.
2186
- const { recipePrototype, creationSettings, } = buildRecipeCreateRequest(recipeCreateOptionsFromFlags(flags), projectKey);
2187
- return {
2188
- method: "POST",
2189
- endpoint,
2190
- identifiers: { name: recipePrototype.name, },
2191
- payload: { recipePrototype, creationSettings, },
2192
- };
2193
- }
2194
- case "recipe.run":
2195
- return {
2196
- exact: false,
2197
- 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.",
2198
- method: "POST",
2199
- endpoint: projectEndpoint("/jobs/"),
2200
- identifiers: { recipe: id, },
2201
- wait: recipeRunShouldWait(flags),
2202
- };
2203
- case "code.run": {
2204
- const { script, source, } = resolveCodeInputWithSource(args, flags);
2205
- const sourceSha256 = sha256Hex(script);
2206
- const scenarioBase = projectEndpoint("/scenarios/{generatedScenarioId}");
2207
- const envName = flags["env"];
2208
- const keepScenario = flags["keep"] === true;
2209
- const timeoutMs = parseCodeRunIntegerFlag(flags["timeout"], "--timeout") ?? 120_000;
2210
- const maxLogBytes = flags["full-log"] === true
2211
- ? 0
2212
- : parseCodeRunIntegerFlag(flags["max-log-bytes"], "--max-log-bytes") ?? 1_048_576;
2213
- return {
2214
- method: "POST",
2215
- endpoint: projectEndpoint("/scenarios/"),
2216
- identifiers: {
2217
- source,
2218
- sourceSha256,
2219
- sourceBytes: Buffer.byteLength(script),
2220
- },
2221
- requests: [
2222
- {
2223
- method: "POST",
2224
- endpoint: projectEndpoint("/scenarios/"),
2225
- payload: {
2226
- id: "{generatedScenarioId}",
2227
- name: "dss code run ({generatedScenarioId})",
2228
- projectKey,
2229
- type: "custom_python",
2230
- params: {
2231
- envSelection: envName
2232
- ? { envMode: "EXPLICIT_ENV", envName, }
2233
- : { envMode: "INHERIT", },
2234
- },
2235
- },
2236
- },
2237
- {
2238
- method: "PUT",
2239
- endpoint: `${scenarioBase}/payload`,
2240
- payload: { extension: "py", script: "<omitted>", },
2241
- redactedFields: ["payload.script",],
2242
- },
2243
- { method: "POST", endpoint: `${scenarioBase}/run/`, payload: {}, },
2244
- {
2245
- method: "GET",
2246
- endpoint: `${scenarioBase}/get-run-for-trigger?triggerId={triggerId}&triggerRunId={triggerRunId}`,
2247
- repeat: "until scenarioRun.result.outcome or timeout",
2248
- },
2249
- {
2250
- method: "GET",
2251
- endpoint: `${scenarioBase}/{runId}/log`,
2252
- boundedBytes: maxLogBytes,
2253
- },
2254
- ...keepScenario ? [] : [{ method: "DELETE", endpoint: scenarioBase, cleanup: true, },],
2255
- ],
2256
- wait: {
2257
- timeoutMs,
2258
- pollEndpoint: `${scenarioBase}/get-run-for-trigger`,
2259
- },
2260
- };
2261
- }
2262
- case "recipe.update":
2263
- return {
2264
- method: "PUT",
2265
- endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
2266
- identifiers: { name: id, },
2267
- payload: requiredPlanJsonInput(flags, entry.usage),
2268
- };
2269
- case "recipe.set-payload": {
2270
- const file = requiredPlanFlag(flags, "file", entry.usage);
2271
- const backupDir = flags["no-backup"] === true
2272
- ? undefined
2273
- : flags["backup-dir"]
2274
- ?? path.join(process.cwd(), ".dss-backups", "recipes");
2275
- const backupPath = backupDir ? recipeBackupPath(id, backupDir) : undefined;
2276
- return {
2277
- method: "PUT",
2278
- endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}`),
2279
- identifiers: { name: id, },
2280
- payload: {
2281
- file,
2282
- content: textInput(flags),
2283
- ...(backupPath ? { backupPath, } : {}),
2284
- },
2285
- ...(backupPath
2286
- ? { localWrites: [{ path: backupPath, source: "remote recipe backup", before: "PUT", },], }
2287
- : {}),
2288
- };
2289
- }
2290
- case "job.build":
2291
- case "job.build-and-wait":
2292
- return {
2293
- method: "POST",
2294
- endpoint: projectEndpoint("/jobs/"),
2295
- identifiers: { target: id, },
2296
- payload: jobBuildPayload(id, projectKey, flags),
2297
- wait: action === "build-and-wait" || flags["wait"] === true,
2298
- };
2299
- case "scenario.abort":
2300
- return {
2301
- method: "POST",
2302
- endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/abort`),
2303
- identifiers: { id, },
2304
- };
2305
- case "scenario.payload-set":
2306
- return {
2307
- method: "PUT",
2308
- endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/payload`),
2309
- identifiers: { id, },
2310
- payload: requiredPlanJsonInput(flags, entry.usage),
2311
- };
2312
- case "scenario.active-set": {
2313
- const activeRaw = args[1];
2314
- if (activeRaw !== "true" && activeRaw !== "false") {
2315
- throw new UsageError(`Usage: ${entry.usage}`);
2316
- }
2317
- const lightEndpoint = projectEndpoint(`/scenarios/${encodeURIComponent(id)}/light`);
2318
- const active = activeRaw === "true";
2319
- // DSS parses the light PUT as a full Scenario, so the command echoes the
2320
- // current light status with `active` overridden (see ScenariosResource.setActive).
2321
- return {
2322
- method: "PUT",
2323
- endpoint: lightEndpoint,
2324
- identifiers: { id, },
2325
- payload: { active, },
2326
- requests: [
2327
- { sequence: 1, method: "GET", endpoint: lightEndpoint, },
2328
- {
2329
- sequence: 2,
2330
- method: "PUT",
2331
- endpoint: lightEndpoint,
2332
- payload: { "...current": true, active, },
2333
- },
2334
- { sequence: 3, method: "GET", endpoint: lightEndpoint, },
2335
- ],
2336
- };
2337
- }
2338
- case "folder.create": {
2339
- const name = requiredPlanFlag(flags, "name", entry.usage);
2340
- const type = flags["type"];
2341
- const connection = flags["connection"];
2342
- const pathFlag = flags["path"];
2343
- const body = {
2344
- name,
2345
- projectKey,
2346
- type: type ?? null,
2347
- params: { connection, path: pathFlag?.trim() || "/${projectKey}/${odbId}", },
2348
- };
2349
- // Mirrors FoldersResource.create.
2350
- if (connection === undefined) {
2351
- return {
2352
- exact: false,
2353
- reason: "Without --connection, apply reads DSS admin settings to pick the managed-folder connection (falling back to filesystem_folders); params.connection is set then.",
2354
- method: "POST",
2355
- endpoint: projectEndpoint("/managedfolders/"),
2356
- identifiers: { name, },
2357
- payload: body,
2358
- };
2359
- }
2360
- return {
2361
- method: "POST",
2362
- endpoint: projectEndpoint("/managedfolders/"),
2363
- identifiers: { name, },
2364
- payload: body,
2365
- };
2366
- }
2367
- case "job.abort":
2368
- return {
2369
- method: "POST",
2370
- endpoint: projectEndpoint(`/jobs/${encodeURIComponent(id)}/abort/`),
2371
- identifiers: { id, },
2372
- };
2373
- case "scenario.run":
2374
- case "scenario.run-and-wait":
2375
- return {
2376
- method: "POST",
2377
- endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/run/`),
2378
- identifiers: { id, },
2379
- payload: {},
2380
- wait: action === "run-and-wait" || flags["wait"] === true,
2381
- };
2382
- case "scenario.delete":
2383
- return {
2384
- method: "DELETE",
2385
- endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/`),
2386
- identifiers: { id, },
2387
- };
2388
- case "scenario.create": {
2389
- const type = flags["type"] ?? "step_based";
2390
- return {
2391
- method: "POST",
2392
- endpoint: projectEndpoint("/scenarios/"),
2393
- identifiers: { id: args[0], name: args[1], },
2394
- // Mirrors ScenariosResource.create's default params for step-based scenarios.
2395
- payload: {
2396
- id: args[0],
2397
- name: args[1],
2398
- projectKey,
2399
- type,
2400
- params: type === "step_based" ? { steps: [], triggers: [], reporters: [], } : {},
2401
- },
2402
- };
2403
- }
2404
- case "scenario.update":
2405
- return {
2406
- method: "PUT",
2407
- endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/`),
2408
- identifiers: { id, },
2409
- payload: requiredPlanJsonInput(flags, entry.usage),
2410
- };
2411
- case "folder.update":
2412
- return {
2413
- method: "PUT",
2414
- endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(id)}`),
2415
- identifiers: { folder: id, },
2416
- payload: requiredPlanJsonInput(flags, entry.usage),
2417
- };
2418
- case "folder.delete":
2419
- return {
2420
- method: "DELETE",
2421
- endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(id)}`),
2422
- identifiers: { folder: id, },
2423
- };
2424
- case "folder.upload":
2425
- return {
2426
- method: "POST",
2427
- endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(args[0])}/contents/${encodeURIComponent(args[1])}`),
2428
- identifiers: { folder: args[0], path: args[1], localPath: args[2], },
2429
- };
2430
- case "folder.delete-file":
2431
- return {
2432
- method: "DELETE",
2433
- endpoint: projectEndpoint(`/managedfolders/${encodeURIComponent(args[0])}/contents/${encodeURIComponent(args[1])}`),
2434
- identifiers: { folder: args[0], path: args[1], },
2435
- };
2436
- case "variable.set":
2437
- return {
2438
- exact: false,
2439
- reason: "The payload lists the requested changes; apply PUTs the full {standard, local} object (merged with the current variables unless --replace).",
2440
- method: "PUT",
2441
- endpoint: projectEndpoint("/variables/"),
2442
- payload: {
2443
- standard: optionalJsonFlag(flags, "standard"),
2444
- local: optionalJsonFlag(flags, "local"),
2445
- replace: flags["replace"] === true,
2446
- },
2447
- };
2448
- case "project-deployer.create-project": {
2449
- const payload = requiredPlanJsonInput(flags, entry.usage);
2450
- return {
2451
- method: "POST",
2452
- endpoint: "/public/api/project-deployer/projects",
2453
- identifiers: { projectKey: payload.projectKey, id: payload.id, },
2454
- payload,
2455
- };
2456
- }
2457
- case "project-deployer.upload-bundle":
2458
- return {
2459
- method: "POST",
2460
- endpoint: "/public/api/project-deployer/projects/bundles",
2461
- identifiers: { filePath: id, },
2462
- payload: uploadPayload(id),
2463
- };
2464
- case "project-deployer.create-deployment": {
2465
- const payload = requiredPlanJsonInput(flags, entry.usage);
2466
- return {
2467
- method: "POST",
2468
- endpoint: "/public/api/project-deployer/deployments",
2469
- identifiers: { deploymentId: payload.deploymentId ?? payload.id, },
2470
- payload,
2471
- };
2472
- }
2473
- case "project-deployer.save-deployment-settings":
2474
- return {
2475
- method: "PUT",
2476
- endpoint: `/public/api/project-deployer/deployments/${encodeURIComponent(id)}/settings`,
2477
- identifiers: { deploymentId: id, },
2478
- payload: requiredPlanJsonInput(flags, entry.usage),
2479
- };
2480
- case "project-deployer.deploy":
2481
- return {
2482
- method: "POST",
2483
- endpoint: `/public/api/project-deployer/deployments/${encodeURIComponent(id)}/actions/update`,
2484
- identifiers: { deploymentId: id, },
2485
- payload: {},
2486
- };
2487
- case "project-deployer.delete-deployment":
2488
- return {
2489
- method: "DELETE",
2490
- endpoint: `/public/api/project-deployer/deployments/${encodeURIComponent(id)}`,
2491
- identifiers: { deploymentId: id, },
2492
- };
2493
- case "project-deployer.create-infra": {
2494
- const payload = requiredPlanJsonInput(flags, entry.usage);
2495
- return {
2496
- method: "POST",
2497
- endpoint: "/public/api/project-deployer/infras",
2498
- identifiers: { infraId: payload.id, },
2499
- payload,
2500
- };
2501
- }
2502
- case "api-deployer.create-infra": {
2503
- const payload = requiredPlanJsonInput(flags, entry.usage);
2504
- return {
2505
- method: "POST",
2506
- endpoint: "/public/api/api-deployer/infras",
2507
- identifiers: { infraId: payload.id, },
2508
- payload,
2509
- };
2510
- }
2511
- case "api-deployer.delete-infra":
2512
- return {
2513
- method: "DELETE",
2514
- endpoint: `/public/api/api-deployer/infras/${encodeURIComponent(id)}`,
2515
- identifiers: { infraId: id, },
2516
- };
2517
- case "api-deployer.create-service": {
2518
- const payload = requiredPlanJsonInput(flags, entry.usage);
2519
- return {
2520
- method: "POST",
2521
- endpoint: "/public/api/api-deployer/services",
2522
- identifiers: { serviceId: payload.id ?? payload.publishedServiceId, },
2523
- payload,
2524
- };
2525
- }
2526
- case "api-deployer.delete-service":
2527
- return {
2528
- method: "DELETE",
2529
- endpoint: `/public/api/api-deployer/services/${encodeURIComponent(id)}`,
2530
- identifiers: { serviceId: id, },
2531
- };
2532
- case "api-deployer.publish-version":
2533
- return {
2534
- method: "POST",
2535
- endpoint: `/public/api/api-deployer/services/${encodeURIComponent(args[0])}/versions`,
2536
- identifiers: { serviceId: args[0], filePath: args[1], },
2537
- payload: uploadPayload(args[1]),
2538
- };
2539
- case "api-deployer.delete-version":
2540
- return {
2541
- method: "DELETE",
2542
- endpoint: `/public/api/api-deployer/services/${encodeURIComponent(args[0])}/versions/${encodeURIComponent(args[1])}`,
2543
- identifiers: { serviceId: args[0], version: args[1], },
2544
- };
2545
- case "api-deployer.create-deployment": {
2546
- const payload = requiredPlanJsonInput(flags, entry.usage);
2547
- return {
2548
- method: "POST",
2549
- endpoint: "/public/api/api-deployer/deployments",
2550
- identifiers: { deploymentId: payload.deploymentId ?? payload.id, },
2551
- payload,
2552
- };
2553
- }
2554
- case "api-deployer.save-deployment-settings":
2555
- return {
2556
- method: "PUT",
2557
- endpoint: `/public/api/api-deployer/deployments/${encodeURIComponent(id)}/settings`,
2558
- identifiers: { deploymentId: id, },
2559
- payload: requiredPlanJsonInput(flags, entry.usage),
2560
- };
2561
- case "api-deployer.deploy":
2562
- return {
2563
- method: "POST",
2564
- endpoint: `/public/api/api-deployer/deployments/${encodeURIComponent(id)}/actions/update`,
2565
- identifiers: { deploymentId: id, },
2566
- payload: {},
2567
- };
2568
- case "api-deployer.delete-deployment":
2569
- return {
2570
- method: "DELETE",
2571
- endpoint: `/public/api/api-deployer/deployments/${encodeURIComponent(id)}`,
2572
- identifiers: { deploymentId: id, },
2573
- };
2574
- case "workspace.create": {
2575
- const payload = requiredPlanJsonInput(flags, entry.usage);
2576
- return {
2577
- method: "POST",
2578
- endpoint: "/public/api/workspaces/",
2579
- identifiers: { workspaceKey: payload.workspaceKey, },
2580
- payload,
2581
- };
2582
- }
2583
- case "workspace.update-settings":
2584
- return {
2585
- method: "PUT",
2586
- endpoint: `/public/api/workspaces/${encodeURIComponent(id)}`,
2587
- identifiers: { workspaceKey: id, },
2588
- payload: requiredPlanJsonInput(flags, entry.usage),
2589
- };
2590
- case "workspace.delete":
2591
- return {
2592
- method: "DELETE",
2593
- endpoint: `/public/api/workspaces/${encodeURIComponent(id)}`,
2594
- identifiers: { workspaceKey: id, },
2595
- };
2596
- case "workspace.add-object":
2597
- return {
2598
- method: "POST",
2599
- endpoint: `/public/api/workspaces/${encodeURIComponent(id)}/objects`,
2600
- identifiers: { workspaceKey: id, },
2601
- payload: requiredPlanJsonInput(flags, entry.usage),
2602
- };
2603
- case "meaning.create": {
2604
- const body = jsonInput(flags) ?? {};
2605
- const payload = {
2606
- ...body,
2607
- id: args[0],
2608
- label: args[1],
2609
- type: args[2],
2610
- description: body.description ?? null,
2611
- entries: body.entries ?? null,
2612
- mappings: body.mappings ?? null,
2613
- pattern: body.pattern ?? null,
2614
- normalizationMode: body.normalizationMode ?? null,
2615
- detectable: body.detectable ?? false,
2616
- };
2617
- return {
2618
- method: "POST",
2619
- endpoint: "/public/api/meanings/",
2620
- identifiers: { id: args[0], },
2621
- payload,
2622
- };
2623
- }
2624
- case "meaning.update":
2625
- return {
2626
- method: "PUT",
2627
- endpoint: `/public/api/meanings/${encodeURIComponent(id)}`,
2628
- identifiers: { id, },
2629
- payload: requiredPlanJsonInput(flags, entry.usage),
2630
- };
2631
- case "meaning.delete":
2632
- return {
2633
- method: "DELETE",
2634
- endpoint: `/public/api/meanings/${encodeURIComponent(id)}`,
2635
- identifiers: { id, },
2636
- };
2637
- case "llm.list": {
2638
- const params = new URLSearchParams();
2639
- const purpose = flags["purpose"];
2640
- if (typeof purpose === "string" && purpose.trim() !== "") {
2641
- params.set("purpose", purpose.trim());
2642
- }
2643
- const query = params.size > 0 ? `?${params.toString()}` : "";
2644
- return {
2645
- method: "GET",
2646
- endpoint: projectEndpoint(`/llms/${query}`),
2647
- };
2648
- }
2649
- case "llm.completions":
2650
- case "llm.embeddings": {
2651
- const payload = requiredPlanJsonInput(flags, entry.usage);
2652
- const llmId = stringField(payload, ["llmId",]);
2653
- if (llmId === undefined || llmId.trim() === "") {
2654
- 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");
2655
- }
2656
- const queries = payload["queries"];
2657
- if (!Array.isArray(queries) || queries.length === 0) {
2658
- throw new UsageError(`queries is required and must be a non-empty array of query objects.\nUsage: ${entry.usage}`, "invalid_flag_value");
2659
- }
2660
- return {
2661
- method: "POST",
2662
- endpoint: projectEndpoint(action === "completions" ? "/llms/completions" : "/llms/embeddings"),
2663
- identifiers: { llmId, },
2664
- payload,
2665
- };
2666
- }
2667
- case "knowledge-bank.search": {
2668
- if (typeof id !== "string" || id.trim() === "") {
2669
- throw new UsageError(`knowledgeBankId must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
2670
- }
2671
- const payload = requiredPlanJsonInput(flags, entry.usage);
2672
- const query = stringField(payload, ["query",]);
2673
- if (query === undefined || query.trim() === "") {
2674
- throw new UsageError(`query is required and must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
2675
- }
2676
- return {
2677
- method: "POST",
2678
- endpoint: projectEndpoint(`/knowledge-banks/${encodeURIComponent(id)}/search`),
2679
- identifiers: { knowledgeBankId: id, },
2680
- payload,
2681
- };
2682
- }
2683
- case "knowledge-bank.clear":
2684
- if (typeof id !== "string" || id.trim() === "") {
2685
- throw new UsageError(`knowledgeBankId must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
2686
- }
2687
- return {
2688
- method: "POST",
2689
- endpoint: projectEndpoint(`/knowledge-banks/${encodeURIComponent(id)}/clear`),
2690
- identifiers: { knowledgeBankId: id, },
2691
- };
2692
- case "data-collection.create":
2693
- return {
2694
- method: "POST",
2695
- endpoint: "/public/api/data-collections/",
2696
- payload: requiredPlanJsonInput(flags, entry.usage),
2697
- };
2698
- case "data-collection.settings-set":
2699
- return {
2700
- method: "PUT",
2701
- endpoint: `/public/api/data-collections/${encodeURIComponent(id)}`,
2702
- identifiers: { dataCollectionId: id, },
2703
- payload: requiredPlanJsonInput(flags, entry.usage),
2704
- };
2705
- case "data-collection.delete":
2706
- return {
2707
- method: "DELETE",
2708
- endpoint: `/public/api/data-collections/${encodeURIComponent(id)}`,
2709
- identifiers: { dataCollectionId: id, },
2710
- };
2711
- case "data-collection.add-object": {
2712
- const payload = requiredPlanJsonInput(flags, entry.usage);
2713
- const cid = id
2714
- ?? (typeof payload["dataCollectionId"] === "string"
2715
- ? payload["dataCollectionId"]
2716
- : undefined);
2717
- if (!cid)
2718
- throw new UsageError(`Usage: ${entry.usage}`);
2719
- const reference = { ...payload, };
2720
- delete reference["dataCollectionId"];
2721
- return {
2722
- method: "POST",
2723
- endpoint: `/public/api/data-collections/${encodeURIComponent(cid)}/objects`,
2724
- identifiers: { dataCollectionId: cid, },
2725
- payload: reference,
2726
- };
2727
- }
2728
- case "data-collection.remove-dataset": {
2729
- const projectKeyArg = args[1];
2730
- const datasetName = args[2];
2731
- if (!projectKeyArg || !datasetName) {
2732
- throw new UsageError(`Usage: ${entry.usage}`);
2733
- }
2734
- return {
2735
- method: "DELETE",
2736
- endpoint: `/public/api/data-collections/${encodeURIComponent(id)}/objects/dataset/${encodeURIComponent(projectKeyArg)}/${encodeURIComponent(datasetName)}`,
2737
- identifiers: { dataCollectionId: id, projectKey: projectKeyArg, datasetName, },
2738
- };
2739
- }
2740
- case "project.tags-set":
2741
- return {
2742
- method: "PUT",
2743
- endpoint: encodedProjectEndpointForPlan(projectKey ?? requiredPlanProjectKey(flags, entry.usage), "/tags"),
2744
- identifiers: { projectKey: projectKey ?? requiredPlanProjectKey(flags, entry.usage), },
2745
- payload: requiredPlanJsonInput(flags, entry.usage),
2746
- };
2747
- case "project.metadata-set":
2748
- return {
2749
- method: "PUT",
2750
- endpoint: encodedProjectEndpointForPlan(projectKey ?? requiredPlanProjectKey(flags, entry.usage), "/metadata"),
2751
- identifiers: { projectKey: projectKey ?? requiredPlanProjectKey(flags, entry.usage), },
2752
- payload: requiredPlanJsonInput(flags, entry.usage),
2753
- };
2754
- case "macro.run":
2755
- case "macro.run-and-wait": {
2756
- // The plan payload mirrors the live POST body exactly: params and
2757
- // adminParams when provided via --data, {} otherwise. Validation is
2758
- // identical to the live path (JSON object shape), zero network.
2759
- const data = optionalJsonFlag(flags, "data");
2760
- const runPayload = {};
2761
- if (data && typeof data === "object" && !Array.isArray(data)) {
2762
- if (data["params"] !== undefined)
2763
- runPayload["params"] = data["params"];
2764
- if (data["adminParams"] !== undefined)
2765
- runPayload["adminParams"] = data["adminParams"];
2766
- }
2767
- return {
2768
- method: "POST",
2769
- endpoint: projectEndpoint(`/runnables/${encodeURIComponent(id)}?wait=false`),
2770
- identifiers: { id, },
2771
- payload: runPayload,
2772
- wait: action === "macro.run-and-wait" || flags["wait"] === true,
2773
- };
2774
- }
2775
- case "macro.abort": {
2776
- const runId = args[1];
2777
- if (!runId)
2778
- throw new UsageError(`Usage: ${entry.usage}`);
2779
- return {
2780
- method: "POST",
2781
- endpoint: projectEndpoint(`/runnables/${encodeURIComponent(id)}/abort/${encodeURIComponent(runId)}`),
2782
- identifiers: { id, runId, },
2783
- };
2784
- }
2785
- case "recipe.metadata-set":
2786
- return {
2787
- method: "PUT",
2788
- endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}/metadata`),
2789
- identifiers: { recipeName: id, },
2790
- payload: requiredPlanJsonInput(flags, entry.usage),
2791
- };
2792
- case "user.create":
2793
- return {
2794
- method: "POST",
2795
- endpoint: "/public/api/admin/users",
2796
- payload: redactedUserPayload(requiredPlanJsonInput(flags, entry.usage)),
2797
- };
2798
- case "group.create":
2799
- return {
2800
- method: "POST",
2801
- endpoint: "/public/api/admin/groups",
2802
- payload: requiredPlanJsonInput(flags, entry.usage),
2803
- };
2804
- case "saved-model.create-external": {
2805
- const config = optionalJsonFlag(flags, "configuration") ?? optionalJsonFlag(flags, "data");
2806
- return {
2807
- method: "POST",
2808
- endpoint: projectEndpoint("/savedmodels/"),
2809
- identifiers: { name: id, },
2810
- payload: {
2811
- savedModelType: requiredPlanFlag(flags, "type", entry.usage),
2812
- name: id,
2813
- ...(typeof flags["prediction-type"] === "string"
2814
- ? { predictionType: flags["prediction-type"], }
2815
- : {}),
2816
- ...(config ? { proxyModelConfiguration: config, } : {}),
2817
- },
2818
- };
2819
- }
2820
- case "saved-model.update-settings":
2821
- return {
2822
- method: "PUT",
2823
- endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}`),
2824
- identifiers: { savedModelId: id, },
2825
- payload: requiredPlanJsonInput(flags, entry.usage),
2826
- };
2827
- case "saved-model.delete-versions": {
2828
- const versionsRaw = args[1];
2829
- if (typeof versionsRaw !== "string" || versionsRaw.trim() === "") {
2830
- throw new UsageError(`Usage: ${entry.usage}`);
2831
- }
2832
- return {
2833
- method: "POST",
2834
- endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/actions/delete-versions`),
2835
- identifiers: { savedModelId: id, },
2836
- payload: {
2837
- versions: versionsRaw.split(",").map((v) => v.trim()).filter((v) => v.length > 0),
2838
- removeIntermediate: parseBooleanOption(flags["remove-intermediate"], "--remove-intermediate") ?? true,
2839
- },
2840
- };
2841
- }
2842
- case "saved-model.import-mlflow-version":
2843
- return {
2844
- method: "POST",
2845
- endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}`),
2846
- identifiers: { savedModelId: id, versionId: args[1], },
2847
- payload: {
2848
- source: { kind: "local-archive", archive: requiredPlanFlag(flags, "archive", entry.usage), },
2849
- ...(typeof flags["code-env"] === "string"
2850
- ? { codeEnvName: flags["code-env"], }
2851
- : {}),
2852
- // Execute always sends the query parameter with the
2853
- // external-caller default NONE; the plan mirrors it.
2854
- containerExecConfigName: typeof flags["container-exec-config"] === "string"
2855
- ? flags["container-exec-config"]
2856
- : "NONE",
2857
- setActive: parseBooleanOption(flags["set-active"], "--set-active") ?? true,
2858
- ...(flags["binary-classification-threshold"] !== undefined
2859
- ? {
2860
- binaryClassificationThreshold: num(flags["binary-classification-threshold"], "--binary-classification-threshold"),
2861
- }
2862
- : {}),
2863
- },
2864
- };
2865
- case "saved-model.import-mlflow-version-from-folder":
2866
- return {
2867
- exact: false,
2868
- reason: "Apply sends these values as query parameters (with codeEnvName and binaryClassificationThreshold defaults) on an empty multipart body; the payload shows them structured.",
2869
- method: "POST",
2870
- endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}`),
2871
- identifiers: { savedModelId: id, versionId: args[1], },
2872
- payload: {
2873
- source: {
2874
- kind: "managed-folder",
2875
- folderRef: requiredPlanFlag(flags, "folder", entry.usage),
2876
- path: flags["path"],
2877
- },
2878
- ...(typeof flags["code-env"] === "string" ? { codeEnvName: flags["code-env"], } : {}),
2879
- // Execute always sends the query parameter with the
2880
- // external-caller default NONE; the plan mirrors it.
2881
- containerExecConfigName: typeof flags["container-exec-config"] === "string"
2882
- ? flags["container-exec-config"]
2883
- : "NONE",
2884
- setActive: parseBooleanOption(flags["set-active"], "--set-active") ?? true,
2885
- },
2886
- };
2887
- case "saved-model.external-metadata-put": {
2888
- // DSS requires containerExecConfigName on this endpoint; for an
2889
- // external API caller it resolves as LOCAL-CONFIG -> NONE, so the
2890
- // plan mirrors the NONE default unless --container-exec-config
2891
- // overrides it. Same contract as execute.
2892
- const containerExecConfigName = typeof flags["container-exec-config"] === "string"
2893
- ? flags["container-exec-config"]
2894
- : "NONE";
2895
- return {
2896
- method: "PUT",
2897
- endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}/external-ml/metadata?containerExecConfigName=${encodeURIComponent(containerExecConfigName)}`),
2898
- identifiers: { savedModelId: id, versionId: args[1], },
2899
- payload: requiredPlanJsonInput(flags, entry.usage),
2900
- };
2901
- }
2902
- case "saved-model.evaluate-version": {
2903
- const payload = {
2904
- datasetRef: requiredPlanFlag(flags, "dataset", entry.usage),
2905
- // Execute always sends an explicit containerExecConfigName
2906
- // (official external-caller semantics: LOCAL-CONFIG -> NONE);
2907
- // the plan mirrors that default and any explicit override.
2908
- containerExecConfigName: typeof flags["container-exec-config"] === "string"
2909
- ? flags["container-exec-config"]
2910
- : "NONE",
2911
- };
2912
- // --sampling is a JSON flag value; the plan carries the parsed
2913
- // object under the same key as the execute body (samplingParam).
2914
- if (flags["sampling"] !== undefined) {
2915
- payload.samplingParam = json(flags["sampling"], "--sampling");
2916
- }
2917
- return {
2918
- method: "POST",
2919
- 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}`,
2920
- identifiers: { savedModelId: id, versionId: args[1], },
2921
- payload,
2922
- };
2923
- }
2924
- case "saved-model.set-user-meta":
2925
- return {
2926
- method: "PUT",
2927
- endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}/user-meta`),
2928
- identifiers: { savedModelId: id, versionId: args[1], },
2929
- payload: requiredPlanJsonInput(flags, entry.usage),
2930
- };
2931
- case "user.update":
2932
- return {
2933
- method: "PUT",
2934
- endpoint: `/public/api/admin/users/${encodeURIComponent(id)}`,
2935
- identifiers: { login: id, },
2936
- payload: redactedUserPayload(requiredPlanJsonInput(flags, entry.usage)),
2937
- };
2938
- case "user.delete":
2939
- case "group.delete":
2940
- return {
2941
- method: "DELETE",
2942
- endpoint: resource === "user"
2943
- ? `/public/api/admin/users/${encodeURIComponent(id)}`
2944
- : `/public/api/admin/groups/${encodeURIComponent(id)}`,
2945
- identifiers: resource === "user" ? { login: id, } : { name: id, },
2946
- };
2947
- case "user.resync":
2948
- return {
2949
- method: "POST",
2950
- endpoint: `/public/api/admin/users/${encodeURIComponent(id)}/actions/resync`,
2951
- identifiers: { login: id, },
2952
- };
2953
- case "user.resync-multi": {
2954
- const logins = requiredPlanCsv(flags, "logins", entry.usage);
2955
- return {
2956
- method: "POST",
2957
- endpoint: "/public/api/admin/users/actions/resync-multi",
2958
- identifiers: { count: logins.length, },
2959
- payload: logins,
2960
- };
2961
- }
2962
- case "user.external-users":
2963
- return {
2964
- method: "GET",
2965
- endpoint: "/public/api/admin/users/actions/external-users",
2966
- };
2967
- case "user.external-groups":
2968
- return {
2969
- method: "GET",
2970
- endpoint: "/public/api/admin/users/actions/external-groups",
2971
- };
2972
- case "connection.test":
2973
- return {
2974
- method: "GET",
2975
- endpoint: `/public/api/connections/${encodeURIComponent(id)}/test`,
2976
- identifiers: { connectionName: id, },
2977
- };
2978
- case "user.provision":
2979
- return {
2980
- method: "POST",
2981
- endpoint: "/public/api/admin/users/actions/provision",
2982
- payload: requiredPlanJsonInput(flags, entry.usage),
2983
- };
2984
- case "group.update":
2985
- return {
2986
- method: "PUT",
2987
- endpoint: `/public/api/admin/groups/${encodeURIComponent(id)}`,
2988
- identifiers: { name: id, },
2989
- payload: requiredPlanJsonInput(flags, entry.usage),
2990
- };
2991
- case "connection.create":
2992
- return {
2993
- method: "POST",
2994
- endpoint: "/public/api/admin/connections",
2995
- payload: redactedConnectionPayload(requiredPlanJsonInput(flags, entry.usage)),
2996
- };
2997
- case "connection.update":
2998
- return {
2999
- method: "PUT",
3000
- endpoint: `/public/api/admin/connections/${encodeURIComponent(id)}`,
3001
- identifiers: { connectionName: id, },
3002
- payload: redactedConnectionPayload(requiredPlanJsonInput(flags, entry.usage)),
3003
- };
3004
- case "connection.delete":
3005
- return {
3006
- method: "DELETE",
3007
- endpoint: `/public/api/admin/connections/${encodeURIComponent(id)}`,
3008
- identifiers: { connectionName: id, },
3009
- };
3010
- case "connection.prepare-import": {
3011
- const payload = requiredPlanJsonInput(flags, entry.usage);
3012
- if (!projectKey)
3013
- throw new UsageError(`Missing project key for ${resource} ${action}.`);
3014
- return {
3015
- method: "POST",
3016
- endpoint: encodedProjectEndpointForPlan(projectKey, "/datasets/tables-import/actions/prepare-from-keys"),
3017
- identifiers: { projectKey, },
3018
- payload,
3019
- };
3020
- }
3021
- case "connection.execute-import": {
3022
- const payload = requiredPlanJsonInput(flags, entry.usage);
3023
- if (!projectKey)
3024
- throw new UsageError(`Missing project key for ${resource} ${action}.`);
3025
- return {
3026
- method: "POST",
3027
- endpoint: encodedProjectEndpointForPlan(projectKey, "/datasets/tables-import/actions/execute-from-candidates"),
3028
- identifiers: { projectKey, },
3029
- payload,
3030
- };
3031
- }
3032
- case "plugin.install-from-zip":
3033
- return {
3034
- method: "POST",
3035
- endpoint: "/public/api/plugins/actions/installFromZip",
3036
- payload: { file: requiredPlanFlag(flags, "file", entry.usage), upload: "multipart", },
3037
- };
3038
- case "plugin.install-from-store":
3039
- return {
3040
- method: "POST",
3041
- endpoint: "/public/api/plugins/actions/installFromStore",
3042
- identifiers: { pluginId: id, },
3043
- payload: { pluginId: id, },
3044
- };
3045
- case "plugin.install-from-git": {
3046
- const payload = pluginGitPlanPayload(flags);
3047
- return {
3048
- method: "POST",
3049
- endpoint: "/public/api/plugins/actions/installFromGit",
3050
- payload,
3051
- };
3052
- }
3053
- case "plugin.update-from-zip":
3054
- return {
3055
- method: "POST",
3056
- endpoint: pluginActionEndpoint(id, "updateFromZip"),
3057
- identifiers: { pluginId: id, },
3058
- payload: { file: requiredPlanFlag(flags, "file", entry.usage), upload: "multipart", },
3059
- };
3060
- case "plugin.update-from-store":
3061
- return {
3062
- method: "POST",
3063
- endpoint: pluginActionEndpoint(id, "updateFromStore"),
3064
- identifiers: { pluginId: id, },
3065
- };
3066
- case "plugin.update-from-git":
3067
- return {
3068
- method: "POST",
3069
- endpoint: pluginActionEndpoint(id, "updateFromGit"),
3070
- identifiers: { pluginId: id, },
3071
- payload: pluginGitPlanPayload(flags),
3072
- };
3073
- case "plugin.settings-set": {
3074
- const scope = optionalPlanProjectScope(flags);
3075
- return {
3076
- method: "POST",
3077
- endpoint: `${pluginRootEndpoint(id, "/settings")}${scope}`,
3078
- identifiers: { pluginId: id, },
3079
- payload: { configKeys: settingsConfigKeys(flags), },
3080
- };
3081
- }
3082
- case "plugin.code-env-create": {
3083
- const conda = parseBooleanOption(flags["conda"], "--conda") ?? false;
3084
- return {
3085
- method: "POST",
3086
- endpoint: `${pluginRootEndpoint(id, "/code-env/actions/create")}`,
3087
- identifiers: { pluginId: id, },
3088
- payload: {
3089
- deploymentMode: "PLUGIN_MANAGED",
3090
- conda,
3091
- pythonInterpreter: flags["python-interpreter"] ?? null,
3092
- },
3093
- wait: flags["wait"] === true,
3094
- };
3095
- }
3096
- case "plugin.code-env-update":
3097
- return {
3098
- method: "POST",
3099
- endpoint: `${pluginRootEndpoint(id, "/code-env/actions/update")}`,
3100
- identifiers: { pluginId: id, },
3101
- wait: flags["wait"] === true,
3102
- };
3103
- case "plugin.move-to-dev":
3104
- return {
3105
- method: "POST",
3106
- endpoint: pluginActionEndpoint(id, "moveToDev"),
3107
- identifiers: { pluginId: id, },
3108
- };
3109
- case "plugin.delete":
3110
- return {
3111
- method: "POST",
3112
- endpoint: pluginActionEndpoint(id, "delete"),
3113
- identifiers: { pluginId: id, },
3114
- payload: { force: parseBooleanOption(flags["force"], "--force") ?? false, },
3115
- };
3116
- case "plugin.create-dev": {
3117
- const creationMode = requiredPlanFlag(flags, "creation-mode", entry.usage);
3118
- const needsGit = creationMode !== "EMPTY";
3119
- // Mirrors PluginsResource.createDev: git fields are always present, null when unused.
3120
- const payload = {
3121
- pluginId: id,
3122
- creationMode,
3123
- gitRepository: needsGit && typeof flags["repository"] === "string"
3124
- ? validatedPlanRepositoryUrl(flags["repository"], "repository", "--repository URL")
3125
- : null,
3126
- gitCheckout: needsGit && typeof flags["checkout"] === "string" ? flags["checkout"] : null,
3127
- gitSubpath: creationMode === "GIT_EXPORT" && typeof flags["path-in-repository"] === "string"
3128
- ? flags["path-in-repository"]
3129
- : null,
3130
- };
3131
- return {
3132
- method: "POST",
3133
- endpoint: "/public/api/plugins/actions/createDev",
3134
- identifiers: { pluginId: id, creationMode, },
3135
- payload,
3136
- };
3137
- }
3138
- case "plugin.set-git-remote": {
3139
- const repository = requiredPlanFlag(flags, "repository", entry.usage);
3140
- return {
3141
- method: "POST",
3142
- endpoint: `${pluginRootEndpoint(id, "/gitRemote")}`,
3143
- identifiers: { pluginId: id, },
3144
- payload: { repositoryUrl: validatedPlanRepositoryUrl(repository, "repository", entry.usage), },
3145
- };
3146
- }
3147
- case "plugin.delete-git-remote":
3148
- return {
3149
- method: "DELETE",
3150
- endpoint: pluginRootEndpoint(id, "/gitRemote"),
3151
- identifiers: { pluginId: id, },
3152
- };
3153
- case "plugin.push":
3154
- case "plugin.pull":
3155
- case "plugin.fetch":
3156
- case "plugin.reset-local":
3157
- case "plugin.reset-remote": {
3158
- const actionMap = {
3159
- "plugin.push": "push",
3160
- "plugin.pull": "pullRebase",
3161
- "plugin.fetch": "fetch",
3162
- "plugin.reset-local": "resetToLocalHeadState",
3163
- "plugin.reset-remote": "resetToRemoteHeadState",
3164
- };
3165
- return {
3166
- method: "POST",
3167
- endpoint: pluginActionEndpoint(id, actionMap[`${resource}.${action}`]),
3168
- identifiers: { pluginId: id, },
3169
- };
3170
- }
3171
- case "plugin.contents-put":
3172
- return {
3173
- method: "POST",
3174
- endpoint: pluginContentsEndpoint(id, args[1] ?? ""),
3175
- identifiers: { pluginId: id, path: args[1], },
3176
- payload: { contentSource: contentSourceKind(flags), },
3177
- };
3178
- case "plugin.contents-delete":
3179
- return {
3180
- method: "DELETE",
3181
- endpoint: pluginContentsEndpoint(id, args[1] ?? ""),
3182
- identifiers: { pluginId: id, path: args[1], },
3183
- };
3184
- case "plugin.folder-add":
3185
- return {
3186
- method: "POST",
3187
- endpoint: pluginFoldersEndpoint(id, args[1] ?? ""),
3188
- identifiers: { pluginId: id, path: args[1], },
3189
- };
3190
- case "plugin.rename": {
3191
- if (!args[2])
3192
- throw new UsageError(`Usage: ${entry.usage}`);
3193
- return {
3194
- method: "POST",
3195
- endpoint: `${pluginRootEndpoint(id, "/contents-actions/rename")}`,
3196
- identifiers: { pluginId: id, path: args[1], },
3197
- payload: { oldPath: `/${validatePluginPath(args[1] ?? "")}`, newName: args[2], },
3198
- };
3199
- }
3200
- case "plugin.move": {
3201
- const destination = args[2];
3202
- if (!destination)
3203
- throw new UsageError(`Usage: ${entry.usage}`);
3204
- return {
3205
- method: "POST",
3206
- endpoint: `${pluginRootEndpoint(id, "/contents-actions/move")}`,
3207
- identifiers: { pluginId: id, path: args[1], },
3208
- payload: {
3209
- oldPath: `/${validatePluginPath(args[1] ?? "")}`,
3210
- newPath: validatePluginDestinationPath(destination),
3211
- },
3212
- };
3213
- }
3214
- case "business-app.save-settings":
3215
- return {
3216
- method: "PUT",
3217
- endpoint: `/public/api/business-apps/${encodeURIComponent(id)}/settings`,
3218
- identifiers: { id, },
3219
- payload: requiredPlanJsonInput(flags, entry.usage),
3220
- };
3221
- case "business-app.create-instance":
3222
- return {
3223
- method: "POST",
3224
- endpoint: `/public/api/business-apps/${encodeURIComponent(id)}/instances`,
3225
- identifiers: { id, },
3226
- payload: requiredPlanJsonInput(flags, entry.usage),
3227
- };
3228
- case "business-app.upgrade-instance":
3229
- return {
3230
- method: "POST",
3231
- endpoint: `/public/api/business-apps/${encodeURIComponent(args[0])}/instances/${encodeURIComponent(args[1])}/upgrade`,
3232
- identifiers: { id: args[0], projectKey: args[1], },
3233
- payload: {},
3234
- };
3235
- case "business-app.install-from-archive":
3236
- return {
3237
- method: "POST",
3238
- endpoint: "/public/api/business-apps/install-from-archive",
3239
- identifiers: { filePath: id, },
3240
- payload: uploadPayload(id),
3241
- };
3242
- case "app.save-instance-manifest": {
3243
- const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
3244
- return {
3245
- method: "PUT",
3246
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`,
3247
- identifiers: { projectKey: targetProjectKey, },
3248
- payload: requiredPlanJsonInput(flags, entry.usage),
3249
- };
3250
- }
3251
- case "app.create-instance": {
3252
- const payload = requiredPlanJsonInput(flags, entry.usage);
3253
- if (payload["targetProjectKey"] === undefined) {
3254
- // Generated-at-apply mode: --plan never allocates or reserves a
3255
- // random key. The payload stays exactly as supplied, the runtime
3256
- // generates the key (and defaults the display name to it) during
3257
- // apply, and no absence probe runs for a generated key, so the
3258
- // plan carries no preflight requests and no fake concrete GET
3259
- // path: `{targetProjectKey}` resolves at apply time.
3260
- return {
3261
- exact: false,
3262
- reason: "targetProjectKey (and targetProjectName when omitted) are generated during apply; the sent body adds them.",
3263
- method: "POST",
3264
- endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
3265
- identifiers: {
3266
- appId: id,
3267
- targetProjectKeyGeneratedDuringApply: true,
3268
- ...(payload["targetProjectName"] === undefined
3269
- ? { targetProjectNameGeneratedDuringApply: true, }
3270
- : {}),
3271
- preflightExecuted: false,
3272
- preflightWillRunDuringApply: false,
3273
- preflightRequests: [],
3274
- incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
3275
- incarnationObservationRequests: [
3276
- {
3277
- method: "GET",
3278
- endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3279
- when: flags["wait"] === true
3280
- ? "after-terminal-future-target"
3281
- : "conditional-inline-hasResult-target",
3282
- intent: "After DSS reports inline or terminal creation success for the generated key, observe creationTag for later cleanup binding.",
3283
- },
3284
- ],
3285
- 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.",
3286
- },
3287
- payload,
3288
- wait: flags["wait"] === true,
3289
- };
3290
- }
3291
- const rawTargetProjectKey = stringField(payload, ["targetProjectKey",]);
3292
- if (!rawTargetProjectKey || rawTargetProjectKey.trim() === "") {
3293
- throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${entry.usage}`);
3294
- }
3295
- // The runtime trims and rewrites body.targetProjectKey; the plan
3296
- // advertises the normalized identifier and payload.
3297
- const targetProjectKey = rawTargetProjectKey.trim();
3298
- const normalizedPayload = { ...payload, targetProjectKey, };
3299
- return {
3300
- method: "POST",
3301
- endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
3302
- identifiers: {
3303
- appId: id,
3304
- targetProjectKey,
3305
- preflightExecuted: false,
3306
- preflightWillRunDuringApply: true,
3307
- incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
3308
- incarnationObservationRequests: [
3309
- {
3310
- method: "GET",
3311
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
3312
- when: flags["wait"] === true
3313
- ? "after-terminal-future-target"
3314
- : "conditional-inline-hasResult-target",
3315
- intent: "After DSS reports inline or terminal creation success for the requested key, observe creationTag for later cleanup binding.",
3316
- },
3317
- ],
3318
- preflightRequests: [
3319
- {
3320
- method: "GET",
3321
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
3322
- when: "before-create",
3323
- intent: "Require the target project to be absent before creating the instance.",
3324
- },
3325
- {
3326
- method: "GET",
3327
- endpoint: "/public/api/projects/",
3328
- when: "conditional",
3329
- intent: "Fallback list probe issued only when the direct project GET is forbidden (403). Presence proves a collision; absence cannot prove availability.",
3330
- },
3331
- {
3332
- method: "GET",
3333
- endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
3334
- when: "conditional",
3335
- 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.",
3336
- },
3337
- ],
3338
- note: "Strict preflight: the instance POST runs only after the payload targetProjectKey is confirmed absent. Creation never writes into an existing or unprovable project.",
3339
- },
3340
- payload: normalizedPayload,
3341
- wait: flags["wait"] === true,
3342
- };
3343
- }
3344
- case "app.set-manifest-version": {
3345
- const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
3346
- const payloadPatch = {};
3347
- const version = flags["manifest-version"];
3348
- if (version !== undefined) {
3349
- if (version.trim() === "") {
3350
- throw new UsageError("App manifest version must be a non-empty string.", "validation_failed", `Usage: ${entry.usage}`);
3351
- }
3352
- payloadPatch.version = version;
3353
- }
3354
- const versionNotes = flags["version-notes"];
3355
- if (versionNotes !== undefined)
3356
- payloadPatch.versionNotes = versionNotes;
3357
- if (version === undefined && versionNotes === undefined) {
3358
- throw new UsageError("At least one of --manifest-version or --version-notes is required.", "usage_error", `Usage: ${entry.usage}`);
3359
- }
3360
- const expectHash = flags["expect-hash"];
3361
- if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3362
- throw new UsageError("Expected manifest hash must be a 64-character SHA-256 hex digest.", "validation_failed", `Usage: ${entry.usage}`);
3363
- }
3364
- return {
3365
- method: "PUT",
3366
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`,
3367
- identifiers: {
3368
- projectKey: targetProjectKey,
3369
- payloadPatch,
3370
- ...(expectHash !== undefined ? { expectHash: expectHash.toLowerCase(), } : {}),
3371
- concurrencyControl: APP_MANIFEST_CONCURRENCY_CONTROL,
3372
- staleReadCheck: expectHash === undefined
3373
- ? "none"
3374
- : "client-side-expect-hash-compare-before-put",
3375
- note: expectHash === undefined
3376
- ? "Unconditional PUT: no stale-read check is armed because --expect-hash was not supplied."
3377
- : "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.",
3378
- },
3379
- };
3380
- }
3381
- case "app.create-successor-instance": {
3382
- const sourceProjectKey = requiredPlanFlag(flags, "from", entry.usage);
3383
- const targetProjectKey = optionalPlanFlag(flags, "to", entry.usage);
3384
- if (targetProjectKey !== undefined && sourceProjectKey === targetProjectKey) {
3385
- throw new UsageError("--from and --to must be different project keys.", "validation_failed", `Usage: ${entry.usage}`);
3386
- }
3387
- const targetProjectName = flags["name"];
3388
- const copyPermissions = parseBooleanOption(flags["copy-permissions"], "--copy-permissions")
3389
- ?? false;
3390
- if (targetProjectKey === undefined) {
3391
- // Generated-at-apply mode: --plan never allocates a key and no
3392
- // target exists yet, so the plan makes no target-absence claim
3393
- // and advertises no target probe. The key is generated once
3394
- // during apply, immediately before the single instance POST;
3395
- // `{targetProjectKey}` resolves only after the terminal future
3396
- // names it. Source and template gates still run before the POST.
3397
- return {
3398
- method: "POST",
3399
- endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
3400
- identifiers: {
3401
- appId: id,
3402
- sourceProjectKey,
3403
- targetProjectKeyGeneratedDuringApply: true,
3404
- targetPreflight: "not-applicable-generated-key",
3405
- preflightExecuted: false,
3406
- preflightWillRunDuringApply: true,
3407
- ...(targetProjectName !== undefined
3408
- ? { targetProjectName, }
3409
- : { targetProjectNameGeneratedDuringApply: true, }),
3410
- copyPermissions,
3411
- incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
3412
- incarnationObservationRequests: [
3413
- {
3414
- method: "GET",
3415
- endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3416
- when: "after-terminal-future-target",
3417
- intent: "After the terminal future names the generated successor key, observe creationTag and bind later target checks and cleanup to that hash.",
3418
- },
3419
- ],
3420
- preflightRequests: [
3421
- {
3422
- method: "GET",
3423
- endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
3424
- when: "before-create",
3425
- intent: "Verify the --from project is a registered instance of the app.",
3426
- },
3427
- {
3428
- method: "GET",
3429
- endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/app-manifest`,
3430
- when: "before-create",
3431
- intent: "Verify the --from project is an APP_INSTANCE project.",
3432
- },
3433
- ],
3434
- ...(copyPermissions
3435
- ? {
3436
- permissionConcurrencyControl: "client-side-non-atomic-stale-identity-and-hash-checks",
3437
- permissionRequests: [
3438
- {
3439
- method: "GET",
3440
- endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
3441
- intent: "Snapshot the predecessor instance ACL before creation.",
3442
- },
3443
- {
3444
- method: "GET",
3445
- endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
3446
- intent: "Read the generated successor ACL to decide whether the copy is a no-op.",
3447
- },
3448
- {
3449
- method: "GET",
3450
- endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3451
- intent: "Recheck generated successor creationTag after reading its ACL; stop if the project key was reused.",
3452
- },
3453
- {
3454
- method: "GET",
3455
- endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
3456
- intent: "Recheck the predecessor ACL immediately before the write and stop if its hash drifted.",
3457
- },
3458
- {
3459
- method: "GET",
3460
- endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3461
- intent: "Recheck generated successor creationTag immediately before the unconditional permission PUT.",
3462
- },
3463
- {
3464
- method: "PUT",
3465
- endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
3466
- intent: "Apply the predecessor ACL snapshot to the generated successor instance.",
3467
- },
3468
- {
3469
- method: "GET",
3470
- endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
3471
- intent: "Verify the generated successor ACL hash equals the predecessor snapshot hash.",
3472
- },
3473
- {
3474
- method: "GET",
3475
- endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3476
- intent: "Detect generated successor project-key reuse across the permission write and verification read.",
3477
- },
3478
- ],
3479
- }
3480
- : {}),
3481
- 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.",
3482
- },
3483
- payload: targetProjectName !== undefined ? { targetProjectName, } : {},
3484
- wait: true,
3485
- };
3486
- }
3487
- return {
3488
- method: "POST",
3489
- endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
3490
- identifiers: {
3491
- appId: id,
3492
- sourceProjectKey,
3493
- targetProjectKey,
3494
- preflightExecuted: false,
3495
- preflightWillRunDuringApply: true,
3496
- ...(targetProjectName !== undefined ? { targetProjectName, } : {}),
3497
- copyPermissions,
3498
- incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
3499
- postFutureRequests: [
3500
- {
3501
- method: "GET",
3502
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
3503
- intent: "After the terminal future names the successor key, observe creationTag and bind later target checks and cleanup to that hash.",
3504
- },
3505
- ],
3506
- ...(copyPermissions
3507
- ? {
3508
- permissionConcurrencyControl: "client-side-non-atomic-stale-identity-and-hash-checks",
3509
- }
3510
- : {}),
3511
- preflightRequests: [
3512
- {
3513
- method: "GET",
3514
- endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
3515
- when: "before-create",
3516
- intent: "Verify the --from project is a registered instance of the app.",
3517
- },
3518
- {
3519
- method: "GET",
3520
- endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/app-manifest`,
3521
- when: "before-create",
3522
- intent: "Verify the --from project is an APP_INSTANCE project.",
3523
- },
3524
- {
3525
- method: "GET",
3526
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
3527
- when: "before-create",
3528
- intent: "Require the --to target project to be absent before creating the successor.",
3529
- },
3530
- {
3531
- method: "GET",
3532
- endpoint: "/public/api/projects/",
3533
- when: "conditional",
3534
- 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.",
3535
- },
3536
- ],
3537
- ...(copyPermissions
3538
- ? {
3539
- permissionRequests: [
3540
- {
3541
- method: "GET",
3542
- endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
3543
- intent: "Snapshot the predecessor instance ACL before creation.",
3544
- },
3545
- {
3546
- method: "GET",
3547
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
3548
- intent: "Read the successor ACL to decide whether the copy is a no-op.",
3549
- },
3550
- {
3551
- method: "GET",
3552
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
3553
- intent: "Recheck successor creationTag after reading its ACL; stop if the project key was reused.",
3554
- },
3555
- {
3556
- method: "GET",
3557
- endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
3558
- intent: "Recheck the predecessor ACL immediately before the write and stop if its hash drifted.",
3559
- },
3560
- {
3561
- method: "GET",
3562
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
3563
- intent: "Recheck successor creationTag immediately before the unconditional permission PUT.",
3564
- },
3565
- {
3566
- method: "PUT",
3567
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
3568
- intent: "Apply the predecessor ACL snapshot to the successor instance.",
3569
- },
3570
- {
3571
- method: "GET",
3572
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`,
3573
- intent: "Verify the successor ACL hash equals the predecessor snapshot hash.",
3574
- },
3575
- {
3576
- method: "GET",
3577
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`,
3578
- intent: "Detect successor project-key reuse across the permission write and verification read.",
3579
- },
3580
- ],
3581
- }
3582
- : {}),
3583
- 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.",
3584
- },
3585
- payload: {
3586
- targetProjectKey,
3587
- targetProjectName: targetProjectName ?? targetProjectKey,
3588
- },
3589
- wait: true,
3590
- };
3591
- }
3592
- case "app.delete-instance": {
3593
- const targetProjectKey = requiredPlanFlag(flags, "project-key", entry.usage);
3594
- const futureId = flags["future-id"] === undefined
3595
- ? undefined
3596
- : requiredPlanFlag(flags, "future-id", entry.usage);
3597
- const expectedProjectIncarnation = flags["expect-project-incarnation"] === undefined
3598
- ? undefined
3599
- : requiredPlanFlag(flags, "expect-project-incarnation", entry.usage);
3600
- if (expectedProjectIncarnation !== undefined
3601
- && !/^[0-9a-f]{64}$/.test(expectedProjectIncarnation)) {
3602
- throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
3603
- }
3604
- const unconfirmedCreation = parseBooleanOption(flags["unconfirmed-creation"], "--unconfirmed-creation") ?? false;
3605
- const manifestProbe = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/app-manifest`;
3606
- const projectDetailsProbe = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`;
3607
- const typeValidationRequests = [
3608
- {
3609
- method: "GET",
3610
- endpoint: manifestProbe,
3611
- when: "before-delete",
3612
- intent: "Verify the target is an APP_INSTANCE project before deleting.",
3613
- },
3614
- {
3615
- method: "GET",
3616
- endpoint: projectDetailsProbe,
3617
- when: expectedProjectIncarnation === undefined
3618
- ? "conditional-type-check-before-delete"
3619
- : "incarnation-and-conditional-type-check-before-delete",
3620
- intent: expectedProjectIncarnation === undefined
3621
- ? "Fallback probe issued only when the app-manifest response omits projectAppType (live DSS does)."
3622
- : "Recompute the current project-incarnation hash from creationTag after the manifest probe; the same response supplies projectAppType when the manifest omits it.",
3623
- },
3624
- ];
3625
- const preflightRequests = typeValidationRequests;
3626
- if (unconfirmedCreation) {
3627
- return {
3628
- identifiers: {
3629
- projectKey: targetProjectKey,
3630
- ...(futureId !== undefined ? { futureId, } : {}),
3631
- unconfirmedCreation: true,
3632
- 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.",
3633
- },
3634
- };
3635
- }
3636
- if (futureId === undefined) {
3637
- return {
3638
- method: "DELETE",
3639
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}`,
3640
- identifiers: {
3641
- projectKey: targetProjectKey,
3642
- ...(expectedProjectIncarnation === undefined
3643
- ? {}
3644
- : {
3645
- projectIncarnationGate: {
3646
- required: false,
3647
- provided: true,
3648
- expectedHash: expectedProjectIncarnation,
3649
- },
3650
- incarnationControl: "client-side-non-atomic-stale-identity-check",
3651
- }),
3652
- preflightRequests,
3653
- note: expectedProjectIncarnation === undefined
3654
- ? "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."
3655
- : "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.",
3656
- },
3657
- };
3658
- }
3659
- return {
3660
- method: "DELETE",
3661
- endpoint: `/public/api/projects/${encodeURIComponent(targetProjectKey)}`,
3662
- identifiers: {
3663
- projectKey: targetProjectKey,
3664
- futureId,
3665
- projectIncarnationGate: {
3666
- required: true,
3667
- provided: expectedProjectIncarnation !== undefined,
3668
- ...(expectedProjectIncarnation === undefined
3669
- ? {}
3670
- : { expectedHash: expectedProjectIncarnation, }),
3671
- },
3672
- incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
3673
- preflightRequests: [
3674
- {
3675
- method: "GET",
3676
- endpoint: manifestProbe,
3677
- when: "before-future-wait",
3678
- intent: "Verify the target is an APP_INSTANCE project before the supplied future is touched, so an invalid target cannot affect it.",
3679
- },
3680
- {
3681
- method: "GET",
3682
- endpoint: projectDetailsProbe,
3683
- when: "conditional-before-wait",
3684
- intent: "Fallback probe issued only when the pre-wait app-manifest response omits projectAppType (live DSS does).",
3685
- },
3686
- ],
3687
- futureGate: [
3688
- {
3689
- method: "GET",
3690
- endpoint: `/public/api/futures/${encodeURIComponent(futureId)}?peek=false`,
3691
- 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.",
3692
- },
3693
- ],
3694
- postFutureValidationRequests: expectedProjectIncarnation === undefined
3695
- ? []
3696
- : typeValidationRequests,
3697
- note: expectedProjectIncarnation === undefined
3698
- ? "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."
3699
- : "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.",
3700
- },
3701
- };
3702
- }
3703
- case "app.permissions-restore": {
3704
- const file = requiredPlanFlag(flags, "file", entry.usage);
3705
- const targetProjectKey = requiredPlanProjectKey(flags, entry.usage);
3706
- const permissionsEndpoint = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/permissions`;
3707
- const projectDetailsEndpoint = `/public/api/projects/${encodeURIComponent(targetProjectKey)}/`;
3708
- return {
3709
- method: "PUT",
3710
- endpoint: permissionsEndpoint,
3711
- identifiers: {
3712
- file,
3713
- projectKey: targetProjectKey,
3714
- localPreflight: [
3715
- "Read and hash-verify the owner-only snapshot file.",
3716
- "Require its project key and canonical DSS URL to match this invocation.",
3717
- ],
3718
- preflightRequests: [
3719
- {
3720
- method: "GET",
3721
- endpoint: projectDetailsEndpoint,
3722
- intent: "Require the snapshot project-incarnation hash to match creationTag.",
3723
- },
3724
- {
3725
- method: "GET",
3726
- endpoint: permissionsEndpoint,
3727
- intent: "Read current permissions; an equal hash makes the PUT unnecessary.",
3728
- },
3729
- {
3730
- method: "GET",
3731
- endpoint: projectDetailsEndpoint,
3732
- intent: "Recheck the project incarnation after the permission read and immediately before any PUT.",
3733
- },
3734
- ],
3735
- conditionalWrite: {
3736
- method: "PUT",
3737
- endpoint: permissionsEndpoint,
3738
- when: "permissions-differ-and-dry-run-is-false",
3739
- },
3740
- verificationRequests: [
3741
- {
3742
- method: "GET",
3743
- endpoint: permissionsEndpoint,
3744
- intent: "Verify DSS persisted the desired permission hash.",
3745
- },
3746
- {
3747
- method: "GET",
3748
- endpoint: projectDetailsEndpoint,
3749
- intent: "Detect project-key reuse across the permission write.",
3750
- },
3751
- ],
3752
- incarnationControl: "client-side-non-atomic-stale-identity-check",
3753
- 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.",
3754
- },
3755
- };
3756
- }
3757
- case "statistics.create-worksheet":
3758
- return {
3759
- method: "POST",
3760
- endpoint: statisticsWorksheetsEndpoint(args[0]),
3761
- identifiers: { dataset: args[0], },
3762
- payload: requiredPlanJsonInput(flags, entry.usage),
3763
- };
3764
- case "statistics.update-worksheet":
3765
- return {
3766
- method: "PUT",
3767
- endpoint: statisticsWorksheetEndpoint(args[0], args[1]),
3768
- identifiers: { dataset: args[0], worksheetId: args[1], },
3769
- payload: requiredPlanJsonInput(flags, entry.usage),
3770
- };
3771
- case "statistics.delete-worksheet":
3772
- return {
3773
- method: "DELETE",
3774
- endpoint: statisticsWorksheetEndpoint(args[0], args[1]),
3775
- identifiers: { dataset: args[0], worksheetId: args[1], },
3776
- };
3777
- case "statistics.run-worksheet":
3778
- return {
3779
- method: "POST",
3780
- endpoint: `${statisticsWorksheetEndpoint(args[0], args[1])}/actions/run-card`,
3781
- identifiers: { dataset: args[0], worksheetId: args[1], },
3782
- };
3783
- case "statistics.run-card":
3784
- return {
3785
- method: "POST",
3786
- endpoint: `${statisticsWorksheetEndpoint(args[0], args[1])}/actions/run-card`,
3787
- identifiers: { dataset: args[0], worksheetId: args[1], },
3788
- payload: requiredPlanJsonInput(flags, entry.usage),
3789
- };
3790
- case "statistics.run-computation":
3791
- return {
3792
- method: "POST",
3793
- endpoint: `${statisticsWorksheetEndpoint(args[0], args[1])}/actions/run-computation`,
3794
- identifiers: { dataset: args[0], worksheetId: args[1], },
3795
- payload: requiredPlanJsonInput(flags, entry.usage),
3796
- };
3797
- case "webapp.create":
3798
- return {
3799
- method: "POST",
3800
- endpoint: projectEndpoint("/webapps/"),
3801
- payload: requiredPlanJsonInput(flags, entry.usage),
3802
- };
3803
- case "webapp.update-settings": {
3804
- const patch = requiredPlanJsonInput(flags, entry.usage);
3805
- const expectHash = flags["expect-hash"];
3806
- if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3807
- throw new UsageError("Expected webapp hash must be a 64-character SHA-256 hex digest.");
3808
- }
3809
- const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}`);
3810
- return {
3811
- exact: false,
3812
- reason: "Apply GETs the current settings, deep-merges the patch, then PUTs the full object.",
3813
- method: "PUT",
3814
- endpoint,
3815
- identifiers: {
3816
- webappId: id,
3817
- patchHash: stableHash(patch),
3818
- ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3819
- },
3820
- payload: { patch: "<omitted>", patchHash: stableHash(patch), },
3821
- requests: [
3822
- { method: "GET", endpoint, purpose: "read full settings and verify expectHash", },
3823
- {
3824
- method: "PUT",
3825
- endpoint,
3826
- condition: "settings read and expected hash matched",
3827
- payload: "<merged full settings>",
3828
- },
3829
- ],
3830
- };
3831
- }
3832
- case "webapp.stop-backend":
3833
- return {
3834
- method: "PUT",
3835
- endpoint: projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/stop`),
3836
- identifiers: { webappId: id, },
3837
- payload: {},
3838
- };
3839
- case "webapp.restart-backend": {
3840
- const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/restart`);
3841
- return {
3842
- method: "PUT",
3843
- endpoint,
3844
- identifiers: { webappId: id, },
3845
- payload: {},
3846
- wait: {
3847
- requested: flags["wait"] === true,
3848
- timeoutMs: num(flags["timeout"], "--timeout"),
3849
- pollIntervalMs: num(flags["poll-interval"], "--poll-interval"),
3850
- },
3851
- };
3852
- }
3853
- case "api-service.create":
3854
- return {
3855
- method: "POST",
3856
- endpoint: projectEndpoint(`/apiservices/${encodeURIComponent(id)}`),
3857
- identifiers: { serviceId: id, },
3858
- payload: {},
3859
- };
3860
- case "api-service.save-settings": {
3861
- const settings = requiredPlanJsonInput(flags, entry.usage);
3862
- const expectHash = flags["expect-hash"];
3863
- if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3864
- throw new UsageError("Expected API service settings hash must be a 64-character SHA-256 hex digest.");
3865
- }
3866
- const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
3867
- const payload = {
3868
- settings: "<omitted>",
3869
- settingsHash: stableHash(settings),
3870
- ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3871
- };
3872
- return {
3873
- method: "PUT",
3874
- endpoint,
3875
- identifiers: { serviceId: id, },
3876
- payload,
3877
- requests: [
3878
- ...(expectHash
3879
- ? [{ method: "GET", endpoint, purpose: "verify expectHash", },]
3880
- : []),
3881
- {
3882
- method: "PUT",
3883
- endpoint,
3884
- condition: expectHash ? "hash matched" : "unconditional",
3885
- payload,
3886
- },
3887
- ],
3888
- };
3889
- }
3890
- case "api-service.add-prediction-endpoint": {
3891
- const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
3892
- return {
3893
- exact: false,
3894
- reason: "Apply GETs the full settings, appends one endpoint, then PUTs the full object.",
3895
- method: "PUT",
3896
- endpoint,
3897
- identifiers: { serviceId: id, endpointId: args[1], savedModelId: args[2], },
3898
- payload: { id: args[1], type: "STD_PREDICTION", modelRef: args[2], },
3899
- requests: [
3900
- { method: "GET", endpoint, purpose: "read full settings", },
3901
- { method: "PUT", endpoint, payload: "<merged full settings>", },
3902
- ],
3903
- };
3904
- }
3905
- case "api-service.create-package":
3906
- case "api-service.delete-package":
3907
- case "api-service.publish-package": {
3908
- const serviceId = args[0];
3909
- const packageId = args[1];
3910
- const packageEndpoint = projectEndpoint(`/apiservices/${encodeURIComponent(serviceId)}/packages/${encodeURIComponent(packageId)}`);
3911
- if (action === "delete-package") {
3912
- return {
3913
- method: "DELETE",
3914
- endpoint: packageEndpoint,
3915
- identifiers: { serviceId, packageId, },
3916
- };
3917
- }
3918
- if (action === "publish-package") {
3919
- return {
3920
- method: "POST",
3921
- endpoint: `${packageEndpoint}/publish${querySuffix({
3922
- publishedServiceId: flags["published-service-id"],
3923
- })}`,
3924
- identifiers: { serviceId, packageId, },
3925
- };
3926
- }
3927
- return {
3928
- method: "POST",
3929
- endpoint: packageEndpoint + querySuffix({
3930
- releaseNotes: flags["release-notes"],
3931
- }),
3932
- identifiers: { serviceId, packageId, },
3933
- };
3934
- }
3935
- case "bundle.export": {
3936
- const evaluateProjectStandardsChecks = parseBooleanOption(flags["evaluate-standards-checks"], "--evaluate-standards-checks") ?? true;
3937
- return {
3938
- method: "PUT",
3939
- endpoint: projectEndpoint("/bundles/exported/" + encodeURIComponent(id)) + querySuffix({
3940
- releaseNotes: flags["release-notes"],
3941
- evaluateProjectStandardsChecks,
3942
- }),
3943
- identifiers: { bundleId: id, },
3944
- };
3945
- }
3946
- case "bundle.publish":
3947
- return {
3948
- method: "POST",
3949
- endpoint: projectEndpoint("/bundles/" + encodeURIComponent(id) + "/publish") + querySuffix({
3950
- publishedProjectKey: flags["published-project-key"],
3951
- }),
3952
- identifiers: { bundleId: id, },
3953
- payload: {},
3954
- };
3955
- case "bundle.activate": {
3956
- const rawScenarios = flags["scenarios"];
3957
- let scenariosToEnable;
3958
- if (rawScenarios !== undefined && rawScenarios !== false) {
3959
- const parsed = json(rawScenarios);
3960
- if (typeof parsed !== "object"
3961
- || parsed === null
3962
- || Array.isArray(parsed)
3963
- || !Object.values(parsed).every((value) => typeof value === "boolean")) {
3964
- throw new UsageError("--scenarios must be a JSON object mapping scenario IDs to true|false.");
3965
- }
3966
- scenariosToEnable = parsed;
3967
- }
3968
- return {
3969
- method: "POST",
3970
- endpoint: projectEndpoint(`/bundles/imported/${encodeURIComponent(id)}/actions/activate`),
3971
- identifiers: { bundleId: id, },
3972
- payload: scenariosToEnable === undefined
3973
- ? {}
3974
- : { scenariosActiveOnActivation: scenariosToEnable, },
3975
- };
3976
- }
3977
- case "bundle.preload":
3978
- return {
3979
- method: "POST",
3980
- endpoint: projectEndpoint(`/bundles/imported/${encodeURIComponent(id)}/actions/preload`),
3981
- identifiers: { bundleId: id, },
3982
- payload: {},
3983
- };
3984
- case "project-library.create-file":
3985
- case "project-library.create-folder": {
3986
- const libraryPath = validateLibraryPath(id);
3987
- const kind = action === "create-file" ? "contents" : "folders";
3988
- return {
3989
- exact: false,
3990
- reason: "Apply performs a live absence check before POST; --if-not-exists may stop after that read, and create never overwrites an existing item.",
3991
- method: "POST",
3992
- endpoint: projectEndpoint(`/libraries/${kind}/${encodeLibraryPath(libraryPath)}`),
3993
- identifiers: { path: libraryPath, },
3994
- };
3995
- }
3996
- case "project-library.put": {
3997
- const libraryPath = validateLibraryPath(id);
3998
- const endpoint = projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`);
3999
- const payload = projectLibraryPutPayload(flags);
4000
- return {
4001
- method: "POST",
4002
- endpoint,
4003
- identifiers: {
4004
- path: libraryPath,
4005
- concurrencyControl: payload.expectSha256
4006
- ? PROJECT_LIBRARY_CONCURRENCY_CONTROL
4007
- : "none",
4008
- },
4009
- payload,
4010
- ...(payload.expectSha256
4011
- ? {
4012
- requests: [
4013
- {
4014
- method: "GET",
4015
- endpoint: `${endpoint}?dataEncoding=base64`,
4016
- purpose: "verify expectSha256",
4017
- },
4018
- { method: "POST", endpoint, condition: "hash matched", payload, },
4019
- ],
4020
- }
4021
- : {}),
4022
- };
4023
- }
4024
- case "project-library.delete": {
4025
- const libraryPath = validateLibraryPath(id);
4026
- return {
4027
- method: "DELETE",
4028
- endpoint: projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`),
4029
- identifiers: { path: libraryPath, },
4030
- };
4031
- }
4032
- case "project-library.rename": {
4033
- const libraryPath = validateLibraryPath(id);
4034
- const newName = validateLibraryName(args[1]);
4035
- return {
4036
- method: "POST",
4037
- endpoint: projectEndpoint("/libraries/contents-actions/rename/"),
4038
- identifiers: { path: libraryPath, newName, },
4039
- payload: { oldPath: `/${libraryPath}`, newName, },
4040
- };
4041
- }
4042
- case "project-library.move": {
4043
- const libraryPath = validateLibraryPath(id);
4044
- const destinationFolder = validateLibraryDestinationPath(args[1]);
4045
- return {
4046
- method: "POST",
4047
- endpoint: projectEndpoint("/libraries/contents-actions/move"),
4048
- identifiers: { path: libraryPath, destinationFolder, },
4049
- payload: { oldPath: `/${libraryPath}`, newPath: destinationFolder, },
4050
- };
4051
- }
4052
- case "code-env.create": {
4053
- const wait = codeEnvWait(flags);
4054
- return {
4055
- method: "POST",
4056
- endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
4057
- identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
4058
- payload: {
4059
- ...codeEnvParams(flags),
4060
- deploymentMode: requiredPlanFlag(flags, "deployment-mode", entry.usage),
4061
- },
4062
- wait,
4063
- };
4064
- }
4065
- case "code-env.set-definition": {
4066
- const definition = requiredPlanJsonInput(flags, entry.usage);
4067
- const expectHash = flags["expect-hash"];
4068
- if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
4069
- throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
4070
- }
4071
- const endpoint = codeEnvEndpoint();
4072
- const definitionHash = stableHash(definition);
4073
- const normalizedExpectHash = expectHash?.toLowerCase();
4074
- return {
4075
- exact: false,
4076
- reason: "Apply PUTs the supplied definition object unchanged; this plan omits that body and reports only its hash.",
4077
- method: "PUT",
4078
- endpoint,
4079
- identifiers: {
4080
- lang: codeEnvLang(args[0], entry.usage),
4081
- name: args[1],
4082
- definitionHash,
4083
- ...(normalizedExpectHash ? { expectHash: normalizedExpectHash, } : {}),
4084
- },
4085
- payload: "<omitted>",
4086
- ...(normalizedExpectHash
4087
- ? {
4088
- requests: [
4089
- { method: "GET", endpoint, purpose: "verify expectHash", },
4090
- {
4091
- method: "PUT",
4092
- endpoint,
4093
- condition: "hash matched",
4094
- payload: "<omitted>",
4095
- redactedFields: ["payload",],
4096
- },
4097
- ],
4098
- }
4099
- : {}),
4100
- };
4101
- }
4102
- case "code-env.set-packages": {
4103
- const installCorePackages = parseBooleanOption(flags["install-core-packages"], "--install-core-packages");
4104
- const expectHash = flags["expect-hash"];
4105
- if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
4106
- throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
4107
- }
4108
- const endpoint = codeEnvEndpoint();
4109
- return {
4110
- exact: false,
4111
- reason: "Apply GETs the full definition, merges only package fields, then PUTs the full result.",
4112
- method: "PUT",
4113
- endpoint,
4114
- identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
4115
- payload: {
4116
- packages: codeEnvPackageList(flags),
4117
- ...(installCorePackages !== undefined ? { installCorePackages, } : {}),
4118
- ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
4119
- },
4120
- requests: [
4121
- { method: "GET", endpoint, purpose: "read full definition and verify expectHash", },
4122
- {
4123
- method: "PUT",
4124
- endpoint,
4125
- condition: "definition read and hash matched",
4126
- payload: "<merged full definition>",
4127
- },
4128
- ],
4129
- };
4130
- }
4131
- case "code-env.update-packages": {
4132
- const versionToUpdate = typeof flags["env-version"] === "string"
4133
- ? flags["env-version"]
4134
- : typeof flags["version"] === "string"
4135
- ? flags["version"]
4136
- : undefined;
4137
- const wait = codeEnvWait(flags);
4138
- return {
4139
- method: "POST",
4140
- endpoint: `${codeEnvEndpoint("/packages")}${querySuffix({
4141
- forceRebuildEnv: flags["force-rebuild"] === true,
4142
- versionToUpdate,
4143
- wait,
4144
- })}`,
4145
- identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
4146
- wait,
4147
- };
4148
- }
4149
- case "code-env.update-images": {
4150
- const wait = codeEnvWait(flags);
4151
- return {
4152
- method: "POST",
4153
- endpoint: `${codeEnvEndpoint("/images")}${querySuffix({ envVersion: flags["env-version"], wait, })}`,
4154
- identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
4155
- wait,
4156
- };
4157
- }
4158
- case "code-env.set-jupyter": {
4159
- const active = parseBooleanOption(flags["active"], "--active");
4160
- if (active === undefined) {
4161
- throw new UsageError(`--active is required. Usage: ${commandUsage("code-env", "set-jupyter")}`);
4162
- }
4163
- const wait = codeEnvWait(flags);
4164
- return {
4165
- method: "POST",
4166
- endpoint: `${codeEnvEndpoint("/jupyter")}${querySuffix({ active, wait, })}`,
4167
- identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
4168
- wait,
4169
- };
4170
- }
4171
- case "code-env.delete": {
4172
- const wait = codeEnvWait(flags);
4173
- return {
4174
- method: "DELETE",
4175
- endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
4176
- identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
4177
- wait,
4178
- };
4179
- }
4180
- case "notebook.save-jupyter": {
4181
- const content = requiredPlanJsonInput(flags, entry.usage);
4182
- const expectHash = flags["expect-hash"];
4183
- if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
4184
- throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
4185
- }
4186
- const endpoint = projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`);
4187
- return {
4188
- exact: false,
4189
- reason: "A fresh GET selects POST when absent or PUT when present; a confirming GET supplies the persisted hash.",
4190
- endpoint,
4191
- identifiers: {
4192
- name: id,
4193
- contentHash: stableHash(content),
4194
- ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
4195
- },
4196
- requests: [
4197
- { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
4198
- {
4199
- method: "POST|PUT",
4200
- endpoint,
4201
- condition: "POST after not_found; otherwise PUT",
4202
- payload: "<omitted>",
4203
- redactedFields: ["payload",],
4204
- },
4205
- { method: "GET", endpoint, purpose: "confirm persisted hash", },
4206
- ],
4207
- };
4208
- }
4209
- case "notebook.delete-jupyter":
4210
- return {
4211
- method: "DELETE",
4212
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`),
4213
- identifiers: { name: id, },
4214
- };
4215
- case "notebook.clear-jupyter-outputs":
4216
- return {
4217
- method: "DELETE",
4218
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}/outputs`),
4219
- identifiers: { name: id, },
4220
- };
4221
- case "notebook.unload-jupyter":
4222
- if (flags["all"] === true) {
4223
- if (args.length > 0)
4224
- throw new UsageError(entry.usage);
4225
- return {
4226
- exact: false,
4227
- reason: "DSS has no unload-all endpoint; apply lists active notebooks and sessions, then issues one verified session DELETE per result.",
4228
- identifiers: { all: true, },
4229
- requests: [
4230
- { method: "GET", endpoint: projectEndpoint("/jupyter-notebooks/?active=true"), },
4231
- {
4232
- method: "GET",
4233
- endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions"),
4234
- forEach: "active notebook",
4235
- },
4236
- {
4237
- method: "DELETE",
4238
- endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions/{sessionId}"),
4239
- forEach: "listed session",
4240
- },
4241
- ],
4242
- };
4243
- }
4244
- requireArgs(args, 2, entry.usage);
4245
- return {
4246
- method: "DELETE",
4247
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(args[0])}/sessions/${encodeURIComponent(args[1])}`),
4248
- identifiers: { name: args[0], sessionId: args[1], },
4249
- };
4250
- case "notebook.save-sql": {
4251
- const content = requiredPlanJsonInput(flags, entry.usage);
4252
- const expectHash = flags["expect-hash"];
4253
- if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
4254
- throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
4255
- }
4256
- const endpoint = projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`);
4257
- return {
4258
- exact: false,
4259
- reason: "A fresh GET selects collection POST when absent or entity PUT when present; a confirming GET supplies the persisted hash.",
4260
- endpoint,
4261
- identifiers: {
4262
- id,
4263
- contentHash: stableHash(content),
4264
- ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
4265
- },
4266
- requests: [
4267
- { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
4268
- {
4269
- method: "POST",
4270
- endpoint: projectEndpoint("/sql-notebooks/"),
4271
- condition: "after not_found",
4272
- payload: "<omitted; includes id and projectKey>",
4273
- redactedFields: ["payload",],
4274
- },
4275
- {
4276
- method: "PUT",
4277
- endpoint,
4278
- condition: "when present",
4279
- payload: "<omitted>",
4280
- redactedFields: ["payload",],
4281
- },
4282
- { method: "GET", endpoint, purpose: "confirm persisted hash", },
4283
- ],
4284
- };
4285
- }
4286
- case "notebook.delete-sql":
4287
- return {
4288
- method: "DELETE",
4289
- endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`),
4290
- identifiers: { id, },
4291
- };
4292
- case "notebook.clear-sql-history":
4293
- return {
4294
- method: "POST",
4295
- endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}/history/clear`),
4296
- identifiers: { id, },
4297
- payload: {
4298
- cellId: flags["cell-id"],
4299
- numRunsToRetain: num(flags["retain"], "--retain"),
4300
- },
4301
- };
4302
- case "project.create": {
4303
- const settings = jsonInput(flags) ?? null;
4304
- return {
4305
- method: "POST",
4306
- endpoint: "/public/api/projects/",
4307
- identifiers: { projectKey: args[0], name: args[1], },
4308
- payload: {
4309
- projectKey: args[0],
4310
- name: args[1],
4311
- owner: flags["owner"] ?? null,
4312
- settings,
4313
- description: null,
4314
- permissions: [],
4315
- tags: [],
4316
- },
4317
- };
4318
- }
4319
- case "project.delete": {
4320
- const expectedProjectIncarnation = flags["expect-project-incarnation"] === undefined
4321
- ? undefined
4322
- : requiredPlanFlag(flags, "expect-project-incarnation", entry.usage);
4323
- if (expectedProjectIncarnation !== undefined
4324
- && !/^[0-9a-f]{64}$/.test(expectedProjectIncarnation)) {
4325
- throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
4326
- }
4327
- const endpoint = `/public/api/projects/${encodeURIComponent(id)}${querySuffix({
4328
- clearManagedDatasets: flags["drop-data"] === true,
4329
- clearOutputManagedFolders: false,
4330
- clearJobAndScenarioLogs: true,
4331
- wait: true,
4332
- })}`;
4333
- const guarded = flags["if-exists"] === true
4334
- || executionMode(flags).dryRun
4335
- || expectedProjectIncarnation !== undefined;
4336
- if (!guarded) {
4337
- return {
4338
- method: "DELETE",
4339
- endpoint,
4340
- identifiers: { projectKey: id, },
4341
- };
4342
- }
4343
- const projectProbe = `/public/api/projects/${encodeURIComponent(id)}/`;
4344
- const incarnationGate = expectedProjectIncarnation === undefined
4345
- ? { required: false, provided: false, }
4346
- : {
4347
- required: true,
4348
- provided: true,
4349
- expectedHash: expectedProjectIncarnation,
4350
- };
4351
- if (executionMode(flags).dryRun) {
4352
- return {
4353
- method: "GET",
4354
- endpoint: projectProbe,
4355
- identifiers: {
4356
- projectKey: id,
4357
- dryRun: true,
4358
- ifExists: flags["if-exists"] === true,
4359
- projectIncarnationGate: incarnationGate,
4360
- note: "Read-only guarded-delete preflight. No DELETE is issued; a supplied creationTag hash must match.",
4361
- },
4362
- };
4363
- }
4364
- return {
4365
- method: "DELETE",
4366
- endpoint,
4367
- identifiers: {
4368
- projectKey: id,
4369
- ifExists: flags["if-exists"] === true,
4370
- projectIncarnationGate: incarnationGate,
4371
- incarnationControl: expectedProjectIncarnation === undefined
4372
- ? "none"
4373
- : "client-side-non-atomic-stale-identity-check",
4374
- preflightRequests: [{
4375
- method: "GET",
4376
- endpoint: projectProbe,
4377
- intent: expectedProjectIncarnation === undefined
4378
- ? "Confirm existence before a convergent delete."
4379
- : "Recompute the current project-incarnation hash from creationTag before DELETE.",
4380
- },],
4381
- note: expectedProjectIncarnation === undefined
4382
- ? "Convergent delete: a 404 preflight is an already-absent success only with --if-exists."
4383
- : "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.",
4384
- },
4385
- };
4386
- }
4387
- case "project.duplicate": {
4388
- const options = jsonInput(flags);
4389
- return {
4390
- method: "POST",
4391
- endpoint: `/public/api/projects/${encodeURIComponent(args[0])}/duplicate/`,
4392
- identifiers: { sourceKey: args[0], targetKey: args[1], targetName: args[2], },
4393
- payload: {
4394
- targetProjectName: args[2],
4395
- targetProjectKey: args[1],
4396
- duplicationMode: options?.duplicationMode ?? "MINIMAL",
4397
- exportAnalysisModels: options?.exportAnalysisModels ?? true,
4398
- exportSavedModels: options?.exportSavedModels ?? true,
4399
- exportGitRepository: options?.exportGitRepository ?? null,
4400
- exportInsightsData: options?.exportInsightsData ?? true,
4401
- remapping: options?.remapping ?? {},
4402
- ...(options?.targetProjectFolderId !== undefined
4403
- ? { targetProjectFolderId: options.targetProjectFolderId, }
4404
- : {}),
4405
- },
4406
- };
4407
- }
4408
- case "project.import": {
4409
- const settings = jsonInput(flags) ?? {};
4410
- const rawTarget = flags["target-project-key"];
4411
- const targetProjectKey = rawTarget?.trim();
4412
- if (rawTarget !== undefined && targetProjectKey === "") {
4413
- throw new UsageError(`--target-project-key must not be empty. Usage: ${entry.usage}`);
4414
- }
4415
- const settingsTarget = settings.targetProjectKey;
4416
- if (settingsTarget !== undefined
4417
- && (typeof settingsTarget !== "string" || settingsTarget.trim() === "")) {
4418
- throw new UsageError(`targetProjectKey in import settings must be a non-empty string. Usage: ${entry.usage}`);
4419
- }
4420
- if (targetProjectKey !== undefined
4421
- && settingsTarget !== undefined
4422
- && targetProjectKey !== settingsTarget.trim()) {
4423
- throw new UsageError(`--target-project-key conflicts with targetProjectKey in import settings. Usage: ${entry.usage}`);
4424
- }
4425
- const processPayload = targetProjectKey === undefined
4426
- ? settings
4427
- : { ...settings, targetProjectKey, };
4428
- const finalizedPayload = Object.keys(processPayload).length === 0
4429
- ? { _: "_", }
4430
- : processPayload;
4431
- const upload = {
4432
- method: "POST",
4433
- endpoint: "/public/api/projects/import/upload",
4434
- payload: {
4435
- contentType: "multipart/form-data",
4436
- fileField: "file",
4437
- filePath: id,
4438
- fileName: "tmp-import.zip",
4439
- },
4440
- };
4441
- return {
4442
- ...upload,
4443
- identifiers: {
4444
- filePath: id,
4445
- ...(targetProjectKey ? { targetProjectKey, } : {}),
4446
- archivePreflight: {
4447
- local: true,
4448
- required: true,
4449
- checks: [
4450
- "zip-integrity",
4451
- "safe-unique-members",
4452
- "manifest",
4453
- "flow-references",
4454
- "orphan-members",
4455
- ],
4456
- },
4457
- successVerification: {
4458
- usedProjectKeyRequired: true,
4459
- projectReadRequired: true,
4460
- projectIncarnationRequired: true,
4461
- remappingReported: true,
4462
- },
4463
- ...(flags["record-cleanup"] === undefined
4464
- ? {}
4465
- : {
4466
- cleanupBinding: "actual used project key plus verified creationTag incarnation hash",
4467
- }),
4468
- },
4469
- requests: [
4470
- { sequence: 1, ...upload, },
4471
- {
4472
- sequence: 2,
4473
- method: "POST",
4474
- endpoint: "/public/api/projects/import/{importId}/process",
4475
- pathBindings: { importId: "requests[0].response.id", },
4476
- payload: finalizedPayload,
4477
- },
4478
- ],
4479
- };
4480
- }
4481
- case "project.settings-set":
4482
- return {
4483
- method: "PUT",
4484
- endpoint: projectEndpoint("/settings"),
4485
- payload: requiredPlanJsonInput(flags, entry.usage),
4486
- };
4487
- case "project.permissions-set":
4488
- return {
4489
- method: "PUT",
4490
- endpoint: projectEndpoint("/permissions"),
4491
- payload: requiredPlanJsonInput(flags, entry.usage),
4492
- };
4493
- case "continuous-activity.start":
4494
- return {
4495
- method: "POST",
4496
- endpoint: projectEndpoint(`/continuous-activities/${encodeURIComponent(id)}/start`),
4497
- identifiers: { recipeId: id, },
4498
- payload: jsonInput(flags) ?? {},
4499
- };
4500
- case "continuous-activity.stop":
4501
- return {
4502
- method: "POST",
4503
- endpoint: projectEndpoint(`/continuous-activities/${encodeURIComponent(id)}/stop`),
4504
- identifiers: { recipeId: id, },
4505
- };
4506
- case "metrics.dataset-compute":
4507
- return {
4508
- method: "POST",
4509
- endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/actions/computeMetrics?partition=`),
4510
- identifiers: { dataset: id, },
4511
- };
4512
- case "flow-zone.organize":
4513
- return {
4514
- method: "POST",
4515
- endpoint: projectEndpoint("/flow/zones"),
4516
- payload: jsonInput(flags),
4517
- };
4518
- /* ---- project-git: mutations only; reads are never planned ---- */
4519
- case "project-git.set-remote": {
4520
- const remote = flags["name"] ?? "origin";
4521
- return {
4522
- method: "POST",
4523
- endpoint: projectGitEndpoint(projectKey, `/remotes/${encodeURIComponent(remote)}`),
4524
- identifiers: { remote, },
4525
- payload: { url: requiredPlanRepositoryUrl(flags, "repository", entry.usage), },
4526
- };
4527
- }
4528
- case "project-git.remove-remote": {
4529
- const remote = flags["name"] ?? "origin";
4530
- return {
4531
- method: "DELETE",
4532
- endpoint: projectGitEndpoint(projectKey, `/remotes/${encodeURIComponent(remote)}`),
4533
- identifiers: { remote, },
4534
- };
4535
- }
4536
- case "project-git.create-branch":
4537
- return {
4538
- method: "POST",
4539
- endpoint: projectGitEndpoint(projectKey, "/branches/"),
4540
- identifiers: { name: id, },
4541
- payload: {
4542
- name: id,
4543
- commit: flags["commit"] ?? null,
4544
- duplicateProject: parseBooleanOption(flags["duplicate-project"], "--duplicate-project") ?? false,
4545
- targetProjectKey: flags["target-project-key"] ?? null,
4546
- targetProjectFolderId: flags["target-project-folder-id"] ?? null,
4547
- },
4548
- };
4549
- case "project-git.delete-branch": {
4550
- const remote = parseBooleanOption(flags["remote"], "--remote") ?? false;
4551
- return {
4552
- method: "POST",
4553
- endpoint: projectGitEndpoint(projectKey, "/actions/deleteBranch"),
4554
- identifiers: { name: id, remote, },
4555
- payload: {
4556
- name: id,
4557
- remote,
4558
- deleteRemotely: parseBooleanOption(flags["delete-remotely"], "--delete-remotely") ?? false,
4559
- forceDelete: parseBooleanOption(flags["force-delete"], "--force-delete") ?? false,
4560
- },
4561
- };
4562
- }
4563
- case "project-git.switch":
4564
- return {
4565
- method: "POST",
4566
- endpoint: projectGitEndpoint(projectKey, `/actions/switchBranch${querySuffix({ branchName: id, })}`),
4567
- identifiers: { branch: id, },
4568
- };
4569
- case "project-git.create-tag":
4570
- return {
4571
- method: "POST",
4572
- endpoint: projectGitEndpoint(projectKey, "/tags/"),
4573
- identifiers: { name: id, },
4574
- payload: {
4575
- name: id,
4576
- reference: flags["reference"] ?? "HEAD",
4577
- message: flags["message"] ?? "",
4578
- },
4579
- };
4580
- case "project-git.delete-tag":
4581
- return {
4582
- method: "POST",
4583
- endpoint: projectGitEndpoint(projectKey, "/actions/deleteTag"),
4584
- identifiers: { name: id, },
4585
- payload: { name: id, },
4586
- };
4587
- case "project-git.fetch":
4588
- return {
4589
- method: "POST",
4590
- endpoint: projectGitEndpoint(projectKey, "/actions/fetch"),
4591
- };
4592
- case "project-git.pull":
4593
- return {
4594
- method: "POST",
4595
- endpoint: projectGitEndpoint(projectKey, `/actions/pullRebase${querySuffix({
4596
- branchName: flags["branch"],
4597
- })}`),
4598
- ...(flags["branch"] !== undefined
4599
- ? { identifiers: { branch: flags["branch"], }, }
4600
- : {}),
4601
- };
4602
- case "project-git.push":
4603
- return {
4604
- method: "POST",
4605
- endpoint: projectGitEndpoint(projectKey, `/actions/push${querySuffix({
4606
- branchName: flags["branch"],
4607
- })}`),
4608
- ...(flags["branch"] !== undefined
4609
- ? { identifiers: { branch: flags["branch"], }, }
4610
- : {}),
4611
- };
4612
- case "project-git.commit": {
4613
- const message = requiredPlanFlag(flags, "message", entry.usage);
4614
- return {
4615
- method: "POST",
4616
- endpoint: projectGitEndpoint(projectKey, "/actions/commit"),
4617
- identifiers: { message, },
4618
- payload: { message, },
4619
- };
4620
- }
4621
- case "project-git.revert-to-revision":
4622
- return {
4623
- method: "POST",
4624
- endpoint: projectGitEndpoint(projectKey, `/actions/revertToRevision${querySuffix({ commit: id, })}`),
4625
- identifiers: { commit: id, },
4626
- };
4627
- case "project-git.revert-commit":
4628
- return {
4629
- method: "POST",
4630
- endpoint: projectGitEndpoint(projectKey, `/actions/revertCommit${querySuffix({ commit: id, })}`),
4631
- identifiers: { commit: id, },
4632
- };
4633
- case "project-git.reset-to-head":
4634
- return {
4635
- method: "POST",
4636
- endpoint: projectGitEndpoint(projectKey, "/actions/resetToLocalHeadState"),
4637
- };
4638
- case "project-git.reset-to-upstream":
4639
- return {
4640
- method: "POST",
4641
- endpoint: projectGitEndpoint(projectKey, "/actions/resetToRemoteHeadState"),
4642
- };
4643
- case "project-git.drop-and-rebuild": {
4644
- if (parseBooleanOption(flags["i-know-what-i-am-doing"], "--i-know-what-i-am-doing") !== true) {
4645
- throw new UsageError(`--i-know-what-i-am-doing is required to acknowledge the irreversible Git history loss. Usage: ${entry.usage}`);
4646
- }
4647
- return {
4648
- method: "POST",
4649
- endpoint: projectGitEndpoint(projectKey, `/actions/dropAndRebuild${querySuffix({ iKnowWhatIAmDoing: true, })}`),
4650
- };
4651
- }
4652
- case "project-git.add-library": {
4653
- const targetPath = id;
4654
- return {
4655
- method: "POST",
4656
- endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/"),
4657
- identifiers: { localTargetPath: targetPath, },
4658
- payload: {
4659
- repository: requiredPlanRepositoryUrl(flags, "repository", entry.usage),
4660
- login: flags["login"] ?? null,
4661
- password: flags["password-env"] !== undefined ? "***" : null,
4662
- pathInGitRepository: flags["path-in-repository"] ?? "",
4663
- localTargetPath: targetPath,
4664
- checkout: requiredPlanFlag(flags, "checkout", entry.usage),
4665
- addToPythonPath: parseBooleanOption(flags["no-add-to-python-path"], "--no-add-to-python-path") !== true,
4666
- },
4667
- wait: projectGitFutureWait(),
4668
- };
4669
- }
4670
- case "project-git.set-library": {
4671
- const targetPath = validateGitReferencePath(id);
4672
- return {
4673
- method: "PUT",
4674
- endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}`),
4675
- identifiers: { library: targetPath, },
4676
- payload: {
4677
- repository: requiredPlanRepositoryUrl(flags, "repository", entry.usage),
4678
- login: flags["login"] ?? null,
4679
- password: flags["password-env"] !== undefined ? "***" : null,
4680
- pathInGitRepository: flags["path-in-repository"] ?? "",
4681
- checkout: requiredPlanFlag(flags, "checkout", entry.usage),
4682
- },
4683
- };
4684
- }
4685
- case "project-git.remove-library": {
4686
- const targetPath = validateGitReferencePath(id);
4687
- return {
4688
- method: "DELETE",
4689
- endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}${querySuffix({
4690
- deleteDirectory: parseBooleanOption(flags["delete-directory"], "--delete-directory") ?? false,
4691
- })}`),
4692
- identifiers: { library: targetPath, },
4693
- };
4694
- }
4695
- case "project-git.reset-library": {
4696
- const targetPath = validateGitReferencePath(id);
4697
- return {
4698
- method: "POST",
4699
- endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/reset"),
4700
- identifiers: { library: targetPath, },
4701
- payload: { gitRef: targetPath, },
4702
- wait: projectGitFutureWait(),
4703
- };
4704
- }
4705
- case "project-git.push-library": {
4706
- const targetPath = validateGitReferencePath(id);
4707
- const message = requiredPlanFlag(flags, "message", entry.usage);
4708
- return {
4709
- method: "POST",
4710
- endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/push"),
4711
- identifiers: { library: targetPath, message, },
4712
- payload: { gitRef: targetPath, commitMessage: message, },
4713
- wait: projectGitFutureWait(),
4714
- };
4715
- }
4716
- case "project-git.push-all-libraries": {
4717
- const message = requiredPlanFlag(flags, "message", entry.usage);
4718
- return {
4719
- method: "POST",
4720
- endpoint: projectGitEndpoint(projectKey, "/actions/git-refs/push-all"),
4721
- identifiers: { message, },
4722
- payload: { commitMessage: message, },
4723
- wait: projectGitFutureWait(),
4724
- };
4725
- }
4726
- case "project-git.reset-all-libraries":
4727
- return {
4728
- method: "POST",
4729
- endpoint: projectGitEndpoint(projectKey, "/actions/git-refs/reset-all"),
4730
- wait: projectGitFutureWait(),
4731
- };
4732
- case "project-git.future-abort":
4733
- return {
4734
- method: "DELETE",
4735
- endpoint: projectGitFutureEndpoint(id),
4736
- identifiers: { jobId: id, },
4737
- };
4738
- case "project-folder.settings-set": {
4739
- const payload = requiredPlanJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${entry.usage}`);
4740
- return {
4741
- method: "PUT",
4742
- endpoint: `${projectFolderEndpoint(id)}/settings`,
4743
- identifiers: { folderId: id, },
4744
- payload,
4745
- };
4746
- }
4747
- case "project-folder.move": {
4748
- const destination = args[1];
4749
- if (!destination)
4750
- throw new UsageError(`Usage: ${entry.usage}`);
4751
- return {
4752
- method: "POST",
4753
- endpoint: `${projectFolderEndpoint(id)}/move${querySuffix({ destination, })}`,
4754
- identifiers: { folderId: id, destination, },
4755
- };
4756
- }
4757
- case "project-folder.delete":
4758
- return {
4759
- method: "DELETE",
4760
- endpoint: projectFolderEndpoint(id),
4761
- identifiers: { folderId: id, },
4762
- };
4763
- case "project-folder.create-child": {
4764
- const name = requiredPlanFlag(flags, "name", entry.usage);
4765
- return {
4766
- method: "POST",
4767
- endpoint: `${projectFolderEndpoint(id)}/children${querySuffix({ name, })}`,
4768
- identifiers: { parentFolderId: id, name, },
4769
- };
4770
- }
4771
- case "project-folder.move-project": {
4772
- const projectKeyArg = args[1];
4773
- const destination = args[2];
4774
- if (!projectKeyArg || !destination) {
4775
- throw new UsageError(`Usage: ${entry.usage}`);
4776
- }
4777
- return {
4778
- method: "POST",
4779
- endpoint: `${projectFolderEndpoint(id)}/projects/${encodeURIComponent(projectKeyArg)}/move${querySuffix({ destination, })}`,
4780
- identifiers: { folderId: id, projectKey: projectKeyArg, destination, },
4781
- };
4782
- }
4783
- default:
4784
- return {
4785
- exact: false,
4786
- reason: `No exact offline request shape is defined for ${resource}.${action}; no endpoint was guessed.`,
4787
- identifiers: id ? { id, } : undefined,
4788
- };
4789
- }
4790
- }
4791
- export function buildMutationPlan(resource, action, meta, args, flags) {
4792
- const entry = buildRegistryEntry(resource, action, meta);
4793
- if (!entry.mutatesDss && entry.sideEffect !== "write") {
4794
- throw new UsageError(`--plan is only supported for mutating commands. Usage: ${meta.usage}`);
4795
- }
4796
- const requiredPositionals = entry.positionalArguments.filter((positional) => positional.required);
4797
- requireArgs(args, requiredPositionals.length, meta.usage);
4798
- const projectKey = projectKeyForPlan(entry, flags);
4799
- const shape = commandPlanShape(resource, action, args, flags, entry, projectKey);
4800
- return planResult(resource, action, {
4801
- ...shape,
4802
- asyncKind: entry.async,
4803
- exitCodesOnFailure: exitCodesOnFailure(entry),
4804
- idempotency: entry.idempotency,
4805
- plannedAndDryRun: executionMode(flags).dryRun,
4806
- });
4807
- }
4808
1551
  /**
4809
1552
  * Batch plans may surface exit 4: any dispatched step can be a long-running
4810
1553
  * lifecycle command whose remote work fails, or a synchronous assertion