dataiku-sdk 1.0.0 → 2.0.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 (43) hide show
  1. package/README.md +64 -10
  2. package/bin/dss.js +67 -78
  3. package/dist/build-metadata.json +1 -0
  4. package/dist/src/cli/commands/api-service.js +5 -4
  5. package/dist/src/cli/commands/app.js +45 -12
  6. package/dist/src/cli/commands/bundle.js +5 -4
  7. package/dist/src/cli/commands/data-quality.js +42 -0
  8. package/dist/src/cli/commands/dataset.d.ts +23 -0
  9. package/dist/src/cli/commands/dataset.js +187 -6
  10. package/dist/src/cli/commands/job.js +15 -2
  11. package/dist/src/cli/commands/project-library.js +4 -4
  12. package/dist/src/cli/commands/project.js +116 -12
  13. package/dist/src/cli/contract.d.ts +1 -0
  14. package/dist/src/cli/contract.js +194 -15
  15. package/dist/src/cli/doctor.d.ts +12 -2
  16. package/dist/src/cli/doctor.js +44 -13
  17. package/dist/src/cli/flags.js +2 -0
  18. package/dist/src/cli/helpers/app-successor.d.ts +15 -3
  19. package/dist/src/cli/helpers/app-successor.js +155 -15
  20. package/dist/src/cli/helpers/cleanup.js +30 -1
  21. package/dist/src/cli/output.d.ts +3 -1
  22. package/dist/src/cli/output.js +11 -6
  23. package/dist/src/cli/types.d.ts +2 -0
  24. package/dist/src/cli/version.d.ts +47 -4
  25. package/dist/src/cli/version.js +176 -9
  26. package/dist/src/cli.js +55 -14
  27. package/dist/src/client.d.ts +28 -1
  28. package/dist/src/client.js +34 -7
  29. package/dist/src/errors.d.ts +1 -1
  30. package/dist/src/index.d.ts +3 -2
  31. package/dist/src/index.js +1 -0
  32. package/dist/src/resources/datasets.d.ts +43 -0
  33. package/dist/src/resources/datasets.js +199 -28
  34. package/dist/src/resources/folders.js +6 -8
  35. package/dist/src/resources/projects.d.ts +36 -3
  36. package/dist/src/resources/projects.js +122 -20
  37. package/dist/src/skill.js +1 -18
  38. package/dist/src/utils/project-archive.d.ts +32 -0
  39. package/dist/src/utils/project-archive.js +623 -0
  40. package/dist/src/utils/stable-hash.js +1 -2
  41. package/package.json +10 -10
  42. package/plugin.json +1 -1
  43. package/skills/dataiku-dss/SKILL.md +27 -35
@@ -1,10 +1,102 @@
1
1
  import { buildDatasetCloneSettings, } from "../../resources/datasets.js";
2
2
  import { deepMerge, } from "../../utils/deep-merge.js";
3
- import { jsonInput, num, schemaColumnsInput, } from "../coerce.js";
3
+ import { compareStrings, stableHash, } from "../../utils/stable-hash.js";
4
+ import { jsonInput, num, schemaColumnsInput, unknownJsonInput, } from "../coerce.js";
4
5
  import { datasetSourceSummary, } from "../helpers/dataset.js";
5
6
  import { moveCreatedItemsToZone, resolveFlowZoneIdFromFlags, } from "../helpers/flow-zone.js";
6
7
  import { enqueueCliWarning, readIfExists, skipResult, } from "../output.js";
7
8
  import { requireArgs, UsageError, } from "../usage.js";
9
+ const MAX_SCHEMA_DIFFERENCES = 50;
10
+ function isPlainObject(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ function appendSchemaPath(parent, key) {
14
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
15
+ ? (parent ? `${parent}.${key}` : key)
16
+ : `${parent}[${JSON.stringify(key)}]`;
17
+ }
18
+ function collectSchemaDifferences(expected, actual, path, acc) {
19
+ // Hash equality is the deterministic equivalence test: structurally equal
20
+ // subtrees (any key order) match exactly and are never descended into.
21
+ if (stableHash(expected) === stableHash(actual))
22
+ return;
23
+ if (isPlainObject(expected) && isPlainObject(actual)) {
24
+ const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual),]),].sort(compareStrings);
25
+ for (const key of keys) {
26
+ const nextPath = appendSchemaPath(path, key);
27
+ const hasExpected = key in expected;
28
+ const hasActual = key in actual;
29
+ if (hasExpected && hasActual) {
30
+ collectSchemaDifferences(expected[key], actual[key], nextPath, acc);
31
+ continue;
32
+ }
33
+ acc.total += 1;
34
+ if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
35
+ acc.items.push({
36
+ path: nextPath,
37
+ kind: hasActual ? "added" : "removed",
38
+ ...(hasActual
39
+ ? { actualHash: stableHash(actual[key]), }
40
+ : { expectedHash: stableHash(expected[key]), }),
41
+ });
42
+ }
43
+ }
44
+ return;
45
+ }
46
+ if (Array.isArray(expected) && Array.isArray(actual)) {
47
+ const shared = Math.min(expected.length, actual.length);
48
+ for (let i = 0; i < shared; i++) {
49
+ collectSchemaDifferences(expected[i], actual[i], `${path}[${i}]`, acc);
50
+ }
51
+ for (let i = shared; i < expected.length; i++) {
52
+ acc.total += 1;
53
+ if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
54
+ acc.items.push({
55
+ path: `${path}[${i}]`,
56
+ kind: "removed",
57
+ expectedHash: stableHash(expected[i]),
58
+ });
59
+ }
60
+ }
61
+ for (let i = shared; i < actual.length; i++) {
62
+ acc.total += 1;
63
+ if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
64
+ acc.items.push({
65
+ path: `${path}[${i}]`,
66
+ kind: "added",
67
+ actualHash: stableHash(actual[i]),
68
+ });
69
+ }
70
+ }
71
+ return;
72
+ }
73
+ acc.total += 1;
74
+ if (acc.items.length < MAX_SCHEMA_DIFFERENCES) {
75
+ acc.items.push({
76
+ path: path || "$",
77
+ kind: "changed",
78
+ expectedHash: stableHash(expected),
79
+ actualHash: stableHash(actual),
80
+ });
81
+ }
82
+ }
83
+ /**
84
+ * Compare two schema values deterministically — object key order never matters
85
+ * — and report each structural difference as a concise per-path entry carrying
86
+ * subtree hashes instead of subtree values.
87
+ */
88
+ export function compareDatasetSchemas(expected, actual) {
89
+ const acc = { items: [], total: 0, };
90
+ collectSchemaDifferences(expected, actual, "", acc);
91
+ return {
92
+ equal: acc.items.length === 0,
93
+ expectedHash: stableHash(expected),
94
+ actualHash: stableHash(actual),
95
+ differences: acc.items,
96
+ totalDifferences: acc.total,
97
+ differencesTruncated: acc.items.length < acc.total,
98
+ };
99
+ }
8
100
  export const datasetCommands = {
9
101
  list: {
10
102
  handler: (c, _a, f) => c.datasets.list(f["project-key"]),
@@ -69,6 +161,34 @@ export const datasetCommands = {
69
161
  description: "Show backing connection, catalog/schema/table, path, and format for a dataset.",
70
162
  examples: ["dss dataset source orders",],
71
163
  },
164
+ files: {
165
+ handler: (c, a, f) => {
166
+ requireArgs(a, 1, "dss dataset files <name> [--project-key KEY]");
167
+ return c.datasets.listUploadedFiles(a[0], f["project-key"]);
168
+ },
169
+ usage: "dss dataset files <name> [--project-key KEY]",
170
+ description: "List files stored by an UploadedFiles dataset.",
171
+ examples: ["dss dataset files uploaded_input",],
172
+ },
173
+ "upload-file": {
174
+ handler: (c, a, f) => {
175
+ const usage = "dss dataset upload-file <name> <localPath> --file-name NAME [--project-key KEY]";
176
+ requireArgs(a, 2, usage);
177
+ const fileName = f["file-name"];
178
+ if (!fileName || fileName.trim() === "") {
179
+ throw new UsageError(`--file-name is required. Usage: ${usage}`);
180
+ }
181
+ return c.datasets.uploadDatasetFile(a[0], a[1], {
182
+ fileName,
183
+ projectKey: f["project-key"],
184
+ });
185
+ },
186
+ usage: "dss dataset upload-file <name> <localPath> --file-name NAME [--project-key KEY]",
187
+ description: "Add one new file to an UploadedFiles dataset and verify the stored byte length.",
188
+ examples: [
189
+ "dss dataset upload-file uploaded_input ./input.csv --file-name input.csv",
190
+ ],
191
+ },
72
192
  "refresh-schema": {
73
193
  handler: async (c, a, f) => {
74
194
  const usage = "dss dataset refresh-schema <name> [--data JSON | --data-file PATH | --stdin] [--dry-run] [--project-key KEY]";
@@ -105,18 +225,79 @@ export const datasetCommands = {
105
225
  examples: ["dss dataset validate-build orders",],
106
226
  },
107
227
  preview: {
108
- handler: (c, a, f) => {
109
- requireArgs(a, 1, "dss dataset preview <name>");
110
- return c.datasets.preview(a[0], {
111
- maxRows: num(f["max-rows"], "--max-rows"),
228
+ handler: async (c, a, f) => {
229
+ const previewUsage = "dss dataset preview <name> [--max-rows N] [--rows N] [--project-key KEY] [--timeout MS]";
230
+ requireArgs(a, 1, previewUsage);
231
+ const maxRows = num(f["max-rows"], "--max-rows");
232
+ if (maxRows !== undefined && (!Number.isSafeInteger(maxRows) || maxRows <= 0 || maxRows > 500)) {
233
+ throw new UsageError(`--max-rows must be a positive integer no greater than 500 (got "${String(f["max-rows"])}"). `
234
+ + `Usage: ${previewUsage}`, "validation_failed");
235
+ }
236
+ const result = await c.datasets.preview(a[0], {
237
+ maxRows,
112
238
  projectKey: f["project-key"],
113
239
  timeoutMs: num(f["timeout"], "--timeout"),
114
240
  });
241
+ if (result.truncated) {
242
+ enqueueCliWarning({
243
+ code: "dataset_preview_truncated",
244
+ message: `Preview of '${a[0]}' stopped at the ${result.limit}-row cap; the dataset has more rows. ${result.limit >= 500
245
+ ? "The preview cap of 500 rows is the public maximum; use `dss dataset download` for more rows."
246
+ : "Re-run with --max-rows N (up to 500) for more rows."}`,
247
+ dataset: a[0],
248
+ rows: result.rowCount,
249
+ limit: result.limit,
250
+ });
251
+ }
252
+ return result;
115
253
  },
116
254
  usage: "dss dataset preview <name> [--max-rows N] [--rows N] [--project-key KEY] [--timeout MS]",
117
- description: "Preview dataset rows (--rows is an alias for --max-rows).",
255
+ description: "Preview dataset rows (--rows is an alias for --max-rows). Returns { columns, rows, rowCount, truncated, limit }; when the dataset has more rows than the cap, truncated is true and a dataset_preview_truncated warning is written to stderr.",
118
256
  examples: ["dss dataset preview orders", "dss dataset preview orders --rows 5",],
119
257
  },
258
+ "assert-count": {
259
+ handler: async (c, a, f) => {
260
+ const assertUsage = "dss dataset assert-count <dataset> --expected N [--project-key KEY]";
261
+ requireArgs(a, 1, assertUsage);
262
+ const expected = num(f["expected"], "--expected");
263
+ if (expected === undefined || !Number.isSafeInteger(expected) || expected < 0) {
264
+ throw new UsageError(`--expected must be a non-negative integer (got "${String(f["expected"])}"). `
265
+ + `Usage: ${assertUsage}`, "validation_failed");
266
+ }
267
+ const result = await c.datasets.assertRowCount(a[0], expected, f["project-key"]);
268
+ return { ...result, dataset: a[0], };
269
+ },
270
+ usage: "dss dataset assert-count <dataset> --expected N [--project-key KEY]",
271
+ description: "Stream the dataset and assert it holds exactly --expected rows, probing at most N+1 rows. Returns { satisfied, expected, count, exact }; a mismatch exits 4 with assertion_failed.",
272
+ examples: ["dss dataset assert-count orders --expected 1000",],
273
+ },
274
+ "assert-schema": {
275
+ handler: async (c, a, f) => {
276
+ const assertUsage = "dss dataset assert-schema <dataset> (--data JSON | --data-file PATH | --stdin) [--project-key KEY]";
277
+ requireArgs(a, 1, assertUsage);
278
+ const expected = unknownJsonInput(f);
279
+ if (expected === undefined) {
280
+ throw new UsageError(`--data, --data-file, or --stdin is required. Usage: ${assertUsage}`);
281
+ }
282
+ const actual = await c.datasets.getSchemaObject(a[0], f["project-key"]);
283
+ const comparison = compareDatasetSchemas(expected, actual);
284
+ return {
285
+ satisfied: comparison.equal,
286
+ dataset: a[0],
287
+ expectedHash: comparison.expectedHash,
288
+ actualHash: comparison.actualHash,
289
+ totalDifferences: comparison.totalDifferences,
290
+ ...(comparison.differencesTruncated ? { differencesTruncated: true, } : {}),
291
+ differences: comparison.differences,
292
+ };
293
+ },
294
+ usage: "dss dataset assert-schema <dataset> (--data JSON | --data-file PATH | --stdin) [--project-key KEY]",
295
+ description: "Assert the dataset's full schema equals the expected schema object; reports concise per-path differences with sha256 subtree hashes. A mismatch exits 4 with assertion_failed.",
296
+ examples: [
297
+ `dss dataset assert-schema orders --data-file expected-schema.json`,
298
+ `dss dataset assert-schema orders --data '{"columns":[]}'`,
299
+ ],
300
+ },
120
301
  metadata: {
121
302
  handler: (c, a, f) => {
122
303
  requireArgs(a, 1, "dss dataset metadata <name>");
@@ -33,6 +33,19 @@ function firstNumberPath(value, paths) {
33
33
  }
34
34
  return undefined;
35
35
  }
36
+ function aggregateJobWaitResults(jobs, until) {
37
+ if (jobs.length === 1)
38
+ return jobs[0];
39
+ const failed = jobs.find((job) => !job.success);
40
+ return {
41
+ elapsedMs: jobs.reduce((maximum, job) => Math.max(maximum, job.elapsedMs), 0),
42
+ jobs,
43
+ pollCount: jobs.reduce((total, job) => total + job.pollCount, 0),
44
+ state: failed?.state ?? "DONE",
45
+ success: failed === undefined,
46
+ until,
47
+ };
48
+ }
36
49
  function jobSummaryId(job, fallback) {
37
50
  return stringPath(job, ["baseStatus", "def", "id",])
38
51
  ?? stringPath(job, ["def", "id",])
@@ -357,7 +370,7 @@ export const jobCommands = {
357
370
  projectKey: f["project-key"],
358
371
  };
359
372
  const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
360
- return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
373
+ return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
361
374
  },
362
375
  usage: "dss job monitor <id...> [--summary] [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
363
376
  description: "Monitor one or more existing jobs and summarize progress counters from logs.",
@@ -376,7 +389,7 @@ export const jobCommands = {
376
389
  projectKey: f["project-key"],
377
390
  };
378
391
  const jobs = await Promise.all(a.map((jobId) => c.jobs.wait(jobId, options)));
379
- return a.length === 1 ? jobs[0] : { jobs, until: f["until"] ?? "all-done", };
392
+ return aggregateJobWaitResults(jobs, String(f["until"] ?? "all-done"));
380
393
  },
381
394
  usage: "dss job watch <id...> [--include-logs] [--log-filter stdout|stderr|user|errors] [--max-log-lines N] [--timeout MS] [--poll-interval MS] [--until all-done] [--project-key KEY]",
382
395
  description: "Watch one or more existing jobs with progress extraction enabled.",
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, } from "node:fs";
1
+ import * as fs from "node:fs";
2
2
  import { readStdinText, } from "../coerce.js";
3
3
  import { encodedProjectEndpoint, planResult, } from "../output.js";
4
4
  import { requireArgs, UsageError, } from "../usage.js";
@@ -68,8 +68,8 @@ export const projectLibraryCommands = {
68
68
  if (!out)
69
69
  throw new UsageError("--output PATH is required.", "missing_required_flag");
70
70
  const bytes = await c.projectLibrary.getFileBytes(a[0], f["project-key"]);
71
- writeFileSync(out, bytes);
72
- return { path: out, bytes: bytes.length, };
71
+ const written = await Bun.write(out, bytes, { createPath: false, });
72
+ return { path: out, bytes: written, };
73
73
  },
74
74
  usage: "dss project-library get-bytes <path> --output PATH [--project-key KEY]",
75
75
  description: "Download a project library file's raw bytes to a local file.",
@@ -126,7 +126,7 @@ export const projectLibraryCommands = {
126
126
  const content = typeof f["content"] === "string"
127
127
  ? f["content"]
128
128
  : typeof f["file"] === "string"
129
- ? readFileSync(f["file"], "utf-8")
129
+ ? fs.readFileSync(f["file"], "utf-8")
130
130
  : await readStdinText();
131
131
  await c.projectLibrary.addOrUpdateFile(a[0], content, pk);
132
132
  return { updated: a[0], };
@@ -1,6 +1,16 @@
1
+ import { ClientValidationError, DataikuError, } from "../../errors.js";
1
2
  import { deepMerge, } from "../../utils/deep-merge.js";
2
- import { jsonInput, num, requiredJsonInput, } from "../coerce.js";
3
+ import { inspectProjectArchive, } from "../../utils/project-archive.js";
4
+ import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
5
+ import { jsonInput, num, requiredJsonInput, requiredStringFlag, } from "../coerce.js";
6
+ import { CommandResultFailure, } from "../output.js";
3
7
  import { requireArgs, UsageError, } from "../usage.js";
8
+ const INSPECT_ARCHIVE_USAGE = "dss project inspect-archive <file>";
9
+ /** Local, DSS-free archive inspection shared by the local handler and the command handler. */
10
+ function inspectArchiveCommand(args) {
11
+ requireArgs(args, 1, INSPECT_ARCHIVE_USAGE);
12
+ return inspectProjectArchive(args[0]);
13
+ }
4
14
  export const projectCommands = {
5
15
  list: {
6
16
  handler: (c) => c.projects.list(),
@@ -56,12 +66,75 @@ export const projectCommands = {
56
66
  },
57
67
  delete: {
58
68
  handler: async (c, a, f) => {
59
- requireArgs(a, 1, "dss project delete <projectKey> [--drop-data]");
60
- await c.projects.deleteProject(a[0], f["drop-data"] === true);
61
- return { deleted: a[0], };
69
+ const usage = "dss project delete <projectKey> [--drop-data] [--if-exists] [--dry-run] [--expect-project-incarnation HASH]";
70
+ requireArgs(a, 1, usage);
71
+ const projectKey = a[0];
72
+ const expectedIncarnation = f["expect-project-incarnation"] === undefined
73
+ ? undefined
74
+ : requiredStringFlag(f, "expect-project-incarnation", usage);
75
+ if (expectedIncarnation !== undefined
76
+ && !/^[0-9a-f]{64}$/.test(expectedIncarnation)) {
77
+ throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
78
+ }
79
+ const guarded = f["if-exists"] === true
80
+ || f["dry-run"] === true
81
+ || expectedIncarnation !== undefined;
82
+ if (!guarded) {
83
+ // Purely user-requested delete without any guard flag: keep the
84
+ // historical direct DELETE, with no preflight probe.
85
+ await c.projects.deleteProject(projectKey, f["drop-data"] === true);
86
+ return { deleted: projectKey, };
87
+ }
88
+ // Guarded path: existence and identity must be verified before any
89
+ // DELETE. A missing creationTag or an incarnation mismatch refuses
90
+ // before the DELETE under possible project-key reuse.
91
+ let currentIncarnation;
92
+ try {
93
+ const details = await c.projects.get(projectKey);
94
+ if (expectedIncarnation !== undefined) {
95
+ currentIncarnation = projectIncarnationHash(projectKey, details);
96
+ if (currentIncarnation === undefined) {
97
+ throw new ClientValidationError(`DSS did not return creationTag identity for project ${projectKey}. Refusing to delete without project-incarnation verification.`, "validation_failed", "Project-key reuse cannot be excluded. Refusing the DELETE; re-read the project and retry with a hash captured from that incarnation.", { projectKey, });
98
+ }
99
+ if (currentIncarnation !== expectedIncarnation) {
100
+ throw new ClientValidationError(`Project ${projectKey} is not the same project incarnation recorded for this operation. Refusing to delete after project-key reuse.`, "validation_failed", "Re-read the current project and retry only with an artifact captured from that incarnation.", {
101
+ projectKey,
102
+ expectedProjectIncarnationHash: expectedIncarnation,
103
+ currentProjectIncarnationHash: currentIncarnation,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ catch (error) {
109
+ if (error instanceof DataikuError && error.status === 404) {
110
+ // Already absent: converge only when the caller asked for it.
111
+ if (f["if-exists"] !== true)
112
+ throw error;
113
+ return { deleted: false, alreadyAbsent: true, projectKey, };
114
+ }
115
+ throw error;
116
+ }
117
+ if (f["dry-run"] === true) {
118
+ return {
119
+ deleted: false,
120
+ dryRun: true,
121
+ projectKey,
122
+ ...(currentIncarnation !== undefined
123
+ ? { projectIncarnationHash: currentIncarnation, }
124
+ : {}),
125
+ };
126
+ }
127
+ await c.projects.deleteProject(projectKey, f["drop-data"] === true);
128
+ return {
129
+ deleted: true,
130
+ projectKey,
131
+ ...(currentIncarnation !== undefined
132
+ ? { projectIncarnationHash: currentIncarnation, }
133
+ : {}),
134
+ };
62
135
  },
63
- usage: "dss project delete <projectKey> [--drop-data]",
64
- description: "Delete a project (destructive). --drop-data also clears managed datasets.",
136
+ usage: "dss project delete <projectKey> [--drop-data] [--if-exists] [--dry-run] [--expect-project-incarnation HASH]",
137
+ description: "Delete a project (destructive). --drop-data also clears managed datasets; --if-exists treats an absent project as already deleted; --dry-run verifies without deleting; --expect-project-incarnation HASH refuses deletion unless the project still matches the recorded incarnation.",
65
138
  examples: ["dss project delete MY_PROJ",],
66
139
  },
67
140
  duplicate: {
@@ -87,13 +160,44 @@ export const projectCommands = {
87
160
  examples: ["dss project export MY_PROJ --output ./my_proj.zip",],
88
161
  },
89
162
  import: {
90
- handler: async (c, a) => {
91
- requireArgs(a, 1, "dss project import <filePath>");
92
- return c.projects.importProjectFromArchive(a[0]);
163
+ handler: async (c, a, f) => {
164
+ const usage = "dss project import <filePath> [--target-project-key KEY] [--data JSON|--data-file PATH|--stdin]";
165
+ requireArgs(a, 1, usage);
166
+ const settings = jsonInput(f) ?? {};
167
+ const rawTarget = f["target-project-key"];
168
+ const targetProjectKey = rawTarget?.trim();
169
+ if (rawTarget !== undefined && targetProjectKey === "") {
170
+ throw new UsageError(`--target-project-key must not be empty. Usage: ${usage}`);
171
+ }
172
+ const settingsTarget = settings.targetProjectKey;
173
+ if (settingsTarget !== undefined
174
+ && (typeof settingsTarget !== "string" || settingsTarget.trim() === "")) {
175
+ throw new UsageError(`targetProjectKey in import settings must be a non-empty string. Usage: ${usage}`);
176
+ }
177
+ if (targetProjectKey !== undefined
178
+ && settingsTarget !== undefined
179
+ && targetProjectKey !== settingsTarget.trim()) {
180
+ throw new UsageError(`--target-project-key conflicts with targetProjectKey in import settings. Usage: ${usage}`);
181
+ }
182
+ const result = await c.projects.importProjectFromArchive(a[0], targetProjectKey === undefined ? settings : { ...settings, targetProjectKey, });
183
+ if (result.success !== true) {
184
+ throw new CommandResultFailure({ ...result, resource: "project", action: "import", stage: "process", }, 2);
185
+ }
186
+ return result;
93
187
  },
94
- usage: "dss project import <filePath>",
95
- description: "Upload a project archive to prepare an import (returns a temporary import id).",
96
- examples: ["dss project import ./my_proj.zip",],
188
+ usage: "dss project import <filePath> [--target-project-key KEY] [--data JSON|--data-file PATH|--stdin]",
189
+ description: "Upload and process a project archive, optionally overriding its project key. The archive is validated locally before upload; verified success reports the used project key, explicit request/actual remapping, and the landed incarnation hash.",
190
+ examples: [
191
+ "dss project import ./my_proj.zip",
192
+ "dss project import ./my_proj.zip --target-project-key MY_PROJ",
193
+ ],
194
+ },
195
+ "inspect-archive": {
196
+ handler: async (_c, a) => inspectArchiveCommand(a),
197
+ localHandler: inspectArchiveCommand,
198
+ usage: INSPECT_ARCHIVE_USAGE,
199
+ 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.",
200
+ examples: ["dss project inspect-archive ./my_proj.zip",],
97
201
  },
98
202
  "permissions-get": {
99
203
  handler: (c, _a, f) => c.projects.getPermissions(f["project-key"]),
@@ -101,6 +101,7 @@ export declare function commandPlanShape(resource: string, action: string, args:
101
101
  payload?: unknown;
102
102
  localWrites?: unknown;
103
103
  wait?: unknown;
104
+ requests?: unknown;
104
105
  };
105
106
  export declare function buildMutationPlan(resource: string, action: string, meta: CommandMeta, args: string[], flags: Record<string, string | boolean>): Record<string, unknown>;
106
107
  /**