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.
- package/README.md +132 -17
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +39 -4
- package/dist/packages/types/src/index.js +31 -2
- package/dist/src/cli/commands/api-service.js +62 -16
- package/dist/src/cli/commands/bundle.js +49 -15
- package/dist/src/cli/commands/code-env.js +210 -49
- package/dist/src/cli/commands/code.d.ts +19 -0
- package/dist/src/cli/commands/code.js +87 -15
- package/dist/src/cli/commands/dataset.js +8 -3
- package/dist/src/cli/commands/flow-zone.js +33 -5
- package/dist/src/cli/commands/notebook.js +72 -41
- package/dist/src/cli/commands/project-git.js +8 -5
- package/dist/src/cli/commands/project-library.d.ts +21 -0
- package/dist/src/cli/commands/project-library.js +121 -50
- package/dist/src/cli/commands/project.js +16 -2
- package/dist/src/cli/commands/recipe.js +37 -12
- package/dist/src/cli/commands/sql.d.ts +10 -0
- package/dist/src/cli/commands/sql.js +39 -17
- package/dist/src/cli/commands/webapp.js +67 -13
- package/dist/src/cli/contract.d.ts +15 -0
- package/dist/src/cli/contract.js +870 -107
- package/dist/src/cli/env.d.ts +9 -0
- package/dist/src/cli/env.js +20 -2
- package/dist/src/cli/flags.js +9 -0
- package/dist/src/cli/helpers/cleanup.js +104 -3
- package/dist/src/cli/helpers/flow-zone.d.ts +3 -0
- package/dist/src/cli/helpers/flow-zone.js +33 -3
- package/dist/src/cli/helpers/recipe.d.ts +20 -0
- package/dist/src/cli/helpers/recipe.js +158 -3
- package/dist/src/cli/output.d.ts +2 -0
- package/dist/src/cli/output.js +33 -6
- package/dist/src/cli/runtime.d.ts +35 -1
- package/dist/src/cli/runtime.js +125 -18
- package/dist/src/cli/types.d.ts +7 -0
- package/dist/src/cli.js +135 -28
- package/dist/src/client.d.ts +60 -0
- package/dist/src/client.js +237 -42
- package/dist/src/errors.d.ts +8 -2
- package/dist/src/errors.js +40 -0
- package/dist/src/index.d.ts +4 -3
- package/dist/src/index.js +2 -1
- package/dist/src/resources/api-services.d.ts +48 -6
- package/dist/src/resources/api-services.js +54 -8
- package/dist/src/resources/bundles.d.ts +51 -6
- package/dist/src/resources/bundles.js +39 -9
- package/dist/src/resources/code-envs.d.ts +25 -3
- package/dist/src/resources/code-envs.js +104 -2
- package/dist/src/resources/datasets.d.ts +20 -0
- package/dist/src/resources/datasets.js +232 -52
- package/dist/src/resources/jobs.d.ts +35 -10
- package/dist/src/resources/jobs.js +92 -17
- package/dist/src/resources/notebooks.d.ts +77 -7
- package/dist/src/resources/notebooks.js +143 -19
- package/dist/src/resources/project-git.d.ts +12 -0
- package/dist/src/resources/project-git.js +39 -8
- package/dist/src/resources/project-library.d.ts +122 -7
- package/dist/src/resources/project-library.js +494 -25
- package/dist/src/resources/projects.d.ts +5 -2
- package/dist/src/resources/projects.js +18 -2
- package/dist/src/resources/scenarios.d.ts +30 -0
- package/dist/src/resources/scenarios.js +73 -27
- package/dist/src/resources/sql.d.ts +6 -0
- package/dist/src/resources/sql.js +22 -2
- package/dist/src/resources/webapps.d.ts +35 -4
- package/dist/src/resources/webapps.js +54 -5
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/skill.d.ts +9 -0
- package/dist/src/skill.js +37 -4
- package/dist/src/utils/flow-analysis.d.ts +53 -0
- package/dist/src/utils/flow-analysis.js +382 -0
- package/dist/src/utils/project-archive.d.ts +41 -2
- package/dist/src/utils/project-archive.js +159 -16
- package/package.json +4 -4
- package/packages/types/dist/index.d.ts +39 -4
- package/packages/types/dist/index.js +31 -2
- package/plugin.json +1 -1
- 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
|
|
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 (
|
|
242
|
-
await c.flowZones.moveItems(zone.id,
|
|
243
|
-
moved.push({ zoneId: zone.id, name: zone.name, 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,
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
100
|
-
description: "Unload a Jupyter notebook kernel session.",
|
|
101
|
-
examples: [
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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),
|
|
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),
|
|
472
|
-
return { removed:
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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"]
|
|
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
|
-
|
|
132
|
-
return { updated: a[0], };
|
|
195
|
+
return c.projectLibrary.diffFile(a[0], local, pk, maxLines !== undefined ? { maxLines, } : {});
|
|
133
196
|
},
|
|
134
|
-
usage: "dss project-library
|
|
135
|
-
description: "
|
|
136
|
-
examples: [
|
|
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:
|
|
165
|
-
payload: { oldPath: `/${
|
|
233
|
+
identifiers: { path: validPath, newName: validName, },
|
|
234
|
+
payload: { oldPath: `/${validPath}`, newName: validName, },
|
|
166
235
|
});
|
|
167
236
|
}
|
|
168
|
-
await c.projectLibrary.rename(
|
|
169
|
-
return { renamed:
|
|
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:
|
|
254
|
+
identifiers: { path: validPath, destinationFolder: destination, },
|
|
184
255
|
payload: {
|
|
185
|
-
oldPath: `/${
|
|
186
|
-
newPath:
|
|
256
|
+
oldPath: `/${validPath}`,
|
|
257
|
+
newPath: destination,
|
|
187
258
|
},
|
|
188
259
|
});
|
|
189
260
|
}
|
|
190
|
-
await c.projectLibrary.move(
|
|
191
|
-
return { moved:
|
|
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
|
-
|
|
47
|
-
|
|
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
|
},
|