dataiku-sdk 3.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 (79) hide show
  1. package/README.md +132 -17
  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/cli/commands/api-service.js +62 -16
  6. package/dist/src/cli/commands/bundle.js +49 -15
  7. package/dist/src/cli/commands/code-env.js +210 -49
  8. package/dist/src/cli/commands/code.d.ts +19 -0
  9. package/dist/src/cli/commands/code.js +87 -15
  10. package/dist/src/cli/commands/dataset.js +8 -3
  11. package/dist/src/cli/commands/flow-zone.js +33 -5
  12. package/dist/src/cli/commands/notebook.js +72 -41
  13. package/dist/src/cli/commands/project-git.js +8 -5
  14. package/dist/src/cli/commands/project-library.d.ts +21 -0
  15. package/dist/src/cli/commands/project-library.js +121 -50
  16. package/dist/src/cli/commands/project.js +16 -2
  17. package/dist/src/cli/commands/recipe.js +37 -12
  18. package/dist/src/cli/commands/sql.d.ts +10 -0
  19. package/dist/src/cli/commands/sql.js +39 -17
  20. package/dist/src/cli/commands/webapp.js +67 -13
  21. package/dist/src/cli/contract.d.ts +15 -0
  22. package/dist/src/cli/contract.js +870 -107
  23. package/dist/src/cli/env.d.ts +9 -0
  24. package/dist/src/cli/env.js +20 -2
  25. package/dist/src/cli/flags.js +9 -0
  26. package/dist/src/cli/helpers/cleanup.js +104 -3
  27. package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
  28. package/dist/src/cli/helpers/flow-zone.js +33 -3
  29. package/dist/src/cli/helpers/recipe.d.ts +20 -0
  30. package/dist/src/cli/helpers/recipe.js +158 -3
  31. package/dist/src/cli/output.d.ts +2 -0
  32. package/dist/src/cli/output.js +33 -6
  33. package/dist/src/cli/runtime.d.ts +35 -1
  34. package/dist/src/cli/runtime.js +125 -18
  35. package/dist/src/cli/types.d.ts +7 -0
  36. package/dist/src/cli.js +135 -28
  37. package/dist/src/client.d.ts +60 -0
  38. package/dist/src/client.js +237 -42
  39. package/dist/src/errors.d.ts +8 -2
  40. package/dist/src/errors.js +40 -0
  41. package/dist/src/index.d.ts +4 -3
  42. package/dist/src/index.js +2 -1
  43. package/dist/src/resources/api-services.d.ts +48 -6
  44. package/dist/src/resources/api-services.js +54 -8
  45. package/dist/src/resources/bundles.d.ts +51 -6
  46. package/dist/src/resources/bundles.js +39 -9
  47. package/dist/src/resources/code-envs.d.ts +25 -3
  48. package/dist/src/resources/code-envs.js +104 -2
  49. package/dist/src/resources/datasets.d.ts +20 -0
  50. package/dist/src/resources/datasets.js +232 -52
  51. package/dist/src/resources/jobs.d.ts +35 -10
  52. package/dist/src/resources/jobs.js +92 -17
  53. package/dist/src/resources/notebooks.d.ts +77 -7
  54. package/dist/src/resources/notebooks.js +143 -19
  55. package/dist/src/resources/project-git.d.ts +12 -0
  56. package/dist/src/resources/project-git.js +39 -8
  57. package/dist/src/resources/project-library.d.ts +122 -7
  58. package/dist/src/resources/project-library.js +494 -25
  59. package/dist/src/resources/projects.d.ts +5 -2
  60. package/dist/src/resources/projects.js +18 -2
  61. package/dist/src/resources/scenarios.d.ts +30 -0
  62. package/dist/src/resources/scenarios.js +73 -27
  63. package/dist/src/resources/sql.d.ts +6 -0
  64. package/dist/src/resources/sql.js +22 -2
  65. package/dist/src/resources/webapps.d.ts +35 -4
  66. package/dist/src/resources/webapps.js +54 -5
  67. package/dist/src/schemas.d.ts +2 -2
  68. package/dist/src/schemas.js +1 -1
  69. package/dist/src/skill.d.ts +9 -0
  70. package/dist/src/skill.js +37 -4
  71. package/dist/src/utils/flow-analysis.d.ts +53 -0
  72. package/dist/src/utils/flow-analysis.js +382 -0
  73. package/dist/src/utils/project-archive.d.ts +41 -2
  74. package/dist/src/utils/project-archive.js +159 -16
  75. package/package.json +4 -4
  76. package/packages/types/dist/index.d.ts +39 -4
  77. package/packages/types/dist/index.js +31 -2
  78. package/plugin.json +1 -1
  79. package/skills/dataiku-dss/SKILL.md +74 -47
@@ -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, };
@@ -1,24 +1,12 @@
1
1
  import * as fs from "node:fs";
2
- import { readStdinText, } from "../coerce.js";
3
- import { encodedProjectEndpoint, planResult, } from "../output.js";
2
+ import { encodeLibraryPath, EXPECT_SHA256_PATTERN, validateLibraryDestinationPath, validateLibraryName, validateLibraryPath, } from "../../resources/project-library.js";
3
+ import { numFlag, readStdinText, sha256Hex, } from "../coerce.js";
4
+ import { encodedProjectEndpoint, planResult, skipResult, } from "../output.js";
4
5
  import { requireArgs, UsageError, } from "../usage.js";
5
6
  const PROJECT_LIBRARY_EXIT_CODES = { usage: 1, error: 2, transient: 3, };
6
- function normalizeLibraryPath(path) {
7
- const normalized = path.replace(/^\/+/, "");
8
- if (!normalized)
9
- throw new Error("Project library path is required");
10
- return normalized;
11
- }
12
- function normalizeLibraryDestinationPath(path) {
13
- if (path === "/")
14
- return "/";
15
- return `/${normalizeLibraryPath(path)}`;
16
- }
17
- function encodeLibraryPath(path) {
18
- return normalizeLibraryPath(path)
19
- .split("/")
20
- .map((segment) => encodeURIComponent(segment))
21
- .join("/");
7
+ const PUT_FLAG_ERROR_HINT = "Pass a 64-character SHA-256 hex digest (lowercase or uppercase).";
8
+ function sha256BytesHex(bytes) {
9
+ return new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
22
10
  }
23
11
  function projectLibraryContentsEndpoint(client, projectKey, path) {
24
12
  return encodedProjectEndpoint(client, projectKey, `/libraries/contents/${encodeLibraryPath(path)}`);
@@ -29,22 +17,53 @@ function projectLibraryFolderEndpoint(client, projectKey, path) {
29
17
  function projectLibraryActionEndpoint(client, projectKey, action) {
30
18
  return encodedProjectEndpoint(client, projectKey, `/libraries/contents-actions/${action}`);
31
19
  }
32
- function projectLibraryPutPayload(flags) {
33
- if (typeof flags["content"] === "string")
34
- return { contentSource: "flag", };
35
- if (typeof flags["file"] === "string")
36
- return { contentSource: "file", file: flags["file"], };
37
- return { contentSource: "stdin", };
20
+ /**
21
+ * Put-input metadata for plans and reports. Plans must never include content
22
+ * bytes: this helper reports only the source, byte count, and SHA-256 digest —
23
+ * and only when they are available without consuming stdin (dry-run keeps
24
+ * stdin unread, so the stdin source carries no size/hash).
25
+ */
26
+ export function projectLibraryPutPayload(flags) {
27
+ const expectSha256 = expectSha256FromFlags(flags);
28
+ if (typeof flags["content"] === "string") {
29
+ const text = flags["content"];
30
+ return {
31
+ contentSource: "flag",
32
+ bytes: Buffer.byteLength(text, "utf8"),
33
+ sha256: sha256Hex(text),
34
+ ...(expectSha256 ? { expectSha256, } : {}),
35
+ };
36
+ }
37
+ if (typeof flags["file"] === "string") {
38
+ const bytes = fs.readFileSync(flags["file"]);
39
+ return {
40
+ contentSource: "file",
41
+ file: flags["file"],
42
+ bytes: bytes.length,
43
+ sha256: sha256BytesHex(bytes),
44
+ ...(expectSha256 ? { expectSha256, } : {}),
45
+ };
46
+ }
47
+ return { contentSource: "stdin", ...(expectSha256 ? { expectSha256, } : {}), };
38
48
  }
39
49
  function projectLibraryPlan(action, options) {
40
50
  return planResult("project-library", action, {
41
51
  ...options,
52
+ idempotency: options.idempotency ?? "none",
42
53
  asyncKind: "none",
43
54
  exitCodesOnFailure: PROJECT_LIBRARY_EXIT_CODES,
44
- idempotency: "none",
45
55
  plannedAndDryRun: true,
46
56
  });
47
57
  }
58
+ function expectSha256FromFlags(flags) {
59
+ const value = flags["expect-sha256"];
60
+ if (value === undefined || value === false)
61
+ return undefined;
62
+ if (typeof value !== "string" || !EXPECT_SHA256_PATTERN.test(value)) {
63
+ throw new UsageError("--expect-sha256 must be a 64-character SHA-256 hex digest.", "validation_failed", PUT_FLAG_ERROR_HINT);
64
+ }
65
+ return value;
66
+ }
48
67
  export const projectLibraryCommands = {
49
68
  list: {
50
69
  handler: (c, _a, f) => c.projectLibrary.listContents(f["project-key"]),
@@ -55,6 +74,7 @@ export const projectLibraryCommands = {
55
74
  get: {
56
75
  handler: (c, a, f) => {
57
76
  requireArgs(a, 1, "dss project-library get <path> [--project-key KEY]");
77
+ validateLibraryPath(a[0]);
58
78
  return c.projectLibrary.getFile(a[0], f["project-key"]);
59
79
  },
60
80
  usage: "dss project-library get <path> [--project-key KEY]",
@@ -69,7 +89,7 @@ export const projectLibraryCommands = {
69
89
  throw new UsageError("--output PATH is required.", "missing_required_flag");
70
90
  const bytes = await c.projectLibrary.getFileBytes(a[0], f["project-key"]);
71
91
  const written = await Bun.write(out, bytes, { createPath: false, });
72
- return { path: out, bytes: written, };
92
+ return { path: out, bytes: written, sha256: sha256BytesHex(bytes), };
73
93
  },
74
94
  usage: "dss project-library get-bytes <path> --output PATH [--project-key KEY]",
75
95
  description: "Download a project library file's raw bytes to a local file.",
@@ -77,44 +97,62 @@ export const projectLibraryCommands = {
77
97
  },
78
98
  "create-file": {
79
99
  handler: async (c, a, f) => {
80
- requireArgs(a, 1, "dss project-library create-file <path> [--dry-run] [--project-key KEY]");
100
+ requireArgs(a, 1, "dss project-library create-file <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
81
101
  const pk = f["project-key"];
102
+ validateLibraryPath(a[0]);
82
103
  if (f["dry-run"] === true) {
83
104
  return projectLibraryPlan("create-file", {
84
105
  method: "POST",
85
106
  endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
86
107
  identifiers: { path: a[0], },
108
+ idempotency: "if-not-exists",
87
109
  });
88
110
  }
111
+ if (f["if-not-exists"] === true) {
112
+ const exists = await c.projectLibrary.hasLibraryItem(a[0], pk);
113
+ if (exists) {
114
+ return skipResult("project-library", a[0], "exists", { kind: "file", });
115
+ }
116
+ }
89
117
  await c.projectLibrary.addFile(a[0], pk);
90
118
  return { created: a[0], };
91
119
  },
92
- usage: "dss project-library create-file <path> [--dry-run] [--project-key KEY]",
93
- description: "Create an empty file in the project library.",
94
- examples: ["dss project-library create-file python/mylib/new.py",],
120
+ usage: "dss project-library create-file <path> [--if-not-exists] [--dry-run] [--project-key KEY]",
121
+ description: "Create an empty file in the project library; refuses to overwrite an existing item.",
122
+ examples: ["dss project-library create-file python/mylib/new.py --if-not-exists",],
95
123
  },
96
124
  "create-folder": {
97
125
  handler: async (c, a, f) => {
98
- requireArgs(a, 1, "dss project-library create-folder <path> [--dry-run] [--project-key KEY]");
126
+ requireArgs(a, 1, "dss project-library create-folder <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
99
127
  const pk = f["project-key"];
128
+ validateLibraryPath(a[0]);
100
129
  if (f["dry-run"] === true) {
101
130
  return projectLibraryPlan("create-folder", {
102
131
  method: "POST",
103
132
  endpoint: projectLibraryFolderEndpoint(c, pk, a[0]),
104
133
  identifiers: { path: a[0], },
134
+ idempotency: "if-not-exists",
105
135
  });
106
136
  }
137
+ if (f["if-not-exists"] === true) {
138
+ const exists = await c.projectLibrary.hasLibraryItem(a[0], pk);
139
+ if (exists) {
140
+ return skipResult("project-library", a[0], "exists", { kind: "folder", });
141
+ }
142
+ }
107
143
  await c.projectLibrary.addFolder(a[0], pk);
108
144
  return { created: a[0], };
109
145
  },
110
- usage: "dss project-library create-folder <path> [--dry-run] [--project-key KEY]",
111
- description: "Create a folder in the project library.",
112
- examples: ["dss project-library create-folder python/mylib",],
146
+ usage: "dss project-library create-folder <path> [--if-not-exists] [--dry-run] [--project-key KEY]",
147
+ description: "Create a folder in the project library; refuses to overwrite an existing item.",
148
+ examples: ["dss project-library create-folder python/mylib --if-not-exists",],
113
149
  },
114
150
  put: {
115
151
  handler: async (c, a, f) => {
116
- requireArgs(a, 1, "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--dry-run] [--project-key KEY]");
152
+ requireArgs(a, 1, "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--expect-sha256 SHA256] [--dry-run] [--project-key KEY]");
117
153
  const pk = f["project-key"];
154
+ const expectSha256 = expectSha256FromFlags(f);
155
+ validateLibraryPath(a[0]);
118
156
  if (f["dry-run"] === true) {
119
157
  return projectLibraryPlan("put", {
120
158
  method: "POST",
@@ -126,19 +164,48 @@ export const projectLibraryCommands = {
126
164
  const content = typeof f["content"] === "string"
127
165
  ? f["content"]
128
166
  : typeof f["file"] === "string"
129
- ? fs.readFileSync(f["file"], "utf-8")
167
+ ? fs.readFileSync(f["file"])
168
+ : await readStdinText();
169
+ const result = await c.projectLibrary.addOrUpdateFile(a[0], content, pk, expectSha256 !== undefined ? { expectSha256, } : undefined);
170
+ return {
171
+ updated: result.path,
172
+ bytes: result.bytes,
173
+ sha256: result.sha256,
174
+ ...(result.beforeSha256 !== undefined ? { beforeSha256: result.beforeSha256, } : {}),
175
+ };
176
+ },
177
+ usage: "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--expect-sha256 SHA256] [--dry-run] [--project-key KEY]",
178
+ description: "Create or overwrite a project library file with text or binary content; reports the written byte count and sha256.",
179
+ examples: [
180
+ "dss project-library put python/mylib/utils.py --file ./utils.py",
181
+ "dss project-library put python/mylib/utils.py --file ./utils.py --expect-sha256 <previous-sha256>",
182
+ ],
183
+ },
184
+ diff: {
185
+ handler: async (c, a, f) => {
186
+ requireArgs(a, 1, "dss project-library diff <path> (--content TEXT|--file PATH|--stdin) [--max-lines N] [--project-key KEY]");
187
+ const pk = f["project-key"];
188
+ validateLibraryPath(a[0]);
189
+ const maxLines = numFlag(f, ["max-lines",]);
190
+ const local = typeof f["content"] === "string"
191
+ ? f["content"]
192
+ : typeof f["file"] === "string"
193
+ ? fs.readFileSync(f["file"])
130
194
  : await readStdinText();
131
- await c.projectLibrary.addOrUpdateFile(a[0], content, pk);
132
- return { updated: a[0], };
195
+ return c.projectLibrary.diffFile(a[0], local, pk, maxLines !== undefined ? { maxLines, } : {});
133
196
  },
134
- usage: "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--dry-run] [--project-key KEY]",
135
- description: "Create or overwrite a project library file with raw text content.",
136
- examples: ["dss project-library put python/mylib/utils.py --file ./utils.py",],
197
+ usage: "dss project-library diff <path> (--content TEXT|--file PATH|--stdin) [--max-lines N] [--project-key KEY]",
198
+ description: "Diff a project library file against local content; capped unified text diff, binary files detected instead of dumped.",
199
+ examples: [
200
+ "dss project-library diff python/mylib/utils.py --file ./utils.py",
201
+ "dss project-library diff python/mylib/utils.py --content 'print(1)' --max-lines 50",
202
+ ],
137
203
  },
138
204
  delete: {
139
205
  handler: async (c, a, f) => {
140
206
  requireArgs(a, 1, "dss project-library delete <path> [--dry-run] [--project-key KEY]");
141
207
  const pk = f["project-key"];
208
+ validateLibraryPath(a[0]);
142
209
  if (f["dry-run"] === true) {
143
210
  return projectLibraryPlan("delete", {
144
211
  method: "DELETE",
@@ -157,16 +224,18 @@ export const projectLibraryCommands = {
157
224
  handler: async (c, a, f) => {
158
225
  requireArgs(a, 2, "dss project-library rename <path> <new-name> [--dry-run] [--project-key KEY]");
159
226
  const pk = f["project-key"];
227
+ const validPath = validateLibraryPath(a[0]);
228
+ const validName = validateLibraryName(a[1]);
160
229
  if (f["dry-run"] === true) {
161
230
  return projectLibraryPlan("rename", {
162
231
  method: "POST",
163
232
  endpoint: projectLibraryActionEndpoint(c, pk, "rename/"),
164
- identifiers: { path: a[0], newName: a[1], },
165
- payload: { oldPath: `/${normalizeLibraryPath(a[0])}`, newName: a[1], },
233
+ identifiers: { path: validPath, newName: validName, },
234
+ payload: { oldPath: `/${validPath}`, newName: validName, },
166
235
  });
167
236
  }
168
- await c.projectLibrary.rename(a[0], a[1], pk);
169
- return { renamed: a[0], to: a[1], };
237
+ await c.projectLibrary.rename(validPath, validName, pk);
238
+ return { renamed: validPath, to: validName, };
170
239
  },
171
240
  usage: "dss project-library rename <path> <new-name> [--dry-run] [--project-key KEY]",
172
241
  description: "Rename a project library file or folder within its parent.",
@@ -176,19 +245,21 @@ export const projectLibraryCommands = {
176
245
  handler: async (c, a, f) => {
177
246
  requireArgs(a, 2, "dss project-library move <path> <destination-folder> [--dry-run] [--project-key KEY]");
178
247
  const pk = f["project-key"];
248
+ const validPath = validateLibraryPath(a[0]);
249
+ const destination = validateLibraryDestinationPath(a[1]);
179
250
  if (f["dry-run"] === true) {
180
251
  return projectLibraryPlan("move", {
181
252
  method: "POST",
182
253
  endpoint: projectLibraryActionEndpoint(c, pk, "move"),
183
- identifiers: { path: a[0], destinationFolder: a[1], },
254
+ identifiers: { path: validPath, destinationFolder: destination, },
184
255
  payload: {
185
- oldPath: `/${normalizeLibraryPath(a[0])}`,
186
- newPath: normalizeLibraryDestinationPath(a[1]),
256
+ oldPath: `/${validPath}`,
257
+ newPath: destination,
187
258
  },
188
259
  });
189
260
  }
190
- await c.projectLibrary.move(a[0], a[1], pk);
191
- return { moved: a[0], to: a[1], };
261
+ await c.projectLibrary.move(validPath, destination, pk);
262
+ return { moved: validPath, to: destination, };
192
263
  },
193
264
  usage: "dss project-library move <path> <destination-folder> [--dry-run] [--project-key KEY]",
194
265
  description: "Move a project library file or folder into another folder.",
@@ -6,6 +6,13 @@ import { jsonInput, num, requiredJsonInput, requiredStringFlag, } from "../coerc
6
6
  import { CommandResultFailure, } from "../output.js";
7
7
  import { requireArgs, UsageError, } from "../usage.js";
8
8
  const INSPECT_ARCHIVE_USAGE = "dss project inspect-archive <file>";
9
+ function flowRenderFormat(value) {
10
+ if (value === undefined)
11
+ return undefined;
12
+ if (value === "ascii" || value === "mermaid")
13
+ return value;
14
+ throw new UsageError("--render must be ascii or mermaid.", "invalid_enum");
15
+ }
9
16
  /** Local, DSS-free archive inspection shared by the local handler and the command handler. */
10
17
  function inspectArchiveCommand(args) {
11
18
  requireArgs(args, 1, INSPECT_ARCHIVE_USAGE);
@@ -42,12 +49,19 @@ export const projectCommands = {
42
49
  maxNodes: num(f["max-nodes"], "--max-nodes"),
43
50
  maxEdges: num(f["max-edges"], "--max-edges"),
44
51
  includeRaw: f["include-raw"] === true,
52
+ render: flowRenderFormat(f["render"]),
45
53
  }),
46
- usage: "dss project map [--max-nodes N] [--max-edges N] [--include-raw] [--project-key KEY]",
47
- description: "Get a summarized, truncated flow map.",
54
+ validate: (_a, f) => {
55
+ flowRenderFormat(f["render"]);
56
+ num(f["max-nodes"], "--max-nodes");
57
+ num(f["max-edges"], "--max-edges");
58
+ },
59
+ usage: "dss project map [--max-nodes N] [--max-edges N] [--include-raw] [--render ascii|mermaid] [--project-key KEY]",
60
+ description: "Get an analyzed flow map with zones, layers, components, and diagnostics.",
48
61
  examples: [
49
62
  "dss project map",
50
63
  "dss project map --max-nodes 50 --max-edges 100",
64
+ "dss project map --render mermaid",
51
65
  "dss project map --include-raw",
52
66
  ],
53
67
  },