dataiku-sdk 0.12.7 → 1.0.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 (51) hide show
  1. package/README.md +60 -14
  2. package/bin/dss.js +1 -1
  3. package/dist/packages/types/src/index.d.ts +128 -3
  4. package/dist/packages/types/src/index.js +106 -3
  5. package/dist/src/cli/coerce.d.ts +2 -1
  6. package/dist/src/cli/coerce.js +84 -26
  7. package/dist/src/cli/commands/app.js +6 -6
  8. package/dist/src/cli/commands/code.js +2 -2
  9. package/dist/src/cli/commands/data-quality.js +8 -8
  10. package/dist/src/cli/commands/dataset.js +11 -7
  11. package/dist/src/cli/commands/folder.js +5 -4
  12. package/dist/src/cli/commands/future.js +2 -2
  13. package/dist/src/cli/commands/index.js +2 -0
  14. package/dist/src/cli/commands/job.js +11 -11
  15. package/dist/src/cli/commands/notebook.js +1 -1
  16. package/dist/src/cli/commands/project-git.d.ts +2 -0
  17. package/dist/src/cli/commands/project-git.js +563 -0
  18. package/dist/src/cli/commands/project.js +2 -2
  19. package/dist/src/cli/commands/recipe.js +14 -17
  20. package/dist/src/cli/commands/scenario.js +4 -4
  21. package/dist/src/cli/contract.d.ts +5 -4
  22. package/dist/src/cli/contract.js +413 -55
  23. package/dist/src/cli/doctor.js +6 -6
  24. package/dist/src/cli/flags.js +21 -2
  25. package/dist/src/cli/helpers/dataset.js +1 -1
  26. package/dist/src/cli/output.d.ts +2 -1
  27. package/dist/src/cli/output.js +2 -6
  28. package/dist/src/cli/usage.d.ts +1 -1
  29. package/dist/src/cli/usage.js +2 -1
  30. package/dist/src/cli/version.d.ts +2 -2
  31. package/dist/src/cli/version.js +2 -2
  32. package/dist/src/cli.js +43 -26
  33. package/dist/src/client.d.ts +3 -0
  34. package/dist/src/client.js +5 -0
  35. package/dist/src/errors.d.ts +1 -1
  36. package/dist/src/index.d.ts +3 -2
  37. package/dist/src/index.js +2 -1
  38. package/dist/src/resources/connections.d.ts +3 -1
  39. package/dist/src/resources/connections.js +82 -21
  40. package/dist/src/resources/datasets.js +42 -2
  41. package/dist/src/resources/folders.js +7 -3
  42. package/dist/src/resources/project-git.d.ts +154 -0
  43. package/dist/src/resources/project-git.js +427 -0
  44. package/dist/src/resources/recipes.js +4 -4
  45. package/dist/src/schemas.d.ts +2 -2
  46. package/dist/src/schemas.js +1 -1
  47. package/package.json +1 -1
  48. package/packages/types/dist/index.d.ts +128 -3
  49. package/packages/types/dist/index.js +106 -3
  50. package/plugin.json +1 -1
  51. package/skills/dataiku-dss/SKILL.md +26 -19
@@ -4,7 +4,7 @@ import { validateCredentials, } from "../auth.js";
4
4
  import { getCredentialsPath, saveCredentials, } from "../config.js";
5
5
  import { DataikuError, } from "../errors.js";
6
6
  import { APP_MANIFEST_CONCURRENCY_CONTROL, } from "../resources/applications.js";
7
- import { jobBuildTargetTypeFromFlags, json, jsonInput, num, parseBooleanOption, parseJsonObject, requiredJsonInput, rewritePairsFromFlags, schemaColumnsInput, stringField, textInput, } from "./coerce.js";
7
+ import { jobBuildTargetTypeFromFlags, json, jsonInput, num, parseBooleanOption, parseJsonObject, recipeInputDatasetsFromFlags, requiredJsonInput, rewritePairsFromFlags, schemaColumnsInput, stringField, textInput, } from "./coerce.js";
8
8
  import { commands, } from "./commands/index.js";
9
9
  import { dataikuEnvironmentEnabled, } from "./env.js";
10
10
  import { BOOLEAN_FLAGS, FLAG_ALIASES, KNOWN_LONG_FLAGS, SHORT_FLAGS, } from "./flags.js";
@@ -151,7 +151,64 @@ const PROJECT_SCOPED_RESOURCES = new Set([
151
151
  "saved-model",
152
152
  "wiki",
153
153
  ]);
154
- const GLOBAL_AGENT_FLAGS = ["json", "verbose", "fields",];
154
+ /**
155
+ * Project Git actions that only observe repository state. Everything else in the
156
+ * `project-git` resource is a mutation: the verb-shaped names (`fetch`, `pull`,
157
+ * `switch`, `commit`, `reset-*`, `revert-*`, `drop-and-rebuild`, `future-abort`)
158
+ * match none of the mutating-verb patterns and would otherwise fall through to
159
+ * the read default, advertising network and history writes as safe reads.
160
+ */
161
+ const PROJECT_GIT_READ_ACTIONS = {
162
+ branches: true,
163
+ "current-branch": true,
164
+ diff: true,
165
+ "future-status": true,
166
+ "future-wait": true,
167
+ "get-remote": true,
168
+ "list-libraries": true,
169
+ log: true,
170
+ status: true,
171
+ tags: true,
172
+ };
173
+ /**
174
+ * Git mutations that destroy work rather than add to it: `drop-and-rebuild`
175
+ * discards the repository and rebuilds it from DSS, the resets and reverts throw
176
+ * away local commits or working state, `remove-library` can delete the checked
177
+ * out directory, and the branch/tag deletions can remove remote refs.
178
+ */
179
+ const PROJECT_GIT_DESTRUCTIVE_ACTIONS = {
180
+ "delete-branch": true,
181
+ "delete-tag": true,
182
+ "drop-and-rebuild": true,
183
+ "future-abort": true,
184
+ "remove-library": true,
185
+ "reset-all-libraries": true,
186
+ "reset-library": true,
187
+ "reset-to-head": true,
188
+ "reset-to-upstream": true,
189
+ "revert-commit": true,
190
+ "revert-to-revision": true,
191
+ };
192
+ /** Project Git actions that hand back a DSS future (`{jobId}`) instead of a result. */
193
+ const PROJECT_GIT_FUTURE_ACTIONS = {
194
+ "add-library": true,
195
+ "future-abort": true,
196
+ "future-status": true,
197
+ "future-wait": true,
198
+ "push-all-libraries": true,
199
+ "push-library": true,
200
+ "reset-all-libraries": true,
201
+ "reset-library": true,
202
+ };
203
+ /** Git mutations that converge: replaying them lands on the same repository state. */
204
+ const PROJECT_GIT_CONVERGENT_ACTIONS = {
205
+ fetch: true,
206
+ "reset-all-libraries": true,
207
+ "reset-library": true,
208
+ "reset-to-head": true,
209
+ "reset-to-upstream": true,
210
+ };
211
+ const GLOBAL_AGENT_FLAGS = ["verbose", "fields",];
155
212
  const AUTHENTICATED_AGENT_FLAGS = [
156
213
  "url",
157
214
  "api-key",
@@ -160,19 +217,21 @@ const AUTHENTICATED_AGENT_FLAGS = [
160
217
  "insecure",
161
218
  "ca-cert",
162
219
  ];
163
- export const COMMANDS_USAGE = "dss commands run [--fields PATHS] [--json]";
164
- const COMMANDS_DESCRIPTION = "Print the machine-readable command registry for agent planning; scope it with --fields RESOURCE, RESOURCE.ACTION, or RESOURCE.ACTION.FIELD instead of reading the whole registry.";
220
+ export const COMMANDS_USAGE = "dss commands run [--fields PATHS] [--output PATH]";
221
+ 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.";
165
222
  const COMMANDS_EXAMPLES = [
166
- "dss commands run --fields dataset --json",
167
- "dss commands run --fields dataset.create --json",
168
- "dss commands run --fields dataset.create.usage,dataset.create.description,dataset.create.flags,dataset.create.examples --json",
223
+ "dss commands run",
224
+ "dss commands run --fields dataset",
225
+ "dss commands run --fields dataset.create",
226
+ "dss commands run --fields dataset.create.usage,dataset.create.description,dataset.create.flags,dataset.create.examples",
227
+ "dss commands run --output commands.json",
169
228
  ];
170
229
  export const AGENT_CONTRACT_COMMAND = "dss agent contract";
171
- export const AGENT_CONTRACT_USAGE = "dss agent contract [--fields PATHS] [--json]";
230
+ export const AGENT_CONTRACT_USAGE = "dss agent contract [--fields PATHS]";
172
231
  const AGENT_CONTRACT_DESCRIPTION = "Print the versioned JSON agent contract; scope bootstrap fields, use commands.actions to enumerate the surface, and read schemas only when needed.";
173
232
  const AGENT_CONTRACT_EXAMPLES = [
174
- "dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility --json",
175
- "dss agent contract --fields commands.actions --json",
233
+ "dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility",
234
+ "dss agent contract --fields commands.actions",
176
235
  ];
177
236
  const VERSION_USAGE = "dss version";
178
237
  const VERSION_DESCRIPTION = "Print the CLI version and git revision as JSON.";
@@ -189,11 +248,11 @@ const CLEANUP_EXAMPLES = [
189
248
  "dss cleanup --file cleanup.jsonl",
190
249
  "dss cleanup --file cleanup.jsonl --apply",
191
250
  ];
192
- const FIXTURES_USAGE = "dss fixtures [--json] [--project-key KEY] [--allow-types CSV]";
251
+ const FIXTURES_USAGE = "dss fixtures [--project-key KEY] [--allow-types CSV]";
193
252
  const FIXTURES_DESCRIPTION = "Discover safe live-test fixtures for agent workflows.";
194
253
  const FIXTURES_EXAMPLES = [
195
- "dss fixtures --json",
196
- "dss fixtures --json --allow-types Filesystem,Inline",
254
+ "dss fixtures",
255
+ "dss fixtures --allow-types Filesystem,Inline",
197
256
  ];
198
257
  const ALLOWED_CLEANUP_ACTIONS = new Set([
199
258
  // Must mirror every cleanup.argv shape emitted by cleanupLedgerEntry().
@@ -395,7 +454,7 @@ function shortFlagPattern(flags) {
395
454
  * between `dss` and the first positional. Synthetic single-action commands
396
455
  * whose usage omits their action token (e.g. `dss batch ...`, `dss version`)
397
456
  * use an actionless prefix, while real command paths that spell `run` in their
398
- * usage (e.g. `dss commands run [--json]`) keep the action token.
457
+ * usage (e.g. `dss commands run`) keep the action token.
399
458
  */
400
459
  function argvPrefix(resource, action, usage) {
401
460
  const usageTokens = usage.split(/\s+/).filter((token) => token.length > 0);
@@ -481,18 +540,8 @@ function argvJsonSchema(resource, action, flags, usage, requiredFlags, requiredO
481
540
  },
482
541
  };
483
542
  }
484
- function unsafeOutputs(resource, action, flags, producesLocalFile) {
543
+ function unsafeOutputs(resource, action, producesLocalFile) {
485
544
  const outputs = [];
486
- if (flags.some((flag) => flag.name === "raw")) {
487
- outputs.push({
488
- condition: "--raw without --output",
489
- kind: "raw-stdout",
490
- detail: `${resource} ${action} can intentionally write raw payload bytes/text instead of JSON.`,
491
- safeAlternative: flags.some((flag) => flag.name === "output")
492
- ? "Pass --output PATH so stdout remains a JSON string containing the path."
493
- : "Omit --raw when a JSON stdout value is required.",
494
- });
495
- }
496
545
  if (producesLocalFile) {
497
546
  const sensitivePermissions = resource === "app" && action === "permissions-snapshot";
498
547
  outputs.push({
@@ -589,6 +638,13 @@ function inferSideEffect(resource, action) {
589
638
  return "read";
590
639
  if (resource === "data-quality" && action === "compute")
591
640
  return "write";
641
+ // Project Git is classified from the explicit read table, not by verb shape:
642
+ // `switch`, `fetch`, `pull`, `push`, `commit`, the `reset-*`/`revert-*`
643
+ // families, `drop-and-rebuild`, and `future-abort` all mutate repository or
644
+ // future state yet match none of the mutating-verb patterns.
645
+ if (resource === "project-git") {
646
+ return PROJECT_GIT_READ_ACTIONS[action] === true ? "read" : "write";
647
+ }
592
648
  if (READ_ACTIONS.has(action))
593
649
  return "read";
594
650
  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)/
@@ -606,13 +662,15 @@ function inferRequiresAuth(resource) {
606
662
  && resource !== "install-skill"
607
663
  && resource !== "version";
608
664
  }
609
- export function inferRequiresProject(resource, _action, usage) {
665
+ export function inferRequiresProject(resource, action, usage) {
610
666
  if (resource === "agent" || resource === "auth" || resource === "doctor" || resource === "commands"
611
667
  || resource === "install-skill" || resource === "version") {
612
668
  return false;
613
669
  }
614
670
  if (PROJECT_SCOPED_RESOURCES.has(resource))
615
671
  return true;
672
+ if (resource === "project-git")
673
+ return !action.startsWith("future-");
616
674
  return usage.includes("--project-key");
617
675
  }
618
676
  const ARRAY_OUTPUT_ACTIONS = new Set([
@@ -639,6 +697,13 @@ const STRING_OUTPUT_ACTIONS = new Set([
639
697
  "log-url",
640
698
  ]);
641
699
  function inferOutputShape(resource, action) {
700
+ if (resource === "project-git") {
701
+ if (action === "branches" || action === "tags" || action === "list-libraries")
702
+ return "array";
703
+ if (action === "set-library")
704
+ return "string";
705
+ return "object";
706
+ }
642
707
  if (resource === "agent" || resource === "auth" || resource === "commands"
643
708
  || resource === "install-skill"
644
709
  || resource === "version") {
@@ -830,9 +895,12 @@ export function supportsCleanupLedger(resource, action) {
830
895
  return cleanupCommandFromDeleteUsage(resource, action) !== undefined
831
896
  || `${resource}.${action}` === "folder.upload";
832
897
  }
833
- function inferDestructiveLevel(sideEffect, action) {
898
+ function inferDestructiveLevel(resource, sideEffect, action) {
834
899
  if (sideEffect !== "write")
835
900
  return "none";
901
+ if (resource === "project-git") {
902
+ return PROJECT_GIT_DESTRUCTIVE_ACTIONS[action] === true ? "destructive" : "reversible";
903
+ }
836
904
  if (/^(delete|abort|clear|unload|logout)/.test(action))
837
905
  return "destructive";
838
906
  return "reversible";
@@ -850,6 +918,10 @@ function inferAsyncKind(resource, action) {
850
918
  }
851
919
  if (resource === "data-quality" && action === "compute")
852
920
  return "future";
921
+ // Project Git mutation results carry a future job id only for the library
922
+ // calls and the future lifecycle itself; plain Git actions settle inline.
923
+ if (resource === "project-git" && PROJECT_GIT_FUTURE_ACTIONS[action] === true)
924
+ return "future";
853
925
  if (resource === "code" && action === "run")
854
926
  return "future";
855
927
  if (resource === "app"
@@ -872,6 +944,11 @@ function inferIdempotency(resource, sideEffect, action, usage) {
872
944
  // error, so replaying the delete cannot fail on absence alone.
873
945
  if (`${resource}.${action}` === "app.delete-instance")
874
946
  return "convergent";
947
+ // Git fetch and the repository resets converge: replaying them lands on the
948
+ // same repository state instead of failing on an already-applied change.
949
+ if (resource === "project-git" && PROJECT_GIT_CONVERGENT_ACTIONS[action] === true) {
950
+ return "convergent";
951
+ }
875
952
  if (/^(clear|refresh|set|save)/.test(action))
876
953
  return "convergent";
877
954
  return "none";
@@ -890,6 +967,10 @@ const LEDGER_ONLY_CLEANUP_KEYS = {
890
967
  };
891
968
  const LEDGER_ONLY_CLEANUP_HINT = "Pass `--record-cleanup PATH` when creating, then recover with `dss cleanup --file PATH --apply`. Do not issue direct instance deletion as cleanup: it bypasses the creation-future gate and can delete the project while DSS is still creating it.";
892
969
  export function inferCleanupHint(resource, action) {
970
+ // Git mutations are not cleanup-ledger operations: no project-git branch or
971
+ // tag create has a generic `delete` command to close it out.
972
+ if (resource === "project-git")
973
+ return undefined;
893
974
  if (!(action.startsWith("create") || action === "clone"))
894
975
  return undefined;
895
976
  if (LEDGER_ONLY_CLEANUP_KEYS[`${resource}.${action}`] === true) {
@@ -914,7 +995,7 @@ function buildRegistryEntry(resource, action, meta) {
914
995
  const requiresAuth = inferRequiresAuth(resource);
915
996
  const requiresProject = inferRequiresProject(resource, action, meta.usage);
916
997
  const sideEffect = inferSideEffect(resource, action);
917
- const destructive = inferDestructiveLevel(sideEffect, action);
998
+ const destructive = inferDestructiveLevel(resource, sideEffect, action);
918
999
  const asyncKind = inferAsyncKind(resource, action);
919
1000
  const mutatesDss = sideEffect === "write" && resource !== "auth" && resource !== "install-skill";
920
1001
  const supportsPlan = mutatesDss || sideEffect === "write";
@@ -980,7 +1061,7 @@ function buildRegistryEntry(resource, action, meta) {
980
1061
  || meta.usage.includes("--output-file PATH");
981
1062
  const uniqueRequiredFlags = uniqueStrings(requiredFlags);
982
1063
  const uniqueOptionalFlags = uniqueStrings(optionalFlags);
983
- const unsafe = unsafeOutputs(resource, action, flagMetadata, producesLocalFile);
1064
+ const unsafe = unsafeOutputs(resource, action, producesLocalFile);
984
1065
  return {
985
1066
  resource,
986
1067
  action,
@@ -1229,7 +1310,7 @@ export function agentContractJsonSchema() {
1229
1310
  },
1230
1311
  };
1231
1312
  }
1232
- function commandActionSummary(registry) {
1313
+ export function commandActionSummary(registry) {
1233
1314
  const summary = {};
1234
1315
  for (const [resource, actions,] of Object.entries(registry)) {
1235
1316
  summary[resource] = Object.keys(actions).sort();
@@ -1244,15 +1325,14 @@ export function buildAgentContract() {
1244
1325
  cli: cliVersionResult(),
1245
1326
  commands: {
1246
1327
  discoveryCommand: "dss commands run",
1247
- scopedDiscoveryCommand: "dss commands run --fields RESOURCE[.ACTION[.FIELD...]] --json",
1248
- actionIndexCommand: "dss agent contract --fields commands.actions --json",
1249
- compactOutputFlag: "--json",
1250
- compactOutputHint: "Pass --json to emit compact single-line JSON and reduce agent context usage.",
1328
+ fullRegistryExportCommand: "dss commands run --output PATH",
1329
+ scopedDiscoveryCommand: "dss commands run --fields RESOURCE[.ACTION[.FIELD...]]",
1330
+ actionIndexCommand: "dss agent contract --fields commands.actions",
1251
1331
  scopedDiscoveryExamples: [
1252
- "dss commands run --fields dataset --json",
1253
- "dss commands run --fields dataset.create --json",
1332
+ "dss commands run --fields dataset",
1333
+ "dss commands run --fields dataset.create",
1254
1334
  ],
1255
- scopedDiscoveryHint: "--fields RESOURCE returns every action of one resource; --fields RESOURCE.ACTION returns a single registry entry keyed by the dotted path; append .FIELD paths to project nested metadata. Comma-separate paths to select several.",
1335
+ scopedDiscoveryHint: "`dss commands run` returns the resource/action summary. --fields RESOURCE returns every action of one resource; --fields RESOURCE.ACTION returns a single registry entry keyed by the dotted path; append .FIELD paths to project nested metadata. Comma-separate paths to select several. Use --output PATH to export the full registry.",
1256
1336
  actions: commandActionSummary(registry),
1257
1337
  },
1258
1338
  schemas: {
@@ -1265,28 +1345,22 @@ export function buildAgentContract() {
1265
1345
  },
1266
1346
  stdio: {
1267
1347
  stdout: {
1348
+ format: "compact-json",
1268
1349
  success: "single-json-value",
1269
- rawEscapeHatches: [
1270
- "recipe get-payload --raw without --output",
1271
- "recipe cat --raw without --output",
1272
- ],
1350
+ failure: "structured-error-object",
1351
+ richFailureResults: "doctor/batch/cleanup nonzero outcomes are their own compact result on stdout ({ok:false,...}) with the command's exit code; not re-wrapped.",
1273
1352
  },
1274
1353
  stderr: {
1275
1354
  format: "jsonl",
1276
- events: ["warning", "trace", "error",],
1277
- error: "single-final-error-event-on-nonzero-exit",
1278
- failureRouting: {
1279
- commandFailure: "doctor/batch/cleanup failures: single JSON stdout; empty stderr",
1280
- dispatchFailure: "usage/unknown/transport/internal failures: empty stdout; single JSON error stderr",
1281
- },
1355
+ events: ["warning", "trace",],
1282
1356
  },
1283
1357
  },
1284
1358
  planning: {
1285
1359
  discoveryCommand: "dss commands run",
1286
1360
  contractCommand: AGENT_CONTRACT_COMMAND,
1287
- bootstrapCommand: "dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility --json",
1288
- preferredDiscoveryCommand: "dss commands run --fields RESOURCE.ACTION --json",
1289
- actionIndexCommand: "dss agent contract --fields commands.actions --json",
1361
+ bootstrapCommand: "dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility",
1362
+ preferredDiscoveryCommand: "dss commands run --fields RESOURCE.ACTION",
1363
+ actionIndexCommand: "dss agent contract --fields commands.actions",
1290
1364
  mutatingCommandsAdvertisePlan: true,
1291
1365
  },
1292
1366
  compatibility: {
@@ -1375,6 +1449,67 @@ function querySuffix(params) {
1375
1449
  const raw = search.toString();
1376
1450
  return raw ? `?${raw}` : "";
1377
1451
  }
1452
+ /**
1453
+ * Git and future API mount. The official Python client mounts these routes on
1454
+ * `/dip/publicapi`, which is also what the project-git resource uses; do not
1455
+ * normalize them to the repo-wide `/public/api`.
1456
+ */
1457
+ const PROJECT_GIT_API_ROOT = "/dip/publicapi";
1458
+ function projectGitEndpoint(projectKey, suffix) {
1459
+ if (!projectKey)
1460
+ throw new UsageError("--project-key is required for project-git mutations.");
1461
+ return `${PROJECT_GIT_API_ROOT}/projects/${encodeURIComponent(projectKey)}/git${suffix}`;
1462
+ }
1463
+ function projectGitFutureEndpoint(jobId) {
1464
+ return `${PROJECT_GIT_API_ROOT}/futures/${encodeURIComponent(jobId)}`;
1465
+ }
1466
+ /** Encode a library reference path per segment, mirroring the resource helper. */
1467
+ function encodeGitReferencePath(pathValue) {
1468
+ const normalized = pathValue.replace(/^\/+/, "").replace(/\/+$/, "");
1469
+ return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1470
+ }
1471
+ /**
1472
+ * `--plan` bypasses the SDK boundary guard, so HTTP(S) URLs with embedded
1473
+ * userinfo are rejected here before any plan is printed. SSH/scp-style URLs
1474
+ * (`git@host:org/repo.git`, `ssh://...`) remain valid.
1475
+ */
1476
+ function validatedPlanRepositoryUrl(url, flag, usage) {
1477
+ const candidate = url.trim();
1478
+ if (/[\u0000-\u001f\u007f]/u.test(candidate)) {
1479
+ throw new UsageError(`--${flag} must not contain control characters. Usage: ${usage}`);
1480
+ }
1481
+ if (/^https?:/i.test(candidate)) {
1482
+ if (candidate.includes("\\")) {
1483
+ throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
1484
+ }
1485
+ if (!/^https?:\/\//i.test(candidate)) {
1486
+ throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
1487
+ }
1488
+ let parsed;
1489
+ try {
1490
+ parsed = new URL(candidate);
1491
+ }
1492
+ catch {
1493
+ throw new UsageError(`--${flag} must be a valid HTTP(S) URL. Usage: ${usage}`);
1494
+ }
1495
+ if (parsed.username !== "" || parsed.password !== "") {
1496
+ throw new UsageError(`--${flag} must not contain embedded credentials (userinfo). Usage: ${usage}`);
1497
+ }
1498
+ }
1499
+ return candidate;
1500
+ }
1501
+ function requiredPlanRepositoryUrl(flags, flag, usage) {
1502
+ const url = requiredPlanFlag(flags, flag, usage);
1503
+ return validatedPlanRepositoryUrl(url, flag, usage);
1504
+ }
1505
+ /** Wait procedure advertised for the library calls that return a job id. */
1506
+ function projectGitFutureWait() {
1507
+ return {
1508
+ when: "after-dispatch",
1509
+ endpoint: `${PROJECT_GIT_API_ROOT}/futures/{jobId}?peek=false`,
1510
+ description: "Poll the returned job id until the future reports a result; never abort it.",
1511
+ };
1512
+ }
1378
1513
  function jobBuildPayload(target, projectKey, flags) {
1379
1514
  const targetType = jobBuildTargetTypeFromFlags(flags);
1380
1515
  const partition = flags["partition"];
@@ -1609,8 +1744,11 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1609
1744
  };
1610
1745
  case "dataset.create": {
1611
1746
  const name = requiredPlanFlag(flags, "name", entry.usage);
1612
- const connection = requiredPlanFlag(flags, "connection", entry.usage);
1747
+ const connection = flags["connection"];
1613
1748
  const dsType = requiredPlanFlag(flags, "type", entry.usage);
1749
+ if (!connection && dsType.toLowerCase() !== "uploadedfiles") {
1750
+ throw new UsageError("--connection is required unless --type is UploadedFiles.");
1751
+ }
1614
1752
  return {
1615
1753
  method: "POST",
1616
1754
  endpoint: projectEndpoint("/datasets/"),
@@ -1740,7 +1878,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1740
1878
  payload: {
1741
1879
  type,
1742
1880
  name: flags["name"],
1743
- inputDatasets: flags["input"] ? [flags["input"],] : undefined,
1881
+ inputDatasets: recipeInputDatasetsFromFlags(flags),
1744
1882
  outputDataset,
1745
1883
  outputFolder,
1746
1884
  outputConnection: flags["output-connection"],
@@ -1840,8 +1978,8 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1840
1978
  };
1841
1979
  case "folder.create": {
1842
1980
  const name = requiredPlanFlag(flags, "name", entry.usage);
1843
- const type = requiredPlanFlag(flags, "type", entry.usage);
1844
- const connection = requiredPlanFlag(flags, "connection", entry.usage);
1981
+ const type = flags["type"];
1982
+ const connection = flags["connection"];
1845
1983
  return {
1846
1984
  method: "POST",
1847
1985
  endpoint: projectEndpoint("/managedfolders/"),
@@ -2655,7 +2793,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2655
2793
  method: "PUT",
2656
2794
  endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}/history`),
2657
2795
  identifiers: { id, cellId: flags["cell-id"], },
2658
- payload: { retain: num(flags["retain"]), },
2796
+ payload: { retain: num(flags["retain"], "--retain"), },
2659
2797
  };
2660
2798
  case "project.create": {
2661
2799
  const settings = jsonInput(flags) ?? null;
@@ -2755,6 +2893,226 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2755
2893
  endpoint: projectEndpoint("/flow/zones"),
2756
2894
  payload: jsonInput(flags),
2757
2895
  };
2896
+ /* ---- project-git: mutations only; reads are never planned ---- */
2897
+ case "project-git.set-remote": {
2898
+ const remote = flags["name"] ?? "origin";
2899
+ return {
2900
+ method: "POST",
2901
+ endpoint: projectGitEndpoint(projectKey, `/remotes/${encodeURIComponent(remote)}`),
2902
+ identifiers: { remote, },
2903
+ payload: { url: requiredPlanRepositoryUrl(flags, "repository", entry.usage), },
2904
+ };
2905
+ }
2906
+ case "project-git.remove-remote": {
2907
+ const remote = flags["name"] ?? "origin";
2908
+ return {
2909
+ method: "DELETE",
2910
+ endpoint: projectGitEndpoint(projectKey, `/remotes/${encodeURIComponent(remote)}`),
2911
+ identifiers: { remote, },
2912
+ };
2913
+ }
2914
+ case "project-git.create-branch":
2915
+ return {
2916
+ method: "POST",
2917
+ endpoint: projectGitEndpoint(projectKey, "/branches/"),
2918
+ identifiers: { name: id, },
2919
+ payload: {
2920
+ name: id,
2921
+ commit: flags["commit"] ?? null,
2922
+ duplicateProject: parseBooleanOption(flags["duplicate-project"], "--duplicate-project") ?? false,
2923
+ targetProjectKey: flags["target-project-key"] ?? null,
2924
+ targetProjectFolderId: flags["target-project-folder-id"] ?? null,
2925
+ },
2926
+ };
2927
+ case "project-git.delete-branch": {
2928
+ const remote = parseBooleanOption(flags["remote"], "--remote") ?? false;
2929
+ return {
2930
+ method: "POST",
2931
+ endpoint: projectGitEndpoint(projectKey, "/actions/deleteBranch"),
2932
+ identifiers: { name: id, remote, },
2933
+ payload: {
2934
+ name: id,
2935
+ remote,
2936
+ deleteRemotely: parseBooleanOption(flags["delete-remotely"], "--delete-remotely") ?? false,
2937
+ forceDelete: parseBooleanOption(flags["force-delete"], "--force-delete") ?? false,
2938
+ },
2939
+ };
2940
+ }
2941
+ case "project-git.switch":
2942
+ return {
2943
+ method: "POST",
2944
+ endpoint: projectGitEndpoint(projectKey, `/actions/switchBranch${querySuffix({ branchName: id, })}`),
2945
+ identifiers: { branch: id, },
2946
+ };
2947
+ case "project-git.create-tag":
2948
+ return {
2949
+ method: "POST",
2950
+ endpoint: projectGitEndpoint(projectKey, "/tags/"),
2951
+ identifiers: { name: id, },
2952
+ payload: {
2953
+ name: id,
2954
+ reference: flags["reference"] ?? "HEAD",
2955
+ message: flags["message"] ?? "",
2956
+ },
2957
+ };
2958
+ case "project-git.delete-tag":
2959
+ return {
2960
+ method: "POST",
2961
+ endpoint: projectGitEndpoint(projectKey, "/actions/deleteTag"),
2962
+ identifiers: { name: id, },
2963
+ payload: { name: id, },
2964
+ };
2965
+ case "project-git.fetch":
2966
+ return {
2967
+ method: "POST",
2968
+ endpoint: projectGitEndpoint(projectKey, "/actions/fetch"),
2969
+ };
2970
+ case "project-git.pull":
2971
+ return {
2972
+ method: "POST",
2973
+ endpoint: projectGitEndpoint(projectKey, `/actions/pullRebase${querySuffix({
2974
+ branchName: flags["branch"],
2975
+ })}`),
2976
+ ...(flags["branch"] !== undefined
2977
+ ? { identifiers: { branch: flags["branch"], }, }
2978
+ : {}),
2979
+ };
2980
+ case "project-git.push":
2981
+ return {
2982
+ method: "POST",
2983
+ endpoint: projectGitEndpoint(projectKey, `/actions/push${querySuffix({
2984
+ branchName: flags["branch"],
2985
+ })}`),
2986
+ ...(flags["branch"] !== undefined
2987
+ ? { identifiers: { branch: flags["branch"], }, }
2988
+ : {}),
2989
+ };
2990
+ case "project-git.commit": {
2991
+ const message = requiredPlanFlag(flags, "message", entry.usage);
2992
+ return {
2993
+ method: "POST",
2994
+ endpoint: projectGitEndpoint(projectKey, "/actions/commit"),
2995
+ identifiers: { message, },
2996
+ payload: { message, },
2997
+ };
2998
+ }
2999
+ case "project-git.revert-to-revision":
3000
+ return {
3001
+ method: "POST",
3002
+ endpoint: projectGitEndpoint(projectKey, `/actions/revertToRevision${querySuffix({ commit: id, })}`),
3003
+ identifiers: { commit: id, },
3004
+ };
3005
+ case "project-git.revert-commit":
3006
+ return {
3007
+ method: "POST",
3008
+ endpoint: projectGitEndpoint(projectKey, `/actions/revertCommit${querySuffix({ commit: id, })}`),
3009
+ identifiers: { commit: id, },
3010
+ };
3011
+ case "project-git.reset-to-head":
3012
+ return {
3013
+ method: "POST",
3014
+ endpoint: projectGitEndpoint(projectKey, "/actions/resetToLocalHeadState"),
3015
+ };
3016
+ case "project-git.reset-to-upstream":
3017
+ return {
3018
+ method: "POST",
3019
+ endpoint: projectGitEndpoint(projectKey, "/actions/resetToRemoteHeadState"),
3020
+ };
3021
+ case "project-git.drop-and-rebuild": {
3022
+ if (parseBooleanOption(flags["i-know-what-i-am-doing"], "--i-know-what-i-am-doing") !== true) {
3023
+ throw new UsageError(`--i-know-what-i-am-doing is required to acknowledge the irreversible Git history loss. Usage: ${entry.usage}`);
3024
+ }
3025
+ return {
3026
+ method: "POST",
3027
+ endpoint: projectGitEndpoint(projectKey, `/actions/dropAndRebuild${querySuffix({ iKnowWhatIAmDoing: true, })}`),
3028
+ };
3029
+ }
3030
+ case "project-git.add-library": {
3031
+ const targetPath = id;
3032
+ return {
3033
+ method: "POST",
3034
+ endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/"),
3035
+ identifiers: { localTargetPath: targetPath, },
3036
+ payload: {
3037
+ repository: requiredPlanRepositoryUrl(flags, "repository", entry.usage),
3038
+ login: flags["login"] ?? null,
3039
+ password: flags["password-env"] !== undefined ? "***" : null,
3040
+ pathInGitRepository: flags["path-in-repository"] ?? "",
3041
+ localTargetPath: targetPath,
3042
+ checkout: requiredPlanFlag(flags, "checkout", entry.usage),
3043
+ addToPythonPath: parseBooleanOption(flags["no-add-to-python-path"], "--no-add-to-python-path") !== true,
3044
+ },
3045
+ wait: projectGitFutureWait(),
3046
+ };
3047
+ }
3048
+ case "project-git.set-library": {
3049
+ const targetPath = id;
3050
+ return {
3051
+ method: "PUT",
3052
+ endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}`),
3053
+ identifiers: { library: targetPath, },
3054
+ payload: {
3055
+ repository: requiredPlanRepositoryUrl(flags, "repository", entry.usage),
3056
+ login: flags["login"] ?? null,
3057
+ password: flags["password-env"] !== undefined ? "***" : null,
3058
+ pathInGitRepository: flags["path-in-repository"] ?? "",
3059
+ checkout: requiredPlanFlag(flags, "checkout", entry.usage),
3060
+ },
3061
+ };
3062
+ }
3063
+ case "project-git.remove-library": {
3064
+ const targetPath = id;
3065
+ return {
3066
+ method: "DELETE",
3067
+ endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}${querySuffix({
3068
+ deleteDirectory: parseBooleanOption(flags["delete-directory"], "--delete-directory") ?? false,
3069
+ })}`),
3070
+ identifiers: { library: targetPath, },
3071
+ };
3072
+ }
3073
+ case "project-git.reset-library": {
3074
+ const targetPath = id;
3075
+ return {
3076
+ method: "POST",
3077
+ endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/reset"),
3078
+ identifiers: { library: targetPath, },
3079
+ payload: { gitRef: targetPath, },
3080
+ wait: projectGitFutureWait(),
3081
+ };
3082
+ }
3083
+ case "project-git.push-library": {
3084
+ const targetPath = id;
3085
+ const message = requiredPlanFlag(flags, "message", entry.usage);
3086
+ return {
3087
+ method: "POST",
3088
+ endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/push"),
3089
+ identifiers: { library: targetPath, message, },
3090
+ payload: { gitRef: targetPath, commitMessage: message, },
3091
+ wait: projectGitFutureWait(),
3092
+ };
3093
+ }
3094
+ case "project-git.push-all-libraries": {
3095
+ const message = requiredPlanFlag(flags, "message", entry.usage);
3096
+ return {
3097
+ method: "POST",
3098
+ endpoint: projectGitEndpoint(projectKey, "/actions/git-refs/push-all"),
3099
+ identifiers: { message, },
3100
+ payload: { commitMessage: message, },
3101
+ wait: projectGitFutureWait(),
3102
+ };
3103
+ }
3104
+ case "project-git.reset-all-libraries":
3105
+ return {
3106
+ method: "POST",
3107
+ endpoint: projectGitEndpoint(projectKey, "/actions/git-refs/reset-all"),
3108
+ wait: projectGitFutureWait(),
3109
+ };
3110
+ case "project-git.future-abort":
3111
+ return {
3112
+ method: "DELETE",
3113
+ endpoint: projectGitFutureEndpoint(id),
3114
+ identifiers: { jobId: id, },
3115
+ };
2758
3116
  default:
2759
3117
  return {
2760
3118
  method: action.startsWith("delete") || action === "abort" ? "DELETE" : "POST",