dataiku-sdk 3.0.0 → 3.1.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 (79) hide show
  1. package/README.md +132 -17
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/packages/types/src/index.d.ts +39 -4
  4. package/dist/packages/types/src/index.js +31 -2
  5. package/dist/src/cli/commands/api-service.js +62 -16
  6. package/dist/src/cli/commands/bundle.js +49 -15
  7. package/dist/src/cli/commands/code-env.js +210 -49
  8. package/dist/src/cli/commands/code.d.ts +19 -0
  9. package/dist/src/cli/commands/code.js +87 -15
  10. package/dist/src/cli/commands/dataset.js +8 -3
  11. package/dist/src/cli/commands/flow-zone.js +33 -5
  12. package/dist/src/cli/commands/notebook.js +72 -41
  13. package/dist/src/cli/commands/project-git.js +8 -5
  14. package/dist/src/cli/commands/project-library.d.ts +21 -0
  15. package/dist/src/cli/commands/project-library.js +121 -50
  16. package/dist/src/cli/commands/project.js +16 -2
  17. package/dist/src/cli/commands/recipe.js +37 -12
  18. package/dist/src/cli/commands/sql.d.ts +10 -0
  19. package/dist/src/cli/commands/sql.js +39 -17
  20. package/dist/src/cli/commands/webapp.js +67 -13
  21. package/dist/src/cli/contract.d.ts +15 -0
  22. package/dist/src/cli/contract.js +870 -107
  23. package/dist/src/cli/env.d.ts +9 -0
  24. package/dist/src/cli/env.js +20 -2
  25. package/dist/src/cli/flags.js +9 -0
  26. package/dist/src/cli/helpers/cleanup.js +104 -3
  27. package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
  28. package/dist/src/cli/helpers/flow-zone.js +33 -3
  29. package/dist/src/cli/helpers/recipe.d.ts +20 -0
  30. package/dist/src/cli/helpers/recipe.js +158 -3
  31. package/dist/src/cli/output.d.ts +2 -0
  32. package/dist/src/cli/output.js +33 -6
  33. package/dist/src/cli/runtime.d.ts +35 -1
  34. package/dist/src/cli/runtime.js +125 -18
  35. package/dist/src/cli/types.d.ts +7 -0
  36. package/dist/src/cli.js +135 -28
  37. package/dist/src/client.d.ts +60 -0
  38. package/dist/src/client.js +237 -42
  39. package/dist/src/errors.d.ts +8 -2
  40. package/dist/src/errors.js +40 -0
  41. package/dist/src/index.d.ts +4 -3
  42. package/dist/src/index.js +2 -1
  43. package/dist/src/resources/api-services.d.ts +48 -6
  44. package/dist/src/resources/api-services.js +54 -8
  45. package/dist/src/resources/bundles.d.ts +51 -6
  46. package/dist/src/resources/bundles.js +39 -9
  47. package/dist/src/resources/code-envs.d.ts +25 -3
  48. package/dist/src/resources/code-envs.js +104 -2
  49. package/dist/src/resources/datasets.d.ts +20 -0
  50. package/dist/src/resources/datasets.js +232 -52
  51. package/dist/src/resources/jobs.d.ts +35 -10
  52. package/dist/src/resources/jobs.js +92 -17
  53. package/dist/src/resources/notebooks.d.ts +77 -7
  54. package/dist/src/resources/notebooks.js +143 -19
  55. package/dist/src/resources/project-git.d.ts +12 -0
  56. package/dist/src/resources/project-git.js +39 -8
  57. package/dist/src/resources/project-library.d.ts +122 -7
  58. package/dist/src/resources/project-library.js +494 -25
  59. package/dist/src/resources/projects.d.ts +5 -2
  60. package/dist/src/resources/projects.js +18 -2
  61. package/dist/src/resources/scenarios.d.ts +30 -0
  62. package/dist/src/resources/scenarios.js +73 -27
  63. package/dist/src/resources/sql.d.ts +6 -0
  64. package/dist/src/resources/sql.js +22 -2
  65. package/dist/src/resources/webapps.d.ts +35 -4
  66. package/dist/src/resources/webapps.js +54 -5
  67. package/dist/src/schemas.d.ts +2 -2
  68. package/dist/src/schemas.js +1 -1
  69. package/dist/src/skill.d.ts +9 -0
  70. package/dist/src/skill.js +37 -4
  71. package/dist/src/utils/flow-analysis.d.ts +53 -0
  72. package/dist/src/utils/flow-analysis.js +382 -0
  73. package/dist/src/utils/project-archive.d.ts +41 -2
  74. package/dist/src/utils/project-archive.js +159 -16
  75. package/package.json +4 -4
  76. package/packages/types/dist/index.d.ts +39 -4
  77. package/packages/types/dist/index.js +31 -2
  78. package/plugin.json +1 -1
  79. package/skills/dataiku-dss/SKILL.md +74 -47
@@ -4,19 +4,31 @@ 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, recipeInputDatasetsFromFlags, requiredJsonInput, rewritePairsFromFlags, schemaColumnsInput, stringField, textInput, } from "./coerce.js";
7
+ import { encodeGitReferencePath, validateGitReferencePath, } from "../resources/project-git.js";
8
+ import { encodeLibraryPath, PROJECT_LIBRARY_CONCURRENCY_CONTROL, validateLibraryDestinationPath, validateLibraryName, validateLibraryPath, } from "../resources/project-library.js";
9
+ import { CodeEnvDetailsSchema, CodeEnvLogSummaryArraySchema, CodeEnvSummaryArraySchema, CodeEnvUsageArraySchema, CodeEnvVersionForProjectSchema, DatasetDetailsSchema, DatasetSchemaSchema, DatasetSummaryArraySchema, FlowZoneArraySchema, FlowZoneSchema, JobSummaryArraySchema, JobWaitResultSchema, JupyterNotebookContentSchema, JupyterNotebookSummaryArraySchema, NotebookSessionArraySchema, ProjectDetailsSchema, ProjectMetadataSchema, ProjectSummaryArraySchema, RecipeSummaryArraySchema, ScenarioDetailsSchema, ScenarioStatusSchema, ScenarioSummaryArraySchema, SqlNotebookContentSchema, SqlNotebookHistorySchema, SqlNotebookSummaryArraySchema, SqlQueryResponseSchema, } from "../schemas.js";
10
+ import { jobBuildTargetTypeFromFlags, json, jsonInput, num, parseBooleanOption, parseJsonObject, recipeInputDatasetsFromFlags, requiredJsonInput, rewritePairsFromFlags, schemaColumnsInput, sha256Hex, stableHash, stringField, textInput, } from "./coerce.js";
11
+ import { parseCodeRunIntegerFlag, resolveCodeInputWithSource, } from "./commands/code.js";
8
12
  import { commands, } from "./commands/index.js";
13
+ import { projectLibraryPutPayload, } from "./commands/project-library.js";
14
+ import { resolveSqlQueryInvocation, } from "./commands/sql.js";
9
15
  import { dataikuEnvironmentEnabled, } from "./env.js";
10
16
  import { BOOLEAN_FLAGS, FLAG_ALIASES, KNOWN_LONG_FLAGS, SHORT_FLAGS, } from "./flags.js";
11
17
  import { flowZoneColor, flowZoneMoveItems, flowZoneName, } from "./helpers/flow-zone.js";
12
18
  import { recipeBackupPath, recipeRunShouldWait, } from "./helpers/recipe.js";
13
19
  import { encodedProjectEndpointForPlan, planResult, } from "./output.js";
14
- import { resolveTlsSettings, } from "./runtime.js";
20
+ import { resolveLoginCredentials, } from "./runtime.js";
15
21
  import { requireArgs, UsageError, } from "./usage.js";
16
22
  import { AGENT_CONTRACT_SCHEMA_ID, AGENT_CONTRACT_VERSION, cliVersionResult, JSON_SCHEMA_DRAFT, } from "./version.js";
17
23
  function codeEnvWait(flags) {
18
24
  return flags["no-wait"] !== true;
19
25
  }
26
+ function codeEnvLang(value, usage) {
27
+ if (value !== "PYTHON" && value !== "R") {
28
+ throw new UsageError(`Invalid code environment language ${JSON.stringify(value)}. Usage: ${usage}`);
29
+ }
30
+ return value;
31
+ }
20
32
  function codeEnvParams(flags) {
21
33
  const params = json(flags["params"]) ?? jsonInput(flags) ?? {};
22
34
  if (typeof flags["python-interpreter"] === "string") {
@@ -46,23 +58,7 @@ function codeEnvPackageList(flags) {
46
58
  export const AUTH_ACTIONS = {
47
59
  login: {
48
60
  handler: async (flags) => {
49
- const tlsSettings = resolveTlsSettings(flags);
50
- const useEnv = dataikuEnvironmentEnabled();
51
- const url = typeof flags["url"] === "string"
52
- ? flags["url"]
53
- : useEnv
54
- ? process.env.DATAIKU_URL ?? ""
55
- : "";
56
- const apiKey = typeof flags["api-key"] === "string"
57
- ? flags["api-key"]
58
- : useEnv
59
- ? process.env.DATAIKU_API_KEY ?? ""
60
- : "";
61
- const projectKey = typeof flags["project-key"] === "string"
62
- ? flags["project-key"]
63
- : useEnv
64
- ? process.env.DATAIKU_PROJECT_KEY
65
- : undefined;
61
+ const { url, apiKey, projectKey, tlsSettings, } = resolveLoginCredentials(flags);
66
62
  if (!url || !apiKey) {
67
63
  throw new UsageError("Missing --url and/or --api-key for auth login.", "missing_required_flag", "Pass --url and --api-key, or set DATAIKU_URL and DATAIKU_API_KEY.", { requiredFlags: ["url", "api-key",], env: ["DATAIKU_URL", "DATAIKU_API_KEY",], });
68
64
  }
@@ -237,13 +233,13 @@ const VERSION_USAGE = "dss version";
237
233
  const VERSION_DESCRIPTION = "Print the CLI version, checkout/build revisions, load source, runtime, and stale-build status as JSON.";
238
234
  const VERSION_EXAMPLES = ["dss version", "dss --version",];
239
235
  const INSTALL_SKILL_USAGE = "dss install-skill [--global] [--agent NAME] [--target PATH] [--list-agents] [--dry-run] [--plan]";
240
- const INSTALL_SKILL_DESCRIPTION = "Install the dataiku-dss agent skill for detected coding agents.";
236
+ const INSTALL_SKILL_DESCRIPTION = "Report missing/stale/current skill status and atomically install changed dataiku-dss agent skills.";
241
237
  const INSTALL_SKILL_EXAMPLES = [
242
238
  "dss install-skill --list-agents",
243
239
  "dss install-skill --agent omp --dry-run",
244
240
  ];
245
241
  export const CLEANUP_USAGE = "dss cleanup --file PATH [--dry-run|--apply] [--continue-on-error]";
246
- const CLEANUP_DESCRIPTION = "Replay cleanup ledger entries in reverse order.";
242
+ const CLEANUP_DESCRIPTION = "Replay cleanup entries in reverse order; failures include structured code/category/exitCode/retryability summaries.";
247
243
  const CLEANUP_EXAMPLES = [
248
244
  "dss cleanup --file cleanup.jsonl",
249
245
  "dss cleanup --file cleanup.jsonl --apply",
@@ -272,6 +268,12 @@ const ALLOWED_CLEANUP_ACTIONS = new Set([
272
268
  "folder delete-file",
273
269
  "project delete",
274
270
  "app delete-instance",
271
+ "meaning delete",
272
+ "notebook delete-jupyter",
273
+ "notebook delete-sql",
274
+ "project-library delete",
275
+ "streaming-endpoint delete",
276
+ "workspace delete",
275
277
  ]);
276
278
  export function isAllowedCleanupAction(resource, action) {
277
279
  return ALLOWED_CLEANUP_ACTIONS.has(`${resource} ${action}`);
@@ -407,7 +409,202 @@ function structuredExamples(examples, examplePayload) {
407
409
  };
408
410
  });
409
411
  }
410
- function outputJsonSchema(shape) {
412
+ const PROJECT_LIBRARY_ITEM_OUTPUT_SCHEMA = {
413
+ type: "object",
414
+ additionalProperties: false,
415
+ required: ["name",],
416
+ properties: {
417
+ name: { type: "string", },
418
+ path: { type: "string", },
419
+ size: { type: "integer", minimum: 0, },
420
+ mimeType: { type: "string", },
421
+ hasData: { type: "boolean", },
422
+ lastModified: { type: "number", },
423
+ children: { type: "array", items: { type: "object", additionalProperties: true, }, },
424
+ },
425
+ };
426
+ const NOTEBOOK_SAVE_OUTPUT_SCHEMA = {
427
+ type: "object",
428
+ additionalProperties: false,
429
+ required: ["saved", "resource", "created", "hash",],
430
+ properties: {
431
+ saved: { type: "string", },
432
+ resource: { enum: ["jupyter-notebook", "sql-notebook",], },
433
+ created: { type: "boolean", },
434
+ hash: { type: "string", pattern: "^[a-f0-9]{64}$", },
435
+ },
436
+ };
437
+ const COMMAND_OUTPUT_SCHEMAS = {
438
+ "code-env.list": CodeEnvSummaryArraySchema,
439
+ "code-env.get": CodeEnvDetailsSchema,
440
+ "code-env.list-logs": CodeEnvLogSummaryArraySchema,
441
+ "code-env.get-log": {
442
+ oneOf: [
443
+ {
444
+ type: "object",
445
+ required: ["log", "bytes", "truncated", "tailed", "envLang", "envName", "logName",],
446
+ properties: {
447
+ log: { type: "string", },
448
+ bytes: { type: "integer", minimum: 0, },
449
+ truncated: { type: "boolean", },
450
+ tailed: { type: "boolean", },
451
+ envLang: { enum: ["PYTHON", "R",], },
452
+ envName: { type: "string", },
453
+ logName: { type: "string", },
454
+ },
455
+ },
456
+ {
457
+ type: "object",
458
+ required: ["path", "bytes", "truncated", "tailed", "envLang", "envName", "logName",],
459
+ properties: {
460
+ path: { type: "string", },
461
+ bytes: { type: "integer", minimum: 0, },
462
+ truncated: { type: "boolean", },
463
+ tailed: { type: "boolean", },
464
+ envLang: { enum: ["PYTHON", "R",], },
465
+ envName: { type: "string", },
466
+ logName: { type: "string", },
467
+ },
468
+ },
469
+ ],
470
+ },
471
+ "code-env.version": CodeEnvVersionForProjectSchema,
472
+ "code-env.usages": CodeEnvUsageArraySchema,
473
+ "code.run": {
474
+ type: "object",
475
+ additionalProperties: false,
476
+ required: [
477
+ "outcome",
478
+ "success",
479
+ "runId",
480
+ "elapsedMs",
481
+ "pollCount",
482
+ "output",
483
+ "logTruncated",
484
+ "maxLogBytes",
485
+ "cleanup",
486
+ ],
487
+ properties: {
488
+ outcome: { type: "string", },
489
+ success: { type: "boolean", },
490
+ runId: { type: "string", },
491
+ elapsedMs: { type: "number", minimum: 0, },
492
+ pollCount: { type: "integer", minimum: 0, },
493
+ output: { type: "string", },
494
+ log: { type: "string", },
495
+ logTruncated: { type: "boolean", },
496
+ maxLogBytes: { type: "integer", minimum: 0, },
497
+ timedOut: { const: true, },
498
+ timeoutMs: { type: "integer", minimum: 0, },
499
+ cleanup: {
500
+ type: "object",
501
+ additionalProperties: false,
502
+ required: ["status",],
503
+ properties: {
504
+ status: { enum: ["deleted", "kept", "failed",], },
505
+ error: { type: "string", },
506
+ },
507
+ },
508
+ },
509
+ },
510
+ "notebook.list-jupyter": JupyterNotebookSummaryArraySchema,
511
+ "notebook.get-jupyter": JupyterNotebookContentSchema,
512
+ "notebook.sessions-jupyter": NotebookSessionArraySchema,
513
+ "notebook.list-sql": SqlNotebookSummaryArraySchema,
514
+ "notebook.get-sql": SqlNotebookContentSchema,
515
+ "notebook.history-sql": SqlNotebookHistorySchema,
516
+ "notebook.save-jupyter": NOTEBOOK_SAVE_OUTPUT_SCHEMA,
517
+ "notebook.save-sql": NOTEBOOK_SAVE_OUTPUT_SCHEMA,
518
+ "project-library.list": { type: "array", items: PROJECT_LIBRARY_ITEM_OUTPUT_SCHEMA, },
519
+ "project-library.get-bytes": {
520
+ type: "object",
521
+ additionalProperties: false,
522
+ required: ["path", "bytes", "sha256",],
523
+ properties: {
524
+ path: { type: "string", },
525
+ bytes: { type: "integer", minimum: 0, },
526
+ sha256: { type: "string", pattern: "^[a-f0-9]{64}$", },
527
+ },
528
+ },
529
+ "project-library.put": {
530
+ type: "object",
531
+ additionalProperties: false,
532
+ required: ["updated", "bytes", "sha256",],
533
+ properties: {
534
+ updated: { type: "string", },
535
+ bytes: { type: "integer", minimum: 0, },
536
+ sha256: { type: "string", pattern: "^[a-f0-9]{64}$", },
537
+ beforeSha256: { type: "string", pattern: "^[a-f0-9]{64}$", },
538
+ },
539
+ },
540
+ "project-library.diff": {
541
+ type: "object",
542
+ additionalProperties: false,
543
+ required: [
544
+ "path",
545
+ "unchanged",
546
+ "added",
547
+ "removed",
548
+ "diff",
549
+ "diffTruncated",
550
+ "localSha256",
551
+ "localBytes",
552
+ "maxLines",
553
+ ],
554
+ properties: {
555
+ path: { type: "string", },
556
+ unchanged: { type: "boolean", },
557
+ added: { type: "integer", minimum: 0, },
558
+ removed: { type: "integer", minimum: 0, },
559
+ diff: { type: "string", },
560
+ diffTruncated: { type: "boolean", },
561
+ binary: { type: "boolean", },
562
+ remoteAbsent: { type: "boolean", },
563
+ remoteSha256: { type: "string", pattern: "^[a-f0-9]{64}$", },
564
+ remoteBytes: { type: "integer", minimum: 0, },
565
+ localSha256: { type: "string", pattern: "^[a-f0-9]{64}$", },
566
+ localBytes: { type: "integer", minimum: 0, },
567
+ maxLines: { type: "integer", minimum: 0, },
568
+ },
569
+ },
570
+ "project.list": ProjectSummaryArraySchema,
571
+ "project.get": ProjectDetailsSchema,
572
+ "project.metadata": ProjectMetadataSchema,
573
+ "dataset.list": DatasetSummaryArraySchema,
574
+ "dataset.get": DatasetDetailsSchema,
575
+ "dataset.schema": DatasetSchemaSchema,
576
+ "recipe.list": RecipeSummaryArraySchema,
577
+ "job.list": JobSummaryArraySchema,
578
+ "job.wait": JobWaitResultSchema,
579
+ "job.monitor": JobWaitResultSchema,
580
+ "scenario.list": ScenarioSummaryArraySchema,
581
+ "scenario.get": ScenarioDetailsSchema,
582
+ "scenario.status": ScenarioStatusSchema,
583
+ "flow-zone.list": FlowZoneArraySchema,
584
+ "flow-zone.get": FlowZoneSchema,
585
+ "sql.query": {
586
+ anyOf: [
587
+ SqlQueryResponseSchema,
588
+ {
589
+ type: "object",
590
+ required: ["queryId", "rowCount", "preview",],
591
+ additionalProperties: true,
592
+ properties: {
593
+ queryId: { type: "string", },
594
+ rowCount: { type: "number", },
595
+ preview: { type: "array", items: true, },
596
+ truncated: { type: "boolean", },
597
+ outputPath: { type: "string", },
598
+ written: { type: "string", },
599
+ },
600
+ },
601
+ ],
602
+ },
603
+ };
604
+ function outputJsonSchema(resource, action, shape) {
605
+ const precise = COMMAND_OUTPUT_SCHEMAS[registryKey(resource, action)];
606
+ if (precise)
607
+ return precise;
411
608
  if (shape === "array")
412
609
  return { type: "array", items: true, };
413
610
  if (shape === "string")
@@ -417,6 +614,9 @@ function outputJsonSchema(shape) {
417
614
  function payloadJsonSchema(payloadSchema) {
418
615
  if (!payloadSchema)
419
616
  return undefined;
617
+ if (payloadSchema.contentType === "text/plain") {
618
+ return { type: "string", contentMediaType: "text/plain", };
619
+ }
420
620
  return payloadSchema.jsonShape === "array"
421
621
  ? { type: "array", items: true, }
422
622
  : { type: "object", additionalProperties: true, };
@@ -564,10 +764,13 @@ export function buildCommandSchemas(resource, action, flags, requiredFlags, requ
564
764
  return {
565
765
  argv: argvJsonSchema(resource, action, flags, usage, requiredFlags, requiredOneOf),
566
766
  ...(payloadSchema ? { input: payloadJsonSchema(payloadSchema), } : {}),
567
- output: outputJsonSchema(outputShape),
767
+ output: outputJsonSchema(resource, action, outputShape),
568
768
  };
569
769
  }
570
770
  const EXPLICIT_REGISTRY_OVERRIDES = {
771
+ "code.run": {
772
+ payloadSchema: { stdin: true, contentType: "text/plain", },
773
+ },
571
774
  "dashboard.create": {
572
775
  examplePayload: { name: "Agent dashboard", pages: [], },
573
776
  },
@@ -620,8 +823,78 @@ function extractUsageFlags(usage) {
620
823
  }
621
824
  return uniqueStrings(flags).filter((flag) => KNOWN_LONG_FLAGS.has(flag));
622
825
  }
623
- function extractPositionals(usage) {
624
- return uniqueStrings([...usage.matchAll(/<([^>]+)>/g),].map((match) => match[1]));
826
+ function extractPositionalArguments(usage) {
827
+ const positionals = [];
828
+ const add = (name, required) => {
829
+ const existing = positionals.find((positional) => positional.name === name);
830
+ if (existing)
831
+ existing.required ||= required;
832
+ else
833
+ positionals.push({ name, required, });
834
+ };
835
+ for (const match of usage.matchAll(/<([^>]+)>/g)) {
836
+ const before = usage.slice(0, match.index);
837
+ const openParen = before.lastIndexOf("(");
838
+ const insideParen = openParen > before.lastIndexOf(")");
839
+ const closeParen = insideParen ? usage.indexOf(")", match.index) : -1;
840
+ const group = closeParen >= 0 ? usage.slice(openParen + 1, closeParen) : "";
841
+ const hasFlagAlternative = group.split("|").some((alternative) => alternative.includes("--"));
842
+ const required = before.lastIndexOf("[") <= before.lastIndexOf("]")
843
+ && !hasFlagAlternative;
844
+ add(match[1], required);
845
+ }
846
+ for (const suffix of usage.split("[").slice(1)) {
847
+ const candidate = suffix.split("]", 1)[0];
848
+ if (candidate && /^[A-Za-z][A-Za-z0-9_-]*$/.test(candidate))
849
+ add(candidate, false);
850
+ }
851
+ for (const group of topLevelParenGroups(usage)) {
852
+ const candidate = group.split("|", 1)[0]?.trim();
853
+ if (candidate && /^[A-Z][A-Z0-9_-]*$/.test(candidate))
854
+ add(candidate, false);
855
+ }
856
+ return positionals;
857
+ }
858
+ const COMMAND_REQUIRED_INPUT_GROUPS = {
859
+ "notebook.unload-jupyter": [
860
+ {
861
+ oneOf: [
862
+ { positionals: ["name", "sessionId",], },
863
+ { flags: ["all",], },
864
+ ],
865
+ },
866
+ ],
867
+ "sql.query": [
868
+ {
869
+ oneOf: [
870
+ { positionals: ["SQL",], },
871
+ { flags: ["sql",], },
872
+ { flags: ["sql-file",], },
873
+ { flags: ["stdin",], },
874
+ ],
875
+ },
876
+ { oneOf: [{ flags: ["connection",], }, { flags: ["dataset",], },], },
877
+ ],
878
+ "flow-zone.move": [
879
+ {
880
+ oneOf: [
881
+ { positionals: ["id",], },
882
+ { flags: ["zone",], },
883
+ { flags: ["zone-id",], },
884
+ ],
885
+ },
886
+ {
887
+ oneOf: [
888
+ { flags: ["dataset",], },
889
+ { flags: ["recipe",], },
890
+ { flags: ["folder",], },
891
+ { flags: ["object",], },
892
+ ],
893
+ },
894
+ ],
895
+ };
896
+ function requiredInputGroups(resource, action) {
897
+ return COMMAND_REQUIRED_INPUT_GROUPS[registryKey(resource, action)] ?? [];
625
898
  }
626
899
  function inferSideEffect(resource, action) {
627
900
  if (resource === "auth")
@@ -633,6 +906,10 @@ function inferSideEffect(resource, action) {
633
906
  }
634
907
  if (resource === "install-skill")
635
908
  return "write";
909
+ if (resource === "sql" && action === "query")
910
+ return "write";
911
+ if (resource === "ml-task" && action === "train")
912
+ return "write";
636
913
  // Project and dashboard exports stream a rendered/archive artifact and write it
637
914
  // locally; the DSS-side resource is untouched.
638
915
  if ((resource === "project" || resource === "dashboard") && action === "export")
@@ -668,6 +945,8 @@ export function inferRequiresProject(resource, action, usage) {
668
945
  || resource === "install-skill" || resource === "version") {
669
946
  return false;
670
947
  }
948
+ if (resource === "sql" && action === "query")
949
+ return false;
671
950
  if (PROJECT_SCOPED_RESOURCES.has(resource))
672
951
  return true;
673
952
  if (resource === "project-git")
@@ -828,6 +1107,11 @@ const GLOBAL_FLAG_VALUE_HINTS = {
828
1107
  "ca-cert": { valueType: "PATH", },
829
1108
  "project-key": { valueType: "KEY", },
830
1109
  "record-cleanup": { valueType: "PATH", },
1110
+ color: { valueType: "HEX", },
1111
+ data: { valueType: "JSON", },
1112
+ local: { valueType: "JSON", },
1113
+ standard: { valueType: "JSON", },
1114
+ until: { valueType: "STATE", },
831
1115
  };
832
1116
  /** Derive a value placeholder (and enum members) for each value flag from its usage token. */
833
1117
  function extractFlagValueHints(usage) {
@@ -866,6 +1150,7 @@ function inferExitCodes(resource, action, asyncKind) {
866
1150
  || `${resource}.${action}` === "dataset.assert-count"
867
1151
  || `${resource}.${action}` === "dataset.assert-schema"
868
1152
  || `${resource}.${action}` === "data-quality.assert-results"
1153
+ || `${resource}.${action}` === "flow-zone.organize"
869
1154
  || `${resource}.${action}` === "batch.run";
870
1155
  return {
871
1156
  ok: 0,
@@ -877,6 +1162,12 @@ function inferExitCodes(resource, action, asyncKind) {
877
1162
  };
878
1163
  }
879
1164
  function cleanupCommandFromDeleteUsage(resource, action) {
1165
+ if (`${resource}.${action}` === "notebook.save-jupyter") {
1166
+ return "dss notebook delete-jupyter <name> --if-exists";
1167
+ }
1168
+ if (`${resource}.${action}` === "notebook.save-sql") {
1169
+ return "dss notebook delete-sql <id> --if-exists";
1170
+ }
880
1171
  if (`${resource}.${action}` === "project.import") {
881
1172
  return "dss project delete <used-project-key> --if-exists --expect-project-incarnation <hash>";
882
1173
  }
@@ -911,6 +1202,7 @@ export function supportsCleanupLedger(resource, action) {
911
1202
  */
912
1203
  const EXPLICIT_DESTRUCTIVE_KEYS = {
913
1204
  "dataset.upload-file": true,
1205
+ "sql.query": true,
914
1206
  };
915
1207
  function inferDestructiveLevel(resource, sideEffect, action) {
916
1208
  if (sideEffect !== "write")
@@ -930,6 +1222,8 @@ function inferAsyncKind(resource, action) {
930
1222
  }
931
1223
  if (resource === "recipe" && action === "run")
932
1224
  return "job";
1225
+ if (resource === "ml-task" && action === "train")
1226
+ return "job";
933
1227
  if (resource === "future" && ["get", "peek", "wait", "abort",].includes(action))
934
1228
  return "future";
935
1229
  if (resource === "scenario" && ["run", "run-and-wait", "status",].includes(action)) {
@@ -943,6 +1237,11 @@ function inferAsyncKind(resource, action) {
943
1237
  return "future";
944
1238
  if (resource === "code" && action === "run")
945
1239
  return "future";
1240
+ if (resource === "webapp" && action === "restart-backend")
1241
+ return "future";
1242
+ if (resource === "code-env"
1243
+ && ["create", "update-packages", "update-images", "set-jupyter", "delete",].includes(action))
1244
+ return "future";
946
1245
  if (resource === "app"
947
1246
  && ["create-instance", "create-successor-instance", "delete-instance",].includes(action)) {
948
1247
  return "future";
@@ -952,6 +1251,8 @@ function inferAsyncKind(resource, action) {
952
1251
  function inferIdempotency(resource, sideEffect, action, usage) {
953
1252
  if (sideEffect === "read")
954
1253
  return "safe";
1254
+ if (`${resource}.${action}` === "install-skill.run")
1255
+ return "convergent";
955
1256
  if (action.startsWith("create") && usage.includes("--if-not-exists"))
956
1257
  return "if-not-exists";
957
1258
  if (action.startsWith("delete") && usage.includes("--if-exists"))
@@ -990,18 +1291,26 @@ export function inferCleanupHint(resource, action) {
990
1291
  // tag create has a generic `delete` command to close it out.
991
1292
  if (resource === "project-git")
992
1293
  return undefined;
1294
+ const key = `${resource}.${action}`;
1295
+ if (key === "notebook.save-jupyter") {
1296
+ return "When the save reports created:true, delete with `dss notebook delete-jupyter <name> --if-exists`.";
1297
+ }
1298
+ if (key === "notebook.save-sql") {
1299
+ return "When the save reports created:true, delete with `dss notebook delete-sql <id> --if-exists`.";
1300
+ }
993
1301
  if (!(action.startsWith("create") || action === "clone"))
994
1302
  return undefined;
995
- if (LEDGER_ONLY_CLEANUP_KEYS[`${resource}.${action}`] === true) {
1303
+ if (LEDGER_ONLY_CLEANUP_KEYS[key] === true)
996
1304
  return LEDGER_ONLY_CLEANUP_HINT;
997
- }
998
1305
  const deleteAction = action === "create-rule"
999
1306
  ? "delete-rule"
1000
1307
  : action === "create-instance" || action === "create-successor-instance"
1001
1308
  ? "delete-instance"
1002
1309
  : "delete";
1003
1310
  const deleteUsage = commands[resource]?.[deleteAction]?.usage;
1004
- const ifExists = deleteUsage?.includes("--if-exists") ? " --if-exists" : "";
1311
+ if (!deleteUsage)
1312
+ return undefined;
1313
+ const ifExists = deleteUsage.includes("--if-exists") ? " --if-exists" : "";
1005
1314
  if (resource === "code-env") {
1006
1315
  return `Delete with \`dss code-env delete <lang> <name>${ifExists}\`.`;
1007
1316
  }
@@ -1074,7 +1383,9 @@ function buildRegistryEntry(resource, action, meta) {
1074
1383
  ...allowEmptyPart,
1075
1384
  };
1076
1385
  });
1077
- const positionals = extractPositionals(meta.usage);
1386
+ const positionalArguments = extractPositionalArguments(meta.usage);
1387
+ const positionals = positionalArguments.map((positional) => positional.name);
1388
+ const inputGroups = requiredInputGroups(resource, action);
1078
1389
  const outputShape = inferOutputShape(resource, action);
1079
1390
  const producesLocalFile = meta.usage.includes("--output PATH")
1080
1391
  || meta.usage.includes("--output-file PATH");
@@ -1090,6 +1401,7 @@ function buildRegistryEntry(resource, action, meta) {
1090
1401
  structuredExamples: structuredExamples(meta.examples, examplePayload),
1091
1402
  flags: flagMetadata,
1092
1403
  positionals,
1404
+ positionalArguments,
1093
1405
  sideEffect,
1094
1406
  requiresAuth,
1095
1407
  requiresProject,
@@ -1104,6 +1416,7 @@ function buildRegistryEntry(resource, action, meta) {
1104
1416
  requiredFlags: uniqueRequiredFlags,
1105
1417
  optionalFlags: uniqueOptionalFlags,
1106
1418
  ...(requiredOneOf.length > 0 ? { requiredOneOf, } : {}),
1419
+ ...(inputGroups.length > 0 ? { requiredInputGroups: inputGroups, } : {}),
1107
1420
  ...(payloadSchema ? { payloadSchema, } : {}),
1108
1421
  schemas: buildCommandSchemas(resource, action, flagMetadata, uniqueRequiredFlags, requiredOneOf, payloadSchema, outputShape, meta.usage),
1109
1422
  ...(unsafe ? { unsafeOutputs: unsafe, } : {}),
@@ -1216,6 +1529,7 @@ function commandRegistryEntryJsonSchema() {
1216
1529
  "usage",
1217
1530
  "flags",
1218
1531
  "positionals",
1532
+ "positionalArguments",
1219
1533
  "sideEffect",
1220
1534
  "requiresAuth",
1221
1535
  "requiresProject",
@@ -1235,6 +1549,15 @@ function commandRegistryEntryJsonSchema() {
1235
1549
  structuredExamples: { type: "array", items: { type: "object", additionalProperties: true, }, },
1236
1550
  flags: { type: "array", items: commandFlagJsonSchema(), },
1237
1551
  positionals: { type: "array", items: { type: "string", }, },
1552
+ positionalArguments: {
1553
+ type: "array",
1554
+ items: {
1555
+ type: "object",
1556
+ required: ["name", "required",],
1557
+ additionalProperties: false,
1558
+ properties: { name: { type: "string", }, required: { type: "boolean", }, },
1559
+ },
1560
+ },
1238
1561
  sideEffect: { enum: ["read", "write", "auth",], },
1239
1562
  requiresAuth: { type: "boolean", },
1240
1563
  requiresProject: { type: "boolean", },
@@ -1247,6 +1570,7 @@ function commandRegistryEntryJsonSchema() {
1247
1570
  idempotency: { enum: ["safe", "convergent", "if-not-exists", "if-exists", "none",], },
1248
1571
  dryRun: { type: "boolean", },
1249
1572
  requiredFlags: { type: "array", items: { type: "string", }, },
1573
+ requiredInputGroups: { type: "array", items: { type: "object", additionalProperties: true, }, },
1250
1574
  optionalFlags: { type: "array", items: { type: "string", }, },
1251
1575
  schemas: { type: "object", additionalProperties: true, },
1252
1576
  unsafeOutputs: { type: "array", items: { type: "object", additionalProperties: true, }, },
@@ -1275,8 +1599,13 @@ function errorEnvelopeJsonSchema() {
1275
1599
  ok: { const: false, },
1276
1600
  error: { type: "string", },
1277
1601
  code: { type: "string", },
1278
- category: { enum: ["usage", "dss", "internal",], },
1602
+ category: { enum: ["usage", "permission_or_environment", "dss", "internal",], },
1279
1603
  exitCode: { type: "number", },
1604
+ hint: { type: "string", },
1605
+ status: { type: "number", },
1606
+ retryable: { type: "boolean", },
1607
+ requestId: { type: "string", },
1608
+ details: { type: "object", additionalProperties: true, },
1280
1609
  resource: { type: "string", },
1281
1610
  action: { type: "string", },
1282
1611
  projectKey: { type: "string", },
@@ -1367,6 +1696,8 @@ export function buildAgentContract() {
1367
1696
  format: "compact-json",
1368
1697
  success: "single-json-value",
1369
1698
  failure: "structured-error-object",
1699
+ failureResultDetailLimitBytes: 65_536,
1700
+ fieldProjection: "Missing --fields paths stay null on stdout and emit field_projection_missing on stderr.",
1370
1701
  richFailureResults: "doctor/batch/cleanup nonzero outcomes are their own compact result on stdout ({ok:false,...}) with the command's exit code; not re-wrapped.",
1371
1702
  },
1372
1703
  stderr: {
@@ -1454,7 +1785,9 @@ function requiredPlanJsonInput(flags, usage) {
1454
1785
  return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
1455
1786
  }
1456
1787
  function requiredPlanPositionals(usage) {
1457
- return [...stripOptionalUsageGroups(usage).matchAll(/<([^>]+)>/g),].map((match) => match[1]);
1788
+ return extractPositionalArguments(usage)
1789
+ .filter((positional) => positional.required)
1790
+ .map((positional) => positional.name);
1458
1791
  }
1459
1792
  function dataQualityEndpoint(projectKey, datasetName, suffix) {
1460
1793
  return encodedProjectEndpointForPlan(projectKey, `/datasets/${encodeURIComponent(datasetName)}/data-quality${suffix}`);
@@ -1482,11 +1815,6 @@ function projectGitEndpoint(projectKey, suffix) {
1482
1815
  function projectGitFutureEndpoint(jobId) {
1483
1816
  return `${PROJECT_GIT_API_ROOT}/futures/${encodeURIComponent(jobId)}`;
1484
1817
  }
1485
- /** Encode a library reference path per segment, mirroring the resource helper. */
1486
- function encodeGitReferencePath(pathValue) {
1487
- const normalized = pathValue.replace(/^\/+/, "").replace(/\/+$/, "");
1488
- return normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/");
1489
- }
1490
1818
  /**
1491
1819
  * `--plan` bypasses the SDK boundary guard, so HTTP(S) URLs with embedded
1492
1820
  * userinfo are rejected here before any plan is printed. SSH/scp-style URLs
@@ -1566,10 +1894,33 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1566
1894
  return encodedProjectEndpointForPlan(projectKey, suffix);
1567
1895
  };
1568
1896
  const id = args[0];
1569
- const codeEnvEndpoint = (suffix = "") => `/public/api/admin/code-envs/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}${suffix}`;
1897
+ const codeEnvEndpoint = (suffix = "") => `/public/api/admin/code-envs/${encodeURIComponent(codeEnvLang(args[0], entry.usage))}/${encodeURIComponent(args[1])}${suffix}`;
1570
1898
  const statisticsWorksheetsEndpoint = (datasetName) => projectEndpoint(`/datasets/${encodeURIComponent(datasetName)}/statistics/worksheets/`);
1571
1899
  const statisticsWorksheetEndpoint = (datasetName, worksheetId) => `${statisticsWorksheetsEndpoint(datasetName)}${encodeURIComponent(worksheetId)}`;
1572
1900
  switch (`${resource}.${action}`) {
1901
+ case "sql.query": {
1902
+ const payload = resolveSqlQueryInvocation(args, flags, planProjectKeyFromArgs(flags));
1903
+ return {
1904
+ method: "POST",
1905
+ endpoint: "/public/api/sql/queries/",
1906
+ identifiers: {
1907
+ connection: payload.connection,
1908
+ dataset: payload.datasetFullName,
1909
+ },
1910
+ payload,
1911
+ };
1912
+ }
1913
+ case "ml-task.train":
1914
+ return {
1915
+ method: "POST",
1916
+ endpoint: projectEndpoint(`/models/lab/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}/train`),
1917
+ identifiers: { analysisId: args[0], mlTaskId: args[1], },
1918
+ payload: {
1919
+ sessionName: flags["session-name"],
1920
+ runQueue: false,
1921
+ },
1922
+ wait: flags["wait"] === true,
1923
+ };
1573
1924
  case "wiki.create": {
1574
1925
  const name = requiredPlanFlag(flags, "name", entry.usage);
1575
1926
  return {
@@ -1939,17 +2290,72 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1939
2290
  }
1940
2291
  case "recipe.run":
1941
2292
  return {
2293
+ exact: false,
2294
+ reason: "The job endpoint is exact, but DSS reads the recipe and its outputs before constructing the job payload; use recipe run --dry-run for a resolved payload.",
1942
2295
  method: "POST",
1943
2296
  endpoint: projectEndpoint("/jobs/"),
1944
2297
  identifiers: { recipe: id, },
1945
- payload: {
1946
- recipe: id,
1947
- outputResolution: "dynamic",
1948
- projectKey,
1949
- partition: flags["partition"],
1950
- },
1951
2298
  wait: recipeRunShouldWait(flags),
1952
2299
  };
2300
+ case "code.run": {
2301
+ const { script, source, } = resolveCodeInputWithSource(args, flags);
2302
+ const sourceSha256 = sha256Hex(script);
2303
+ const scenarioBase = projectEndpoint("/scenarios/{generatedScenarioId}");
2304
+ const envName = flags["env"];
2305
+ const keepScenario = flags["keep"] === true;
2306
+ const timeoutMs = parseCodeRunIntegerFlag(flags["timeout"], "--timeout") ?? 120_000;
2307
+ const maxLogBytes = flags["full-log"] === true
2308
+ ? 0
2309
+ : parseCodeRunIntegerFlag(flags["max-log-bytes"], "--max-log-bytes") ?? 1_048_576;
2310
+ return {
2311
+ method: "POST",
2312
+ endpoint: projectEndpoint("/scenarios/"),
2313
+ identifiers: {
2314
+ source,
2315
+ sourceSha256,
2316
+ sourceBytes: Buffer.byteLength(script),
2317
+ },
2318
+ requests: [
2319
+ {
2320
+ method: "POST",
2321
+ endpoint: projectEndpoint("/scenarios/"),
2322
+ payload: {
2323
+ id: "{generatedScenarioId}",
2324
+ name: "dss code run ({generatedScenarioId})",
2325
+ projectKey,
2326
+ type: "custom_python",
2327
+ params: {
2328
+ envSelection: envName
2329
+ ? { envMode: "EXPLICIT_ENV", envName, }
2330
+ : { envMode: "INHERIT", },
2331
+ },
2332
+ },
2333
+ },
2334
+ {
2335
+ method: "PUT",
2336
+ endpoint: `${scenarioBase}/payload`,
2337
+ payload: { extension: "py", script: "<omitted>", },
2338
+ redactedFields: ["payload.script",],
2339
+ },
2340
+ { method: "POST", endpoint: `${scenarioBase}/run/`, payload: {}, },
2341
+ {
2342
+ method: "GET",
2343
+ endpoint: `${scenarioBase}/get-run-for-trigger?triggerId={triggerId}&triggerRunId={triggerRunId}`,
2344
+ repeat: "until scenarioRun.result.outcome or timeout",
2345
+ },
2346
+ {
2347
+ method: "GET",
2348
+ endpoint: `${scenarioBase}/{runId}/log`,
2349
+ boundedBytes: maxLogBytes,
2350
+ },
2351
+ ...keepScenario ? [] : [{ method: "DELETE", endpoint: scenarioBase, cleanup: true, },],
2352
+ ],
2353
+ wait: {
2354
+ timeoutMs,
2355
+ pollEndpoint: `${scenarioBase}/get-run-for-trigger`,
2356
+ },
2357
+ };
2358
+ }
1953
2359
  case "recipe.update":
1954
2360
  return {
1955
2361
  method: "PUT",
@@ -2710,27 +3116,187 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2710
3116
  identifiers: { dataset: args[0], worksheetId: args[1], },
2711
3117
  payload: requiredPlanJsonInput(flags, entry.usage),
2712
3118
  };
2713
- case "bundle.export":
3119
+ case "webapp.create":
3120
+ return {
3121
+ method: "POST",
3122
+ endpoint: projectEndpoint("/webapps/"),
3123
+ payload: requiredPlanJsonInput(flags, entry.usage),
3124
+ };
3125
+ case "webapp.update-settings": {
3126
+ const patch = requiredPlanJsonInput(flags, entry.usage);
3127
+ const expectHash = flags["expect-hash"];
3128
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3129
+ throw new UsageError("Expected webapp hash must be a 64-character SHA-256 hex digest.");
3130
+ }
3131
+ const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}`);
3132
+ return {
3133
+ exact: false,
3134
+ reason: "Apply GETs the current settings, deep-merges the patch, then PUTs the full object.",
3135
+ method: "PUT",
3136
+ endpoint,
3137
+ identifiers: {
3138
+ webappId: id,
3139
+ patchHash: stableHash(patch),
3140
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3141
+ },
3142
+ payload: { patch: "<omitted>", patchHash: stableHash(patch), },
3143
+ requests: [
3144
+ { method: "GET", endpoint, purpose: "read full settings and verify expectHash", },
3145
+ {
3146
+ method: "PUT",
3147
+ endpoint,
3148
+ condition: "settings read and expected hash matched",
3149
+ payload: "<merged full settings>",
3150
+ },
3151
+ ],
3152
+ };
3153
+ }
3154
+ case "webapp.stop-backend":
3155
+ return {
3156
+ method: "PUT",
3157
+ endpoint: projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/stop`),
3158
+ identifiers: { webappId: id, },
3159
+ payload: {},
3160
+ };
3161
+ case "webapp.restart-backend": {
3162
+ const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/restart`);
2714
3163
  return {
2715
3164
  method: "PUT",
2716
- endpoint: projectEndpoint(`/bundles/exported/${encodeURIComponent(id)}`),
3165
+ endpoint,
3166
+ identifiers: { webappId: id, },
3167
+ payload: {},
3168
+ wait: {
3169
+ requested: flags["wait"] === true,
3170
+ timeoutMs: num(flags["timeout"], "--timeout"),
3171
+ pollIntervalMs: num(flags["poll-interval"], "--poll-interval"),
3172
+ },
3173
+ };
3174
+ }
3175
+ case "api-service.create":
3176
+ return {
3177
+ method: "POST",
3178
+ endpoint: projectEndpoint(`/apiservices/${encodeURIComponent(id)}`),
3179
+ identifiers: { serviceId: id, },
3180
+ payload: {},
3181
+ };
3182
+ case "api-service.save-settings": {
3183
+ const settings = requiredPlanJsonInput(flags, entry.usage);
3184
+ const expectHash = flags["expect-hash"];
3185
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3186
+ throw new UsageError("Expected API service settings hash must be a 64-character SHA-256 hex digest.");
3187
+ }
3188
+ const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
3189
+ const payload = {
3190
+ settings: "<omitted>",
3191
+ settingsHash: stableHash(settings),
3192
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3193
+ };
3194
+ return {
3195
+ method: "PUT",
3196
+ endpoint,
3197
+ identifiers: { serviceId: id, },
3198
+ payload,
3199
+ requests: [
3200
+ ...(expectHash
3201
+ ? [{ method: "GET", endpoint, purpose: "verify expectHash", },]
3202
+ : []),
3203
+ {
3204
+ method: "PUT",
3205
+ endpoint,
3206
+ condition: expectHash ? "hash matched" : "unconditional",
3207
+ payload,
3208
+ },
3209
+ ],
3210
+ };
3211
+ }
3212
+ case "api-service.add-prediction-endpoint": {
3213
+ const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
3214
+ return {
3215
+ exact: false,
3216
+ reason: "Apply GETs the full settings, appends one endpoint, then PUTs the full object.",
3217
+ method: "PUT",
3218
+ endpoint,
3219
+ identifiers: { serviceId: id, endpointId: args[1], savedModelId: args[2], },
3220
+ payload: { id: args[1], type: "STD_PREDICTION", modelRef: args[2], },
3221
+ requests: [
3222
+ { method: "GET", endpoint, purpose: "read full settings", },
3223
+ { method: "PUT", endpoint, payload: "<merged full settings>", },
3224
+ ],
3225
+ };
3226
+ }
3227
+ case "api-service.create-package":
3228
+ case "api-service.delete-package":
3229
+ case "api-service.publish-package": {
3230
+ const serviceId = args[0];
3231
+ const packageId = args[1];
3232
+ const packageEndpoint = projectEndpoint(`/apiservices/${encodeURIComponent(serviceId)}/packages/${encodeURIComponent(packageId)}`);
3233
+ if (action === "delete-package") {
3234
+ return {
3235
+ method: "DELETE",
3236
+ endpoint: packageEndpoint,
3237
+ identifiers: { serviceId, packageId, },
3238
+ };
3239
+ }
3240
+ if (action === "publish-package") {
3241
+ return {
3242
+ method: "POST",
3243
+ endpoint: `${packageEndpoint}/publish${querySuffix({
3244
+ publishedServiceId: flags["published-service-id"],
3245
+ })}`,
3246
+ identifiers: { serviceId, packageId, },
3247
+ };
3248
+ }
3249
+ return {
3250
+ method: "POST",
3251
+ endpoint: packageEndpoint + querySuffix({
3252
+ releaseNotes: flags["release-notes"],
3253
+ }),
3254
+ identifiers: { serviceId, packageId, },
3255
+ };
3256
+ }
3257
+ case "bundle.export": {
3258
+ const evaluateProjectStandardsChecks = parseBooleanOption(flags["evaluate-standards-checks"], "--evaluate-standards-checks") ?? true;
3259
+ return {
3260
+ method: "PUT",
3261
+ endpoint: projectEndpoint("/bundles/exported/" + encodeURIComponent(id)) + querySuffix({
3262
+ releaseNotes: flags["release-notes"],
3263
+ evaluateProjectStandardsChecks,
3264
+ }),
2717
3265
  identifiers: { bundleId: id, },
2718
3266
  payload: {},
2719
3267
  };
3268
+ }
2720
3269
  case "bundle.publish":
2721
3270
  return {
2722
3271
  method: "POST",
2723
- endpoint: projectEndpoint(`/bundles/${encodeURIComponent(id)}/publish`),
3272
+ endpoint: projectEndpoint("/bundles/" + encodeURIComponent(id) + "/publish") + querySuffix({
3273
+ publishedProjectKey: flags["published-project-key"],
3274
+ }),
2724
3275
  identifiers: { bundleId: id, },
2725
3276
  payload: {},
2726
3277
  };
2727
- case "bundle.activate":
3278
+ case "bundle.activate": {
3279
+ const rawScenarios = flags["scenarios"];
3280
+ let scenariosToEnable;
3281
+ if (rawScenarios !== undefined && rawScenarios !== false) {
3282
+ const parsed = json(rawScenarios);
3283
+ if (typeof parsed !== "object"
3284
+ || parsed === null
3285
+ || Array.isArray(parsed)
3286
+ || !Object.values(parsed).every((value) => typeof value === "boolean")) {
3287
+ throw new UsageError("--scenarios must be a JSON object mapping scenario IDs to true|false.");
3288
+ }
3289
+ scenariosToEnable = parsed;
3290
+ }
2728
3291
  return {
2729
3292
  method: "POST",
2730
3293
  endpoint: projectEndpoint(`/bundles/imported/${encodeURIComponent(id)}/actions/activate`),
2731
3294
  identifiers: { bundleId: id, },
2732
- payload: {},
3295
+ payload: scenariosToEnable === undefined
3296
+ ? {}
3297
+ : { scenariosActiveOnActivation: scenariosToEnable, },
2733
3298
  };
3299
+ }
2734
3300
  case "bundle.preload":
2735
3301
  return {
2736
3302
  method: "POST",
@@ -2738,40 +3304,151 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2738
3304
  identifiers: { bundleId: id, },
2739
3305
  payload: {},
2740
3306
  };
2741
- case "code-env.create":
3307
+ case "project-library.create-file":
3308
+ case "project-library.create-folder": {
3309
+ const libraryPath = validateLibraryPath(id);
3310
+ const kind = action === "create-file" ? "contents" : "folders";
2742
3311
  return {
3312
+ exact: false,
3313
+ reason: "Apply performs a live absence check before POST; --if-not-exists may stop after that read, and create never overwrites an existing item.",
2743
3314
  method: "POST",
2744
- endpoint: "/public/api/admin/code-envs/",
2745
- identifiers: { lang: args[0], name: args[1], },
3315
+ endpoint: projectEndpoint(`/libraries/${kind}/${encodeLibraryPath(libraryPath)}`),
3316
+ identifiers: { path: libraryPath, },
3317
+ };
3318
+ }
3319
+ case "project-library.put": {
3320
+ const libraryPath = validateLibraryPath(id);
3321
+ const endpoint = projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`);
3322
+ const payload = projectLibraryPutPayload(flags);
3323
+ return {
3324
+ method: "POST",
3325
+ endpoint,
3326
+ identifiers: {
3327
+ path: libraryPath,
3328
+ concurrencyControl: payload.expectSha256
3329
+ ? PROJECT_LIBRARY_CONCURRENCY_CONTROL
3330
+ : "none",
3331
+ },
3332
+ payload,
3333
+ ...(payload.expectSha256
3334
+ ? {
3335
+ requests: [
3336
+ {
3337
+ method: "GET",
3338
+ endpoint: `${endpoint}?dataEncoding=base64`,
3339
+ purpose: "verify expectSha256",
3340
+ },
3341
+ { method: "POST", endpoint, condition: "hash matched", payload, },
3342
+ ],
3343
+ }
3344
+ : {}),
3345
+ };
3346
+ }
3347
+ case "project-library.delete": {
3348
+ const libraryPath = validateLibraryPath(id);
3349
+ return {
3350
+ method: "DELETE",
3351
+ endpoint: projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`),
3352
+ identifiers: { path: libraryPath, },
3353
+ };
3354
+ }
3355
+ case "project-library.rename": {
3356
+ const libraryPath = validateLibraryPath(id);
3357
+ const newName = validateLibraryName(args[1]);
3358
+ return {
3359
+ method: "POST",
3360
+ endpoint: projectEndpoint("/libraries/contents-actions/rename/"),
3361
+ identifiers: { path: libraryPath, newName, },
3362
+ payload: { oldPath: `/${libraryPath}`, newName, },
3363
+ };
3364
+ }
3365
+ case "project-library.move": {
3366
+ const libraryPath = validateLibraryPath(id);
3367
+ const destinationFolder = validateLibraryDestinationPath(args[1]);
3368
+ return {
3369
+ method: "POST",
3370
+ endpoint: projectEndpoint("/libraries/contents-actions/move"),
3371
+ identifiers: { path: libraryPath, destinationFolder, },
3372
+ payload: { oldPath: `/${libraryPath}`, newPath: destinationFolder, },
3373
+ };
3374
+ }
3375
+ case "code-env.create": {
3376
+ const wait = codeEnvWait(flags);
3377
+ return {
3378
+ method: "POST",
3379
+ endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
3380
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
2746
3381
  payload: {
2747
- envLang: args[0],
2748
- envName: args[1],
3382
+ ...codeEnvParams(flags),
2749
3383
  deploymentMode: requiredPlanFlag(flags, "deployment-mode", entry.usage),
2750
- params: codeEnvParams(flags),
2751
- wait: codeEnvWait(flags),
2752
3384
  },
2753
- wait: codeEnvWait(flags),
3385
+ wait,
2754
3386
  };
2755
- case "code-env.set-definition":
3387
+ }
3388
+ case "code-env.set-definition": {
3389
+ const definition = requiredPlanJsonInput(flags, entry.usage);
3390
+ const expectHash = flags["expect-hash"];
3391
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3392
+ throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
3393
+ }
3394
+ const endpoint = codeEnvEndpoint();
3395
+ const definitionHash = stableHash(definition);
3396
+ const normalizedExpectHash = expectHash?.toLowerCase();
2756
3397
  return {
3398
+ exact: false,
3399
+ reason: "Apply PUTs the supplied definition object unchanged; this plan omits that body and reports only its hash.",
2757
3400
  method: "PUT",
2758
- endpoint: `/public/api/admin/code-envs/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}`,
2759
- identifiers: { lang: args[0], name: args[1], },
2760
- payload: requiredPlanJsonInput(flags, entry.usage),
3401
+ endpoint,
3402
+ identifiers: {
3403
+ lang: codeEnvLang(args[0], entry.usage),
3404
+ name: args[1],
3405
+ definitionHash,
3406
+ ...(normalizedExpectHash ? { expectHash: normalizedExpectHash, } : {}),
3407
+ },
3408
+ payload: "<omitted>",
3409
+ ...(normalizedExpectHash
3410
+ ? {
3411
+ requests: [
3412
+ { method: "GET", endpoint, purpose: "verify expectHash", },
3413
+ {
3414
+ method: "PUT",
3415
+ endpoint,
3416
+ condition: "hash matched",
3417
+ payload: "<omitted>",
3418
+ redactedFields: ["payload",],
3419
+ },
3420
+ ],
3421
+ }
3422
+ : {}),
2761
3423
  };
3424
+ }
2762
3425
  case "code-env.set-packages": {
2763
3426
  const installCorePackages = parseBooleanOption(flags["install-core-packages"], "--install-core-packages");
2764
- const payload = {
2765
- specPackageList: codeEnvPackageList(flags).join("\n"),
2766
- };
2767
- if (installCorePackages !== undefined) {
2768
- payload.desc = { installCorePackages, };
3427
+ const expectHash = flags["expect-hash"];
3428
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3429
+ throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
2769
3430
  }
3431
+ const endpoint = codeEnvEndpoint();
2770
3432
  return {
3433
+ exact: false,
3434
+ reason: "Apply GETs the full definition, merges only package fields, then PUTs the full result.",
2771
3435
  method: "PUT",
2772
- endpoint: codeEnvEndpoint(),
2773
- identifiers: { lang: args[0], name: args[1], },
2774
- payload,
3436
+ endpoint,
3437
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3438
+ payload: {
3439
+ packages: codeEnvPackageList(flags),
3440
+ ...(installCorePackages !== undefined ? { installCorePackages, } : {}),
3441
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3442
+ },
3443
+ requests: [
3444
+ { method: "GET", endpoint, purpose: "read full definition and verify expectHash", },
3445
+ {
3446
+ method: "PUT",
3447
+ endpoint,
3448
+ condition: "definition read and hash matched",
3449
+ payload: "<merged full definition>",
3450
+ },
3451
+ ],
2775
3452
  };
2776
3453
  }
2777
3454
  case "code-env.update-packages": {
@@ -2780,15 +3457,25 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2780
3457
  : typeof flags["version"] === "string"
2781
3458
  ? flags["version"]
2782
3459
  : undefined;
3460
+ const wait = codeEnvWait(flags);
2783
3461
  return {
2784
3462
  method: "POST",
2785
3463
  endpoint: `${codeEnvEndpoint("/packages")}${querySuffix({
2786
3464
  forceRebuildEnv: flags["force-rebuild"] === true,
2787
3465
  versionToUpdate,
2788
- wait: codeEnvWait(flags),
3466
+ wait,
2789
3467
  })}`,
2790
- identifiers: { lang: args[0], name: args[1], },
2791
- wait: codeEnvWait(flags),
3468
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3469
+ wait,
3470
+ };
3471
+ }
3472
+ case "code-env.update-images": {
3473
+ const wait = codeEnvWait(flags);
3474
+ return {
3475
+ method: "POST",
3476
+ endpoint: `${codeEnvEndpoint("/images")}${querySuffix({ envVersion: flags["env-version"], wait, })}`,
3477
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3478
+ wait,
2792
3479
  };
2793
3480
  }
2794
3481
  case "code-env.set-jupyter": {
@@ -2796,27 +3483,52 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2796
3483
  if (active === undefined) {
2797
3484
  throw new UsageError("--active is required. Usage: dss code-env set-jupyter <lang> <name> --active true|false");
2798
3485
  }
3486
+ const wait = codeEnvWait(flags);
2799
3487
  return {
2800
3488
  method: "POST",
2801
- endpoint: `${codeEnvEndpoint("/jupyter")}${querySuffix({ active, wait: codeEnvWait(flags), })}`,
2802
- identifiers: { lang: args[0], name: args[1], },
2803
- wait: codeEnvWait(flags),
3489
+ endpoint: `${codeEnvEndpoint("/jupyter")}${querySuffix({ active, wait, })}`,
3490
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3491
+ wait,
2804
3492
  };
2805
3493
  }
2806
- case "code-env.delete":
3494
+ case "code-env.delete": {
3495
+ const wait = codeEnvWait(flags);
2807
3496
  return {
2808
3497
  method: "DELETE",
2809
- endpoint: `/public/api/admin/code-envs/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}`,
2810
- identifiers: { lang: args[0], name: args[1], },
2811
- wait: codeEnvWait(flags),
3498
+ endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
3499
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3500
+ wait,
2812
3501
  };
2813
- case "notebook.save-jupyter":
3502
+ }
3503
+ case "notebook.save-jupyter": {
3504
+ const content = requiredPlanJsonInput(flags, entry.usage);
3505
+ const expectHash = flags["expect-hash"];
3506
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3507
+ throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
3508
+ }
3509
+ const endpoint = projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`);
2814
3510
  return {
2815
- method: "PUT",
2816
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`),
2817
- identifiers: { name: id, },
2818
- payload: requiredPlanJsonInput(flags, entry.usage),
3511
+ exact: false,
3512
+ reason: "A fresh GET selects POST when absent or PUT when present; a confirming GET supplies the persisted hash.",
3513
+ endpoint,
3514
+ identifiers: {
3515
+ name: id,
3516
+ contentHash: stableHash(content),
3517
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3518
+ },
3519
+ requests: [
3520
+ { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
3521
+ {
3522
+ method: "POST|PUT",
3523
+ endpoint,
3524
+ condition: "POST after not_found; otherwise PUT",
3525
+ payload: "<omitted>",
3526
+ redactedFields: ["payload",],
3527
+ },
3528
+ { method: "GET", endpoint, purpose: "confirm persisted hash", },
3529
+ ],
2819
3530
  };
3531
+ }
2820
3532
  case "notebook.delete-jupyter":
2821
3533
  return {
2822
3534
  method: "DELETE",
@@ -2825,24 +3537,75 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2825
3537
  };
2826
3538
  case "notebook.clear-jupyter-outputs":
2827
3539
  return {
2828
- method: "PUT",
2829
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`),
3540
+ method: "DELETE",
3541
+ endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}/outputs`),
2830
3542
  identifiers: { name: id, },
2831
- payload: { clearOutputs: true, },
2832
3543
  };
2833
3544
  case "notebook.unload-jupyter":
3545
+ if (flags["all"] === true) {
3546
+ if (args.length > 0)
3547
+ throw new UsageError(entry.usage);
3548
+ return {
3549
+ exact: false,
3550
+ reason: "DSS has no unload-all endpoint; apply lists active notebooks and sessions, then issues one verified session DELETE per result.",
3551
+ identifiers: { all: true, },
3552
+ requests: [
3553
+ { method: "GET", endpoint: projectEndpoint("/jupyter-notebooks/?active=true"), },
3554
+ {
3555
+ method: "GET",
3556
+ endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions"),
3557
+ forEach: "active notebook",
3558
+ },
3559
+ {
3560
+ method: "DELETE",
3561
+ endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions/{sessionId}"),
3562
+ forEach: "listed session",
3563
+ },
3564
+ ],
3565
+ };
3566
+ }
3567
+ requireArgs(args, 2, entry.usage);
2834
3568
  return {
2835
- method: "POST",
2836
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(args[0])}/sessions/${encodeURIComponent(args[1])}/unload`),
3569
+ method: "DELETE",
3570
+ endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(args[0])}/sessions/${encodeURIComponent(args[1])}`),
2837
3571
  identifiers: { name: args[0], sessionId: args[1], },
2838
3572
  };
2839
- case "notebook.save-sql":
3573
+ case "notebook.save-sql": {
3574
+ const content = requiredPlanJsonInput(flags, entry.usage);
3575
+ const expectHash = flags["expect-hash"];
3576
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3577
+ throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
3578
+ }
3579
+ const endpoint = projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`);
2840
3580
  return {
2841
- method: "PUT",
2842
- endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`),
2843
- identifiers: { id, },
2844
- payload: requiredPlanJsonInput(flags, entry.usage),
3581
+ exact: false,
3582
+ reason: "A fresh GET selects collection POST when absent or entity PUT when present; a confirming GET supplies the persisted hash.",
3583
+ endpoint,
3584
+ identifiers: {
3585
+ id,
3586
+ contentHash: stableHash(content),
3587
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3588
+ },
3589
+ requests: [
3590
+ { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
3591
+ {
3592
+ method: "POST",
3593
+ endpoint: projectEndpoint("/sql-notebooks/"),
3594
+ condition: "after not_found",
3595
+ payload: "<omitted; includes id and projectKey>",
3596
+ redactedFields: ["payload",],
3597
+ },
3598
+ {
3599
+ method: "PUT",
3600
+ endpoint,
3601
+ condition: "when present",
3602
+ payload: "<omitted>",
3603
+ redactedFields: ["payload",],
3604
+ },
3605
+ { method: "GET", endpoint, purpose: "confirm persisted hash", },
3606
+ ],
2845
3607
  };
3608
+ }
2846
3609
  case "notebook.delete-sql":
2847
3610
  return {
2848
3611
  method: "DELETE",
@@ -2851,10 +3614,13 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2851
3614
  };
2852
3615
  case "notebook.clear-sql-history":
2853
3616
  return {
2854
- method: "PUT",
2855
- endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}/history`),
2856
- identifiers: { id, cellId: flags["cell-id"], },
2857
- payload: { retain: num(flags["retain"], "--retain"), },
3617
+ method: "POST",
3618
+ endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}/history/clear`),
3619
+ identifiers: { id, },
3620
+ payload: {
3621
+ cellId: flags["cell-id"],
3622
+ numRunsToRetain: num(flags["retain"], "--retain"),
3623
+ },
2858
3624
  };
2859
3625
  case "project.create": {
2860
3626
  const settings = jsonInput(flags) ?? null;
@@ -3225,7 +3991,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3225
3991
  };
3226
3992
  }
3227
3993
  case "project-git.set-library": {
3228
- const targetPath = id;
3994
+ const targetPath = validateGitReferencePath(id);
3229
3995
  return {
3230
3996
  method: "PUT",
3231
3997
  endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}`),
@@ -3240,7 +4006,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3240
4006
  };
3241
4007
  }
3242
4008
  case "project-git.remove-library": {
3243
- const targetPath = id;
4009
+ const targetPath = validateGitReferencePath(id);
3244
4010
  return {
3245
4011
  method: "DELETE",
3246
4012
  endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}${querySuffix({
@@ -3250,7 +4016,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3250
4016
  };
3251
4017
  }
3252
4018
  case "project-git.reset-library": {
3253
- const targetPath = id;
4019
+ const targetPath = validateGitReferencePath(id);
3254
4020
  return {
3255
4021
  method: "POST",
3256
4022
  endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/reset"),
@@ -3260,7 +4026,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3260
4026
  };
3261
4027
  }
3262
4028
  case "project-git.push-library": {
3263
- const targetPath = id;
4029
+ const targetPath = validateGitReferencePath(id);
3264
4030
  const message = requiredPlanFlag(flags, "message", entry.usage);
3265
4031
  return {
3266
4032
  method: "POST",
@@ -3294,12 +4060,9 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3294
4060
  };
3295
4061
  default:
3296
4062
  return {
3297
- method: action.startsWith("delete") || action === "abort" ? "DELETE" : "POST",
3298
- endpoint: projectKey
3299
- ? projectEndpoint(`/${resource}s/${id ? encodeURIComponent(id) : ""}`)
3300
- : undefined,
4063
+ exact: false,
4064
+ reason: `No exact offline request shape is defined for ${resource}.${action}; no endpoint was guessed.`,
3301
4065
  identifiers: id ? { id, } : undefined,
3302
- payload: jsonInput(flags),
3303
4066
  };
3304
4067
  }
3305
4068
  }
@@ -3333,7 +4096,7 @@ export const BATCH_PLAN_EXIT_CODES = {
3333
4096
  assertionFailure: 4,
3334
4097
  };
3335
4098
  export const BATCH_USAGE = "dss batch (--data JSON|--data-file PATH|--stdin) [--continue-on-error] [--dry-run]";
3336
- const BATCH_DESCRIPTION = "Run a sequence of dss commands from a JSON array of argv arrays. Fail-fast by default; returns one envelope with a per-step ok/result/error and exits non-zero if any step failed.";
4099
+ const BATCH_DESCRIPTION = "Run dss argv arrays fail-fast by default. The result aggregates failed/retryable/failureCodes; process exit remains the first failed step's exit code.";
3337
4100
  export const BATCH_HINT = 'Pass a JSON array of argv arrays, e.g. [["dataset","list"],["recipe","update","r","--data-file","p.json"]].';
3338
4101
  export const BATCH_EXAMPLE_PAYLOAD = [
3339
4102
  ["recipe", "set-payload", "compute_orders", "--file", "code.py", "--no-backup",],