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,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,
@@ -314,6 +315,7 @@ export const datasetCommands = {
314
315
  outputPath: f["output"],
315
316
  projectKey: f["project-key"],
316
317
  limit: num(f["limit"], "--limit"),
318
+ rawData: f["raw-data"] === true,
317
319
  });
318
320
  if (result.truncated) {
319
321
  enqueueCliWarning({
@@ -337,9 +339,13 @@ export const datasetCommands = {
337
339
  }
338
340
  return result;
339
341
  },
340
- usage: "dss dataset download <name> [--output PATH] [--limit N] [--project-key KEY]",
341
- description: "Download up to --limit rows (default 100k) as CSV; returns { path, rows, truncated, limit }. When truncated, a dataset_download_truncated warning is written to stderr; when --output is omitted, a dataset_download_default_location warning names the file's path in the working directory so the write is never silent.",
342
- examples: ["dss dataset download orders", "dss dataset download orders --output ./data/",],
342
+ usage: "dss dataset download <name> [--output PATH] [--limit N] [--raw-data] [--project-key KEY]",
343
+ description: "Download up to --limit rows (default 100k) as CSV and return { path, rows, truncated, limit }. Formula-like cells are neutralized for spreadsheets; --raw-data preserves exact bytes. Warnings report truncation and the default output path.",
344
+ examples: [
345
+ "dss dataset download orders",
346
+ "dss dataset download orders --output ./data/",
347
+ "dss dataset download orders --raw-data",
348
+ ],
343
349
  },
344
350
  create: {
345
351
  handler: async (c, _a, f) => {
@@ -360,13 +366,13 @@ export const datasetCommands = {
360
366
  projectKey: pk,
361
367
  };
362
368
  const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
363
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
369
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
364
370
  const list = await c.datasets.list(pk);
365
371
  const existing = list.find((d) => d.name === name);
366
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
372
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
367
373
  return skipResult("dataset", name, "exists", { current: existing, });
368
374
  }
369
- if (f["dry-run"] === true) {
375
+ if (executionMode(f).dryRun) {
370
376
  return {
371
377
  dryRun: true,
372
378
  action: "create",
@@ -405,7 +411,7 @@ export const datasetCommands = {
405
411
  const current = await c.datasets.get(a[0], pk);
406
412
  const next = buildDatasetCloneSettings(current, a[1], pk ?? c.resolveProjectKey(pk), opts);
407
413
  const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
408
- if (f["dry-run"] === true) {
414
+ if (executionMode(f).dryRun) {
409
415
  return {
410
416
  dryRun: true,
411
417
  action: "clone",
@@ -432,11 +438,11 @@ export const datasetCommands = {
432
438
  handler: async (c, a, f) => {
433
439
  requireArgs(a, 1, "dss dataset delete <name>");
434
440
  const pk = f["project-key"];
435
- if (f["dry-run"] === true || f["if-exists"] === true) {
441
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
436
442
  const current = await readIfExists(() => c.datasets.get(a[0], pk));
437
443
  if (!current)
438
444
  return skipResult("dataset", a[0], "missing");
439
- if (f["dry-run"] === true) {
445
+ if (executionMode(f).dryRun) {
440
446
  return { dryRun: true, action: "delete", resource: "dataset", name: a[0], current, };
441
447
  }
442
448
  }
@@ -455,7 +461,7 @@ export const datasetCommands = {
455
461
  throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin]");
456
462
  }
457
463
  const pk = f["project-key"];
458
- if (f["dry-run"] === true) {
464
+ if (executionMode(f).dryRun) {
459
465
  const current = await c.datasets.get(a[0], pk);
460
466
  const next = deepMerge(current, data);
461
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",],
@@ -1,5 +1,7 @@
1
+ import { ClientValidationError, } from "../../errors.js";
1
2
  import { deepMerge, } from "../../utils/deep-merge.js";
2
- import { ensureFlowZonePlanTarget, findFlowZoneForPlan, flowZoneColor, flowZoneContains, flowZoneCurrentPosition, flowZoneDetailSummary, flowZoneId, flowZoneMoveItems, flowZoneName, flowZoneOrganizeStep, flowZonePlanItemKeys, flowZonePruneItems, flowZoneSamePosition, flowZoneSummary, readFlowZoneOrganizePlan, resolveFlowZoneIdFromFlags, throwFlowZoneValidationError, validateFlowZoneOrganizeObjects, } from "../helpers/flow-zone.js";
3
+ import { executionMode, } from "../flags.js";
4
+ import { ensureFlowZonePlanTarget, findFlowZoneForPlan, flowZoneColor, flowZoneContains, flowZoneCurrentPosition, flowZoneDetailSummary, flowZoneId, flowZoneMoveDelta, flowZoneMoveItems, flowZoneName, flowZoneOrganizeStep, flowZonePlanFromZones, flowZonePlanItemKeys, flowZonePruneItems, flowZoneSamePosition, flowZoneSummary, readFlowZoneOrganizePlan, resolveFlowZoneIdFromFlags, throwFlowZoneValidationError, validateFlowZoneOrganizeObjects, } from "../helpers/flow-zone.js";
3
5
  import { readIfExists, skipResult, } from "../output.js";
4
6
  import { requireArgs, UsageError, } from "../usage.js";
5
7
  export const flowZoneCommands = {
@@ -62,13 +64,13 @@ export const flowZoneCommands = {
62
64
  color: flowZoneColor(f["color"]),
63
65
  projectKey: pk,
64
66
  };
65
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
67
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
66
68
  const list = await c.flowZones.list(pk);
67
69
  const existing = list.find((zone) => zone.name === name);
68
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
70
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
69
71
  return skipResult("flow-zone", existing.id, "exists", { current: existing, });
70
72
  }
71
- if (f["dry-run"] === true) {
73
+ if (executionMode(f).dryRun) {
72
74
  return {
73
75
  dryRun: true,
74
76
  action: "create",
@@ -102,7 +104,7 @@ export const flowZoneCommands = {
102
104
  color: flowZoneColor(f["color"]),
103
105
  projectKey: pk,
104
106
  };
105
- if (f["dry-run"] === true) {
107
+ if (executionMode(f).dryRun) {
106
108
  const current = await c.flowZones.get(zoneId, pk);
107
109
  const next = deepMerge(current, patch);
108
110
  return { dryRun: true, action: "update", resource: "flow-zone", id: zoneId, current, next, };
@@ -117,11 +119,11 @@ export const flowZoneCommands = {
117
119
  handler: async (c, a, f) => {
118
120
  requireArgs(a, 1, "dss flow-zone delete <id>");
119
121
  const zoneId = flowZoneId(a[0]);
120
- if (f["dry-run"] === true || f["if-exists"] === true) {
122
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
121
123
  const current = await readIfExists(() => c.flowZones.get(zoneId, f["project-key"]));
122
124
  if (!current)
123
125
  return skipResult("flow-zone", zoneId, "missing");
124
- if (f["dry-run"] === true) {
126
+ if (executionMode(f).dryRun) {
125
127
  return { dryRun: true, action: "delete", resource: "flow-zone", id: zoneId, current, };
126
128
  }
127
129
  }
@@ -146,7 +148,7 @@ export const flowZoneCommands = {
146
148
  if (items.length === 0) {
147
149
  throw new UsageError("At least one object is required. Use --dataset, --recipe, --folder, or --object TYPE:ID.");
148
150
  }
149
- if (f["dry-run"] === true) {
151
+ if (executionMode(f).dryRun) {
150
152
  return {
151
153
  dryRun: true,
152
154
  action: "move",
@@ -166,6 +168,19 @@ export const flowZoneCommands = {
166
168
  "dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
167
169
  ],
168
170
  },
171
+ plan: {
172
+ handler: async (c, _a, f) => {
173
+ const pk = f["project-key"];
174
+ const [zones, topologyFingerprint,] = await Promise.all([
175
+ c.flowZones.list(pk),
176
+ c.projects.topologyFingerprint(pk),
177
+ ]);
178
+ return flowZonePlanFromZones(zones, topologyFingerprint);
179
+ },
180
+ usage: "dss flow-zone plan [--project-key KEY]",
181
+ description: "Export current visual flow-zone organization as an organize-compatible plan.",
182
+ examples: ["dss flow-zone plan --project-key MYPROJ",],
183
+ },
169
184
  organize: {
170
185
  handler: async (c, _a, f) => {
171
186
  const usage = "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]";
@@ -173,7 +188,13 @@ export const flowZoneCommands = {
173
188
  const plan = readFlowZoneOrganizePlan(f, usage);
174
189
  const sync = f["sync"] === true;
175
190
  const validateObjects = f["validate-objects"] === true;
176
- const zones = await c.flowZones.list(pk);
191
+ const [zones, topologyFingerprint,] = await Promise.all([
192
+ c.flowZones.list(pk),
193
+ c.projects.topologyFingerprint(pk),
194
+ ]);
195
+ if (plan.topologyFingerprint && plan.topologyFingerprint !== topologyFingerprint) {
196
+ throw new ClientValidationError("Flow topology changed after this visual organization plan was generated.", "assertion_failed", "Regenerate the plan with dss flow-zone plan, then review it before applying.", { expected: plan.topologyFingerprint, actual: topologyFingerprint, });
197
+ }
177
198
  const plannedItemKeys = flowZonePlanItemKeys(plan);
178
199
  const planned = plan.zones.map((zonePlan) => flowZoneOrganizeStep(zonePlan, findFlowZoneForPlan(zones, zonePlan), sync, plannedItemKeys));
179
200
  const validation = validateObjects
@@ -186,7 +207,7 @@ export const flowZoneCommands = {
186
207
  const pruneItems = Array.isArray(step.pruneItems) ? step.pruneItems : [];
187
208
  return count + pruneItems.length;
188
209
  }, 0);
189
- if (f["dry-run"] === true) {
210
+ if (executionMode(f).dryRun) {
190
211
  return {
191
212
  dryRun: true,
192
213
  action: "organize",
@@ -194,6 +215,7 @@ export const flowZoneCommands = {
194
215
  projectKey: pk,
195
216
  sync,
196
217
  validateObjects,
218
+ topologyFingerprint,
197
219
  zoneCount: plan.zones.length,
198
220
  itemCount,
199
221
  pruneItemCount,
@@ -209,6 +231,7 @@ export const flowZoneCommands = {
209
231
  for (const zonePlan of plan.zones) {
210
232
  let zone = findFlowZoneForPlan(currentZones, zonePlan);
211
233
  ensureFlowZonePlanTarget(zonePlan, zone);
234
+ const moveItems = flowZoneMoveDelta(zone, zonePlan.items);
212
235
  const pruneItems = sync ? flowZonePruneItems(zone, plannedItemKeys) : [];
213
236
  if (!zone) {
214
237
  zone = await c.flowZones.create({
@@ -238,15 +261,19 @@ export const flowZoneCommands = {
238
261
  updated.push(zone);
239
262
  }
240
263
  }
241
- if (zonePlan.items.length > 0) {
242
- await c.flowZones.moveItems(zone.id, zonePlan.items, pk);
243
- moved.push({ zoneId: zone.id, name: zone.name, items: zonePlan.items, });
264
+ if (moveItems.length > 0) {
265
+ await c.flowZones.moveItems(zone.id, moveItems, pk);
266
+ moved.push({ zoneId: zone.id, name: zone.name, items: moveItems, });
244
267
  }
245
268
  if (pruneItems.length > 0) {
246
269
  await c.flowZones.moveItems("default", pruneItems, pk);
247
270
  pruned.push({ zoneId: "default", fromZoneId: zone.id, name: zone.name, items: pruneItems, });
248
271
  }
249
272
  }
273
+ const topologyFingerprintAfter = await c.projects.topologyFingerprint(pk);
274
+ if (topologyFingerprintAfter !== topologyFingerprint) {
275
+ throw new ClientValidationError("Visual flow-zone changes modified flow topology.", "assertion_failed", "Inspect the flow before applying any further organization changes.", { before: topologyFingerprint, after: topologyFingerprintAfter, });
276
+ }
250
277
  return {
251
278
  organized: true,
252
279
  action: "organize",
@@ -254,6 +281,8 @@ export const flowZoneCommands = {
254
281
  projectKey: pk,
255
282
  sync,
256
283
  validateObjects,
284
+ topologyFingerprint,
285
+ topologyUnchanged: true,
257
286
  zoneCount: plan.zones.length,
258
287
  itemCount,
259
288
  pruneItemCount,
@@ -1,5 +1,6 @@
1
1
  import { deepMerge, } from "../../utils/deep-merge.js";
2
2
  import { jsonInput, } from "../coerce.js";
3
+ import { executionMode, } from "../flags.js";
3
4
  import { addTransientTargetContext, encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
4
5
  import { requireArgs, UsageError, } from "../usage.js";
5
6
  async function resolveFolderId(client, nameOrId, flags) {
@@ -28,13 +29,13 @@ export const folderCommands = {
28
29
  path: f["path"],
29
30
  projectKey: pk,
30
31
  };
31
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
32
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
32
33
  const list = await c.folders.list(pk);
33
34
  const existing = list.find((folder) => folder.name === name);
34
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
35
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
35
36
  return skipResult("folder", existing.id ?? name, "exists", { current: existing, });
36
37
  }
37
- if (f["dry-run"] === true) {
38
+ if (executionMode(f).dryRun) {
38
39
  return {
39
40
  dryRun: true,
40
41
  action: "create",
@@ -74,7 +75,7 @@ export const folderCommands = {
74
75
  }
75
76
  const pk = f["project-key"];
76
77
  const folderId = await resolveFolderId(c, a[0], f);
77
- if (f["dry-run"] === true) {
78
+ if (executionMode(f).dryRun) {
78
79
  const current = await c.folders.get(folderId, pk);
79
80
  const next = deepMerge(current, data);
80
81
  return {
@@ -102,11 +103,11 @@ export const folderCommands = {
102
103
  requireArgs(a, 1, "dss folder delete <name-or-id>");
103
104
  const pk = f["project-key"];
104
105
  const folderId = await resolveFolderId(c, a[0], f);
105
- if (f["dry-run"] === true || f["if-exists"] === true) {
106
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
106
107
  const current = await readIfExists(() => c.folders.get(folderId, pk));
107
108
  if (!current)
108
109
  return skipResult("folder", folderId, "missing");
109
- if (f["dry-run"] === true) {
110
+ if (executionMode(f).dryRun) {
110
111
  return {
111
112
  dryRun: true,
112
113
  action: "delete",
@@ -167,7 +168,7 @@ export const folderCommands = {
167
168
  requireArgs(a, 3, "dss folder upload <name-or-id> <path> <localPath>");
168
169
  const pk = f["project-key"];
169
170
  const folderId = await resolveFolderId(c, a[0], f);
170
- if (f["dry-run"] === true) {
171
+ if (executionMode(f).dryRun) {
171
172
  return {
172
173
  dryRun: true,
173
174
  action: "upload",
@@ -190,7 +191,7 @@ export const folderCommands = {
190
191
  "delete-file": {
191
192
  handler: async (c, a, f) => {
192
193
  requireArgs(a, 2, "dss folder delete-file <name-or-id> <path>");
193
- if (f["dry-run"] === true) {
194
+ if (executionMode(f).dryRun) {
194
195
  return { dryRun: true, action: "delete-file", resource: "folder", folder: a[0], path: a[1], };
195
196
  }
196
197
  await c.folders.deleteFile(await resolveFolderId(c, a[0], f), a[1], f["project-key"]);
@@ -1,4 +1,5 @@
1
1
  import { num, } from "../coerce.js";
2
+ import { executionMode, } from "../flags.js";
2
3
  import { planResult, } from "../output.js";
3
4
  import { requireArgs, } from "../usage.js";
4
5
  export const futureCommands = {
@@ -36,7 +37,7 @@ export const futureCommands = {
36
37
  handler: async (c, a, f) => {
37
38
  requireArgs(a, 1, "dss future abort <id>");
38
39
  const id = a[0];
39
- if (f["dry-run"] === true) {
40
+ if (executionMode(f).dryRun) {
40
41
  return planResult("future", "abort", {
41
42
  method: "POST",
42
43
  endpoint: `/public/api/futures/${encodeURIComponent(id)}/abort`,
@@ -1,5 +1,6 @@
1
1
  import { deepMerge, } from "../../utils/deep-merge.js";
2
2
  import { json, jsonInput, parseBooleanOption, textInput, } from "../coerce.js";
3
+ import { executionMode, } from "../flags.js";
3
4
  import { readIfExists, skipResult, } from "../output.js";
4
5
  import { requireArgs, UsageError, } from "../usage.js";
5
6
  export const insightCommands = {
@@ -40,17 +41,17 @@ export const insightCommands = {
40
41
  if (params !== undefined)
41
42
  prototype.params = params;
42
43
  const pk = f["project-key"];
43
- if (f["if-not-exists"] === true || f["dry-run"] === true) {
44
+ if (f["if-not-exists"] === true || executionMode(f).dryRun) {
44
45
  const existing = name
45
46
  ? (await c.insights.list(pk)).find((insight) => insight.name === name)
46
47
  : undefined;
47
- if (existing && f["if-not-exists"] === true && f["dry-run"] !== true) {
48
+ if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
48
49
  return skipResult("insight", existing.id, "exists", { current: existing, });
49
50
  }
50
- if (f["if-not-exists"] === true && !name && f["dry-run"] !== true) {
51
+ if (f["if-not-exists"] === true && !name && !executionMode(f).dryRun) {
51
52
  throw new UsageError("--if-not-exists requires --name for insight create.");
52
53
  }
53
- if (f["dry-run"] === true) {
54
+ if (executionMode(f).dryRun) {
54
55
  return {
55
56
  dryRun: true,
56
57
  action: "create",
@@ -95,7 +96,7 @@ export const insightCommands = {
95
96
  && contentType === undefined && payload === undefined) {
96
97
  throw new UsageError("--name, --listed, --params, --content, --file, --data, --data-file, or --stdin is required.");
97
98
  }
98
- if (f["dry-run"] === true) {
99
+ if (executionMode(f).dryRun) {
99
100
  const current = await c.insights.get(a[0], f["project-key"]);
100
101
  const next = deepMerge(current, data ?? {});
101
102
  if (name !== undefined)
@@ -138,11 +139,11 @@ export const insightCommands = {
138
139
  handler: async (c, a, f) => {
139
140
  requireArgs(a, 1, "dss insight delete <id>");
140
141
  const pk = f["project-key"];
141
- if (f["dry-run"] === true || f["if-exists"] === true) {
142
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
142
143
  const current = await readIfExists(() => c.insights.get(a[0], pk));
143
144
  if (!current)
144
145
  return skipResult("insight", a[0], "missing");
145
- if (f["dry-run"] === true) {
146
+ if (executionMode(f).dryRun) {
146
147
  return { dryRun: true, action: "delete", resource: "insight", id: a[0], current, };
147
148
  }
148
149
  }
@@ -2,6 +2,7 @@ import { mkdir, writeFile, } from "node:fs/promises";
2
2
  import { dirname, resolve, } from "node:path";
3
3
  import { parseJobLogProgress, } from "../../resources/jobs.js";
4
4
  import { jobBuildTargetTypeFromFlags, jobLogFilterFromFlag, maxLogLinesFromFlags, num, plainRecord, } from "../coerce.js";
5
+ import { executionMode, } from "../flags.js";
5
6
  import { encodedProjectEndpoint, } from "../output.js";
6
7
  import { requireArgs, UsageError, } from "../usage.js";
7
8
  // ---------------------------------------------------------------------------
@@ -276,7 +277,7 @@ export const jobCommands = {
276
277
  targetType: jobBuildTargetTypeFromFlags(f),
277
278
  timeoutMs: num(f["timeout"], "--timeout"),
278
279
  };
279
- if (f["dry-run"] === true) {
280
+ if (executionMode(f).dryRun) {
280
281
  return {
281
282
  dryRun: true,
282
283
  action: "build",
@@ -315,7 +316,7 @@ export const jobCommands = {
315
316
  summary: f["summary"] === true,
316
317
  targetType: jobBuildTargetTypeFromFlags(f),
317
318
  };
318
- if (f["dry-run"] === true) {
319
+ if (executionMode(f).dryRun) {
319
320
  return {
320
321
  dryRun: true,
321
322
  action: "build-and-wait",
@@ -399,7 +400,7 @@ export const jobCommands = {
399
400
  handler: async (c, a, f) => {
400
401
  requireArgs(a, 1, "dss job abort <id>");
401
402
  const pk = f["project-key"];
402
- if (f["dry-run"] === true) {
403
+ if (executionMode(f).dryRun) {
403
404
  return {
404
405
  dryRun: true,
405
406
  action: "abort",
@@ -1,4 +1,5 @@
1
1
  import { jsonInput, requiredJsonInput, } from "../coerce.js";
2
+ import { executionMode, } from "../flags.js";
2
3
  import { isNotFoundError, planResult, skipResult, } from "../output.js";
3
4
  import { requireArgs, } from "../usage.js";
4
5
  const MEANING_PLAN_FAILURE_CODES = { usage: 1, error: 2, transient: 3, };
@@ -40,7 +41,7 @@ export const meaningCommands = {
40
41
  requireArgs(a, 3, "dss meaning create <id> <label> <type> [--data JSON|--data-file PATH|--stdin] [--dry-run]");
41
42
  const body = jsonInput(f) ?? {};
42
43
  const payload = meaningCreatePayload(a[0], a[1], a[2], body);
43
- if (f["dry-run"] === true) {
44
+ if (executionMode(f).dryRun) {
44
45
  return planResult("meaning", "create", {
45
46
  asyncKind: "none",
46
47
  method: "POST",
@@ -62,7 +63,7 @@ export const meaningCommands = {
62
63
  handler: async (c, a, f) => {
63
64
  requireArgs(a, 1, "dss meaning update <meaningId> (--data JSON|--data-file PATH|--stdin) [--dry-run]");
64
65
  const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (meaning definition).");
65
- if (f["dry-run"] === true) {
66
+ if (executionMode(f).dryRun) {
66
67
  return planResult("meaning", "update", {
67
68
  asyncKind: "none",
68
69
  method: "PUT",
@@ -83,7 +84,7 @@ export const meaningCommands = {
83
84
  delete: {
84
85
  handler: async (c, a, f) => {
85
86
  requireArgs(a, 1, "dss meaning delete <meaningId>");
86
- if (f["dry-run"] === true || f["if-exists"] === true) {
87
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
87
88
  const current = await c.meanings.get(a[0]).catch((error) => {
88
89
  if (isNotFoundError(error)
89
90
  || (error instanceof Error && /unknown meaning/i.test(error.message))) {
@@ -93,7 +94,7 @@ export const meaningCommands = {
93
94
  });
94
95
  if (!current)
95
96
  return skipResult("meaning", a[0], "missing");
96
- if (f["dry-run"] === true) {
97
+ if (executionMode(f).dryRun) {
97
98
  return planResult("meaning", "delete", {
98
99
  asyncKind: "none",
99
100
  method: "DELETE",
@@ -1,4 +1,5 @@
1
1
  import { requiredJsonInput, requiredStringFlag, } from "../coerce.js";
2
+ import { executionMode, } from "../flags.js";
2
3
  import { requireArgs, UsageError, } from "../usage.js";
3
4
  function optionalStringFlag(flags, name) {
4
5
  const value = flags[name];
@@ -127,7 +128,7 @@ export const mlTaskCommands = {
127
128
  testDatasetRef: optionalStringFlag(f, "test-dataset"),
128
129
  projectKey: f["project-key"],
129
130
  };
130
- if (f["dry-run"] === true) {
131
+ if (executionMode(f).dryRun) {
131
132
  return {
132
133
  dryRun: true,
133
134
  action: "deploy",
@@ -148,7 +149,7 @@ export const mlTaskCommands = {
148
149
  handler: async (c, a, f) => {
149
150
  requireArgs(a, 2, "dss ml-task delete <analysisId> <mlTaskId> [--dry-run] [--project-key KEY]");
150
151
  const projectKey = f["project-key"];
151
- if (f["dry-run"] === true) {
152
+ if (executionMode(f).dryRun) {
152
153
  return {
153
154
  dryRun: true,
154
155
  action: "delete",
@@ -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 modelEvaluationStoreCommands = {
@@ -23,7 +24,7 @@ export const modelEvaluationStoreCommands = {
23
24
  const name = requiredStringFlag(f, "name", usage);
24
25
  const projectKey = f["project-key"];
25
26
  const options = { name, projectKey, };
26
- if (f["dry-run"] === true) {
27
+ if (executionMode(f).dryRun) {
27
28
  return {
28
29
  dryRun: true,
29
30
  action: "create",
@@ -56,11 +57,11 @@ export const modelEvaluationStoreCommands = {
56
57
  handler: async (c, a, f) => {
57
58
  requireArgs(a, 1, "dss model-evaluation-store delete <storeId> [--if-exists] [--dry-run] [--project-key KEY]");
58
59
  const projectKey = f["project-key"];
59
- if (f["dry-run"] === true || f["if-exists"] === true) {
60
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
60
61
  const current = await readIfExists(() => c.modelEvaluationStores.get(a[0], projectKey));
61
62
  if (!current)
62
63
  return skipResult("model-evaluation-store", a[0], "missing");
63
- if (f["dry-run"] === true) {
64
+ if (executionMode(f).dryRun) {
64
65
  return {
65
66
  dryRun: true,
66
67
  action: "delete",