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,12 +1,18 @@
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 { executionMode, } from "../flags.js";
3
+ import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
4
+ import { requireArgs, requireNoArgs, UsageError, } from "../usage.js";
4
5
  export const notebookCommands = {
5
6
  "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",],
7
+ handler: (c, _a, f) => c.notebooks.listJupyter(f["project-key"], {
8
+ active: parseBooleanOption(f["active"], "--active"),
9
+ }),
10
+ usage: "dss notebook list-jupyter [--active true|false] [--project-key KEY]",
11
+ description: "List Jupyter notebooks. Pass --active to use the official ?active= filter: true lists currently running notebooks, false lists non-running ones.",
12
+ examples: [
13
+ "dss notebook list-jupyter",
14
+ "dss notebook list-jupyter --active true",
15
+ ],
10
16
  },
11
17
  "get-jupyter": {
12
18
  handler: (c, a, f) => {
@@ -21,11 +27,11 @@ export const notebookCommands = {
21
27
  handler: async (c, a, f) => {
22
28
  requireArgs(a, 1, "dss notebook delete-jupyter <name>");
23
29
  const pk = f["project-key"];
24
- if (f["dry-run"] === true || f["if-exists"] === true) {
30
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
25
31
  const current = await readIfExists(() => c.notebooks.getJupyter(a[0], pk));
26
32
  if (!current)
27
33
  return skipResult("jupyter-notebook", a[0], "missing");
28
- if (f["dry-run"] === true) {
34
+ if (executionMode(f).dryRun) {
29
35
  return { dryRun: true, action: "delete", resource: "jupyter-notebook", name: a[0], current, };
30
36
  }
31
37
  }
@@ -40,23 +46,20 @@ export const notebookCommands = {
40
46
  handler: async (c, a, f) => {
41
47
  requireArgs(a, 1, "dss notebook clear-jupyter-outputs <name>");
42
48
  const pk = f["project-key"];
43
- if (f["dry-run"] === true) {
49
+ if (executionMode(f).dryRun) {
50
+ // The real mutation is a single server-side DELETE of the
51
+ // outputs endpoint, so the dry run can only report the current
52
+ // content (read-only) and the exact request that would run; the
53
+ // stripped `next` shape would misdescribe the operation.
44
54
  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
55
  return {
54
56
  dryRun: true,
55
57
  action: "clear-jupyter-outputs",
56
58
  resource: "jupyter-notebook",
57
59
  name: a[0],
58
60
  current,
59
- next,
61
+ endpoint: encodedProjectEndpoint(c, pk, `/jupyter-notebooks/${encodeURIComponent(a[0])}/outputs`),
62
+ method: "DELETE",
60
63
  };
61
64
  }
62
65
  await c.notebooks.clearJupyterOutputs(a[0], pk);
@@ -77,9 +80,34 @@ export const notebookCommands = {
77
80
  },
78
81
  "unload-jupyter": {
79
82
  handler: async (c, a, f) => {
80
- requireArgs(a, 2, "dss notebook unload-jupyter <name> <sessionId>");
81
83
  const pk = f["project-key"];
82
- if (f["dry-run"] === true) {
84
+ if (f["all"] === true) {
85
+ requireNoArgs(a, "dss notebook unload-jupyter --all [--dry-run] [--project-key KEY]");
86
+ if (executionMode(f).dryRun) {
87
+ const planned = [];
88
+ for (const notebook of await c.notebooks.listJupyter(pk, { active: true, })) {
89
+ for (const session of await c.notebooks.listJupyterSessions(notebook.name, pk)) {
90
+ planned.push({
91
+ name: notebook.name,
92
+ sessionId: session.sessionId,
93
+ endpoint: encodedProjectEndpoint(c, pk, `/jupyter-notebooks/${encodeURIComponent(notebook.name)}/sessions/${encodeURIComponent(session.sessionId)}`),
94
+ method: "DELETE",
95
+ });
96
+ }
97
+ }
98
+ return {
99
+ dryRun: true,
100
+ action: "unload-jupyter",
101
+ resource: "jupyter-notebook",
102
+ all: true,
103
+ planned,
104
+ };
105
+ }
106
+ const unloaded = await c.notebooks.unloadJupyterAll(pk);
107
+ return { unloaded, resource: "jupyter-notebook", all: true, };
108
+ }
109
+ requireArgs(a, 2, "dss notebook unload-jupyter <name> <sessionId>");
110
+ if (executionMode(f).dryRun) {
83
111
  const sessions = await c.notebooks.listJupyterSessions(a[0], pk);
84
112
  const current = sessions.find((session) => session.sessionId === a[1]);
85
113
  return {
@@ -96,9 +124,12 @@ export const notebookCommands = {
96
124
  await c.notebooks.unloadJupyter(a[0], a[1], pk);
97
125
  return { unloaded: a[0], sessionId: a[1], resource: "jupyter-notebook", };
98
126
  },
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",],
127
+ usage: "dss notebook unload-jupyter (<name> <sessionId>|--all) [--dry-run] [--project-key KEY]",
128
+ description: "Unload a Jupyter notebook kernel session, or every session of every running notebook with --all.",
129
+ examples: [
130
+ "dss notebook unload-jupyter my_notebook SESSION_ID --dry-run",
131
+ "dss notebook unload-jupyter --all --dry-run",
132
+ ],
102
133
  },
103
134
  "list-sql": {
104
135
  handler: (c, _a, f) => c.notebooks.listSql(f["project-key"]),
@@ -119,11 +150,11 @@ export const notebookCommands = {
119
150
  handler: async (c, a, f) => {
120
151
  requireArgs(a, 1, "dss notebook delete-sql <id>");
121
152
  const pk = f["project-key"];
122
- if (f["dry-run"] === true || f["if-exists"] === true) {
153
+ if (executionMode(f).dryRun || f["if-exists"] === true) {
123
154
  const current = await readIfExists(() => c.notebooks.getSql(a[0], pk));
124
155
  if (!current)
125
156
  return skipResult("sql-notebook", a[0], "missing");
126
- if (f["dry-run"] === true) {
157
+ if (executionMode(f).dryRun) {
127
158
  return { dryRun: true, action: "delete", resource: "sql-notebook", id: a[0], current, };
128
159
  }
129
160
  }
@@ -151,7 +182,7 @@ export const notebookCommands = {
151
182
  throw new UsageError("--data, --data-file, or --stdin is required (notebook JSON content).");
152
183
  }
153
184
  const pk = f["project-key"];
154
- if (f["dry-run"] === true) {
185
+ if (executionMode(f).dryRun) {
155
186
  const current = await readIfExists(() => c.notebooks.getJupyter(a[0], pk));
156
187
  return {
157
188
  dryRun: true,
@@ -162,18 +193,18 @@ export const notebookCommands = {
162
193
  next: data,
163
194
  };
164
195
  }
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", };
196
+ const result = await c.notebooks.saveOrCreateJupyter(a[0], data, pk, {
197
+ expectHash: f["expect-hash"],
198
+ });
199
+ return {
200
+ saved: a[0],
201
+ resource: "jupyter-notebook",
202
+ created: result.created,
203
+ hash: result.hash,
204
+ };
174
205
  },
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.",
206
+ usage: "dss notebook save-jupyter <name> (--data '{...}' | --data-file PATH | --stdin) [--expect-hash SHA256] [--dry-run] [--project-key KEY]",
207
+ 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
208
  examples: [
178
209
  "dss notebook save-jupyter my_notebook --data-file notebook.json --dry-run",
179
210
  "cat notebook.json | dss notebook save-jupyter my_notebook --stdin",
@@ -187,7 +218,7 @@ export const notebookCommands = {
187
218
  throw new UsageError("--data, --data-file, or --stdin is required (SQL notebook content JSON).");
188
219
  }
189
220
  const pk = f["project-key"];
190
- if (f["dry-run"] === true) {
221
+ if (executionMode(f).dryRun) {
191
222
  const current = await readIfExists(() => c.notebooks.getSql(a[0], pk));
192
223
  return {
193
224
  dryRun: true,
@@ -198,17 +229,18 @@ export const notebookCommands = {
198
229
  next: data,
199
230
  };
200
231
  }
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", };
232
+ const result = await c.notebooks.saveOrCreateSql(a[0], data, pk, {
233
+ expectHash: f["expect-hash"],
234
+ });
235
+ return {
236
+ saved: a[0],
237
+ resource: "sql-notebook",
238
+ created: result.created,
239
+ hash: result.hash,
240
+ };
209
241
  },
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.",
242
+ usage: "dss notebook save-sql <id> (--data '{...}' | --data-file PATH | --stdin) [--expect-hash SHA256] [--dry-run] [--project-key KEY]",
243
+ 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
244
  examples: ["dss notebook save-sql my_sql_notebook --data-file content.json --dry-run",],
213
245
  },
214
246
  "clear-sql-history": {
@@ -220,7 +252,7 @@ export const notebookCommands = {
220
252
  numRunsToRetain: num(f["retain"], "--retain"),
221
253
  projectKey: pk,
222
254
  };
223
- if (f["dry-run"] === true) {
255
+ if (executionMode(f).dryRun) {
224
256
  const current = await c.notebooks.getSqlHistory(a[0], pk);
225
257
  return {
226
258
  dryRun: true,
@@ -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,13 @@
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 { executionMode, } from "../flags.js";
5
+ import { encodedProjectEndpoint, planResult, skipResult, } from "../output.js";
4
6
  import { requireArgs, UsageError, } from "../usage.js";
5
7
  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("/");
8
+ const PUT_FLAG_ERROR_HINT = "Pass a 64-character SHA-256 hex digest (lowercase or uppercase).";
9
+ function sha256BytesHex(bytes) {
10
+ return new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
22
11
  }
23
12
  function projectLibraryContentsEndpoint(client, projectKey, path) {
24
13
  return encodedProjectEndpoint(client, projectKey, `/libraries/contents/${encodeLibraryPath(path)}`);
@@ -29,22 +18,53 @@ function projectLibraryFolderEndpoint(client, projectKey, path) {
29
18
  function projectLibraryActionEndpoint(client, projectKey, action) {
30
19
  return encodedProjectEndpoint(client, projectKey, `/libraries/contents-actions/${action}`);
31
20
  }
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", };
21
+ /**
22
+ * Put-input metadata for plans and reports. Plans must never include content
23
+ * bytes: this helper reports only the source, byte count, and SHA-256 digest —
24
+ * and only when they are available without consuming stdin (dry-run keeps
25
+ * stdin unread, so the stdin source carries no size/hash).
26
+ */
27
+ export function projectLibraryPutPayload(flags) {
28
+ const expectSha256 = expectSha256FromFlags(flags);
29
+ if (typeof flags["content"] === "string") {
30
+ const text = flags["content"];
31
+ return {
32
+ contentSource: "flag",
33
+ bytes: Buffer.byteLength(text, "utf8"),
34
+ sha256: sha256Hex(text),
35
+ ...(expectSha256 ? { expectSha256, } : {}),
36
+ };
37
+ }
38
+ if (typeof flags["file"] === "string") {
39
+ const bytes = fs.readFileSync(flags["file"]);
40
+ return {
41
+ contentSource: "file",
42
+ file: flags["file"],
43
+ bytes: bytes.length,
44
+ sha256: sha256BytesHex(bytes),
45
+ ...(expectSha256 ? { expectSha256, } : {}),
46
+ };
47
+ }
48
+ return { contentSource: "stdin", ...(expectSha256 ? { expectSha256, } : {}), };
38
49
  }
39
50
  function projectLibraryPlan(action, options) {
40
51
  return planResult("project-library", action, {
41
52
  ...options,
53
+ idempotency: options.idempotency ?? "none",
42
54
  asyncKind: "none",
43
55
  exitCodesOnFailure: PROJECT_LIBRARY_EXIT_CODES,
44
- idempotency: "none",
45
56
  plannedAndDryRun: true,
46
57
  });
47
58
  }
59
+ function expectSha256FromFlags(flags) {
60
+ const value = flags["expect-sha256"];
61
+ if (value === undefined || value === false)
62
+ return undefined;
63
+ if (typeof value !== "string" || !EXPECT_SHA256_PATTERN.test(value)) {
64
+ throw new UsageError("--expect-sha256 must be a 64-character SHA-256 hex digest.", "validation_failed", PUT_FLAG_ERROR_HINT);
65
+ }
66
+ return value;
67
+ }
48
68
  export const projectLibraryCommands = {
49
69
  list: {
50
70
  handler: (c, _a, f) => c.projectLibrary.listContents(f["project-key"]),
@@ -55,6 +75,7 @@ export const projectLibraryCommands = {
55
75
  get: {
56
76
  handler: (c, a, f) => {
57
77
  requireArgs(a, 1, "dss project-library get <path> [--project-key KEY]");
78
+ validateLibraryPath(a[0]);
58
79
  return c.projectLibrary.getFile(a[0], f["project-key"]);
59
80
  },
60
81
  usage: "dss project-library get <path> [--project-key KEY]",
@@ -69,7 +90,7 @@ export const projectLibraryCommands = {
69
90
  throw new UsageError("--output PATH is required.", "missing_required_flag");
70
91
  const bytes = await c.projectLibrary.getFileBytes(a[0], f["project-key"]);
71
92
  const written = await Bun.write(out, bytes, { createPath: false, });
72
- return { path: out, bytes: written, };
93
+ return { path: out, bytes: written, sha256: sha256BytesHex(bytes), };
73
94
  },
74
95
  usage: "dss project-library get-bytes <path> --output PATH [--project-key KEY]",
75
96
  description: "Download a project library file's raw bytes to a local file.",
@@ -77,45 +98,63 @@ export const projectLibraryCommands = {
77
98
  },
78
99
  "create-file": {
79
100
  handler: async (c, a, f) => {
80
- requireArgs(a, 1, "dss project-library create-file <path> [--dry-run] [--project-key KEY]");
101
+ requireArgs(a, 1, "dss project-library create-file <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
81
102
  const pk = f["project-key"];
82
- if (f["dry-run"] === true) {
103
+ validateLibraryPath(a[0]);
104
+ if (executionMode(f).dryRun) {
83
105
  return projectLibraryPlan("create-file", {
84
106
  method: "POST",
85
107
  endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
86
108
  identifiers: { path: a[0], },
109
+ idempotency: "if-not-exists",
87
110
  });
88
111
  }
112
+ if (f["if-not-exists"] === true) {
113
+ const exists = await c.projectLibrary.hasLibraryItem(a[0], pk);
114
+ if (exists) {
115
+ return skipResult("project-library", a[0], "exists", { kind: "file", });
116
+ }
117
+ }
89
118
  await c.projectLibrary.addFile(a[0], pk);
90
119
  return { created: a[0], };
91
120
  },
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",],
121
+ usage: "dss project-library create-file <path> [--if-not-exists] [--dry-run] [--project-key KEY]",
122
+ description: "Create an empty file in the project library; refuses to overwrite an existing item.",
123
+ examples: ["dss project-library create-file python/mylib/new.py --if-not-exists",],
95
124
  },
96
125
  "create-folder": {
97
126
  handler: async (c, a, f) => {
98
- requireArgs(a, 1, "dss project-library create-folder <path> [--dry-run] [--project-key KEY]");
127
+ requireArgs(a, 1, "dss project-library create-folder <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
99
128
  const pk = f["project-key"];
100
- if (f["dry-run"] === true) {
129
+ validateLibraryPath(a[0]);
130
+ if (executionMode(f).dryRun) {
101
131
  return projectLibraryPlan("create-folder", {
102
132
  method: "POST",
103
133
  endpoint: projectLibraryFolderEndpoint(c, pk, a[0]),
104
134
  identifiers: { path: a[0], },
135
+ idempotency: "if-not-exists",
105
136
  });
106
137
  }
138
+ if (f["if-not-exists"] === true) {
139
+ const exists = await c.projectLibrary.hasLibraryItem(a[0], pk);
140
+ if (exists) {
141
+ return skipResult("project-library", a[0], "exists", { kind: "folder", });
142
+ }
143
+ }
107
144
  await c.projectLibrary.addFolder(a[0], pk);
108
145
  return { created: a[0], };
109
146
  },
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",],
147
+ usage: "dss project-library create-folder <path> [--if-not-exists] [--dry-run] [--project-key KEY]",
148
+ description: "Create a folder in the project library; refuses to overwrite an existing item.",
149
+ examples: ["dss project-library create-folder python/mylib --if-not-exists",],
113
150
  },
114
151
  put: {
115
152
  handler: async (c, a, f) => {
116
- requireArgs(a, 1, "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--dry-run] [--project-key KEY]");
153
+ requireArgs(a, 1, "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--expect-sha256 SHA256] [--dry-run] [--project-key KEY]");
117
154
  const pk = f["project-key"];
118
- if (f["dry-run"] === true) {
155
+ const expectSha256 = expectSha256FromFlags(f);
156
+ validateLibraryPath(a[0]);
157
+ if (executionMode(f).dryRun) {
119
158
  return projectLibraryPlan("put", {
120
159
  method: "POST",
121
160
  endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
@@ -126,20 +165,49 @@ export const projectLibraryCommands = {
126
165
  const content = typeof f["content"] === "string"
127
166
  ? f["content"]
128
167
  : typeof f["file"] === "string"
129
- ? fs.readFileSync(f["file"], "utf-8")
168
+ ? fs.readFileSync(f["file"])
169
+ : await readStdinText();
170
+ const result = await c.projectLibrary.addOrUpdateFile(a[0], content, pk, expectSha256 !== undefined ? { expectSha256, } : undefined);
171
+ return {
172
+ updated: result.path,
173
+ bytes: result.bytes,
174
+ sha256: result.sha256,
175
+ ...(result.beforeSha256 !== undefined ? { beforeSha256: result.beforeSha256, } : {}),
176
+ };
177
+ },
178
+ usage: "dss project-library put <path> (--content TEXT|--file PATH|--stdin) [--expect-sha256 SHA256] [--dry-run] [--project-key KEY]",
179
+ description: "Create or overwrite a project library file with text or binary content; reports the written byte count and sha256.",
180
+ examples: [
181
+ "dss project-library put python/mylib/utils.py --file ./utils.py",
182
+ "dss project-library put python/mylib/utils.py --file ./utils.py --expect-sha256 <previous-sha256>",
183
+ ],
184
+ },
185
+ diff: {
186
+ handler: async (c, a, f) => {
187
+ requireArgs(a, 1, "dss project-library diff <path> (--content TEXT|--file PATH|--stdin) [--max-lines N] [--project-key KEY]");
188
+ const pk = f["project-key"];
189
+ validateLibraryPath(a[0]);
190
+ const maxLines = numFlag(f, ["max-lines",]);
191
+ const local = typeof f["content"] === "string"
192
+ ? f["content"]
193
+ : typeof f["file"] === "string"
194
+ ? fs.readFileSync(f["file"])
130
195
  : await readStdinText();
131
- await c.projectLibrary.addOrUpdateFile(a[0], content, pk);
132
- return { updated: a[0], };
196
+ return c.projectLibrary.diffFile(a[0], local, pk, maxLines !== undefined ? { maxLines, } : {});
133
197
  },
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",],
198
+ usage: "dss project-library diff <path> (--content TEXT|--file PATH|--stdin) [--max-lines N] [--project-key KEY]",
199
+ description: "Diff a project library file against local content; capped unified text diff, binary files detected instead of dumped.",
200
+ examples: [
201
+ "dss project-library diff python/mylib/utils.py --file ./utils.py",
202
+ "dss project-library diff python/mylib/utils.py --content 'print(1)' --max-lines 50",
203
+ ],
137
204
  },
138
205
  delete: {
139
206
  handler: async (c, a, f) => {
140
207
  requireArgs(a, 1, "dss project-library delete <path> [--dry-run] [--project-key KEY]");
141
208
  const pk = f["project-key"];
142
- if (f["dry-run"] === true) {
209
+ validateLibraryPath(a[0]);
210
+ if (executionMode(f).dryRun) {
143
211
  return projectLibraryPlan("delete", {
144
212
  method: "DELETE",
145
213
  endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
@@ -157,16 +225,18 @@ export const projectLibraryCommands = {
157
225
  handler: async (c, a, f) => {
158
226
  requireArgs(a, 2, "dss project-library rename <path> <new-name> [--dry-run] [--project-key KEY]");
159
227
  const pk = f["project-key"];
160
- if (f["dry-run"] === true) {
228
+ const validPath = validateLibraryPath(a[0]);
229
+ const validName = validateLibraryName(a[1]);
230
+ if (executionMode(f).dryRun) {
161
231
  return projectLibraryPlan("rename", {
162
232
  method: "POST",
163
233
  endpoint: projectLibraryActionEndpoint(c, pk, "rename/"),
164
- identifiers: { path: a[0], newName: a[1], },
165
- payload: { oldPath: `/${normalizeLibraryPath(a[0])}`, newName: a[1], },
234
+ identifiers: { path: validPath, newName: validName, },
235
+ payload: { oldPath: `/${validPath}`, newName: validName, },
166
236
  });
167
237
  }
168
- await c.projectLibrary.rename(a[0], a[1], pk);
169
- return { renamed: a[0], to: a[1], };
238
+ await c.projectLibrary.rename(validPath, validName, pk);
239
+ return { renamed: validPath, to: validName, };
170
240
  },
171
241
  usage: "dss project-library rename <path> <new-name> [--dry-run] [--project-key KEY]",
172
242
  description: "Rename a project library file or folder within its parent.",
@@ -176,19 +246,21 @@ export const projectLibraryCommands = {
176
246
  handler: async (c, a, f) => {
177
247
  requireArgs(a, 2, "dss project-library move <path> <destination-folder> [--dry-run] [--project-key KEY]");
178
248
  const pk = f["project-key"];
179
- if (f["dry-run"] === true) {
249
+ const validPath = validateLibraryPath(a[0]);
250
+ const destination = validateLibraryDestinationPath(a[1]);
251
+ if (executionMode(f).dryRun) {
180
252
  return projectLibraryPlan("move", {
181
253
  method: "POST",
182
254
  endpoint: projectLibraryActionEndpoint(c, pk, "move"),
183
- identifiers: { path: a[0], destinationFolder: a[1], },
255
+ identifiers: { path: validPath, destinationFolder: destination, },
184
256
  payload: {
185
- oldPath: `/${normalizeLibraryPath(a[0])}`,
186
- newPath: normalizeLibraryDestinationPath(a[1]),
257
+ oldPath: `/${validPath}`,
258
+ newPath: destination,
187
259
  },
188
260
  });
189
261
  }
190
- await c.projectLibrary.move(a[0], a[1], pk);
191
- return { moved: a[0], to: a[1], };
262
+ await c.projectLibrary.move(validPath, destination, pk);
263
+ return { moved: validPath, to: destination, };
192
264
  },
193
265
  usage: "dss project-library move <path> <destination-folder> [--dry-run] [--project-key KEY]",
194
266
  description: "Move a project library file or folder into another folder.",
@@ -3,9 +3,17 @@ import { deepMerge, } from "../../utils/deep-merge.js";
3
3
  import { inspectProjectArchive, } from "../../utils/project-archive.js";
4
4
  import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
5
5
  import { jsonInput, num, requiredJsonInput, requiredStringFlag, } from "../coerce.js";
6
+ import { executionMode, } from "../flags.js";
6
7
  import { CommandResultFailure, } from "../output.js";
7
8
  import { requireArgs, UsageError, } from "../usage.js";
8
9
  const INSPECT_ARCHIVE_USAGE = "dss project inspect-archive <file>";
10
+ function flowRenderFormat(value) {
11
+ if (value === undefined)
12
+ return undefined;
13
+ if (value === "ascii" || value === "mermaid")
14
+ return value;
15
+ throw new UsageError("--render must be ascii or mermaid.", "invalid_enum");
16
+ }
9
17
  /** Local, DSS-free archive inspection shared by the local handler and the command handler. */
10
18
  function inspectArchiveCommand(args) {
11
19
  requireArgs(args, 1, INSPECT_ARCHIVE_USAGE);
@@ -42,12 +50,19 @@ export const projectCommands = {
42
50
  maxNodes: num(f["max-nodes"], "--max-nodes"),
43
51
  maxEdges: num(f["max-edges"], "--max-edges"),
44
52
  includeRaw: f["include-raw"] === true,
53
+ render: flowRenderFormat(f["render"]),
45
54
  }),
46
- usage: "dss project map [--max-nodes N] [--max-edges N] [--include-raw] [--project-key KEY]",
47
- description: "Get a summarized, truncated flow map.",
55
+ validate: (_a, f) => {
56
+ flowRenderFormat(f["render"]);
57
+ num(f["max-nodes"], "--max-nodes");
58
+ num(f["max-edges"], "--max-edges");
59
+ },
60
+ usage: "dss project map [--max-nodes N] [--max-edges N] [--include-raw] [--render ascii|mermaid] [--project-key KEY]",
61
+ description: "Get an analyzed flow map with zones, layers, components, and diagnostics.",
48
62
  examples: [
49
63
  "dss project map",
50
64
  "dss project map --max-nodes 50 --max-edges 100",
65
+ "dss project map --render mermaid",
51
66
  "dss project map --include-raw",
52
67
  ],
53
68
  },
@@ -77,7 +92,7 @@ export const projectCommands = {
77
92
  throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
78
93
  }
79
94
  const guarded = f["if-exists"] === true
80
- || f["dry-run"] === true
95
+ || executionMode(f).dryRun
81
96
  || expectedIncarnation !== undefined;
82
97
  if (!guarded) {
83
98
  // Purely user-requested delete without any guard flag: keep the
@@ -114,7 +129,7 @@ export const projectCommands = {
114
129
  }
115
130
  throw error;
116
131
  }
117
- if (f["dry-run"] === true) {
132
+ if (executionMode(f).dryRun) {
118
133
  return {
119
134
  deleted: false,
120
135
  dryRun: true,