dataiku-sdk 1.1.0 → 3.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.
@@ -2315,6 +2315,8 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2315
2315
  identifiers: {
2316
2316
  appId: id,
2317
2317
  targetProjectKey,
2318
+ preflightExecuted: false,
2319
+ preflightWillRunDuringApply: true,
2318
2320
  incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
2319
2321
  incarnationObservationRequests: [
2320
2322
  {
@@ -2337,7 +2339,13 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2337
2339
  method: "GET",
2338
2340
  endpoint: "/public/api/projects/",
2339
2341
  when: "conditional",
2340
- 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.",
2341
2349
  },
2342
2350
  ],
2343
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.",
@@ -2399,6 +2407,8 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2399
2407
  appId: id,
2400
2408
  sourceProjectKey,
2401
2409
  targetProjectKey,
2410
+ preflightExecuted: false,
2411
+ preflightWillRunDuringApply: true,
2402
2412
  ...(targetProjectName !== undefined ? { targetProjectName, } : {}),
2403
2413
  copyPermissions,
2404
2414
  incarnationControl: "client-side-non-atomic-future-target-and-creation-tag-join",
@@ -2437,7 +2447,7 @@ export function commandPlanShape(resource, action, args, flags, entry, projectKe
2437
2447
  method: "GET",
2438
2448
  endpoint: "/public/api/projects/",
2439
2449
  when: "conditional",
2440
- 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.",
2441
2451
  },
2442
2452
  ],
2443
2453
  ...(copyPermissions
@@ -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 {};
@@ -150,6 +150,7 @@ export async function verifyAppInstance(client, appId, projectKey, expectVersion
150
150
  }
151
151
  const SUCCESSOR_REMEDIATION = "The successor project may exist: replay the recorded cleanup entry, which future-gates deletion when DSS supplied a future ID. Without a cleanup entry, inspect the creation future and wait for it to become terminal before deleting the requested successor project. The predecessor project was not modified.";
152
152
  const CREATE_REJECTED_REMEDIATION = "DSS refused the creation request, so no successor project was created: fix the reported problem and retry. The predecessor project was not modified.";
153
+ const SUCCESSOR_UNBOUND_REMEDIATION = "DSS may have accepted the successor request, but no future ID or project incarnation is available to authorize cleanup. Inspect the app's instances and DSS task history; do not delete the requested key until creation is known terminal and its identity is verified. The predecessor project was not modified.";
153
154
  /**
154
155
  * True only when DSS definitively refused a write: a non-retryable 4xx with a
155
156
  * classified client-side taxonomy. Transport failures (status 0), 5xx, retryable
@@ -186,7 +187,19 @@ export function futureTargetIdentity(result) {
186
187
  * A hidden 403 is not absence: project listing can prove a collision, but it
187
188
  * cannot prove that an inaccessible project does not exist.
188
189
  */
189
- export async function requireAbsentProjectTarget(client, targetProjectKey, targetFlag) {
190
+ export async function requireAbsentProjectTarget(client, targetProjectKey, targetFlag, context) {
191
+ if (context.visibleAppInstanceKeys?.includes(targetProjectKey) === true) {
192
+ throw new UsageError(`Target project ${targetProjectKey} already exists as an app instance.`, "validation_failed", `Choose an unused ${targetFlag}: app instance creation never writes into an existing project.`, {
193
+ targetProjectKey,
194
+ targetFlag,
195
+ directTargetProbe: "not-executed",
196
+ targetVisibleInProjectList: null,
197
+ targetVisibleInAppInstances: true,
198
+ appInstancesProbe: 200,
199
+ preflightExecuted: true,
200
+ creationPostAttempted: false,
201
+ });
202
+ }
190
203
  try {
191
204
  const existing = await readIfExists(() => client.projects.get(targetProjectKey));
192
205
  if (existing === undefined)
@@ -195,10 +208,61 @@ export async function requireAbsentProjectTarget(client, targetProjectKey, targe
195
208
  catch (error) {
196
209
  if (!(error instanceof DataikuError) || error.status !== 403)
197
210
  throw error;
198
- const accessible = await client.projects.list();
199
- if (!accessible.some((project) => project.projectKey === targetProjectKey)) {
200
- throw new UsageError(`Could not confirm target project ${targetProjectKey} is absent.`, "validation_failed", "Grant project-list/details visibility or choose a target key whose absence DSS can confirm; creation never risks cleanup against an unproven target.", { targetProjectKey, targetFlag, targetProbe: "forbidden-and-not-listable", });
211
+ let projectListProbe = 200;
212
+ let targetVisibleInProjectList;
213
+ try {
214
+ const accessible = await client.projects.list();
215
+ targetVisibleInProjectList = accessible.some((project) => project.projectKey === targetProjectKey);
216
+ }
217
+ catch (projectListError) {
218
+ if (!(projectListError instanceof DataikuError) || projectListError.status !== 403) {
219
+ throw projectListError;
220
+ }
221
+ projectListProbe = 403;
222
+ targetVisibleInProjectList = null;
201
223
  }
224
+ let appInstancesProbe = 200;
225
+ let visibleAppInstanceKeys = context.visibleAppInstanceKeys;
226
+ if (visibleAppInstanceKeys === undefined) {
227
+ try {
228
+ visibleAppInstanceKeys = (await client.applications.listInstances(context.appId))
229
+ .map((instance) => instance.projectKey);
230
+ }
231
+ catch (appInstancesError) {
232
+ if (!(appInstancesError instanceof DataikuError) || appInstancesError.status !== 403) {
233
+ throw appInstancesError;
234
+ }
235
+ appInstancesProbe = 403;
236
+ visibleAppInstanceKeys = [];
237
+ }
238
+ }
239
+ const targetVisibleInAppInstances = appInstancesProbe === 200
240
+ ? visibleAppInstanceKeys?.includes(targetProjectKey) ?? false
241
+ : null;
242
+ const probeDetails = {
243
+ targetProjectKey,
244
+ targetFlag,
245
+ directTargetProbe: 403,
246
+ projectListProbe,
247
+ targetVisibleInProjectList,
248
+ targetVisibleInAppInstances,
249
+ appInstancesProbe,
250
+ preflightExecuted: true,
251
+ creationPostAttempted: false,
252
+ };
253
+ if (targetVisibleInProjectList !== true && targetVisibleInAppInstances !== true) {
254
+ throw new UsageError(`Could not confirm target project ${targetProjectKey} is absent.`, "target_absence_unverifiable", "DSS exposes no permission-independent project-key availability endpoint, and app-instance creation has no published atomic duplicate-key guarantee. Retry with an identity that has global project visibility; creation was not attempted.", {
255
+ ...probeDetails,
256
+ targetProbe: "forbidden-and-not-listable",
257
+ deploymentMasksUnknownProjects: "possible-but-unproven",
258
+ supportedRecoveryModes: ["grant-global-project-visibility",],
259
+ unavailableRecoveryModes: [
260
+ "use-supported-key-availability-endpoint",
261
+ "server-atomic-create",
262
+ ],
263
+ });
264
+ }
265
+ throw new UsageError(`Target project ${targetProjectKey} already exists.`, "validation_failed", `Choose an unused ${targetFlag}: app instance creation never writes into an existing project.`, probeDetails);
202
266
  }
203
267
  throw new UsageError(`Target project ${targetProjectKey} already exists.`, "validation_failed", `Choose an unused ${targetFlag}: app instance creation never writes into an existing project.`, { targetProjectKey, targetFlag, });
204
268
  }
@@ -226,12 +290,26 @@ async function preflightSuccessor(client, request, usage) {
226
290
  if (source.projectAppType !== "APP_INSTANCE") {
227
291
  throw new UsageError(`Project ${from} is a ${source.projectAppType ?? "non-app"} project, not a classic Dataiku App instance.`, "validation_failed", "--from must name an APP_INSTANCE project; app templates are versioned with `dss app set-manifest-version`.", { sourceProjectKey: from, projectAppType: source.projectAppType ?? null, });
228
292
  }
229
- await requireAbsentProjectTarget(client, to, "--to");
293
+ await requireAbsentProjectTarget(client, to, "--to", {
294
+ appId,
295
+ visibleAppInstanceKeys: instances.map((instance) => instance.projectKey),
296
+ });
230
297
  const templateManifest = await client.applications.getAppManifest(appId);
231
298
  const templateVersion = rawVersion(templateManifest);
232
299
  if (templateVersion === null) {
233
300
  throw new UsageError(`App template ${appId} has no manifest version, so a successor instance cannot be verified against it.`, "validation_failed", "Set the template version first with `dss app set-manifest-version --manifest-version V --project-key <templateProjectKey>`.", { appId, });
234
301
  }
302
+ const templateReferenceValidation = await client.applications.validateAppManifest(templateManifest, appId);
303
+ if (!templateReferenceValidation.valid) {
304
+ throw new UsageError(`App template ${appId} failed manifest reference validation.`, "validation_failed", "Fix the template manifest references before changing its version or creating a successor.", {
305
+ appId,
306
+ manifestHash: templateReferenceValidation.manifestHash,
307
+ checks: templateReferenceValidation.checks,
308
+ errors: templateReferenceValidation.errors,
309
+ preflightExecuted: true,
310
+ creationPostAttempted: false,
311
+ });
312
+ }
235
313
  const sourcePermissions = request.copyPermissions
236
314
  ? await client.projects.getPermissions(from)
237
315
  : undefined;
@@ -243,6 +321,7 @@ async function preflightSuccessor(client, request, usage) {
243
321
  targetExists: false,
244
322
  templateManifestHash: stableHash(templateManifest),
245
323
  templateVersion,
324
+ templateReferenceValidation,
246
325
  ...(sourcePermissions
247
326
  ? {
248
327
  sourcePermissions,
@@ -251,6 +330,52 @@ async function preflightSuccessor(client, request, usage) {
251
330
  : {}),
252
331
  };
253
332
  }
333
+ /**
334
+ * Run every read-only gate used by successor creation without changing the
335
+ * template version or issuing the creation POST. Its manifest hash is suitable
336
+ * for the subsequent set-manifest-version --expect-hash guard.
337
+ */
338
+ export async function preflightSuccessorInstance(client, request, usage) {
339
+ const preflight = await preflightSuccessor(client, request, usage);
340
+ return {
341
+ action: "successor-preflight",
342
+ preflight: "passed",
343
+ preflightExecuted: true,
344
+ creationPostAttempted: false,
345
+ versionMutationAttempted: false,
346
+ appId: request.appId,
347
+ source: {
348
+ projectKey: request.from,
349
+ projectAppType: preflight.sourceProjectAppType,
350
+ version: preflight.sourceVersion,
351
+ manifestHash: preflight.sourceManifestHash,
352
+ ...(preflight.sourcePermissionsHash
353
+ ? { permissionsHash: preflight.sourcePermissionsHash, }
354
+ : {}),
355
+ },
356
+ target: {
357
+ projectKey: request.to,
358
+ name: preflight.targetProjectName,
359
+ exists: false,
360
+ },
361
+ targetPreflight: "confirmed-absent",
362
+ template: {
363
+ projectKey: request.appId,
364
+ version: preflight.templateVersion,
365
+ manifestHash: preflight.templateManifestHash,
366
+ referenceValidation: {
367
+ valid: preflight.templateReferenceValidation.valid,
368
+ checks: preflight.templateReferenceValidation.checks,
369
+ errors: preflight.templateReferenceValidation.errors,
370
+ },
371
+ },
372
+ copyPermissions: request.copyPermissions,
373
+ next: {
374
+ setVersion: `dss app set-manifest-version --manifest-version <VERSION> --expect-hash ${preflight.templateManifestHash} --project-key ${request.appId}`,
375
+ createSuccessor: `dss app create-successor-instance ${request.appId} --from ${request.from} --to ${request.to}${request.name !== undefined ? ` --name ${JSON.stringify(request.name)}` : ""}${request.copyPermissions ? " --copy-permissions" : ""}`,
376
+ },
377
+ };
378
+ }
254
379
  /**
255
380
  * Create a new app instance from the current template version alongside an
256
381
  * existing instance. Classic app instances have no public upgrade endpoint, so
@@ -258,8 +383,9 @@ async function preflightSuccessor(client, request, usage) {
258
383
  * and deliberately retired.
259
384
  *
260
385
  * After the creation POST this function never throws. A post-POST failure
261
- * returns a failed-wait result carrying the target project key, so the CLI still
262
- * exits 4 and the cleanup ledger still records an addressable successor.
386
+ * returns a failed-wait result. Only a DSS future ID or verified project
387
+ * incarnation can authorize cleanup; an ambiguous POST with neither stays
388
+ * indeterminate and produces no cleanup entry.
263
389
  */
264
390
  export async function createSuccessorInstance(client, request, usage) {
265
391
  const { appId, from, to, } = request;
@@ -312,12 +438,17 @@ export async function createSuccessorInstance(client, request, usage) {
312
438
  projectKey: to,
313
439
  target: { projectKey: to, name: preflight.targetProjectName, },
314
440
  success: false,
315
- state: "CREATE_FAILED",
441
+ state: rejected ? "CREATE_FAILED" : "INDETERMINATE",
316
442
  elapsedMs: 0,
317
443
  pollCount: 0,
318
444
  stage: "create",
319
- cleanupEligible: !rejected,
320
- remediation: rejected ? CREATE_REJECTED_REMEDIATION : SUCCESSOR_REMEDIATION,
445
+ outcome: rejected ? "rejected" : "indeterminate",
446
+ creationPostAttempted: true,
447
+ cleanupEligible: false,
448
+ recoveryCommands: rejected
449
+ ? []
450
+ : [`dss app instances ${appId}`,],
451
+ remediation: rejected ? CREATE_REJECTED_REMEDIATION : SUCCESSOR_UNBOUND_REMEDIATION,
321
452
  ...safeErrorSummary(error),
322
453
  };
323
454
  }
@@ -342,11 +473,13 @@ export async function createSuccessorInstance(client, request, usage) {
342
473
  stage: "create",
343
474
  ...(instance ? { instance, } : {}),
344
475
  ...(jobId !== undefined ? { futureId: jobId, jobId, } : {}),
345
- cleanupEligible: true,
476
+ cleanupEligible: jobId !== undefined,
346
477
  error: "DSS named a different target project in the creation response.",
347
478
  expected: { projectKey: to, },
348
479
  actual: { projectKey: echoedTarget, },
349
- remediation: SUCCESSOR_REMEDIATION,
480
+ remediation: jobId !== undefined
481
+ ? SUCCESSOR_REMEDIATION
482
+ : SUCCESSOR_UNBOUND_REMEDIATION,
350
483
  };
351
484
  }
352
485
  const base = {
@@ -381,13 +514,16 @@ export async function createSuccessorInstance(client, request, usage) {
381
514
  : "scalar",
382
515
  }),
383
516
  success: false,
384
- state: "UNTRACKABLE",
517
+ state: "INDETERMINATE",
385
518
  elapsedMs: 0,
386
519
  pollCount: 0,
387
520
  stage: "future-wait",
388
- cleanupEligible: true,
521
+ outcome: "indeterminate",
522
+ creationPostAttempted: true,
523
+ cleanupEligible: false,
389
524
  error: "DSS returned no instance-creation future ID, so the successor instance cannot be verified.",
390
- remediation: SUCCESSOR_REMEDIATION,
525
+ remediation: SUCCESSOR_UNBOUND_REMEDIATION,
526
+ recoveryCommands: [`dss app instances ${appId}`,],
391
527
  };
392
528
  }
393
529
  else {
@@ -408,6 +544,7 @@ export async function createSuccessorInstance(client, request, usage) {
408
544
  pollCount: 0,
409
545
  stage: "future-wait",
410
546
  cleanupEligible: true,
547
+ recoveryCommands: [`dss future wait ${jobId}`, `dss app instances ${appId}`,],
411
548
  remediation: SUCCESSOR_REMEDIATION,
412
549
  ...safeErrorSummary(error),
413
550
  };
@@ -451,6 +588,9 @@ export async function createSuccessorInstance(client, request, usage) {
451
588
  if (!waited.success) {
452
589
  return failure(waited.state, "future-wait", {
453
590
  error: "The instance-creation future did not complete successfully.",
591
+ recoveryCommands: jobId !== undefined
592
+ ? [`dss future wait ${jobId}`, `dss app instances ${appId}`,]
593
+ : [`dss app instances ${appId}`,],
454
594
  });
455
595
  }
456
596
  try {
@@ -12,7 +12,7 @@ export declare function isFullRevision(value: unknown): boolean;
12
12
  export declare function gitFullRevision(packageRoot: string | undefined): string | undefined;
13
13
  export declare function gitRevision(packageRoot: string | undefined): string | undefined;
14
14
  export type CliLoadSource = "source" | "dist";
15
- export type CliRuntime = "bun" | "node";
15
+ export type CliRuntime = "bun";
16
16
  export declare function detectLoadSource(modulePath?: string): CliLoadSource;
17
17
  export declare function buildMetadataRevision(packageRoot: string | undefined): string | undefined;
18
18
  /**
@@ -37,6 +37,7 @@ export interface CliVersionPayload {
37
37
  buildRevision: string | null;
38
38
  staleBuild: boolean;
39
39
  }
40
+ /** Bun is the only supported runtime; the field is retained for payload stability. */
40
41
  export declare function detectRuntime(): CliRuntime;
41
42
  export declare function buildVersionPayload(input: {
42
43
  packageVersion: string;
@@ -182,8 +182,9 @@ export function sourceTreeNewerThanBuild(packageRoot) {
182
182
  return false;
183
183
  }
184
184
  }
185
+ /** Bun is the only supported runtime; the field is retained for payload stability. */
185
186
  export function detectRuntime() {
186
- return process.versions.bun ? "bun" : "node";
187
+ return "bun";
187
188
  }
188
189
  export function buildVersionPayload(input) {
189
190
  const isDist = input.loadSource === "dist";
package/dist/src/cli.js CHANGED
@@ -1241,8 +1241,11 @@ function buildErrorReport(err) {
1241
1241
  ok: false,
1242
1242
  error: err.message,
1243
1243
  code: err.code,
1244
- category: "usage",
1244
+ category: err.code === "target_absence_unverifiable"
1245
+ ? "permission_or_environment"
1246
+ : "usage",
1245
1247
  exitCode,
1248
+ ...(err.code === "target_absence_unverifiable" ? { retryable: false, } : {}),
1246
1249
  ...(err.hint ? { hint: err.hint, } : {}),
1247
1250
  ...(err.details ? { details: err.details, } : {}),
1248
1251
  ...context,
@@ -1,4 +1,4 @@
1
- import { openAsBlob, readFileSync, } from "node:fs";
1
+ import { readFileSync, } from "node:fs";
2
2
  import { basename, } from "node:path";
3
3
  import { getCACertificates, } from "node:tls";
4
4
  import { Value, } from "@sinclair/typebox/value";
@@ -479,7 +479,7 @@ export class DataikuClient {
479
479
  });
480
480
  }
481
481
  async uploadResponse(path, filePath, fileName) {
482
- const fileBlob = await openAsBlob(filePath);
482
+ const fileBlob = Bun.file(filePath);
483
483
  const formData = new FormData();
484
484
  formData.append("file", fileBlob, fileName ?? basename(filePath));
485
485
  return this.fetchWithRetry(`${this.baseUrl}${path}`, {
@@ -1,5 +1,5 @@
1
1
  export type DataikuErrorCategory = "not_found" | "forbidden" | "validation" | "transient" | "unknown";
2
- export type StableErrorCode = "usage_error" | "unknown_flag" | "missing_required_arg" | "missing_required_flag" | "invalid_flag_value" | "conflicting_input_sources" | "invalid_enum" | "not_found" | "permission_denied" | "validation_failed" | "transient" | "long_running_failure" | "command_result_failure" | "assertion_failed" | "ambiguous_outcome" | "internal_error";
2
+ export type StableErrorCode = "usage_error" | "unknown_flag" | "missing_required_arg" | "missing_required_flag" | "invalid_flag_value" | "conflicting_input_sources" | "invalid_enum" | "not_found" | "permission_denied" | "target_absence_unverifiable" | "validation_failed" | "transient" | "long_running_failure" | "command_result_failure" | "assertion_failed" | "ambiguous_outcome" | "internal_error";
3
3
  export declare function dataikuErrorCode(category: DataikuErrorCategory): StableErrorCode;
4
4
  export interface DataikuErrorTaxonomy {
5
5
  category: DataikuErrorCategory;
@@ -1,7 +1,4 @@
1
- import { createWriteStream, } from "node:fs";
2
- import { resolve, } from "node:path";
3
- import { Readable, } from "node:stream";
4
- import { pipeline, } from "node:stream/promises";
1
+ import * as nodePath from "node:path";
5
2
  import { FolderDetailsSchema, FolderItemArraySchema, FolderSummaryArraySchema, } from "../schemas.js";
6
3
  import { deepMerge, } from "../utils/deep-merge.js";
7
4
  import { sanitizeFileName, } from "../utils/sanitize.js";
@@ -72,10 +69,11 @@ export class FoldersResource extends BaseResource {
72
69
  const normalizedPath = normalizeRemotePath(path);
73
70
  const pEnc = encodeURIComponent(normalizedPath);
74
71
  const res = await this.client.stream(`/public/api/projects/${this.enc(opts?.projectKey)}/managedfolders/${fEnc}/contents/${pEnc}`);
75
- const dest = opts?.localPath ?? resolve(process.cwd(), inferDownloadFileName(normalizedPath));
76
- const nodeStream = Readable.fromWeb(res.body);
77
- const fileOut = createWriteStream(dest);
78
- await pipeline(nodeStream, fileOut);
72
+ if (!res.body)
73
+ throw new Error("folders.download response did not include a body");
74
+ const dest = opts?.localPath
75
+ ?? nodePath.resolve(process.cwd(), inferDownloadFileName(normalizedPath));
76
+ await Bun.write(dest, new Response(res.body), { createPath: false, });
79
77
  return dest;
80
78
  }
81
79
  upload(folderId, path, localPath, projectKey) {
@@ -1,5 +1,3 @@
1
- import { createWriteStream, } from "node:fs";
2
- import { Writable, } from "node:stream";
3
1
  import { ClientValidationError, DataikuError, } from "../errors.js";
4
2
  import { ProjectDetailsSchema, ProjectMetadataSchema, ProjectSummaryArraySchema, } from "../schemas.js";
5
3
  import { normalizeFlowGraph, } from "../utils/flow-map.js";
@@ -159,14 +157,7 @@ export class ProjectsResource extends BaseResource {
159
157
  const res = await this.postStream(`/public/api/projects/${encodeURIComponent(projectKey)}/export`, options ?? {});
160
158
  if (!res.body)
161
159
  throw new Error("projects.exportArchive response did not include a body");
162
- const file = createWriteStream(filePath);
163
- try {
164
- await res.body.pipeTo(Writable.toWeb(file));
165
- }
166
- catch (error) {
167
- file.destroy();
168
- throw error;
169
- }
160
+ await Bun.write(filePath, new Response(res.body), { createPath: false, });
170
161
  }
171
162
  /** Upload a project archive and return its temporary import handle. */
172
163
  async prepareProjectImport(filePath) {
package/dist/src/skill.js CHANGED
@@ -1,4 +1,3 @@
1
- import { execFileSync, } from "node:child_process";
2
1
  import * as fs from "node:fs";
3
2
  import * as os from "node:os";
4
3
  import * as path from "node:path";
@@ -61,24 +60,11 @@ export const AGENTS = {
61
60
  content: skillContent,
62
61
  },
63
62
  };
64
- // ---------------------------------------------------------------------------
65
- // Agent detection
66
- // ---------------------------------------------------------------------------
67
- function binaryExists(name) {
68
- const cmd = process.platform === "win32" ? "where" : "which";
69
- try {
70
- execFileSync(cmd, [name,], { stdio: "pipe", });
71
- return true;
72
- }
73
- catch {
74
- return false;
75
- }
76
- }
77
63
  export function detectAgents() {
78
64
  const home = os.homedir();
79
65
  const found = [];
80
66
  for (const [id, def,] of Object.entries(AGENTS)) {
81
- const hasBinary = binaryExists(def.binary);
67
+ const hasBinary = Bun.which(def.binary) !== null;
82
68
  const hasConfigDir = fs.existsSync(path.join(home, def.configDir));
83
69
  if (hasBinary && (!def.configDirRequired || hasConfigDir)) {
84
70
  found.push({ id, def, via: "binary", });
@@ -90,9 +76,6 @@ export function detectAgents() {
90
76
  return found;
91
77
  }
92
78
  // ---------------------------------------------------------------------------
93
- // Skill installation
94
- // ---------------------------------------------------------------------------
95
- // ---------------------------------------------------------------------------
96
79
  // Workspace root detection
97
80
  // ---------------------------------------------------------------------------
98
81
  const WORKSPACE_MARKERS = [".git",];
@@ -17,7 +17,7 @@ export interface ProjectArchiveInspection {
17
17
  * Inspect a DSS project export ZIP archive without extracting anything to
18
18
  * disk: enumerate every central-directory record, validate member paths
19
19
  * independently, stream every file member through a size/CRC-32 check with
20
- * bounded memory (manual `node:zlib.crc32`, covering data-descriptor
20
+ * bounded memory (manual `Bun.hash.crc32`, covering data-descriptor
21
21
  * records), parse `export-manifest.json` and the source project key, and run
22
22
  * the generic dangling-reference/orphan-member graph checks. Identity,
23
23
  * history and machine-specific residue is reported only as warnings carrying
@@ -1,6 +1,5 @@
1
1
  import StreamZip from "node-stream-zip";
2
2
  import { stat, } from "node:fs/promises";
3
- import { crc32, } from "node:zlib";
4
3
  import { ClientValidationError, } from "../errors.js";
5
4
  const MANIFEST_MEMBER = "export-manifest.json";
6
5
  const DATASETS_ROOT = "project_config/datasets";
@@ -106,7 +105,7 @@ function isUnsafeMemberPath(name) {
106
105
  /**
107
106
  * Stream one member through a bounded-memory CRC-32/size check. The library's
108
107
  * own verification is bypassed for data-descriptor records (bit 3 set), so the
109
- * manual `node:zlib.crc32` computation covers every record uniformly.
108
+ * manual `Bun.hash.crc32` computation covers every record uniformly.
110
109
  *
111
110
  * When `accumulate` is set, buffered content never exceeds `maxBytes`: the
112
111
  * buffer is dropped as soon as the cap is reached while the member is still
@@ -130,7 +129,7 @@ function streamMember(zip, entry, accumulate, maxBytes = Number.POSITIVE_INFINIT
130
129
  stream.on("data", (chunk) => {
131
130
  const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
132
131
  bytes += buffer.length;
133
- crc = crc32(buffer, crc) >>> 0;
132
+ crc = Bun.hash.crc32(buffer, crc) >>> 0;
134
133
  if (!content || truncated)
135
134
  return;
136
135
  if (bytes > maxBytes) {
@@ -307,7 +306,7 @@ function residueIssue(member, counts) {
307
306
  * Inspect a DSS project export ZIP archive without extracting anything to
308
307
  * disk: enumerate every central-directory record, validate member paths
309
308
  * independently, stream every file member through a size/CRC-32 check with
310
- * bounded memory (manual `node:zlib.crc32`, covering data-descriptor
309
+ * bounded memory (manual `Bun.hash.crc32`, covering data-descriptor
311
310
  * records), parse `export-manifest.json` and the source project key, and run
312
311
  * the generic dangling-reference/orphan-member graph checks. Identity,
313
312
  * history and machine-specific residue is reported only as warnings carrying
@@ -1,6 +1,5 @@
1
- import { createHash, } from "node:crypto";
2
1
  export function sha256Hex(value) {
3
- return createHash("sha256").update(value).digest("hex");
2
+ return new Bun.CryptoHasher("sha256").update(value).digest("hex");
4
3
  }
5
4
  /** Locale-independent UTF-16 code-unit order for deterministic machine output. */
6
5
  export function compareStrings(a, b) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "1.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -19,15 +19,14 @@
19
19
  "packages/types/dist/"
20
20
  ],
21
21
  "engines": {
22
- "node": ">=22.15.0",
23
- "bun": ">=1.3.14"
22
+ "bun": ">=1.4.0"
24
23
  },
25
24
  "scripts": {
26
- "build": "tsc && node scripts/generate-build-metadata.mjs && tsc -p packages/types/tsconfig.json",
27
- "prepack": "node scripts/sync-plugin-version.mjs --check && tsc && node scripts/generate-build-metadata.mjs && tsc -p packages/types/tsconfig.json",
28
- "prepublishOnly": "node scripts/sync-plugin-version.mjs --check && tsc && node scripts/generate-build-metadata.mjs && tsc -p packages/types/tsconfig.json",
29
- "check": "tsc --noEmit && node scripts/sync-plugin-version.mjs --check",
30
- "version": "node scripts/sync-plugin-version.mjs && git add plugin.json",
25
+ "build": "tsc && bun scripts/generate-build-metadata.mjs && tsc -p packages/types/tsconfig.json",
26
+ "prepack": "bun scripts/sync-plugin-version.mjs --check && tsc && bun scripts/generate-build-metadata.mjs && tsc -p packages/types/tsconfig.json",
27
+ "prepublishOnly": "bun scripts/sync-plugin-version.mjs --check && tsc && bun scripts/generate-build-metadata.mjs && tsc -p packages/types/tsconfig.json",
28
+ "check": "tsc --noEmit && bun scripts/sync-plugin-version.mjs --check",
29
+ "version": "bun scripts/sync-plugin-version.mjs && git add plugin.json",
31
30
  "lint": "oxlint src/",
32
31
  "lint:fix": "oxlint --fix src/",
33
32
  "format": "dprint fmt",
@@ -41,7 +40,7 @@
41
40
  "test:integration:rigorous:mutating": "cross-env RUN_DATAIKU_INTEGRATION=1 RUN_DATAIKU_INTEGRATION_MUTATING=1 bun test tests/integration-rigorous.test.ts"
42
41
  },
43
42
  "devDependencies": {
44
- "@types/bun": "^1.3.14",
43
+ "@types/bun": "^1.4.0",
45
44
  "cross-env": "^10.1.0",
46
45
  "dprint": "^0.55.2",
47
46
  "js-tiktoken": "1.0.21",
@@ -57,7 +56,7 @@
57
56
  "bundledDependencies": [
58
57
  "@sinclair/typebox"
59
58
  ],
60
- "license": "MIT",
59
+ "license": "SEE LICENSE IN LICENSE",
61
60
  "repository": {
62
61
  "type": "git",
63
62
  "url": "https://github.com/clssck/dataiku-sdk.git"
package/plugin.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "dataiku-sdk",
4
- "version": "1.1.0",
4
+ "version": "3.0.0",
5
5
  "description": "Dataiku DSS SDK and agent-oriented CLI for programmatic access to DSS REST APIs.",
6
6
  "homepage": "https://github.com/clssck/dataiku-sdk",
7
7
  "repository": "https://github.com/clssck/dataiku-sdk",
8
- "license": "MIT",
8
+ "license": "LicenseRef-Dataiku-SDK-Limited-Use-1.0",
9
9
  "keywords": [
10
10
  "dataiku",
11
11
  "dss",