dataiku-sdk 3.1.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 (58) hide show
  1. package/README.md +124 -12
  2. package/dist/build-metadata.json +1 -1
  3. package/dist/src/auth.js +1 -1
  4. package/dist/src/cli/coerce.js +11 -2
  5. package/dist/src/cli/commands/analysis.js +4 -3
  6. package/dist/src/cli/commands/api-service.js +5 -3
  7. package/dist/src/cli/commands/app.js +4 -3
  8. package/dist/src/cli/commands/bundle.js +2 -1
  9. package/dist/src/cli/commands/code-env.js +10 -9
  10. package/dist/src/cli/commands/dashboard.js +7 -6
  11. package/dist/src/cli/commands/data-quality.js +9 -8
  12. package/dist/src/cli/commands/dataset.js +9 -8
  13. package/dist/src/cli/commands/doctor.js +4 -2
  14. package/dist/src/cli/commands/flow-zone.js +9 -8
  15. package/dist/src/cli/commands/folder.js +9 -8
  16. package/dist/src/cli/commands/future.js +2 -1
  17. package/dist/src/cli/commands/insight.js +8 -7
  18. package/dist/src/cli/commands/job.js +4 -3
  19. package/dist/src/cli/commands/meaning.js +5 -4
  20. package/dist/src/cli/commands/ml-task.js +3 -2
  21. package/dist/src/cli/commands/model-evaluation-store.js +4 -3
  22. package/dist/src/cli/commands/notebook.js +11 -10
  23. package/dist/src/cli/commands/project-library.js +7 -6
  24. package/dist/src/cli/commands/project.js +3 -2
  25. package/dist/src/cli/commands/recipe.js +14 -13
  26. package/dist/src/cli/commands/saved-model.js +4 -3
  27. package/dist/src/cli/commands/scenario.js +9 -8
  28. package/dist/src/cli/commands/sql.js +3 -2
  29. package/dist/src/cli/commands/statistics.js +7 -6
  30. package/dist/src/cli/commands/variable.js +2 -1
  31. package/dist/src/cli/commands/webapp.js +2 -1
  32. package/dist/src/cli/commands/wiki.js +7 -6
  33. package/dist/src/cli/contract.d.ts +1 -1
  34. package/dist/src/cli/contract.js +100 -82
  35. package/dist/src/cli/flags.d.ts +4 -0
  36. package/dist/src/cli/flags.js +20 -1
  37. package/dist/src/cli.js +92 -68
  38. package/dist/src/client.d.ts +3 -1
  39. package/dist/src/client.js +78 -21
  40. package/dist/src/errors.js +2 -0
  41. package/dist/src/resources/folders.js +2 -1
  42. package/dist/src/resources/projects.d.ts +0 -2
  43. package/dist/src/resources/projects.js +3 -17
  44. package/dist/src/resources/sql.js +1 -1
  45. package/dist/src/skill.d.ts +13 -4
  46. package/dist/src/skill.js +75 -24
  47. package/dist/src/utils/response-file.d.ts +2 -0
  48. package/dist/src/utils/response-file.js +34 -0
  49. package/package.json +6 -4
  50. package/plugin.json +1 -1
  51. package/skills/dataiku-dss/SKILL.md +44 -224
  52. package/skills/dataiku-dss/references/app-releases.md +27 -0
  53. package/skills/dataiku-dss/references/authentication.md +39 -0
  54. package/skills/dataiku-dss/references/coding.md +8 -0
  55. package/skills/dataiku-dss/references/discovery.md +30 -0
  56. package/skills/dataiku-dss/references/flow-maps.md +12 -0
  57. package/skills/dataiku-dss/references/mutations.md +33 -0
  58. package/skills/dataiku-dss/references/troubleshooting.md +29 -0
package/README.md CHANGED
@@ -23,12 +23,99 @@ Bun is the only supported runtime and the package manager. Examples below assume
23
23
  `--no-env-file` disables Bun's automatic preloading only; the CLI still applies its documented `.env` handling unless `DATAIKU_DISABLE_ENV=1` is set.
24
24
  `dss version` reports whether the running code came from `source` or `dist`, the runtime (always `bun`), the packaged build revision when available, and `staleBuild` when that revision differs from the checkout. Release builds generate `dist/build-metadata.json`; source snapshots without Git metadata still build but report no build revision.
25
25
 
26
+ ## Self-provisioning live tests
27
+
28
+ From a checkout, `test:live` is a separate Bun script, not a `dss` command. It reads
29
+ the existing `.env` without changing it and needs project-creation access plus a
30
+ writable managed storage connection. No imported tutorials are required.
31
+
32
+ ```sh
33
+ bun run test:live setup
34
+ bun run test:live run --case core.dataset.baseline
35
+ bun run test:live run --case "core.recipe.*"
36
+ bun run test:live run # all core cases + migrated integration suites
37
+ bun run test:live run --profile ml --case "ml.*"
38
+ bun run test:live status
39
+ bun run test:live clean # delete only this lab, including its root project
40
+ # CI: setup + run + cleanup, even when cases fail
41
+ bun run test:live all
42
+ ```
43
+
44
+ `setup` is idempotent for a ready lab; `run` reuses its datasets, graph and collaboration
45
+ fixtures. Cases use temporary resources or restore baseline values. Cases accept exact
46
+ IDs, comma-separated lists, repeated `--case`, and trailing-prefix `*` matching; quote
47
+ wildcards. Selected runs omit the legacy suites. Unknown, setup-only or inactive-profile
48
+ or empty selections fail before credentials, state creation or provisioning; `--case` is valid only
49
+ for `run`/`all`. `run` refuses
50
+ an incomplete or cleaned lab rather than silently rebuilding it. Clean failed setup
51
+ before provisioning a replacement. Authentication and transport failures retain their
52
+ original errors; they are not evidence that the lab is missing.
53
+
54
+ Core fixtures include deterministic CSVs with nulls, duplicates, Unicode and quoting;
55
+ server-provisioned managed datasets; sync, Prepare, join, fuzzy-join, grouping and Python
56
+ recipes; folders, scenarios, variables, wiki, notebooks, dashboards, insights, libraries,
57
+ metrics and quality rules. Project export/import/duplicate cases use separate owned
58
+ projects. Managed dataset types, paths and recipe output schemas come from DSS rather
59
+ than tutorial-specific connection guesses.
60
+
61
+ Profiles add to core: `ml` trains and deploys a small decision tree and checks clustering;
62
+ `applications` requires `DATAIKU_LIVE_APP_TEMPLATE_ID`; `infrastructure` requires
63
+ `DATAIKU_SQL_CONNECTION` or `DATAIKU_SQL_DATASET_FULL_NAME` for read-only SQL.
64
+ The SQL case permits only the fixed, table-free `SELECT 1 AS one` probe with an
65
+ explicit target and an owned project whose incarnation is checked before execution.
66
+ It does not authorize arbitrary SQL, alternate query inputs or global mutations. Missing
67
+ prerequisites are recorded as **blocked**, never passed. Required blocked cases exit
68
+ nonzero; optional blocked cases remain visible in reports. Global administration,
69
+ external Git mutations and unsupported capabilities are not silently exercised.
70
+
71
+ For an admin-only disposable SQL target (verified on the hosted trial), create a uniquely
72
+ named connection with `POST /public/api/admin/connections/` and body
73
+ `{"name":"<unique-name>","type":"JDBC","params":{"driver":"org.sqlite.JDBC","jdbcurl":"jdbc:sqlite::memory:","properties":[]},"usableBy":"ALLOWED","allowedGroups":[],"allowWrite":false,"allowManagedDatasets":false}`.
74
+ Run `DATAIKU_SQL_CONNECTION=<name> bun run test:live all --profile infrastructure --case infrastructure.sql-select`.
75
+ In a `finally` block, verify that the connection still matches the created definition, then
76
+ remove it with `DELETE /public/api/admin/connections/<name>`. `all` cleans the owned projects,
77
+ not this separately created connection. The in-memory database creates no database file.
78
+
79
+ State is ignored by Git under `.live-tests/<server-hash>/`. Use `--state-dir PATH` for
80
+ another lab or `--manifest PATH` to select an existing manifest for `run`, `clean` or
81
+ `status` (not `setup`/`all`). Keep the manifest and
82
+ `cleanup.jsonl`: cleanup requires exact project identities and creation-incarnation
83
+ hashes, never a prefix sweep. A changed server/project identity or unconfirmed creation
84
+ fails closed; investigate its journal before manual recovery. Locks prevent concurrent
85
+ lab runs; after an uncatchable process kill, confirm its recorded process is gone before
86
+ removing a stale lock. Interrupting a run preserves the lab; `all` attempts cleanup.
87
+
88
+ Each iteration retains logs and `report.json` with cases, timing, executed actions,
89
+ capability reasons, coverage and external-project metadata integrity. Unavailable project
90
+ metadata is reported separately in `integrity.unreadable`, not as confirmed drift;
91
+ integrity remains unverified and the run fails until those reads succeed. `clean` retains
92
+ `cleanup-report.json`, including deletion failures; starting teardown invalidates lab
93
+ readiness even if cleanup only partially succeeds. If cleanup and integrity verification
94
+ both fail, the cleanup error remains primary and the report preserves both results.
95
+ Planned lifecycle commands retain ownership checks; local outputs stay inside the lab
96
+ for both `--output` and `--output-file`. Treat local reports and exported archives as private project
97
+ data. Coverage is an explicit inventory of every registered action, not a claim that
98
+ every action has a live test: unexercised actions remain **uncovered**. Add cases through
99
+ the typed catalogue in `tests/live-cases.ts`, `LiveContext.check` and guarded helpers,
100
+ then update `tests/live-coverage.ts` when the
101
+ registry changes; catalogue drift fails the offline gate. Plan/dry-run calls do not count
102
+ as executed live coverage.
103
+
104
+ Regular `bun run check` and `bun run lint` include the live runner and fixtures.
105
+ For focused verification:
106
+
107
+ ```sh
108
+ bun run check:live
109
+ bun test tests/live-context.test.ts tests/live-runner.test.ts tests/live-coverage.test.ts
110
+ ```
111
+
26
112
  ## CLI contract
27
113
 
28
114
  The complete agent-facing contract — stdout JSON discipline, stderr JSONL diagnostics, the error
29
115
  envelope, exit codes, `--fields` projection, command discovery, and planning/safety rules — is the
30
- canonical skill at [`skills/dataiku-dss/SKILL.md`](skills/dataiku-dss/SKILL.md); keep that file as
31
- the single source of truth for agent integrations and for the `dss install-skill` payload. Human
116
+ skill bundle rooted at [`skills/dataiku-dss/SKILL.md`](skills/dataiku-dss/SKILL.md). The short
117
+ entrypoint routes agents to focused references; the installed command registry remains the source
118
+ of truth for command syntax and schemas. Human
32
119
  essentials: exactly one compact JSON value on stdout per command (void success is `{ok:true}`),
33
120
  failures as one structured JSON error object on stdout with a nonzero exit code, stderr reserved
34
121
  for JSONL diagnostics only, and exit codes `0` success / `1` usage error / `2` DSS or internal
@@ -46,8 +133,7 @@ dss commands run --output commands.json
46
133
  dss agent contract --fields protocol,agentContractVersion,cli,stdio,planning,compatibility
47
134
  ```
48
135
 
49
- `dss commands run` prints the compact resource/action summary; registry entries (~220k tokens
50
- exported) never dump to stdout — they travel only via `--output PATH`. See the canonical skill for
136
+ `dss commands run` prints the compact resource/action summary; registry entries never dump to stdout — they travel only via `--output PATH`. See the canonical skill for
51
137
  the full `--fields` projection rules and planning workflow.
52
138
 
53
139
  ## Agent skill installation
@@ -61,7 +147,7 @@ dss install-skill --global --agent omp
61
147
 
62
148
  `--list-agents` only reports targetable agents; it does not write files. Auto-detection checks supported agent binaries/config directories (`claude`, `codex`, `cursor`, `pi`, `omp`). Passing `--agent NAME` forces one entry and reports `via:"flag"`.
63
149
 
64
- Project installs write `SKILL.md` under the target workspace:
150
+ Project installs write `SKILL.md` and its sibling `references/` directory under the target workspace:
65
151
 
66
152
  - Claude: `.claude/skills/dataiku-dss/SKILL.md`
67
153
  - Codex: `.codex/skills/dataiku-dss/SKILL.md`
@@ -69,16 +155,42 @@ Project installs write `SKILL.md` under the target workspace:
69
155
  - Pi: `.pi/skills/dataiku-dss/SKILL.md`
70
156
  - OMP: `.omp/skills/dataiku-dss/SKILL.md`
71
157
 
72
- Global installs write under the agent's home config path, for example OMP: `~/.omp/agent/skills/dataiku-dss/SKILL.md`.
158
+ Global installs write under the agent's home config path, for example OMP: `~/.omp/agent/skills/dataiku-dss/SKILL.md`. The reported `target` is the home directory for global installs and the resolved workspace directory for project installs; `installed[].path` names each entrypoint.
159
+
160
+ Each entry in the `installed` array reports pre-install bundle state:
161
+
162
+ - `status`: `missing` when the entrypoint is absent; `stale` when any bundled file is missing or differs; `current` when every bundled file matches.
163
+ - `changed`: whether any bundled file would (or did) change. Byte-identical files are not rewritten.
164
+ - `expectedSha256` and `actualSha256`: the canonical and existing **entrypoint** hashes. The latter is absent when the entrypoint is missing.
165
+ - `files`: each bundled file's portable `relativePath`, destination `path`, `status`, `changed`, and hashes. Missing or stale references are detected even when `SKILL.md` itself is current.
166
+
167
+ `--dry-run` and `--plan` never write. Real installs replace changed files through same-directory temporary files and rename, writing references before the entrypoint. Atomicity is per file, not a bundle-wide transaction. Unrelated files in the destination are preserved.
168
+
169
+ ### Token-efficient skill structure
170
+
171
+ The entrypoint contains bootstrap/discovery, output interpretation, and universal safety rules. Seven focused files under [references/](skills/dataiku-dss/references/) cover authentication, discovery, mutations, application releases, flow maps, coding, and troubleshooting. All are linked directly from `SKILL.md` with task-specific read conditions; agents should not preload the whole directory.
172
+
173
+ The entrypoint measures **750 o200k_base tokens**, down from **851** in the previous revision (12% less) and **4,272** before progressive disclosure. The token-budget test now caps it at **825** tokens. Claude V5 reconstruction counts fell **1,420 → 1,250** and GLM5 **858 → 752**. Safety rules and all seven reference links remain. Reference content consumes context only when read; all five agent installers ship the complete bundle.
174
+
175
+ All seven references are token-budgeted (each measured `o200k_base` baseline plus 5% headroom). Per-file baselines live in [the token-budget tests](tests/cli/agent-token-budget.test.ts); run `bun run test:tokens` to check them.
176
+
177
+ References total **4,179 → 3,545 tokens (15.2% less)**. With the 750-token entrypoint, the whole bundle totals **4,295**; this is a sum of file counts, not a recommendation to preload them. All references shrink across ten encodings; whole-bundle Claude V5 reconstruction **8,200 → 7,362**, GLM5 **4,943 → 4,305**. Runnable examples and safety conditions are retained.
178
+
179
+ This uses progressive disclosure from the [Agent Skills specification](https://agentskills.io/specification) and [Anthropic's authoring guidance](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices): concise metadata/instructions, direct relative reference links, and domain-focused documents. No scripts or assets are needed here; generated command metadata is queried from the CLI rather than duplicated in the skill.
180
+
181
+ `bun run test:tokens` enforces the pinned OpenAI `o200k_base` budgets. To also check ten native encodings (OpenAI, Claude, GLM, Qwen, DeepSeek, and Kimi), point the optional development-only gate at an OMP native module:
182
+
183
+ ```bash
184
+ DSS_TOKENIZER_MODULE=/absolute/path/to/oh-my-pi/packages/natives/native/index.js bun run test:tokens
185
+ ```
186
+
187
+ Without this variable, only the native cross-model case is skipped. A configured missing/incompatible module fails rather than estimating tokens. The cross-model gate permits 10% growth over its recorded text baselines for bootstrap and action discovery; the pinned OpenAI gate also enforces the skill ceiling above. Claude counts use ctok reconstructions, not an official tokenizer, and all counts exclude provider message/tool framing. No native dependency is added to the CLI.
73
188
 
74
- Each entry in the `installed` array reports deterministic file state:
189
+ Measured full `dataset.create` discovery versus its `usage,description,flags,examples` projection: OpenAI **1,059 → 356**, Claude V5 reconstruction **1,771 → 640**, GLM5 **1,015 → 347** tokens. Use the projection when those fields suffice; fetch the full action when schema details are needed. This is selective disclosure, not lossless compression.
75
190
 
76
- - `status`: `missing` (no file at the destination), `stale` (file present but bytes differ from the canonical skill), or `current` (byte-identical to the canonical skill).
77
- - `changed`: whether the install writes the file — `true` for `missing` and `stale`, `false` for `current`; installs skip byte-identical files and replace missing/stale files through a same-directory temporary file followed by rename.
78
- - `expectedSha256`: SHA-256 of the canonical skill bytes this SDK ships.
79
- - `actualSha256`: SHA-256 of the file currently at the destination; present only when the file exists.
191
+ Boolean flags accept bare flags or explicit `=true`/`=false` values, including aliases. Invalid boolean values fail before dispatch. Direct, batch, and local commands share execution-mode resolution; `--plan --dry-run` returns a plan with `plannedAndDryRun:true` when both modes are supported. Unreadable JSON/text payload files produce `validation_failed` usage errors (exit 1) with the flag, path, and filesystem cause.
80
192
 
81
- `--dry-run` and `--plan` never write; they report the same `status`/`changed`/hash fields so you can see which copies a real install would refresh — a `stale` copy is detected and would be refreshed, while a `current` copy is already byte-identical and needs no refresh.
193
+ For raw streaming downloads, `--request-timeout MS` bounds each pending body read, not the total transfer duration: large progressing transfers remain supported. Buffered text/JSON consumers retain their bounded body-read budget. Request retries do not replay partially consumed streams. Project, bundle, and API-service archives and managed-folder downloads are written to private temporary files beside the destination, then renamed on success. Failures leave no partial output and preserve any existing destination.
82
194
 
83
195
  ## Credentials
84
196
 
@@ -1 +1 @@
1
- {"format":1,"buildRevision":"ceeb399ea1f0d7ea078db09da940b79fb732e2ad"}
1
+ {"format":1,"buildRevision":"aa9d13ec085be8f8637265cabcc8466df4817c88"}
package/dist/src/auth.js CHANGED
@@ -1,7 +1,7 @@
1
- import { DataikuClient, } from "./client.js";
2
1
  import { DataikuError, } from "./errors.js";
3
2
  export async function validateCredentials(url, apiKey, options = {}) {
4
3
  try {
4
+ const { DataikuClient, } = await import("./client.js");
5
5
  const client = new DataikuClient({
6
6
  url,
7
7
  apiKey,
@@ -169,6 +169,15 @@ function selectSingleSource(flags, sourceFlags) {
169
169
  }
170
170
  return provided[0];
171
171
  }
172
+ function readInputText(path, flag) {
173
+ try {
174
+ return readFileSync(path, "utf-8");
175
+ }
176
+ catch (error) {
177
+ const cause = error.code;
178
+ throw new UsageError(`Could not read ${flag} file: ${path}`, "validation_failed", "Verify that the path names an existing, readable file.", { flag, path, ...(cause ? { cause, } : {}), });
179
+ }
180
+ }
172
181
  function resolveJsonSourceText(flags, kind) {
173
182
  const source = selectSingleSource(flags, JSON_SOURCE_FLAGS);
174
183
  if (source === undefined)
@@ -182,7 +191,7 @@ function resolveJsonSourceText(flags, kind) {
182
191
  if (source === "data-file") {
183
192
  const fileValue = flags["data-file"];
184
193
  if (typeof fileValue === "string") {
185
- const jsonText = readFileSync(fileValue, "utf-8");
194
+ const jsonText = readInputText(fileValue, "--data-file");
186
195
  return kind === "object"
187
196
  ? parseJsonObject(jsonText, fileValue)
188
197
  : parseJsonValue(jsonText, fileValue);
@@ -241,7 +250,7 @@ export function textInput(flags) {
241
250
  if (source === "file") {
242
251
  const fileValue = flags["file"];
243
252
  if (typeof fileValue === "string")
244
- return readFileSync(fileValue, "utf-8");
253
+ return readInputText(fileValue, "--file");
245
254
  }
246
255
  const contentValue = flags["content"];
247
256
  if (typeof contentValue === "string")
@@ -1,4 +1,5 @@
1
1
  import { requiredStringFlag, } from "../coerce.js";
2
+ import { executionMode, } from "../flags.js";
2
3
  import { readIfExists, skipResult, } from "../output.js";
3
4
  import { requireArgs, } from "../usage.js";
4
5
  export const analysisCommands = {
@@ -23,7 +24,7 @@ export const analysisCommands = {
23
24
  const inputDataset = requiredStringFlag(f, "input-dataset", usage);
24
25
  const projectKey = f["project-key"];
25
26
  const options = { inputDataset, projectKey, };
26
- if (f["dry-run"] === true) {
27
+ if (executionMode(f).dryRun) {
27
28
  return {
28
29
  dryRun: true,
29
30
  action: "create",
@@ -45,11 +46,11 @@ export const analysisCommands = {
45
46
  handler: async (c, a, f) => {
46
47
  requireArgs(a, 1, "dss analysis delete <analysisId> [--if-exists] [--dry-run] [--project-key KEY]");
47
48
  const projectKey = f["project-key"];
48
- if (f["dry-run"] === true || f["if-exists"] === true) {
49
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
49
50
  const current = await readIfExists(() => c.analyses.get(a[0], projectKey));
50
51
  if (!current)
51
52
  return skipResult("analysis", a[0], "missing");
52
- if (f["dry-run"] === true) {
53
+ if (executionMode(f).dryRun) {
53
54
  return { dryRun: true, action: "delete", resource: "analysis", id: a[0], current, };
54
55
  }
55
56
  }
@@ -1,6 +1,8 @@
1
1
  import { ClientValidationError, } from "../../errors.js";
2
+ import { writeResponseToFile, } from "../../utils/response-file.js";
2
3
  import { stableHash, } from "../../utils/stable-hash.js";
3
4
  import { requiredJsonInput, } from "../coerce.js";
5
+ import { executionMode, } from "../flags.js";
4
6
  import { requireArgs, UsageError, } from "../usage.js";
5
7
  function validateExpectHash(value) {
6
8
  if (value === undefined || value === false)
@@ -42,7 +44,7 @@ export const apiServiceCommands = {
42
44
  const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (service settings).");
43
45
  const projectKey = f["project-key"];
44
46
  const expectHash = validateExpectHash(f["expect-hash"]);
45
- if (f["dry-run"] === true) {
47
+ if (executionMode(f).dryRun) {
46
48
  const current = await c.apiServices.getSettings(a[0], projectKey);
47
49
  const currentHash = stableHash(current);
48
50
  if (expectHash !== undefined && currentHash !== expectHash.toLowerCase()) {
@@ -81,7 +83,7 @@ export const apiServiceCommands = {
81
83
  type: "STD_PREDICTION",
82
84
  modelRef: a[2],
83
85
  };
84
- if (f["dry-run"] === true)
86
+ if (executionMode(f).dryRun)
85
87
  return endpoint;
86
88
  return c.apiServices.addPredictionEndpoint(a[0], a[1], a[2], f["project-key"]);
87
89
  },
@@ -143,7 +145,7 @@ export const apiServiceCommands = {
143
145
  if (!res.body) {
144
146
  throw new Error("apiServices.downloadPackageArchive response did not include a body");
145
147
  }
146
- const bytes = await Bun.write(out, new Response(res.body), { createPath: false, });
148
+ const bytes = await writeResponseToFile(out, res);
147
149
  return { path: out, bytes, };
148
150
  },
149
151
  usage: "dss api-service download-package <serviceId> <packageId> --output PATH [--project-key KEY]",
@@ -1,6 +1,7 @@
1
1
  import { ClientValidationError, DataikuError, } from "../../errors.js";
2
2
  import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
3
3
  import { jsonInput, num, parseBooleanOption, plainRecord, requiredJsonInput, requiredStringFlag, stringField, } from "../coerce.js";
4
+ import { executionMode, } from "../flags.js";
4
5
  import { appPermissionsHash, appPermissionsVerificationError, assertAppPermissionsSnapshotBinding, buildAppPermissionsSnapshot, diffAppPermissions, readAppPermissionsSnapshot, writeAppPermissionsSnapshot, } from "../helpers/app-permissions.js";
5
6
  import { createSuccessorInstance, futureTargetIdentity, isDefinitiveRejection, preflightSuccessorInstance, requireAbsentProjectTarget, safeErrorSummary, verifyAppInstance, VISUAL_UI_GATE, } from "../helpers/app-successor.js";
6
7
  import { CommandResultFailure, } from "../output.js";
@@ -385,7 +386,7 @@ export const appCommands = {
385
386
  to: requiredStringFlag(f, "to", CREATE_SUCCESSOR_USAGE),
386
387
  ...(f["name"] !== undefined ? { name: f["name"], } : {}),
387
388
  copyPermissions: parseBooleanOption(f["copy-permissions"], "--copy-permissions") ?? false,
388
- dryRun: parseBooleanOption(f["dry-run"], "--dry-run") ?? false,
389
+ dryRun: executionMode(f).dryRun,
389
390
  timeoutMs: num(f["timeout"], "--timeout"),
390
391
  pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
391
392
  }, CREATE_SUCCESSOR_USAGE);
@@ -445,7 +446,7 @@ export const appCommands = {
445
446
  ...(version !== undefined ? { version, } : {}),
446
447
  ...(versionNotes !== undefined ? { versionNotes, } : {}),
447
448
  ...(f["expect-hash"] !== undefined ? { expectHash: f["expect-hash"], } : {}),
448
- dryRun: parseBooleanOption(f["dry-run"], "--dry-run") ?? false,
449
+ dryRun: executionMode(f).dryRun,
449
450
  }, f["project-key"]);
450
451
  if (result.outcome === "indeterminate") {
451
452
  // The PUT may or may not have landed. Fail nonzero without
@@ -756,7 +757,7 @@ export const appCommands = {
756
757
  assertAppPermissionsSnapshotBinding(snapshot, projectKey, c.getBaseUrl());
757
758
  const currentProjectIncarnationHash = await requireProjectIncarnationHash(c, projectKey);
758
759
  assertAppPermissionsSnapshotBinding(snapshot, projectKey, c.getBaseUrl(), currentProjectIncarnationHash);
759
- const dryRun = parseBooleanOption(f["dry-run"], "--dry-run") ?? false;
760
+ const dryRun = executionMode(f).dryRun;
760
761
  const before = await c.projects.getPermissions(projectKey);
761
762
  await assertProjectIncarnationHash(c, projectKey, snapshot.projectIncarnationHash);
762
763
  const beforeHash = appPermissionsHash(before);
@@ -1,3 +1,4 @@
1
+ import { writeResponseToFile, } from "../../utils/response-file.js";
1
2
  import { json, parseBooleanOption, } from "../coerce.js";
2
3
  import { requireArgs, UsageError, } from "../usage.js";
3
4
  export const bundleCommands = {
@@ -48,7 +49,7 @@ export const bundleCommands = {
48
49
  if (!res.body) {
49
50
  throw new Error("bundles.downloadExportedArchive response did not include a body");
50
51
  }
51
- const bytes = await Bun.write(out, new Response(res.body), { createPath: false, });
52
+ const bytes = await writeResponseToFile(out, res);
52
53
  return { path: out, bytes, };
53
54
  },
54
55
  usage: "dss bundle download-exported <bundleId> --output PATH [--project-key KEY]",
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, } from "node:fs";
2
2
  import { ClientValidationError, } from "../../errors.js";
3
3
  import { stableHash, } from "../../utils/stable-hash.js";
4
4
  import { json, jsonInput, num, numFlag, parseBooleanOption, } from "../coerce.js";
5
+ import { executionMode, } from "../flags.js";
5
6
  import { enqueueCliWarning, isNotFoundError, readIfExists, skipResult, } from "../output.js";
6
7
  import { requireArgs, UsageError, } from "../usage.js";
7
8
  const CODE_ENV_LANGS = ["PYTHON", "R",];
@@ -175,13 +176,13 @@ export const codeEnvCommands = {
175
176
  }
176
177
  const params = codeEnvParams(f);
177
178
  const wait = codeEnvWait(f);
178
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
179
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
179
180
  const existing = (await c.codeEnvs.list({ envLang, }))
180
181
  .find((env) => env.envName === a[1]);
181
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
182
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
182
183
  return skipResult("code-env", a[1], "exists", { envLang, current: existing, });
183
184
  }
184
- if (f["dry-run"] === true) {
185
+ if (executionMode(f).dryRun) {
185
186
  return {
186
187
  dryRun: true,
187
188
  action: "create",
@@ -224,7 +225,7 @@ export const codeEnvCommands = {
224
225
  }
225
226
  const expectHash = validateExpectHash(f["expect-hash"]);
226
227
  const definitionHash = stableHash(definition);
227
- if (f["dry-run"] === true) {
228
+ if (executionMode(f).dryRun) {
228
229
  const current = await readIfExists(() => c.codeEnvs.getDefinition(envLang, a[1]));
229
230
  const currentHash = current ? stableHash(current) : undefined;
230
231
  if (expectHash !== undefined && currentHash !== expectHash) {
@@ -260,7 +261,7 @@ export const codeEnvCommands = {
260
261
  const packages = codeEnvPackageList(f);
261
262
  const installCorePackages = parseBooleanOption(f["install-core-packages"], "--install-core-packages");
262
263
  const expectHash = validateExpectHash(f["expect-hash"]);
263
- if (f["dry-run"] === true) {
264
+ if (executionMode(f).dryRun) {
264
265
  return {
265
266
  dryRun: true,
266
267
  action: "set-packages",
@@ -297,7 +298,7 @@ export const codeEnvCommands = {
297
298
  versionToUpdate,
298
299
  wait,
299
300
  };
300
- if (f["dry-run"] === true) {
301
+ if (executionMode(f).dryRun) {
301
302
  return {
302
303
  dryRun: true,
303
304
  action: "update-packages",
@@ -320,7 +321,7 @@ export const codeEnvCommands = {
320
321
  const envLang = requireEnvLang(a[0], usage);
321
322
  const wait = codeEnvWait(f);
322
323
  const envVersion = typeof f["env-version"] === "string" ? f["env-version"] : undefined;
323
- if (f["dry-run"] === true) {
324
+ if (executionMode(f).dryRun) {
324
325
  return {
325
326
  dryRun: true,
326
327
  action: "update-images",
@@ -350,7 +351,7 @@ export const codeEnvCommands = {
350
351
  throw new UsageError("--active is required. Usage: dss code-env set-jupyter <lang> <name> --active true|false");
351
352
  }
352
353
  const wait = codeEnvWait(f);
353
- if (f["dry-run"] === true) {
354
+ if (executionMode(f).dryRun) {
354
355
  return {
355
356
  dryRun: true,
356
357
  action: "set-jupyter",
@@ -373,7 +374,7 @@ export const codeEnvCommands = {
373
374
  requireArgs(a, 2, usage);
374
375
  const envLang = requireEnvLang(a[0], usage);
375
376
  const wait = codeEnvWait(f);
376
- if (f["dry-run"] === true) {
377
+ if (executionMode(f).dryRun) {
377
378
  const current = await readIfExists(() => c.codeEnvs.get(envLang, a[1]));
378
379
  if (!current)
379
380
  return skipResult("code-env", a[1], "missing", { envLang, });
@@ -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
  }
@@ -1,5 +1,6 @@
1
1
  import { deepMerge, } from "../../utils/deep-merge.js";
2
2
  import { num, parseBooleanOption, requiredJsonInput, } from "../coerce.js";
3
+ import { executionMode, } from "../flags.js";
3
4
  import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
4
5
  import { requireArgs, UsageError, } from "../usage.js";
5
6
  /**
@@ -52,20 +53,20 @@ export const dataQualityCommands = {
52
53
  : typeof config.displayName === "string"
53
54
  ? config.displayName
54
55
  : undefined;
55
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
56
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
56
57
  const existing = identity
57
58
  ? (await c.dataQuality.listRules(a[0], pk)).find((rule) => rule.id === identity || rule.displayName === identity)
58
59
  : undefined;
59
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
60
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
60
61
  return skipResult("data-quality", identity ?? existing.id, "exists", {
61
62
  dataset: a[0],
62
63
  current: existing,
63
64
  });
64
65
  }
65
- if (f["if-not-exists"] === true && !identity && f["dry-run"] !== true) {
66
+ if (f["if-not-exists"] === true && !identity && !executionMode(f).dryRun) {
66
67
  throw new UsageError("--if-not-exists requires rule id or displayName in the rule JSON.");
67
68
  }
68
- if (f["dry-run"] === true) {
69
+ if (executionMode(f).dryRun) {
69
70
  return {
70
71
  dryRun: true,
71
72
  action: "create-rule",
@@ -97,7 +98,7 @@ export const dataQualityCommands = {
97
98
  handler: async (c, a, f) => {
98
99
  requireArgs(a, 2, "dss data-quality update-rule <dataset> <rule-id> --data JSON");
99
100
  const data = requiredJsonInput(f, "--data, --data-file, or --stdin is required.");
100
- if (f["dry-run"] === true) {
101
+ if (executionMode(f).dryRun) {
101
102
  const current = await c.dataQuality.getRule(a[0], a[1], f["project-key"]);
102
103
  const next = deepMerge(current, data);
103
104
  return {
@@ -125,11 +126,11 @@ export const dataQualityCommands = {
125
126
  handler: async (c, a, f) => {
126
127
  requireArgs(a, 2, "dss data-quality delete-rule <dataset> <rule-id>");
127
128
  const pk = f["project-key"];
128
- if (f["dry-run"] === true || f["if-exists"] === true) {
129
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
129
130
  const current = await readIfExists(() => c.dataQuality.getRule(a[0], a[1], pk));
130
131
  if (!current)
131
132
  return skipResult("data-quality", a[1], "missing", { dataset: a[0], });
132
- if (f["dry-run"] === true) {
133
+ if (executionMode(f).dryRun) {
133
134
  return {
134
135
  dryRun: true,
135
136
  action: "delete-rule",
@@ -248,7 +249,7 @@ export const dataQualityCommands = {
248
249
  projectKey: pk,
249
250
  timeoutMs: num(f["timeout"], "--timeout"),
250
251
  };
251
- if (f["dry-run"] === true) {
252
+ if (executionMode(f).dryRun) {
252
253
  const params = new URLSearchParams();
253
254
  params.set("partition", options.partition?.trim() ? options.partition : "NP");
254
255
  if (options.ruleId !== undefined)
@@ -2,6 +2,7 @@ import { buildDatasetCloneSettings, } from "../../resources/datasets.js";
2
2
  import { deepMerge, } from "../../utils/deep-merge.js";
3
3
  import { compareStrings, stableHash, } from "../../utils/stable-hash.js";
4
4
  import { jsonInput, num, schemaColumnsInput, unknownJsonInput, } from "../coerce.js";
5
+ import { executionMode, } from "../flags.js";
5
6
  import { datasetSourceSummary, } from "../helpers/dataset.js";
6
7
  import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
7
8
  import { enqueueCliWarning, readIfExists, skipResult, } from "../output.js";
@@ -195,7 +196,7 @@ export const datasetCommands = {
195
196
  requireArgs(a, 1, usage);
196
197
  const columns = schemaColumnsInput(f, usage);
197
198
  const pk = f["project-key"];
198
- if (f["dry-run"] === true) {
199
+ if (executionMode(f).dryRun) {
199
200
  const current = await c.datasets.schema(a[0], pk);
200
201
  return {
201
202
  dryRun: true,
@@ -365,13 +366,13 @@ export const datasetCommands = {
365
366
  projectKey: pk,
366
367
  };
367
368
  const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
368
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
369
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
369
370
  const list = await c.datasets.list(pk);
370
371
  const existing = list.find((d) => d.name === name);
371
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
372
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
372
373
  return skipResult("dataset", name, "exists", { current: existing, });
373
374
  }
374
- if (f["dry-run"] === true) {
375
+ if (executionMode(f).dryRun) {
375
376
  return {
376
377
  dryRun: true,
377
378
  action: "create",
@@ -410,7 +411,7 @@ export const datasetCommands = {
410
411
  const current = await c.datasets.get(a[0], pk);
411
412
  const next = buildDatasetCloneSettings(current, a[1], pk ?? c.resolveProjectKey(pk), opts);
412
413
  const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
413
- if (f["dry-run"] === true) {
414
+ if (executionMode(f).dryRun) {
414
415
  return {
415
416
  dryRun: true,
416
417
  action: "clone",
@@ -437,11 +438,11 @@ export const datasetCommands = {
437
438
  handler: async (c, a, f) => {
438
439
  requireArgs(a, 1, "dss dataset delete <name>");
439
440
  const pk = f["project-key"];
440
- if (f["dry-run"] === true || f["if-exists"] === true) {
441
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
441
442
  const current = await readIfExists(() => c.datasets.get(a[0], pk));
442
443
  if (!current)
443
444
  return skipResult("dataset", a[0], "missing");
444
- if (f["dry-run"] === true) {
445
+ if (executionMode(f).dryRun) {
445
446
  return { dryRun: true, action: "delete", resource: "dataset", name: a[0], current, };
446
447
  }
447
448
  }
@@ -460,7 +461,7 @@ export const datasetCommands = {
460
461
  throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin]");
461
462
  }
462
463
  const pk = f["project-key"];
463
- if (f["dry-run"] === true) {
464
+ if (executionMode(f).dryRun) {
464
465
  const current = await c.datasets.get(a[0], pk);
465
466
  const next = deepMerge(current, data);
466
467
  return { dryRun: true, action: "update", resource: "dataset", name: a[0], current, next, };
@@ -1,7 +1,9 @@
1
- import { runDoctor, } from "../doctor.js";
2
1
  export const doctorCommands = {
3
2
  run: {
4
- handler: async (_c, _a, f) => (await runDoctor(f)).result,
3
+ handler: async (_c, _a, f) => {
4
+ const { runDoctor, } = await import("../doctor.js");
5
+ return (await runDoctor(f)).result;
6
+ },
5
7
  usage: "dss doctor [--project-key KEY] [--capabilities] [--fast]",
6
8
  description: "Run JSON diagnostics for DSS credentials, connectivity, and project access.",
7
9
  examples: ["dss doctor", "dss doctor --project-key MYPROJ", "dss doctor --capabilities --fast",],