dataiku-sdk 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +107 -17
  2. package/README.md +144 -17
  3. package/dist/build-metadata.json +1 -1
  4. package/dist/packages/types/src/index.d.ts +39 -4
  5. package/dist/packages/types/src/index.js +31 -2
  6. package/dist/src/cli/commands/api-service.js +62 -16
  7. package/dist/src/cli/commands/bundle.js +49 -15
  8. package/dist/src/cli/commands/code-env.js +210 -49
  9. package/dist/src/cli/commands/code.d.ts +19 -0
  10. package/dist/src/cli/commands/code.js +87 -15
  11. package/dist/src/cli/commands/dataset.js +8 -3
  12. package/dist/src/cli/commands/flow-zone.js +33 -5
  13. package/dist/src/cli/commands/notebook.js +72 -41
  14. package/dist/src/cli/commands/project-git.js +8 -5
  15. package/dist/src/cli/commands/project-library.d.ts +21 -0
  16. package/dist/src/cli/commands/project-library.js +121 -50
  17. package/dist/src/cli/commands/project.js +16 -2
  18. package/dist/src/cli/commands/recipe.js +37 -12
  19. package/dist/src/cli/commands/sql.d.ts +10 -0
  20. package/dist/src/cli/commands/sql.js +39 -17
  21. package/dist/src/cli/commands/webapp.js +67 -13
  22. package/dist/src/cli/contract.d.ts +15 -0
  23. package/dist/src/cli/contract.js +870 -107
  24. package/dist/src/cli/env.d.ts +9 -0
  25. package/dist/src/cli/env.js +20 -2
  26. package/dist/src/cli/flags.js +9 -0
  27. package/dist/src/cli/helpers/cleanup.js +104 -3
  28. package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
  29. package/dist/src/cli/helpers/flow-zone.js +33 -3
  30. package/dist/src/cli/helpers/recipe.d.ts +20 -0
  31. package/dist/src/cli/helpers/recipe.js +158 -3
  32. package/dist/src/cli/output.d.ts +2 -0
  33. package/dist/src/cli/output.js +33 -6
  34. package/dist/src/cli/runtime.d.ts +35 -1
  35. package/dist/src/cli/runtime.js +125 -18
  36. package/dist/src/cli/types.d.ts +7 -0
  37. package/dist/src/cli.js +135 -28
  38. package/dist/src/client.d.ts +60 -0
  39. package/dist/src/client.js +237 -42
  40. package/dist/src/errors.d.ts +8 -2
  41. package/dist/src/errors.js +40 -0
  42. package/dist/src/index.d.ts +4 -3
  43. package/dist/src/index.js +2 -1
  44. package/dist/src/resources/api-services.d.ts +48 -6
  45. package/dist/src/resources/api-services.js +54 -8
  46. package/dist/src/resources/bundles.d.ts +51 -6
  47. package/dist/src/resources/bundles.js +39 -9
  48. package/dist/src/resources/code-envs.d.ts +25 -3
  49. package/dist/src/resources/code-envs.js +104 -2
  50. package/dist/src/resources/datasets.d.ts +20 -0
  51. package/dist/src/resources/datasets.js +232 -52
  52. package/dist/src/resources/jobs.d.ts +35 -10
  53. package/dist/src/resources/jobs.js +92 -17
  54. package/dist/src/resources/notebooks.d.ts +77 -7
  55. package/dist/src/resources/notebooks.js +143 -19
  56. package/dist/src/resources/project-git.d.ts +12 -0
  57. package/dist/src/resources/project-git.js +39 -8
  58. package/dist/src/resources/project-library.d.ts +122 -7
  59. package/dist/src/resources/project-library.js +494 -25
  60. package/dist/src/resources/projects.d.ts +5 -2
  61. package/dist/src/resources/projects.js +18 -2
  62. package/dist/src/resources/scenarios.d.ts +30 -0
  63. package/dist/src/resources/scenarios.js +73 -27
  64. package/dist/src/resources/sql.d.ts +6 -0
  65. package/dist/src/resources/sql.js +22 -2
  66. package/dist/src/resources/webapps.d.ts +35 -4
  67. package/dist/src/resources/webapps.js +54 -5
  68. package/dist/src/schemas.d.ts +2 -2
  69. package/dist/src/schemas.js +1 -1
  70. package/dist/src/skill.d.ts +9 -0
  71. package/dist/src/skill.js +37 -4
  72. package/dist/src/utils/flow-analysis.d.ts +53 -0
  73. package/dist/src/utils/flow-analysis.js +382 -0
  74. package/dist/src/utils/project-archive.d.ts +41 -2
  75. package/dist/src/utils/project-archive.js +159 -16
  76. package/package.json +5 -5
  77. package/packages/types/dist/index.d.ts +39 -4
  78. package/packages/types/dist/index.js +31 -2
  79. package/plugin.json +2 -2
  80. package/skills/dataiku-dss/SKILL.md +75 -48
@@ -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",
@@ -314,6 +314,7 @@ export const datasetCommands = {
314
314
  outputPath: f["output"],
315
315
  projectKey: f["project-key"],
316
316
  limit: num(f["limit"], "--limit"),
317
+ rawData: f["raw-data"] === true,
317
318
  });
318
319
  if (result.truncated) {
319
320
  enqueueCliWarning({
@@ -337,9 +338,13 @@ export const datasetCommands = {
337
338
  }
338
339
  return result;
339
340
  },
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/",],
341
+ usage: "dss dataset download <name> [--output PATH] [--limit N] [--raw-data] [--project-key KEY]",
342
+ 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.",
343
+ examples: [
344
+ "dss dataset download orders",
345
+ "dss dataset download orders --output ./data/",
346
+ "dss dataset download orders --raw-data",
347
+ ],
343
348
  },
344
349
  create: {
345
350
  handler: async (c, _a, f) => {
@@ -1,5 +1,6 @@
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 { 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
4
  import { readIfExists, skipResult, } from "../output.js";
4
5
  import { requireArgs, UsageError, } from "../usage.js";
5
6
  export const flowZoneCommands = {
@@ -166,6 +167,19 @@ export const flowZoneCommands = {
166
167
  "dss flow-zone move ZONE_ID --object SAVED_MODEL:model_id",
167
168
  ],
168
169
  },
170
+ plan: {
171
+ handler: async (c, _a, f) => {
172
+ const pk = f["project-key"];
173
+ const [zones, topologyFingerprint,] = await Promise.all([
174
+ c.flowZones.list(pk),
175
+ c.projects.topologyFingerprint(pk),
176
+ ]);
177
+ return flowZonePlanFromZones(zones, topologyFingerprint);
178
+ },
179
+ usage: "dss flow-zone plan [--project-key KEY]",
180
+ description: "Export current visual flow-zone organization as an organize-compatible plan.",
181
+ examples: ["dss flow-zone plan --project-key MYPROJ",],
182
+ },
169
183
  organize: {
170
184
  handler: async (c, _a, f) => {
171
185
  const usage = "dss flow-zone organize (--data JSON|--data-file PATH|--file PATH|--stdin) [--sync] [--validate-objects] [--dry-run] [--project-key KEY]";
@@ -173,7 +187,13 @@ export const flowZoneCommands = {
173
187
  const plan = readFlowZoneOrganizePlan(f, usage);
174
188
  const sync = f["sync"] === true;
175
189
  const validateObjects = f["validate-objects"] === true;
176
- const zones = await c.flowZones.list(pk);
190
+ const [zones, topologyFingerprint,] = await Promise.all([
191
+ c.flowZones.list(pk),
192
+ c.projects.topologyFingerprint(pk),
193
+ ]);
194
+ if (plan.topologyFingerprint && plan.topologyFingerprint !== topologyFingerprint) {
195
+ 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, });
196
+ }
177
197
  const plannedItemKeys = flowZonePlanItemKeys(plan);
178
198
  const planned = plan.zones.map((zonePlan) => flowZoneOrganizeStep(zonePlan, findFlowZoneForPlan(zones, zonePlan), sync, plannedItemKeys));
179
199
  const validation = validateObjects
@@ -194,6 +214,7 @@ export const flowZoneCommands = {
194
214
  projectKey: pk,
195
215
  sync,
196
216
  validateObjects,
217
+ topologyFingerprint,
197
218
  zoneCount: plan.zones.length,
198
219
  itemCount,
199
220
  pruneItemCount,
@@ -209,6 +230,7 @@ export const flowZoneCommands = {
209
230
  for (const zonePlan of plan.zones) {
210
231
  let zone = findFlowZoneForPlan(currentZones, zonePlan);
211
232
  ensureFlowZonePlanTarget(zonePlan, zone);
233
+ const moveItems = flowZoneMoveDelta(zone, zonePlan.items);
212
234
  const pruneItems = sync ? flowZonePruneItems(zone, plannedItemKeys) : [];
213
235
  if (!zone) {
214
236
  zone = await c.flowZones.create({
@@ -238,15 +260,19 @@ export const flowZoneCommands = {
238
260
  updated.push(zone);
239
261
  }
240
262
  }
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, });
263
+ if (moveItems.length > 0) {
264
+ await c.flowZones.moveItems(zone.id, moveItems, pk);
265
+ moved.push({ zoneId: zone.id, name: zone.name, items: moveItems, });
244
266
  }
245
267
  if (pruneItems.length > 0) {
246
268
  await c.flowZones.moveItems("default", pruneItems, pk);
247
269
  pruned.push({ zoneId: "default", fromZoneId: zone.id, name: zone.name, items: pruneItems, });
248
270
  }
249
271
  }
272
+ const topologyFingerprintAfter = await c.projects.topologyFingerprint(pk);
273
+ if (topologyFingerprintAfter !== topologyFingerprint) {
274
+ 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, });
275
+ }
250
276
  return {
251
277
  organized: true,
252
278
  action: "organize",
@@ -254,6 +280,8 @@ export const flowZoneCommands = {
254
280
  projectKey: pk,
255
281
  sync,
256
282
  validateObjects,
283
+ topologyFingerprint,
284
+ topologyUnchanged: true,
257
285
  zoneCount: plan.zones.length,
258
286
  itemCount,
259
287
  pruneItemCount,
@@ -1,12 +1,17 @@
1
- import { jsonInput, num, } from "../coerce.js";
2
- import { encodedProjectEndpoint, isNotFoundError, readIfExists, skipResult, } from "../output.js";
3
- import { requireArgs, UsageError, } from "../usage.js";
1
+ import { jsonInput, num, parseBooleanOption, } from "../coerce.js";
2
+ import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
3
+ import { requireArgs, requireNoArgs, UsageError, } from "../usage.js";
4
4
  export const notebookCommands = {
5
5
  "list-jupyter": {
6
- handler: (c, _a, f) => c.notebooks.listJupyter(f["project-key"]),
7
- usage: "dss notebook list-jupyter [--project-key KEY]",
8
- description: "List Jupyter notebooks.",
9
- examples: ["dss notebook list-jupyter",],
6
+ handler: (c, _a, f) => c.notebooks.listJupyter(f["project-key"], {
7
+ active: parseBooleanOption(f["active"], "--active"),
8
+ }),
9
+ usage: "dss notebook list-jupyter [--active true|false] [--project-key KEY]",
10
+ description: "List Jupyter notebooks. Pass --active to use the official ?active= filter: true lists currently running notebooks, false lists non-running ones.",
11
+ examples: [
12
+ "dss notebook list-jupyter",
13
+ "dss notebook list-jupyter --active true",
14
+ ],
10
15
  },
11
16
  "get-jupyter": {
12
17
  handler: (c, a, f) => {
@@ -41,22 +46,19 @@ export const notebookCommands = {
41
46
  requireArgs(a, 1, "dss notebook clear-jupyter-outputs <name>");
42
47
  const pk = f["project-key"];
43
48
  if (f["dry-run"] === true) {
49
+ // The real mutation is a single server-side DELETE of the
50
+ // outputs endpoint, so the dry run can only report the current
51
+ // content (read-only) and the exact request that would run; the
52
+ // stripped `next` shape would misdescribe the operation.
44
53
  const current = await c.notebooks.getJupyter(a[0], pk);
45
- const next = {
46
- ...current,
47
- cells: current.cells.map((cell) => {
48
- if (cell.cell_type !== "code")
49
- return cell;
50
- return { ...cell, outputs: [], execution_count: 0, };
51
- }),
52
- };
53
54
  return {
54
55
  dryRun: true,
55
56
  action: "clear-jupyter-outputs",
56
57
  resource: "jupyter-notebook",
57
58
  name: a[0],
58
59
  current,
59
- next,
60
+ endpoint: encodedProjectEndpoint(c, pk, `/jupyter-notebooks/${encodeURIComponent(a[0])}/outputs`),
61
+ method: "DELETE",
60
62
  };
61
63
  }
62
64
  await c.notebooks.clearJupyterOutputs(a[0], pk);
@@ -77,8 +79,33 @@ export const notebookCommands = {
77
79
  },
78
80
  "unload-jupyter": {
79
81
  handler: async (c, a, f) => {
80
- requireArgs(a, 2, "dss notebook unload-jupyter <name> <sessionId>");
81
82
  const pk = f["project-key"];
83
+ if (f["all"] === true) {
84
+ requireNoArgs(a, "dss notebook unload-jupyter --all [--dry-run] [--project-key KEY]");
85
+ if (f["dry-run"] === true) {
86
+ const planned = [];
87
+ for (const notebook of await c.notebooks.listJupyter(pk, { active: true, })) {
88
+ for (const session of await c.notebooks.listJupyterSessions(notebook.name, pk)) {
89
+ planned.push({
90
+ name: notebook.name,
91
+ sessionId: session.sessionId,
92
+ endpoint: encodedProjectEndpoint(c, pk, `/jupyter-notebooks/${encodeURIComponent(notebook.name)}/sessions/${encodeURIComponent(session.sessionId)}`),
93
+ method: "DELETE",
94
+ });
95
+ }
96
+ }
97
+ return {
98
+ dryRun: true,
99
+ action: "unload-jupyter",
100
+ resource: "jupyter-notebook",
101
+ all: true,
102
+ planned,
103
+ };
104
+ }
105
+ const unloaded = await c.notebooks.unloadJupyterAll(pk);
106
+ return { unloaded, resource: "jupyter-notebook", all: true, };
107
+ }
108
+ requireArgs(a, 2, "dss notebook unload-jupyter <name> <sessionId>");
82
109
  if (f["dry-run"] === true) {
83
110
  const sessions = await c.notebooks.listJupyterSessions(a[0], pk);
84
111
  const current = sessions.find((session) => session.sessionId === a[1]);
@@ -96,9 +123,12 @@ export const notebookCommands = {
96
123
  await c.notebooks.unloadJupyter(a[0], a[1], pk);
97
124
  return { unloaded: a[0], sessionId: a[1], resource: "jupyter-notebook", };
98
125
  },
99
- usage: "dss notebook unload-jupyter <name> <sessionId> [--dry-run] [--project-key KEY]",
100
- description: "Unload a Jupyter notebook kernel session.",
101
- examples: ["dss notebook unload-jupyter my_notebook SESSION_ID --dry-run",],
126
+ usage: "dss notebook unload-jupyter (<name> <sessionId>|--all) [--dry-run] [--project-key KEY]",
127
+ description: "Unload a Jupyter notebook kernel session, or every session of every running notebook with --all.",
128
+ examples: [
129
+ "dss notebook unload-jupyter my_notebook SESSION_ID --dry-run",
130
+ "dss notebook unload-jupyter --all --dry-run",
131
+ ],
102
132
  },
103
133
  "list-sql": {
104
134
  handler: (c, _a, f) => c.notebooks.listSql(f["project-key"]),
@@ -162,18 +192,18 @@ export const notebookCommands = {
162
192
  next: data,
163
193
  };
164
194
  }
165
- try {
166
- await c.notebooks.saveJupyter(a[0], data, pk);
167
- }
168
- catch (error) {
169
- if (!isNotFoundError(error))
170
- throw error;
171
- await c.notebooks.createJupyter(a[0], data, pk);
172
- }
173
- return { saved: a[0], resource: "jupyter-notebook", };
195
+ const result = await c.notebooks.saveOrCreateJupyter(a[0], data, pk, {
196
+ expectHash: f["expect-hash"],
197
+ });
198
+ return {
199
+ saved: a[0],
200
+ resource: "jupyter-notebook",
201
+ created: result.created,
202
+ hash: result.hash,
203
+ };
174
204
  },
175
- usage: "dss notebook save-jupyter <name> (--data '{...}' | --data-file PATH | --stdin) [--dry-run] [--project-key KEY]",
176
- description: "Save content to a Jupyter notebook, creating it if missing.",
205
+ usage: "dss notebook save-jupyter <name> (--data '{...}' | --data-file PATH | --stdin) [--expect-hash SHA256] [--dry-run] [--project-key KEY]",
206
+ description: "Save content to a Jupyter notebook, creating it if missing. Reports created versus updated and the persisted content hash; --expect-hash aborts with a stale-read error when the stored content hash changed.",
177
207
  examples: [
178
208
  "dss notebook save-jupyter my_notebook --data-file notebook.json --dry-run",
179
209
  "cat notebook.json | dss notebook save-jupyter my_notebook --stdin",
@@ -198,17 +228,18 @@ export const notebookCommands = {
198
228
  next: data,
199
229
  };
200
230
  }
201
- const current = await readIfExists(() => c.notebooks.getSql(a[0], pk));
202
- if (current) {
203
- await c.notebooks.saveSql(a[0], data, pk);
204
- }
205
- else {
206
- await c.notebooks.createSql(a[0], data, pk);
207
- }
208
- return { saved: a[0], resource: "sql-notebook", };
231
+ const result = await c.notebooks.saveOrCreateSql(a[0], data, pk, {
232
+ expectHash: f["expect-hash"],
233
+ });
234
+ return {
235
+ saved: a[0],
236
+ resource: "sql-notebook",
237
+ created: result.created,
238
+ hash: result.hash,
239
+ };
209
240
  },
210
- usage: "dss notebook save-sql <id> (--data '{...}' | --data-file PATH | --stdin) [--dry-run] [--project-key KEY]",
211
- description: "Save content to a SQL notebook, creating it if missing.",
241
+ usage: "dss notebook save-sql <id> (--data '{...}' | --data-file PATH | --stdin) [--expect-hash SHA256] [--dry-run] [--project-key KEY]",
242
+ description: "Save content to a SQL notebook, creating it if missing. Reports created versus updated and the persisted content hash; --expect-hash rejects a stale read before writing.",
212
243
  examples: ["dss notebook save-sql my_sql_notebook --data-file content.json --dry-run",],
213
244
  },
214
245
  "clear-sql-history": {
@@ -1,3 +1,4 @@
1
+ import { validateGitReferencePath, } from "../../resources/project-git.js";
1
2
  import { num, parseBooleanOption, requiredStringFlag, } from "../coerce.js";
2
3
  import { CommandResultFailure, } from "../output.js";
3
4
  import { requireArgs, requireNoArgs, UsageError, } from "../usage.js";
@@ -448,8 +449,9 @@ export const projectGitCommands = {
448
449
  "set-library": {
449
450
  handler: async (c, a, f) => {
450
451
  requireArgs(a, 1, SET_LIBRARY_USAGE);
452
+ const targetPath = validateGitReferencePath(a[0]);
451
453
  const password = libraryPassword(f);
452
- return sanitizeProjectGitOutput(await c.projectGit.setLibrary(requiredStringFlag(f, "project-key", SET_LIBRARY_USAGE), a[0], {
454
+ return sanitizeProjectGitOutput(await c.projectGit.setLibrary(requiredStringFlag(f, "project-key", SET_LIBRARY_USAGE), targetPath, {
453
455
  repository: requiredStringFlag(f, "repository", SET_LIBRARY_USAGE),
454
456
  pathInGitRepository: optionalStringFlag(f, "path-in-repository"),
455
457
  checkout: requiredStringFlag(f, "checkout", SET_LIBRARY_USAGE),
@@ -466,10 +468,11 @@ export const projectGitCommands = {
466
468
  "remove-library": {
467
469
  handler: async (c, a, f) => {
468
470
  requireArgs(a, 1, REMOVE_LIBRARY_USAGE);
471
+ const targetPath = validateGitReferencePath(a[0]);
469
472
  const deleteDirectory = parseBooleanOption(f["delete-directory"], "--delete-directory")
470
473
  ?? false;
471
- await c.projectGit.removeLibrary(requiredStringFlag(f, "project-key", REMOVE_LIBRARY_USAGE), a[0], deleteDirectory);
472
- return { removed: a[0], resource: "project-git-library", deleteDirectory, };
474
+ await c.projectGit.removeLibrary(requiredStringFlag(f, "project-key", REMOVE_LIBRARY_USAGE), targetPath, deleteDirectory);
475
+ return { removed: targetPath, resource: "project-git-library", deleteDirectory, };
473
476
  },
474
477
  usage: REMOVE_LIBRARY_USAGE,
475
478
  description: "Detach an external Git library. --delete-directory also removes its local directory from the project library.",
@@ -478,7 +481,7 @@ export const projectGitCommands = {
478
481
  "reset-library": {
479
482
  handler: (c, a, f) => {
480
483
  requireArgs(a, 1, RESET_LIBRARY_USAGE);
481
- return c.projectGit.resetLibrary(requiredStringFlag(f, "project-key", RESET_LIBRARY_USAGE), a[0]);
484
+ return c.projectGit.resetLibrary(requiredStringFlag(f, "project-key", RESET_LIBRARY_USAGE), validateGitReferencePath(a[0]));
482
485
  },
483
486
  usage: RESET_LIBRARY_USAGE,
484
487
  description: "Reset one attached library to its remote state, discarding local edits. Returns the DSS future that performs the reset.",
@@ -487,7 +490,7 @@ export const projectGitCommands = {
487
490
  "push-library": {
488
491
  handler: (c, a, f) => {
489
492
  requireArgs(a, 1, PUSH_LIBRARY_USAGE);
490
- return c.projectGit.pushLibrary(requiredStringFlag(f, "project-key", PUSH_LIBRARY_USAGE), a[0], requiredStringFlag(f, "message", PUSH_LIBRARY_USAGE));
493
+ return c.projectGit.pushLibrary(requiredStringFlag(f, "project-key", PUSH_LIBRARY_USAGE), validateGitReferencePath(a[0]), requiredStringFlag(f, "message", PUSH_LIBRARY_USAGE));
491
494
  },
492
495
  usage: PUSH_LIBRARY_USAGE,
493
496
  description: "Commit and push one attached library. Returns the DSS future that performs the push.",
@@ -1,2 +1,23 @@
1
+ import { type ProjectLibraryDiffResult } from "../../resources/project-library.js";
1
2
  import type { CommandMeta } from "../types.js";
3
+ export interface ProjectLibraryPutPayload {
4
+ /** Where the written bytes come from: --content flag, --file, or --stdin. */
5
+ contentSource: "flag" | "file" | "stdin";
6
+ /** Local file path when contentSource is "file". */
7
+ file?: string;
8
+ /** Byte count of the content when it is available without consuming stdin. */
9
+ bytes?: number;
10
+ /** SHA-256 hex digest of the exact bytes to write, when safely available. */
11
+ sha256?: string;
12
+ /** SHA-256 hex digest the caller expects the remote file to carry before the write. */
13
+ expectSha256?: string;
14
+ }
15
+ /**
16
+ * Put-input metadata for plans and reports. Plans must never include content
17
+ * bytes: this helper reports only the source, byte count, and SHA-256 digest —
18
+ * and only when they are available without consuming stdin (dry-run keeps
19
+ * stdin unread, so the stdin source carries no size/hash).
20
+ */
21
+ export declare function projectLibraryPutPayload(flags: Record<string, string | boolean>): ProjectLibraryPutPayload;
2
22
  export declare const projectLibraryCommands: Record<string, CommandMeta>;
23
+ export type { ProjectLibraryDiffResult, };