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.
package/LICENSE CHANGED
@@ -1,21 +1,111 @@
1
- MIT License
1
+ Dataiku SDK Limited Use License
2
+ Version 1.0 — 31 August 2026
2
3
 
3
4
  Copyright (c) 2026 clssck
4
5
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
6
+ By copying, installing, accessing, or using this software and its associated
7
+ documentation (the "Software"), you agree to this license.
8
+
9
+ 1. Definitions
10
+
11
+ "Licensor" means clssck, the copyright holder.
12
+
13
+ "Individual" means a natural person acting solely on their own behalf. A person
14
+ acting for an employer, client, company, or other organization is not acting as
15
+ an Individual for that use.
16
+
17
+ "Company" means a for-profit legal entity, including a sole proprietorship,
18
+ partnership, corporation, or limited liability company. Companies under common
19
+ control are treated as one Company.
20
+
21
+ "Annual Revenue" means a Company's total gross revenue, together with the gross
22
+ revenue of every entity under common control, for its most recently completed
23
+ fiscal year, before deductions. A Company without a completed fiscal year must
24
+ use its good-faith projected gross revenue for its first twelve months. Revenue
25
+ in another currency must be converted to United States dollars using a
26
+ reasonable, consistently applied exchange rate.
27
+
28
+ "Qualified User" means either:
29
+
30
+ (a) an Individual; or
31
+ (b) a Company with Annual Revenue of less than US $1,000,000.
32
+
33
+ 2. Free Limited License for Qualified Users
34
+
35
+ Subject to every term of this license, the Licensor grants each Qualified User
36
+ a non-exclusive, non-transferable, and non-sublicensable license, without
37
+ a license fee, to copy, install, and run unmodified copies of the Software for
38
+ the Qualified User's own use.
39
+
40
+ Use by an employee, contractor, agent, or other person on behalf of a Company is
41
+ use by that Company. An Individual license does not extend to such use.
42
+
43
+ 3. Users That Do Not Qualify
44
+
45
+ A Company with Annual Revenue of US $1,000,000 or more, and any person or entity
46
+ that is not a Qualified User, receives no right to use the Software under
47
+ Section 2. Such a user must request and obtain a separate written license from
48
+ the Licensor before copying, installing, or using the Software. Public access
49
+ to the source code does not itself grant permission to use it.
50
+
51
+ License and approval requests may be submitted to the Licensor through:
52
+ https://github.com/clssck/dataiku-sdk
53
+
54
+ 4. Prior Written Approval Required
55
+
56
+ The rights granted in Section 2 do not include any right to:
57
+
58
+ (a) modify, adapt, translate, or create derivative works of the Software,
59
+ including modifications made only for private or internal use; or
60
+ (b) publish, distribute, provide, transfer, sublicense, sell, or otherwise make
61
+ all or any part of the Software available to another person or entity, whether
62
+ in source, binary, modified, or unmodified form.
63
+
64
+ Each activity listed above requires the Licensor's prior written approval.
65
+ Approval of one activity or version does not approve another. The Licensor may
66
+ condition or deny approval at its discretion.
67
+
68
+ 5. Attribution and Citation
69
+
70
+ Every permitted use must preserve all copyright, attribution, and license
71
+ notices included with the Software. The following credit and link must also
72
+ appear in documentation, notices, or an About page accompanying the use:
73
+
74
+ dataiku-sdk by clssck — https://github.com/clssck/dataiku-sdk
75
+
76
+ The credit must be reasonably prominent and may not suggest endorsement by the
77
+ Licensor. Prior written approval under Section 4 does not waive these
78
+ requirements unless the approval expressly says so.
79
+
80
+ 6. No Other Rights
81
+
82
+ The Software is licensed, not sold. Except for the limited rights expressly
83
+ granted in this license or in a separate written agreement signed by the
84
+ Licensor, the Licensor reserves all rights. This license grants no right to use
85
+ any trademark, service mark, trade name, or logo of the Licensor.
86
+
87
+ 7. Termination
88
+
89
+ This license terminates automatically upon breach. After termination, you must
90
+ stop using the Software and delete every copy in your possession or control.
91
+ Sections 5, 6, 8, 9, and 10 survive termination.
92
+
93
+ 8. Disclaimer of Warranty
94
+
95
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
96
+ IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
97
+ PURPOSE, TITLE, AND NON-INFRINGEMENT. YOU BEAR ALL RISK ARISING FROM USE OF THE
21
98
  SOFTWARE.
99
+
100
+ 9. Limitation of Liability
101
+
102
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSOR WILL NOT BE LIABLE FOR ANY
103
+ CLAIM, LOSS, OR DAMAGES ARISING FROM OR RELATED TO THE SOFTWARE OR THIS LICENSE,
104
+ WHETHER IN CONTRACT, TORT, OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF
105
+ SUCH DAMAGES.
106
+
107
+ 10. Earlier Licenses
108
+
109
+ This license applies only to copies of the Software distributed with this
110
+ license. It does not revoke or restrict rights validly granted for copies that a
111
+ recipient previously received under another license.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Agent-only TypeScript SDK and `dss` CLI for Dataiku DSS automation.
4
4
 
5
5
  ## Platform support
6
6
 
7
- The published `dss` CLI supports Linux, macOS, and Windows under Bun >= 1.3.14 or Node.js >= 22.15.0. The release gate runs Bun 1.3.14 with Node 24 on all three operating systems, plus a lightweight minimum-Node 22.15 package smoke on Linux. The runtime dependency is pure JavaScript, so the same package also runs on runtime-supported x64 and ARM64 systems.
7
+ The published `dss` CLI requires Bun >= 1.4.0 and supports Linux, macOS, and Windows. The release gate runs Bun 1.4.0 on all three operating systems. The runtime dependency is pure JavaScript, so the same package also runs on every Bun-supported x64 and ARM64 system.
8
8
 
9
9
  Run directly with Bun:
10
10
 
@@ -17,10 +17,11 @@ Or install the npm binary:
17
17
  ```text
18
18
  npm install --global dataiku-sdk
19
19
  ```
20
+ The installed `dss` bin bootstraps Bun with automatic `.env` preloading disabled, so Bun >= 1.4.0 must be on `PATH`; the CLI's own `.env` handling stays authoritative. `bunx --bun` forces Bun's Node-compatibility mode, which provides the same no-preload guarantee across operating systems.
20
21
 
21
- Bun is the primary development runtime and package manager. Examples below assume an installed `dss` binary. From a checkout, use `bun --no-env-file src/cli.ts ...` or the cross-runtime `bun --no-env-file ./bin/dss.js ...` launcher. Node users can invoke the same launcher as `node ./bin/dss.js ...`; if `dist/` is absent, it delegates to Bun. From another working directory, pass the checkout's absolute `bin/dss.js` path to Bun or Node.
22
+ Bun is the only supported runtime and the package manager. Examples below assume an installed `dss` binary. From a checkout, use `bun --no-env-file src/cli.ts ...` or the packaged launcher `bun --no-env-file ./bin/dss.js ...`. From another working directory, pass the checkout's absolute `bin/dss.js` path to Bun.
22
23
  `--no-env-file` disables Bun's automatic preloading only; the CLI still applies its documented `.env` handling unless `DATAIKU_DISABLE_ENV=1` is set.
23
- `dss version` reports whether the running code came from `source` or `dist`, the Node/Bun runtime, the packaged build revision when available, and `staleBuild` when that revision differs from the checkout. Release builds generate `dist/build-metadata.json`; source snapshots without Git metadata still build but report no build revision.
24
+ `dss version` reports whether the running code came from `source` or `dist`, the runtime (always `bun`), the packaged build revision when available, and `staleBuild` when that revision differs from the checkout. Release builds generate `dist/build-metadata.json`; source snapshots without Git metadata still build but report no build revision.
24
25
 
25
26
  ## CLI contract
26
27
 
@@ -135,7 +136,8 @@ dss app validate-manifest --project-key MYAPP_INSTANCE
135
136
  dss app compare-manifest my-app --project-key MYAPP_INSTANCE
136
137
  dss app create-instance my-app --data '{"targetProjectKey":"MYAPP_INSTANCE"}' --wait --record-cleanup cleanup.jsonl
137
138
  dss app manifest-version --project-key MYAPP_TEMPLATE
138
- dss app set-manifest-version --manifest-version 1.4.0 --project-key MYAPP_TEMPLATE
139
+ dss app successor-preflight my-app --from MYAPP_INSTANCE --to MYAPP_INSTANCE_V2 --copy-permissions
140
+ dss app set-manifest-version --manifest-version 1.4.0 --expect-hash PREFLIGHT_TEMPLATE_MANIFEST_HASH --project-key MYAPP_TEMPLATE
139
141
  dss app create-successor-instance my-app --from MYAPP_INSTANCE --to MYAPP_INSTANCE_V2 --copy-permissions --record-cleanup cleanup.jsonl
140
142
  dss app verify-instance my-app --project-key MYAPP_INSTANCE_V2 --expect-version 1.4.0
141
143
  dss app permissions-snapshot --project-key MYAPP_INSTANCE --output permissions.json
@@ -182,6 +184,12 @@ dss app compare-manifest APP_ID --project-key RELEASE_INSTANCE
182
184
  # Read the raw `version` from the public app manifest (string fields only; never appVersion).
183
185
  dss app manifest-version --project-key APP_TEMPLATE
184
186
 
187
+ # Run every read-only successor gate before changing the template version. This validates the
188
+ # template references, verifies the predecessor, proves target absence, and optionally snapshots
189
+ # the predecessor ACL. Keep template.manifestHash from the result for the next command.
190
+ dss app successor-preflight APP_ID \
191
+ --from RELEASE_INSTANCE --to RELEASE_INSTANCE_V2 --copy-permissions
192
+
185
193
  # Write version/versionNotes through the public app-manifest endpoint. This is NOT a publish
186
194
  # transaction: it only changes the raw manifest metadata. --expect-hash SHA256 is a non-atomic
187
195
  # stale-read guard, not a conditional write: it refuses the write when the manifest already
@@ -189,7 +197,8 @@ dss app manifest-version --project-key APP_TEMPLATE
189
197
  # overwrite a write landing between its read and PUT, and the final read cannot detect that lost
190
198
  # update when this command's payload wins. Every result reports
191
199
  # concurrencyControl: "client-side-non-atomic-stale-read-check".
192
- dss app set-manifest-version --manifest-version 1.4.0 --project-key APP_TEMPLATE
200
+ dss app set-manifest-version --manifest-version 1.4.0 \
201
+ --expect-hash PREFLIGHT_TEMPLATE_MANIFEST_HASH --project-key APP_TEMPLATE
193
202
 
194
203
  # Create an instance only after its target key is confirmed absent, wait on the DSS future, and
195
204
  # record deterministic cleanup.
@@ -200,7 +209,8 @@ dss app create-instance APP_ID \
200
209
  # Roll out a new template version to an existing instance as an additive successor: the old
201
210
  # instance is never modified or deleted, and the command always waits on the DSS future
202
211
  # (there is no --wait flag). The recorded cleanup entry targets only the new project key. If DSS
203
- # accepts creation but returns no future ID, cleanup stops unresolved instead of racing creation.
212
+ # may have accepted creation but no future ID is returned, the outcome is indeterminate and no
213
+ # unbound cleanup entry is written.
204
214
  dss app create-successor-instance APP_ID \
205
215
  --from RELEASE_INSTANCE --to RELEASE_INSTANCE_V2 \
206
216
  --name "Release instance v2" --copy-permissions --record-cleanup cleanup.jsonl
@@ -233,12 +243,24 @@ managed-folder IDs, and runtime-form parameter names against supported public pr
233
243
 
234
244
  Some DSS deployments hide unknown project keys behind `403` instead of returning `404`.
235
245
  Instance creation requires confirmed target absence before POST: an inaccessible target not present
236
- in the visible project list is still unconfirmed and is rejected rather than risking cleanup
237
- against a pre-existing project. A definitive create rejection never produces a cleanup entry.
246
+ in the visible project and app-instance lists is still unconfirmed and is rejected with
247
+ `target_absence_unverifiable` / `permission_or_environment` rather than risking cleanup against a
248
+ pre-existing project. Choosing another key does not solve deployments that mask every unknown key.
249
+ DSS exposes no permission-independent public availability endpoint, and its published app-instance
250
+ API does not guarantee duplicate-key rejection before any write or non-overwrite behavior, so
251
+ there is no unsafe force or unconfirmed-target bypass. Use an identity with global project
252
+ visibility. A definitive create rejection never produces a cleanup entry; an ambiguous POST
253
+ without a future ID or verified
254
+ incarnation also produces no cleanup entry.
255
+
256
+ Static create plans describe, but do not execute, these reads:
257
+ `preflightExecuted:false` and `preflightWillRunDuringApply:true`. Run `successor-preflight` when
258
+ live read evidence is required before changing the template version.
238
259
 
239
260
  Every new cleanup entry records the canonical DSS URL. App-instance cleanup records a
240
- `creationTag` hash observed after the DSS future identifies the target key; unconfirmed creation
241
- stops unresolved. The future target and later `creationTag` are independent, non-atomic
261
+ `creationTag` hash observed after the DSS future identifies the target key; a future-addressable
262
+ entry stops unresolved until the future names that key and its incarnation can be bound. An
263
+ ambiguous POST with no future ID produces no entry. The future target and later `creationTag` are independent, non-atomic
242
264
  observations: the public API exposes neither an immutable project ID joined to the future nor a
243
265
  conditional DELETE. Cleanup rechecks type and `creationTag` immediately before deletion, rejecting
244
266
  detected key reuse, but cannot eliminate replacement in the remaining check-to-DELETE gap.
@@ -309,3 +331,15 @@ The `project-git` resource does not manage these settings, so public/auth-less a
309
331
  already-configured remotes are the supported automation paths.
310
332
 
311
333
  For fake-DSS smoke tests, return project lists as JSON arrays such as `[{ "projectKey": "MYPROJ", "name": "My Project" }]` from `/public/api/projects/`; recipe payload commands read `/public/api/projects/<PROJECT>/recipes/<NAME>?includePayload=true` and expect a JSON object shaped like `{ "recipe": { "name": "<NAME>", "type": "python" }, "payload": "..." }`.
334
+
335
+ ## License
336
+
337
+ This project is source-available under the [Dataiku SDK Limited Use License 1.0](LICENSE), not an open-source license.
338
+
339
+ - Individuals acting on their own behalf may use unmodified copies without a license fee.
340
+ - Companies with total annual gross revenue below US $1,000,000 may use unmodified copies without a license fee.
341
+ - Companies at or above that threshold, and all other entities, must request and obtain a separate written license before use.
342
+ - Every modification, including private or internal changes, and every form of redistribution requires prior written approval.
343
+ - Permitted use must preserve the license notices and display: `dataiku-sdk by clssck — https://github.com/clssck/dataiku-sdk`.
344
+
345
+ Copies previously received under another license retain the rights granted for those copies. See [LICENSE](LICENSE) for the controlling terms and request path.
package/bin/dss.js CHANGED
@@ -1,117 +1,87 @@
1
1
  #!/usr/bin/env node
2
+ // npm bin for the dss CLI. Bun is the only runtime that executes CLI code.
3
+ // A Node-launched npm shim only bootstraps Bun with automatic .env loading
4
+ // disabled; `bunx --bun` enters Bun's Node-compatibility mode, which disables
5
+ // that loading before this module runs.
2
6
  import { spawnSync, } from "node:child_process";
3
- import * as fs from "node:fs";
4
7
  import * as path from "node:path";
5
8
  import { fileURLToPath, pathToFileURL, } from "node:url";
6
9
 
7
- const args = process.argv.slice(2,);
8
- const optionArgs = args.includes("--",) ? args.slice(0, args.indexOf("--",),) : args;
9
- const here = path.dirname(fileURLToPath(import.meta.url,),);
10
- const distCliPath = path.resolve(here, "../dist/src/cli.js",);
11
- const sourceCliPath = path.resolve(here, "../src/cli.ts",);
12
- const cliPath = fs.existsSync(distCliPath,) ? distCliPath : sourceCliPath;
13
- const cliUrl = pathToFileURL(cliPath,).href;
10
+ const runningUnderBun = typeof process.versions.bun === "string";
11
+ const bunNodeCompatibilityMode = runningUnderBun
12
+ && (
13
+ path.basename(process.argv0,).toLowerCase().startsWith("node",)
14
+ || (
15
+ process.env.npm_lifecycle_event === "bunx"
16
+ && ["dss", "dataiku-sdk",].includes(process.env.npm_lifecycle_script ?? "",)
17
+ && /^bun(?:\.exe)?$/i.test(path.basename(process.env.npm_execpath ?? "",),)
18
+ )
19
+ );
20
+ const envFileAutoloadDisabled = process.execArgv.includes("--no-env-file",)
21
+ || bunNodeCompatibilityMode;
14
22
 
15
- function flagValue(names,) {
16
- for (let i = 0; i < optionArgs.length; i++) {
17
- const arg = optionArgs[i];
18
- for (const name of names) {
19
- if (arg === name) return optionArgs[i + 1];
20
- if (arg.startsWith(`${name}=`,)) return arg.slice(name.length + 1,);
21
- }
22
- }
23
- return undefined;
24
- }
25
-
26
- function hasFlag(names,) {
27
- return names.some((name,) => optionArgs.includes(name,));
28
- }
29
-
30
- async function loadSavedTlsSettings() {
31
- try {
32
- const { loadCredentials, } = await import("../dist/src/config.js");
33
- const creds = loadCredentials();
34
- return {
35
- caCertPath: creds?.caCertPath,
36
- tlsRejectUnauthorized: creds?.tlsRejectUnauthorized,
37
- };
38
- } catch {
39
- return {};
40
- }
41
- }
42
-
43
- const savedTls = await loadSavedTlsSettings();
44
- const env = { ...process.env, };
45
- const explicitCaCert = flagValue(["--ca-cert", "--extra-ca-certs",],);
46
- if (explicitCaCert) {
47
- env.NODE_EXTRA_CA_CERTS = explicitCaCert;
48
- } else if (!env.NODE_EXTRA_CA_CERTS && savedTls.caCertPath) {
49
- env.NODE_EXTRA_CA_CERTS = savedTls.caCertPath;
50
- }
51
-
52
- if (hasFlag(["--insecure", "--skip-tls-verify",],)) {
53
- env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
54
- } else if (
55
- env.NODE_TLS_REJECT_UNAUTHORIZED === undefined && savedTls.tlsRejectUnauthorized === false
56
- ) {
57
- env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
58
- }
59
- const runningOnBun = Boolean(process.versions.bun,);
60
- const usesSourceCli = cliPath === sourceCliPath;
61
-
62
- // Published dist carries dist/build-metadata.json with the revision it was
63
- // built from. Forward it to the CLI so provenance can report dist source and
64
- // its build revision; only a full lowercase hexadecimal revision is accepted,
65
- // so an inherited or corrupt variable never stands in for packaged metadata.
66
- const distMetadataPath = path.resolve(here, "../dist/build-metadata.json",);
67
- if (!usesSourceCli) {
68
- env.DSS_LOAD_SOURCE = "dist";
69
- delete env.DSS_BUILD_REVISION;
70
- try {
71
- const metadata = JSON.parse(fs.readFileSync(distMetadataPath, "utf-8",),);
72
- const revision = metadata.buildRevision;
73
- if (typeof revision === "string" && /^[0-9a-f]{40}$/.test(revision,)) {
74
- env.DSS_BUILD_REVISION = revision;
75
- }
76
- } catch {
77
- // Dist without build metadata: provenance still reports dist source.
78
- }
79
- }
80
-
81
- const runtimeBin = runningOnBun ? process.execPath : usesSourceCli ? "bun" : process.execPath;
82
- const runtimeArgs = runningOnBun || usesSourceCli ? ["--no-env-file",] : ["--use-system-ca",];
83
- const result = spawnSync(
84
- runtimeBin,
85
- [...runtimeArgs, cliPath, ...args,],
86
- {
87
- stdio: "inherit",
88
- env,
89
- },
90
- );
91
-
92
- if (result.error) {
93
- const message = usesSourceCli
94
- ? `Unable to start Bun runtime for source dss CLI (${result.error.message}).`
95
- : `Unable to start ${
96
- runningOnBun ? "Bun" : "Node"
97
- } runtime for packaged dss CLI (${result.error.message}); falling back to the current runtime.`;
98
- if (usesSourceCli) {
23
+ if (!runningUnderBun) {
24
+ const result = spawnSync(
25
+ "bun",
26
+ ["--no-env-file", fileURLToPath(import.meta.url,), ...process.argv.slice(2,),],
27
+ { stdio: "inherit", env: process.env, },
28
+ );
29
+ if (result.error) {
99
30
  process.stdout.write(`${
100
31
  JSON.stringify({
101
32
  type: "error",
102
33
  ok: false,
103
- error: message,
34
+ error: "Unable to start the required Bun runtime.",
104
35
  code: "internal_error",
105
36
  category: "internal",
106
37
  exitCode: 2,
38
+ hint: "Install Bun >= 1.4.0 and ensure `bun` is on PATH.",
107
39
  },)
108
40
  }\n`,);
109
41
  process.exitCode = 2;
42
+ } else if (result.signal) {
43
+ process.kill(process.pid, result.signal,);
110
44
  } else {
111
- await import(cliUrl);
45
+ process.exitCode = result.status ?? 1;
112
46
  }
113
- } else if (result.signal) {
114
- process.kill(process.pid, result.signal,);
47
+ } else if (!envFileAutoloadDisabled) {
48
+ process.stdout.write(`${
49
+ JSON.stringify({
50
+ type: "error",
51
+ ok: false,
52
+ error: "Bun automatic .env loading must be disabled for the dss launcher.",
53
+ code: "env_autoload_enabled",
54
+ category: "usage",
55
+ exitCode: 1,
56
+ hint: "Use `bunx --bun dataiku-sdk` or pass `--no-env-file` before the script path.",
57
+ },)
58
+ }\n`,);
59
+ process.exitCode = 1;
115
60
  } else {
116
- process.exit(result.status ?? 1,);
61
+ const here = import.meta.dir;
62
+ const distCliPath = path.resolve(here, "../dist/src/cli.js",);
63
+ const sourceCliPath = path.resolve(here, "../src/cli.ts",);
64
+ const usesDistCli = await Bun.file(distCliPath,).exists();
65
+ const cliPath = usesDistCli ? distCliPath : sourceCliPath;
66
+
67
+ // Published dist carries dist/build-metadata.json with the revision it was
68
+ // built from. Hand it to the CLI so provenance can report dist source and its
69
+ // build revision; only a full lowercase hexadecimal revision is accepted, so an
70
+ // inherited or corrupt variable never stands in for packaged metadata.
71
+ if (usesDistCli) {
72
+ process.env.DSS_LOAD_SOURCE = "dist";
73
+ delete process.env.DSS_BUILD_REVISION;
74
+ try {
75
+ const metadata = await Bun.file(path.resolve(here, "../dist/build-metadata.json",),).json();
76
+ const revision = metadata.buildRevision;
77
+ if (typeof revision === "string" && /^[0-9a-f]{40}$/.test(revision,)) {
78
+ process.env.DSS_BUILD_REVISION = revision;
79
+ }
80
+ } catch {
81
+ // Dist without build metadata: provenance still reports dist source.
82
+ }
83
+ }
84
+
85
+ // Same process, same argv: the CLI sees process.argv.slice(2) exactly as before.
86
+ await import(pathToFileURL(cliPath,).href);
117
87
  }
@@ -1 +1 @@
1
- {"format":1,"buildRevision":"70487e2275a70acce6862f126a89b1bc852ebb11"}
1
+ {"format":1,"buildRevision":"894cf4823fd8a3ffb25cdaa93a09bcd222454565"}
@@ -1,4 +1,3 @@
1
- import { writeFileSync, } from "node:fs";
2
1
  import { requiredJsonInput, } from "../coerce.js";
3
2
  import { requireArgs, UsageError, } from "../usage.js";
4
3
  export const apiServiceCommands = {
@@ -100,9 +99,11 @@ export const apiServiceCommands = {
100
99
  if (!out)
101
100
  throw new UsageError("--output PATH is required.", "missing_required_flag");
102
101
  const res = await c.apiServices.downloadPackageArchive(a[0], a[1], f["project-key"]);
103
- const buf = Buffer.from(await res.arrayBuffer());
104
- writeFileSync(out, buf);
105
- return { path: out, bytes: buf.length, };
102
+ if (!res.body) {
103
+ throw new Error("apiServices.downloadPackageArchive response did not include a body");
104
+ }
105
+ const bytes = await Bun.write(out, new Response(res.body), { createPath: false, });
106
+ return { path: out, bytes, };
106
107
  },
107
108
  usage: "dss api-service download-package <serviceId> <packageId> --output PATH [--project-key KEY]",
108
109
  description: "Download an API service package archive to a local file.",
@@ -2,16 +2,17 @@ import { ClientValidationError, DataikuError, } from "../../errors.js";
2
2
  import { projectIncarnationHash, } from "../../utils/project-incarnation.js";
3
3
  import { jsonInput, num, parseBooleanOption, plainRecord, requiredJsonInput, requiredStringFlag, stringField, } from "../coerce.js";
4
4
  import { appPermissionsHash, appPermissionsVerificationError, assertAppPermissionsSnapshotBinding, buildAppPermissionsSnapshot, diffAppPermissions, readAppPermissionsSnapshot, writeAppPermissionsSnapshot, } from "../helpers/app-permissions.js";
5
- import { createSuccessorInstance, futureTargetIdentity, isDefinitiveRejection, requireAbsentProjectTarget, safeErrorSummary, verifyAppInstance, VISUAL_UI_GATE, } from "../helpers/app-successor.js";
5
+ import { createSuccessorInstance, futureTargetIdentity, isDefinitiveRejection, preflightSuccessorInstance, requireAbsentProjectTarget, safeErrorSummary, verifyAppInstance, VISUAL_UI_GATE, } from "../helpers/app-successor.js";
6
6
  import { CommandResultFailure, } from "../output.js";
7
7
  import { requireArgs, requireNoArgs, UsageError, } from "../usage.js";
8
8
  const CREATE_INSTANCE_USAGE = "dss app create-instance <appId> (--data JSON|--data-file PATH|--stdin) [--wait] [--timeout MS] [--poll-interval MS]";
9
9
  const MANIFEST_VERSION_USAGE = "dss app manifest-version [--project-key KEY]";
10
10
  const SET_MANIFEST_VERSION_USAGE = "dss app set-manifest-version (--manifest-version V|--version-notes NOTES) [--expect-hash SHA256] [--dry-run] [--project-key KEY]";
11
11
  const CREATE_SUCCESSOR_USAGE = "dss app create-successor-instance <appId> --from KEY --to KEY [--name NAME] [--copy-permissions] [--timeout MS] [--poll-interval MS] [--dry-run] [--record-cleanup PATH]";
12
+ const SUCCESSOR_PREFLIGHT_USAGE = "dss app successor-preflight <appId> --from KEY --to KEY [--name NAME] [--copy-permissions]";
12
13
  const VERIFY_INSTANCE_USAGE = "dss app verify-instance <appId> --project-key KEY [--expect-version V]";
13
14
  const DELETE_INSTANCE_USAGE = "dss app delete-instance --project-key KEY [--future-id ID] [--expect-project-incarnation SHA256] [--unconfirmed-creation] [--timeout MS] [--poll-interval MS]";
14
- const CREATE_INSTANCE_INDETERMINATE_REMEDIATION = "DSS may have accepted the creation, but no future ID proves when it settles. The recorded cleanup entry intentionally stops as unresolved; inspect `dss app instances <appId>` and retry deletion only after creation is known terminal.";
15
+ const CREATE_INSTANCE_INDETERMINATE_REMEDIATION = "DSS may have accepted the creation, but no future ID or project incarnation authorizes cleanup. Inspect `dss app instances <appId>` and DSS task history; delete only after creation is known terminal and the target identity is verified.";
15
16
  const CREATE_INSTANCE_REJECTED_REMEDIATION = "DSS refused the creation request, so no instance project was created: fix the reported problem and retry.";
16
17
  const DELETE_INSTANCE_FUTURE_REMEDIATION = "The supplied creation future could not authorize deletion. Inspect that future and the target project, then retry only with a future whose terminal result reports this project.";
17
18
  const DELETE_INSTANCE_UNCONFIRMED_REMEDIATION = "Creation may still be running and DSS returned no future ID. Inspect the app's instances and wait until creation is known terminal before running delete-instance without --unconfirmed-creation.";
@@ -116,7 +117,9 @@ export const appCommands = {
116
117
  throw new UsageError("Instance creation payload must include a non-empty targetProjectKey.", "validation_failed", `Usage: ${CREATE_INSTANCE_USAGE}`);
117
118
  }
118
119
  body.targetProjectKey = requestedProjectKey;
119
- await requireAbsentProjectTarget(c, requestedProjectKey, "targetProjectKey");
120
+ await requireAbsentProjectTarget(c, requestedProjectKey, "targetProjectKey", {
121
+ appId: a[0],
122
+ });
120
123
  let created;
121
124
  try {
122
125
  created = await c.applications.createInstance(a[0], body);
@@ -124,15 +127,19 @@ export const appCommands = {
124
127
  catch (error) {
125
128
  // A definitive rejection proves nothing was created; anything else
126
129
  // (5xx, retryable 4xx, transport failure) leaves the outcome
127
- // unknown and stays cleanup-eligible against the requested key.
130
+ // unknown. Without a returned future ID or verified incarnation,
131
+ // cleanup cannot be authorized.
128
132
  const rejected = isDefinitiveRejection(error);
129
133
  return {
130
134
  success: false,
131
- state: "CREATE_FAILED",
135
+ state: rejected ? "CREATE_FAILED" : "INDETERMINATE",
132
136
  elapsedMs: 0,
133
137
  pollCount: 0,
134
138
  projectKey: requestedProjectKey,
135
- cleanupEligible: !rejected,
139
+ outcome: rejected ? "rejected" : "indeterminate",
140
+ creationPostAttempted: true,
141
+ cleanupEligible: false,
142
+ recoveryCommands: rejected ? [] : [`dss app instances ${a[0]}`,],
136
143
  remediation: rejected
137
144
  ? CREATE_INSTANCE_REJECTED_REMEDIATION
138
145
  : CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
@@ -143,11 +150,13 @@ export const appCommands = {
143
150
  if (!instance) {
144
151
  return {
145
152
  success: false,
146
- state: "UNTRACKABLE",
153
+ state: "INDETERMINATE",
147
154
  elapsedMs: 0,
148
155
  pollCount: 0,
149
156
  projectKey: requestedProjectKey,
150
- cleanupEligible: true,
157
+ outcome: "indeterminate",
158
+ creationPostAttempted: true,
159
+ cleanupEligible: false,
151
160
  responseKind: created === undefined
152
161
  ? "empty"
153
162
  : created === null
@@ -157,6 +166,7 @@ export const appCommands = {
157
166
  : "scalar",
158
167
  error: "DSS accepted instance creation without returning a structured creation state.",
159
168
  remediation: CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
169
+ recoveryCommands: [`dss app instances ${a[0]}`,],
160
170
  };
161
171
  }
162
172
  // The create endpoint's `jobId` is a DSS future ID (the official client wraps it in
@@ -174,7 +184,7 @@ export const appCommands = {
174
184
  projectKey: requestedProjectKey,
175
185
  instance,
176
186
  ...(jobId !== undefined ? { futureId: jobId, jobId, } : {}),
177
- cleanupEligible: true,
187
+ cleanupEligible: jobId !== undefined,
178
188
  error: "DSS named a different target project in the creation response.",
179
189
  expected: { projectKey: requestedProjectKey, },
180
190
  actual: { projectKey: echoedTarget, },
@@ -251,14 +261,17 @@ export const appCommands = {
251
261
  if (!jobId) {
252
262
  return {
253
263
  success: false,
254
- state: "UNTRACKABLE",
264
+ state: "INDETERMINATE",
255
265
  elapsedMs: 0,
256
266
  pollCount: 0,
257
267
  projectKey: requestedProjectKey,
258
268
  instance,
259
- cleanupEligible: true,
260
- error: "DSS returned no instance-creation future ID. The instance may still have been created; inspect app instances or replay the recorded cleanup entry.",
269
+ outcome: "indeterminate",
270
+ creationPostAttempted: true,
271
+ cleanupEligible: false,
272
+ error: "DSS returned no instance-creation future ID. The instance may still have been created; inspect app instances before any recovery action.",
261
273
  remediation: CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
274
+ recoveryCommands: [`dss app instances ${a[0]}`,],
262
275
  };
263
276
  }
264
277
  let waited;
@@ -278,6 +291,7 @@ export const appCommands = {
278
291
  instance,
279
292
  jobId,
280
293
  cleanupEligible: true,
294
+ recoveryCommands: [`dss future wait ${jobId}`, `dss app instances ${a[0]}`,],
281
295
  remediation: CREATE_INSTANCE_INDETERMINATE_REMEDIATION,
282
296
  ...safeErrorSummary(error),
283
297
  };
@@ -343,6 +357,25 @@ export const appCommands = {
343
357
  'dss app create-instance my-app --data \'{"targetProjectKey":"NEWPROJ"}\' --wait --timeout 120000 --poll-interval 2000',
344
358
  ],
345
359
  },
360
+ "successor-preflight": {
361
+ handler: async (c, a, f) => {
362
+ requireArgs(a, 1, SUCCESSOR_PREFLIGHT_USAGE);
363
+ return preflightSuccessorInstance(c, {
364
+ appId: a[0],
365
+ from: requiredStringFlag(f, "from", SUCCESSOR_PREFLIGHT_USAGE),
366
+ to: requiredStringFlag(f, "to", SUCCESSOR_PREFLIGHT_USAGE),
367
+ ...(f["name"] !== undefined ? { name: f["name"], } : {}),
368
+ copyPermissions: parseBooleanOption(f["copy-permissions"], "--copy-permissions") ?? false,
369
+ dryRun: true,
370
+ }, SUCCESSOR_PREFLIGHT_USAGE);
371
+ },
372
+ usage: SUCCESSOR_PREFLIGHT_USAGE,
373
+ description: "Run every read-only successor gate before changing the template version: validate the template, verify the predecessor, prove target absence, and optionally snapshot the predecessor ACL.",
374
+ examples: [
375
+ "dss app successor-preflight my-app --from OLD_INSTANCE --to NEW_INSTANCE",
376
+ "dss app successor-preflight my-app --from OLD_INSTANCE --to NEW_INSTANCE --copy-permissions",
377
+ ],
378
+ },
346
379
  "create-successor-instance": {
347
380
  handler: async (c, a, f) => {
348
381
  requireArgs(a, 1, CREATE_SUCCESSOR_USAGE);
@@ -1,4 +1,3 @@
1
- import { writeFileSync, } from "node:fs";
2
1
  import { requireArgs, UsageError, } from "../usage.js";
3
2
  export const bundleCommands = {
4
3
  "list-exported": {
@@ -34,9 +33,11 @@ export const bundleCommands = {
34
33
  if (!out)
35
34
  throw new UsageError("--output PATH is required.", "missing_required_flag");
36
35
  const res = await c.bundles.downloadExportedArchive(a[0], f["project-key"]);
37
- const buf = Buffer.from(await res.arrayBuffer());
38
- writeFileSync(out, buf);
39
- return { path: out, bytes: buf.length, };
36
+ if (!res.body) {
37
+ throw new Error("bundles.downloadExportedArchive response did not include a body");
38
+ }
39
+ const bytes = await Bun.write(out, new Response(res.body), { createPath: false, });
40
+ return { path: out, bytes, };
40
41
  },
41
42
  usage: "dss bundle download-exported <bundleId> --output PATH [--project-key KEY]",
42
43
  description: "Download an exported Design-node bundle archive to a local file.",
@@ -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], };