dataiku-sdk 0.1.2 → 0.2.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.
@@ -0,0 +1,4 @@
1
+ export declare function validateCredentials(url: string, apiKey: string): Promise<{
2
+ valid: boolean;
3
+ error?: string;
4
+ }>;
@@ -0,0 +1,20 @@
1
+ import { DataikuClient, } from "./client.js";
2
+ import { DataikuError, } from "./errors.js";
3
+ export async function validateCredentials(url, apiKey) {
4
+ try {
5
+ const client = new DataikuClient({
6
+ url,
7
+ apiKey,
8
+ requestTimeoutMs: 10_000,
9
+ retryMaxAttempts: 1,
10
+ });
11
+ await client.projects.list();
12
+ return { valid: true, };
13
+ }
14
+ catch (err) {
15
+ if (err instanceof DataikuError) {
16
+ return { valid: false, error: err.message, };
17
+ }
18
+ return { valid: false, error: err instanceof Error ? err.message : String(err), };
19
+ }
20
+ }
package/dist/src/cli.js CHANGED
@@ -1,12 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, } from "node:fs";
3
+ import { writeFile, } from "node:fs/promises";
3
4
  import { dirname, resolve, } from "node:path";
5
+ import { createInterface, } from "node:readline";
6
+ import { Writable, } from "node:stream";
4
7
  import { fileURLToPath, } from "node:url";
8
+ import { validateCredentials, } from "./auth.js";
5
9
  import { DataikuClient, } from "./client.js";
10
+ import { deleteCredentials, getCredentialsPath, loadCredentials, maskApiKey, saveCredentials, } from "./config.js";
6
11
  import { DataikuError, } from "./errors.js";
12
+ import { AGENTS, detectAgents, installSkill, } from "./skill.js";
7
13
  // ---------------------------------------------------------------------------
8
14
  // Utility helpers
9
15
  // ---------------------------------------------------------------------------
16
+ const CLI_VERSION = (() => {
17
+ try {
18
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
19
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
20
+ }
21
+ catch {
22
+ return "unknown";
23
+ }
24
+ })();
10
25
  function num(v) {
11
26
  if (typeof v !== "string")
12
27
  return undefined;
@@ -63,9 +78,29 @@ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
63
78
  function parseOutputFormat(v) {
64
79
  if (v === undefined)
65
80
  return "json";
66
- if (v === "json" || v === "quiet" || v === "tsv")
81
+ if (v === "json" || v === "quiet" || v === "table" || v === "tsv")
67
82
  return v;
68
- throw new UsageError(`Invalid --format value: ${String(v)}. Use json, tsv, or quiet.`);
83
+ throw new UsageError(`Invalid --format value: ${String(v)}. Use json, tsv, table, or quiet.`);
84
+ }
85
+ function writeTable(items) {
86
+ if (items.length === 0)
87
+ return;
88
+ const keys = Object.keys(items[0]);
89
+ const maxWidths = keys.map((k) => {
90
+ const values = items.map((item) => String(item[k] ?? ""));
91
+ return Math.min(40, Math.max(k.length, ...values.map((v) => v.length)));
92
+ });
93
+ process.stdout.write(`${keys.map((k, i) => k.padEnd(maxWidths[i])).join(" ")}\n`);
94
+ process.stdout.write(`${maxWidths.map((w) => "-".repeat(w)).join(" ")}\n`);
95
+ for (const item of items) {
96
+ const row = keys.map((k, i) => {
97
+ const val = String(item[k] ?? "");
98
+ return (val.length > maxWidths[i]
99
+ ? `${val.slice(0, maxWidths[i] - 1)}\u2026`
100
+ : val).padEnd(maxWidths[i]);
101
+ });
102
+ process.stdout.write(`${row.join(" ")}\n`);
103
+ }
69
104
  }
70
105
  function writeCommandResult(result, format) {
71
106
  if (result === undefined || result === null) {
@@ -84,9 +119,9 @@ function writeCommandResult(result, format) {
84
119
  }
85
120
  if (format === "quiet")
86
121
  return;
87
- if (format === "tsv"
88
- && Array.isArray(result)
89
- && result.every((item) => item !== null && typeof item === "object" && !Array.isArray(item))) {
122
+ const isArrayOfObjects = Array.isArray(result)
123
+ && result.every((item) => item !== null && typeof item === "object" && !Array.isArray(item));
124
+ if (format === "tsv" && isArrayOfObjects) {
90
125
  const items = result;
91
126
  if (items.length === 0)
92
127
  return;
@@ -97,8 +132,23 @@ function writeCommandResult(result, format) {
97
132
  }
98
133
  return;
99
134
  }
135
+ if (format === "table" && isArrayOfObjects) {
136
+ writeTable(result);
137
+ return;
138
+ }
100
139
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
101
140
  }
141
+ // ---------------------------------------------------------------------------
142
+ // Arg parsing
143
+ // ---------------------------------------------------------------------------
144
+ const BOOLEAN_FLAGS = new Set(["help", "verbose", "version", "stdin", "global", "list-agents",]);
145
+ const SHORT_FLAGS = {
146
+ h: "help",
147
+ v: "verbose",
148
+ V: "version",
149
+ f: "format",
150
+ o: "output",
151
+ };
102
152
  function parseArgs(argv) {
103
153
  const positional = [];
104
154
  const flags = {};
@@ -115,16 +165,43 @@ function parseArgs(argv) {
115
165
  flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
116
166
  }
117
167
  else {
118
- const next = argv[i + 1];
119
- if (next !== undefined && !next.startsWith("--")) {
120
- flags[arg.slice(2)] = next;
121
- i++;
168
+ const flagName = arg.slice(2);
169
+ if (BOOLEAN_FLAGS.has(flagName)) {
170
+ flags[flagName] = true;
122
171
  }
123
172
  else {
124
- flags[arg.slice(2)] = true;
173
+ const next = argv[i + 1];
174
+ if (next !== undefined && !next.startsWith("-")) {
175
+ flags[flagName] = next;
176
+ i++;
177
+ }
178
+ else {
179
+ flags[flagName] = true;
180
+ }
125
181
  }
126
182
  }
127
183
  }
184
+ else if (arg.length === 2 && arg[0] === "-" && arg[1] !== "-") {
185
+ const long = SHORT_FLAGS[arg[1]];
186
+ if (long) {
187
+ if (BOOLEAN_FLAGS.has(long)) {
188
+ flags[long] = true;
189
+ }
190
+ else {
191
+ const next = argv[i + 1];
192
+ if (next !== undefined && !next.startsWith("-")) {
193
+ flags[long] = next;
194
+ i++;
195
+ }
196
+ else {
197
+ flags[long] = true;
198
+ }
199
+ }
200
+ }
201
+ else {
202
+ positional.push(arg);
203
+ }
204
+ }
128
205
  else {
129
206
  positional.push(arg);
130
207
  }
@@ -325,6 +402,33 @@ const commands = {
325
402
  },
326
403
  usage: "dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
327
404
  },
405
+ "get-payload": {
406
+ handler: async (c, a, f) => {
407
+ requireArgs(a, 1, "dss recipe get-payload <name>");
408
+ const payload = await c.recipes.getPayload(a[0], {
409
+ projectKey: f["project-key"],
410
+ });
411
+ if (typeof f["output"] === "string") {
412
+ await writeFile(f["output"], payload, "utf-8");
413
+ return f["output"];
414
+ }
415
+ return payload;
416
+ },
417
+ usage: "dss recipe get-payload <name> [--output PATH] [--project-key KEY]",
418
+ },
419
+ "set-payload": {
420
+ handler: async (c, a, f) => {
421
+ requireArgs(a, 1, "dss recipe set-payload <name> --file PATH");
422
+ const filePath = f["file"];
423
+ if (!filePath)
424
+ throw new UsageError("--file is required.");
425
+ const content = readFileSync(filePath, "utf-8");
426
+ await c.recipes.setPayload(a[0], content, {
427
+ projectKey: f["project-key"],
428
+ });
429
+ },
430
+ usage: "dss recipe set-payload <name> --file PATH [--project-key KEY]",
431
+ },
328
432
  },
329
433
  job: {
330
434
  list: {
@@ -648,21 +752,35 @@ const commands = {
648
752
  // ---------------------------------------------------------------------------
649
753
  // Help
650
754
  // ---------------------------------------------------------------------------
651
- const RESOURCE_NAMES = Object.keys(commands).sort();
755
+ const RESOURCE_NAMES = [...Object.keys(commands), "auth", "install-skill",].sort();
652
756
  function printTopLevelHelp() {
653
757
  const lines = [
654
758
  "Usage: dss <resource> <action> [args...] [--flags]",
655
759
  "",
656
760
  "Global flags:",
657
- " --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
658
- " --api-key KEY API key (env: DATAIKU_API_KEY)",
659
- " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
660
- " --format FORMAT Output format: json|tsv|quiet",
661
- " --verbose Log HTTP requests to stderr",
662
- " --help Show help",
761
+ " -h, --help Show help",
762
+ " -v, --verbose Log HTTP requests to stderr",
763
+ " -V, --version Show version",
764
+ " -f, --format FORMAT Output format: json|tsv|table|quiet",
765
+ " -o, --output PATH Write output to file (recipe get-payload)",
766
+ " --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
767
+ " --api-key KEY API key (env: DATAIKU_API_KEY)",
768
+ " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
769
+ " --timeout MS Request timeout in ms (default: 30000)",
663
770
  "",
664
771
  "Resources:",
665
772
  ...RESOURCE_NAMES.map((r) => ` ${r}`),
773
+ "",
774
+ "Quick start:",
775
+ " dss auth login Save DSS credentials",
776
+ " dss auth status Verify connection",
777
+ " dss project list List accessible projects",
778
+ " dss dataset list List datasets in default project",
779
+ " dss dataset preview <name> Preview dataset rows as CSV",
780
+ " dss recipe get-payload <name> Print recipe code to stdout",
781
+ " dss recipe download-code <name> Download recipe code to a file",
782
+ " dss job log <id> View job log output",
783
+ " dss install-skill Install agent skill for coding agents",
666
784
  ];
667
785
  process.stderr.write(`${lines.join("\n")}\n`);
668
786
  }
@@ -728,11 +846,133 @@ function loadEnvFile() {
728
846
  }
729
847
  }
730
848
  // ---------------------------------------------------------------------------
849
+ // Auth commands (run before client creation)
850
+ // ---------------------------------------------------------------------------
851
+ const AUTH_ACTIONS = {
852
+ login: {
853
+ handler: async (flags) => {
854
+ let { url, apiKey, projectKey, } = resolveCredentials(flags);
855
+ if (!url || !apiKey) {
856
+ if (!process.stdin.isTTY) {
857
+ throw new UsageError("Missing --url and/or --api-key. Provide them as flags or run interactively.");
858
+ }
859
+ if (!url)
860
+ url = await promptLine("DSS URL: ");
861
+ if (!apiKey)
862
+ apiKey = await promptSecret("API key: ");
863
+ if (!projectKey)
864
+ projectKey = (await promptLine("Project key (optional): ")) || undefined;
865
+ }
866
+ if (!url)
867
+ throw new UsageError("URL is required.");
868
+ if (!apiKey)
869
+ throw new UsageError("API key is required.");
870
+ process.stderr.write("Validating credentials... ");
871
+ const result = await validateCredentials(url, apiKey);
872
+ if (!result.valid) {
873
+ process.stderr.write(`✗ Failed\n`);
874
+ throw new DataikuError(0, "Authentication Failed", result.error ?? "Credential validation failed");
875
+ }
876
+ process.stderr.write("\u2713 Connected\n");
877
+ saveCredentials({ url, apiKey, projectKey, });
878
+ process.stderr.write(`Credentials saved to ${getCredentialsPath()}\n`);
879
+ },
880
+ usage: "dss auth login [--url URL] [--api-key KEY] [--project-key KEY]",
881
+ },
882
+ status: {
883
+ handler: async (_flags) => {
884
+ const creds = loadCredentials();
885
+ if (!creds) {
886
+ process.stderr.write("No saved credentials. Run: dss auth login\n");
887
+ return;
888
+ }
889
+ const lines = [
890
+ `URL: ${creds.url}`,
891
+ `API key: ${maskApiKey(creds.apiKey)}`,
892
+ `Project key: ${creds.projectKey ?? "(not set)"}`,
893
+ ];
894
+ for (const line of lines)
895
+ process.stderr.write(`${line}\n`);
896
+ const result = await validateCredentials(creds.url, creds.apiKey);
897
+ if (result.valid) {
898
+ process.stderr.write("Connection: \u2713 Valid\n");
899
+ }
900
+ else {
901
+ process.stderr.write(`Connection: \u2717 Failed (${result.error ?? "unknown error"})\n`);
902
+ }
903
+ process.stderr.write(`Config: ${getCredentialsPath()}\n`);
904
+ },
905
+ usage: "dss auth status",
906
+ },
907
+ logout: {
908
+ handler: async (_flags) => {
909
+ deleteCredentials();
910
+ process.stderr.write("Credentials removed.\n");
911
+ },
912
+ usage: "dss auth logout",
913
+ },
914
+ };
915
+ // ---------------------------------------------------------------------------
916
+ // Interactive prompts
917
+ // ---------------------------------------------------------------------------
918
+ function promptLine(label) {
919
+ return new Promise((res, rej) => {
920
+ const rl = createInterface({ input: process.stdin, output: process.stderr, });
921
+ rl.on("close", () => rej(new UsageError("Input closed before a value was provided.")));
922
+ rl.question(label, (answer) => {
923
+ rl.close();
924
+ res(answer.trim());
925
+ });
926
+ });
927
+ }
928
+ function promptSecret(label) {
929
+ return new Promise((res, rej) => {
930
+ const muted = new Writable({
931
+ write(_chunk, _encoding, cb) {
932
+ cb();
933
+ },
934
+ });
935
+ const rl = createInterface({ input: process.stdin, output: muted, terminal: true, });
936
+ rl.on("close", () => rej(new UsageError("Input closed before a value was provided.")));
937
+ process.stderr.write(label);
938
+ rl.question("", (answer) => {
939
+ rl.close();
940
+ process.stderr.write("\n");
941
+ res(answer.trim());
942
+ });
943
+ });
944
+ }
945
+ // ---------------------------------------------------------------------------
946
+ // Credential resolution
947
+ // ---------------------------------------------------------------------------
948
+ function resolveCredentials(flags) {
949
+ let url = flags["url"];
950
+ let apiKey = flags["api-key"];
951
+ let projectKey = flags["project-key"];
952
+ url ??= process.env.DATAIKU_URL;
953
+ apiKey ??= process.env.DATAIKU_API_KEY;
954
+ projectKey ??= process.env.DATAIKU_PROJECT_KEY;
955
+ if (!url || !apiKey) {
956
+ const saved = loadCredentials();
957
+ if (saved) {
958
+ url ??= saved.url;
959
+ apiKey ??= saved.apiKey;
960
+ projectKey ??= saved.projectKey;
961
+ }
962
+ }
963
+ return { url: url ?? "", apiKey: apiKey ?? "", projectKey, };
964
+ }
965
+ // ---------------------------------------------------------------------------
731
966
  // Main
732
967
  // ---------------------------------------------------------------------------
733
968
  async function main() {
734
969
  loadEnvFile();
735
970
  const { positional, flags, } = parseArgs(process.argv.slice(2));
971
+ // --version
972
+ if (flags["version"] === true) {
973
+ process.stdout.write(`${CLI_VERSION}\n`);
974
+ process.exit(0);
975
+ }
736
976
  // Top-level help
737
977
  if (positional.length === 0 || (positional.length === 0 && flags["help"])) {
738
978
  printTopLevelHelp();
@@ -741,13 +981,91 @@ async function main() {
741
981
  process.exit(1);
742
982
  }
743
983
  const resource = positional[0];
984
+ // Auth commands — dispatched before client creation
985
+ if (resource === "auth") {
986
+ const action = positional[1];
987
+ if (!action || flags["help"] === true) {
988
+ const lines = [
989
+ "Usage: dss auth <action> [--flags]",
990
+ "",
991
+ "Actions:",
992
+ ...Object.entries(AUTH_ACTIONS).map(([name, meta,]) => ` ${name} \u2192 ${meta.usage}`),
993
+ ];
994
+ process.stderr.write(`${lines.join("\n")}\n`);
995
+ process.exit(flags["help"] === true ? 0 : 1);
996
+ }
997
+ const authMeta = AUTH_ACTIONS[action];
998
+ if (!authMeta) {
999
+ process.stderr.write(`Unknown action: auth ${action}\nAvailable: ${Object.keys(AUTH_ACTIONS).join(", ")}\n`);
1000
+ process.exit(1);
1001
+ }
1002
+ await authMeta.handler(flags);
1003
+ return;
1004
+ }
1005
+ // install-skill — dispatched before client creation
1006
+ if (resource === "install-skill") {
1007
+ if (flags["help"] === true) {
1008
+ const lines = [
1009
+ "Usage: dss install-skill [--global] [--agent NAME] [--list-agents]",
1010
+ "",
1011
+ "Install the dataiku-dss agent skill for detected coding agents.",
1012
+ "",
1013
+ "Flags:",
1014
+ " --global Install to user-level global scope (default: project)",
1015
+ " --agent NAME Target a specific agent: claude, codex, cursor, pi, omp",
1016
+ " --list-agents Print detected agents and exit",
1017
+ ];
1018
+ process.stderr.write(`${lines.join("\n")}\n`);
1019
+ process.exit(0);
1020
+ }
1021
+ const listOnly = flags["list-agents"] === true;
1022
+ const agentFilter = typeof flags["agent"] === "string" ? flags["agent"] : undefined;
1023
+ const isGlobal = flags["global"] === true;
1024
+ // Resolve target agents
1025
+ let targets;
1026
+ if (agentFilter) {
1027
+ const def = AGENTS[agentFilter];
1028
+ if (!def) {
1029
+ throw new UsageError(`Unknown agent: ${agentFilter}. Available: ${Object.keys(AGENTS).join(", ")}`);
1030
+ }
1031
+ targets = [{ id: agentFilter, def, via: "flag", },];
1032
+ }
1033
+ else {
1034
+ targets = detectAgents();
1035
+ }
1036
+ if (listOnly) {
1037
+ if (targets.length === 0) {
1038
+ process.stderr.write("No coding agents detected.\n");
1039
+ }
1040
+ else {
1041
+ process.stderr.write("Detected agents:\n");
1042
+ for (const t of targets) {
1043
+ process.stderr.write(` ${t.id} (${t.def.name}, via ${t.via})\n`);
1044
+ }
1045
+ }
1046
+ process.exit(0);
1047
+ }
1048
+ if (targets.length === 0) {
1049
+ throw new UsageError("No coding agents detected. Install one (claude, codex, cursor, pi, omp) or use --agent NAME.");
1050
+ }
1051
+ const scope = isGlobal ? "global" : "project";
1052
+ process.stderr.write(`Installing dataiku-dss skill (${scope} scope):\n`);
1053
+ const results = installSkill(targets, { global: isGlobal, cwd: process.cwd(), });
1054
+ for (const r of results) {
1055
+ process.stderr.write(` ${r.agent} \u2192 ${r.path}\n`);
1056
+ }
1057
+ if (results.length > 0) {
1058
+ process.stderr.write(`\nDone. ${results.length} skill(s) installed.\n`);
1059
+ }
1060
+ return;
1061
+ }
744
1062
  // Unknown resource
745
1063
  if (!commands[resource]) {
746
1064
  if (flags["help"]) {
747
1065
  printTopLevelHelp();
748
1066
  process.exit(0);
749
1067
  }
750
- process.stderr.write(`Unknown resource: ${resource}\nAvailable: ${RESOURCE_NAMES.join(", ")}\n`);
1068
+ process.stderr.write(`Unknown resource: ${resource} \nAvailable: ${RESOURCE_NAMES.join(", ")} \n`);
751
1069
  process.exit(1);
752
1070
  }
753
1071
  // Resource-level help
@@ -763,7 +1081,7 @@ async function main() {
763
1081
  const actionMeta = commands[resource][action];
764
1082
  // Unknown action
765
1083
  if (!actionMeta) {
766
- process.stderr.write(`Unknown action: ${resource} ${action}\nAvailable actions for ${resource}: ${Object.keys(commands[resource]).join(", ")}\n`);
1084
+ process.stderr.write(`Unknown action: ${resource} ${action} \nAvailable actions for ${resource}: ${Object.keys(commands[resource]).join(", ")} \n`);
767
1085
  process.exit(1);
768
1086
  }
769
1087
  // Action-level help
@@ -771,22 +1089,21 @@ async function main() {
771
1089
  printActionHelp(resource, action);
772
1090
  process.exit(0);
773
1091
  }
774
- // Validate config
775
- const url = flags["url"] ?? process.env.DATAIKU_URL ?? "";
776
- const apiKey = flags["api-key"] ?? process.env.DATAIKU_API_KEY ?? "";
1092
+ // Resolve credentials: flags > env > saved > .env
1093
+ const { url, apiKey, projectKey, } = resolveCredentials(flags);
777
1094
  if (!url) {
778
- process.stderr.write(`${JSON.stringify({ error: "Missing Dataiku URL. Set DATAIKU_URL or pass --url.", }, null, 2)}\n`);
779
- process.exit(1);
1095
+ throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
780
1096
  }
781
1097
  if (!apiKey) {
782
- process.stderr.write(`${JSON.stringify({ error: "Missing API key. Set DATAIKU_API_KEY or pass --api-key.", }, null, 2)}\n`);
783
- process.exit(1);
1098
+ throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
784
1099
  }
1100
+ const requestTimeoutMs = num(flags["timeout"]) ?? undefined;
785
1101
  const client = new DataikuClient({
786
1102
  url,
787
1103
  apiKey,
788
- projectKey: flags["project-key"] ?? process.env.DATAIKU_PROJECT_KEY,
1104
+ projectKey,
789
1105
  verbose: flags["verbose"] === true,
1106
+ requestTimeoutMs,
790
1107
  });
791
1108
  const args = positional.slice(2);
792
1109
  const format = parseOutputFormat(flags["format"]);
@@ -795,7 +1112,7 @@ async function main() {
795
1112
  }
796
1113
  main().catch((err) => {
797
1114
  if (err instanceof UsageError) {
798
- process.stderr.write(`${err.message}\n`);
1115
+ process.stderr.write(`${err.message} \n`);
799
1116
  process.exit(1);
800
1117
  }
801
1118
  if (err instanceof DataikuError) {
@@ -806,10 +1123,10 @@ main().catch((err) => {
806
1123
  };
807
1124
  if (err.retryHint)
808
1125
  payload.retryHint = err.retryHint;
809
- process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
1126
+ process.stderr.write(`${JSON.stringify(payload, null, 2)} \n`);
810
1127
  process.exit(err.category === "transient" ? 3 : 2);
811
1128
  }
812
1129
  const message = err instanceof Error ? err.message : String(err);
813
- process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)}\n`);
1130
+ process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)} \n`);
814
1131
  process.exit(1);
815
1132
  });
@@ -0,0 +1,11 @@
1
+ export interface DssCredentials {
2
+ url: string;
3
+ apiKey: string;
4
+ projectKey?: string;
5
+ }
6
+ export declare function getConfigDir(): string;
7
+ export declare function getCredentialsPath(): string;
8
+ export declare function loadCredentials(): DssCredentials | null;
9
+ export declare function saveCredentials(creds: DssCredentials): void;
10
+ export declare function deleteCredentials(): void;
11
+ export declare function maskApiKey(apiKey: string): string;
@@ -0,0 +1,64 @@
1
+ import { chmodSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { homedir, } from "node:os";
3
+ import { dirname, join, resolve, } from "node:path";
4
+ export function getConfigDir() {
5
+ if (process.env.DSS_CONFIG_DIR)
6
+ return process.env.DSS_CONFIG_DIR;
7
+ if (process.env.XDG_CONFIG_HOME)
8
+ return resolve(process.env.XDG_CONFIG_HOME, "dataiku");
9
+ if (process.platform === "win32" && process.env.APPDATA) {
10
+ return resolve(process.env.APPDATA, "dataiku");
11
+ }
12
+ return resolve(homedir(), ".config", "dataiku");
13
+ }
14
+ export function getCredentialsPath() {
15
+ return join(getConfigDir(), "credentials.json");
16
+ }
17
+ export function loadCredentials() {
18
+ try {
19
+ const raw = readFileSync(getCredentialsPath(), "utf-8");
20
+ const parsed = JSON.parse(raw);
21
+ if (!parsed
22
+ || typeof parsed !== "object"
23
+ || Array.isArray(parsed)
24
+ || typeof parsed.url !== "string"
25
+ || typeof parsed.apiKey !== "string") {
26
+ return null;
27
+ }
28
+ const obj = parsed;
29
+ return {
30
+ url: obj.url,
31
+ apiKey: obj.apiKey,
32
+ projectKey: typeof obj.projectKey === "string" ? obj.projectKey : undefined,
33
+ };
34
+ }
35
+ catch (err) {
36
+ if (err.code === "ENOENT")
37
+ return null;
38
+ throw err;
39
+ }
40
+ }
41
+ export function saveCredentials(creds) {
42
+ const path = getCredentialsPath();
43
+ mkdirSync(dirname(path), { recursive: true, });
44
+ const data = { url: creds.url, apiKey: creds.apiKey, };
45
+ if (creds.projectKey)
46
+ data.projectKey = creds.projectKey;
47
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
48
+ chmodSync(path, 0o600);
49
+ }
50
+ export function deleteCredentials() {
51
+ try {
52
+ unlinkSync(getCredentialsPath());
53
+ }
54
+ catch (err) {
55
+ if (err.code === "ENOENT")
56
+ return;
57
+ throw err;
58
+ }
59
+ }
60
+ export function maskApiKey(apiKey) {
61
+ if (apiKey.length <= 12)
62
+ return "***";
63
+ return `${apiKey.slice(0, 6)}...${apiKey.slice(-6)}`;
64
+ }
@@ -1,4 +1,6 @@
1
1
  export { DataikuClient, type DataikuClientConfig, } from "./client.js";
2
+ export { validateCredentials, } from "./auth.js";
3
+ export { deleteCredentials, type DssCredentials, getConfigDir, getCredentialsPath, loadCredentials, maskApiKey, saveCredentials, } from "./config.js";
2
4
  export { DataikuError, type DataikuErrorCategory, type DataikuErrorTaxonomy, type DataikuRetryMetadata, } from "./errors.js";
3
5
  export { CodeEnvsResource, } from "./resources/code-envs.js";
4
6
  export { ConnectionsResource, } from "./resources/connections.js";
package/dist/src/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // Client
2
2
  export { DataikuClient, } from "./client.js";
3
+ // Auth & Config
4
+ export { validateCredentials, } from "./auth.js";
5
+ export { deleteCredentials, getConfigDir, getCredentialsPath, loadCredentials, maskApiKey, saveCredentials, } from "./config.js";
3
6
  // Errors
4
7
  export { DataikuError, } from "./errors.js";
5
8
  // Resources (for advanced use / extension)
@@ -23,14 +23,22 @@ export declare class RecipesResource extends BaseResource {
23
23
  */
24
24
  update(recipeName: string, data: Record<string, unknown>, projectKey?: string): Promise<void>;
25
25
  /**
26
- * Download a recipe code payload to a local file.
26
+ * Download a recipe code payload to a local file.
27
27
 
28
- * Returns the path to the written file.
29
- */
28
+ * Returns the path to the written file.
29
+ */
30
30
  downloadCode(recipeName: string, opts?: {
31
31
  outputPath?: string;
32
32
  projectKey?: string;
33
33
  }): Promise<string>;
34
+ /** Get only the code payload of a recipe as a raw string. */
35
+ getPayload(recipeName: string, opts?: {
36
+ projectKey?: string;
37
+ }): Promise<string>;
38
+ /** Replace only the code payload of a recipe. */
39
+ setPayload(recipeName: string, payload: string, opts?: {
40
+ projectKey?: string;
41
+ }): Promise<void>;
34
42
  /** Delete a recipe. */
35
43
  delete(recipeName: string, projectKey?: string): Promise<void>;
36
44
  /**
@@ -279,10 +279,10 @@ export class RecipesResource extends BaseResource {
279
279
  await this.client.put(`/public/api/projects/${enc}/recipes/${rnEnc}`, merged);
280
280
  }
281
281
  /**
282
- * Download a recipe code payload to a local file.
282
+ * Download a recipe code payload to a local file.
283
283
 
284
- * Returns the path to the written file.
285
- */
284
+ * Returns the path to the written file.
285
+ */
286
286
  async downloadCode(recipeName, opts) {
287
287
  const result = await this.get(recipeName, {
288
288
  includePayload: true,
@@ -296,6 +296,30 @@ export class RecipesResource extends BaseResource {
296
296
  await writeFile(filePath, result.payload, "utf-8");
297
297
  return filePath;
298
298
  }
299
+ /** Get only the code payload of a recipe as a raw string. */
300
+ async getPayload(recipeName, opts) {
301
+ const result = await this.get(recipeName, {
302
+ includePayload: true,
303
+ projectKey: opts?.projectKey,
304
+ });
305
+ if (!result.payload) {
306
+ throw new Error(`Recipe "${recipeName}" has no code payload.`);
307
+ }
308
+ return result.payload;
309
+ }
310
+ /** Replace only the code payload of a recipe. */
311
+ async setPayload(recipeName, payload, opts) {
312
+ const current = await this.get(recipeName, {
313
+ includePayload: true,
314
+ projectKey: opts?.projectKey,
315
+ });
316
+ const enc = this.enc(opts?.projectKey);
317
+ const rnEnc = encodeURIComponent(recipeName);
318
+ await this.client.put(`/public/api/projects/${enc}/recipes/${rnEnc}`, {
319
+ ...current,
320
+ payload,
321
+ });
322
+ }
299
323
  /** Delete a recipe. */
300
324
  async delete(recipeName, projectKey) {
301
325
  const enc = this.enc(projectKey);
@@ -0,0 +1,33 @@
1
+ export interface AgentDef {
2
+ /** Display name */
3
+ name: string;
4
+ /** CLI binary name (for `which` detection) */
5
+ binary: string;
6
+ /** Config directory relative to HOME (for fallback detection) */
7
+ configDir: string;
8
+ /** Require config dir to exist even when binary is found (disambiguates shared binary names) */
9
+ configDirRequired?: boolean;
10
+ /** Skill path relative to HOME (global install) */
11
+ globalPath: (home: string) => string;
12
+ /** Skill path relative to CWD (project install). null = not supported. */
13
+ projectPath: string | null;
14
+ /** File to write inside the skill directory */
15
+ filename: string;
16
+ /** Content generator: standard SKILL.md or Cursor MDC */
17
+ content: () => string;
18
+ }
19
+ export declare const AGENTS: Record<string, AgentDef>;
20
+ export interface DetectedAgent {
21
+ id: string;
22
+ def: AgentDef;
23
+ via: "binary" | "config-dir" | "flag";
24
+ }
25
+ export declare function detectAgents(): DetectedAgent[];
26
+ export interface InstallResult {
27
+ agent: string;
28
+ path: string;
29
+ }
30
+ export declare function installSkill(agents: DetectedAgent[], opts: {
31
+ global: boolean;
32
+ cwd: string;
33
+ }): InstallResult[];
@@ -0,0 +1,229 @@
1
+ import { execFileSync, } from "node:child_process";
2
+ import { existsSync, mkdirSync, writeFileSync, } from "node:fs";
3
+ import { homedir, } from "node:os";
4
+ import { join, } from "node:path";
5
+ const SKILL_BODY = `# Dataiku DSS CLI
6
+
7
+ The \`dss\` CLI (npm: dataiku-sdk) manages Dataiku DSS resources from the terminal.
8
+
9
+ ## When to use
10
+
11
+ - Query, create, or modify DSS projects, datasets, recipes, jobs, or scenarios.
12
+ - Build datasets or run scenarios and wait for completion.
13
+ - Download or upload recipe code, dataset data, or managed folder files.
14
+ - Run SQL queries against DSS connections.
15
+ - Inspect project flows, job logs, or dataset schemas.
16
+
17
+ ## Installation
18
+
19
+ Requires [Bun](https://bun.sh) runtime.
20
+
21
+ \`\`\`bash
22
+ bun add -g dataiku-sdk # global install \u2014 provides the \`dss\` command
23
+ \`\`\`
24
+
25
+ Or run without installing:
26
+
27
+ \`\`\`bash
28
+ bunx dataiku-sdk <command> # e.g. bunx dataiku-sdk auth login
29
+ \`\`\`
30
+
31
+ ## Authentication
32
+
33
+ \`\`\`bash
34
+ dss auth login # interactive: prompts for URL, API key, project key
35
+ dss auth login --url https://dss.example.com --api-key YOUR_KEY
36
+ dss auth status # verify connection
37
+ \`\`\`
38
+
39
+ Credentials are saved to \`~/.dss/credentials.json\`. Alternatively set environment variables:
40
+
41
+ \`\`\`bash
42
+ export DATAIKU_URL=https://dss.example.com
43
+ export DATAIKU_API_KEY=your-api-key
44
+ export DATAIKU_PROJECT_KEY=MYPROJ # optional default project
45
+ \`\`\`
46
+
47
+ ## Workflows
48
+
49
+ ### Inspect a project
50
+
51
+ \`\`\`bash
52
+ dss project list # find the project key
53
+ dss dataset list --project-key MYPROJ # list its datasets
54
+ dss dataset preview orders --max-rows 10 # peek at data
55
+ dss dataset schema orders # inspect columns
56
+ \`\`\`
57
+
58
+ ### Edit recipe code
59
+
60
+ \`\`\`bash
61
+ dss recipe download-code my-recipe -o code.py # download
62
+ # ... edit code.py ...
63
+ dss recipe diff my-recipe --file code.py # review changes
64
+ dss recipe set-payload my-recipe --file code.py # upload
65
+ \`\`\`
66
+
67
+ ### Build and monitor
68
+
69
+ \`\`\`bash
70
+ dss job build-and-wait my-dataset --include-logs # build + wait + stream logs
71
+ dss job list # recent jobs
72
+ dss job log <job-id> # full log output
73
+ \`\`\`
74
+
75
+ ### Run a scenario
76
+
77
+ \`\`\`bash
78
+ dss scenario run my-scenario
79
+ dss scenario status my-scenario # check if finished
80
+ \`\`\`
81
+
82
+ ## Command reference
83
+
84
+ \`\`\`
85
+ dss <resource> <action> [args...] [--flags]
86
+
87
+ Resources: project, dataset, recipe, job, scenario, folder, notebook,
88
+ variable, code-env, connection, sql, auth, install-skill
89
+ \`\`\`
90
+
91
+ Use \`dss <resource> --help\` to see all actions and flags for any resource.
92
+
93
+ ## Key flags
94
+
95
+ \`\`\`
96
+ -f, --format FORMAT json (default) | tsv | table | quiet
97
+ -o, --output PATH write output to file instead of stdout
98
+ -v, --verbose log HTTP requests to stderr
99
+ --project-key KEY override default project for any command
100
+ --timeout MS request timeout (default: 30000)
101
+ --stdin read JSON input from stdin
102
+ \`\`\`
103
+
104
+ ## Gotchas
105
+
106
+ - **Most commands need a project key.** Set it once via \`dss auth login\` or \`DATAIKU_PROJECT_KEY\` to avoid passing \`--project-key\` on every call.
107
+ - **Output is JSON by default.** Use \`-f table\` when showing results to a user; use \`-f tsv\` when piping to scripts.
108
+ - **\`dss job build\` returns immediately.** Use \`dss job build-and-wait\` to block until the build finishes. Add \`--include-logs\` to stream log output.
109
+ - **Folder commands accept names or IDs.** If a folder name contains spaces, quote it. The CLI resolves names to IDs automatically.
110
+ - **Recipe set-payload overwrites the entire payload.** Always download first, edit, diff, then upload.
111
+ - **Transient errors exit code 3, API errors exit code 2, usage errors exit code 1.** Check exit codes to distinguish retriable failures.
112
+ `;
113
+ const SKILL_FRONTMATTER = `---
114
+ name: dataiku-dss
115
+ description: >-
116
+ Interact with Dataiku DSS from the command line \u2014 list projects, query datasets,
117
+ download and upload recipe code, build datasets, run scenarios, and manage jobs.
118
+ Use when the user wants to work with Dataiku DSS resources, inspect a DSS project,
119
+ modify recipes, trigger builds, check job logs, or run SQL against DSS connections,
120
+ even if they don't explicitly mention the dss CLI.
121
+ ---
122
+
123
+ `;
124
+ function skillContent() {
125
+ return SKILL_FRONTMATTER + SKILL_BODY;
126
+ }
127
+ export const AGENTS = {
128
+ claude: {
129
+ name: "Claude Code",
130
+ binary: "claude",
131
+ configDir: ".claude",
132
+ globalPath: (home) => join(home, ".claude", "skills", "dataiku-dss"),
133
+ projectPath: ".claude/skills/dataiku-dss",
134
+ filename: "SKILL.md",
135
+ content: skillContent,
136
+ },
137
+ codex: {
138
+ name: "Codex",
139
+ binary: "codex",
140
+ configDir: ".codex",
141
+ globalPath: (home) => join(home, ".codex", "skills", "dataiku-dss"),
142
+ projectPath: ".codex/skills/dataiku-dss",
143
+ filename: "SKILL.md",
144
+ content: skillContent,
145
+ },
146
+ cursor: {
147
+ name: "Cursor",
148
+ binary: "cursor",
149
+ configDir: ".cursor",
150
+ globalPath: (home) => join(home, ".cursor", "skills", "dataiku-dss"),
151
+ projectPath: ".cursor/skills/dataiku-dss",
152
+ filename: "SKILL.md",
153
+ content: skillContent,
154
+ },
155
+ pi: {
156
+ name: "Pi",
157
+ binary: "pi",
158
+ configDir: ".pi",
159
+ globalPath: (home) => join(home, ".pi", "agent", "skills", "dataiku-dss"),
160
+ projectPath: ".pi/skills/dataiku-dss",
161
+ filename: "SKILL.md",
162
+ content: skillContent,
163
+ },
164
+ omp: {
165
+ name: "OhMyPi",
166
+ binary: "omp",
167
+ configDir: join(".omp", "agent"),
168
+ configDirRequired: true,
169
+ globalPath: (home) => join(home, ".omp", "agent", "skills", "dataiku-dss"),
170
+ projectPath: ".omp/skills/dataiku-dss",
171
+ filename: "SKILL.md",
172
+ content: skillContent,
173
+ },
174
+ };
175
+ // ---------------------------------------------------------------------------
176
+ // Agent detection
177
+ // ---------------------------------------------------------------------------
178
+ function binaryExists(name) {
179
+ const cmd = process.platform === "win32" ? "where" : "which";
180
+ try {
181
+ execFileSync(cmd, [name,], { stdio: "pipe", });
182
+ return true;
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ }
188
+ export function detectAgents() {
189
+ const home = homedir();
190
+ const found = [];
191
+ for (const [id, def,] of Object.entries(AGENTS)) {
192
+ const hasBinary = binaryExists(def.binary);
193
+ const hasConfigDir = existsSync(join(home, def.configDir));
194
+ if (hasBinary && (!def.configDirRequired || hasConfigDir)) {
195
+ found.push({ id, def, via: "binary", });
196
+ }
197
+ else if (hasConfigDir) {
198
+ found.push({ id, def, via: "config-dir", });
199
+ }
200
+ }
201
+ return found;
202
+ }
203
+ export function installSkill(agents, opts) {
204
+ const home = homedir();
205
+ const results = [];
206
+ for (const { id, def, } of agents) {
207
+ let dir;
208
+ if (opts.global) {
209
+ const globalDir = def.globalPath(home);
210
+ if (!globalDir) {
211
+ process.stderr.write(` ${def.name}: skipped (no global path available)\n`);
212
+ continue;
213
+ }
214
+ dir = globalDir;
215
+ }
216
+ else {
217
+ if (!def.projectPath) {
218
+ process.stderr.write(` ${def.name}: skipped (no project path available)\n`);
219
+ continue;
220
+ }
221
+ dir = join(opts.cwd, def.projectPath);
222
+ }
223
+ mkdirSync(dir, { recursive: true, });
224
+ const filePath = join(dir, def.filename);
225
+ writeFileSync(filePath, def.content(), "utf-8");
226
+ results.push({ agent: id, path: filePath, });
227
+ }
228
+ return results;
229
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dataiku-sdk",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Dataiku DSS SDK and CLI for programmatic access to DSS REST APIs",
5
5
  "type": "module",
6
6
  "workspaces": [