dataiku-sdk 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +4 -4
  2. package/bin/dss.js +28 -2
  3. package/dist/build-metadata.json +1 -1
  4. package/dist/src/cli/coerce.d.ts +0 -1
  5. package/dist/src/cli/coerce.js +0 -5
  6. package/dist/src/cli/command-syntax.json +14524 -0
  7. package/dist/src/cli/commands/analysis.js +6 -9
  8. package/dist/src/cli/commands/api-deployer.js +15 -33
  9. package/dist/src/cli/commands/api-service.js +17 -27
  10. package/dist/src/cli/commands/app.js +25 -41
  11. package/dist/src/cli/commands/bundle.js +15 -24
  12. package/dist/src/cli/commands/business-app.js +10 -17
  13. package/dist/src/cli/commands/code-env.js +23 -36
  14. package/dist/src/cli/commands/code.js +4 -4
  15. package/dist/src/cli/commands/connection.js +12 -22
  16. package/dist/src/cli/commands/continuous-activity.js +6 -9
  17. package/dist/src/cli/commands/dashboard.js +8 -13
  18. package/dist/src/cli/commands/data-collection.js +9 -16
  19. package/dist/src/cli/commands/data-quality.js +14 -26
  20. package/dist/src/cli/commands/dataset.js +29 -54
  21. package/dist/src/cli/commands/discussion.js +7 -10
  22. package/dist/src/cli/commands/doctor.js +3 -3
  23. package/dist/src/cli/commands/flow-zone.js +8 -17
  24. package/dist/src/cli/commands/folder.js +12 -20
  25. package/dist/src/cli/commands/future.js +7 -10
  26. package/dist/src/cli/commands/group.js +7 -11
  27. package/dist/src/cli/commands/insight.js +7 -11
  28. package/dist/src/cli/commands/job.js +17 -26
  29. package/dist/src/cli/commands/knowledge-bank.js +5 -6
  30. package/dist/src/cli/commands/llm.js +5 -8
  31. package/dist/src/cli/commands/macro.js +9 -16
  32. package/dist/src/cli/commands/meaning.js +7 -11
  33. package/dist/src/cli/commands/metrics.js +7 -10
  34. package/dist/src/cli/commands/ml-task.js +12 -20
  35. package/dist/src/cli/commands/model-evaluation-store.js +7 -11
  36. package/dist/src/cli/commands/notebook.js +15 -27
  37. package/dist/src/cli/commands/plugin.js +35 -68
  38. package/dist/src/cli/commands/project-deployer.js +10 -22
  39. package/dist/src/cli/commands/project-folder.js +10 -17
  40. package/dist/src/cli/commands/project-git.js +38 -70
  41. package/dist/src/cli/commands/project-library.js +15 -23
  42. package/dist/src/cli/commands/project.js +9 -26
  43. package/dist/src/cli/commands/recipe.d.ts +16 -0
  44. package/dist/src/cli/commands/recipe.js +83 -94
  45. package/dist/src/cli/commands/saved-model.js +23 -39
  46. package/dist/src/cli/commands/scenario.js +20 -34
  47. package/dist/src/cli/commands/sql.js +4 -4
  48. package/dist/src/cli/commands/statistics.js +11 -18
  49. package/dist/src/cli/commands/streaming-endpoint.js +7 -11
  50. package/dist/src/cli/commands/user.js +11 -22
  51. package/dist/src/cli/commands/variable.js +3 -4
  52. package/dist/src/cli/commands/webapp.js +10 -16
  53. package/dist/src/cli/commands/wiki.js +9 -13
  54. package/dist/src/cli/commands/workspace.js +8 -14
  55. package/dist/src/cli/contract.d.ts +5 -15
  56. package/dist/src/cli/contract.js +157 -254
  57. package/dist/src/cli/doctor.js +1 -0
  58. package/dist/src/cli/env.d.ts +2 -0
  59. package/dist/src/cli/env.js +16 -1
  60. package/dist/src/cli/flags.d.ts +1 -0
  61. package/dist/src/cli/flags.js +4 -9
  62. package/dist/src/cli/helpers/app-permissions.js +7 -5
  63. package/dist/src/cli/helpers/flow-zone.js +8 -6
  64. package/dist/src/cli/helpers/recipe.js +2 -1
  65. package/dist/src/cli/syntax.d.ts +83 -0
  66. package/dist/src/cli/syntax.js +217 -0
  67. package/dist/src/cli/types.d.ts +3 -0
  68. package/dist/src/cli/usage.d.ts +1 -1
  69. package/dist/src/cli/usage.js +4 -2
  70. package/dist/src/cli/version.d.ts +0 -2
  71. package/dist/src/cli/version.js +6 -7
  72. package/dist/src/cli.js +132 -274
  73. package/dist/src/client.d.ts +2 -1
  74. package/dist/src/client.js +32 -17
  75. package/dist/src/errors.d.ts +12 -1
  76. package/dist/src/errors.js +28 -3
  77. package/dist/src/index.d.ts +1 -3
  78. package/dist/src/index.js +2 -2
  79. package/dist/src/resources/api-services.js +2 -3
  80. package/dist/src/resources/applications.js +2 -4
  81. package/dist/src/resources/base.d.ts +2 -0
  82. package/dist/src/resources/base.js +14 -2
  83. package/dist/src/resources/code-envs.js +3 -5
  84. package/dist/src/resources/data-quality.js +14 -13
  85. package/dist/src/resources/datasets.d.ts +13 -0
  86. package/dist/src/resources/datasets.js +5 -4
  87. package/dist/src/resources/flow-zones.js +4 -2
  88. package/dist/src/resources/folders.js +4 -2
  89. package/dist/src/resources/futures.js +1 -3
  90. package/dist/src/resources/insights.js +3 -2
  91. package/dist/src/resources/jobs.js +4 -6
  92. package/dist/src/resources/macros.d.ts +1 -1
  93. package/dist/src/resources/macros.js +3 -7
  94. package/dist/src/resources/notebooks.js +2 -3
  95. package/dist/src/resources/project-git.js +9 -11
  96. package/dist/src/resources/project-library.d.ts +0 -2
  97. package/dist/src/resources/project-library.js +2 -7
  98. package/dist/src/resources/projects.js +4 -3
  99. package/dist/src/resources/recipes.d.ts +28 -0
  100. package/dist/src/resources/recipes.js +117 -89
  101. package/dist/src/resources/saved-models.js +1 -13
  102. package/dist/src/resources/scenarios.js +1 -3
  103. package/dist/src/resources/sql.js +9 -8
  104. package/dist/src/resources/statistics.js +3 -4
  105. package/dist/src/resources/variables.js +2 -1
  106. package/dist/src/resources/webapps.js +2 -3
  107. package/dist/src/schemas.d.ts +1 -2
  108. package/dist/src/schemas.js +1 -1
  109. package/dist/src/skill.js +6 -7
  110. package/dist/src/utils/cleanup-ledger.js +9 -8
  111. package/dist/src/utils/deep-merge.js +2 -4
  112. package/dist/src/utils/flow-map.js +1 -6
  113. package/dist/src/utils/polling.d.ts +3 -0
  114. package/dist/src/utils/polling.js +3 -0
  115. package/dist/src/utils/project-archive.js +10 -14
  116. package/dist/src/utils/project-incarnation.js +3 -7
  117. package/dist/src/utils/records.d.ts +4 -0
  118. package/dist/src/utils/records.js +8 -0
  119. package/dist/src/utils/stable-hash.d.ts +3 -1
  120. package/dist/src/utils/stable-hash.js +2 -0
  121. package/package.json +10 -14
  122. package/plugin.json +1 -1
  123. package/skills/dataiku-dss/SKILL.md +3 -4
  124. package/skills/dataiku-dss/references/authentication.md +3 -3
  125. package/skills/dataiku-dss/references/coding.md +1 -1
  126. package/skills/dataiku-dss/references/discovery.md +1 -1
  127. package/skills/dataiku-dss/references/mutations.md +0 -1
  128. package/dist/src/utils/pagination.d.ts +0 -8
  129. package/dist/src/utils/pagination.js +0 -23
  130. package/packages/types/dist/index.d.ts +0 -1062
  131. package/packages/types/dist/index.js +0 -871
@@ -29,6 +29,7 @@ export function firstStringField(items, fields) {
29
29
  return null;
30
30
  }
31
31
  export function integrationFlag(name) {
32
+ // oxlint-disable-next-line dss/no-direct-process-env -- RUN_DATAIKU_* integration opt-in flags
32
33
  const value = process.env[name];
33
34
  return value === "1" || value?.toLowerCase() === "true";
34
35
  }
@@ -8,4 +8,6 @@ export type EnvFileProvenance = "project" | "install";
8
8
  */
9
9
  export declare function envFileProvenance(name: string): EnvFileProvenance | undefined;
10
10
  export declare function dataikuEnvironmentEnabled(): boolean;
11
+ /** DATAIKU_PROJECT_KEY from the environment, unless `DATAIKU_DISABLE_ENV=1`. */
12
+ export declare function ambientProjectKey(): string | undefined;
11
13
  export declare function loadEnvFile(): void;
@@ -20,6 +20,20 @@ export function envFileProvenance(name) {
20
20
  export function dataikuEnvironmentEnabled() {
21
21
  return process.env.DATAIKU_DISABLE_ENV !== "1";
22
22
  }
23
+ /** DATAIKU_PROJECT_KEY from the environment, unless `DATAIKU_DISABLE_ENV=1`. */
24
+ export function ambientProjectKey() {
25
+ return dataikuEnvironmentEnabled() ? process.env.DATAIKU_PROJECT_KEY : undefined;
26
+ }
27
+ const allowedEnvKeys = new Set(["NODE_EXTRA_CA_CERTS", "NODE_TLS_REJECT_UNAUTHORIZED",]);
28
+ /**
29
+ * `.env` files are untrusted input: only the CLI's own `DATAIKU_*` variables and
30
+ * Node's TLS trust knobs are read. Every other key is ignored silently, so a
31
+ * project working tree cannot redirect the CLI (for example by smuggling in
32
+ * `DSS_CONFIG_DIR` and sending saved credentials elsewhere).
33
+ */
34
+ function isAllowedEnvKey(key) {
35
+ return key.startsWith("DATAIKU_") || allowedEnvKeys.has(key);
36
+ }
23
37
  export function loadEnvFile() {
24
38
  if (!dataikuEnvironmentEnabled())
25
39
  return;
@@ -42,7 +56,8 @@ export function loadEnvFile() {
42
56
  continue;
43
57
  const key = trimmed.slice(0, eq).trim();
44
58
  const val = trimmed.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
45
- if (!process.env[key]) {
59
+ // A variable set in the environment, even to "", wins over .env ("" clears it).
60
+ if (isAllowedEnvKey(key) && process.env[key] === undefined) {
46
61
  process.env[key] = val;
47
62
  (index === 0 ? projectEnvVars : installEnvVars).add(key);
48
63
  }
@@ -1,3 +1,4 @@
1
+ /** Planning wins dispatch; dryRun remains set for combined plan metadata. */
1
2
  export declare function executionMode(flags: Record<string, string | boolean>): {
2
3
  readonly plan: boolean;
3
4
  readonly dryRun: boolean;
@@ -1,16 +1,11 @@
1
1
  import { parseBooleanOption, } from "./coerce.js";
2
2
  import { unsupportedHelpFlag, UsageError, } from "./usage.js";
3
3
  /** Planning wins dispatch; dryRun remains set for combined plan metadata. */
4
- const EXECUTION_MODES = [
5
- Object.freeze({ plan: false, dryRun: false, }),
6
- Object.freeze({ plan: false, dryRun: true, }),
7
- Object.freeze({ plan: true, dryRun: false, }),
8
- Object.freeze({ plan: true, dryRun: true, }),
9
- ];
10
4
  export function executionMode(flags) {
11
- const plan = parseBooleanOption(flags["plan"], "--plan") ?? false;
12
- const dryRun = parseBooleanOption(flags["dry-run"], "--dry-run") ?? false;
13
- return EXECUTION_MODES[(plan ? 2 : 0) + (dryRun ? 1 : 0)];
5
+ return {
6
+ plan: parseBooleanOption(flags["plan"], "--plan") ?? false,
7
+ dryRun: parseBooleanOption(flags["dry-run"], "--dry-run") ?? false,
8
+ };
14
9
  }
15
10
  export const BOOLEAN_FLAGS = new Set([
16
11
  "verbose",
@@ -1,7 +1,9 @@
1
1
  import * as fs from "node:fs";
2
+ import { ClientValidationError, } from "../../errors.js";
2
3
  import { canonicalDssUrl, } from "../../utils/dss-url.js";
4
+ import { asRecord, } from "../../utils/records.js";
3
5
  import { compareStrings, } from "../../utils/stable-hash.js";
4
- import { parseJsonObject, plainRecord, stableHash, stableJson, } from "../coerce.js";
6
+ import { parseJsonObject, stableHash, stableJson, } from "../coerce.js";
5
7
  import { UsageError, } from "../usage.js";
6
8
  /**
7
9
  * Version 3 binds every snapshot to both its DSS server and the concrete project
@@ -61,7 +63,7 @@ function writeAllSync(fd, payload) {
61
63
  while (offset < payload.length) {
62
64
  const written = fs.writeSync(fd, payload, offset, payload.length - offset, offset);
63
65
  if (written <= 0) {
64
- throw new Error(`Stalled writing app permissions snapshot at byte ${offset}.`);
66
+ throw new ClientValidationError(`Stalled writing app permissions snapshot at byte ${offset}.`, "internal_error");
65
67
  }
66
68
  offset += written;
67
69
  }
@@ -123,7 +125,7 @@ export function parseAppPermissionsSnapshot(text, source) {
123
125
  const capturedAt = requiredSnapshotString(record, "capturedAt", source);
124
126
  const hash = requiredSnapshotString(record, "hash", source);
125
127
  const permissionsHash = requiredSnapshotString(record, "permissionsHash", source);
126
- const permissions = plainRecord(record["permissions"]);
128
+ const permissions = asRecord(record["permissions"]);
127
129
  if (!permissions) {
128
130
  throw new UsageError(`${source} is not an app permissions snapshot: "permissions" must be a JSON object.`, "validation_failed");
129
131
  }
@@ -208,8 +210,8 @@ function pushDifference(out, path, backup, current) {
208
210
  function collectDifferences(path, backup, current, out) {
209
211
  if (stableJson(backup) === stableJson(current))
210
212
  return;
211
- const backupRecord = plainRecord(backup);
212
- const currentRecord = plainRecord(current);
213
+ const backupRecord = asRecord(backup);
214
+ const currentRecord = asRecord(current);
213
215
  if (backupRecord && currentRecord) {
214
216
  const keys = [...new Set([...Object.keys(backupRecord), ...Object.keys(currentRecord),]),]
215
217
  .sort(compareStrings);
@@ -1,6 +1,8 @@
1
1
  import { readFileSync, } from "node:fs";
2
+ import { asRecord, } from "../../utils/records.js";
2
3
  import { compareStrings, } from "../../utils/stable-hash.js";
3
- import { finiteNumberField, jsonInput, optionalStringField, parseJsonObject, plainRecord, requiredStringArray, splitCsvFlag, } from "../coerce.js";
4
+ import { finiteNumberField, jsonInput, optionalStringField, parseJsonObject, requiredStringArray, splitCsvFlag, } from "../coerce.js";
5
+ import { commandUsage, } from "../syntax.js";
4
6
  import { UsageError, } from "../usage.js";
5
7
  export function flowZoneId(value) {
6
8
  const trimmed = value.trim();
@@ -10,7 +12,7 @@ export function flowZoneId(value) {
10
12
  }
11
13
  export function flowZoneName(value) {
12
14
  if (typeof value !== "string" || value.trim().length === 0) {
13
- throw new UsageError("--name is required. Usage: dss flow-zone create --name NAME [--color #RRGGBB]");
15
+ throw new UsageError(`--name is required. Usage: ${commandUsage("flow-zone", "create")}`);
14
16
  }
15
17
  return value.trim();
16
18
  }
@@ -106,7 +108,7 @@ export function flowZonePlanColor(value, source) {
106
108
  export function flowZonePlanPosition(value, source) {
107
109
  if (value === undefined)
108
110
  return undefined;
109
- const record = plainRecord(value);
111
+ const record = asRecord(value);
110
112
  if (!record) {
111
113
  throw new UsageError(`${source} must be an object with x and y.`, "validation_failed");
112
114
  }
@@ -116,7 +118,7 @@ export function flowZonePlanPosition(value, source) {
116
118
  };
117
119
  }
118
120
  export function flowZoneCurrentPosition(zone) {
119
- const position = plainRecord(zone.position);
121
+ const position = asRecord(zone.position);
120
122
  if (!position)
121
123
  return undefined;
122
124
  const x = position.x;
@@ -133,7 +135,7 @@ export function flowZoneSamePosition(a, b) {
133
135
  export function parseFlowZonePlanItem(value, source) {
134
136
  if (typeof value === "string")
135
137
  return parseFlowZoneObject(value);
136
- const record = plainRecord(value);
138
+ const record = asRecord(value);
137
139
  if (!record) {
138
140
  throw new UsageError(`${source} must be TYPE:ID or an object.`, "validation_failed");
139
141
  }
@@ -212,7 +214,7 @@ export function parseFlowZoneOrganizePlan(input) {
212
214
  ...(topologyFingerprint ? { topologyFingerprint, } : {}),
213
215
  zones: zones.map((value, index) => {
214
216
  const source = `zones[${index}]`;
215
- const record = plainRecord(value);
217
+ const record = asRecord(value);
216
218
  if (!record)
217
219
  throw new UsageError(`${source} must be an object.`, "validation_failed");
218
220
  const id = optionalStringField(record, ["id", "zoneId",]);
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import { tmpdir, } from "node:os";
3
3
  import { join, parse as parsePath, relative as relativePath, resolve as resolvePath, sep, } from "node:path";
4
+ import { ClientValidationError, } from "../../errors.js";
4
5
  import { sanitizeFileName, } from "../../utils/sanitize.js";
5
6
  import { normalizeLineEndings, sha256Hex, stableHash, } from "../coerce.js";
6
7
  import { UsageError, } from "../usage.js";
@@ -126,7 +127,7 @@ function writeAllSync(fd, payload) {
126
127
  while (offset < payload.length) {
127
128
  const written = fs.writeSync(fd, payload, offset, payload.length - offset, offset);
128
129
  if (written <= 0) {
129
- throw new Error(`Stalled writing recipe backup at byte ${offset}.`);
130
+ throw new ClientValidationError(`Stalled writing recipe backup at byte ${offset}.`, "internal_error");
130
131
  }
131
132
  offset += written;
132
133
  }
@@ -0,0 +1,83 @@
1
+ import type { CommandDefinition, CommandFlagChoice, CommandMeta } from "./types.js";
2
+ /**
3
+ * One element of a command's usage line. `src/cli/command-syntax.json` holds an
4
+ * ordered node list per command; it is the source of truth for the usage text
5
+ * (rendered by {@link commandUsage}) and for every usage-level contract field
6
+ * (derived by {@link commandSyntax}).
7
+ */
8
+ export type SyntaxNode =
9
+ /** Literal word: `dss`, the resource, the action. */
10
+ string
11
+ /** Positional argument, rendered `<name>` (or bare `name` when `bare`). */
12
+ | {
13
+ arg: string;
14
+ bare?: boolean;
15
+ }
16
+ /** Flag, rendered `--flag`, `--flag VALUE`, or `--flag a|b` (enum), plus an optional trailing note. */
17
+ | {
18
+ flag: string;
19
+ value?: string;
20
+ values?: string[];
21
+ note?: string;
22
+ }
23
+ /** Optional group `[a|b]`; each alternative is a node sequence. */
24
+ | {
25
+ optional: SyntaxNode[][];
26
+ sep?: string;
27
+ }
28
+ /** Required choice `(a|b)`; each alternative is a node sequence. */
29
+ | {
30
+ oneOf: SyntaxNode[][];
31
+ sep?: string;
32
+ };
33
+ export type FlagNode = Extract<SyntaxNode, {
34
+ flag: string;
35
+ }>;
36
+ export interface CommandPositionalMetadata {
37
+ name: string;
38
+ required: boolean;
39
+ }
40
+ /** Input channels a command accepts for its JSON payload. */
41
+ export interface CommandInputContract {
42
+ stdin?: boolean;
43
+ dataFlag?: boolean;
44
+ dataFileFlag?: boolean;
45
+ }
46
+ /** Usage-level contract fields derived from a command's syntax tree. */
47
+ export interface CommandSyntax {
48
+ /** Canonical names of the flags the usage line lists, in order. */
49
+ flags: string[];
50
+ requiredFlags: string[];
51
+ requiredOneOf: CommandFlagChoice[];
52
+ valueHints: Record<string, {
53
+ valueType: string;
54
+ enumValues?: string[];
55
+ }>;
56
+ inputContract: CommandInputContract;
57
+ positionalArguments: CommandPositionalMetadata[];
58
+ /** Alias spellings (canonical name -> raw names) the usage line uses. */
59
+ aliases: Record<string, string[]>;
60
+ dryRun: boolean;
61
+ producesLocalFile: boolean;
62
+ }
63
+ /** The syntax tree for `resource.action`, or undefined when no such command exists. */
64
+ export declare function commandSyntaxTree(resource: string, action: string): SyntaxNode[] | undefined;
65
+ export declare function commandSyntaxKeys(): string[];
66
+ /** Render a node sequence as usage text; `omitOptional` drops every `[...]` group. */
67
+ export declare function renderNodes(nodes: SyntaxNode[], omitOptional?: boolean): string;
68
+ /** The human usage line for `resource.action`, rendered from its syntax tree. */
69
+ export declare function commandUsage(resource: string, action: string): string;
70
+ /**
71
+ * Attach each action's usage line, rendered from its syntax tree. Every command
72
+ * module exports its definitions through this, so a command without a syntax
73
+ * entry fails at load instead of shipping an undocumented contract.
74
+ */
75
+ export declare function withUsage(resource: string, definitions: Record<string, CommandDefinition>): Record<string, CommandMeta>;
76
+ /** Whether the syntax lists `--name` (the raw spelling, not an alias). */
77
+ export declare function syntaxHasFlag(nodes: SyntaxNode[], name: string): boolean;
78
+ /** Leading literal words after `dss` (resource and, when present, action). */
79
+ export declare function syntaxCommandWords(nodes: SyntaxNode[]): string[];
80
+ /** Usage-level contract fields of a syntax tree (flags, requirements, value hints, positionals, inputs). */
81
+ export declare function deriveSyntax(nodes: SyntaxNode[]): CommandSyntax;
82
+ /** Usage-level contract fields for `resource.action`, derived from its syntax tree. */
83
+ export declare function commandSyntax(resource: string, action: string): CommandSyntax;
@@ -0,0 +1,217 @@
1
+ import { ClientValidationError, } from "../errors.js";
2
+ import commandSyntaxSource from "./command-syntax.json" with { type: "json" };
3
+ import { FLAG_ALIASES, KNOWN_LONG_FLAGS, } from "./flags.js";
4
+ const SOURCE = commandSyntaxSource.commands;
5
+ /** The syntax tree for `resource.action`, or undefined when no such command exists. */
6
+ export function commandSyntaxTree(resource, action) {
7
+ return Object.hasOwn(SOURCE, `${resource}.${action}`)
8
+ ? SOURCE[`${resource}.${action}`]
9
+ : undefined;
10
+ }
11
+ function requiredSyntaxTree(resource, action) {
12
+ const nodes = commandSyntaxTree(resource, action);
13
+ if (!nodes) {
14
+ throw new ClientValidationError(`No syntax for ${resource} ${action} in src/cli/command-syntax.json.`, "internal_error");
15
+ }
16
+ return nodes;
17
+ }
18
+ export function commandSyntaxKeys() {
19
+ return Object.keys(SOURCE);
20
+ }
21
+ function renderNode(node, omitOptional) {
22
+ if (typeof node === "string")
23
+ return node;
24
+ if ("arg" in node)
25
+ return node.bare ? node.arg : `<${node.arg}>`;
26
+ if ("flag" in node) {
27
+ const value = node.values ? node.values.join("|") : node.value;
28
+ return `--${node.flag}${value === undefined ? "" : ` ${value}`}${node.note ? ` ${node.note}` : ""}`;
29
+ }
30
+ if ("optional" in node) {
31
+ return omitOptional ? "" : `[${renderAlternatives(node.optional, node.sep, omitOptional)}]`;
32
+ }
33
+ return `(${renderAlternatives(node.oneOf, node.sep, omitOptional)})`;
34
+ }
35
+ function renderAlternatives(alternatives, sep, omitOptional) {
36
+ return alternatives.map((alternative) => renderNodes(alternative, omitOptional)).join(sep ?? "|");
37
+ }
38
+ /** Render a node sequence as usage text; `omitOptional` drops every `[...]` group. */
39
+ export function renderNodes(nodes, omitOptional = false) {
40
+ return nodes.map((node) => renderNode(node, omitOptional)).filter((part) => part !== "").join(" ");
41
+ }
42
+ const usageCache = new Map();
43
+ /** The human usage line for `resource.action`, rendered from its syntax tree. */
44
+ export function commandUsage(resource, action) {
45
+ const key = `${resource}.${action}`;
46
+ let usage = usageCache.get(key);
47
+ if (usage === undefined) {
48
+ usage = renderNodes(requiredSyntaxTree(resource, action));
49
+ usageCache.set(key, usage);
50
+ }
51
+ return usage;
52
+ }
53
+ /**
54
+ * Attach each action's usage line, rendered from its syntax tree. Every command
55
+ * module exports its definitions through this, so a command without a syntax
56
+ * entry fails at load instead of shipping an undocumented contract.
57
+ */
58
+ export function withUsage(resource, definitions) {
59
+ const commands = {};
60
+ for (const [action, definition,] of Object.entries(definitions)) {
61
+ commands[action] = { ...definition, usage: commandUsage(resource, action), };
62
+ }
63
+ return commands;
64
+ }
65
+ function walk(nodes, visit, context = {
66
+ optional: false,
67
+ oneOf: undefined,
68
+ }) {
69
+ for (const node of nodes) {
70
+ if (typeof node === "string")
71
+ continue;
72
+ visit(node, context);
73
+ if ("optional" in node) {
74
+ for (const alternative of node.optional) {
75
+ walk(alternative, visit, { ...context, optional: true, });
76
+ }
77
+ }
78
+ else if ("oneOf" in node) {
79
+ for (const alternative of node.oneOf) {
80
+ walk(alternative, visit, { ...context, oneOf: node.oneOf, });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ function flagNodes(nodes) {
86
+ const found = [];
87
+ walk(nodes, (node) => {
88
+ if ("flag" in node)
89
+ found.push(node);
90
+ });
91
+ return found;
92
+ }
93
+ /** Whether the syntax lists `--name` (the raw spelling, not an alias). */
94
+ export function syntaxHasFlag(nodes, name) {
95
+ return flagNodes(nodes).some((node) => node.flag === name);
96
+ }
97
+ /** Leading literal words after `dss` (resource and, when present, action). */
98
+ export function syntaxCommandWords(nodes) {
99
+ const words = [];
100
+ for (const node of nodes.slice(1)) {
101
+ if (typeof node !== "string")
102
+ break;
103
+ words.push(node);
104
+ }
105
+ return words;
106
+ }
107
+ function canonicalFlag(name) {
108
+ return FLAG_ALIASES[name] ?? name;
109
+ }
110
+ function knownCanonicalFlags(nodes) {
111
+ return unique(nodes.map((node) => canonicalFlag(node.flag))).filter((flag) => KNOWN_LONG_FLAGS.has(flag));
112
+ }
113
+ function unique(values) {
114
+ return [...new Set(values),];
115
+ }
116
+ /** Flags of a sequence outside its optional groups (a required choice's alternative). */
117
+ function requiredFlagsOf(nodes) {
118
+ const found = [];
119
+ walk(nodes, (node, context) => {
120
+ if ("flag" in node && !context.optional)
121
+ found.push(node);
122
+ });
123
+ return knownCanonicalFlags(found);
124
+ }
125
+ function deriveRequired(nodes) {
126
+ const requiredFlags = knownCanonicalFlags(nodes.filter((node) => typeof node !== "string" && "flag" in node));
127
+ const requiredOneOf = [];
128
+ for (const node of nodes) {
129
+ if (typeof node === "string" || !("oneOf" in node))
130
+ continue;
131
+ const alternatives = node.oneOf.map(requiredFlagsOf);
132
+ if (alternatives.length <= 1) {
133
+ requiredFlags.push(...(alternatives[0] ?? []));
134
+ continue;
135
+ }
136
+ // A choice with a flagless alternative (a positional, e.g. sql query's
137
+ // `(SQL | --sql QUERY | ...)`) cannot be expressed as a flag-only choice:
138
+ // requiring one of the flags would be false. The handler stays the authority.
139
+ if (alternatives.some((alternative) => alternative.length === 0))
140
+ continue;
141
+ requiredOneOf.push({ oneOf: alternatives, });
142
+ }
143
+ return { requiredFlags: unique(requiredFlags), requiredOneOf, };
144
+ }
145
+ const PLACEHOLDER_VALUE = /^(<[^>]+>|[A-Z][A-Za-z0-9_]*)/;
146
+ function deriveValueHints(flags) {
147
+ const hints = {};
148
+ // Enumerations take precedence over a placeholder elsewhere in the line.
149
+ for (const node of flags) {
150
+ const flag = canonicalFlag(node.flag);
151
+ if (node.values && hints[flag] === undefined) {
152
+ hints[flag] = { valueType: "enum", enumValues: node.values, };
153
+ }
154
+ }
155
+ for (const node of flags) {
156
+ const flag = canonicalFlag(node.flag);
157
+ const placeholder = node.value?.match(PLACEHOLDER_VALUE)?.[1];
158
+ if (placeholder && hints[flag] === undefined)
159
+ hints[flag] = { valueType: placeholder, };
160
+ }
161
+ return hints;
162
+ }
163
+ function derivePositionals(nodes) {
164
+ const positionals = [];
165
+ walk(nodes, (node, context) => {
166
+ if (!("arg" in node))
167
+ return;
168
+ const arg = node;
169
+ // A positional inside a required choice that offers a flag instead is optional.
170
+ const flagAlternative = context.oneOf?.some((alternative) => flagNodes(alternative).length > 0)
171
+ ?? false;
172
+ const required = !context.optional && !flagAlternative;
173
+ const existing = positionals.find((positional) => positional.name === arg.arg);
174
+ if (existing)
175
+ existing.required ||= required;
176
+ else
177
+ positionals.push({ name: arg.arg, required, });
178
+ });
179
+ return positionals;
180
+ }
181
+ /** Usage-level contract fields of a syntax tree (flags, requirements, value hints, positionals, inputs). */
182
+ export function deriveSyntax(nodes) {
183
+ const flags = flagNodes(nodes);
184
+ const has = (name) => flags.some((node) => node.flag === name);
185
+ const aliases = {};
186
+ for (const [raw, canonical,] of Object.entries(FLAG_ALIASES)) {
187
+ if (has(raw))
188
+ (aliases[canonical] ??= []).push(raw);
189
+ }
190
+ return {
191
+ flags: knownCanonicalFlags(flags),
192
+ ...deriveRequired(nodes),
193
+ valueHints: deriveValueHints(flags),
194
+ inputContract: {
195
+ ...(has("stdin") ? { stdin: true, } : {}),
196
+ ...(flags.some((node) => node.flag === "data" && (node.value !== undefined || node.values !== undefined))
197
+ ? { dataFlag: true, }
198
+ : {}),
199
+ ...(has("data-file") ? { dataFileFlag: true, } : {}),
200
+ },
201
+ positionalArguments: derivePositionals(nodes),
202
+ aliases,
203
+ dryRun: has("dry-run"),
204
+ producesLocalFile: flags.some((node) => (node.flag === "output" || node.flag === "output-file") && node.value === "PATH"),
205
+ };
206
+ }
207
+ const syntaxCache = new Map();
208
+ /** Usage-level contract fields for `resource.action`, derived from its syntax tree. */
209
+ export function commandSyntax(resource, action) {
210
+ const key = `${resource}.${action}`;
211
+ let syntax = syntaxCache.get(key);
212
+ if (!syntax) {
213
+ syntax = deriveSyntax(requiredSyntaxTree(resource, action));
214
+ syntaxCache.set(key, syntax);
215
+ }
216
+ return syntax;
217
+ }
@@ -28,7 +28,10 @@ export interface CommandMeta extends CommandRegistryOverride {
28
28
  * credentials are configured, instead of being masked by a missing-URL error.
29
29
  */
30
30
  validate?: (args: string[], flags: Record<string, string | boolean>) => void;
31
+ /** Rendered from the command's syntax tree in `src/cli/command-syntax.json`; never hand-written. */
31
32
  usage: string;
32
33
  description?: string;
33
34
  examples?: string[];
34
35
  }
36
+ /** A command as authored: its usage line comes from `src/cli/command-syntax.json` (see `withUsage`). */
37
+ export type CommandDefinition = Omit<CommandMeta, "usage">;
@@ -1,6 +1,6 @@
1
1
  import type { StableErrorCode } from "../errors.js";
2
2
  export declare const RESOURCE_NAMES: string[];
3
- export declare function inferRequiresProject(resource: string, action: string, usage: string): boolean;
3
+ export declare function inferRequiresProject(resource: string, action: string): boolean;
4
4
  export declare class UsageError extends Error {
5
5
  readonly code: StableErrorCode;
6
6
  readonly hint?: string;
@@ -1,3 +1,4 @@
1
+ import { commandSyntaxTree, syntaxHasFlag, } from "./syntax.js";
1
2
  export const RESOURCE_NAMES = [
2
3
  "agent",
3
4
  "app",
@@ -79,7 +80,7 @@ const PROJECT_SCOPED_RESOURCES = new Set([
79
80
  "saved-model",
80
81
  "wiki",
81
82
  ]);
82
- export function inferRequiresProject(resource, action, usage) {
83
+ export function inferRequiresProject(resource, action) {
83
84
  if (resource === "agent" || resource === "auth" || resource === "doctor" || resource === "commands"
84
85
  || resource === "install-skill" || resource === "version")
85
86
  return false;
@@ -92,7 +93,8 @@ export function inferRequiresProject(resource, action, usage) {
92
93
  return false;
93
94
  if (resource === "project-git")
94
95
  return !action.startsWith("future-");
95
- return usage.includes("--project-key");
96
+ const syntax = commandSyntaxTree(resource, action);
97
+ return syntax !== undefined && syntaxHasFlag(syntax, "project-key");
96
98
  }
97
99
  export class UsageError extends Error {
98
100
  code;
@@ -37,8 +37,6 @@ export interface CliVersionPayload {
37
37
  buildRevision: string | null;
38
38
  staleBuild: boolean;
39
39
  }
40
- /** Bun is the only supported runtime; the field is retained for payload stability. */
41
- export declare function detectRuntime(): CliRuntime;
42
40
  export declare function buildVersionPayload(input: {
43
41
  packageVersion: string;
44
42
  checkoutRevision: string | undefined;
@@ -182,10 +182,6 @@ export function sourceTreeNewerThanBuild(packageRoot) {
182
182
  return false;
183
183
  }
184
184
  }
185
- /** Bun is the only supported runtime; the field is retained for payload stability. */
186
- export function detectRuntime() {
187
- return "bun";
188
- }
189
185
  export function buildVersionPayload(input) {
190
186
  const isDist = input.loadSource === "dist";
191
187
  // Source execution never claims a build revision, even if the environment
@@ -212,8 +208,11 @@ export const AGENT_CONTRACT_VERSION = 2;
212
208
  export const JSON_SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema";
213
209
  export const AGENT_CONTRACT_SCHEMA_ID = "https://clssck.github.io/dataiku-sdk/schemas/agent-contract-v2.json";
214
210
  export function cliVersionResult() {
215
- const loadSource = process.env["DSS_LOAD_SOURCE"] ?? detectLoadSource();
216
- const envBuildRevision = process.env["DSS_BUILD_REVISION"];
211
+ // Only code actually loaded from dist/ may report dist provenance; an
212
+ // inherited DSS_LOAD_SOURCE on a source run is ignored.
213
+ const loadSource = detectLoadSource();
214
+ /* oxlint-disable-next-line dss/no-direct-process-env -- launcher-injected build revision */
215
+ const envBuildRevision = loadSource === "dist" ? process.env["DSS_BUILD_REVISION"] : undefined;
217
216
  return buildVersionPayload({
218
217
  packageVersion: CLI_VERSION,
219
218
  checkoutRevision: CLI_GIT_REVISION,
@@ -225,6 +224,6 @@ export function cliVersionResult() {
225
224
  sourceNewerThanBuild: loadSource === "dist"
226
225
  ? sourceTreeNewerThanBuild(PACKAGE_ROOT)
227
226
  : false,
228
- runtime: detectRuntime(),
227
+ runtime: "bun",
229
228
  });
230
229
  }