dataiku-sdk 3.2.0 → 3.3.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 (120) hide show
  1. package/README.md +149 -46
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +102 -0
  4. package/dist/packages/types/src/index.js +76 -0
  5. package/dist/src/cli/commands/app.js +57 -15
  6. package/dist/src/cli/commands/bundle.js +1 -1
  7. package/dist/src/cli/commands/code-env.js +13 -7
  8. package/dist/src/cli/commands/connection.d.ts +9 -0
  9. package/dist/src/cli/commands/connection.js +267 -1
  10. package/dist/src/cli/commands/data-collection.d.ts +2 -0
  11. package/dist/src/cli/commands/data-collection.js +164 -0
  12. package/dist/src/cli/commands/dataset.js +161 -32
  13. package/dist/src/cli/commands/future.js +2 -2
  14. package/dist/src/cli/commands/group.d.ts +2 -0
  15. package/dist/src/cli/commands/group.js +109 -0
  16. package/dist/src/cli/commands/index.js +151 -78
  17. package/dist/src/cli/commands/knowledge-bank.d.ts +2 -0
  18. package/dist/src/cli/commands/knowledge-bank.js +80 -0
  19. package/dist/src/cli/commands/llm.d.ts +2 -0
  20. package/dist/src/cli/commands/llm.js +93 -0
  21. package/dist/src/cli/commands/macro.d.ts +2 -0
  22. package/dist/src/cli/commands/macro.js +221 -0
  23. package/dist/src/cli/commands/ml-task.js +18 -6
  24. package/dist/src/cli/commands/notebook.js +6 -2
  25. package/dist/src/cli/commands/plugin.d.ts +2 -0
  26. package/dist/src/cli/commands/plugin.js +799 -0
  27. package/dist/src/cli/commands/project-folder.d.ts +2 -0
  28. package/dist/src/cli/commands/project-folder.js +197 -0
  29. package/dist/src/cli/commands/project-git.js +14 -54
  30. package/dist/src/cli/commands/project.js +52 -6
  31. package/dist/src/cli/commands/recipe.js +36 -1
  32. package/dist/src/cli/commands/saved-model.d.ts +5 -0
  33. package/dist/src/cli/commands/saved-model.js +388 -1
  34. package/dist/src/cli/commands/scenario.js +138 -1
  35. package/dist/src/cli/commands/user.d.ts +8 -0
  36. package/dist/src/cli/commands/user.js +296 -0
  37. package/dist/src/cli/contract.d.ts +1 -2
  38. package/dist/src/cli/contract.js +1089 -103
  39. package/dist/src/cli/flags.js +30 -1
  40. package/dist/src/cli/helpers/app-successor.d.ts +19 -4
  41. package/dist/src/cli/helpers/app-successor.js +90 -30
  42. package/dist/src/cli/helpers/cleanup.js +3 -1
  43. package/dist/src/cli/helpers/flow-zone.js +1 -2
  44. package/dist/src/cli/output.d.ts +1 -1
  45. package/dist/src/cli/output.js +23 -10
  46. package/dist/src/cli/usage.d.ts +1 -0
  47. package/dist/src/cli/usage.js +47 -1
  48. package/dist/src/cli.js +147 -41
  49. package/dist/src/client.d.ts +83 -5
  50. package/dist/src/client.js +185 -29
  51. package/dist/src/errors.d.ts +3 -0
  52. package/dist/src/errors.js +2 -0
  53. package/dist/src/index.d.ts +16 -7
  54. package/dist/src/index.js +11 -2
  55. package/dist/src/resources/analyses.js +1 -8
  56. package/dist/src/resources/applications.d.ts +8 -6
  57. package/dist/src/resources/applications.js +21 -21
  58. package/dist/src/resources/base.d.ts +4 -0
  59. package/dist/src/resources/base.js +15 -0
  60. package/dist/src/resources/bundles.d.ts +9 -4
  61. package/dist/src/resources/bundles.js +10 -5
  62. package/dist/src/resources/connections.d.ts +78 -1
  63. package/dist/src/resources/connections.js +132 -1
  64. package/dist/src/resources/data-collections.d.ts +118 -0
  65. package/dist/src/resources/data-collections.js +49 -0
  66. package/dist/src/resources/datasets.d.ts +69 -0
  67. package/dist/src/resources/datasets.js +99 -1
  68. package/dist/src/resources/futures.d.ts +4 -1
  69. package/dist/src/resources/futures.js +38 -6
  70. package/dist/src/resources/groups.d.ts +47 -0
  71. package/dist/src/resources/groups.js +56 -0
  72. package/dist/src/resources/jobs.d.ts +8 -13
  73. package/dist/src/resources/jobs.js +41 -16
  74. package/dist/src/resources/knowledge-banks.d.ts +58 -0
  75. package/dist/src/resources/knowledge-banks.js +61 -0
  76. package/dist/src/resources/llms.d.ts +92 -0
  77. package/dist/src/resources/llms.js +62 -0
  78. package/dist/src/resources/macros.d.ts +121 -0
  79. package/dist/src/resources/macros.js +231 -0
  80. package/dist/src/resources/ml-tasks.d.ts +37 -2
  81. package/dist/src/resources/ml-tasks.js +100 -20
  82. package/dist/src/resources/model-evaluation-stores.js +1 -8
  83. package/dist/src/resources/notebooks.d.ts +43 -7
  84. package/dist/src/resources/notebooks.js +53 -13
  85. package/dist/src/resources/plugins.d.ts +265 -0
  86. package/dist/src/resources/plugins.js +458 -0
  87. package/dist/src/resources/project-folders.d.ts +71 -0
  88. package/dist/src/resources/project-folders.js +57 -0
  89. package/dist/src/resources/project-git.d.ts +6 -0
  90. package/dist/src/resources/project-git.js +69 -42
  91. package/dist/src/resources/project-library.d.ts +0 -1
  92. package/dist/src/resources/project-library.js +1 -17
  93. package/dist/src/resources/projects.d.ts +19 -1
  94. package/dist/src/resources/projects.js +27 -1
  95. package/dist/src/resources/recipes.d.ts +15 -1
  96. package/dist/src/resources/recipes.js +73 -6
  97. package/dist/src/resources/saved-models.d.ts +134 -1
  98. package/dist/src/resources/saved-models.js +214 -6
  99. package/dist/src/resources/scenarios.d.ts +105 -0
  100. package/dist/src/resources/scenarios.js +232 -106
  101. package/dist/src/resources/sql.js +5 -1
  102. package/dist/src/resources/users.d.ts +129 -0
  103. package/dist/src/resources/users.js +128 -0
  104. package/dist/src/resources/webapps.d.ts +15 -2
  105. package/dist/src/resources/webapps.js +18 -4
  106. package/dist/src/schemas.d.ts +2 -2
  107. package/dist/src/schemas.js +1 -1
  108. package/dist/src/utils/deep-merge.d.ts +1 -1
  109. package/dist/src/utils/polling.d.ts +20 -0
  110. package/dist/src/utils/polling.js +27 -0
  111. package/dist/src/utils/response-file.js +1 -1
  112. package/dist/src/utils/secret-sanitize.d.ts +54 -0
  113. package/dist/src/utils/secret-sanitize.js +101 -0
  114. package/package.json +6 -5
  115. package/packages/types/dist/index.d.ts +102 -0
  116. package/packages/types/dist/index.js +76 -0
  117. package/plugin.json +1 -1
  118. package/skills/dataiku-dss/SKILL.md +7 -5
  119. package/skills/dataiku-dss/references/app-releases.md +8 -7
  120. package/skills/dataiku-dss/references/discovery.md +4 -4
@@ -18,7 +18,7 @@ import { flowZoneColor, flowZoneMoveItems, flowZoneName, } from "./helpers/flow-
18
18
  import { recipeBackupPath, recipeRunShouldWait, } from "./helpers/recipe.js";
19
19
  import { encodedProjectEndpointForPlan, planResult, } from "./output.js";
20
20
  import { resolveLoginCredentials, } from "./runtime.js";
21
- import { requireArgs, UsageError, } from "./usage.js";
21
+ import { inferRequiresProject, requireArgs, UsageError, } from "./usage.js";
22
22
  import { AGENT_CONTRACT_SCHEMA_ID, AGENT_CONTRACT_VERSION, cliVersionResult, JSON_SCHEMA_DRAFT, } from "./version.js";
23
23
  function codeEnvWait(flags) {
24
24
  return flags["no-wait"] !== true;
@@ -39,19 +39,27 @@ function codeEnvParams(flags) {
39
39
  function splitPackageSpec(raw) {
40
40
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
41
41
  }
42
+ /**
43
+ * Resolve requested package specs from --file/--packages/--package. An
44
+ * explicit source that resolves to zero specs is a legitimate clear
45
+ * (set-packages replaces specPackageList wholesale); only a call with no
46
+ * package source flag at all is a usage error.
47
+ */
42
48
  function codeEnvPackageList(flags) {
43
- const packages = [];
44
- if (typeof flags["file"] === "string") {
45
- packages.push(...splitPackageSpec(fs.readFileSync(flags["file"], "utf-8")));
46
- }
49
+ const file = flags["file"];
50
+ const packages = typeof file === "string"
51
+ ? splitPackageSpec(fs.readFileSync(file, "utf-8"))
52
+ : [];
47
53
  if (typeof flags["packages"] === "string") {
48
54
  packages.push(...splitPackageSpec(flags["packages"]));
49
55
  }
50
56
  if (typeof flags["package"] === "string") {
51
57
  packages.push(...splitPackageSpec(flags["package"]));
52
58
  }
53
- if (packages.length === 0) {
54
- throw new UsageError("--packages, --package, or --file is required. Use newline-separated package specs for version constraints.");
59
+ if (typeof file !== "string"
60
+ && typeof flags["packages"] !== "string"
61
+ && typeof flags["package"] !== "string") {
62
+ 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.");
55
63
  }
56
64
  return packages;
57
65
  }
@@ -127,26 +135,31 @@ const READ_ACTIONS = new Set([
127
135
  "usages",
128
136
  "verify-instance",
129
137
  ]);
130
- const PROJECT_SCOPED_RESOURCES = new Set([
131
- "analysis",
132
- "data-quality",
133
- "dashboard",
134
- "dataset",
135
- "flow-zone",
136
- "insight",
137
- "folder",
138
- "fixtures",
139
- "job",
140
- "notebook",
141
- "ml-task",
142
- "model-evaluation-store",
143
- "recipe",
144
- "scenario",
145
- "sql",
146
- "variable",
147
- "saved-model",
148
- "wiki",
149
- ]);
138
+ /**
139
+ * Security admin actions that hand back a DSS future instead of settling
140
+ * inline: resyncs mint sync jobs, external-user/group fetches mint a
141
+ * supplier-enumeration job, and provision runs a supplier import. Without
142
+ * this table they fall through to the read default and `--plan` rejects
143
+ * them as non-mutating.
144
+ */
145
+ const ADMIN_SECURITY_FUTURE_ACTIONS = {
146
+ "external-groups": true,
147
+ "external-users": true,
148
+ provision: true,
149
+ resync: true,
150
+ "resync-multi": true,
151
+ };
152
+ /** Connection tables-import actions return a DSS future reference. */
153
+ const CONNECTION_FUTURE_ACTIONS = {
154
+ "execute-import": true,
155
+ "prepare-import": true,
156
+ };
157
+ /** Plugin actions that only observe state despite POST shape (documented). */
158
+ const PLUGIN_GIT_OBSERVER_ACTIONS = {
159
+ "git-branches": true,
160
+ list: true,
161
+ usages: true,
162
+ };
150
163
  /**
151
164
  * Project Git actions that only observe repository state. Everything else in the
152
165
  * `project-git` resource is a mutation: the verb-shaped names (`fetch`, `pull`,
@@ -185,6 +198,17 @@ const PROJECT_GIT_DESTRUCTIVE_ACTIONS = {
185
198
  "revert-commit": true,
186
199
  "revert-to-revision": true,
187
200
  };
201
+ /** Plugin actions that discard work or publish it beyond the instance. */
202
+ const PLUGIN_DESTRUCTIVE_ACTIONS = {
203
+ "contents-delete": true,
204
+ delete: true,
205
+ "delete-git-remote": true,
206
+ fetch: true,
207
+ pull: true,
208
+ push: true,
209
+ "reset-local": true,
210
+ "reset-remote": true,
211
+ };
188
212
  /** Project Git actions that hand back a DSS future (`{jobId}`) instead of a result. */
189
213
  const PROJECT_GIT_FUTURE_ACTIONS = {
190
214
  "add-library": true,
@@ -434,6 +458,18 @@ const NOTEBOOK_SAVE_OUTPUT_SCHEMA = {
434
458
  hash: { type: "string", pattern: "^[a-f0-9]{64}$", },
435
459
  },
436
460
  };
461
+ /**
462
+ * `notebook.save-sql` adds `requested`: on create DSS allocates the persisted
463
+ * id (`saved`), while the requested handle only becomes the display name.
464
+ */
465
+ const NOTEBOOK_SAVE_SQL_OUTPUT_SCHEMA = {
466
+ ...NOTEBOOK_SAVE_OUTPUT_SCHEMA,
467
+ required: ["saved", "requested", "resource", "created", "hash",],
468
+ properties: {
469
+ ...NOTEBOOK_SAVE_OUTPUT_SCHEMA["properties"],
470
+ requested: { type: "string", },
471
+ },
472
+ };
437
473
  const COMMAND_OUTPUT_SCHEMAS = {
438
474
  "code-env.list": CodeEnvSummaryArraySchema,
439
475
  "code-env.get": CodeEnvDetailsSchema,
@@ -514,7 +550,7 @@ const COMMAND_OUTPUT_SCHEMAS = {
514
550
  "notebook.get-sql": SqlNotebookContentSchema,
515
551
  "notebook.history-sql": SqlNotebookHistorySchema,
516
552
  "notebook.save-jupyter": NOTEBOOK_SAVE_OUTPUT_SCHEMA,
517
- "notebook.save-sql": NOTEBOOK_SAVE_OUTPUT_SCHEMA,
553
+ "notebook.save-sql": NOTEBOOK_SAVE_SQL_OUTPUT_SCHEMA,
518
554
  "project-library.list": { type: "array", items: PROJECT_LIBRARY_ITEM_OUTPUT_SCHEMA, },
519
555
  "project-library.get-bytes": {
520
556
  type: "object",
@@ -916,10 +952,33 @@ function inferSideEffect(resource, action) {
916
952
  return "read";
917
953
  if (resource === "data-quality" && action === "compute")
918
954
  return "write";
955
+ if (resource === "user" && ADMIN_SECURITY_FUTURE_ACTIONS[action] === true)
956
+ return "write";
957
+ if (resource === "connection" && CONNECTION_FUTURE_ACTIONS[action] === true)
958
+ return "write";
959
+ // Saved-model evaluation triggers scoring work on DSS side (cost-bearing).
960
+ if (resource === "saved-model" && action === "evaluate-version")
961
+ return "write";
962
+ // LLM Mesh completions/embeddings invoke the LLM provider (cost-bearing,
963
+ // arbitrary-prompt execution); the verb shapes match no mutating pattern
964
+ // and would fall through to read, wrongly rejecting --plan. Knowledge-bank
965
+ // search stays a READ per Main's preservation directive.
966
+ if (resource === "llm" && (action === "completions" || action === "embeddings")) {
967
+ return "write";
968
+ }
919
969
  // Project Git is classified from the explicit read table, not by verb shape:
920
970
  // `switch`, `fetch`, `pull`, `push`, `commit`, the `reset-*`/`revert-*`
921
971
  // families, `drop-and-rebuild`, and `future-abort` all mutate repository or
922
972
  // future state yet match none of the mutating-verb patterns.
973
+ // Plugin dev-Git actions mutate repository/plugin state like project-git:
974
+ // `fetch`/`pull`/`push`/`reset-*` never match the generic mutating-verb
975
+ // regex, so they are classified explicitly as write (destructive level
976
+ // from the explicit table below). `git-branches` is a state observer
977
+ // (observed GET on DSS 15; the upstream docs' POST is wrong) and stays
978
+ // read.
979
+ if (resource === "plugin") {
980
+ return PLUGIN_GIT_OBSERVER_ACTIONS[action] === true ? "read" : "write";
981
+ }
923
982
  if (resource === "project-git") {
924
983
  return PROJECT_GIT_READ_ACTIONS[action] === true ? "read" : "write";
925
984
  }
@@ -927,8 +986,9 @@ function inferSideEffect(resource, action) {
927
986
  return "read";
928
987
  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)/
929
988
  .test(action)
930
- // Compound actions whose mutating verb is a suffix (e.g. permissions-set, dataset-compute).
931
- || /-(set|compute|restore)$/.test(action)) {
989
+ // Compound actions whose mutating verb is a suffix (e.g. permissions-set,
990
+ // dataset-compute, saved-model external-metadata-put).
991
+ || /-(set|compute|restore|put)$/.test(action)) {
932
992
  return "write";
933
993
  }
934
994
  return "read";
@@ -940,19 +1000,6 @@ function inferRequiresAuth(resource) {
940
1000
  && resource !== "install-skill"
941
1001
  && resource !== "version";
942
1002
  }
943
- export function inferRequiresProject(resource, action, usage) {
944
- if (resource === "agent" || resource === "auth" || resource === "doctor" || resource === "commands"
945
- || resource === "install-skill" || resource === "version") {
946
- return false;
947
- }
948
- if (resource === "sql" && action === "query")
949
- return false;
950
- if (PROJECT_SCOPED_RESOURCES.has(resource))
951
- return true;
952
- if (resource === "project-git")
953
- return !action.startsWith("future-");
954
- return usage.includes("--project-key");
955
- }
956
1003
  const ARRAY_OUTPUT_ACTIONS = new Set([
957
1004
  "history",
958
1005
  "find",
@@ -1166,7 +1213,7 @@ function cleanupCommandFromDeleteUsage(resource, action) {
1166
1213
  return "dss notebook delete-jupyter <name> --if-exists";
1167
1214
  }
1168
1215
  if (`${resource}.${action}` === "notebook.save-sql") {
1169
- return "dss notebook delete-sql <id> --if-exists";
1216
+ return "dss notebook delete-sql <the returned `saved` id> --if-exists";
1170
1217
  }
1171
1218
  if (`${resource}.${action}` === "project.import") {
1172
1219
  return "dss project delete <used-project-key> --if-exists --expect-project-incarnation <hash>";
@@ -1203,12 +1250,29 @@ export function supportsCleanupLedger(resource, action) {
1203
1250
  const EXPLICIT_DESTRUCTIVE_KEYS = {
1204
1251
  "dataset.upload-file": true,
1205
1252
  "sql.query": true,
1253
+ // Plugin dev-Git and content deletions discard work with no undo route.
1254
+ "plugin.delete": true,
1255
+ "plugin.delete-git-remote": true,
1256
+ "plugin.push": true,
1257
+ "plugin.pull": true,
1258
+ "plugin.fetch": true,
1259
+ "plugin.reset-local": true,
1260
+ "plugin.reset-remote": true,
1261
+ "plugin.contents-delete": true,
1262
+ // Macro runs execute arbitrary plugin code.
1263
+ "macro.run": true,
1264
+ "macro.run-and-wait": true,
1265
+ // Saved-model version deletion removes model versions permanently.
1266
+ "saved-model.delete-versions": true,
1206
1267
  };
1207
1268
  function inferDestructiveLevel(resource, sideEffect, action) {
1208
1269
  if (sideEffect !== "write")
1209
1270
  return "none";
1210
1271
  if (EXPLICIT_DESTRUCTIVE_KEYS[`${resource}.${action}`] === true)
1211
1272
  return "destructive";
1273
+ if (resource === "plugin") {
1274
+ return PLUGIN_DESTRUCTIVE_ACTIONS[action] === true ? "destructive" : "reversible";
1275
+ }
1212
1276
  if (resource === "project-git") {
1213
1277
  return PROJECT_GIT_DESTRUCTIVE_ACTIONS[action] === true ? "destructive" : "reversible";
1214
1278
  }
@@ -1229,6 +1293,10 @@ function inferAsyncKind(resource, action) {
1229
1293
  if (resource === "scenario" && ["run", "run-and-wait", "status",].includes(action)) {
1230
1294
  return "future";
1231
1295
  }
1296
+ if (resource === "user" && ADMIN_SECURITY_FUTURE_ACTIONS[action] === true)
1297
+ return "future";
1298
+ if (resource === "connection" && CONNECTION_FUTURE_ACTIONS[action] === true)
1299
+ return "future";
1232
1300
  if (resource === "data-quality" && action === "compute")
1233
1301
  return "future";
1234
1302
  // Project Git mutation results carry a future job id only for the library
@@ -1296,7 +1364,7 @@ export function inferCleanupHint(resource, action) {
1296
1364
  return "If created:true, delete with `dss notebook delete-jupyter <name> --if-exists`.";
1297
1365
  }
1298
1366
  if (key === "notebook.save-sql") {
1299
- return "If created:true, delete with `dss notebook delete-sql <id> --if-exists`.";
1367
+ return "If created:true, the persisted id is in `saved`; delete with `dss notebook delete-sql <saved> --if-exists`.";
1300
1368
  }
1301
1369
  if (!(action.startsWith("create") || action === "clone"))
1302
1370
  return undefined;
@@ -1427,98 +1495,105 @@ function buildRegistryEntry(resource, action, meta) {
1427
1495
  agentContractVersion: AGENT_CONTRACT_VERSION,
1428
1496
  };
1429
1497
  }
1430
- export function buildCommandRegistry(resourceFilter) {
1498
+ function commandDefinitions(resourceFilter) {
1431
1499
  const registry = {};
1432
- for (const [resource, actions,] of Object.entries(commands)) {
1500
+ for (const resource of Object.keys(commands)) {
1433
1501
  if (resourceFilter !== undefined && resource !== resourceFilter)
1434
1502
  continue;
1435
- registry[resource] = {};
1436
- for (const [action, meta,] of Object.entries(actions)) {
1437
- registry[resource][action] = buildRegistryEntry(resource, action, meta);
1438
- }
1503
+ registry[resource] = commands[resource];
1439
1504
  }
1440
1505
  if (resourceFilter === undefined || resourceFilter === "commands") {
1441
1506
  registry.commands = {
1442
- run: buildRegistryEntry("commands", "run", {
1507
+ run: {
1443
1508
  handler: async () => undefined,
1444
1509
  usage: COMMANDS_USAGE,
1445
1510
  description: COMMANDS_DESCRIPTION,
1446
1511
  examples: COMMANDS_EXAMPLES,
1447
- }),
1512
+ },
1448
1513
  };
1449
1514
  }
1450
1515
  if (resourceFilter === undefined || resourceFilter === "agent") {
1451
1516
  registry.agent = {
1452
- contract: buildRegistryEntry("agent", "contract", {
1517
+ contract: {
1453
1518
  handler: async () => undefined,
1454
1519
  usage: AGENT_CONTRACT_USAGE,
1455
1520
  description: AGENT_CONTRACT_DESCRIPTION,
1456
1521
  examples: AGENT_CONTRACT_EXAMPLES,
1457
- }),
1522
+ },
1458
1523
  };
1459
1524
  }
1460
1525
  if (resourceFilter === undefined || resourceFilter === "version") {
1461
1526
  registry.version = {
1462
- run: buildRegistryEntry("version", "run", {
1527
+ run: {
1463
1528
  handler: async () => undefined,
1464
1529
  usage: VERSION_USAGE,
1465
1530
  description: VERSION_DESCRIPTION,
1466
1531
  examples: VERSION_EXAMPLES,
1467
- }),
1532
+ },
1468
1533
  };
1469
1534
  }
1470
1535
  if (resourceFilter === undefined || resourceFilter === "install-skill") {
1471
1536
  registry["install-skill"] = {
1472
- run: buildRegistryEntry("install-skill", "run", {
1537
+ run: {
1473
1538
  handler: async () => undefined,
1474
1539
  usage: INSTALL_SKILL_USAGE,
1475
1540
  description: INSTALL_SKILL_DESCRIPTION,
1476
1541
  examples: INSTALL_SKILL_EXAMPLES,
1477
- }),
1542
+ },
1478
1543
  };
1479
1544
  }
1480
1545
  if (resourceFilter === undefined || resourceFilter === "cleanup") {
1481
1546
  registry.cleanup = {
1482
- run: buildRegistryEntry("cleanup", "run", {
1547
+ run: {
1483
1548
  handler: async () => undefined,
1484
1549
  usage: CLEANUP_USAGE,
1485
1550
  description: CLEANUP_DESCRIPTION,
1486
1551
  examples: CLEANUP_EXAMPLES,
1487
- }),
1552
+ },
1488
1553
  };
1489
1554
  }
1490
1555
  if (resourceFilter === undefined || resourceFilter === "fixtures") {
1491
1556
  registry.fixtures = {
1492
- run: buildRegistryEntry("fixtures", "run", {
1557
+ run: {
1493
1558
  handler: async () => undefined,
1494
1559
  usage: FIXTURES_USAGE,
1495
1560
  description: FIXTURES_DESCRIPTION,
1496
1561
  examples: FIXTURES_EXAMPLES,
1497
- }),
1562
+ },
1498
1563
  };
1499
1564
  }
1500
1565
  if (resourceFilter === undefined || resourceFilter === "batch") {
1501
1566
  registry.batch = {
1502
- run: buildRegistryEntry("batch", "run", {
1567
+ run: {
1503
1568
  handler: async () => undefined,
1504
1569
  usage: BATCH_USAGE,
1505
1570
  description: BATCH_DESCRIPTION,
1506
1571
  examples: BATCH_EXAMPLES,
1507
1572
  examplePayload: BATCH_EXAMPLE_PAYLOAD,
1508
1573
  payloadSchema: { stdin: true, dataFlag: true, dataFileFlag: true, jsonShape: "array", },
1509
- }),
1574
+ },
1510
1575
  };
1511
1576
  }
1512
1577
  if (resourceFilter === undefined || resourceFilter === "auth") {
1513
1578
  registry.auth = {};
1514
1579
  for (const [action, meta,] of Object.entries(AUTH_ACTIONS)) {
1515
- registry.auth[action] = buildRegistryEntry("auth", action, {
1580
+ registry.auth[action] = {
1516
1581
  handler: async () => undefined,
1517
1582
  usage: meta.usage,
1518
1583
  description: meta.description,
1519
1584
  examples: meta.examples,
1520
1585
  requiredFlags: meta.requiredFlags,
1521
- });
1586
+ };
1587
+ }
1588
+ }
1589
+ return registry;
1590
+ }
1591
+ export function buildCommandRegistry(resourceFilter) {
1592
+ const registry = {};
1593
+ for (const [resource, actions,] of Object.entries(commandDefinitions(resourceFilter))) {
1594
+ registry[resource] = {};
1595
+ for (const [action, meta,] of Object.entries(actions)) {
1596
+ registry[resource][action] = buildRegistryEntry(resource, action, meta);
1522
1597
  }
1523
1598
  }
1524
1599
  return registry;
@@ -1676,38 +1751,43 @@ export function agentContractJsonSchema() {
1676
1751
  },
1677
1752
  };
1678
1753
  }
1679
- export function commandActionSummary(registry) {
1754
+ export function commandActionSummary() {
1680
1755
  const summary = {};
1681
- for (const [resource, actions,] of Object.entries(registry)) {
1756
+ for (const [resource, actions,] of Object.entries(commandDefinitions())) {
1682
1757
  summary[resource] = Object.keys(actions).sort();
1683
1758
  }
1684
1759
  return summary;
1685
1760
  }
1686
1761
  export function buildAgentContract() {
1687
- const registry = buildCommandRegistry();
1762
+ let commandSection;
1763
+ let schemaSection;
1688
1764
  return {
1689
1765
  protocol: "dataiku-sdk-agent",
1690
1766
  agentContractVersion: AGENT_CONTRACT_VERSION,
1691
1767
  cli: cliVersionResult(),
1692
- commands: {
1693
- discoveryCommand: "dss commands run",
1694
- fullRegistryExportCommand: "dss commands run --output PATH",
1695
- scopedDiscoveryCommand: "dss commands run --fields RESOURCE[.ACTION[.FIELD...]]",
1696
- actionIndexCommand: "dss agent contract --fields commands.actions",
1697
- scopedDiscoveryExamples: [
1698
- "dss commands run --fields dataset",
1699
- "dss commands run --fields dataset.create",
1700
- ],
1701
- 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.",
1702
- actions: commandActionSummary(registry),
1768
+ get commands() {
1769
+ return commandSection ??= {
1770
+ discoveryCommand: "dss commands run",
1771
+ fullRegistryExportCommand: "dss commands run --output PATH",
1772
+ scopedDiscoveryCommand: "dss commands run --fields RESOURCE[.ACTION[.FIELD...]]",
1773
+ actionIndexCommand: "dss agent contract --fields commands.actions",
1774
+ scopedDiscoveryExamples: [
1775
+ "dss commands run --fields dataset",
1776
+ "dss commands run --fields dataset.create",
1777
+ ],
1778
+ 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.",
1779
+ actions: commandActionSummary(),
1780
+ };
1703
1781
  },
1704
- schemas: {
1705
- agentContract: agentContractJsonSchema(),
1706
- commandRegistry: commandRegistryJsonSchema(),
1707
- commandRegistryEntry: commandRegistryEntryJsonSchema(),
1708
- errorEnvelope: errorEnvelopeJsonSchema(),
1709
- warningEvent: warningEventJsonSchema(),
1710
- traceEvent: traceEventJsonSchema(),
1782
+ get schemas() {
1783
+ return schemaSection ??= {
1784
+ agentContract: agentContractJsonSchema(),
1785
+ commandRegistry: commandRegistryJsonSchema(),
1786
+ commandRegistryEntry: commandRegistryEntryJsonSchema(),
1787
+ errorEnvelope: errorEnvelopeJsonSchema(),
1788
+ warningEvent: warningEventJsonSchema(),
1789
+ traceEvent: traceEventJsonSchema(),
1790
+ };
1711
1791
  },
1712
1792
  stdio: {
1713
1793
  stdout: {
@@ -1795,6 +1875,19 @@ function requiredPlanFlag(flags, name, usage) {
1795
1875
  return value.trim();
1796
1876
  throw new UsageError(`--${name} is required. Usage: ${usage}`);
1797
1877
  }
1878
+ /**
1879
+ * Optional caller-chosen identifier. Absent selects the command's generated
1880
+ * mode; a present-but-empty value is an input error, never a silent omission.
1881
+ */
1882
+ function optionalPlanFlag(flags, name, usage) {
1883
+ const value = flags[name];
1884
+ if (value === undefined)
1885
+ return undefined;
1886
+ if (typeof value !== "string" || value.trim().length === 0) {
1887
+ throw new UsageError(`--${name} must not be empty when supplied; omit --${name} entirely to generate the successor key during apply. Usage: ${usage}`, "validation_failed");
1888
+ }
1889
+ return value.trim();
1890
+ }
1798
1891
  function optionalJsonFlag(flags, name) {
1799
1892
  const value = flags[name];
1800
1893
  return typeof value === "string" ? parseJsonObject(value, `--${name}`) : undefined;
@@ -1819,6 +1912,9 @@ function querySuffix(params) {
1819
1912
  const raw = search.toString();
1820
1913
  return raw ? `?${raw}` : "";
1821
1914
  }
1915
+ function projectFolderEndpoint(folderId) {
1916
+ return `/public/api/project-folders/${encodeURIComponent(folderId)}`;
1917
+ }
1822
1918
  /**
1823
1919
  * Git and future API mount. The official Python client mounts these routes on
1824
1920
  * `/dip/publicapi`, which is also what the project-git resource uses; do not
@@ -1863,6 +1959,92 @@ function validatedPlanRepositoryUrl(url, flag, usage) {
1863
1959
  }
1864
1960
  return candidate;
1865
1961
  }
1962
+ function pluginRootEndpoint(pluginId, suffix) {
1963
+ return `/public/api/plugins/${encodeURIComponent(pluginId)}${suffix}`;
1964
+ }
1965
+ function pluginActionEndpoint(pluginId, action) {
1966
+ return pluginRootEndpoint(pluginId, `/actions/${action}`);
1967
+ }
1968
+ function pluginContentsEndpoint(pluginId, contentPath) {
1969
+ return `${pluginRootEndpoint(pluginId, "/contents/")}${encodePluginSegmentsForPlan(contentPath)}`;
1970
+ }
1971
+ function pluginFoldersEndpoint(pluginId, contentPath) {
1972
+ return `${pluginRootEndpoint(pluginId, "/folders/")}${encodePluginSegmentsForPlan(contentPath)}`;
1973
+ }
1974
+ /** Encodes a plugin content path per segment for plan-time endpoints. */
1975
+ function encodePluginSegmentsForPlan(contentPath) {
1976
+ return contentPath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1977
+ }
1978
+ /**
1979
+ * Plan payload for plugin git install/update bodies. The repository URL is
1980
+ * validated (embedded userinfo rejected) and secrets are never echoed: the
1981
+ * plan carries the URL only, never embedded credentials.
1982
+ */
1983
+ function pluginGitPlanPayload(flags) {
1984
+ const repository = requiredPlanFlag(flags, "repository", "dss plugin install-from-git --repository URL");
1985
+ const payload = {
1986
+ gitRepositoryUrl: validatedPlanRepositoryUrl(repository, "repository", "--repository URL"),
1987
+ };
1988
+ if (typeof flags["checkout"] === "string")
1989
+ payload.gitCheckout = flags["checkout"];
1990
+ if (typeof flags["path-in-repository"] === "string") {
1991
+ payload.gitSubpath = flags["path-in-repository"];
1992
+ }
1993
+ return payload;
1994
+ }
1995
+ function optionalPlanProjectScope(flags) {
1996
+ const projectKey = flags["project-key"];
1997
+ if (typeof projectKey !== "string" || projectKey.trim() === "")
1998
+ return "";
1999
+ return `?projectKey=${encodeURIComponent(projectKey.trim())}`;
2000
+ }
2001
+ function settingsConfigKeys(flags) {
2002
+ const content = flags["content"];
2003
+ if (typeof content !== "string")
2004
+ return [];
2005
+ try {
2006
+ const parsed = JSON.parse(content);
2007
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
2008
+ return [];
2009
+ return Object.keys(parsed);
2010
+ }
2011
+ catch {
2012
+ return [];
2013
+ }
2014
+ }
2015
+ function contentSourceKind(flags) {
2016
+ if (flags["stdin"] === true)
2017
+ return "stdin";
2018
+ if (typeof flags["file"] === "string")
2019
+ return "file";
2020
+ if (typeof flags["content"] === "string")
2021
+ return "content";
2022
+ return "unknown";
2023
+ }
2024
+ /**
2025
+ * Plan-safe view of a user create/update body: password values are replaced by
2026
+ * a fixed marker so secrets never appear in --plan output.
2027
+ */
2028
+ function redactedUserPayload(payload) {
2029
+ if (payload["password"] === undefined)
2030
+ return payload;
2031
+ return { ...payload, password: "<omitted>", };
2032
+ }
2033
+ /**
2034
+ * Plan-safe view of a connection create/update body: the connection-type
2035
+ * specific `params` object may carry credentials (passwords, keys, tokens), so
2036
+ * the plan carries only the marker, never the raw params.
2037
+ */
2038
+ function redactedConnectionPayload(payload) {
2039
+ if (payload["params"] === undefined)
2040
+ return payload;
2041
+ return { ...payload, params: "<omitted; may include credentials>", };
2042
+ }
2043
+ /** Required CSV flag for plans (comma-separated logins list). */
2044
+ function requiredPlanCsv(flags, name, usage) {
2045
+ const raw = requiredPlanFlag(flags, name, usage);
2046
+ return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
2047
+ }
1866
2048
  function requiredPlanRepositoryUrl(flags, flag, usage) {
1867
2049
  const url = requiredPlanFlag(flags, flag, usage);
1868
2050
  return validatedPlanRepositoryUrl(url, flag, usage);
@@ -2092,8 +2274,8 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2092
2274
  };
2093
2275
  case "future.abort":
2094
2276
  return {
2095
- method: "POST",
2096
- endpoint: `/public/api/futures/${encodeURIComponent(id)}/abort`,
2277
+ method: "DELETE",
2278
+ endpoint: `/public/api/futures/${encodeURIComponent(id)}`,
2097
2279
  identifiers: { id, },
2098
2280
  };
2099
2281
  case "flow-zone.create": {
@@ -2221,8 +2403,38 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2221
2403
  method: "PUT",
2222
2404
  endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}`),
2223
2405
  identifiers: { name: id, },
2406
+ };
2407
+ case "dataset.metadata-set":
2408
+ return {
2409
+ method: "PUT",
2410
+ endpoint: projectEndpoint(`/datasets/${encodeURIComponent(id)}/metadata`),
2411
+ identifiers: { name: id, },
2224
2412
  payload: requiredPlanJsonInput(flags, entry.usage),
2225
2413
  };
2414
+ case "dataset.create-managed": {
2415
+ const name = requiredPlanFlag(flags, "name", entry.usage);
2416
+ const connection = requiredPlanFlag(flags, "connection", entry.usage);
2417
+ const creationSettings = { connectionId: connection, };
2418
+ const specificSettings = {};
2419
+ if (typeof flags["type-option-id"] === "string") {
2420
+ creationSettings.typeOptionId = flags["type-option-id"];
2421
+ }
2422
+ if (typeof flags["format-option-id"] === "string") {
2423
+ specificSettings.formatOptionId = flags["format-option-id"];
2424
+ }
2425
+ if (typeof flags["copy-partitioning-from"] === "string") {
2426
+ specificSettings.partitioningOptionId = `copy:${flags["partitioning-folder"] === true ? "folder" : "dataset"}:${flags["copy-partitioning-from"]}`;
2427
+ }
2428
+ if (Object.keys(specificSettings).length > 0) {
2429
+ creationSettings.specificSettings = specificSettings;
2430
+ }
2431
+ return {
2432
+ method: "POST",
2433
+ endpoint: projectEndpoint("/datasets/managed"),
2434
+ identifiers: { name, },
2435
+ payload: { name, creationSettings, },
2436
+ };
2437
+ }
2226
2438
  case "recipe.add-input":
2227
2439
  case "recipe.remove-input": {
2228
2440
  const role = flags["role"] ?? "main";
@@ -2411,6 +2623,42 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2411
2623
  payload: jobBuildPayload(id, projectKey, flags),
2412
2624
  wait: action === "build-and-wait" || flags["wait"] === true,
2413
2625
  };
2626
+ case "scenario.abort":
2627
+ return {
2628
+ method: "POST",
2629
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/abort`),
2630
+ identifiers: { id, },
2631
+ };
2632
+ case "scenario.payload-set":
2633
+ return {
2634
+ method: "PUT",
2635
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/payload`),
2636
+ identifiers: { id, },
2637
+ payload: requiredPlanJsonInput(flags, entry.usage),
2638
+ };
2639
+ case "scenario.active-set": {
2640
+ const activeRaw = args[1];
2641
+ if (activeRaw !== "true" && activeRaw !== "false") {
2642
+ throw new UsageError(`Usage: ${entry.usage}`);
2643
+ }
2644
+ return {
2645
+ method: "PUT",
2646
+ endpoint: projectEndpoint(`/scenarios/${encodeURIComponent(id)}/light`),
2647
+ identifiers: { id, },
2648
+ payload: { id, active: activeRaw === "true", },
2649
+ };
2650
+ }
2651
+ case "folder.create": {
2652
+ const name = requiredPlanFlag(flags, "name", entry.usage);
2653
+ const type = flags["type"];
2654
+ const connection = flags["connection"];
2655
+ return {
2656
+ method: "POST",
2657
+ endpoint: projectEndpoint("/managedfolders/"),
2658
+ identifiers: { name, },
2659
+ payload: { name, type, connection, path: flags["path"], projectKey, },
2660
+ };
2661
+ }
2414
2662
  case "job.abort":
2415
2663
  return {
2416
2664
  method: "POST",
@@ -2451,17 +2699,6 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2451
2699
  identifiers: { id, },
2452
2700
  payload: requiredPlanJsonInput(flags, entry.usage),
2453
2701
  };
2454
- case "folder.create": {
2455
- const name = requiredPlanFlag(flags, "name", entry.usage);
2456
- const type = flags["type"];
2457
- const connection = flags["connection"];
2458
- return {
2459
- method: "POST",
2460
- endpoint: projectEndpoint("/managedfolders/"),
2461
- identifiers: { name, },
2462
- payload: { name, type, connection, path: flags["path"], projectKey, },
2463
- };
2464
- }
2465
2702
  case "folder.update":
2466
2703
  return {
2467
2704
  method: "PUT",
@@ -2686,6 +2923,577 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2686
2923
  endpoint: `/public/api/meanings/${encodeURIComponent(id)}`,
2687
2924
  identifiers: { id, },
2688
2925
  };
2926
+ case "llm.list": {
2927
+ const params = new URLSearchParams();
2928
+ const purpose = flags["purpose"];
2929
+ if (typeof purpose === "string" && purpose.trim() !== "") {
2930
+ params.set("purpose", purpose.trim());
2931
+ }
2932
+ const query = params.size > 0 ? `?${params.toString()}` : "";
2933
+ return {
2934
+ method: "GET",
2935
+ endpoint: projectEndpoint(`/llms/${query}`),
2936
+ };
2937
+ }
2938
+ case "llm.completions":
2939
+ case "llm.embeddings": {
2940
+ const payload = requiredPlanJsonInput(flags, entry.usage);
2941
+ const llmId = stringField(payload, ["llmId",]);
2942
+ if (llmId === undefined || llmId.trim() === "") {
2943
+ 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");
2944
+ }
2945
+ const queries = payload["queries"];
2946
+ if (!Array.isArray(queries) || queries.length === 0) {
2947
+ throw new UsageError(`queries is required and must be a non-empty array of query objects.\nUsage: ${entry.usage}`, "invalid_flag_value");
2948
+ }
2949
+ return {
2950
+ method: "POST",
2951
+ endpoint: projectEndpoint(action === "completions" ? "/llms/completions" : "/llms/embeddings"),
2952
+ identifiers: { llmId, },
2953
+ payload,
2954
+ };
2955
+ }
2956
+ case "knowledge-bank.search": {
2957
+ if (typeof id !== "string" || id.trim() === "") {
2958
+ throw new UsageError(`knowledgeBankId must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
2959
+ }
2960
+ const payload = requiredPlanJsonInput(flags, entry.usage);
2961
+ const query = stringField(payload, ["query",]);
2962
+ if (query === undefined || query.trim() === "") {
2963
+ throw new UsageError(`query is required and must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
2964
+ }
2965
+ return {
2966
+ method: "POST",
2967
+ endpoint: projectEndpoint(`/knowledge-banks/${encodeURIComponent(id)}/search`),
2968
+ identifiers: { knowledgeBankId: id, },
2969
+ payload,
2970
+ };
2971
+ }
2972
+ case "knowledge-bank.clear":
2973
+ if (typeof id !== "string" || id.trim() === "") {
2974
+ throw new UsageError(`knowledgeBankId must be a non-empty string.\nUsage: ${entry.usage}`, "invalid_flag_value");
2975
+ }
2976
+ return {
2977
+ method: "POST",
2978
+ endpoint: projectEndpoint(`/knowledge-banks/${encodeURIComponent(id)}/clear`),
2979
+ identifiers: { knowledgeBankId: id, },
2980
+ };
2981
+ case "data-collection.create":
2982
+ return {
2983
+ method: "POST",
2984
+ endpoint: "/public/api/data-collections/",
2985
+ payload: requiredPlanJsonInput(flags, entry.usage),
2986
+ };
2987
+ case "data-collection.settings-set":
2988
+ return {
2989
+ method: "PUT",
2990
+ endpoint: `/public/api/data-collections/${encodeURIComponent(id)}`,
2991
+ identifiers: { dataCollectionId: id, },
2992
+ payload: requiredPlanJsonInput(flags, entry.usage),
2993
+ };
2994
+ case "data-collection.delete":
2995
+ return {
2996
+ method: "DELETE",
2997
+ endpoint: `/public/api/data-collections/${encodeURIComponent(id)}`,
2998
+ identifiers: { dataCollectionId: id, },
2999
+ };
3000
+ case "data-collection.add-object": {
3001
+ const payload = requiredPlanJsonInput(flags, entry.usage);
3002
+ const cid = id
3003
+ ?? (typeof payload["dataCollectionId"] === "string"
3004
+ ? payload["dataCollectionId"]
3005
+ : undefined);
3006
+ if (!cid)
3007
+ throw new UsageError(`Usage: ${entry.usage}`);
3008
+ const reference = { ...payload, };
3009
+ delete reference["dataCollectionId"];
3010
+ return {
3011
+ method: "POST",
3012
+ endpoint: `/public/api/data-collections/${encodeURIComponent(cid)}/objects`,
3013
+ identifiers: { dataCollectionId: cid, },
3014
+ payload: reference,
3015
+ };
3016
+ }
3017
+ case "data-collection.remove-dataset": {
3018
+ const projectKeyArg = args[1];
3019
+ const datasetName = args[2];
3020
+ if (!projectKeyArg || !datasetName) {
3021
+ throw new UsageError(`Usage: ${entry.usage}`);
3022
+ }
3023
+ return {
3024
+ method: "DELETE",
3025
+ endpoint: `/public/api/data-collections/${encodeURIComponent(id)}/objects/dataset/${encodeURIComponent(projectKeyArg)}/${encodeURIComponent(datasetName)}`,
3026
+ identifiers: { dataCollectionId: id, projectKey: projectKeyArg, datasetName, },
3027
+ };
3028
+ }
3029
+ case "project.tags-set":
3030
+ return {
3031
+ method: "PUT",
3032
+ endpoint: encodedProjectEndpointForPlan(projectKey ?? requiredPlanProjectKey(flags, entry.usage), "/tags"),
3033
+ identifiers: { projectKey: projectKey ?? requiredPlanProjectKey(flags, entry.usage), },
3034
+ payload: requiredPlanJsonInput(flags, entry.usage),
3035
+ };
3036
+ case "project.metadata-set":
3037
+ return {
3038
+ method: "PUT",
3039
+ endpoint: encodedProjectEndpointForPlan(projectKey ?? requiredPlanProjectKey(flags, entry.usage), "/metadata"),
3040
+ identifiers: { projectKey: projectKey ?? requiredPlanProjectKey(flags, entry.usage), },
3041
+ payload: requiredPlanJsonInput(flags, entry.usage),
3042
+ };
3043
+ case "macro.run":
3044
+ case "macro.run-and-wait": {
3045
+ // The plan payload mirrors the live POST body exactly: params and
3046
+ // adminParams when provided via --data, {} otherwise. Validation is
3047
+ // identical to the live path (JSON object shape), zero network.
3048
+ const data = optionalJsonFlag(flags, "data");
3049
+ const runPayload = {};
3050
+ if (data && typeof data === "object" && !Array.isArray(data)) {
3051
+ if (data["params"] !== undefined)
3052
+ runPayload["params"] = data["params"];
3053
+ if (data["adminParams"] !== undefined)
3054
+ runPayload["adminParams"] = data["adminParams"];
3055
+ }
3056
+ return {
3057
+ method: "POST",
3058
+ endpoint: projectEndpoint(`/runnables/${encodeURIComponent(id)}?wait=false`),
3059
+ identifiers: { id, },
3060
+ payload: runPayload,
3061
+ wait: action === "macro.run-and-wait" || flags["wait"] === true,
3062
+ };
3063
+ }
3064
+ case "macro.abort": {
3065
+ const runId = args[1];
3066
+ if (!runId)
3067
+ throw new UsageError(`Usage: ${entry.usage}`);
3068
+ return {
3069
+ method: "POST",
3070
+ endpoint: projectEndpoint(`/runnables/${encodeURIComponent(id)}/abort/${encodeURIComponent(runId)}`),
3071
+ identifiers: { id, runId, },
3072
+ };
3073
+ }
3074
+ case "recipe.metadata-set":
3075
+ return {
3076
+ method: "PUT",
3077
+ endpoint: projectEndpoint(`/recipes/${encodeURIComponent(id)}/metadata`),
3078
+ identifiers: { recipeName: id, },
3079
+ payload: requiredPlanJsonInput(flags, entry.usage),
3080
+ };
3081
+ case "user.create":
3082
+ return {
3083
+ method: "POST",
3084
+ endpoint: "/public/api/admin/users",
3085
+ payload: redactedUserPayload(requiredPlanJsonInput(flags, entry.usage)),
3086
+ };
3087
+ case "group.create":
3088
+ return {
3089
+ method: "POST",
3090
+ endpoint: "/public/api/admin/groups",
3091
+ payload: requiredPlanJsonInput(flags, entry.usage),
3092
+ };
3093
+ case "saved-model.create-external": {
3094
+ const config = optionalJsonFlag(flags, "configuration") ?? optionalJsonFlag(flags, "data");
3095
+ return {
3096
+ method: "POST",
3097
+ endpoint: projectEndpoint("/savedmodels/"),
3098
+ identifiers: { name: id, },
3099
+ payload: {
3100
+ savedModelType: requiredPlanFlag(flags, "type", entry.usage),
3101
+ name: id,
3102
+ ...(typeof flags["prediction-type"] === "string"
3103
+ ? { predictionType: flags["prediction-type"], }
3104
+ : {}),
3105
+ ...(config ? { proxyModelConfiguration: config, } : {}),
3106
+ },
3107
+ };
3108
+ }
3109
+ case "saved-model.update-settings":
3110
+ return {
3111
+ method: "PUT",
3112
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}`),
3113
+ identifiers: { savedModelId: id, },
3114
+ payload: requiredPlanJsonInput(flags, entry.usage),
3115
+ };
3116
+ case "saved-model.delete-versions": {
3117
+ const versionsRaw = args[1];
3118
+ if (typeof versionsRaw !== "string" || versionsRaw.trim() === "") {
3119
+ throw new UsageError(`Usage: ${entry.usage}`);
3120
+ }
3121
+ return {
3122
+ method: "POST",
3123
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/actions/delete-versions`),
3124
+ identifiers: { savedModelId: id, },
3125
+ payload: {
3126
+ versions: versionsRaw.split(",").map((v) => v.trim()).filter((v) => v.length > 0),
3127
+ removeIntermediate: parseBooleanOption(flags["remove-intermediate"], "--remove-intermediate") ?? true,
3128
+ },
3129
+ };
3130
+ }
3131
+ case "saved-model.import-mlflow-version":
3132
+ return {
3133
+ method: "POST",
3134
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}`),
3135
+ identifiers: { savedModelId: id, versionId: args[1], },
3136
+ payload: {
3137
+ source: { kind: "local-archive", archive: requiredPlanFlag(flags, "archive", entry.usage), },
3138
+ ...(typeof flags["code-env"] === "string"
3139
+ ? { codeEnvName: flags["code-env"], }
3140
+ : {}),
3141
+ // Execute always sends the query parameter with the
3142
+ // external-caller default NONE; the plan mirrors it.
3143
+ containerExecConfigName: typeof flags["container-exec-config"] === "string"
3144
+ ? flags["container-exec-config"]
3145
+ : "NONE",
3146
+ setActive: parseBooleanOption(flags["set-active"], "--set-active") ?? true,
3147
+ ...(flags["binary-classification-threshold"] !== undefined
3148
+ ? {
3149
+ binaryClassificationThreshold: num(flags["binary-classification-threshold"], "--binary-classification-threshold"),
3150
+ }
3151
+ : {}),
3152
+ },
3153
+ };
3154
+ case "saved-model.import-mlflow-version-from-folder":
3155
+ return {
3156
+ method: "POST",
3157
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}`),
3158
+ identifiers: { savedModelId: id, versionId: args[1], },
3159
+ payload: {
3160
+ source: {
3161
+ kind: "managed-folder",
3162
+ folderRef: requiredPlanFlag(flags, "folder", entry.usage),
3163
+ path: flags["path"],
3164
+ },
3165
+ ...(typeof flags["code-env"] === "string" ? { codeEnvName: flags["code-env"], } : {}),
3166
+ // Execute always sends the query parameter with the
3167
+ // external-caller default NONE; the plan mirrors it.
3168
+ containerExecConfigName: typeof flags["container-exec-config"] === "string"
3169
+ ? flags["container-exec-config"]
3170
+ : "NONE",
3171
+ setActive: parseBooleanOption(flags["set-active"], "--set-active") ?? true,
3172
+ },
3173
+ };
3174
+ case "saved-model.external-metadata-put": {
3175
+ // DSS requires containerExecConfigName on this endpoint; for an
3176
+ // external API caller it resolves as LOCAL-CONFIG -> NONE, so the
3177
+ // plan mirrors the NONE default unless --container-exec-config
3178
+ // overrides it. Same contract as execute.
3179
+ const containerExecConfigName = typeof flags["container-exec-config"] === "string"
3180
+ ? flags["container-exec-config"]
3181
+ : "NONE";
3182
+ return {
3183
+ method: "PUT",
3184
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}/external-ml/metadata?containerExecConfigName=${encodeURIComponent(containerExecConfigName)}`),
3185
+ identifiers: { savedModelId: id, versionId: args[1], },
3186
+ payload: requiredPlanJsonInput(flags, entry.usage),
3187
+ };
3188
+ }
3189
+ case "saved-model.evaluate-version": {
3190
+ const payload = {
3191
+ datasetRef: requiredPlanFlag(flags, "dataset", entry.usage),
3192
+ // Execute always sends an explicit containerExecConfigName
3193
+ // (official external-caller semantics: LOCAL-CONFIG -> NONE);
3194
+ // the plan mirrors that default and any explicit override.
3195
+ containerExecConfigName: typeof flags["container-exec-config"] === "string"
3196
+ ? flags["container-exec-config"]
3197
+ : "NONE",
3198
+ };
3199
+ // --sampling is a JSON flag value; the plan carries the parsed
3200
+ // object under the same key as the execute body (samplingParam).
3201
+ if (flags["sampling"] !== undefined) {
3202
+ payload.samplingParam = json(flags["sampling"], "--sampling");
3203
+ }
3204
+ return {
3205
+ method: "POST",
3206
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}/external-ml/actions/evaluate`),
3207
+ identifiers: { savedModelId: id, versionId: args[1], },
3208
+ payload,
3209
+ };
3210
+ }
3211
+ case "saved-model.set-user-meta":
3212
+ return {
3213
+ method: "PUT",
3214
+ endpoint: projectEndpoint(`/savedmodels/${encodeURIComponent(id)}/versions/${encodeURIComponent(args[1] ?? "")}/user-meta`),
3215
+ identifiers: { savedModelId: id, versionId: args[1], },
3216
+ payload: requiredPlanJsonInput(flags, entry.usage),
3217
+ };
3218
+ case "user.update":
3219
+ return {
3220
+ method: "PUT",
3221
+ endpoint: `/public/api/admin/users/${encodeURIComponent(id)}`,
3222
+ identifiers: { login: id, },
3223
+ payload: redactedUserPayload(requiredPlanJsonInput(flags, entry.usage)),
3224
+ };
3225
+ case "user.delete":
3226
+ case "group.delete":
3227
+ return {
3228
+ method: "DELETE",
3229
+ endpoint: resource === "user"
3230
+ ? `/public/api/admin/users/${encodeURIComponent(id)}`
3231
+ : `/public/api/admin/groups/${encodeURIComponent(id)}`,
3232
+ identifiers: resource === "user" ? { login: id, } : { name: id, },
3233
+ };
3234
+ case "user.resync":
3235
+ return {
3236
+ method: "POST",
3237
+ endpoint: `/public/api/admin/users/${encodeURIComponent(id)}/actions/resync`,
3238
+ identifiers: { login: id, },
3239
+ };
3240
+ case "user.resync-multi": {
3241
+ const logins = requiredPlanCsv(flags, "logins", entry.usage);
3242
+ return {
3243
+ method: "POST",
3244
+ endpoint: "/public/api/admin/users/actions/resync-multi",
3245
+ identifiers: { count: logins.length, },
3246
+ payload: logins,
3247
+ };
3248
+ }
3249
+ case "user.external-users":
3250
+ return {
3251
+ method: "GET",
3252
+ endpoint: "/public/api/admin/users/actions/external-users",
3253
+ };
3254
+ case "user.external-groups":
3255
+ return {
3256
+ method: "GET",
3257
+ endpoint: "/public/api/admin/users/actions/external-groups",
3258
+ };
3259
+ case "connection.test":
3260
+ return {
3261
+ method: "GET",
3262
+ endpoint: `/public/api/connections/${encodeURIComponent(id)}/test`,
3263
+ identifiers: { connectionName: id, },
3264
+ };
3265
+ case "user.provision":
3266
+ return {
3267
+ method: "POST",
3268
+ endpoint: "/public/api/admin/users/actions/provision",
3269
+ payload: requiredPlanJsonInput(flags, entry.usage),
3270
+ };
3271
+ case "group.update":
3272
+ return {
3273
+ method: "PUT",
3274
+ endpoint: `/public/api/admin/groups/${encodeURIComponent(id)}`,
3275
+ identifiers: { name: id, },
3276
+ payload: requiredPlanJsonInput(flags, entry.usage),
3277
+ };
3278
+ case "connection.create":
3279
+ return {
3280
+ method: "POST",
3281
+ endpoint: "/public/api/admin/connections",
3282
+ payload: redactedConnectionPayload(requiredPlanJsonInput(flags, entry.usage)),
3283
+ };
3284
+ case "connection.update":
3285
+ return {
3286
+ method: "PUT",
3287
+ endpoint: `/public/api/admin/connections/${encodeURIComponent(id)}`,
3288
+ identifiers: { connectionName: id, },
3289
+ payload: redactedConnectionPayload(requiredPlanJsonInput(flags, entry.usage)),
3290
+ };
3291
+ case "connection.delete":
3292
+ return {
3293
+ method: "DELETE",
3294
+ endpoint: `/public/api/admin/connections/${encodeURIComponent(id)}`,
3295
+ identifiers: { connectionName: id, },
3296
+ };
3297
+ case "connection.prepare-import": {
3298
+ const payload = requiredPlanJsonInput(flags, entry.usage);
3299
+ if (!projectKey)
3300
+ throw new UsageError(`Missing project key for ${resource} ${action}.`);
3301
+ return {
3302
+ method: "POST",
3303
+ endpoint: encodedProjectEndpointForPlan(projectKey, "/datasets/tables-import/actions/prepare-from-keys"),
3304
+ identifiers: { projectKey, },
3305
+ payload,
3306
+ };
3307
+ }
3308
+ case "connection.execute-import": {
3309
+ const payload = requiredPlanJsonInput(flags, entry.usage);
3310
+ if (!projectKey)
3311
+ throw new UsageError(`Missing project key for ${resource} ${action}.`);
3312
+ return {
3313
+ method: "POST",
3314
+ endpoint: encodedProjectEndpointForPlan(projectKey, "/datasets/tables-import/actions/execute-from-candidates"),
3315
+ identifiers: { projectKey, },
3316
+ payload,
3317
+ };
3318
+ }
3319
+ case "plugin.install-from-zip":
3320
+ return {
3321
+ method: "POST",
3322
+ endpoint: "/public/api/plugins/actions/installFromZip",
3323
+ payload: { file: requiredPlanFlag(flags, "file", entry.usage), upload: "multipart", },
3324
+ };
3325
+ case "plugin.install-from-store":
3326
+ return {
3327
+ method: "POST",
3328
+ endpoint: "/public/api/plugins/actions/installFromStore",
3329
+ identifiers: { pluginId: id, },
3330
+ payload: { pluginId: id, },
3331
+ };
3332
+ case "plugin.install-from-git": {
3333
+ const payload = pluginGitPlanPayload(flags);
3334
+ return {
3335
+ method: "POST",
3336
+ endpoint: "/public/api/plugins/actions/installFromGit",
3337
+ payload,
3338
+ };
3339
+ }
3340
+ case "plugin.update-from-zip":
3341
+ return {
3342
+ method: "POST",
3343
+ endpoint: pluginActionEndpoint(id, "updateFromZip"),
3344
+ identifiers: { pluginId: id, },
3345
+ payload: { file: requiredPlanFlag(flags, "file", entry.usage), upload: "multipart", },
3346
+ };
3347
+ case "plugin.update-from-store":
3348
+ return {
3349
+ method: "POST",
3350
+ endpoint: pluginActionEndpoint(id, "updateFromStore"),
3351
+ identifiers: { pluginId: id, },
3352
+ };
3353
+ case "plugin.update-from-git":
3354
+ return {
3355
+ method: "POST",
3356
+ endpoint: pluginActionEndpoint(id, "updateFromGit"),
3357
+ identifiers: { pluginId: id, },
3358
+ payload: pluginGitPlanPayload(flags),
3359
+ };
3360
+ case "plugin.settings-set": {
3361
+ const scope = optionalPlanProjectScope(flags);
3362
+ return {
3363
+ method: "POST",
3364
+ endpoint: `${pluginRootEndpoint(id, "/settings")}${scope}`,
3365
+ identifiers: { pluginId: id, },
3366
+ payload: { configKeys: settingsConfigKeys(flags), },
3367
+ };
3368
+ }
3369
+ case "plugin.code-env-create": {
3370
+ const conda = parseBooleanOption(flags["conda"], "--conda") ?? false;
3371
+ return {
3372
+ method: "POST",
3373
+ endpoint: `${pluginRootEndpoint(id, "/code-env/actions/create")}`,
3374
+ identifiers: { pluginId: id, },
3375
+ payload: {
3376
+ deploymentMode: "PLUGIN_MANAGED",
3377
+ conda,
3378
+ pythonInterpreter: flags["python-interpreter"] ?? null,
3379
+ },
3380
+ wait: flags["wait"] === true,
3381
+ };
3382
+ }
3383
+ case "plugin.code-env-update":
3384
+ return {
3385
+ method: "POST",
3386
+ endpoint: `${pluginRootEndpoint(id, "/code-env/actions/update")}`,
3387
+ identifiers: { pluginId: id, },
3388
+ wait: flags["wait"] === true,
3389
+ };
3390
+ case "plugin.move-to-dev":
3391
+ return {
3392
+ method: "POST",
3393
+ endpoint: pluginActionEndpoint(id, "moveToDev"),
3394
+ identifiers: { pluginId: id, },
3395
+ };
3396
+ case "plugin.delete":
3397
+ return {
3398
+ method: "POST",
3399
+ endpoint: pluginActionEndpoint(id, "delete"),
3400
+ identifiers: { pluginId: id, },
3401
+ payload: { force: parseBooleanOption(flags["force"], "--force") ?? false, },
3402
+ };
3403
+ case "plugin.create-dev": {
3404
+ const creationMode = requiredPlanFlag(flags, "creation-mode", entry.usage);
3405
+ const payload = {
3406
+ pluginId: id,
3407
+ creationMode,
3408
+ };
3409
+ if (typeof flags["repository"] === "string")
3410
+ payload.gitRepository = flags["repository"];
3411
+ if (typeof flags["checkout"] === "string")
3412
+ payload.gitCheckout = flags["checkout"];
3413
+ if (typeof flags["path-in-repository"] === "string") {
3414
+ payload.gitSubpath = flags["path-in-repository"];
3415
+ }
3416
+ return {
3417
+ method: "POST",
3418
+ endpoint: "/public/api/plugins/actions/createDev",
3419
+ identifiers: { pluginId: id, creationMode, },
3420
+ payload,
3421
+ };
3422
+ }
3423
+ case "plugin.set-git-remote": {
3424
+ const repository = requiredPlanFlag(flags, "repository", entry.usage);
3425
+ return {
3426
+ method: "POST",
3427
+ endpoint: `${pluginRootEndpoint(id, "/gitRemote")}`,
3428
+ identifiers: { pluginId: id, },
3429
+ payload: { repositoryUrl: validatedPlanRepositoryUrl(repository, "repository", entry.usage), },
3430
+ };
3431
+ }
3432
+ case "plugin.delete-git-remote":
3433
+ return {
3434
+ method: "DELETE",
3435
+ endpoint: pluginRootEndpoint(id, "/gitRemote"),
3436
+ identifiers: { pluginId: id, },
3437
+ };
3438
+ case "plugin.push":
3439
+ case "plugin.pull":
3440
+ case "plugin.fetch":
3441
+ case "plugin.reset-local":
3442
+ case "plugin.reset-remote": {
3443
+ const actionMap = {
3444
+ "plugin.push": "push",
3445
+ "plugin.pull": "pullRebase",
3446
+ "plugin.fetch": "fetch",
3447
+ "plugin.reset-local": "resetToLocalHeadState",
3448
+ "plugin.reset-remote": "resetToRemoteHeadState",
3449
+ };
3450
+ return {
3451
+ method: "POST",
3452
+ endpoint: pluginActionEndpoint(id, actionMap[`${resource}.${action}`]),
3453
+ identifiers: { pluginId: id, },
3454
+ };
3455
+ }
3456
+ case "plugin.contents-put":
3457
+ return {
3458
+ method: "POST",
3459
+ endpoint: pluginContentsEndpoint(id, args[1] ?? ""),
3460
+ identifiers: { pluginId: id, path: args[1], },
3461
+ payload: { contentSource: contentSourceKind(flags), },
3462
+ };
3463
+ case "plugin.contents-delete":
3464
+ return {
3465
+ method: "DELETE",
3466
+ endpoint: pluginContentsEndpoint(id, args[1] ?? ""),
3467
+ identifiers: { pluginId: id, path: args[1], },
3468
+ };
3469
+ case "plugin.folder-add":
3470
+ return {
3471
+ method: "POST",
3472
+ endpoint: pluginFoldersEndpoint(id, args[1] ?? ""),
3473
+ identifiers: { pluginId: id, path: args[1], },
3474
+ };
3475
+ case "plugin.rename": {
3476
+ const newName = requiredPlanPositionals(entry.usage)[2];
3477
+ if (!newName)
3478
+ throw new UsageError(`Usage: ${entry.usage}`);
3479
+ return {
3480
+ method: "POST",
3481
+ endpoint: `${pluginRootEndpoint(id, "/contents-actions/rename")}`,
3482
+ identifiers: { pluginId: id, path: args[1], },
3483
+ payload: { oldPath: args[1], newName: args[2], },
3484
+ };
3485
+ }
3486
+ case "plugin.move": {
3487
+ const destination = args[2];
3488
+ if (!destination)
3489
+ throw new UsageError(`Usage: ${entry.usage}`);
3490
+ return {
3491
+ method: "POST",
3492
+ endpoint: `${pluginRootEndpoint(id, "/contents-actions/move")}`,
3493
+ identifiers: { pluginId: id, path: args[1], },
3494
+ payload: { oldPath: args[1], newPath: destination, },
3495
+ };
3496
+ }
2689
3497
  case "business-app.save-settings":
2690
3498
  return {
2691
3499
  method: "PUT",
@@ -2725,6 +3533,42 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2725
3533
  }
2726
3534
  case "app.create-instance": {
2727
3535
  const payload = requiredPlanJsonInput(flags, entry.usage);
3536
+ if (payload["targetProjectKey"] === undefined) {
3537
+ // Generated-at-apply mode: --plan never allocates or reserves a
3538
+ // random key. The payload stays exactly as supplied, the runtime
3539
+ // generates the key (and defaults the display name to it) during
3540
+ // apply, and no absence probe runs for a generated key, so the
3541
+ // plan carries no preflight requests and no fake concrete GET
3542
+ // path: `{targetProjectKey}` resolves at apply time.
3543
+ return {
3544
+ method: "POST",
3545
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
3546
+ identifiers: {
3547
+ appId: id,
3548
+ targetProjectKeyGeneratedDuringApply: true,
3549
+ ...(payload["targetProjectName"] === undefined
3550
+ ? { targetProjectNameGeneratedDuringApply: true, }
3551
+ : {}),
3552
+ preflightExecuted: false,
3553
+ preflightWillRunDuringApply: false,
3554
+ preflightRequests: [],
3555
+ incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
3556
+ incarnationObservationRequests: [
3557
+ {
3558
+ method: "GET",
3559
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3560
+ when: flags["wait"] === true
3561
+ ? "after-terminal-future-target"
3562
+ : "conditional-inline-hasResult-target",
3563
+ intent: "After DSS reports inline or terminal creation success for the generated key, observe creationTag for later cleanup binding.",
3564
+ },
3565
+ ],
3566
+ 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.",
3567
+ },
3568
+ payload,
3569
+ wait: flags["wait"] === true,
3570
+ };
3571
+ }
2728
3572
  const rawTargetProjectKey = stringField(payload, ["targetProjectKey",]);
2729
3573
  if (!rawTargetProjectKey || rawTargetProjectKey.trim() === "") {
2730
3574
  throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${entry.usage}`);
@@ -2817,13 +3661,110 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2817
3661
  }
2818
3662
  case "app.create-successor-instance": {
2819
3663
  const sourceProjectKey = requiredPlanFlag(flags, "from", entry.usage);
2820
- const targetProjectKey = requiredPlanFlag(flags, "to", entry.usage);
2821
- if (sourceProjectKey === targetProjectKey) {
3664
+ const targetProjectKey = optionalPlanFlag(flags, "to", entry.usage);
3665
+ if (targetProjectKey !== undefined && sourceProjectKey === targetProjectKey) {
2822
3666
  throw new UsageError("--from and --to must be different project keys.", "validation_failed", `Usage: ${entry.usage}`);
2823
3667
  }
2824
3668
  const targetProjectName = flags["name"];
2825
3669
  const copyPermissions = parseBooleanOption(flags["copy-permissions"], "--copy-permissions")
2826
3670
  ?? false;
3671
+ if (targetProjectKey === undefined) {
3672
+ // Generated-at-apply mode: --plan never allocates a key and no
3673
+ // target exists yet, so the plan makes no target-absence claim
3674
+ // and advertises no target probe. The key is generated once
3675
+ // during apply, immediately before the single instance POST;
3676
+ // `{targetProjectKey}` resolves only after the terminal future
3677
+ // names it. Source and template gates still run before the POST.
3678
+ return {
3679
+ method: "POST",
3680
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
3681
+ identifiers: {
3682
+ appId: id,
3683
+ sourceProjectKey,
3684
+ targetProjectKeyGeneratedDuringApply: true,
3685
+ targetPreflight: "not-applicable-generated-key",
3686
+ preflightExecuted: false,
3687
+ preflightWillRunDuringApply: true,
3688
+ ...(targetProjectName !== undefined
3689
+ ? { targetProjectName, }
3690
+ : { targetProjectNameGeneratedDuringApply: true, }),
3691
+ copyPermissions,
3692
+ incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
3693
+ incarnationObservationRequests: [
3694
+ {
3695
+ method: "GET",
3696
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3697
+ when: "after-terminal-future-target",
3698
+ intent: "After the terminal future names the generated successor key, observe creationTag and bind later target checks and cleanup to that hash.",
3699
+ },
3700
+ ],
3701
+ preflightRequests: [
3702
+ {
3703
+ method: "GET",
3704
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
3705
+ when: "before-create",
3706
+ intent: "Verify the --from project is a registered instance of the app.",
3707
+ },
3708
+ {
3709
+ method: "GET",
3710
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/app-manifest`,
3711
+ when: "before-create",
3712
+ intent: "Verify the --from project is an APP_INSTANCE project.",
3713
+ },
3714
+ ],
3715
+ ...(copyPermissions
3716
+ ? {
3717
+ permissionConcurrencyControl: "client-side-non-atomic-stale-identity-and-hash-checks",
3718
+ permissionRequests: [
3719
+ {
3720
+ method: "GET",
3721
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
3722
+ intent: "Snapshot the predecessor instance ACL before creation.",
3723
+ },
3724
+ {
3725
+ method: "GET",
3726
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
3727
+ intent: "Read the generated successor ACL to decide whether the copy is a no-op.",
3728
+ },
3729
+ {
3730
+ method: "GET",
3731
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3732
+ intent: "Recheck generated successor creationTag after reading its ACL; stop if the project key was reused.",
3733
+ },
3734
+ {
3735
+ method: "GET",
3736
+ endpoint: `/public/api/projects/${encodeURIComponent(sourceProjectKey)}/permissions`,
3737
+ intent: "Recheck the predecessor ACL immediately before the write and stop if its hash drifted.",
3738
+ },
3739
+ {
3740
+ method: "GET",
3741
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3742
+ intent: "Recheck generated successor creationTag immediately before the unconditional permission PUT.",
3743
+ },
3744
+ {
3745
+ method: "PUT",
3746
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
3747
+ intent: "Apply the predecessor ACL snapshot to the generated successor instance.",
3748
+ },
3749
+ {
3750
+ method: "GET",
3751
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/permissions",
3752
+ intent: "Verify the generated successor ACL hash equals the predecessor snapshot hash.",
3753
+ },
3754
+ {
3755
+ method: "GET",
3756
+ endpointTemplate: "/public/api/projects/{targetProjectKey}/",
3757
+ intent: "Detect generated successor project-key reuse across the permission write and verification read.",
3758
+ },
3759
+ ],
3760
+ }
3761
+ : {}),
3762
+ 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.",
3763
+ },
3764
+ payload: targetProjectName !== undefined ? { targetProjectName, } : {},
3765
+ wait: true,
3766
+ };
3767
+ }
2827
3768
  return {
2828
3769
  method: "POST",
2829
3770
  endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances`,
@@ -4076,6 +5017,51 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
4076
5017
  endpoint: projectGitFutureEndpoint(id),
4077
5018
  identifiers: { jobId: id, },
4078
5019
  };
5020
+ case "project-folder.settings-set": {
5021
+ const payload = requiredPlanJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${entry.usage}`);
5022
+ return {
5023
+ method: "PUT",
5024
+ endpoint: `${projectFolderEndpoint(id)}/settings`,
5025
+ identifiers: { folderId: id, },
5026
+ payload,
5027
+ };
5028
+ }
5029
+ case "project-folder.move": {
5030
+ const destination = args[1];
5031
+ if (!destination)
5032
+ throw new UsageError(`Usage: ${entry.usage}`);
5033
+ return {
5034
+ method: "POST",
5035
+ endpoint: `${projectFolderEndpoint(id)}/move${querySuffix({ destination, })}`,
5036
+ identifiers: { folderId: id, destination, },
5037
+ };
5038
+ }
5039
+ case "project-folder.delete":
5040
+ return {
5041
+ method: "DELETE",
5042
+ endpoint: projectFolderEndpoint(id),
5043
+ identifiers: { folderId: id, },
5044
+ };
5045
+ case "project-folder.create-child": {
5046
+ const name = requiredPlanFlag(flags, "name", entry.usage);
5047
+ return {
5048
+ method: "POST",
5049
+ endpoint: `${projectFolderEndpoint(id)}/children${querySuffix({ name, })}`,
5050
+ identifiers: { parentFolderId: id, name, },
5051
+ };
5052
+ }
5053
+ case "project-folder.move-project": {
5054
+ const projectKeyArg = args[1];
5055
+ const destination = args[2];
5056
+ if (!projectKeyArg || !destination) {
5057
+ throw new UsageError(`Usage: ${entry.usage}`);
5058
+ }
5059
+ return {
5060
+ method: "POST",
5061
+ endpoint: `${projectFolderEndpoint(id)}/projects/${encodeURIComponent(projectKeyArg)}/move${querySuffix({ destination, })}`,
5062
+ identifiers: { folderId: id, projectKey: projectKeyArg, destination, },
5063
+ };
5064
+ }
4079
5065
  default:
4080
5066
  return {
4081
5067
  exact: false,