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
@@ -1,7 +1,34 @@
1
- import { readFileSync, } from "node:fs";
2
- import { json, jsonInput, parseBooleanOption, } from "../coerce.js";
3
- import { isNotFoundError, readIfExists, skipResult, } from "../output.js";
1
+ import { readFileSync, writeFileSync, } from "node:fs";
2
+ import { ClientValidationError, } from "../../errors.js";
3
+ import { stableHash, } from "../../utils/stable-hash.js";
4
+ import { json, jsonInput, num, numFlag, parseBooleanOption, } from "../coerce.js";
5
+ import { executionMode, } from "../flags.js";
6
+ import { enqueueCliWarning, isNotFoundError, readIfExists, skipResult, } from "../output.js";
4
7
  import { requireArgs, UsageError, } from "../usage.js";
8
+ const CODE_ENV_LANGS = ["PYTHON", "R",];
9
+ /** Validate a positional `<lang>` argument: required, must be PYTHON or R. */
10
+ function requireEnvLang(value, usage) {
11
+ if (value !== "PYTHON" && value !== "R") {
12
+ throw new UsageError(`env language must be one of ${CODE_ENV_LANGS.join(", ")}. Usage: ${usage}`, "invalid_enum", undefined, { value, allowed: CODE_ENV_LANGS, });
13
+ }
14
+ return value;
15
+ }
16
+ function definitionHashMismatch(envLang, envName, expected, current) {
17
+ return new ClientValidationError(`Code env ${envLang}/${envName} changed since the expected definition hash was captured; refusing to overwrite it.`, "validation_failed", "Re-read the definition with dss code-env get-definition (definitionHash comes from code-env get), review the diff, and retry with the fresh hash.", {
18
+ envLang,
19
+ envName,
20
+ expectedDefinitionHash: expected,
21
+ ...(current !== undefined ? { currentDefinitionHash: current, } : {}),
22
+ });
23
+ }
24
+ function validateExpectHash(value) {
25
+ if (value === undefined || value === false)
26
+ return undefined;
27
+ if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) {
28
+ throw new ClientValidationError("Expected code env definition hash must be a 64-character SHA-256 hex digest.", "validation_failed", "Use the definitionHash value returned by dss code-env get.");
29
+ }
30
+ return value;
31
+ }
5
32
  function codeEnvWait(flags) {
6
33
  return flags["no-wait"] !== true;
7
34
  }
@@ -33,73 +60,155 @@ function codeEnvPackageList(flags) {
33
60
  }
34
61
  export const codeEnvCommands = {
35
62
  list: {
36
- handler: (c, _a, f) => c.codeEnvs.list({
37
- envLang: f["lang"],
38
- }),
39
- usage: "dss code-env list [--lang LANG]",
40
- description: "List code environments.",
63
+ handler: (c, _a, f) => {
64
+ const usage = "dss code-env list [--lang PYTHON|R]";
65
+ const lang = f["lang"];
66
+ return c.codeEnvs.list({
67
+ envLang: typeof lang === "string"
68
+ ? requireEnvLang(lang, usage)
69
+ : undefined,
70
+ });
71
+ },
72
+ usage: "dss code-env list [--lang PYTHON|R]",
73
+ description: "List code environments, optionally filtered to one language.",
41
74
  examples: ["dss code-env list", "dss code-env list --lang PYTHON",],
42
75
  },
43
76
  get: {
44
77
  handler: (c, a) => {
45
- requireArgs(a, 2, "dss code-env get <lang> <name>");
46
- return c.codeEnvs.get(a[0], a[1]);
78
+ const usage = "dss code-env get <lang> <name>";
79
+ requireArgs(a, 2, usage);
80
+ const envLang = requireEnvLang(a[0], usage);
81
+ return c.codeEnvs.get(envLang, a[1]);
47
82
  },
48
83
  usage: "dss code-env get <lang> <name>",
49
- description: "Get code environment details.",
84
+ description: "Get code environment details: requested vs installed packages, Python interpreter, deployment mode, and the stable definition hash (definitionHash) used by set-definition --expect-hash.",
50
85
  examples: ["dss code-env get PYTHON my_env",],
51
86
  },
52
87
  "get-definition": {
53
88
  handler: (c, a) => {
54
- requireArgs(a, 2, "dss code-env get-definition <lang> <name>");
55
- return c.codeEnvs.getDefinition(a[0], a[1]);
89
+ const usage = "dss code-env get-definition <lang> <name>";
90
+ requireArgs(a, 2, usage);
91
+ const envLang = requireEnvLang(a[0], usage);
92
+ return c.codeEnvs.getDefinition(envLang, a[1]);
56
93
  },
57
94
  usage: "dss code-env get-definition <lang> <name>",
58
- description: "Get raw code environment definition.",
95
+ description: "Get the raw code environment definition exactly as DSS stores it; capture its hash (definitionHash from code-env get) before replacing it with set-definition --expect-hash.",
59
96
  examples: ["dss code-env get-definition PYTHON my_env",],
60
97
  },
98
+ "list-logs": {
99
+ handler: (c, a) => {
100
+ const usage = "dss code-env list-logs <lang> <name>";
101
+ requireArgs(a, 2, usage);
102
+ const envLang = requireEnvLang(a[0], usage);
103
+ return c.codeEnvs.listLogs(envLang, a[1]);
104
+ },
105
+ usage: "dss code-env list-logs <lang> <name>",
106
+ description: "List the build log files DSS keeps for a code environment.",
107
+ examples: ["dss code-env list-logs PYTHON my_env",],
108
+ },
109
+ "get-log": {
110
+ handler: async (c, a, f) => {
111
+ const usage = "dss code-env get-log <lang> <name> <logName> [--max-lines N|--max-log-lines N] [--max-log-bytes N] [--output PATH]";
112
+ requireArgs(a, 3, usage);
113
+ const envLang = requireEnvLang(a[0], usage);
114
+ const maxLines = numFlag(f, ["max-lines", "max-log-lines",]);
115
+ const maxBytes = num(f["max-log-bytes"], "--max-log-bytes");
116
+ const result = await c.codeEnvs.getLog(envLang, a[1], a[2], {
117
+ ...(maxLines !== undefined ? { maxLines, } : {}),
118
+ ...(maxBytes !== undefined ? { maxBytes, } : {}),
119
+ });
120
+ const output = f["output"];
121
+ if (typeof output === "string" && output.trim().length > 0) {
122
+ writeFileSync(output, result.log, "utf-8");
123
+ if (result.truncated || result.tailed) {
124
+ const limits = [
125
+ result.tailed ? `tail limited to ${maxLines ?? 500} lines` : undefined,
126
+ result.truncated ? "byte cap reached" : undefined,
127
+ ].filter((part) => part !== undefined);
128
+ enqueueCliWarning({
129
+ code: "code_env_log_truncated",
130
+ message: `Code env ${envLang}/${a[1]} log '${a[2]}' was truncated (${limits.join("; ")},). Pass --max-log-bytes 0 and --max-lines 0 for the full log, or refetch with higher limits.`,
131
+ envLang,
132
+ envName: a[1],
133
+ logName: a[2],
134
+ truncated: result.truncated,
135
+ tailed: result.tailed,
136
+ });
137
+ }
138
+ return {
139
+ path: output,
140
+ bytes: result.bytes,
141
+ truncated: result.truncated,
142
+ tailed: result.tailed,
143
+ envLang,
144
+ envName: a[1],
145
+ logName: a[2],
146
+ };
147
+ }
148
+ return { ...result, envLang, envName: a[1], logName: a[2], };
149
+ },
150
+ usage: "dss code-env get-log <lang> <name> <logName> [--max-lines N|--max-log-lines N] [--max-log-bytes N] [--output PATH]",
151
+ description: "Fetch one code-env build log, bounded for safe output: the last --max-lines lines (default 500, 0 for all) and at most --max-log-bytes bytes (default 10 MiB, 0 for all). --output PATH writes the kept content to a file; stdout then carries the path metadata. A truncation warning names which cap hit.",
152
+ examples: [
153
+ "dss code-env get-log PYTHON my_env install.log --max-lines 200",
154
+ "dss code-env get-log PYTHON my_env install.log --output ./install.log --max-log-bytes 0",
155
+ ],
156
+ },
157
+ version: {
158
+ handler: (c, a) => {
159
+ const usage = "dss code-env version <lang> <name> <projectKey>";
160
+ requireArgs(a, 3, usage);
161
+ const envLang = requireEnvLang(a[0], usage);
162
+ return c.codeEnvs.getVersionForProject(envLang, a[1], a[2]);
163
+ },
164
+ usage: "dss code-env version <lang> <name> <projectKey>",
165
+ description: "Resolve the code environment version a project uses (versioned automation environments); empty version for unversioned environments.",
166
+ examples: ["dss code-env version PYTHON my_env MY_PROJ",],
167
+ },
61
168
  create: {
62
169
  handler: async (c, a, f) => {
63
- requireArgs(a, 2, "dss code-env create <lang> <name> --deployment-mode MODE");
170
+ const usage = "dss code-env create <lang> <name> --deployment-mode MODE";
171
+ requireArgs(a, 2, usage);
172
+ const envLang = requireEnvLang(a[0], usage);
64
173
  const deploymentMode = f["deployment-mode"];
65
174
  if (!deploymentMode) {
66
175
  throw new UsageError("--deployment-mode is required. Usage: dss code-env create <lang> <name> --deployment-mode MODE");
67
176
  }
68
177
  const params = codeEnvParams(f);
69
178
  const wait = codeEnvWait(f);
70
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
71
- const existing = (await c.codeEnvs.list({ envLang: a[0], }))
179
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
180
+ const existing = (await c.codeEnvs.list({ envLang, }))
72
181
  .find((env) => env.envName === a[1]);
73
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
74
- return skipResult("code-env", a[1], "exists", { envLang: a[0], current: existing, });
182
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
183
+ return skipResult("code-env", a[1], "exists", { envLang, current: existing, });
75
184
  }
76
- if (f["dry-run"] === true) {
185
+ if (executionMode(f).dryRun) {
77
186
  return {
78
187
  dryRun: true,
79
188
  action: "create",
80
189
  resource: "code-env",
81
- envLang: a[0],
190
+ envLang,
82
191
  envName: a[1],
83
192
  payload: {
84
193
  deploymentMode,
85
194
  params,
86
- wait,
87
195
  },
196
+ wait,
88
197
  ...(existing ? { current: existing, } : {}),
89
198
  };
90
199
  }
91
200
  }
92
201
  const created = await c.codeEnvs.create({
93
- envLang: a[0],
202
+ envLang,
94
203
  envName: a[1],
95
204
  deploymentMode,
96
205
  params,
97
206
  wait,
98
207
  });
99
- return { created: a[1], resource: "code-env", envLang: a[0], ...created, };
208
+ return { created: a[1], resource: "code-env", envLang, ...created, };
100
209
  },
101
210
  usage: "dss code-env create <lang> <name> --deployment-mode MODE [--params JSON|--data JSON|--data-file PATH|--stdin] [--python-interpreter PYTHON311] [--no-wait] [--if-not-exists] [--dry-run]",
102
- description: "Create a code environment.",
211
+ description: "Create a code environment; --no-wait hands back a DSS future payload you can settle with dss future wait.",
103
212
  examples: [
104
213
  "dss code-env create PYTHON my_env --deployment-mode DESIGN_MANAGED --python-interpreter PYTHON311",
105
214
  'dss code-env create PYTHON my_env --deployment-mode DESIGN_MANAGED --params \'{"pythonInterpreter":"PYTHON311"}\' --dry-run',
@@ -107,47 +216,67 @@ export const codeEnvCommands = {
107
216
  },
108
217
  "set-definition": {
109
218
  handler: async (c, a, f) => {
110
- requireArgs(a, 2, "dss code-env set-definition <lang> <name> --data JSON");
219
+ const usage = "dss code-env set-definition <lang> <name> (--data JSON|--data-file PATH|--stdin) [--expect-hash SHA256]";
220
+ requireArgs(a, 2, usage);
221
+ const envLang = requireEnvLang(a[0], usage);
111
222
  const definition = jsonInput(f);
112
223
  if (!definition) {
113
224
  throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss code-env set-definition <lang> <name> --data JSON");
114
225
  }
115
- if (f["dry-run"] === true) {
226
+ const expectHash = validateExpectHash(f["expect-hash"]);
227
+ const definitionHash = stableHash(definition);
228
+ if (executionMode(f).dryRun) {
229
+ const current = await readIfExists(() => c.codeEnvs.getDefinition(envLang, a[1]));
230
+ const currentHash = current ? stableHash(current) : undefined;
231
+ if (expectHash !== undefined && currentHash !== expectHash) {
232
+ throw definitionHashMismatch(envLang, a[1], expectHash, currentHash);
233
+ }
116
234
  return {
117
235
  dryRun: true,
118
236
  action: "set-definition",
119
237
  resource: "code-env",
120
- envLang: a[0],
238
+ envLang,
121
239
  envName: a[1],
122
240
  definition,
241
+ definitionHash,
242
+ ...(currentHash !== undefined
243
+ ? { currentDefinitionHash: currentHash, changed: currentHash !== definitionHash, }
244
+ : {}),
245
+ ...(expectHash !== undefined ? { expectHash, provenanceVerified: true, } : {}),
123
246
  };
124
247
  }
125
- return c.codeEnvs.setDefinition(a[0], a[1], definition);
248
+ return c.codeEnvs.setDefinition(envLang, a[1], definition, { expectHash, });
126
249
  },
127
- usage: "dss code-env set-definition <lang> <name> (--data JSON|--data-file PATH|--stdin) [--dry-run]",
128
- description: "Replace a code environment definition previously fetched from DSS.",
129
- examples: ["dss code-env set-definition PYTHON my_env --data-file code-env.json --dry-run",],
250
+ usage: "dss code-env set-definition <lang> <name> (--data JSON|--data-file PATH|--stdin) [--expect-hash SHA256] [--dry-run]",
251
+ description: "Replace a code environment definition previously fetched from DSS (get-definition). --expect-hash SHA256 refuses the PUT unless the current definition still hashes to the hash captured with code-env get, so concurrent edits are never clobbered; --dry-run also fetches the current definition and reports both hashes plus a changed flag.",
252
+ examples: [
253
+ "dss code-env set-definition PYTHON my_env --data-file code-env.json --expect-hash $HASH --dry-run",
254
+ ],
130
255
  },
131
256
  "set-packages": {
132
257
  handler: async (c, a, f) => {
133
- requireArgs(a, 2, "dss code-env set-packages <lang> <name> --packages PKGS");
258
+ const usage = "dss code-env set-packages <lang> <name> (--packages PKGS|--package PKG|--file PATH)";
259
+ requireArgs(a, 2, usage);
260
+ const envLang = requireEnvLang(a[0], usage);
134
261
  const packages = codeEnvPackageList(f);
135
262
  const installCorePackages = parseBooleanOption(f["install-core-packages"], "--install-core-packages");
136
- if (f["dry-run"] === true) {
263
+ const expectHash = validateExpectHash(f["expect-hash"]);
264
+ if (executionMode(f).dryRun) {
137
265
  return {
138
266
  dryRun: true,
139
267
  action: "set-packages",
140
268
  resource: "code-env",
141
- envLang: a[0],
269
+ envLang,
142
270
  envName: a[1],
143
271
  packages,
144
272
  installCorePackages,
273
+ ...(expectHash !== undefined ? { expectHash, } : {}),
145
274
  };
146
275
  }
147
- return c.codeEnvs.setPackages(a[0], a[1], packages, { installCorePackages, });
276
+ return c.codeEnvs.setPackages(envLang, a[1], packages, { installCorePackages, expectHash, });
148
277
  },
149
- usage: "dss code-env set-packages <lang> <name> (--packages PKGS|--package PKG|--file PATH) [--install-core-packages true|false] [--dry-run]",
150
- description: "Update requested package specs without rebuilding packages.",
278
+ usage: "dss code-env set-packages <lang> <name> (--packages PKGS|--package PKG|--file PATH) [--install-core-packages true|false] [--expect-hash SHA256] [--dry-run]",
279
+ description: "Fetch the current definition, merge the requested package specs (and --install-core-packages) into it, then PUT the merged definition, so fields outside the package list are never dropped; --expect-hash refuses the merge when DSS changed since the hash was captured.",
151
280
  examples: [
152
281
  "dss code-env set-packages PYTHON my_env --packages 'tabulate\\nnameparser' --dry-run",
153
282
  "dss code-env set-packages PYTHON my_env --file requirements.txt",
@@ -155,7 +284,9 @@ export const codeEnvCommands = {
155
284
  },
156
285
  "update-packages": {
157
286
  handler: async (c, a, f) => {
158
- requireArgs(a, 2, "dss code-env update-packages <lang> <name>");
287
+ const usage = "dss code-env update-packages <lang> <name>";
288
+ requireArgs(a, 2, usage);
289
+ const envLang = requireEnvLang(a[0], usage);
159
290
  const wait = codeEnvWait(f);
160
291
  const versionToUpdate = typeof f["env-version"] === "string"
161
292
  ? f["env-version"]
@@ -167,77 +298,108 @@ export const codeEnvCommands = {
167
298
  versionToUpdate,
168
299
  wait,
169
300
  };
170
- if (f["dry-run"] === true) {
301
+ if (executionMode(f).dryRun) {
171
302
  return {
172
303
  dryRun: true,
173
304
  action: "update-packages",
174
305
  resource: "code-env",
175
- envLang: a[0],
306
+ envLang,
176
307
  envName: a[1],
177
308
  ...opts,
178
309
  };
179
310
  }
180
- return c.codeEnvs.updatePackages(a[0], a[1], opts);
311
+ return c.codeEnvs.updatePackages(envLang, a[1], opts);
181
312
  },
182
313
  usage: "dss code-env update-packages <lang> <name> [--force-rebuild] [--env-version VERSION] [--no-wait] [--dry-run]",
183
- description: "Rebuild or update code environment packages to match the requested specs.",
314
+ description: "Rebuild or update code environment packages to match the requested specs; --no-wait hands back a DSS future payload you can settle with dss future wait.",
184
315
  examples: ["dss code-env update-packages PYTHON my_env --force-rebuild --dry-run",],
185
316
  },
317
+ "update-images": {
318
+ handler: async (c, a, f) => {
319
+ const usage = "dss code-env update-images <lang> <name>";
320
+ requireArgs(a, 2, usage);
321
+ const envLang = requireEnvLang(a[0], usage);
322
+ const wait = codeEnvWait(f);
323
+ const envVersion = typeof f["env-version"] === "string" ? f["env-version"] : undefined;
324
+ if (executionMode(f).dryRun) {
325
+ return {
326
+ dryRun: true,
327
+ action: "update-images",
328
+ resource: "code-env",
329
+ envLang,
330
+ envName: a[1],
331
+ envVersion,
332
+ wait,
333
+ };
334
+ }
335
+ return c.codeEnvs.updateImages(envLang, a[1], { envVersion, wait, });
336
+ },
337
+ usage: "dss code-env update-images <lang> <name> [--env-version VERSION] [--no-wait] [--dry-run]",
338
+ description: "Rebuild the Docker image(s) of a code environment to match its settings; --env-version targets one version of a versioned environment and --no-wait hands back a DSS future payload you can settle with dss future wait.",
339
+ examples: [
340
+ "dss code-env update-images PYTHON my_env --dry-run",
341
+ "dss code-env update-images PYTHON my_env --env-version v3 --no-wait",
342
+ ],
343
+ },
186
344
  "set-jupyter": {
187
345
  handler: async (c, a, f) => {
188
- requireArgs(a, 2, "dss code-env set-jupyter <lang> <name> --active true|false");
346
+ const usage = "dss code-env set-jupyter <lang> <name> --active true|false";
347
+ requireArgs(a, 2, usage);
348
+ const envLang = requireEnvLang(a[0], usage);
189
349
  const active = parseBooleanOption(f["active"], "--active");
190
350
  if (active === undefined) {
191
351
  throw new UsageError("--active is required. Usage: dss code-env set-jupyter <lang> <name> --active true|false");
192
352
  }
193
353
  const wait = codeEnvWait(f);
194
- if (f["dry-run"] === true) {
354
+ if (executionMode(f).dryRun) {
195
355
  return {
196
356
  dryRun: true,
197
357
  action: "set-jupyter",
198
358
  resource: "code-env",
199
- envLang: a[0],
359
+ envLang,
200
360
  envName: a[1],
201
361
  active,
202
362
  wait,
203
363
  };
204
364
  }
205
- return c.codeEnvs.setJupyterSupport(a[0], a[1], active, { wait, });
365
+ return c.codeEnvs.setJupyterSupport(envLang, a[1], active, { wait, });
206
366
  },
207
367
  usage: "dss code-env set-jupyter <lang> <name> --active true|false [--no-wait] [--dry-run]",
208
- description: "Enable or disable Jupyter support for a code environment.",
368
+ description: "Enable or disable Jupyter support for a code environment; --no-wait hands back a DSS future payload you can settle with dss future wait.",
209
369
  examples: ["dss code-env set-jupyter PYTHON my_env --active true --dry-run",],
210
370
  },
211
371
  delete: {
212
372
  handler: async (c, a, f) => {
213
- requireArgs(a, 2, "dss code-env delete <lang> <name>");
373
+ const usage = "dss code-env delete <lang> <name>";
374
+ requireArgs(a, 2, usage);
375
+ const envLang = requireEnvLang(a[0], usage);
214
376
  const wait = codeEnvWait(f);
215
- if (f["dry-run"] === true) {
216
- const current = await readIfExists(() => c.codeEnvs.get(a[0], a[1]));
377
+ if (executionMode(f).dryRun) {
378
+ const current = await readIfExists(() => c.codeEnvs.get(envLang, a[1]));
217
379
  if (!current)
218
- return skipResult("code-env", a[1], "missing", { envLang: a[0], });
380
+ return skipResult("code-env", a[1], "missing", { envLang, });
219
381
  return {
220
382
  dryRun: true,
221
383
  action: "delete",
222
384
  resource: "code-env",
223
- envLang: a[0],
385
+ envLang,
224
386
  envName: a[1],
225
387
  wait,
226
388
  current,
227
389
  };
228
390
  }
229
391
  if (f["if-exists"] === true) {
230
- return await c.codeEnvs.delete(a[0], a[1], { wait, }).catch((error) => {
392
+ return await c.codeEnvs.delete(envLang, a[1], { wait, }).catch((error) => {
231
393
  if (isNotFoundError(error)) {
232
- return skipResult("code-env", a[1], "missing", { envLang: a[0], });
394
+ return skipResult("code-env", a[1], "missing", { envLang, });
233
395
  }
234
396
  throw error;
235
397
  });
236
398
  }
237
- return c.codeEnvs.delete(a[0], a[1], { wait, });
399
+ return c.codeEnvs.delete(envLang, a[1], { wait, });
238
400
  },
239
401
  usage: "dss code-env delete <lang> <name> [--no-wait] [--if-exists] [--dry-run]",
240
- description: "Delete a code environment.",
402
+ description: "Delete a code environment; --no-wait hands back a DSS future payload you can settle with dss future wait.",
241
403
  examples: ["dss code-env delete PYTHON my_env --dry-run",],
242
404
  },
243
405
  usages: {
@@ -1,2 +1,21 @@
1
1
  import type { CommandMeta } from "../types.js";
2
+ export interface CodeInputSource {
3
+ kind: "file" | "stdin";
4
+ path?: string;
5
+ }
6
+ export interface ResolvedCodeInput {
7
+ script: string;
8
+ source: CodeInputSource;
9
+ }
10
+ /**
11
+ * Canonical validation of `code run` input: rejects positional args and
12
+ * conflicting sources, reads the chosen source, strips a UTF-8 BOM, and
13
+ * rejects empty scripts. Returns the script plus source metadata so plan or
14
+ * replay payloads can hash the script and reference the source without
15
+ * embedding its contents.
16
+ */
17
+ export declare function resolveCodeInputWithSource(args: string[], flags: Record<string, string | boolean>): ResolvedCodeInput;
18
+ export declare function resolveCodeInput(args: string[], flags: Record<string, string | boolean>): string;
19
+ /** Parse an integer flag value, rejecting negative and fractional values before any DSS request. */
20
+ export declare function parseCodeRunIntegerFlag(value: string | boolean | undefined, flagName: string): number | undefined;
2
21
  export declare const codeCommands: Record<string, CommandMeta>;
@@ -1,17 +1,44 @@
1
1
  import { readFileSync, } from "node:fs";
2
+ import { ScenarioScriptRunWithCleanupFailureError, } from "../../resources/scenarios.js";
2
3
  import { num, readStdinText, stripUtf8Bom, } from "../coerce.js";
4
+ import { enqueueCliWarning, } from "../output.js";
3
5
  import { UsageError, } from "../usage.js";
4
- const CODE_RUN_USAGE = "dss code run (--file PATH | --stdin) [--env ENV] [--timeout MS] [--keep] [--full-log] [--max-log-bytes N] [--project-key KEY]";
5
- function resolveCodeInput(args, flags) {
6
+ const CODE_RUN_USAGE = "dss code run (--file PATH | --stdin) [--env ENV] [--timeout MS (default 120000)] [--keep] [--full-log] [--max-log-bytes N (default 1048576)] [--project-key KEY]";
7
+ function previewFlagValue(value) {
8
+ const text = typeof value === "string" ? value : String(value);
9
+ return text.length <= 32 ? text : `${text.slice(0, 32)}…`;
10
+ }
11
+ /** Read a local Python source file, mapping read failures to usage validation. */
12
+ function readCodeSourceFile(filePath) {
13
+ try {
14
+ return readFileSync(filePath, "utf-8");
15
+ }
16
+ catch (error) {
17
+ throw new UsageError(`Could not read --file ${filePath}: ${error instanceof Error ? error.message : String(error)}.`, "not_found", "Verify the file path and that it is readable.");
18
+ }
19
+ }
20
+ /**
21
+ * Canonical validation of `code run` input: rejects positional args and
22
+ * conflicting sources, reads the chosen source, strips a UTF-8 BOM, and
23
+ * rejects empty scripts. Returns the script plus source metadata so plan or
24
+ * replay payloads can hash the script and reference the source without
25
+ * embedding its contents.
26
+ */
27
+ export function resolveCodeInputWithSource(args, flags) {
6
28
  if (args.length > 0) {
7
29
  throw new UsageError(`code run takes no positional arguments; pass the script via --file PATH or --stdin. Usage: ${CODE_RUN_USAGE}`);
8
30
  }
9
31
  const sources = [];
10
32
  if (typeof flags["file"] === "string") {
11
- sources.push({ label: "--file", read: () => readFileSync(flags["file"], "utf-8"), });
33
+ sources.push({
34
+ kind: "file",
35
+ label: "--file",
36
+ path: flags["file"],
37
+ read: () => readCodeSourceFile(flags["file"]),
38
+ });
12
39
  }
13
40
  if (flags["stdin"] === true) {
14
- sources.push({ label: "--stdin", read: readStdinText, });
41
+ sources.push({ kind: "stdin", label: "--stdin", read: readStdinText, });
15
42
  }
16
43
  if (sources.length === 0) {
17
44
  throw new UsageError(`Python source is required: pass --file PATH or --stdin. Usage: ${CODE_RUN_USAGE}`);
@@ -19,23 +46,63 @@ function resolveCodeInput(args, flags) {
19
46
  if (sources.length > 1) {
20
47
  throw new UsageError(`Choose exactly one Python source: --file or --stdin. Usage: ${CODE_RUN_USAGE}`);
21
48
  }
22
- const script = stripUtf8Bom(sources[0].read());
49
+ const source = sources[0];
50
+ const script = stripUtf8Bom(source.read());
23
51
  if (script.trim().length === 0) {
24
- throw new UsageError(`Python source from ${sources[0].label} must not be empty. Usage: ${CODE_RUN_USAGE}`);
52
+ throw new UsageError(`Python source from ${source.label} must not be empty. Usage: ${CODE_RUN_USAGE}`);
53
+ }
54
+ return {
55
+ script,
56
+ source: source.kind === "file" ? { kind: "file", path: source.path, } : { kind: "stdin", },
57
+ };
58
+ }
59
+ export function resolveCodeInput(args, flags) {
60
+ return resolveCodeInputWithSource(args, flags).script;
61
+ }
62
+ /** Parse an integer flag value, rejecting negative and fractional values before any DSS request. */
63
+ export function parseCodeRunIntegerFlag(value, flagName) {
64
+ const parsed = num(value, flagName);
65
+ if (parsed === undefined)
66
+ return undefined;
67
+ if (!Number.isInteger(parsed) || parsed < 0) {
68
+ const valuePreview = previewFlagValue(value);
69
+ throw new UsageError(`Invalid value for ${flagName}: expected a non-negative integer, got ${valuePreview}.`, "invalid_flag_value", undefined, { flag: flagName, value: valuePreview, });
25
70
  }
26
- return script;
71
+ return parsed;
72
+ }
73
+ function reportCleanupFailure(cleanupFailure) {
74
+ enqueueCliWarning({
75
+ type: "code_run_cleanup_failed",
76
+ scenarioId: cleanupFailure.scenarioId,
77
+ error: cleanupFailure.error,
78
+ });
79
+ }
80
+ function unwrapCodeRunError(error) {
81
+ if (error instanceof ScenarioScriptRunWithCleanupFailureError) {
82
+ reportCleanupFailure(error.cleanupFailure);
83
+ return error.cause;
84
+ }
85
+ return error;
27
86
  }
28
87
  export const codeCommands = {
29
88
  run: {
30
89
  handler: async (c, a, f) => {
90
+ const timeoutMs = parseCodeRunIntegerFlag(f["timeout"], "--timeout");
91
+ const maxLogBytes = parseCodeRunIntegerFlag(f["max-log-bytes"], "--max-log-bytes");
31
92
  const script = resolveCodeInput(a, f);
32
- const run = await c.scenarios.runScript(script, {
33
- envName: f["env"],
34
- projectKey: f["project-key"],
35
- timeoutMs: num(f["timeout"], "--timeout"),
36
- keepScenario: f["keep"] === true,
37
- maxLogBytes: num(f["max-log-bytes"], "--max-log-bytes"),
38
- });
93
+ let run;
94
+ try {
95
+ run = await c.scenarios.runScript(script, {
96
+ envName: f["env"],
97
+ projectKey: f["project-key"],
98
+ timeoutMs,
99
+ keepScenario: f["keep"] === true,
100
+ maxLogBytes,
101
+ });
102
+ }
103
+ catch (error) {
104
+ throw unwrapCodeRunError(error);
105
+ }
39
106
  const result = {
40
107
  outcome: run.outcome,
41
108
  success: run.success,
@@ -45,14 +112,19 @@ export const codeCommands = {
45
112
  output: run.output ?? "",
46
113
  logTruncated: run.logTruncated,
47
114
  maxLogBytes: run.maxLogBytes,
115
+ cleanup: run.cleanup,
116
+ ...(run.timedOut === true ? { timedOut: true, timeoutMs: run.timeoutMs, } : {}),
48
117
  };
49
118
  if (f["full-log"] === true || run.output === undefined) {
50
119
  result.log = run.log;
51
120
  }
121
+ if (run.cleanup.status === "failed") {
122
+ reportCleanupFailure({ scenarioId: run.scenarioId, error: run.cleanup.error ?? "unknown", });
123
+ }
52
124
  return result;
53
125
  },
54
126
  usage: CODE_RUN_USAGE,
55
- description: "Run one-off Python in a DSS code env via a throwaway custom-python scenario; returns the script's captured output (stdout+stderr) plus outcome/success. Log retrieval is capped by --max-log-bytes (default 1048576); pass --full-log to include the capped raw DSS run log. Exits 4 on a non-SUCCESS outcome.",
127
+ description: "Run Python through a throwaway custom-python scenario and return captured output plus outcome. --timeout caps waiting; --max-log-bytes caps logs; --full-log returns the capped raw log. Exits 4 unless outcome is SUCCESS.",
56
128
  examples: [
57
129
  "dss code run --file inspect.py",
58
130
  "dss code run --file inspect.py --env py39_pandas",
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import { ClientValidationError, } from "../../errors.js";
3
3
  import { deepMerge, } from "../../utils/deep-merge.js";
4
4
  import { jsonInput, parseBooleanOption, } from "../coerce.js";
5
+ import { executionMode, } from "../flags.js";
5
6
  import { readIfExists, skipResult, } from "../output.js";
6
7
  import { requireArgs, UsageError, } from "../usage.js";
7
8
  export const dashboardCommands = {
@@ -106,12 +107,12 @@ export const dashboardCommands = {
106
107
  if (listed !== undefined)
107
108
  payload.listed = listed;
108
109
  const pk = f["project-key"];
109
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
110
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
110
111
  const existing = (await c.dashboards.list(pk)).find((dashboard) => dashboard.name === name);
111
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
112
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
112
113
  return skipResult("dashboard", existing.id, "exists", { current: existing, });
113
114
  }
114
- if (f["dry-run"] === true) {
115
+ if (executionMode(f).dryRun) {
115
116
  return {
116
117
  dryRun: true,
117
118
  action: "create",
@@ -146,7 +147,7 @@ export const dashboardCommands = {
146
147
  if (name === undefined && listed === undefined && !data) {
147
148
  throw new UsageError("--name, --listed, --data, --data-file, or --stdin is required.");
148
149
  }
149
- if (f["dry-run"] === true) {
150
+ if (executionMode(f).dryRun) {
150
151
  const current = await c.dashboards.get(a[0], f["project-key"]);
151
152
  const next = deepMerge(current, data ?? {});
152
153
  if (name !== undefined)
@@ -170,11 +171,11 @@ export const dashboardCommands = {
170
171
  handler: async (c, a, f) => {
171
172
  requireArgs(a, 1, "dss dashboard delete <id>");
172
173
  const pk = f["project-key"];
173
- if (f["dry-run"] === true || f["if-exists"] === true) {
174
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
174
175
  const current = await readIfExists(() => c.dashboards.get(a[0], pk));
175
176
  if (!current)
176
177
  return skipResult("dashboard", a[0], "missing");
177
- if (f["dry-run"] === true) {
178
+ if (executionMode(f).dryRun) {
178
179
  return { dryRun: true, action: "delete", resource: "dashboard", id: a[0], current, };
179
180
  }
180
181
  }