dataiku-sdk 3.0.0 → 3.2.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 (109) hide show
  1. package/README.md +246 -19
  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/auth.js +1 -1
  6. package/dist/src/cli/coerce.js +11 -2
  7. package/dist/src/cli/commands/analysis.js +4 -3
  8. package/dist/src/cli/commands/api-service.js +66 -18
  9. package/dist/src/cli/commands/app.js +4 -3
  10. package/dist/src/cli/commands/bundle.js +51 -16
  11. package/dist/src/cli/commands/code-env.js +219 -57
  12. package/dist/src/cli/commands/code.d.ts +19 -0
  13. package/dist/src/cli/commands/code.js +87 -15
  14. package/dist/src/cli/commands/dashboard.js +7 -6
  15. package/dist/src/cli/commands/data-quality.js +9 -8
  16. package/dist/src/cli/commands/dataset.js +17 -11
  17. package/dist/src/cli/commands/doctor.js +4 -2
  18. package/dist/src/cli/commands/flow-zone.js +42 -13
  19. package/dist/src/cli/commands/folder.js +9 -8
  20. package/dist/src/cli/commands/future.js +2 -1
  21. package/dist/src/cli/commands/insight.js +8 -7
  22. package/dist/src/cli/commands/job.js +4 -3
  23. package/dist/src/cli/commands/meaning.js +5 -4
  24. package/dist/src/cli/commands/ml-task.js +3 -2
  25. package/dist/src/cli/commands/model-evaluation-store.js +4 -3
  26. package/dist/src/cli/commands/notebook.js +82 -50
  27. package/dist/src/cli/commands/project-git.js +8 -5
  28. package/dist/src/cli/commands/project-library.d.ts +21 -0
  29. package/dist/src/cli/commands/project-library.js +128 -56
  30. package/dist/src/cli/commands/project.js +19 -4
  31. package/dist/src/cli/commands/recipe.js +51 -25
  32. package/dist/src/cli/commands/saved-model.js +4 -3
  33. package/dist/src/cli/commands/scenario.js +9 -8
  34. package/dist/src/cli/commands/sql.d.ts +10 -0
  35. package/dist/src/cli/commands/sql.js +42 -19
  36. package/dist/src/cli/commands/statistics.js +7 -6
  37. package/dist/src/cli/commands/variable.js +2 -1
  38. package/dist/src/cli/commands/webapp.js +68 -13
  39. package/dist/src/cli/commands/wiki.js +7 -6
  40. package/dist/src/cli/contract.d.ts +16 -1
  41. package/dist/src/cli/contract.js +967 -186
  42. package/dist/src/cli/env.d.ts +9 -0
  43. package/dist/src/cli/env.js +20 -2
  44. package/dist/src/cli/flags.d.ts +4 -0
  45. package/dist/src/cli/flags.js +29 -1
  46. package/dist/src/cli/helpers/cleanup.js +104 -3
  47. package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
  48. package/dist/src/cli/helpers/flow-zone.js +33 -3
  49. package/dist/src/cli/helpers/recipe.d.ts +20 -0
  50. package/dist/src/cli/helpers/recipe.js +158 -3
  51. package/dist/src/cli/output.d.ts +2 -0
  52. package/dist/src/cli/output.js +33 -6
  53. package/dist/src/cli/runtime.d.ts +35 -1
  54. package/dist/src/cli/runtime.js +125 -18
  55. package/dist/src/cli/types.d.ts +7 -0
  56. package/dist/src/cli.js +226 -95
  57. package/dist/src/client.d.ts +63 -1
  58. package/dist/src/client.js +305 -53
  59. package/dist/src/errors.d.ts +8 -2
  60. package/dist/src/errors.js +42 -0
  61. package/dist/src/index.d.ts +4 -3
  62. package/dist/src/index.js +2 -1
  63. package/dist/src/resources/api-services.d.ts +48 -6
  64. package/dist/src/resources/api-services.js +54 -8
  65. package/dist/src/resources/bundles.d.ts +51 -6
  66. package/dist/src/resources/bundles.js +39 -9
  67. package/dist/src/resources/code-envs.d.ts +25 -3
  68. package/dist/src/resources/code-envs.js +104 -2
  69. package/dist/src/resources/datasets.d.ts +20 -0
  70. package/dist/src/resources/datasets.js +232 -52
  71. package/dist/src/resources/folders.js +2 -1
  72. package/dist/src/resources/jobs.d.ts +35 -10
  73. package/dist/src/resources/jobs.js +92 -17
  74. package/dist/src/resources/notebooks.d.ts +77 -7
  75. package/dist/src/resources/notebooks.js +143 -19
  76. package/dist/src/resources/project-git.d.ts +12 -0
  77. package/dist/src/resources/project-git.js +39 -8
  78. package/dist/src/resources/project-library.d.ts +122 -7
  79. package/dist/src/resources/project-library.js +494 -25
  80. package/dist/src/resources/projects.d.ts +5 -4
  81. package/dist/src/resources/projects.js +21 -19
  82. package/dist/src/resources/scenarios.d.ts +30 -0
  83. package/dist/src/resources/scenarios.js +73 -27
  84. package/dist/src/resources/sql.d.ts +6 -0
  85. package/dist/src/resources/sql.js +23 -3
  86. package/dist/src/resources/webapps.d.ts +35 -4
  87. package/dist/src/resources/webapps.js +54 -5
  88. package/dist/src/schemas.d.ts +2 -2
  89. package/dist/src/schemas.js +1 -1
  90. package/dist/src/skill.d.ts +18 -0
  91. package/dist/src/skill.js +93 -9
  92. package/dist/src/utils/flow-analysis.d.ts +53 -0
  93. package/dist/src/utils/flow-analysis.js +382 -0
  94. package/dist/src/utils/project-archive.d.ts +41 -2
  95. package/dist/src/utils/project-archive.js +159 -16
  96. package/dist/src/utils/response-file.d.ts +2 -0
  97. package/dist/src/utils/response-file.js +34 -0
  98. package/package.json +9 -7
  99. package/packages/types/dist/index.d.ts +39 -4
  100. package/packages/types/dist/index.js +31 -2
  101. package/plugin.json +1 -1
  102. package/skills/dataiku-dss/SKILL.md +44 -197
  103. package/skills/dataiku-dss/references/app-releases.md +27 -0
  104. package/skills/dataiku-dss/references/authentication.md +39 -0
  105. package/skills/dataiku-dss/references/coding.md +8 -0
  106. package/skills/dataiku-dss/references/discovery.md +30 -0
  107. package/skills/dataiku-dss/references/flow-maps.md +12 -0
  108. package/skills/dataiku-dss/references/mutations.md +33 -0
  109. package/skills/dataiku-dss/references/troubleshooting.md +29 -0
@@ -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
- import { BOOLEAN_FLAGS, FLAG_ALIASES, KNOWN_LONG_FLAGS, SHORT_FLAGS, } from "./flags.js";
16
+ import { BOOLEAN_FLAGS, executionMode, 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, };
@@ -549,11 +749,11 @@ function unsafeOutputs(resource, action, producesLocalFile) {
549
749
  condition: "--output or --output-file",
550
750
  kind: "local-file",
551
751
  detail: sensitivePermissions
552
- ? "Command writes access-control identities and permissions to an owner-only local file."
553
- : "Command writes bytes to a local path and returns JSON metadata on stdout.",
752
+ ? "Writes access-control identities and permissions to an owner-only local file."
753
+ : "Writes a local file; stdout contains JSON metadata.",
554
754
  ...(sensitivePermissions
555
755
  ? {
556
- safeAlternative: "Keep the file owner-only and outside version control unless repository policy explicitly permits committing access-control data.",
756
+ safeAlternative: "Keep owner-only and outside version control unless repository policy explicitly permits committing access-control data.",
557
757
  }
558
758
  : {}),
559
759
  });
@@ -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,25 +1291,33 @@ 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 "If created:true, delete with `dss notebook delete-jupyter <name> --if-exists`.";
1297
+ }
1298
+ if (key === "notebook.save-sql") {
1299
+ return "If 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
  }
1008
1317
  if (resource === "data-quality") {
1009
1318
  return `Delete with \`dss data-quality delete-rule <dataset> <rule-id>${ifExists}\`.`;
1010
1319
  }
1011
- return `Delete with \`dss ${resource} delete <id>${ifExists}\` when the created object is disposable.`;
1320
+ return `For disposable creates: \`dss ${resource} delete <id>${ifExists}\`.`;
1012
1321
  }
1013
1322
  function buildRegistryEntry(resource, action, meta) {
1014
1323
  const requiresAuth = meta.localHandler === undefined && inferRequiresAuth(resource);
@@ -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, } : {}),
@@ -1114,81 +1427,99 @@ function buildRegistryEntry(resource, action, meta) {
1114
1427
  agentContractVersion: AGENT_CONTRACT_VERSION,
1115
1428
  };
1116
1429
  }
1117
- export function buildCommandRegistry() {
1430
+ export function buildCommandRegistry(resourceFilter) {
1118
1431
  const registry = {};
1119
1432
  for (const [resource, actions,] of Object.entries(commands)) {
1433
+ if (resourceFilter !== undefined && resource !== resourceFilter)
1434
+ continue;
1120
1435
  registry[resource] = {};
1121
1436
  for (const [action, meta,] of Object.entries(actions)) {
1122
1437
  registry[resource][action] = buildRegistryEntry(resource, action, meta);
1123
1438
  }
1124
1439
  }
1125
- registry.commands = {
1126
- run: buildRegistryEntry("commands", "run", {
1127
- handler: async () => undefined,
1128
- usage: COMMANDS_USAGE,
1129
- description: COMMANDS_DESCRIPTION,
1130
- examples: COMMANDS_EXAMPLES,
1131
- }),
1132
- };
1133
- registry.agent = {
1134
- contract: buildRegistryEntry("agent", "contract", {
1135
- handler: async () => undefined,
1136
- usage: AGENT_CONTRACT_USAGE,
1137
- description: AGENT_CONTRACT_DESCRIPTION,
1138
- examples: AGENT_CONTRACT_EXAMPLES,
1139
- }),
1140
- };
1141
- registry.version = {
1142
- run: buildRegistryEntry("version", "run", {
1143
- handler: async () => undefined,
1144
- usage: VERSION_USAGE,
1145
- description: VERSION_DESCRIPTION,
1146
- examples: VERSION_EXAMPLES,
1147
- }),
1148
- };
1149
- registry["install-skill"] = {
1150
- run: buildRegistryEntry("install-skill", "run", {
1151
- handler: async () => undefined,
1152
- usage: INSTALL_SKILL_USAGE,
1153
- description: INSTALL_SKILL_DESCRIPTION,
1154
- examples: INSTALL_SKILL_EXAMPLES,
1155
- }),
1156
- };
1157
- registry.cleanup = {
1158
- run: buildRegistryEntry("cleanup", "run", {
1159
- handler: async () => undefined,
1160
- usage: CLEANUP_USAGE,
1161
- description: CLEANUP_DESCRIPTION,
1162
- examples: CLEANUP_EXAMPLES,
1163
- }),
1164
- };
1165
- registry.fixtures = {
1166
- run: buildRegistryEntry("fixtures", "run", {
1167
- handler: async () => undefined,
1168
- usage: FIXTURES_USAGE,
1169
- description: FIXTURES_DESCRIPTION,
1170
- examples: FIXTURES_EXAMPLES,
1171
- }),
1172
- };
1173
- registry.batch = {
1174
- run: buildRegistryEntry("batch", "run", {
1175
- handler: async () => undefined,
1176
- usage: BATCH_USAGE,
1177
- description: BATCH_DESCRIPTION,
1178
- examples: BATCH_EXAMPLES,
1179
- examplePayload: BATCH_EXAMPLE_PAYLOAD,
1180
- payloadSchema: { stdin: true, dataFlag: true, dataFileFlag: true, jsonShape: "array", },
1181
- }),
1182
- };
1183
- registry.auth = {};
1184
- for (const [action, meta,] of Object.entries(AUTH_ACTIONS)) {
1185
- registry.auth[action] = buildRegistryEntry("auth", action, {
1186
- handler: async () => undefined,
1187
- usage: meta.usage,
1188
- description: meta.description,
1189
- examples: meta.examples,
1190
- requiredFlags: meta.requiredFlags,
1191
- });
1440
+ if (resourceFilter === undefined || resourceFilter === "commands") {
1441
+ registry.commands = {
1442
+ run: buildRegistryEntry("commands", "run", {
1443
+ handler: async () => undefined,
1444
+ usage: COMMANDS_USAGE,
1445
+ description: COMMANDS_DESCRIPTION,
1446
+ examples: COMMANDS_EXAMPLES,
1447
+ }),
1448
+ };
1449
+ }
1450
+ if (resourceFilter === undefined || resourceFilter === "agent") {
1451
+ registry.agent = {
1452
+ contract: buildRegistryEntry("agent", "contract", {
1453
+ handler: async () => undefined,
1454
+ usage: AGENT_CONTRACT_USAGE,
1455
+ description: AGENT_CONTRACT_DESCRIPTION,
1456
+ examples: AGENT_CONTRACT_EXAMPLES,
1457
+ }),
1458
+ };
1459
+ }
1460
+ if (resourceFilter === undefined || resourceFilter === "version") {
1461
+ registry.version = {
1462
+ run: buildRegistryEntry("version", "run", {
1463
+ handler: async () => undefined,
1464
+ usage: VERSION_USAGE,
1465
+ description: VERSION_DESCRIPTION,
1466
+ examples: VERSION_EXAMPLES,
1467
+ }),
1468
+ };
1469
+ }
1470
+ if (resourceFilter === undefined || resourceFilter === "install-skill") {
1471
+ registry["install-skill"] = {
1472
+ run: buildRegistryEntry("install-skill", "run", {
1473
+ handler: async () => undefined,
1474
+ usage: INSTALL_SKILL_USAGE,
1475
+ description: INSTALL_SKILL_DESCRIPTION,
1476
+ examples: INSTALL_SKILL_EXAMPLES,
1477
+ }),
1478
+ };
1479
+ }
1480
+ if (resourceFilter === undefined || resourceFilter === "cleanup") {
1481
+ registry.cleanup = {
1482
+ run: buildRegistryEntry("cleanup", "run", {
1483
+ handler: async () => undefined,
1484
+ usage: CLEANUP_USAGE,
1485
+ description: CLEANUP_DESCRIPTION,
1486
+ examples: CLEANUP_EXAMPLES,
1487
+ }),
1488
+ };
1489
+ }
1490
+ if (resourceFilter === undefined || resourceFilter === "fixtures") {
1491
+ registry.fixtures = {
1492
+ run: buildRegistryEntry("fixtures", "run", {
1493
+ handler: async () => undefined,
1494
+ usage: FIXTURES_USAGE,
1495
+ description: FIXTURES_DESCRIPTION,
1496
+ examples: FIXTURES_EXAMPLES,
1497
+ }),
1498
+ };
1499
+ }
1500
+ if (resourceFilter === undefined || resourceFilter === "batch") {
1501
+ registry.batch = {
1502
+ run: buildRegistryEntry("batch", "run", {
1503
+ handler: async () => undefined,
1504
+ usage: BATCH_USAGE,
1505
+ description: BATCH_DESCRIPTION,
1506
+ examples: BATCH_EXAMPLES,
1507
+ examplePayload: BATCH_EXAMPLE_PAYLOAD,
1508
+ payloadSchema: { stdin: true, dataFlag: true, dataFileFlag: true, jsonShape: "array", },
1509
+ }),
1510
+ };
1511
+ }
1512
+ if (resourceFilter === undefined || resourceFilter === "auth") {
1513
+ registry.auth = {};
1514
+ for (const [action, meta,] of Object.entries(AUTH_ACTIONS)) {
1515
+ registry.auth[action] = buildRegistryEntry("auth", action, {
1516
+ handler: async () => undefined,
1517
+ usage: meta.usage,
1518
+ description: meta.description,
1519
+ examples: meta.examples,
1520
+ requiredFlags: meta.requiredFlags,
1521
+ });
1522
+ }
1192
1523
  }
1193
1524
  return registry;
1194
1525
  }
@@ -1216,6 +1547,7 @@ function commandRegistryEntryJsonSchema() {
1216
1547
  "usage",
1217
1548
  "flags",
1218
1549
  "positionals",
1550
+ "positionalArguments",
1219
1551
  "sideEffect",
1220
1552
  "requiresAuth",
1221
1553
  "requiresProject",
@@ -1235,6 +1567,15 @@ function commandRegistryEntryJsonSchema() {
1235
1567
  structuredExamples: { type: "array", items: { type: "object", additionalProperties: true, }, },
1236
1568
  flags: { type: "array", items: commandFlagJsonSchema(), },
1237
1569
  positionals: { type: "array", items: { type: "string", }, },
1570
+ positionalArguments: {
1571
+ type: "array",
1572
+ items: {
1573
+ type: "object",
1574
+ required: ["name", "required",],
1575
+ additionalProperties: false,
1576
+ properties: { name: { type: "string", }, required: { type: "boolean", }, },
1577
+ },
1578
+ },
1238
1579
  sideEffect: { enum: ["read", "write", "auth",], },
1239
1580
  requiresAuth: { type: "boolean", },
1240
1581
  requiresProject: { type: "boolean", },
@@ -1247,6 +1588,7 @@ function commandRegistryEntryJsonSchema() {
1247
1588
  idempotency: { enum: ["safe", "convergent", "if-not-exists", "if-exists", "none",], },
1248
1589
  dryRun: { type: "boolean", },
1249
1590
  requiredFlags: { type: "array", items: { type: "string", }, },
1591
+ requiredInputGroups: { type: "array", items: { type: "object", additionalProperties: true, }, },
1250
1592
  optionalFlags: { type: "array", items: { type: "string", }, },
1251
1593
  schemas: { type: "object", additionalProperties: true, },
1252
1594
  unsafeOutputs: { type: "array", items: { type: "object", additionalProperties: true, }, },
@@ -1275,8 +1617,13 @@ function errorEnvelopeJsonSchema() {
1275
1617
  ok: { const: false, },
1276
1618
  error: { type: "string", },
1277
1619
  code: { type: "string", },
1278
- category: { enum: ["usage", "dss", "internal",], },
1620
+ category: { enum: ["usage", "permission_or_environment", "dss", "internal",], },
1279
1621
  exitCode: { type: "number", },
1622
+ hint: { type: "string", },
1623
+ status: { type: "number", },
1624
+ retryable: { type: "boolean", },
1625
+ requestId: { type: "string", },
1626
+ details: { type: "object", additionalProperties: true, },
1280
1627
  resource: { type: "string", },
1281
1628
  action: { type: "string", },
1282
1629
  projectKey: { type: "string", },
@@ -1351,7 +1698,7 @@ export function buildAgentContract() {
1351
1698
  "dss commands run --fields dataset",
1352
1699
  "dss commands run --fields dataset.create",
1353
1700
  ],
1354
- 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.",
1701
+ scopedDiscoveryHint: "Default: resource/action summary. --fields RESOURCE: all resource entries; RESOURCE.ACTION: one entry keyed by that path; append .FIELD for nested metadata. Comma-separate paths. --output PATH exports the full registry.",
1355
1702
  actions: commandActionSummary(registry),
1356
1703
  },
1357
1704
  schemas: {
@@ -1367,7 +1714,9 @@ export function buildAgentContract() {
1367
1714
  format: "compact-json",
1368
1715
  success: "single-json-value",
1369
1716
  failure: "structured-error-object",
1370
- richFailureResults: "doctor/batch/cleanup nonzero outcomes are their own compact result on stdout ({ok:false,...}) with the command's exit code; not re-wrapped.",
1717
+ failureResultDetailLimitBytes: 65_536,
1718
+ fieldProjection: "Missing --fields paths: null on stdout; field_projection_missing on stderr.",
1719
+ richFailureResults: "doctor/batch/cleanup failures: own compact {ok:false,...} result on stdout and command exit code; no wrapper.",
1371
1720
  },
1372
1721
  stderr: {
1373
1722
  format: "jsonl",
@@ -1384,7 +1733,7 @@ export function buildAgentContract() {
1384
1733
  },
1385
1734
  compatibility: {
1386
1735
  fieldsAreAdditiveWithinMajor: true,
1387
- failFastWhenUnsupported: "Check agentContractVersion before planning commands.",
1736
+ failFastWhenUnsupported: "Check agentContractVersion before planning.",
1388
1737
  },
1389
1738
  };
1390
1739
  }
@@ -1454,7 +1803,9 @@ function requiredPlanJsonInput(flags, usage) {
1454
1803
  return requiredJsonInput(flags, `--data, --data-file, or --stdin is required. Usage: ${usage}`);
1455
1804
  }
1456
1805
  function requiredPlanPositionals(usage) {
1457
- return [...stripOptionalUsageGroups(usage).matchAll(/<([^>]+)>/g),].map((match) => match[1]);
1806
+ return extractPositionalArguments(usage)
1807
+ .filter((positional) => positional.required)
1808
+ .map((positional) => positional.name);
1458
1809
  }
1459
1810
  function dataQualityEndpoint(projectKey, datasetName, suffix) {
1460
1811
  return encodedProjectEndpointForPlan(projectKey, `/datasets/${encodeURIComponent(datasetName)}/data-quality${suffix}`);
@@ -1482,11 +1833,6 @@ function projectGitEndpoint(projectKey, suffix) {
1482
1833
  function projectGitFutureEndpoint(jobId) {
1483
1834
  return `${PROJECT_GIT_API_ROOT}/futures/${encodeURIComponent(jobId)}`;
1484
1835
  }
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
1836
  /**
1491
1837
  * `--plan` bypasses the SDK boundary guard, so HTTP(S) URLs with embedded
1492
1838
  * userinfo are rejected here before any plan is printed. SSH/scp-style URLs
@@ -1566,10 +1912,33 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1566
1912
  return encodedProjectEndpointForPlan(projectKey, suffix);
1567
1913
  };
1568
1914
  const id = args[0];
1569
- const codeEnvEndpoint = (suffix = "") => `/public/api/admin/code-envs/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}${suffix}`;
1915
+ const codeEnvEndpoint = (suffix = "") => `/public/api/admin/code-envs/${encodeURIComponent(codeEnvLang(args[0], entry.usage))}/${encodeURIComponent(args[1])}${suffix}`;
1570
1916
  const statisticsWorksheetsEndpoint = (datasetName) => projectEndpoint(`/datasets/${encodeURIComponent(datasetName)}/statistics/worksheets/`);
1571
1917
  const statisticsWorksheetEndpoint = (datasetName, worksheetId) => `${statisticsWorksheetsEndpoint(datasetName)}${encodeURIComponent(worksheetId)}`;
1572
1918
  switch (`${resource}.${action}`) {
1919
+ case "sql.query": {
1920
+ const payload = resolveSqlQueryInvocation(args, flags, planProjectKeyFromArgs(flags));
1921
+ return {
1922
+ method: "POST",
1923
+ endpoint: "/public/api/sql/queries/",
1924
+ identifiers: {
1925
+ connection: payload.connection,
1926
+ dataset: payload.datasetFullName,
1927
+ },
1928
+ payload,
1929
+ };
1930
+ }
1931
+ case "ml-task.train":
1932
+ return {
1933
+ method: "POST",
1934
+ endpoint: projectEndpoint(`/models/lab/${encodeURIComponent(args[0])}/${encodeURIComponent(args[1])}/train`),
1935
+ identifiers: { analysisId: args[0], mlTaskId: args[1], },
1936
+ payload: {
1937
+ sessionName: flags["session-name"],
1938
+ runQueue: false,
1939
+ },
1940
+ wait: flags["wait"] === true,
1941
+ };
1573
1942
  case "wiki.create": {
1574
1943
  const name = requiredPlanFlag(flags, "name", entry.usage);
1575
1944
  return {
@@ -1939,17 +2308,72 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1939
2308
  }
1940
2309
  case "recipe.run":
1941
2310
  return {
2311
+ exact: false,
2312
+ 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
2313
  method: "POST",
1943
2314
  endpoint: projectEndpoint("/jobs/"),
1944
2315
  identifiers: { recipe: id, },
1945
- payload: {
1946
- recipe: id,
1947
- outputResolution: "dynamic",
1948
- projectKey,
1949
- partition: flags["partition"],
1950
- },
1951
2316
  wait: recipeRunShouldWait(flags),
1952
2317
  };
2318
+ case "code.run": {
2319
+ const { script, source, } = resolveCodeInputWithSource(args, flags);
2320
+ const sourceSha256 = sha256Hex(script);
2321
+ const scenarioBase = projectEndpoint("/scenarios/{generatedScenarioId}");
2322
+ const envName = flags["env"];
2323
+ const keepScenario = flags["keep"] === true;
2324
+ const timeoutMs = parseCodeRunIntegerFlag(flags["timeout"], "--timeout") ?? 120_000;
2325
+ const maxLogBytes = flags["full-log"] === true
2326
+ ? 0
2327
+ : parseCodeRunIntegerFlag(flags["max-log-bytes"], "--max-log-bytes") ?? 1_048_576;
2328
+ return {
2329
+ method: "POST",
2330
+ endpoint: projectEndpoint("/scenarios/"),
2331
+ identifiers: {
2332
+ source,
2333
+ sourceSha256,
2334
+ sourceBytes: Buffer.byteLength(script),
2335
+ },
2336
+ requests: [
2337
+ {
2338
+ method: "POST",
2339
+ endpoint: projectEndpoint("/scenarios/"),
2340
+ payload: {
2341
+ id: "{generatedScenarioId}",
2342
+ name: "dss code run ({generatedScenarioId})",
2343
+ projectKey,
2344
+ type: "custom_python",
2345
+ params: {
2346
+ envSelection: envName
2347
+ ? { envMode: "EXPLICIT_ENV", envName, }
2348
+ : { envMode: "INHERIT", },
2349
+ },
2350
+ },
2351
+ },
2352
+ {
2353
+ method: "PUT",
2354
+ endpoint: `${scenarioBase}/payload`,
2355
+ payload: { extension: "py", script: "<omitted>", },
2356
+ redactedFields: ["payload.script",],
2357
+ },
2358
+ { method: "POST", endpoint: `${scenarioBase}/run/`, payload: {}, },
2359
+ {
2360
+ method: "GET",
2361
+ endpoint: `${scenarioBase}/get-run-for-trigger?triggerId={triggerId}&triggerRunId={triggerRunId}`,
2362
+ repeat: "until scenarioRun.result.outcome or timeout",
2363
+ },
2364
+ {
2365
+ method: "GET",
2366
+ endpoint: `${scenarioBase}/{runId}/log`,
2367
+ boundedBytes: maxLogBytes,
2368
+ },
2369
+ ...keepScenario ? [] : [{ method: "DELETE", endpoint: scenarioBase, cleanup: true, },],
2370
+ ],
2371
+ wait: {
2372
+ timeoutMs,
2373
+ pollEndpoint: `${scenarioBase}/get-run-for-trigger`,
2374
+ },
2375
+ };
2376
+ }
1953
2377
  case "recipe.update":
1954
2378
  return {
1955
2379
  method: "PUT",
@@ -2710,27 +3134,187 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2710
3134
  identifiers: { dataset: args[0], worksheetId: args[1], },
2711
3135
  payload: requiredPlanJsonInput(flags, entry.usage),
2712
3136
  };
2713
- case "bundle.export":
3137
+ case "webapp.create":
3138
+ return {
3139
+ method: "POST",
3140
+ endpoint: projectEndpoint("/webapps/"),
3141
+ payload: requiredPlanJsonInput(flags, entry.usage),
3142
+ };
3143
+ case "webapp.update-settings": {
3144
+ const patch = requiredPlanJsonInput(flags, entry.usage);
3145
+ const expectHash = flags["expect-hash"];
3146
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3147
+ throw new UsageError("Expected webapp hash must be a 64-character SHA-256 hex digest.");
3148
+ }
3149
+ const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}`);
3150
+ return {
3151
+ exact: false,
3152
+ reason: "Apply GETs the current settings, deep-merges the patch, then PUTs the full object.",
3153
+ method: "PUT",
3154
+ endpoint,
3155
+ identifiers: {
3156
+ webappId: id,
3157
+ patchHash: stableHash(patch),
3158
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3159
+ },
3160
+ payload: { patch: "<omitted>", patchHash: stableHash(patch), },
3161
+ requests: [
3162
+ { method: "GET", endpoint, purpose: "read full settings and verify expectHash", },
3163
+ {
3164
+ method: "PUT",
3165
+ endpoint,
3166
+ condition: "settings read and expected hash matched",
3167
+ payload: "<merged full settings>",
3168
+ },
3169
+ ],
3170
+ };
3171
+ }
3172
+ case "webapp.stop-backend":
2714
3173
  return {
2715
3174
  method: "PUT",
2716
- endpoint: projectEndpoint(`/bundles/exported/${encodeURIComponent(id)}`),
3175
+ endpoint: projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/stop`),
3176
+ identifiers: { webappId: id, },
3177
+ payload: {},
3178
+ };
3179
+ case "webapp.restart-backend": {
3180
+ const endpoint = projectEndpoint(`/webapps/${encodeURIComponent(id)}/backend/actions/restart`);
3181
+ return {
3182
+ method: "PUT",
3183
+ endpoint,
3184
+ identifiers: { webappId: id, },
3185
+ payload: {},
3186
+ wait: {
3187
+ requested: flags["wait"] === true,
3188
+ timeoutMs: num(flags["timeout"], "--timeout"),
3189
+ pollIntervalMs: num(flags["poll-interval"], "--poll-interval"),
3190
+ },
3191
+ };
3192
+ }
3193
+ case "api-service.create":
3194
+ return {
3195
+ method: "POST",
3196
+ endpoint: projectEndpoint(`/apiservices/${encodeURIComponent(id)}`),
3197
+ identifiers: { serviceId: id, },
3198
+ payload: {},
3199
+ };
3200
+ case "api-service.save-settings": {
3201
+ const settings = requiredPlanJsonInput(flags, entry.usage);
3202
+ const expectHash = flags["expect-hash"];
3203
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3204
+ throw new UsageError("Expected API service settings hash must be a 64-character SHA-256 hex digest.");
3205
+ }
3206
+ const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
3207
+ const payload = {
3208
+ settings: "<omitted>",
3209
+ settingsHash: stableHash(settings),
3210
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3211
+ };
3212
+ return {
3213
+ method: "PUT",
3214
+ endpoint,
3215
+ identifiers: { serviceId: id, },
3216
+ payload,
3217
+ requests: [
3218
+ ...(expectHash
3219
+ ? [{ method: "GET", endpoint, purpose: "verify expectHash", },]
3220
+ : []),
3221
+ {
3222
+ method: "PUT",
3223
+ endpoint,
3224
+ condition: expectHash ? "hash matched" : "unconditional",
3225
+ payload,
3226
+ },
3227
+ ],
3228
+ };
3229
+ }
3230
+ case "api-service.add-prediction-endpoint": {
3231
+ const endpoint = projectEndpoint(`/apiservices/${encodeURIComponent(id)}/settings`);
3232
+ return {
3233
+ exact: false,
3234
+ reason: "Apply GETs the full settings, appends one endpoint, then PUTs the full object.",
3235
+ method: "PUT",
3236
+ endpoint,
3237
+ identifiers: { serviceId: id, endpointId: args[1], savedModelId: args[2], },
3238
+ payload: { id: args[1], type: "STD_PREDICTION", modelRef: args[2], },
3239
+ requests: [
3240
+ { method: "GET", endpoint, purpose: "read full settings", },
3241
+ { method: "PUT", endpoint, payload: "<merged full settings>", },
3242
+ ],
3243
+ };
3244
+ }
3245
+ case "api-service.create-package":
3246
+ case "api-service.delete-package":
3247
+ case "api-service.publish-package": {
3248
+ const serviceId = args[0];
3249
+ const packageId = args[1];
3250
+ const packageEndpoint = projectEndpoint(`/apiservices/${encodeURIComponent(serviceId)}/packages/${encodeURIComponent(packageId)}`);
3251
+ if (action === "delete-package") {
3252
+ return {
3253
+ method: "DELETE",
3254
+ endpoint: packageEndpoint,
3255
+ identifiers: { serviceId, packageId, },
3256
+ };
3257
+ }
3258
+ if (action === "publish-package") {
3259
+ return {
3260
+ method: "POST",
3261
+ endpoint: `${packageEndpoint}/publish${querySuffix({
3262
+ publishedServiceId: flags["published-service-id"],
3263
+ })}`,
3264
+ identifiers: { serviceId, packageId, },
3265
+ };
3266
+ }
3267
+ return {
3268
+ method: "POST",
3269
+ endpoint: packageEndpoint + querySuffix({
3270
+ releaseNotes: flags["release-notes"],
3271
+ }),
3272
+ identifiers: { serviceId, packageId, },
3273
+ };
3274
+ }
3275
+ case "bundle.export": {
3276
+ const evaluateProjectStandardsChecks = parseBooleanOption(flags["evaluate-standards-checks"], "--evaluate-standards-checks") ?? true;
3277
+ return {
3278
+ method: "PUT",
3279
+ endpoint: projectEndpoint("/bundles/exported/" + encodeURIComponent(id)) + querySuffix({
3280
+ releaseNotes: flags["release-notes"],
3281
+ evaluateProjectStandardsChecks,
3282
+ }),
2717
3283
  identifiers: { bundleId: id, },
2718
3284
  payload: {},
2719
3285
  };
3286
+ }
2720
3287
  case "bundle.publish":
2721
3288
  return {
2722
3289
  method: "POST",
2723
- endpoint: projectEndpoint(`/bundles/${encodeURIComponent(id)}/publish`),
3290
+ endpoint: projectEndpoint("/bundles/" + encodeURIComponent(id) + "/publish") + querySuffix({
3291
+ publishedProjectKey: flags["published-project-key"],
3292
+ }),
2724
3293
  identifiers: { bundleId: id, },
2725
3294
  payload: {},
2726
3295
  };
2727
- case "bundle.activate":
3296
+ case "bundle.activate": {
3297
+ const rawScenarios = flags["scenarios"];
3298
+ let scenariosToEnable;
3299
+ if (rawScenarios !== undefined && rawScenarios !== false) {
3300
+ const parsed = json(rawScenarios);
3301
+ if (typeof parsed !== "object"
3302
+ || parsed === null
3303
+ || Array.isArray(parsed)
3304
+ || !Object.values(parsed).every((value) => typeof value === "boolean")) {
3305
+ throw new UsageError("--scenarios must be a JSON object mapping scenario IDs to true|false.");
3306
+ }
3307
+ scenariosToEnable = parsed;
3308
+ }
2728
3309
  return {
2729
3310
  method: "POST",
2730
3311
  endpoint: projectEndpoint(`/bundles/imported/${encodeURIComponent(id)}/actions/activate`),
2731
3312
  identifiers: { bundleId: id, },
2732
- payload: {},
3313
+ payload: scenariosToEnable === undefined
3314
+ ? {}
3315
+ : { scenariosActiveOnActivation: scenariosToEnable, },
2733
3316
  };
3317
+ }
2734
3318
  case "bundle.preload":
2735
3319
  return {
2736
3320
  method: "POST",
@@ -2738,40 +3322,151 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2738
3322
  identifiers: { bundleId: id, },
2739
3323
  payload: {},
2740
3324
  };
2741
- case "code-env.create":
3325
+ case "project-library.create-file":
3326
+ case "project-library.create-folder": {
3327
+ const libraryPath = validateLibraryPath(id);
3328
+ const kind = action === "create-file" ? "contents" : "folders";
2742
3329
  return {
3330
+ exact: false,
3331
+ 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
3332
  method: "POST",
2744
- endpoint: "/public/api/admin/code-envs/",
2745
- identifiers: { lang: args[0], name: args[1], },
3333
+ endpoint: projectEndpoint(`/libraries/${kind}/${encodeLibraryPath(libraryPath)}`),
3334
+ identifiers: { path: libraryPath, },
3335
+ };
3336
+ }
3337
+ case "project-library.put": {
3338
+ const libraryPath = validateLibraryPath(id);
3339
+ const endpoint = projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`);
3340
+ const payload = projectLibraryPutPayload(flags);
3341
+ return {
3342
+ method: "POST",
3343
+ endpoint,
3344
+ identifiers: {
3345
+ path: libraryPath,
3346
+ concurrencyControl: payload.expectSha256
3347
+ ? PROJECT_LIBRARY_CONCURRENCY_CONTROL
3348
+ : "none",
3349
+ },
3350
+ payload,
3351
+ ...(payload.expectSha256
3352
+ ? {
3353
+ requests: [
3354
+ {
3355
+ method: "GET",
3356
+ endpoint: `${endpoint}?dataEncoding=base64`,
3357
+ purpose: "verify expectSha256",
3358
+ },
3359
+ { method: "POST", endpoint, condition: "hash matched", payload, },
3360
+ ],
3361
+ }
3362
+ : {}),
3363
+ };
3364
+ }
3365
+ case "project-library.delete": {
3366
+ const libraryPath = validateLibraryPath(id);
3367
+ return {
3368
+ method: "DELETE",
3369
+ endpoint: projectEndpoint(`/libraries/contents/${encodeLibraryPath(libraryPath)}`),
3370
+ identifiers: { path: libraryPath, },
3371
+ };
3372
+ }
3373
+ case "project-library.rename": {
3374
+ const libraryPath = validateLibraryPath(id);
3375
+ const newName = validateLibraryName(args[1]);
3376
+ return {
3377
+ method: "POST",
3378
+ endpoint: projectEndpoint("/libraries/contents-actions/rename/"),
3379
+ identifiers: { path: libraryPath, newName, },
3380
+ payload: { oldPath: `/${libraryPath}`, newName, },
3381
+ };
3382
+ }
3383
+ case "project-library.move": {
3384
+ const libraryPath = validateLibraryPath(id);
3385
+ const destinationFolder = validateLibraryDestinationPath(args[1]);
3386
+ return {
3387
+ method: "POST",
3388
+ endpoint: projectEndpoint("/libraries/contents-actions/move"),
3389
+ identifiers: { path: libraryPath, destinationFolder, },
3390
+ payload: { oldPath: `/${libraryPath}`, newPath: destinationFolder, },
3391
+ };
3392
+ }
3393
+ case "code-env.create": {
3394
+ const wait = codeEnvWait(flags);
3395
+ return {
3396
+ method: "POST",
3397
+ endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
3398
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
2746
3399
  payload: {
2747
- envLang: args[0],
2748
- envName: args[1],
3400
+ ...codeEnvParams(flags),
2749
3401
  deploymentMode: requiredPlanFlag(flags, "deployment-mode", entry.usage),
2750
- params: codeEnvParams(flags),
2751
- wait: codeEnvWait(flags),
2752
3402
  },
2753
- wait: codeEnvWait(flags),
3403
+ wait,
2754
3404
  };
2755
- case "code-env.set-definition":
3405
+ }
3406
+ case "code-env.set-definition": {
3407
+ const definition = requiredPlanJsonInput(flags, entry.usage);
3408
+ const expectHash = flags["expect-hash"];
3409
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3410
+ throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
3411
+ }
3412
+ const endpoint = codeEnvEndpoint();
3413
+ const definitionHash = stableHash(definition);
3414
+ const normalizedExpectHash = expectHash?.toLowerCase();
2756
3415
  return {
3416
+ exact: false,
3417
+ reason: "Apply PUTs the supplied definition object unchanged; this plan omits that body and reports only its hash.",
2757
3418
  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),
3419
+ endpoint,
3420
+ identifiers: {
3421
+ lang: codeEnvLang(args[0], entry.usage),
3422
+ name: args[1],
3423
+ definitionHash,
3424
+ ...(normalizedExpectHash ? { expectHash: normalizedExpectHash, } : {}),
3425
+ },
3426
+ payload: "<omitted>",
3427
+ ...(normalizedExpectHash
3428
+ ? {
3429
+ requests: [
3430
+ { method: "GET", endpoint, purpose: "verify expectHash", },
3431
+ {
3432
+ method: "PUT",
3433
+ endpoint,
3434
+ condition: "hash matched",
3435
+ payload: "<omitted>",
3436
+ redactedFields: ["payload",],
3437
+ },
3438
+ ],
3439
+ }
3440
+ : {}),
2761
3441
  };
3442
+ }
2762
3443
  case "code-env.set-packages": {
2763
3444
  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, };
3445
+ const expectHash = flags["expect-hash"];
3446
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3447
+ throw new UsageError("--expect-hash must be a 64-character SHA-256 hex digest.");
2769
3448
  }
3449
+ const endpoint = codeEnvEndpoint();
2770
3450
  return {
3451
+ exact: false,
3452
+ reason: "Apply GETs the full definition, merges only package fields, then PUTs the full result.",
2771
3453
  method: "PUT",
2772
- endpoint: codeEnvEndpoint(),
2773
- identifiers: { lang: args[0], name: args[1], },
2774
- payload,
3454
+ endpoint,
3455
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3456
+ payload: {
3457
+ packages: codeEnvPackageList(flags),
3458
+ ...(installCorePackages !== undefined ? { installCorePackages, } : {}),
3459
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3460
+ },
3461
+ requests: [
3462
+ { method: "GET", endpoint, purpose: "read full definition and verify expectHash", },
3463
+ {
3464
+ method: "PUT",
3465
+ endpoint,
3466
+ condition: "definition read and hash matched",
3467
+ payload: "<merged full definition>",
3468
+ },
3469
+ ],
2775
3470
  };
2776
3471
  }
2777
3472
  case "code-env.update-packages": {
@@ -2780,15 +3475,25 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2780
3475
  : typeof flags["version"] === "string"
2781
3476
  ? flags["version"]
2782
3477
  : undefined;
3478
+ const wait = codeEnvWait(flags);
2783
3479
  return {
2784
3480
  method: "POST",
2785
3481
  endpoint: `${codeEnvEndpoint("/packages")}${querySuffix({
2786
3482
  forceRebuildEnv: flags["force-rebuild"] === true,
2787
3483
  versionToUpdate,
2788
- wait: codeEnvWait(flags),
3484
+ wait,
2789
3485
  })}`,
2790
- identifiers: { lang: args[0], name: args[1], },
2791
- wait: codeEnvWait(flags),
3486
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3487
+ wait,
3488
+ };
3489
+ }
3490
+ case "code-env.update-images": {
3491
+ const wait = codeEnvWait(flags);
3492
+ return {
3493
+ method: "POST",
3494
+ endpoint: `${codeEnvEndpoint("/images")}${querySuffix({ envVersion: flags["env-version"], wait, })}`,
3495
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3496
+ wait,
2792
3497
  };
2793
3498
  }
2794
3499
  case "code-env.set-jupyter": {
@@ -2796,27 +3501,52 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2796
3501
  if (active === undefined) {
2797
3502
  throw new UsageError("--active is required. Usage: dss code-env set-jupyter <lang> <name> --active true|false");
2798
3503
  }
3504
+ const wait = codeEnvWait(flags);
2799
3505
  return {
2800
3506
  method: "POST",
2801
- endpoint: `${codeEnvEndpoint("/jupyter")}${querySuffix({ active, wait: codeEnvWait(flags), })}`,
2802
- identifiers: { lang: args[0], name: args[1], },
2803
- wait: codeEnvWait(flags),
3507
+ endpoint: `${codeEnvEndpoint("/jupyter")}${querySuffix({ active, wait, })}`,
3508
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3509
+ wait,
2804
3510
  };
2805
3511
  }
2806
- case "code-env.delete":
3512
+ case "code-env.delete": {
3513
+ const wait = codeEnvWait(flags);
2807
3514
  return {
2808
3515
  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),
3516
+ endpoint: `${codeEnvEndpoint()}?wait=${wait}`,
3517
+ identifiers: { lang: codeEnvLang(args[0], entry.usage), name: args[1], },
3518
+ wait,
2812
3519
  };
2813
- case "notebook.save-jupyter":
3520
+ }
3521
+ case "notebook.save-jupyter": {
3522
+ const content = requiredPlanJsonInput(flags, entry.usage);
3523
+ const expectHash = flags["expect-hash"];
3524
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3525
+ throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
3526
+ }
3527
+ const endpoint = projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`);
2814
3528
  return {
2815
- method: "PUT",
2816
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`),
2817
- identifiers: { name: id, },
2818
- payload: requiredPlanJsonInput(flags, entry.usage),
3529
+ exact: false,
3530
+ reason: "A fresh GET selects POST when absent or PUT when present; a confirming GET supplies the persisted hash.",
3531
+ endpoint,
3532
+ identifiers: {
3533
+ name: id,
3534
+ contentHash: stableHash(content),
3535
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3536
+ },
3537
+ requests: [
3538
+ { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
3539
+ {
3540
+ method: "POST|PUT",
3541
+ endpoint,
3542
+ condition: "POST after not_found; otherwise PUT",
3543
+ payload: "<omitted>",
3544
+ redactedFields: ["payload",],
3545
+ },
3546
+ { method: "GET", endpoint, purpose: "confirm persisted hash", },
3547
+ ],
2819
3548
  };
3549
+ }
2820
3550
  case "notebook.delete-jupyter":
2821
3551
  return {
2822
3552
  method: "DELETE",
@@ -2825,24 +3555,75 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2825
3555
  };
2826
3556
  case "notebook.clear-jupyter-outputs":
2827
3557
  return {
2828
- method: "PUT",
2829
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}`),
3558
+ method: "DELETE",
3559
+ endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(id)}/outputs`),
2830
3560
  identifiers: { name: id, },
2831
- payload: { clearOutputs: true, },
2832
3561
  };
2833
3562
  case "notebook.unload-jupyter":
3563
+ if (flags["all"] === true) {
3564
+ if (args.length > 0)
3565
+ throw new UsageError(entry.usage);
3566
+ return {
3567
+ exact: false,
3568
+ reason: "DSS has no unload-all endpoint; apply lists active notebooks and sessions, then issues one verified session DELETE per result.",
3569
+ identifiers: { all: true, },
3570
+ requests: [
3571
+ { method: "GET", endpoint: projectEndpoint("/jupyter-notebooks/?active=true"), },
3572
+ {
3573
+ method: "GET",
3574
+ endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions"),
3575
+ forEach: "active notebook",
3576
+ },
3577
+ {
3578
+ method: "DELETE",
3579
+ endpoint: projectEndpoint("/jupyter-notebooks/{name}/sessions/{sessionId}"),
3580
+ forEach: "listed session",
3581
+ },
3582
+ ],
3583
+ };
3584
+ }
3585
+ requireArgs(args, 2, entry.usage);
2834
3586
  return {
2835
- method: "POST",
2836
- endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(args[0])}/sessions/${encodeURIComponent(args[1])}/unload`),
3587
+ method: "DELETE",
3588
+ endpoint: projectEndpoint(`/jupyter-notebooks/${encodeURIComponent(args[0])}/sessions/${encodeURIComponent(args[1])}`),
2837
3589
  identifiers: { name: args[0], sessionId: args[1], },
2838
3590
  };
2839
- case "notebook.save-sql":
3591
+ case "notebook.save-sql": {
3592
+ const content = requiredPlanJsonInput(flags, entry.usage);
3593
+ const expectHash = flags["expect-hash"];
3594
+ if (expectHash !== undefined && !/^[a-fA-F0-9]{64}$/.test(expectHash)) {
3595
+ throw new UsageError("Expected notebook hash must be a 64-character SHA-256 hex digest.");
3596
+ }
3597
+ const endpoint = projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`);
2840
3598
  return {
2841
- method: "PUT",
2842
- endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}`),
2843
- identifiers: { id, },
2844
- payload: requiredPlanJsonInput(flags, entry.usage),
3599
+ exact: false,
3600
+ reason: "A fresh GET selects collection POST when absent or entity PUT when present; a confirming GET supplies the persisted hash.",
3601
+ endpoint,
3602
+ identifiers: {
3603
+ id,
3604
+ contentHash: stableHash(content),
3605
+ ...(expectHash ? { expectHash: expectHash.toLowerCase(), } : {}),
3606
+ },
3607
+ requests: [
3608
+ { method: "GET", endpoint, purpose: "resolve create/update and check --expect-hash", },
3609
+ {
3610
+ method: "POST",
3611
+ endpoint: projectEndpoint("/sql-notebooks/"),
3612
+ condition: "after not_found",
3613
+ payload: "<omitted; includes id and projectKey>",
3614
+ redactedFields: ["payload",],
3615
+ },
3616
+ {
3617
+ method: "PUT",
3618
+ endpoint,
3619
+ condition: "when present",
3620
+ payload: "<omitted>",
3621
+ redactedFields: ["payload",],
3622
+ },
3623
+ { method: "GET", endpoint, purpose: "confirm persisted hash", },
3624
+ ],
2845
3625
  };
3626
+ }
2846
3627
  case "notebook.delete-sql":
2847
3628
  return {
2848
3629
  method: "DELETE",
@@ -2851,10 +3632,13 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2851
3632
  };
2852
3633
  case "notebook.clear-sql-history":
2853
3634
  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"), },
3635
+ method: "POST",
3636
+ endpoint: projectEndpoint(`/sql-notebooks/${encodeURIComponent(id)}/history/clear`),
3637
+ identifiers: { id, },
3638
+ payload: {
3639
+ cellId: flags["cell-id"],
3640
+ numRunsToRetain: num(flags["retain"], "--retain"),
3641
+ },
2858
3642
  };
2859
3643
  case "project.create": {
2860
3644
  const settings = jsonInput(flags) ?? null;
@@ -2888,7 +3672,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2888
3672
  wait: true,
2889
3673
  })}`;
2890
3674
  const guarded = flags["if-exists"] === true
2891
- || flags["dry-run"] === true
3675
+ || executionMode(flags).dryRun
2892
3676
  || expectedProjectIncarnation !== undefined;
2893
3677
  if (!guarded) {
2894
3678
  return {
@@ -2905,7 +3689,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2905
3689
  provided: true,
2906
3690
  expectedHash: expectedProjectIncarnation,
2907
3691
  };
2908
- if (flags["dry-run"] === true) {
3692
+ if (executionMode(flags).dryRun) {
2909
3693
  return {
2910
3694
  method: "GET",
2911
3695
  endpoint: projectProbe,
@@ -3225,7 +4009,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3225
4009
  };
3226
4010
  }
3227
4011
  case "project-git.set-library": {
3228
- const targetPath = id;
4012
+ const targetPath = validateGitReferencePath(id);
3229
4013
  return {
3230
4014
  method: "PUT",
3231
4015
  endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}`),
@@ -3240,7 +4024,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3240
4024
  };
3241
4025
  }
3242
4026
  case "project-git.remove-library": {
3243
- const targetPath = id;
4027
+ const targetPath = validateGitReferencePath(id);
3244
4028
  return {
3245
4029
  method: "DELETE",
3246
4030
  endpoint: projectGitEndpoint(projectKey, `/lib-git-refs/${encodeGitReferencePath(targetPath)}${querySuffix({
@@ -3250,7 +4034,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3250
4034
  };
3251
4035
  }
3252
4036
  case "project-git.reset-library": {
3253
- const targetPath = id;
4037
+ const targetPath = validateGitReferencePath(id);
3254
4038
  return {
3255
4039
  method: "POST",
3256
4040
  endpoint: projectGitEndpoint(projectKey, "/lib-git-refs/action/reset"),
@@ -3260,7 +4044,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3260
4044
  };
3261
4045
  }
3262
4046
  case "project-git.push-library": {
3263
- const targetPath = id;
4047
+ const targetPath = validateGitReferencePath(id);
3264
4048
  const message = requiredPlanFlag(flags, "message", entry.usage);
3265
4049
  return {
3266
4050
  method: "POST",
@@ -3294,12 +4078,9 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
3294
4078
  };
3295
4079
  default:
3296
4080
  return {
3297
- method: action.startsWith("delete") || action === "abort" ? "DELETE" : "POST",
3298
- endpoint: projectKey
3299
- ? projectEndpoint(`/${resource}s/${id ? encodeURIComponent(id) : ""}`)
3300
- : undefined,
4081
+ exact: false,
4082
+ reason: `No exact offline request shape is defined for ${resource}.${action}; no endpoint was guessed.`,
3301
4083
  identifiers: id ? { id, } : undefined,
3302
- payload: jsonInput(flags),
3303
4084
  };
3304
4085
  }
3305
4086
  }
@@ -3317,7 +4098,7 @@ export function buildMutationPlan(resource, action, meta, args, flags) {
3317
4098
  asyncKind: entry.async,
3318
4099
  exitCodesOnFailure: exitCodesOnFailure(entry),
3319
4100
  idempotency: entry.idempotency,
3320
- plannedAndDryRun: flags["dry-run"] === true,
4101
+ plannedAndDryRun: executionMode(flags).dryRun,
3321
4102
  });
3322
4103
  }
3323
4104
  /**
@@ -3333,7 +4114,7 @@ export const BATCH_PLAN_EXIT_CODES = {
3333
4114
  assertionFailure: 4,
3334
4115
  };
3335
4116
  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.";
4117
+ 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
4118
  export const BATCH_HINT = 'Pass a JSON array of argv arrays, e.g. [["dataset","list"],["recipe","update","r","--data-file","p.json"]].';
3338
4119
  export const BATCH_EXAMPLE_PAYLOAD = [
3339
4120
  ["recipe", "set-payload", "compute_orders", "--file", "code.py", "--no-backup",],