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
@@ -234,7 +234,7 @@ const AGENT_CONTRACT_EXAMPLES = [
234
234
  "dss agent contract --fields commands.actions",
235
235
  ];
236
236
  const VERSION_USAGE = "dss version";
237
- const VERSION_DESCRIPTION = "Print the CLI version and git revision as JSON.";
237
+ const VERSION_DESCRIPTION = "Print the CLI version, checkout/build revisions, load source, runtime, and stale-build status as JSON.";
238
238
  const VERSION_EXAMPLES = ["dss version", "dss --version",];
239
239
  const INSTALL_SKILL_USAGE = "dss install-skill [--global] [--agent NAME] [--target PATH] [--list-agents] [--dry-run] [--plan]";
240
240
  const INSTALL_SKILL_DESCRIPTION = "Install the dataiku-dss agent skill for detected coding agents.";
@@ -270,6 +270,7 @@ const ALLOWED_CLEANUP_ACTIONS = new Set([
270
270
  "code-env delete",
271
271
  "folder delete",
272
272
  "folder delete-file",
273
+ "project delete",
273
274
  "app delete-instance",
274
275
  ]);
275
276
  export function isAllowedCleanupAction(resource, action) {
@@ -676,6 +677,7 @@ export function inferRequiresProject(resource, action, usage) {
676
677
  const ARRAY_OUTPUT_ACTIONS = new Set([
677
678
  "history",
678
679
  "find",
680
+ "files",
679
681
  "infer",
680
682
  "last-results",
681
683
  "list",
@@ -861,6 +863,9 @@ function inferPayloadSchema(inputContract) {
861
863
  function inferExitCodes(resource, action, asyncKind) {
862
864
  const longRunning = asyncKind !== "none" || `${resource}.${action}` === "batch.run";
863
865
  const assertion = `${resource}.${action}` === "recipe.assert-unchanged"
866
+ || `${resource}.${action}` === "dataset.assert-count"
867
+ || `${resource}.${action}` === "dataset.assert-schema"
868
+ || `${resource}.${action}` === "data-quality.assert-results"
864
869
  || `${resource}.${action}` === "batch.run";
865
870
  return {
866
871
  ok: 0,
@@ -872,6 +877,9 @@ function inferExitCodes(resource, action, asyncKind) {
872
877
  };
873
878
  }
874
879
  function cleanupCommandFromDeleteUsage(resource, action) {
880
+ if (`${resource}.${action}` === "project.import") {
881
+ return "dss project delete <used-project-key> --if-exists --expect-project-incarnation <hash>";
882
+ }
875
883
  if (resource === "project" && (action === "create" || action === "duplicate")) {
876
884
  return undefined;
877
885
  }
@@ -895,9 +903,20 @@ export function supportsCleanupLedger(resource, action) {
895
903
  return cleanupCommandFromDeleteUsage(resource, action) !== undefined
896
904
  || `${resource}.${action}` === "folder.upload";
897
905
  }
906
+ /**
907
+ * Write actions that no public DSS route can undo. `dataset upload-file` adds a
908
+ * file to the dataset's uploaded-files set, and DSS exposes no endpoint to
909
+ * replace or delete an individual uploaded file, so the write is permanent and
910
+ * no cleanup-ledger entry can recover the previous file set.
911
+ */
912
+ const EXPLICIT_DESTRUCTIVE_KEYS = {
913
+ "dataset.upload-file": true,
914
+ };
898
915
  function inferDestructiveLevel(resource, sideEffect, action) {
899
916
  if (sideEffect !== "write")
900
917
  return "none";
918
+ if (EXPLICIT_DESTRUCTIVE_KEYS[`${resource}.${action}`] === true)
919
+ return "destructive";
901
920
  if (resource === "project-git") {
902
921
  return PROJECT_GIT_DESTRUCTIVE_ACTIONS[action] === true ? "destructive" : "reversible";
903
922
  }
@@ -992,7 +1011,7 @@ export function inferCleanupHint(resource, action) {
992
1011
  return `Delete with \`dss ${resource} delete <id>${ifExists}\` when the created object is disposable.`;
993
1012
  }
994
1013
  function buildRegistryEntry(resource, action, meta) {
995
- const requiresAuth = inferRequiresAuth(resource);
1014
+ const requiresAuth = meta.localHandler === undefined && inferRequiresAuth(resource);
996
1015
  const requiresProject = inferRequiresProject(resource, action, meta.usage);
997
1016
  const sideEffect = inferSideEffect(resource, action);
998
1017
  const destructive = inferDestructiveLevel(resource, sideEffect, action);
@@ -1781,6 +1800,38 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
1781
1800
  identifiers: { oldName: args[0], newName: args[1], },
1782
1801
  payload: { oldName: args[0], newName: args[1], },
1783
1802
  };
1803
+ case "dataset.upload-file": {
1804
+ const fileName = requiredPlanFlag(flags, "file-name", entry.usage);
1805
+ const datasetEndpoint = projectEndpoint(`/datasets/${encodeURIComponent(args[0])}`);
1806
+ const filesEndpoint = `${datasetEndpoint}/uploaded/files`;
1807
+ const upload = {
1808
+ method: "POST",
1809
+ endpoint: filesEndpoint,
1810
+ payload: { ...uploadPayload(args[1]), fileName, },
1811
+ };
1812
+ return {
1813
+ ...upload,
1814
+ identifiers: {
1815
+ datasetName: args[0],
1816
+ localPath: args[1],
1817
+ fileName,
1818
+ },
1819
+ requests: [
1820
+ { sequence: 1, method: "GET", endpoint: datasetEndpoint, },
1821
+ { sequence: 2, method: "GET", endpoint: filesEndpoint, },
1822
+ { sequence: 3, ...upload, },
1823
+ {
1824
+ sequence: 4,
1825
+ method: "GET",
1826
+ endpoint: filesEndpoint,
1827
+ assert: {
1828
+ filename: fileName,
1829
+ length: "local file byte length",
1830
+ },
1831
+ },
1832
+ ],
1833
+ };
1834
+ }
1784
1835
  case "dataset.delete":
1785
1836
  return {
1786
1837
  method: "DELETE",
@@ -2264,6 +2315,8 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2264
2315
  identifiers: {
2265
2316
  appId: id,
2266
2317
  targetProjectKey,
2318
+ preflightExecuted: false,
2319
+ preflightWillRunDuringApply: true,
2267
2320
  incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
2268
2321
  incarnationObservationRequests: [
2269
2322
  {
@@ -2286,7 +2339,13 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2286
2339
  method: "GET",
2287
2340
  endpoint: "/public/api/projects/",
2288
2341
  when: "conditional",
2289
- intent: "Fallback list probe issued only when the direct project GET is forbidden (403) to confirm the target is absent; an unconfirmed target rejects creation.",
2342
+ intent: "Fallback list probe issued only when the direct project GET is forbidden (403). Presence proves a collision; absence cannot prove availability.",
2343
+ },
2344
+ {
2345
+ method: "GET",
2346
+ endpoint: `/public/api/apps/${encodeURIComponent(id)}/instances/`,
2347
+ when: "conditional",
2348
+ intent: "Fallback app-instance list probe issued when the direct project GET is forbidden (403). Presence proves a collision; absence still rejects creation as unverifiable.",
2290
2349
  },
2291
2350
  ],
2292
2351
  note: "Strict preflight: the instance POST runs only after the payload targetProjectKey is confirmed absent. Creation never writes into an existing or unprovable project.",
@@ -2348,6 +2407,8 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2348
2407
  appId: id,
2349
2408
  sourceProjectKey,
2350
2409
  targetProjectKey,
2410
+ preflightExecuted: false,
2411
+ preflightWillRunDuringApply: true,
2351
2412
  ...(targetProjectName !== undefined ? { targetProjectName, } : {}),
2352
2413
  copyPermissions,
2353
2414
  incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
@@ -2386,7 +2447,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2386
2447
  method: "GET",
2387
2448
  endpoint: "/public/api/projects/",
2388
2449
  when: "conditional",
2389
- intent: "Fallback list probe issued only when the direct project GET is forbidden (403) to confirm the target is absent; an unconfirmed target rejects creation.",
2450
+ intent: "Fallback list probe issued only when the direct project GET is forbidden (403). Presence proves a collision; absence cannot prove availability and rejects creation.",
2390
2451
  },
2391
2452
  ],
2392
2453
  ...(copyPermissions
@@ -2812,17 +2873,74 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2812
2873
  },
2813
2874
  };
2814
2875
  }
2815
- case "project.delete":
2876
+ case "project.delete": {
2877
+ const expectedProjectIncarnation = flags["expect-project-incarnation"] === undefined
2878
+ ? undefined
2879
+ : requiredPlanFlag(flags, "expect-project-incarnation", entry.usage);
2880
+ if (expectedProjectIncarnation !== undefined
2881
+ && !/^[0-9a-f]{64}$/.test(expectedProjectIncarnation)) {
2882
+ throw new UsageError("--expect-project-incarnation must be a 64-character lowercase SHA-256 hash.", "validation_failed");
2883
+ }
2884
+ const endpoint = `/public/api/projects/${encodeURIComponent(id)}${querySuffix({
2885
+ clearManagedDatasets: flags["drop-data"] === true,
2886
+ clearOutputManagedFolders: false,
2887
+ clearJobAndScenarioLogs: true,
2888
+ wait: true,
2889
+ })}`;
2890
+ const guarded = flags["if-exists"] === true
2891
+ || flags["dry-run"] === true
2892
+ || expectedProjectIncarnation !== undefined;
2893
+ if (!guarded) {
2894
+ return {
2895
+ method: "DELETE",
2896
+ endpoint,
2897
+ identifiers: { projectKey: id, },
2898
+ };
2899
+ }
2900
+ const projectProbe = `/public/api/projects/${encodeURIComponent(id)}/`;
2901
+ const incarnationGate = expectedProjectIncarnation === undefined
2902
+ ? { required: false, provided: false, }
2903
+ : {
2904
+ required: true,
2905
+ provided: true,
2906
+ expectedHash: expectedProjectIncarnation,
2907
+ };
2908
+ if (flags["dry-run"] === true) {
2909
+ return {
2910
+ method: "GET",
2911
+ endpoint: projectProbe,
2912
+ identifiers: {
2913
+ projectKey: id,
2914
+ dryRun: true,
2915
+ ifExists: flags["if-exists"] === true,
2916
+ projectIncarnationGate: incarnationGate,
2917
+ note: "Read-only guarded-delete preflight. No DELETE is issued; a supplied creationTag hash must match.",
2918
+ },
2919
+ };
2920
+ }
2816
2921
  return {
2817
2922
  method: "DELETE",
2818
- endpoint: `/public/api/projects/${encodeURIComponent(id)}${querySuffix({
2819
- clearManagedDatasets: flags["drop-data"] === true,
2820
- clearOutputManagedFolders: false,
2821
- clearJobAndScenarioLogs: true,
2822
- wait: true,
2823
- })}`,
2824
- identifiers: { projectKey: id, },
2923
+ endpoint,
2924
+ identifiers: {
2925
+ projectKey: id,
2926
+ ifExists: flags["if-exists"] === true,
2927
+ projectIncarnationGate: incarnationGate,
2928
+ incarnationControl: expectedProjectIncarnation === undefined
2929
+ ? "none"
2930
+ : "client-side-non-atomic-stale-identity-check",
2931
+ preflightRequests: [{
2932
+ method: "GET",
2933
+ endpoint: projectProbe,
2934
+ intent: expectedProjectIncarnation === undefined
2935
+ ? "Confirm existence before a convergent delete."
2936
+ : "Recompute the current project-incarnation hash from creationTag before DELETE.",
2937
+ },],
2938
+ note: expectedProjectIncarnation === undefined
2939
+ ? "Convergent delete: a 404 preflight is an already-absent success only with --if-exists."
2940
+ : "Incarnation-bound delete: the GET must match the expected creationTag hash before an unconditional DELETE. DSS exposes no conditional project DELETE, so the client-side gate cannot serialize against key reuse after the GET.",
2941
+ },
2825
2942
  };
2943
+ }
2826
2944
  case "project.duplicate": {
2827
2945
  const options = jsonInput(flags);
2828
2946
  return {
@@ -2844,11 +2962,32 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2844
2962
  },
2845
2963
  };
2846
2964
  }
2847
- case "project.import":
2848
- return {
2965
+ case "project.import": {
2966
+ const settings = jsonInput(flags) ?? {};
2967
+ const rawTarget = flags["target-project-key"];
2968
+ const targetProjectKey = rawTarget?.trim();
2969
+ if (rawTarget !== undefined && targetProjectKey === "") {
2970
+ throw new UsageError(`--target-project-key must not be empty. Usage: ${entry.usage}`);
2971
+ }
2972
+ const settingsTarget = settings.targetProjectKey;
2973
+ if (settingsTarget !== undefined
2974
+ && (typeof settingsTarget !== "string" || settingsTarget.trim() === "")) {
2975
+ throw new UsageError(`targetProjectKey in import settings must be a non-empty string. Usage: ${entry.usage}`);
2976
+ }
2977
+ if (targetProjectKey !== undefined
2978
+ && settingsTarget !== undefined
2979
+ && targetProjectKey !== settingsTarget.trim()) {
2980
+ throw new UsageError(`--target-project-key conflicts with targetProjectKey in import settings. Usage: ${entry.usage}`);
2981
+ }
2982
+ const processPayload = targetProjectKey === undefined
2983
+ ? settings
2984
+ : { ...settings, targetProjectKey, };
2985
+ const finalizedPayload = Object.keys(processPayload).length === 0
2986
+ ? { _: "_", }
2987
+ : processPayload;
2988
+ const upload = {
2849
2989
  method: "POST",
2850
2990
  endpoint: "/public/api/projects/import/upload",
2851
- identifiers: { filePath: id, },
2852
2991
  payload: {
2853
2992
  contentType: "multipart/form-data",
2854
2993
  fileField: "file",
@@ -2856,6 +2995,46 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2856
2995
  fileName: "tmp-import.zip",
2857
2996
  },
2858
2997
  };
2998
+ return {
2999
+ ...upload,
3000
+ identifiers: {
3001
+ filePath: id,
3002
+ ...(targetProjectKey ? { targetProjectKey, } : {}),
3003
+ archivePreflight: {
3004
+ local: true,
3005
+ required: true,
3006
+ checks: [
3007
+ "zip-integrity",
3008
+ "safe-unique-members",
3009
+ "manifest",
3010
+ "flow-references",
3011
+ "orphan-members",
3012
+ ],
3013
+ },
3014
+ successVerification: {
3015
+ usedProjectKeyRequired: true,
3016
+ projectReadRequired: true,
3017
+ projectIncarnationRequired: true,
3018
+ remappingReported: true,
3019
+ },
3020
+ ...(flags["record-cleanup"] === undefined
3021
+ ? {}
3022
+ : {
3023
+ cleanupBinding: "actual used project key plus verified creationTag incarnation hash",
3024
+ }),
3025
+ },
3026
+ requests: [
3027
+ { sequence: 1, ...upload, },
3028
+ {
3029
+ sequence: 2,
3030
+ method: "POST",
3031
+ endpoint: "/public/api/projects/import/{importId}/process",
3032
+ pathBindings: { importId: "requests[0].response.id", },
3033
+ payload: finalizedPayload,
3034
+ },
3035
+ ],
3036
+ };
3037
+ }
2859
3038
  case "project.settings-set":
2860
3039
  return {
2861
3040
  method: "PUT",
@@ -38,6 +38,7 @@ export interface FixtureDiscoveryResult {
38
38
  export interface DoctorEnvironment {
39
39
  projectKey?: string;
40
40
  dssVersion?: string;
41
+ dssApiVersion?: string;
41
42
  instanceTime?: string;
42
43
  integrationFlags: {
43
44
  mutating: boolean;
@@ -48,6 +49,15 @@ export interface DoctorEnvironment {
48
49
  apiServices: boolean;
49
50
  };
50
51
  }
52
+ /**
53
+ * Server provenance captured from documented response headers on the public
54
+ * project-list connectivity call. Null entries mean the header was absent.
55
+ */
56
+ export interface DoctorVersionInfo {
57
+ dssVersion: string | null;
58
+ dssApiVersion: string | null;
59
+ instanceTime: string | null;
60
+ }
51
61
  export interface DoctorResult {
52
62
  ok: boolean;
53
63
  checks: DoctorCheck[];
@@ -66,7 +76,7 @@ export interface DoctorResult {
66
76
  export declare function errorDetails(error: unknown): Record<string, unknown>;
67
77
  export declare function firstStringField(items: unknown[] | undefined, fields: string[]): string | null;
68
78
  export declare function integrationFlag(name: string): boolean;
69
- export declare function doctorEnvironment(projectKey?: string): DoctorEnvironment;
79
+ export declare function doctorEnvironment(projectKey?: string, versionInfo?: DoctorVersionInfo): DoctorEnvironment;
70
80
  export declare function permissionStatusForError(error: unknown): PermissionStatus;
71
81
  export declare function probeDoctorPermission(probe: () => Promise<unknown>): Promise<{
72
82
  status: PermissionStatus;
@@ -96,7 +106,7 @@ export declare function firstSafeJupyterNotebook(items: unknown[]): {
96
106
  unsafe: FixtureReject[];
97
107
  };
98
108
  export declare function discoverFixtureReport(client: DataikuClient, projectKey: string, flags: Record<string, string | boolean>): Promise<FixtureDiscoveryResult>;
99
- export declare function doctorCapabilities(client: DataikuClient, projectKey: string | undefined, accessibleProjects: unknown[] | undefined, flags: Record<string, string | boolean>): Promise<Pick<DoctorResult, "permissions" | "permissionDetails" | "fixtures" | "environment">>;
109
+ export declare function doctorCapabilities(client: DataikuClient, projectKey: string | undefined, accessibleProjects: unknown[] | undefined, flags: Record<string, string | boolean>, versionInfo?: DoctorVersionInfo): Promise<Pick<DoctorResult, "permissions" | "permissionDetails" | "fixtures" | "environment">>;
100
110
  export declare function runDoctor(flags: Record<string, string | boolean>): Promise<{
101
111
  result: DoctorResult;
102
112
  exitCode: number;
@@ -1,5 +1,6 @@
1
1
  import { DataikuClient, } from "../client.js";
2
2
  import { DataikuError, } from "../errors.js";
3
+ import { ProjectSummaryArraySchema, safeParseSchema, } from "../schemas.js";
3
4
  import { num, splitCsvFlag, stringField, } from "./coerce.js";
4
5
  import { currentCommandContext, resolveCredentials, } from "./runtime.js";
5
6
  import { UsageError, } from "./usage.js";
@@ -31,9 +32,12 @@ export function integrationFlag(name) {
31
32
  const value = process.env[name];
32
33
  return value === "1" || value?.toLowerCase() === "true";
33
34
  }
34
- export function doctorEnvironment(projectKey) {
35
+ export function doctorEnvironment(projectKey, versionInfo) {
35
36
  return {
36
37
  ...(projectKey ? { projectKey, } : {}),
38
+ ...(versionInfo?.dssVersion ? { dssVersion: versionInfo.dssVersion, } : {}),
39
+ ...(versionInfo?.dssApiVersion ? { dssApiVersion: versionInfo.dssApiVersion, } : {}),
40
+ ...(versionInfo?.instanceTime ? { instanceTime: versionInfo.instanceTime, } : {}),
37
41
  integrationFlags: {
38
42
  mutating: integrationFlag("RUN_DATAIKU_INTEGRATION_MUTATING"),
39
43
  adminMutating: integrationFlag("RUN_DATAIKU_ADMIN_MUTATING"),
@@ -174,7 +178,7 @@ export async function discoverFixtureReport(client, projectKey, flags) {
174
178
  },
175
179
  };
176
180
  }
177
- export async function doctorCapabilities(client, projectKey, accessibleProjects, flags) {
181
+ export async function doctorCapabilities(client, projectKey, accessibleProjects, flags, versionInfo) {
178
182
  const probeProjectKey = projectKey ?? firstStringField(accessibleProjects, ["projectKey",])
179
183
  ?? undefined;
180
184
  const probes = {
@@ -210,7 +214,7 @@ export async function doctorCapabilities(client, projectKey, accessibleProjects,
210
214
  const capabilityResult = {
211
215
  permissions,
212
216
  ...(Object.keys(permissionDetails).length > 0 ? { permissionDetails, } : {}),
213
- environment: doctorEnvironment(projectKey),
217
+ environment: doctorEnvironment(projectKey, versionInfo),
214
218
  };
215
219
  if (flags["fast"] !== true && probeProjectKey) {
216
220
  try {
@@ -255,6 +259,7 @@ export async function runDoctor(flags) {
255
259
  : "Missing Dataiku URL and/or API key. Set DATAIKU_URL/DATAIKU_API_KEY or pass --url/--api-key.",
256
260
  });
257
261
  let accessibleProjects;
262
+ let environmentVersion;
258
263
  if (credentialsOk) {
259
264
  const requestTimeoutMs = num(flags["request-timeout"], "--request-timeout");
260
265
  const retryMaxAttempts = num(flags["retries"], "--retries");
@@ -269,14 +274,40 @@ export async function runDoctor(flags) {
269
274
  caCertPath,
270
275
  });
271
276
  try {
272
- const projects = await client.projects.list();
273
- accessibleProjects = projects;
274
- checks.push({
275
- name: "connectivity",
276
- ok: true,
277
- message: "Connected to DSS and listed accessible projects.",
278
- details: { projectCount: projects.length, },
279
- });
277
+ const projects = await client.getWithMetadata("/public/api/projects/");
278
+ // Documented headers on the public project-list call populate the
279
+ // optional environment fields; absent headers stay absent. Header
280
+ // provenance is independent of the body being well-formed.
281
+ environmentVersion = {
282
+ dssVersion: projects.meta.dssVersion,
283
+ dssApiVersion: projects.meta.dssApiVersion,
284
+ instanceTime: projects.meta.date,
285
+ };
286
+ // A reachable endpoint that answers with something other than a
287
+ // project list is not working connectivity: the body never becomes
288
+ // capability input, so probes cannot iterate a non-array.
289
+ const parsed = safeParseSchema(ProjectSummaryArraySchema, projects.data);
290
+ if (parsed.success) {
291
+ accessibleProjects = parsed.data;
292
+ checks.push({
293
+ name: "connectivity",
294
+ ok: true,
295
+ message: "Connected to DSS and listed accessible projects.",
296
+ details: { projectCount: parsed.data.length, },
297
+ });
298
+ }
299
+ else {
300
+ checks.push({
301
+ name: "connectivity",
302
+ ok: false,
303
+ message: "Connected to DSS but the project list response was not a project list.",
304
+ details: {
305
+ reason: "invalid_project_list",
306
+ errorCount: parsed.errors.length,
307
+ errors: parsed.errors.slice(0, 5),
308
+ },
309
+ });
310
+ }
280
311
  }
281
312
  catch (error) {
282
313
  checks.push({
@@ -323,8 +354,8 @@ export async function runDoctor(flags) {
323
354
  tlsRejectUnauthorized,
324
355
  caCertPath,
325
356
  });
326
- result.environment = doctorEnvironment(projectKey);
327
- Object.assign(result, await doctorCapabilities(client, projectKey, accessibleProjects, flags));
357
+ result.environment = doctorEnvironment(projectKey, environmentVersion);
358
+ Object.assign(result, await doctorCapabilities(client, projectKey, accessibleProjects, flags, environmentVersion));
328
359
  }
329
360
  return { result, exitCode: result.ok ? 0 : 2, };
330
361
  }
@@ -96,11 +96,13 @@ export const VALUE_FLAGS = new Set([
96
96
  "env-version",
97
97
  "expect-hash",
98
98
  "expect-project-incarnation",
99
+ "expected",
99
100
  "expect-version",
100
101
  "data-file",
101
102
  "database",
102
103
  "dataset",
103
104
  "file",
105
+ "file-name",
104
106
  "file-type",
105
107
  "env",
106
108
  "install-core-packages",
@@ -87,12 +87,22 @@ export declare function futureTargetIdentity(result: unknown): {
87
87
  projectKey: string;
88
88
  field: "targetProjectKey" | "projectKey";
89
89
  } | null;
90
+ interface TargetAbsenceContext {
91
+ appId: string;
92
+ visibleAppInstanceKeys?: readonly (string | undefined)[];
93
+ }
90
94
  /**
91
95
  * Creation cleanup is safe only when the target was proven absent before POST.
92
96
  * A hidden 403 is not absence: project listing can prove a collision, but it
93
97
  * cannot prove that an inaccessible project does not exist.
94
98
  */
95
- export declare function requireAbsentProjectTarget(client: DataikuClient, targetProjectKey: string, targetFlag: "--to" | "targetProjectKey"): Promise<void>;
99
+ export declare function requireAbsentProjectTarget(client: DataikuClient, targetProjectKey: string, targetFlag: "--to" | "targetProjectKey", context: TargetAbsenceContext): Promise<void>;
100
+ /**
101
+ * Run every read-only gate used by successor creation without changing the
102
+ * template version or issuing the creation POST. Its manifest hash is suitable
103
+ * for the subsequent set-manifest-version --expect-hash guard.
104
+ */
105
+ export declare function preflightSuccessorInstance(client: DataikuClient, request: SuccessorInstanceRequest, usage: string): Promise<Record<string, unknown>>;
96
106
  /**
97
107
  * Create a new app instance from the current template version alongside an
98
108
  * existing instance. Classic app instances have no public upgrade endpoint, so
@@ -100,7 +110,9 @@ export declare function requireAbsentProjectTarget(client: DataikuClient, target
100
110
  * and deliberately retired.
101
111
  *
102
112
  * After the creation POST this function never throws. A post-POST failure
103
- * returns a failed-wait result carrying the target project key, so the CLI still
104
- * exits 4 and the cleanup ledger still records an addressable successor.
113
+ * returns a failed-wait result. Only a DSS future ID or verified project
114
+ * incarnation can authorize cleanup; an ambiguous POST with neither stays
115
+ * indeterminate and produces no cleanup entry.
105
116
  */
106
117
  export declare function createSuccessorInstance(client: DataikuClient, request: SuccessorInstanceRequest, usage: string): Promise<Record<string, unknown>>;
118
+ export {};