dataiku-sdk 0.1.1 → 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.
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;
@@ -18,6 +33,122 @@ function json(v) {
18
33
  return undefined;
19
34
  return JSON.parse(v);
20
35
  }
36
+ function jsonInput(flags) {
37
+ if (flags["stdin"] === true) {
38
+ return JSON.parse(readFileSync(0, "utf-8"));
39
+ }
40
+ if (typeof flags["data-file"] === "string") {
41
+ return JSON.parse(readFileSync(flags["data-file"], "utf-8"));
42
+ }
43
+ if (typeof flags["data"] === "string") {
44
+ return JSON.parse(flags["data"]);
45
+ }
46
+ return undefined;
47
+ }
48
+ async function resolveFolderId(client, nameOrId, flags) {
49
+ return client.folders.resolveId(nameOrId, flags["project-key"]);
50
+ }
51
+ function formatLineDiff(remoteName, localPath, remoteContent, localContent) {
52
+ if (localContent === remoteContent) {
53
+ return "No differences.";
54
+ }
55
+ const localLines = localContent.split("\n");
56
+ const remoteLines = remoteContent.split("\n");
57
+ const lines = [`--- remote:${remoteName}`, `+++ local:${localPath}`, "",];
58
+ const maxLen = Math.max(localLines.length, remoteLines.length);
59
+ for (let i = 0; i < maxLen; i++) {
60
+ const remoteLine = remoteLines[i];
61
+ const localLine = localLines[i];
62
+ if (remoteLine === localLine)
63
+ continue;
64
+ if (remoteLine !== undefined && localLine !== undefined) {
65
+ lines.push(`@@ line ${String(i + 1)} @@`);
66
+ lines.push(`- ${remoteLine}`);
67
+ lines.push(`+ ${localLine}`);
68
+ continue;
69
+ }
70
+ if (remoteLine !== undefined) {
71
+ lines.push(`- ${remoteLine}`);
72
+ continue;
73
+ }
74
+ lines.push(`+ ${localLine}`);
75
+ }
76
+ return lines.join("\n");
77
+ }
78
+ function parseOutputFormat(v) {
79
+ if (v === undefined)
80
+ return "json";
81
+ if (v === "json" || v === "quiet" || v === "table" || v === "tsv")
82
+ return v;
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
+ }
104
+ }
105
+ function writeCommandResult(result, format) {
106
+ if (result === undefined || result === null) {
107
+ if (format !== "quiet") {
108
+ process.stdout.write(`${JSON.stringify({ ok: true, }, null, 2)}\n`);
109
+ }
110
+ return;
111
+ }
112
+ if (typeof result === "string") {
113
+ if (format !== "quiet") {
114
+ process.stdout.write(result);
115
+ if (!result.endsWith("\n"))
116
+ process.stdout.write("\n");
117
+ }
118
+ return;
119
+ }
120
+ if (format === "quiet")
121
+ return;
122
+ const isArrayOfObjects = Array.isArray(result)
123
+ && result.every((item) => item !== null && typeof item === "object" && !Array.isArray(item));
124
+ if (format === "tsv" && isArrayOfObjects) {
125
+ const items = result;
126
+ if (items.length === 0)
127
+ return;
128
+ const keys = Object.keys(items[0]);
129
+ process.stdout.write(`${keys.join("\t")}\n`);
130
+ for (const item of items) {
131
+ process.stdout.write(`${keys.map((key) => String(item[key] ?? "")).join("\t")}\n`);
132
+ }
133
+ return;
134
+ }
135
+ if (format === "table" && isArrayOfObjects) {
136
+ writeTable(result);
137
+ return;
138
+ }
139
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
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
+ };
21
152
  function parseArgs(argv) {
22
153
  const positional = [];
23
154
  const flags = {};
@@ -34,16 +165,43 @@ function parseArgs(argv) {
34
165
  flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
35
166
  }
36
167
  else {
37
- const next = argv[i + 1];
38
- if (next !== undefined && !next.startsWith("--")) {
39
- flags[arg.slice(2)] = next;
40
- i++;
168
+ const flagName = arg.slice(2);
169
+ if (BOOLEAN_FLAGS.has(flagName)) {
170
+ flags[flagName] = true;
41
171
  }
42
172
  else {
43
- 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
+ }
44
181
  }
45
182
  }
46
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
+ }
47
205
  else {
48
206
  positional.push(arg);
49
207
  }
@@ -142,14 +300,14 @@ const commands = {
142
300
  },
143
301
  update: {
144
302
  handler: (c, a, f) => {
145
- requireArgs(a, 1, "dss dataset update <name> --data '{...}'");
146
- const data = json(f["data"]);
303
+ requireArgs(a, 1, "dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin]");
304
+ const data = jsonInput(f);
147
305
  if (!data) {
148
- throw new UsageError("--data is required. Usage: dss dataset update <name> --data '{...}'");
306
+ throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin]");
149
307
  }
150
308
  return c.datasets.update(a[0], data, f["project-key"]);
151
309
  },
152
- usage: "dss dataset update <name> --data '{...}' [--project-key KEY]",
310
+ usage: "dss dataset update <name> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
153
311
  },
154
312
  },
155
313
  recipe: {
@@ -178,37 +336,98 @@ const commands = {
178
336
  requireArgs(a, 1, "dss recipe download <name>");
179
337
  return c.recipes.download(a[0], {
180
338
  outputPath: f["output"],
339
+ projectKey: f["project-key"],
181
340
  });
182
341
  },
183
- usage: "dss recipe download <name> [--output PATH]",
342
+ usage: "dss recipe download <name> [--output PATH] [--project-key KEY]",
343
+ },
344
+ "download-code": {
345
+ handler: (c, a, f) => {
346
+ requireArgs(a, 1, "dss recipe download-code <name>");
347
+ return c.recipes.downloadCode(a[0], {
348
+ outputPath: f["output"],
349
+ projectKey: f["project-key"],
350
+ });
351
+ },
352
+ usage: "dss recipe download-code <name> [--output PATH] [--project-key KEY]",
184
353
  },
185
354
  create: {
186
355
  handler: (c, _a, f) => {
187
356
  const type = f["type"];
188
357
  if (!type) {
189
- throw new UsageError("--type is required. Usage: dss recipe create --type TYPE --input DS");
358
+ throw new UsageError("--type is required. Usage: dss recipe create --type TYPE --input DS --output DS");
359
+ }
360
+ const outputDataset = f["output"];
361
+ if (!outputDataset) {
362
+ throw new UsageError("--output is required. Usage: dss recipe create --type TYPE --input DS --output DS");
190
363
  }
191
364
  return c.recipes.create({
192
365
  type,
193
366
  name: f["name"],
194
367
  inputDatasets: f["input"] ? [f["input"],] : undefined,
195
- outputDataset: f["output"],
368
+ outputDataset,
196
369
  outputConnection: f["output-connection"],
197
370
  projectKey: f["project-key"],
198
371
  });
199
372
  },
200
- usage: "dss recipe create --type TYPE --input DS [--output DS] [--output-connection CONN] [--project-key KEY]",
373
+ usage: "dss recipe create --type TYPE --input DS --output DS [--output-connection CONN] [--project-key KEY]",
374
+ },
375
+ diff: {
376
+ handler: async (c, a, f) => {
377
+ requireArgs(a, 1, "dss recipe diff <name> --file PATH");
378
+ const filePath = f["file"];
379
+ if (!filePath) {
380
+ throw new UsageError("--file is required. Usage: dss recipe diff <name> --file PATH");
381
+ }
382
+ const result = await c.recipes.get(a[0], {
383
+ includePayload: true,
384
+ projectKey: f["project-key"],
385
+ });
386
+ if (!result.payload) {
387
+ throw new Error(`Recipe "${a[0]}" has no code payload to diff.`);
388
+ }
389
+ const localContent = readFileSync(filePath, "utf-8");
390
+ return formatLineDiff(a[0], filePath, result.payload, localContent);
391
+ },
392
+ usage: "dss recipe diff <name> --file PATH [--project-key KEY]",
201
393
  },
202
394
  update: {
203
395
  handler: (c, a, f) => {
204
- requireArgs(a, 1, "dss recipe update <name> --data '{...}'");
205
- const data = json(f["data"]);
396
+ requireArgs(a, 1, "dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
397
+ const data = jsonInput(f);
206
398
  if (!data) {
207
- throw new UsageError("--data is required. Usage: dss recipe update <name> --data '{...}'");
399
+ throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin]");
208
400
  }
209
401
  return c.recipes.update(a[0], data, f["project-key"]);
210
402
  },
211
- usage: "dss recipe update <name> --data '{...}' [--project-key KEY]",
403
+ usage: "dss recipe update <name> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
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]",
212
431
  },
213
432
  },
214
433
  job: {
@@ -316,14 +535,14 @@ const commands = {
316
535
  },
317
536
  update: {
318
537
  handler: (c, a, f) => {
319
- requireArgs(a, 1, "dss scenario update <id> --data '{...}'");
320
- const data = json(f["data"]);
538
+ requireArgs(a, 1, "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
539
+ const data = jsonInput(f);
321
540
  if (!data) {
322
- throw new UsageError("--data is required. Usage: dss scenario update <id> --data '{...}'");
541
+ throw new UsageError("--data, --data-file, or --stdin is required. Usage: dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin]");
323
542
  }
324
543
  return c.scenarios.update(a[0], data, f["project-key"]);
325
544
  },
326
- usage: "dss scenario update <id> --data '{...}' [--project-key KEY]",
545
+ usage: "dss scenario update <id> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
327
546
  },
328
547
  },
329
548
  folder: {
@@ -332,41 +551,44 @@ const commands = {
332
551
  usage: "dss folder list [--project-key KEY]",
333
552
  },
334
553
  get: {
335
- handler: (c, a, f) => {
336
- requireArgs(a, 1, "dss folder get <id>");
337
- return c.folders.get(a[0], f["project-key"]);
554
+ handler: async (c, a, f) => {
555
+ requireArgs(a, 1, "dss folder get <name-or-id>");
556
+ return c.folders.get(await resolveFolderId(c, a[0], f), f["project-key"]);
338
557
  },
339
- usage: "dss folder get <id> [--project-key KEY]",
558
+ usage: "dss folder get <name-or-id> [--project-key KEY]",
340
559
  },
341
560
  contents: {
342
- handler: (c, a, _f) => {
343
- requireArgs(a, 1, "dss folder contents <id>");
344
- return c.folders.contents(a[0]);
561
+ handler: async (c, a, f) => {
562
+ requireArgs(a, 1, "dss folder contents <name-or-id>");
563
+ return c.folders.contents(await resolveFolderId(c, a[0], f), {
564
+ projectKey: f["project-key"],
565
+ });
345
566
  },
346
- usage: "dss folder contents <id>",
567
+ usage: "dss folder contents <name-or-id> [--project-key KEY]",
347
568
  },
348
569
  download: {
349
- handler: (c, a, f) => {
350
- requireArgs(a, 2, "dss folder download <id> <path>");
351
- return c.folders.download(a[0], a[1], {
570
+ handler: async (c, a, f) => {
571
+ requireArgs(a, 2, "dss folder download <name-or-id> <path>");
572
+ return c.folders.download(await resolveFolderId(c, a[0], f), a[1], {
352
573
  localPath: f["output"],
574
+ projectKey: f["project-key"],
353
575
  });
354
576
  },
355
- usage: "dss folder download <id> <path> [--output PATH]",
577
+ usage: "dss folder download <name-or-id> <path> [--output PATH] [--project-key KEY]",
356
578
  },
357
579
  upload: {
358
- handler: (c, a, f) => {
359
- requireArgs(a, 3, "dss folder upload <id> <path> <localPath>");
360
- return c.folders.upload(a[0], a[1], a[2], f["project-key"]);
580
+ handler: async (c, a, f) => {
581
+ requireArgs(a, 3, "dss folder upload <name-or-id> <path> <localPath>");
582
+ return c.folders.upload(await resolveFolderId(c, a[0], f), a[1], a[2], f["project-key"]);
361
583
  },
362
- usage: "dss folder upload <id> <path> <localPath> [--project-key KEY]",
584
+ usage: "dss folder upload <name-or-id> <path> <localPath> [--project-key KEY]",
363
585
  },
364
586
  "delete-file": {
365
- handler: (c, a, f) => {
366
- requireArgs(a, 2, "dss folder delete-file <id> <path>");
367
- return c.folders.deleteFile(a[0], a[1], f["project-key"]);
587
+ handler: async (c, a, f) => {
588
+ requireArgs(a, 2, "dss folder delete-file <name-or-id> <path>");
589
+ return c.folders.deleteFile(await resolveFolderId(c, a[0], f), a[1], f["project-key"]);
368
590
  },
369
- usage: "dss folder delete-file <id> <path> [--project-key KEY]",
591
+ usage: "dss folder delete-file <name-or-id> <path> [--project-key KEY]",
370
592
  },
371
593
  },
372
594
  variable: {
@@ -378,8 +600,10 @@ const commands = {
378
600
  handler: (c, _a, f) => c.variables.set({
379
601
  standard: json(f["standard"]),
380
602
  local: json(f["local"]),
603
+ replace: f["replace"] === true,
604
+ projectKey: f["project-key"],
381
605
  }),
382
- usage: 'dss variable set --standard \'{"k":"v"}\' --local \'{"k":"v"}\'',
606
+ usage: 'dss variable set --standard \'{"k":"v"}\' --local \'{"k":"v"}\' [--replace] [--project-key KEY]',
383
607
  },
384
608
  },
385
609
  connection: {
@@ -492,23 +716,25 @@ const commands = {
492
716
  },
493
717
  "save-jupyter": {
494
718
  handler: (c, a, f) => {
495
- requireArgs(a, 1, "dss notebook save-jupyter <name> --data '{...}'");
496
- const data = json(f["data"]);
497
- if (!data)
498
- throw new UsageError("--data is required (notebook JSON content)");
719
+ requireArgs(a, 1, "dss notebook save-jupyter <name> [--data '{...}' | --data-file PATH | --stdin]");
720
+ const data = jsonInput(f);
721
+ if (!data) {
722
+ throw new UsageError("--data, --data-file, or --stdin is required (notebook JSON content).");
723
+ }
499
724
  return c.notebooks.saveJupyter(a[0], data, f["project-key"]);
500
725
  },
501
- usage: "dss notebook save-jupyter <name> --data '{...}' [--project-key KEY]",
726
+ usage: "dss notebook save-jupyter <name> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
502
727
  },
503
728
  "save-sql": {
504
729
  handler: (c, a, f) => {
505
- requireArgs(a, 1, "dss notebook save-sql <id> --data '{...}'");
506
- const data = json(f["data"]);
507
- if (!data)
508
- throw new UsageError("--data is required (SQL notebook content JSON)");
730
+ requireArgs(a, 1, "dss notebook save-sql <id> [--data '{...}' | --data-file PATH | --stdin]");
731
+ const data = jsonInput(f);
732
+ if (!data) {
733
+ throw new UsageError("--data, --data-file, or --stdin is required (SQL notebook content JSON).");
734
+ }
509
735
  return c.notebooks.saveSql(a[0], data, f["project-key"]);
510
736
  },
511
- usage: "dss notebook save-sql <id> --data '{...}' [--project-key KEY]",
737
+ usage: "dss notebook save-sql <id> [--data '{...}' | --data-file PATH | --stdin] [--project-key KEY]",
512
738
  },
513
739
  "clear-sql-history": {
514
740
  handler: (c, a, f) => {
@@ -526,19 +752,35 @@ const commands = {
526
752
  // ---------------------------------------------------------------------------
527
753
  // Help
528
754
  // ---------------------------------------------------------------------------
529
- const RESOURCE_NAMES = Object.keys(commands).sort();
755
+ const RESOURCE_NAMES = [...Object.keys(commands), "auth", "install-skill",].sort();
530
756
  function printTopLevelHelp() {
531
757
  const lines = [
532
758
  "Usage: dss <resource> <action> [args...] [--flags]",
533
759
  "",
534
760
  "Global flags:",
535
- " --url URL Dataiku DSS base URL (env: DATAIKU_URL)",
536
- " --api-key KEY API key (env: DATAIKU_API_KEY)",
537
- " --project-key KEY Default project key (env: DATAIKU_PROJECT_KEY)",
538
- " --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)",
539
770
  "",
540
771
  "Resources:",
541
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",
542
784
  ];
543
785
  process.stderr.write(`${lines.join("\n")}\n`);
544
786
  }
@@ -604,11 +846,133 @@ function loadEnvFile() {
604
846
  }
605
847
  }
606
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
+ // ---------------------------------------------------------------------------
607
966
  // Main
608
967
  // ---------------------------------------------------------------------------
609
968
  async function main() {
610
969
  loadEnvFile();
611
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
+ }
612
976
  // Top-level help
613
977
  if (positional.length === 0 || (positional.length === 0 && flags["help"])) {
614
978
  printTopLevelHelp();
@@ -617,13 +981,91 @@ async function main() {
617
981
  process.exit(1);
618
982
  }
619
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
+ }
620
1062
  // Unknown resource
621
1063
  if (!commands[resource]) {
622
1064
  if (flags["help"]) {
623
1065
  printTopLevelHelp();
624
1066
  process.exit(0);
625
1067
  }
626
- process.stderr.write(`Unknown resource: ${resource}\nAvailable: ${RESOURCE_NAMES.join(", ")}\n`);
1068
+ process.stderr.write(`Unknown resource: ${resource} \nAvailable: ${RESOURCE_NAMES.join(", ")} \n`);
627
1069
  process.exit(1);
628
1070
  }
629
1071
  // Resource-level help
@@ -639,7 +1081,7 @@ async function main() {
639
1081
  const actionMeta = commands[resource][action];
640
1082
  // Unknown action
641
1083
  if (!actionMeta) {
642
- 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`);
643
1085
  process.exit(1);
644
1086
  }
645
1087
  // Action-level help
@@ -647,29 +1089,30 @@ async function main() {
647
1089
  printActionHelp(resource, action);
648
1090
  process.exit(0);
649
1091
  }
650
- // Validate config
651
- const url = flags["url"] ?? process.env.DATAIKU_URL ?? "";
652
- const apiKey = flags["api-key"] ?? process.env.DATAIKU_API_KEY ?? "";
1092
+ // Resolve credentials: flags > env > saved > .env
1093
+ const { url, apiKey, projectKey, } = resolveCredentials(flags);
653
1094
  if (!url) {
654
- process.stderr.write(`${JSON.stringify({ error: "Missing Dataiku URL. Set DATAIKU_URL or pass --url.", }, null, 2)}\n`);
655
- process.exit(1);
1095
+ throw new UsageError("Missing Dataiku URL. Set DATAIKU_URL, pass --url, or run: dss auth login");
656
1096
  }
657
1097
  if (!apiKey) {
658
- process.stderr.write(`${JSON.stringify({ error: "Missing API key. Set DATAIKU_API_KEY or pass --api-key.", }, null, 2)}\n`);
659
- process.exit(1);
1098
+ throw new UsageError("Missing API key. Set DATAIKU_API_KEY, pass --api-key, or run: dss auth login");
660
1099
  }
1100
+ const requestTimeoutMs = num(flags["timeout"]) ?? undefined;
661
1101
  const client = new DataikuClient({
662
1102
  url,
663
1103
  apiKey,
664
- projectKey: flags["project-key"] ?? process.env.DATAIKU_PROJECT_KEY,
1104
+ projectKey,
1105
+ verbose: flags["verbose"] === true,
1106
+ requestTimeoutMs,
665
1107
  });
666
1108
  const args = positional.slice(2);
1109
+ const format = parseOutputFormat(flags["format"]);
667
1110
  const result = await actionMeta.handler(client, args, flags);
668
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1111
+ writeCommandResult(result, format);
669
1112
  }
670
1113
  main().catch((err) => {
671
1114
  if (err instanceof UsageError) {
672
- process.stderr.write(`${err.message}\n`);
1115
+ process.stderr.write(`${err.message} \n`);
673
1116
  process.exit(1);
674
1117
  }
675
1118
  if (err instanceof DataikuError) {
@@ -680,10 +1123,10 @@ main().catch((err) => {
680
1123
  };
681
1124
  if (err.retryHint)
682
1125
  payload.retryHint = err.retryHint;
683
- process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
684
- process.exit(1);
1126
+ process.stderr.write(`${JSON.stringify(payload, null, 2)} \n`);
1127
+ process.exit(err.category === "transient" ? 3 : 2);
685
1128
  }
686
1129
  const message = err instanceof Error ? err.message : String(err);
687
- process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)}\n`);
1130
+ process.stderr.write(`${JSON.stringify({ error: message, }, null, 2)} \n`);
688
1131
  process.exit(1);
689
1132
  });