dataiku-sdk 3.1.0 → 3.3.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 +253 -38
- package/dist/build-metadata.json +1 -1
- package/dist/packages/types/src/index.d.ts +102 -0
- package/dist/packages/types/src/index.js +76 -0
- package/dist/src/auth.js +1 -1
- package/dist/src/cli/coerce.js +11 -2
- package/dist/src/cli/commands/analysis.js +4 -3
- package/dist/src/cli/commands/api-service.js +5 -3
- package/dist/src/cli/commands/app.js +61 -18
- package/dist/src/cli/commands/bundle.js +3 -2
- package/dist/src/cli/commands/code-env.js +23 -16
- package/dist/src/cli/commands/connection.d.ts +9 -0
- package/dist/src/cli/commands/connection.js +267 -1
- package/dist/src/cli/commands/dashboard.js +7 -6
- package/dist/src/cli/commands/data-collection.d.ts +2 -0
- package/dist/src/cli/commands/data-collection.js +164 -0
- package/dist/src/cli/commands/data-quality.js +9 -8
- package/dist/src/cli/commands/dataset.js +170 -40
- package/dist/src/cli/commands/doctor.js +4 -2
- package/dist/src/cli/commands/flow-zone.js +9 -8
- package/dist/src/cli/commands/folder.js +9 -8
- package/dist/src/cli/commands/future.js +4 -3
- package/dist/src/cli/commands/group.d.ts +2 -0
- package/dist/src/cli/commands/group.js +109 -0
- package/dist/src/cli/commands/index.js +151 -78
- package/dist/src/cli/commands/insight.js +8 -7
- package/dist/src/cli/commands/job.js +4 -3
- package/dist/src/cli/commands/knowledge-bank.d.ts +2 -0
- package/dist/src/cli/commands/knowledge-bank.js +80 -0
- package/dist/src/cli/commands/llm.d.ts +2 -0
- package/dist/src/cli/commands/llm.js +93 -0
- package/dist/src/cli/commands/macro.d.ts +2 -0
- package/dist/src/cli/commands/macro.js +221 -0
- package/dist/src/cli/commands/meaning.js +5 -4
- package/dist/src/cli/commands/ml-task.js +21 -8
- package/dist/src/cli/commands/model-evaluation-store.js +4 -3
- package/dist/src/cli/commands/notebook.js +17 -12
- package/dist/src/cli/commands/plugin.d.ts +2 -0
- package/dist/src/cli/commands/plugin.js +799 -0
- package/dist/src/cli/commands/project-folder.d.ts +2 -0
- package/dist/src/cli/commands/project-folder.js +197 -0
- package/dist/src/cli/commands/project-git.js +14 -54
- package/dist/src/cli/commands/project-library.js +7 -6
- package/dist/src/cli/commands/project.js +55 -8
- package/dist/src/cli/commands/recipe.js +50 -14
- package/dist/src/cli/commands/saved-model.d.ts +5 -0
- package/dist/src/cli/commands/saved-model.js +392 -4
- package/dist/src/cli/commands/scenario.js +147 -9
- package/dist/src/cli/commands/sql.js +3 -2
- package/dist/src/cli/commands/statistics.js +7 -6
- package/dist/src/cli/commands/user.d.ts +8 -0
- package/dist/src/cli/commands/user.js +296 -0
- package/dist/src/cli/commands/variable.js +2 -1
- package/dist/src/cli/commands/webapp.js +2 -1
- package/dist/src/cli/commands/wiki.js +7 -6
- package/dist/src/cli/contract.d.ts +2 -3
- package/dist/src/cli/contract.js +1167 -163
- package/dist/src/cli/flags.d.ts +4 -0
- package/dist/src/cli/flags.js +50 -2
- package/dist/src/cli/helpers/app-successor.d.ts +19 -4
- package/dist/src/cli/helpers/app-successor.js +90 -30
- package/dist/src/cli/helpers/cleanup.js +3 -1
- package/dist/src/cli/helpers/flow-zone.js +1 -2
- package/dist/src/cli/output.d.ts +1 -1
- package/dist/src/cli/output.js +23 -10
- package/dist/src/cli/usage.d.ts +1 -0
- package/dist/src/cli/usage.js +47 -1
- package/dist/src/cli.js +225 -95
- package/dist/src/client.d.ts +86 -6
- package/dist/src/client.js +263 -50
- package/dist/src/errors.d.ts +3 -0
- package/dist/src/errors.js +4 -0
- package/dist/src/index.d.ts +16 -7
- package/dist/src/index.js +11 -2
- package/dist/src/resources/analyses.js +1 -8
- package/dist/src/resources/applications.d.ts +8 -6
- package/dist/src/resources/applications.js +21 -21
- package/dist/src/resources/base.d.ts +4 -0
- package/dist/src/resources/base.js +15 -0
- package/dist/src/resources/bundles.d.ts +9 -4
- package/dist/src/resources/bundles.js +10 -5
- package/dist/src/resources/connections.d.ts +78 -1
- package/dist/src/resources/connections.js +132 -1
- package/dist/src/resources/data-collections.d.ts +118 -0
- package/dist/src/resources/data-collections.js +49 -0
- package/dist/src/resources/datasets.d.ts +69 -0
- package/dist/src/resources/datasets.js +99 -1
- package/dist/src/resources/folders.js +2 -1
- package/dist/src/resources/futures.d.ts +4 -1
- package/dist/src/resources/futures.js +38 -6
- package/dist/src/resources/groups.d.ts +47 -0
- package/dist/src/resources/groups.js +56 -0
- package/dist/src/resources/jobs.d.ts +8 -13
- package/dist/src/resources/jobs.js +41 -16
- package/dist/src/resources/knowledge-banks.d.ts +58 -0
- package/dist/src/resources/knowledge-banks.js +61 -0
- package/dist/src/resources/llms.d.ts +92 -0
- package/dist/src/resources/llms.js +62 -0
- package/dist/src/resources/macros.d.ts +121 -0
- package/dist/src/resources/macros.js +231 -0
- package/dist/src/resources/ml-tasks.d.ts +37 -2
- package/dist/src/resources/ml-tasks.js +100 -20
- package/dist/src/resources/model-evaluation-stores.js +1 -8
- package/dist/src/resources/notebooks.d.ts +43 -7
- package/dist/src/resources/notebooks.js +53 -13
- package/dist/src/resources/plugins.d.ts +265 -0
- package/dist/src/resources/plugins.js +458 -0
- package/dist/src/resources/project-folders.d.ts +71 -0
- package/dist/src/resources/project-folders.js +57 -0
- package/dist/src/resources/project-git.d.ts +6 -0
- package/dist/src/resources/project-git.js +69 -42
- package/dist/src/resources/project-library.d.ts +0 -1
- package/dist/src/resources/project-library.js +1 -17
- package/dist/src/resources/projects.d.ts +19 -3
- package/dist/src/resources/projects.js +30 -18
- package/dist/src/resources/recipes.d.ts +15 -1
- package/dist/src/resources/recipes.js +73 -6
- package/dist/src/resources/saved-models.d.ts +134 -1
- package/dist/src/resources/saved-models.js +214 -6
- package/dist/src/resources/scenarios.d.ts +105 -0
- package/dist/src/resources/scenarios.js +232 -106
- package/dist/src/resources/sql.js +5 -1
- package/dist/src/resources/users.d.ts +129 -0
- package/dist/src/resources/users.js +128 -0
- package/dist/src/resources/webapps.d.ts +15 -2
- package/dist/src/resources/webapps.js +18 -4
- package/dist/src/schemas.d.ts +2 -2
- package/dist/src/schemas.js +1 -1
- package/dist/src/skill.d.ts +13 -4
- package/dist/src/skill.js +75 -24
- package/dist/src/utils/deep-merge.d.ts +1 -1
- package/dist/src/utils/polling.d.ts +20 -0
- package/dist/src/utils/polling.js +27 -0
- package/dist/src/utils/response-file.d.ts +2 -0
- package/dist/src/utils/response-file.js +34 -0
- package/dist/src/utils/secret-sanitize.d.ts +54 -0
- package/dist/src/utils/secret-sanitize.js +101 -0
- package/package.json +8 -5
- package/packages/types/dist/index.d.ts +102 -0
- package/packages/types/dist/index.js +76 -0
- package/plugin.json +1 -1
- package/skills/dataiku-dss/SKILL.md +47 -225
- package/skills/dataiku-dss/references/app-releases.md +28 -0
- package/skills/dataiku-dss/references/authentication.md +39 -0
- package/skills/dataiku-dss/references/coding.md +8 -0
- package/skills/dataiku-dss/references/discovery.md +30 -0
- package/skills/dataiku-dss/references/flow-maps.md +12 -0
- package/skills/dataiku-dss/references/mutations.md +33 -0
- package/skills/dataiku-dss/references/troubleshooting.md +29 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { requiredJsonInput, } from "../coerce.js";
|
|
2
|
+
import { executionMode, } from "../flags.js";
|
|
3
|
+
import { isNotFoundError, planResult, skipResult, } from "../output.js";
|
|
4
|
+
import { requireArgs, UsageError, } from "../usage.js";
|
|
5
|
+
const PLAN_FAILURE_CODES = { usage: 1, error: 2, transient: 3, };
|
|
6
|
+
function folderEndpoint(folderId, suffix = "") {
|
|
7
|
+
return `/public/api/project-folders/${encodeURIComponent(folderId)}${suffix}`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Positional ids are server keys: an empty string would silently target the
|
|
11
|
+
* parent collection (e.g. DELETE /public/api/project-folders/ = the root
|
|
12
|
+
* folder) instead of failing as a usage error. Runs for live, --plan, and
|
|
13
|
+
* --dry-run alike with zero network.
|
|
14
|
+
*/
|
|
15
|
+
function nonEmptyPositionals(args, names, usage) {
|
|
16
|
+
args.forEach((value, index) => {
|
|
17
|
+
if (value.trim().length === 0) {
|
|
18
|
+
throw new UsageError(`<${names[index] ?? `arg${String(index + 1)}`}> must be a non-empty identifier. Usage: ${usage}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const GET_USAGE = "dss project-folder get <folderId>";
|
|
23
|
+
const SETTINGS_GET_USAGE = "dss project-folder settings-get <folderId>";
|
|
24
|
+
const SETTINGS_SET_USAGE = "dss project-folder settings-set <folderId> (--data JSON|--data-file PATH|--stdin) [--dry-run]";
|
|
25
|
+
const MOVE_USAGE = "dss project-folder move <folderId> <destinationFolderId> [--dry-run]";
|
|
26
|
+
const DELETE_USAGE = "dss project-folder delete <folderId> [--if-exists] [--dry-run]";
|
|
27
|
+
const CREATE_CHILD_USAGE = "dss project-folder create-child <parentFolderId> --name NAME [--dry-run]";
|
|
28
|
+
const MOVE_PROJECT_USAGE = "dss project-folder move-project <folderId> <projectKey> <destinationFolderId> [--dry-run]";
|
|
29
|
+
export const projectFolderCommands = {
|
|
30
|
+
root: {
|
|
31
|
+
handler: (c) => c.projectFolders.root(),
|
|
32
|
+
usage: "dss project-folder root",
|
|
33
|
+
description: "Get the root project folder definition (visible children and projects).",
|
|
34
|
+
examples: ["dss project-folder root",],
|
|
35
|
+
},
|
|
36
|
+
get: {
|
|
37
|
+
validate: (a) => {
|
|
38
|
+
requireArgs(a, 1, GET_USAGE);
|
|
39
|
+
nonEmptyPositionals(a, ["folderId",], GET_USAGE);
|
|
40
|
+
},
|
|
41
|
+
handler: (c, a) => c.projectFolders.get(a[0]),
|
|
42
|
+
usage: GET_USAGE,
|
|
43
|
+
description: "Get a project folder definition (children and project keys).",
|
|
44
|
+
examples: ["dss project-folder get KdLmPU6",],
|
|
45
|
+
},
|
|
46
|
+
"settings-get": {
|
|
47
|
+
validate: (a) => {
|
|
48
|
+
requireArgs(a, 1, SETTINGS_GET_USAGE);
|
|
49
|
+
nonEmptyPositionals(a, ["folderId",], SETTINGS_GET_USAGE);
|
|
50
|
+
},
|
|
51
|
+
handler: (c, a) => c.projectFolders.getSettings(a[0]),
|
|
52
|
+
usage: SETTINGS_GET_USAGE,
|
|
53
|
+
description: "Get project folder settings (name, owner, permissions). Admin required.",
|
|
54
|
+
examples: ["dss project-folder settings-get KdLmPU6",],
|
|
55
|
+
},
|
|
56
|
+
"settings-set": {
|
|
57
|
+
validate: (a) => {
|
|
58
|
+
requireArgs(a, 1, SETTINGS_SET_USAGE);
|
|
59
|
+
nonEmptyPositionals(a, ["folderId",], SETTINGS_SET_USAGE);
|
|
60
|
+
},
|
|
61
|
+
handler: async (c, a, f) => {
|
|
62
|
+
const settings = requiredJsonInput(f, "--data, --data-file, or --stdin is required (project folder settings).");
|
|
63
|
+
if (executionMode(f).dryRun) {
|
|
64
|
+
return planResult("project-folder", "settings-set", {
|
|
65
|
+
asyncKind: "none",
|
|
66
|
+
method: "PUT",
|
|
67
|
+
endpoint: folderEndpoint(a[0], "/settings"),
|
|
68
|
+
identifiers: { folderId: a[0], },
|
|
69
|
+
payload: settings,
|
|
70
|
+
idempotency: "none",
|
|
71
|
+
exitCodesOnFailure: PLAN_FAILURE_CODES,
|
|
72
|
+
plannedAndDryRun: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
await c.projectFolders.updateSettings(a[0], settings);
|
|
76
|
+
return { updated: a[0], resource: "project-folder", };
|
|
77
|
+
},
|
|
78
|
+
usage: SETTINGS_SET_USAGE,
|
|
79
|
+
description: "Replace project folder settings (name, owner, permissions). Admin required.",
|
|
80
|
+
examples: [
|
|
81
|
+
"dss project-folder settings-set KdLmPU6 --data-file settings.json --dry-run",
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
move: {
|
|
85
|
+
validate: (a) => {
|
|
86
|
+
requireArgs(a, 2, MOVE_USAGE);
|
|
87
|
+
nonEmptyPositionals(a, ["folderId", "destinationFolderId",], MOVE_USAGE);
|
|
88
|
+
},
|
|
89
|
+
handler: async (c, a, f) => {
|
|
90
|
+
if (executionMode(f).dryRun) {
|
|
91
|
+
return planResult("project-folder", "move", {
|
|
92
|
+
asyncKind: "none",
|
|
93
|
+
method: "POST",
|
|
94
|
+
endpoint: `${folderEndpoint(a[0], "/move")}?destination=${encodeURIComponent(a[1])}`,
|
|
95
|
+
identifiers: { folderId: a[0], destination: a[1], },
|
|
96
|
+
idempotency: "none",
|
|
97
|
+
exitCodesOnFailure: PLAN_FAILURE_CODES,
|
|
98
|
+
plannedAndDryRun: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
await c.projectFolders.move(a[0], a[1]);
|
|
102
|
+
return { moved: a[0], destination: a[1], resource: "project-folder", };
|
|
103
|
+
},
|
|
104
|
+
usage: MOVE_USAGE,
|
|
105
|
+
description: "Move a project folder (and its content) into another project folder. Cannot move into a sub-folder of itself.",
|
|
106
|
+
examples: ["dss project-folder move KdLmPU6 dgKywsx",],
|
|
107
|
+
},
|
|
108
|
+
delete: {
|
|
109
|
+
validate: (a) => {
|
|
110
|
+
requireArgs(a, 1, DELETE_USAGE);
|
|
111
|
+
nonEmptyPositionals(a, ["folderId",], DELETE_USAGE);
|
|
112
|
+
},
|
|
113
|
+
handler: async (c, a, f) => {
|
|
114
|
+
// Dry-run must be a validated plan with zero HTTP: no existence GET.
|
|
115
|
+
if (executionMode(f).dryRun) {
|
|
116
|
+
return planResult("project-folder", "delete", {
|
|
117
|
+
asyncKind: "none",
|
|
118
|
+
method: "DELETE",
|
|
119
|
+
endpoint: folderEndpoint(a[0]),
|
|
120
|
+
identifiers: { folderId: a[0], },
|
|
121
|
+
idempotency: "if-exists",
|
|
122
|
+
exitCodesOnFailure: PLAN_FAILURE_CODES,
|
|
123
|
+
plannedAndDryRun: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// --if-exists (live only) pays one existence GET; a 404 becomes a skip.
|
|
127
|
+
if (f["if-exists"] === true) {
|
|
128
|
+
const current = await c.projectFolders.get(a[0]).catch((error) => {
|
|
129
|
+
if (isNotFoundError(error))
|
|
130
|
+
return undefined;
|
|
131
|
+
throw error;
|
|
132
|
+
});
|
|
133
|
+
if (!current)
|
|
134
|
+
return skipResult("project-folder", a[0], "missing");
|
|
135
|
+
}
|
|
136
|
+
await c.projectFolders.delete(a[0]);
|
|
137
|
+
return { deleted: a[0], resource: "project-folder", };
|
|
138
|
+
},
|
|
139
|
+
usage: DELETE_USAGE,
|
|
140
|
+
description: "Delete an empty project folder (no sub-folders, no projects). Admin required.",
|
|
141
|
+
examples: ["dss project-folder delete KdLmPU6 --dry-run",],
|
|
142
|
+
},
|
|
143
|
+
"create-child": {
|
|
144
|
+
validate: (a, f) => {
|
|
145
|
+
const name = f["name"];
|
|
146
|
+
if (typeof name === "string" && name.trim().length === 0) {
|
|
147
|
+
throw new UsageError(`--name must be a non-empty folder name. Usage: ${CREATE_CHILD_USAGE}`);
|
|
148
|
+
}
|
|
149
|
+
if (!a[0] || typeof name !== "string" || name.length === 0) {
|
|
150
|
+
throw new UsageError(`Usage: ${CREATE_CHILD_USAGE}`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
handler: async (c, a, f) => {
|
|
154
|
+
const name = f["name"];
|
|
155
|
+
if (executionMode(f).dryRun) {
|
|
156
|
+
return planResult("project-folder", "create-child", {
|
|
157
|
+
asyncKind: "none",
|
|
158
|
+
method: "POST",
|
|
159
|
+
endpoint: `${folderEndpoint(a[0], "/children")}?name=${encodeURIComponent(name)}`,
|
|
160
|
+
identifiers: { parentFolderId: a[0], name, },
|
|
161
|
+
idempotency: "none",
|
|
162
|
+
exitCodesOnFailure: PLAN_FAILURE_CODES,
|
|
163
|
+
plannedAndDryRun: true,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const created = await c.projectFolders.createChild(a[0], name);
|
|
167
|
+
return { created: created.id ?? name, resource: "project-folder", ...created, };
|
|
168
|
+
},
|
|
169
|
+
usage: CREATE_CHILD_USAGE,
|
|
170
|
+
description: "Create a sub-project folder. WRITE_CONTENTS required on the parent.",
|
|
171
|
+
examples: ["dss project-folder create-child KdLmPU6 --name my_sub_folder",],
|
|
172
|
+
},
|
|
173
|
+
"move-project": {
|
|
174
|
+
validate: (a) => {
|
|
175
|
+
requireArgs(a, 3, MOVE_PROJECT_USAGE);
|
|
176
|
+
nonEmptyPositionals(a, ["folderId", "projectKey", "destinationFolderId",], MOVE_PROJECT_USAGE);
|
|
177
|
+
},
|
|
178
|
+
handler: async (c, a, f) => {
|
|
179
|
+
if (executionMode(f).dryRun) {
|
|
180
|
+
return planResult("project-folder", "move-project", {
|
|
181
|
+
asyncKind: "none",
|
|
182
|
+
method: "POST",
|
|
183
|
+
endpoint: `${folderEndpoint(a[0], `/projects/${encodeURIComponent(a[1])}/move`)}?destination=${encodeURIComponent(a[2])}`,
|
|
184
|
+
identifiers: { folderId: a[0], projectKey: a[1], destination: a[2], },
|
|
185
|
+
idempotency: "none",
|
|
186
|
+
exitCodesOnFailure: PLAN_FAILURE_CODES,
|
|
187
|
+
plannedAndDryRun: true,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
await c.projectFolders.moveProject(a[0], a[1], a[2]);
|
|
191
|
+
return { moved: a[1], destination: a[2], resource: "project-folder", };
|
|
192
|
+
},
|
|
193
|
+
usage: MOVE_PROJECT_USAGE,
|
|
194
|
+
description: "Move a project from its project folder to another project folder. WRITE_CONTENTS on destination and project admin required.",
|
|
195
|
+
examples: ["dss project-folder move-project KdLmPU6 MYPROJECT dgKywsx",],
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateGitReferencePath, } from "../../resources/project-git.js";
|
|
2
|
+
import { sanitizeErrorSecrets, sanitizeSecrets, } from "../../utils/secret-sanitize.js";
|
|
2
3
|
import { num, parseBooleanOption, requiredStringFlag, } from "../coerce.js";
|
|
3
4
|
import { CommandResultFailure, } from "../output.js";
|
|
4
5
|
import { requireArgs, requireNoArgs, UsageError, } from "../usage.js";
|
|
@@ -28,48 +29,17 @@ const SENSITIVE_GIT_OUTPUT_KEYS = {
|
|
|
28
29
|
password: true,
|
|
29
30
|
token: true,
|
|
30
31
|
};
|
|
32
|
+
const GIT_SANITIZE_OPTIONS = {
|
|
33
|
+
sensitiveKeys: SENSITIVE_GIT_OUTPUT_KEYS,
|
|
34
|
+
redactUrlUserinfo: true,
|
|
35
|
+
};
|
|
31
36
|
/**
|
|
32
37
|
* DSS may already contain legacy remotes with credentials in the URL, and Git
|
|
33
38
|
* error text can echo them. Sanitize every server-derived Project Git value
|
|
34
39
|
* before it reaches stdout or a structured command failure.
|
|
35
40
|
*/
|
|
36
|
-
function redactGitUrlUserinfo(text) {
|
|
37
|
-
return text.replace(/https?:[\\/]{1,2}[^\s"'<>]+/giu, (url) => {
|
|
38
|
-
const schemeEnd = url.indexOf(":") + 1;
|
|
39
|
-
let authorityStart = schemeEnd;
|
|
40
|
-
while (url[authorityStart] === "/" || url[authorityStart] === "\\")
|
|
41
|
-
authorityStart++;
|
|
42
|
-
const slash = url.slice(authorityStart).search(/[\\/]/u);
|
|
43
|
-
const authorityEnd = slash === -1 ? url.length : authorityStart + slash;
|
|
44
|
-
const at = url.lastIndexOf("@", authorityEnd - 1);
|
|
45
|
-
if (at < authorityStart)
|
|
46
|
-
return url;
|
|
47
|
-
return `${url.slice(0, authorityStart)}[redacted]@${url.slice(at + 1)}`;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
41
|
function sanitizeProjectGitOutput(value, secrets = []) {
|
|
51
|
-
|
|
52
|
-
let sanitized = redactGitUrlUserinfo(value);
|
|
53
|
-
for (const secret of secrets) {
|
|
54
|
-
if (secret !== "")
|
|
55
|
-
sanitized = sanitized.split(secret).join("[redacted]");
|
|
56
|
-
}
|
|
57
|
-
return sanitized;
|
|
58
|
-
}
|
|
59
|
-
if (Array.isArray(value)) {
|
|
60
|
-
return value.map((item) => sanitizeProjectGitOutput(item, secrets));
|
|
61
|
-
}
|
|
62
|
-
if (value !== null && typeof value === "object") {
|
|
63
|
-
const sanitized = {};
|
|
64
|
-
for (const [key, item,] of Object.entries(value)) {
|
|
65
|
-
const normalizedKey = key.replace(/[-_]/g, "").toLowerCase();
|
|
66
|
-
sanitized[key] = SENSITIVE_GIT_OUTPUT_KEYS[normalizedKey] === true
|
|
67
|
-
? "[redacted]"
|
|
68
|
-
: sanitizeProjectGitOutput(item, secrets);
|
|
69
|
-
}
|
|
70
|
-
return sanitized;
|
|
71
|
-
}
|
|
72
|
-
return value;
|
|
42
|
+
return sanitizeSecrets(value, { ...GIT_SANITIZE_OPTIONS, secrets, });
|
|
73
43
|
}
|
|
74
44
|
/**
|
|
75
45
|
* Most Git actions answer HTTP 200 with `{success: false}` and the Git stderr
|
|
@@ -84,16 +54,7 @@ function assertGitActionSucceeded(result, action) {
|
|
|
84
54
|
return sanitized;
|
|
85
55
|
}
|
|
86
56
|
function sanitizeProjectGitError(error) {
|
|
87
|
-
|
|
88
|
-
return error;
|
|
89
|
-
error.message = sanitizeProjectGitOutput(error.message);
|
|
90
|
-
if (error.stack !== undefined)
|
|
91
|
-
error.stack = sanitizeProjectGitOutput(error.stack);
|
|
92
|
-
const errorWithBody = error;
|
|
93
|
-
if (typeof errorWithBody.body === "string") {
|
|
94
|
-
errorWithBody.body = sanitizeProjectGitOutput(errorWithBody.body);
|
|
95
|
-
}
|
|
96
|
-
return error;
|
|
57
|
+
return sanitizeErrorSecrets(error, { ...GIT_SANITIZE_OPTIONS, });
|
|
97
58
|
}
|
|
98
59
|
/**
|
|
99
60
|
* External library credentials are read only from the environment variable
|
|
@@ -533,17 +494,16 @@ export const projectGitCommands = {
|
|
|
533
494
|
handler: async (c, a, f) => {
|
|
534
495
|
requireArgs(a, 1, FUTURE_WAIT_USAGE);
|
|
535
496
|
try {
|
|
536
|
-
|
|
497
|
+
// The official Python client returns the future result verbatim
|
|
498
|
+
// (wait_for_result never inspects result fields): a completed
|
|
499
|
+
// future's payload is caller-interpreted data, and a
|
|
500
|
+
// {success:false} bag from DSS means "no diagnostics to show",
|
|
501
|
+
// not an operation failure. Only the git ACTION endpoints carry
|
|
502
|
+
// the {success:false} = failed convention.
|
|
503
|
+
return sanitizeProjectGitOutput(await c.projectGit.waitForFuture(a[0], {
|
|
537
504
|
pollIntervalMs: num(f["poll-interval"], "--poll-interval"),
|
|
538
505
|
timeoutMs: num(f["timeout"], "--timeout"),
|
|
539
506
|
}));
|
|
540
|
-
if (result !== null
|
|
541
|
-
&& typeof result === "object"
|
|
542
|
-
&& !Array.isArray(result)
|
|
543
|
-
&& result.success === false) {
|
|
544
|
-
return assertGitActionSucceeded(result, "future-wait");
|
|
545
|
-
}
|
|
546
|
-
return result;
|
|
547
507
|
}
|
|
548
508
|
catch (error) {
|
|
549
509
|
throw sanitizeProjectGitError(error);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { encodeLibraryPath, EXPECT_SHA256_PATTERN, validateLibraryDestinationPath, validateLibraryName, validateLibraryPath, } from "../../resources/project-library.js";
|
|
3
3
|
import { numFlag, readStdinText, sha256Hex, } from "../coerce.js";
|
|
4
|
+
import { executionMode, } from "../flags.js";
|
|
4
5
|
import { encodedProjectEndpoint, planResult, skipResult, } from "../output.js";
|
|
5
6
|
import { requireArgs, UsageError, } from "../usage.js";
|
|
6
7
|
const PROJECT_LIBRARY_EXIT_CODES = { usage: 1, error: 2, transient: 3, };
|
|
@@ -100,7 +101,7 @@ export const projectLibraryCommands = {
|
|
|
100
101
|
requireArgs(a, 1, "dss project-library create-file <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
|
|
101
102
|
const pk = f["project-key"];
|
|
102
103
|
validateLibraryPath(a[0]);
|
|
103
|
-
if (f
|
|
104
|
+
if (executionMode(f).dryRun) {
|
|
104
105
|
return projectLibraryPlan("create-file", {
|
|
105
106
|
method: "POST",
|
|
106
107
|
endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
|
|
@@ -126,7 +127,7 @@ export const projectLibraryCommands = {
|
|
|
126
127
|
requireArgs(a, 1, "dss project-library create-folder <path> [--if-not-exists] [--dry-run] [--project-key KEY]");
|
|
127
128
|
const pk = f["project-key"];
|
|
128
129
|
validateLibraryPath(a[0]);
|
|
129
|
-
if (f
|
|
130
|
+
if (executionMode(f).dryRun) {
|
|
130
131
|
return projectLibraryPlan("create-folder", {
|
|
131
132
|
method: "POST",
|
|
132
133
|
endpoint: projectLibraryFolderEndpoint(c, pk, a[0]),
|
|
@@ -153,7 +154,7 @@ export const projectLibraryCommands = {
|
|
|
153
154
|
const pk = f["project-key"];
|
|
154
155
|
const expectSha256 = expectSha256FromFlags(f);
|
|
155
156
|
validateLibraryPath(a[0]);
|
|
156
|
-
if (f
|
|
157
|
+
if (executionMode(f).dryRun) {
|
|
157
158
|
return projectLibraryPlan("put", {
|
|
158
159
|
method: "POST",
|
|
159
160
|
endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
|
|
@@ -206,7 +207,7 @@ export const projectLibraryCommands = {
|
|
|
206
207
|
requireArgs(a, 1, "dss project-library delete <path> [--dry-run] [--project-key KEY]");
|
|
207
208
|
const pk = f["project-key"];
|
|
208
209
|
validateLibraryPath(a[0]);
|
|
209
|
-
if (f
|
|
210
|
+
if (executionMode(f).dryRun) {
|
|
210
211
|
return projectLibraryPlan("delete", {
|
|
211
212
|
method: "DELETE",
|
|
212
213
|
endpoint: projectLibraryContentsEndpoint(c, pk, a[0]),
|
|
@@ -226,7 +227,7 @@ export const projectLibraryCommands = {
|
|
|
226
227
|
const pk = f["project-key"];
|
|
227
228
|
const validPath = validateLibraryPath(a[0]);
|
|
228
229
|
const validName = validateLibraryName(a[1]);
|
|
229
|
-
if (f
|
|
230
|
+
if (executionMode(f).dryRun) {
|
|
230
231
|
return projectLibraryPlan("rename", {
|
|
231
232
|
method: "POST",
|
|
232
233
|
endpoint: projectLibraryActionEndpoint(c, pk, "rename/"),
|
|
@@ -247,7 +248,7 @@ export const projectLibraryCommands = {
|
|
|
247
248
|
const pk = f["project-key"];
|
|
248
249
|
const validPath = validateLibraryPath(a[0]);
|
|
249
250
|
const destination = validateLibraryDestinationPath(a[1]);
|
|
250
|
-
if (f
|
|
251
|
+
if (executionMode(f).dryRun) {
|
|
251
252
|
return projectLibraryPlan("move", {
|
|
252
253
|
method: "POST",
|
|
253
254
|
endpoint: projectLibraryActionEndpoint(c, pk, "move"),
|
|
@@ -3,6 +3,7 @@ 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>";
|
|
@@ -91,7 +92,7 @@ export const projectCommands = {
|
|
|
91
92
|
throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
|
|
92
93
|
}
|
|
93
94
|
const guarded = f["if-exists"] === true
|
|
94
|
-
|| f
|
|
95
|
+
|| executionMode(f).dryRun
|
|
95
96
|
|| expectedIncarnation !== undefined;
|
|
96
97
|
if (!guarded) {
|
|
97
98
|
// Purely user-requested delete without any guard flag: keep the
|
|
@@ -128,7 +129,7 @@ export const projectCommands = {
|
|
|
128
129
|
}
|
|
129
130
|
throw error;
|
|
130
131
|
}
|
|
131
|
-
if (f
|
|
132
|
+
if (executionMode(f).dryRun) {
|
|
132
133
|
return {
|
|
133
134
|
deleted: false,
|
|
134
135
|
dryRun: true,
|
|
@@ -213,12 +214,6 @@ export const projectCommands = {
|
|
|
213
214
|
description: "Read-only local inspection of a project export archive: member names, sizes, manifest validity, archive issues, and the source project key. Never contacts DSS.",
|
|
214
215
|
examples: ["dss project inspect-archive ./my_proj.zip",],
|
|
215
216
|
},
|
|
216
|
-
"permissions-get": {
|
|
217
|
-
handler: (c, _a, f) => c.projects.getPermissions(f["project-key"]),
|
|
218
|
-
usage: "dss project permissions-get [--project-key KEY]",
|
|
219
|
-
description: "Get a project's permissions.",
|
|
220
|
-
examples: ["dss project permissions-get --project-key MY_PROJ",],
|
|
221
|
-
},
|
|
222
217
|
"permissions-set": {
|
|
223
218
|
handler: async (c, _a, f) => {
|
|
224
219
|
const body = requiredJsonInput(f, "--data, --data-file, or --stdin is required (permissions object).");
|
|
@@ -229,6 +224,58 @@ export const projectCommands = {
|
|
|
229
224
|
description: "Replace a project's permissions.",
|
|
230
225
|
examples: ["dss project permissions-set --data-file perms.json --project-key MY_PROJ",],
|
|
231
226
|
},
|
|
227
|
+
"permissions-get": {
|
|
228
|
+
handler: (c, _a, f) => c.projects.getPermissions(f["project-key"]),
|
|
229
|
+
usage: "dss project permissions-get [--project-key KEY]",
|
|
230
|
+
description: "Get a project's permissions.",
|
|
231
|
+
examples: ["dss project permissions-get --project-key MY_PROJ",],
|
|
232
|
+
},
|
|
233
|
+
"metadata-set": {
|
|
234
|
+
handler: async (c, _a, f) => {
|
|
235
|
+
const metadata = requiredJsonInput(f, "--data, --data-file, or --stdin is required (full metadata object).");
|
|
236
|
+
if (executionMode(f).dryRun) {
|
|
237
|
+
return {
|
|
238
|
+
dryRun: true,
|
|
239
|
+
action: "metadata-set",
|
|
240
|
+
resource: "project",
|
|
241
|
+
next: metadata,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
await c.projects.setMetadata(f["project-key"], metadata);
|
|
245
|
+
return { updated: true, };
|
|
246
|
+
},
|
|
247
|
+
usage: "dss project metadata-set (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]",
|
|
248
|
+
description: "Replace project metadata with a full object obtained from a previous metadata GET. Fields absent from the payload are removed.",
|
|
249
|
+
examples: [
|
|
250
|
+
"dss project metadata-set --data-file metadata.json --project-key MY_PROJ --dry-run",
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
"tags-get": {
|
|
254
|
+
handler: (c, _a, f) => c.projects.tags(f["project-key"]),
|
|
255
|
+
usage: "dss project tags-get [--project-key KEY]",
|
|
256
|
+
description: "Get project-level tags as a dictionary of tag name to display settings.",
|
|
257
|
+
examples: ["dss project tags-get", "dss project tags-get --project-key MY_PROJ",],
|
|
258
|
+
},
|
|
259
|
+
"tags-set": {
|
|
260
|
+
handler: async (c, _a, f) => {
|
|
261
|
+
const tags = requiredJsonInput(f, "--data, --data-file, or --stdin is required (full tags object).");
|
|
262
|
+
if (executionMode(f).dryRun) {
|
|
263
|
+
return {
|
|
264
|
+
dryRun: true,
|
|
265
|
+
action: "tags-set",
|
|
266
|
+
resource: "project",
|
|
267
|
+
next: tags,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
await c.projects.setTags(f["project-key"], tags);
|
|
271
|
+
return { updated: true, };
|
|
272
|
+
},
|
|
273
|
+
usage: "dss project tags-set (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]",
|
|
274
|
+
description: "Replace project tags with a full object obtained from a previous tags GET. Tags absent from the payload are removed.",
|
|
275
|
+
examples: [
|
|
276
|
+
'dss project tags-set --data \'{"tags":{"production":{"color":"#28aadd"}}}\' --project-key MY_PROJ --dry-run',
|
|
277
|
+
],
|
|
278
|
+
},
|
|
232
279
|
"settings-get": {
|
|
233
280
|
handler: (c, _a, f) => c.projects.getSettings(f["project-key"]),
|
|
234
281
|
usage: "dss project settings-get [--project-key KEY]",
|
|
@@ -2,7 +2,8 @@ import { readFileSync, } from "node:fs";
|
|
|
2
2
|
import { writeFile, } from "node:fs/promises";
|
|
3
3
|
import { join, } from "node:path";
|
|
4
4
|
import { deepMerge, } from "../../utils/deep-merge.js";
|
|
5
|
-
import { jobLogFilterFromFlag, jsonInput, maxLogLinesFromFlags, normalizeLineEndings, num, recipeInputDatasetsFromFlags, requiredStringFlag, rewritePairsFromFlags, sha256Hex, splitCsvFlag, stableHash, } from "../coerce.js";
|
|
5
|
+
import { jobLogFilterFromFlag, jsonInput, maxLogLinesFromFlags, normalizeLineEndings, num, recipeInputDatasetsFromFlags, requiredJsonInput, requiredStringFlag, rewritePairsFromFlags, sha256Hex, splitCsvFlag, stableHash, } from "../coerce.js";
|
|
6
|
+
import { executionMode, } from "../flags.js";
|
|
6
7
|
import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
|
|
7
8
|
import { ensureRecipeBackupDir, readRecipeBackup, recipeBackupDocument, recipeBackupPath, recipeCodeEnv, recipeGraph, recipeInputItemRef, recipeRoleInputItems, recipeRunShouldWait, writeRecipeBackup, } from "../helpers/recipe.js";
|
|
8
9
|
import { encodedProjectEndpoint, readIfExists, skipResult, } from "../output.js";
|
|
@@ -65,6 +66,41 @@ export const recipeCommands = {
|
|
|
65
66
|
description: "List all recipes in a project.",
|
|
66
67
|
examples: ["dss recipe list",],
|
|
67
68
|
},
|
|
69
|
+
metadata: {
|
|
70
|
+
handler: (c, a, f) => {
|
|
71
|
+
requireArgs(a, 1, "dss recipe metadata <name>");
|
|
72
|
+
return c.recipes.metadata(a[0], {
|
|
73
|
+
projectKey: f["project-key"],
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
usage: "dss recipe metadata <name> [--project-key KEY]",
|
|
77
|
+
description: "Get recipe metadata (label, description, tags, custom fields).",
|
|
78
|
+
examples: ["dss recipe metadata compute_orders",],
|
|
79
|
+
},
|
|
80
|
+
"metadata-set": {
|
|
81
|
+
handler: async (c, a, f) => {
|
|
82
|
+
const usage = "dss recipe metadata-set <name> (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]";
|
|
83
|
+
requireArgs(a, 1, usage);
|
|
84
|
+
const metadata = requiredJsonInput(f, "--data, --data-file, or --stdin is required (full metadata object).");
|
|
85
|
+
const pk = f["project-key"];
|
|
86
|
+
if (executionMode(f).dryRun) {
|
|
87
|
+
return {
|
|
88
|
+
dryRun: true,
|
|
89
|
+
action: "metadata-set",
|
|
90
|
+
resource: "recipe",
|
|
91
|
+
name: a[0],
|
|
92
|
+
next: metadata,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
await c.recipes.setMetadata(a[0], metadata, { projectKey: pk, });
|
|
96
|
+
return { updated: a[0], resource: "recipe", };
|
|
97
|
+
},
|
|
98
|
+
usage: "dss recipe metadata-set <name> (--data JSON|--data-file PATH|--stdin) [--dry-run] [--project-key KEY]",
|
|
99
|
+
description: "Replace recipe metadata with a full object obtained from a previous metadata GET. Fields absent from the payload are removed.",
|
|
100
|
+
examples: [
|
|
101
|
+
'dss recipe metadata-set compute_orders --data \'{"label":"Orders"}\' --dry-run',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
68
104
|
get: {
|
|
69
105
|
handler: (c, a, f) => {
|
|
70
106
|
requireArgs(a, 1, "dss recipe get <name>");
|
|
@@ -109,7 +145,7 @@ export const recipeCommands = {
|
|
|
109
145
|
summary: f["summary"] === true,
|
|
110
146
|
wait,
|
|
111
147
|
};
|
|
112
|
-
if (f
|
|
148
|
+
if (executionMode(f).dryRun) {
|
|
113
149
|
const outputs = await c.recipes.resolveRunOutputs(a[0], {
|
|
114
150
|
partition: options.partition,
|
|
115
151
|
projectKey: pk,
|
|
@@ -139,11 +175,11 @@ export const recipeCommands = {
|
|
|
139
175
|
handler: async (c, a, f) => {
|
|
140
176
|
requireArgs(a, 1, "dss recipe delete <name>");
|
|
141
177
|
const pk = f["project-key"];
|
|
142
|
-
if (f
|
|
178
|
+
if (executionMode(f).dryRun || f["if-exists"] === true) {
|
|
143
179
|
const current = await readIfExists(() => c.recipes.get(a[0], { projectKey: pk, includePayload: true, }));
|
|
144
180
|
if (!current)
|
|
145
181
|
return skipResult("recipe", a[0], "missing");
|
|
146
|
-
if (f
|
|
182
|
+
if (executionMode(f).dryRun) {
|
|
147
183
|
return { dryRun: true, action: "delete", resource: "recipe", name: a[0], current, };
|
|
148
184
|
}
|
|
149
185
|
}
|
|
@@ -241,13 +277,13 @@ export const recipeCommands = {
|
|
|
241
277
|
const zoneMove = zoneId && name
|
|
242
278
|
? [{ objectId: name, objectType: "RECIPE", },]
|
|
243
279
|
: undefined;
|
|
244
|
-
if ((f["if-not-exists"] === true || f
|
|
280
|
+
if ((f["if-not-exists"] === true || executionMode(f).dryRun) && name) {
|
|
245
281
|
const list = await c.recipes.list(pk);
|
|
246
282
|
const existing = list.find((r) => r.name === name);
|
|
247
|
-
if (existing && f["if-not-exists"] === true && f
|
|
283
|
+
if (existing && f["if-not-exists"] === true && !executionMode(f).dryRun) {
|
|
248
284
|
return skipResult("recipe", name, "exists", { current: existing, });
|
|
249
285
|
}
|
|
250
|
-
if (f
|
|
286
|
+
if (executionMode(f).dryRun) {
|
|
251
287
|
return {
|
|
252
288
|
dryRun: true,
|
|
253
289
|
action: "create",
|
|
@@ -259,7 +295,7 @@ export const recipeCommands = {
|
|
|
259
295
|
};
|
|
260
296
|
}
|
|
261
297
|
}
|
|
262
|
-
if (f
|
|
298
|
+
if (executionMode(f).dryRun) {
|
|
263
299
|
return {
|
|
264
300
|
dryRun: true,
|
|
265
301
|
action: "create",
|
|
@@ -328,7 +364,7 @@ export const recipeCommands = {
|
|
|
328
364
|
throw new UsageError("Cannot reuse --path or --metastore-table for multiple cloned output datasets.", "invalid_enum");
|
|
329
365
|
}
|
|
330
366
|
const zoneId = await resolveFlowZoneIdFromFlags(c, f, pk);
|
|
331
|
-
if (f
|
|
367
|
+
if (executionMode(f).dryRun) {
|
|
332
368
|
return {
|
|
333
369
|
dryRun: true,
|
|
334
370
|
action: "clone",
|
|
@@ -391,7 +427,7 @@ export const recipeCommands = {
|
|
|
391
427
|
throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
|
|
392
428
|
}
|
|
393
429
|
const pk = f["project-key"];
|
|
394
|
-
if (f
|
|
430
|
+
if (executionMode(f).dryRun) {
|
|
395
431
|
const current = await c.recipes.get(a[0], { projectKey: pk, includePayload: true, });
|
|
396
432
|
const currentRecipe = current.recipe;
|
|
397
433
|
const next = {
|
|
@@ -430,7 +466,7 @@ export const recipeCommands = {
|
|
|
430
466
|
}
|
|
431
467
|
const nextItems = [...items, { ref: a[1], deps: [], },];
|
|
432
468
|
const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
|
|
433
|
-
if (f
|
|
469
|
+
if (executionMode(f).dryRun) {
|
|
434
470
|
return {
|
|
435
471
|
dryRun: true,
|
|
436
472
|
action: "add-input",
|
|
@@ -467,7 +503,7 @@ export const recipeCommands = {
|
|
|
467
503
|
}
|
|
468
504
|
const nextItems = items.filter((item) => recipeInputItemRef(item) !== a[1]);
|
|
469
505
|
const inputs = nextItems.map(recipeInputItemRef).filter((ref) => Boolean(ref));
|
|
470
|
-
if (f
|
|
506
|
+
if (executionMode(f).dryRun) {
|
|
471
507
|
return {
|
|
472
508
|
dryRun: true,
|
|
473
509
|
action: "remove-input",
|
|
@@ -542,7 +578,7 @@ export const recipeCommands = {
|
|
|
542
578
|
projectKey: pk,
|
|
543
579
|
includePayload: true,
|
|
544
580
|
});
|
|
545
|
-
if (f
|
|
581
|
+
if (executionMode(f).dryRun) {
|
|
546
582
|
return {
|
|
547
583
|
dryRun: true,
|
|
548
584
|
action: "set-payload",
|
|
@@ -593,7 +629,7 @@ export const recipeCommands = {
|
|
|
593
629
|
const next = f["payload-only"] === true || !restoredRecipe
|
|
594
630
|
? { ...current, payload, }
|
|
595
631
|
: { ...current, recipe: restoredRecipe, payload, };
|
|
596
|
-
if (f
|
|
632
|
+
if (executionMode(f).dryRun) {
|
|
597
633
|
return {
|
|
598
634
|
dryRun: true,
|
|
599
635
|
action: "restore",
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { CommandMeta } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Mutating actions accept --plan (buildMutationPlan in contract.ts owns the
|
|
4
|
+
* --plan path centrally); the handlers below keep --dry-run support with a
|
|
5
|
+
* zero-request planned payload, mirroring set-active/delete.
|
|
6
|
+
*/
|
|
2
7
|
export declare const savedModelCommands: Record<string, CommandMeta>;
|