@get-dx/cli 0.2.0 → 0.3.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.
@@ -1,10 +1,10 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { input, password, confirm, select } from "@inquirer/prompts";
4
+ import { password, confirm, select } from "@inquirer/prompts";
5
5
  import { Command } from "commander";
6
6
  import { execa } from "execa";
7
- import { buildRuntime, buildRuntimeSafe } from "../runtime.js";
7
+ import { buildLogger, buildRuntime, buildRuntimeSafe } from "../runtime.js";
8
8
  import { getAuthInfo } from "./auth.js";
9
9
  import { renderAuthInfo } from "./authRendering.js";
10
10
  import { loginViaBrowser } from "../loginViaBrowser.js";
@@ -13,29 +13,8 @@ import { wrapAction } from "../commandHelpers.js";
13
13
  import { renderRichText } from "../renderers.js";
14
14
  import * as ui from "../ui.js";
15
15
  import { CliError } from "../errors.js";
16
- import { persistBaseUrl } from "../config.js";
16
+ import { deriveBaseUrlsFromEnv, persistBaseUrls } from "../config.js";
17
17
  import { setToken } from "../secrets.js";
18
- function parseHostname(raw) {
19
- const normalized = raw.trim().replace(/\/$/, "");
20
- if (!normalized || normalized === "app.getdx.com") {
21
- return { type: "cloud" };
22
- }
23
- try {
24
- const url = new URL(normalized.startsWith("http") ? normalized : `https://${normalized}`);
25
- const host = url.hostname;
26
- const dedicatedMatch = host.match(/^(.+)\.getdx\.io$/);
27
- if (dedicatedMatch) {
28
- return { type: "dedicated", accountName: dedicatedMatch[1] };
29
- }
30
- if (host) {
31
- return { type: "managed", uiBaseUrl: url.origin };
32
- }
33
- }
34
- catch {
35
- // fall through to invalid
36
- }
37
- return { type: "invalid" };
38
- }
39
18
  // FIXME: make this WAY more glam
40
19
  // Choose a more interesting ASCII art font from https://patorjk.com/software/taag/
41
20
  const WELCOME_BANNER = ui.indent(`\
@@ -51,14 +30,12 @@ export function initCommand() {
51
30
  const init = new Command()
52
31
  .name("init")
53
32
  .description("Initialize the DX CLI")
54
- .option("--host <hostname>", "DX hostname for dedicated or managed deployments (e.g. mycompany.getdx.io)")
55
33
  .action(wrapAction(async (commandOptions, command) => {
56
34
  const context = getContext(command);
57
35
  let runtime = buildRuntimeSafe(context);
58
36
  ensureInteractive();
59
37
  showWelcomeBanner();
60
- const host = commandOptions.host || process.env.DX_HOST;
61
- runtime = await ensureLoggedIn(runtime, host);
38
+ runtime = await ensureLoggedIn(context, runtime);
62
39
  await optionallySetupSkill(runtime);
63
40
  renderRichText([
64
41
  ui.blankLine(),
@@ -75,7 +52,7 @@ function ensureInteractive() {
75
52
  function showWelcomeBanner() {
76
53
  renderRichText([ui.p(ui.success(WELCOME_BANNER))]);
77
54
  }
78
- async function ensureLoggedIn(runtime, host) {
55
+ async function ensureLoggedIn(context, runtime) {
79
56
  renderRichText([ui.h1(`Checking if you are logged in...`), ui.blankLine()]);
80
57
  if (runtime) {
81
58
  let authInfo = null;
@@ -86,34 +63,17 @@ async function ensureLoggedIn(runtime, host) {
86
63
  // Do nothing, we don't mind if this fails
87
64
  }
88
65
  if (authInfo) {
89
- renderAuthInfo(authInfo, runtime.token, runtime.baseUrl);
66
+ renderAuthInfo(authInfo, runtime.token, runtime.apiBaseUrl);
90
67
  return runtime;
91
68
  }
92
69
  }
93
70
  renderRichText([ui.p(`You are not logged in yet.`), ui.blankLine()]);
94
- const parsed = host ? parseHostname(host) : { type: "cloud" };
95
- if (parsed.type === "invalid") {
96
- throw new CliError(`Could not recognize hostname "${host}". Expected app.getdx.com, <account>.getdx.io, or a custom domain.`);
97
- }
98
- switch (parsed.type) {
99
- case "cloud":
100
- return await attemptLogin("https://api.getdx.com", "https://app.getdx.com");
101
- case "dedicated": {
102
- const { accountName } = parsed;
103
- return await attemptLogin(`https://api.${accountName}.getdx.io`, `https://${accountName}.getdx.io`);
104
- }
105
- case "managed": {
106
- const apiBaseUrl = await input({
107
- message: "What is your API base URL?",
108
- });
109
- if (!apiBaseUrl) {
110
- throw new CliError("API base URL is required");
111
- }
112
- return await attemptLogin(apiBaseUrl, parsed.uiBaseUrl);
113
- }
114
- }
71
+ const { apiBaseUrl, webBaseUrl } = deriveBaseUrlsFromEnv();
72
+ return await attemptLogin(apiBaseUrl, webBaseUrl, context);
115
73
  }
116
- async function attemptLogin(apiBaseUrl, uiBaseUrl) {
74
+ async function attemptLogin(apiBaseUrl, webBaseUrl, context) {
75
+ const logger = buildLogger(context);
76
+ logger.debug("Attempting login", { apiBaseUrl, webBaseUrl });
117
77
  const method = await select({
118
78
  message: "How would you like to log in?",
119
79
  choices: [
@@ -123,7 +83,7 @@ async function attemptLogin(apiBaseUrl, uiBaseUrl) {
123
83
  });
124
84
  let token;
125
85
  if (method === "browser") {
126
- token = await loginViaBrowser(uiBaseUrl);
86
+ token = await loginViaBrowser(webBaseUrl);
127
87
  }
128
88
  else {
129
89
  token = await password({
@@ -134,17 +94,17 @@ async function attemptLogin(apiBaseUrl, uiBaseUrl) {
134
94
  if (!token) {
135
95
  throw new CliError("Account web API token is required");
136
96
  }
137
- const context = createEmptyContext();
138
97
  const runtime = buildRuntime(context, {
139
- baseUrl: apiBaseUrl,
98
+ apiBaseUrl,
140
99
  token,
100
+ webBaseUrl,
141
101
  });
142
102
  renderRichText([ui.blankLine(), ui.p(`Attempting login...`), ui.blankLine()]);
143
103
  const response = await getAuthInfo(runtime);
144
104
  if (!response.ok) {
145
105
  throw new CliError(`Login failed`);
146
106
  }
147
- persistBaseUrl(apiBaseUrl);
107
+ persistBaseUrls(apiBaseUrl, webBaseUrl);
148
108
  setToken(apiBaseUrl, token);
149
109
  renderAuthInfo(response, token, apiBaseUrl);
150
110
  return runtime;
@@ -195,8 +155,3 @@ async function optionallySetupSkill(runtime) {
195
155
  throw new CliError(`Failed to setup the DX skill: ${result.stderr}`);
196
156
  }
197
157
  }
198
- function createEmptyContext() {
199
- return {
200
- json: false,
201
- };
202
- }
@@ -53,7 +53,7 @@ export function scorecardsCommand() {
53
53
  renderJson({ ok: true, scorecard: response.scorecard });
54
54
  }
55
55
  else {
56
- renderScorecard(response.scorecard, null, `${ui.success(ui.GLYPHS.CHECK)} Scorecard created`);
56
+ renderScorecard(response.scorecard, null, runtime, `${ui.success(ui.GLYPHS.CHECK)} Scorecard created`);
57
57
  }
58
58
  }));
59
59
  scorecards
@@ -110,7 +110,7 @@ export function scorecardsCommand() {
110
110
  renderJson({ ok: true, scorecard: processedScorecard });
111
111
  }
112
112
  else {
113
- renderScorecard(response.scorecard, includeSections);
113
+ renderScorecard(response.scorecard, includeSections, runtime);
114
114
  }
115
115
  }));
116
116
  scorecards
@@ -211,7 +211,7 @@ export function scorecardsCommand() {
211
211
  renderJson({ ...response, scorecards: processedScorecards });
212
212
  }
213
213
  else {
214
- renderScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null, includeSections);
214
+ renderScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null, includeSections, runtime);
215
215
  }
216
216
  }));
217
217
  scorecards
@@ -256,7 +256,7 @@ export function scorecardsCommand() {
256
256
  renderJson({ ok: true, scorecard: response.scorecard });
257
257
  }
258
258
  else {
259
- renderScorecard(response.scorecard, null, `${ui.success(ui.GLYPHS.CHECK)} Scorecard updated`);
259
+ renderScorecard(response.scorecard, null, runtime, `${ui.success(ui.GLYPHS.CHECK)} Scorecard updated`);
260
260
  }
261
261
  }));
262
262
  return scorecards;
@@ -1,6 +1,6 @@
1
1
  import { renderRichText } from "../renderers.js";
2
2
  import * as ui from "../ui.js";
3
- export function renderScorecardList(scorecards, nextCursor, includeSections) {
3
+ export function renderScorecardList(scorecards, nextCursor, includeSections, runtime) {
4
4
  const shouldIncludeAll = includeSections === null;
5
5
  const blocks = [ui.h1("Scorecards")];
6
6
  blocks.push(ui.p(`Displaying ${ui.bold(scorecards.length.toString())} scorecards.`));
@@ -11,7 +11,7 @@ export function renderScorecardList(scorecards, nextCursor, includeSections) {
11
11
  blocks.push(ui.h2(`${scorecard.name} (${ui.code(scorecard.id)})`));
12
12
  if (shouldIncludeAll || includeSections.includes("core")) {
13
13
  blocks.push(ui.h3("Basic details"));
14
- blocks.push(...basicDetailsContent(scorecard));
14
+ blocks.push(...basicDetailsContent(scorecard, runtime));
15
15
  blocks.push(ui.h3("Entity filter"));
16
16
  blocks.push(...entityFilterContent(scorecard));
17
17
  if (scorecard.type === "LEVEL") {
@@ -34,12 +34,12 @@ export function renderScorecardList(scorecards, nextCursor, includeSections) {
34
34
  }
35
35
  renderRichText(blocks);
36
36
  }
37
- export function renderScorecard(scorecard, includeSections, title = "Scorecard Information") {
37
+ export function renderScorecard(scorecard, includeSections, runtime, title = "Scorecard Information") {
38
38
  const shouldIncludeAll = includeSections === null;
39
39
  const blocks = [ui.h1(title)];
40
40
  if (shouldIncludeAll || includeSections.includes("core")) {
41
41
  blocks.push(ui.h2("Basic details"));
42
- blocks.push(...basicDetailsContent(scorecard));
42
+ blocks.push(...basicDetailsContent(scorecard, runtime));
43
43
  blocks.push(ui.h2("Entity filter"));
44
44
  blocks.push(...entityFilterContent(scorecard));
45
45
  if (scorecard.type === "LEVEL") {
@@ -61,7 +61,7 @@ export function renderScorecard(scorecard, includeSections, title = "Scorecard I
61
61
  }
62
62
  renderRichText(blocks);
63
63
  }
64
- function basicDetailsContent(scorecard) {
64
+ function basicDetailsContent(scorecard, runtime) {
65
65
  const items = [
66
66
  ui.dli("ID", [ui.p(ui.code(scorecard.id), false)]),
67
67
  ui.dli("Name", [ui.p(scorecard.name, false)]),
@@ -74,6 +74,9 @@ function basicDetailsContent(scorecard) {
74
74
  ui.dli("Published", [
75
75
  ui.p(scorecard.published ? ui.success("Yes") : ui.dim("No (draft)"), false),
76
76
  ]),
77
+ ui.dli("Web link", [
78
+ ui.p(ui.link(ui.webLink(`/scorecards/${scorecard.id}`, runtime)), false),
79
+ ]),
77
80
  ];
78
81
  if (scorecard.tags && scorecard.tags.length > 0) {
79
82
  items.push(ui.dli("Tags", [
@@ -97,7 +100,7 @@ function entityFilterContent(scorecard) {
97
100
  else if (scorecard.entity_filter_type === "sql") {
98
101
  blocks.push(ui.p(`Filter: ${ui.code("sql")}`));
99
102
  if (scorecard.entity_filter_sql) {
100
- blocks.push(truncatedSqlBlock(scorecard.entity_filter_sql));
103
+ blocks.push(ui.truncatedSqlBlock(scorecard.entity_filter_sql));
101
104
  }
102
105
  else {
103
106
  blocks.push(ui.p(ui.dim("(No SQL provided)")));
@@ -190,7 +193,7 @@ function checkContent(check, scorecardType) {
190
193
  }
191
194
  // Query
192
195
  blocks.push(ui.p(ui.bold("Query:")));
193
- blocks.push(truncatedSqlBlock(check.sql));
196
+ blocks.push(ui.truncatedSqlBlock(check.sql));
194
197
  // Output settings
195
198
  if (check.output_enabled) {
196
199
  const outputParts = [`Output: ${ui.code(check.output_type ?? "unknown")}`];
@@ -202,18 +205,10 @@ function checkContent(check, scorecardType) {
202
205
  // Entity filter
203
206
  if (check.filter_sql) {
204
207
  blocks.push(ui.p(ui.bold("Entity filter:")));
205
- blocks.push(truncatedSqlBlock(check.filter_sql));
208
+ blocks.push(ui.truncatedSqlBlock(check.filter_sql));
206
209
  if (check.filter_message) {
207
210
  blocks.push(ui.p(`Excluded message: ${ui.dim(check.filter_message)}`, false));
208
211
  }
209
212
  }
210
213
  return blocks;
211
214
  }
212
- function truncatedSqlBlock(sql) {
213
- const lines = sql.split("\n");
214
- const firstLine = lines[0];
215
- const isLong = firstLine.length > 80 || lines.length > 1;
216
- const truncated = firstLine.length > 80 ? firstLine.slice(0, 80) : firstLine;
217
- const display = isLong ? truncated + ui.dim(" …") : truncated;
218
- return ui.p(ui.code(display));
219
- }
@@ -144,13 +144,13 @@ async function getStudioQueryResults(runtime, id) {
144
144
  return response.body;
145
145
  }
146
146
  async function downloadStudioQueryResultsCsv(runtime, id, filename) {
147
- const response = await fetchWithApiHeaders(runtime, `${runtime.baseUrl}/studio.queryRuns.results?id=${encodeURIComponent(id)}&format=csv`, { redirect: "manual" });
147
+ const response = await fetchWithApiHeaders(runtime, `${runtime.apiBaseUrl}/studio.queryRuns.results?id=${encodeURIComponent(id)}&format=csv`, { redirect: "manual" });
148
148
  if (response.status >= 300 && response.status < 400) {
149
149
  const location = response.headers.get("location");
150
150
  if (!location) {
151
151
  throw new HttpError("CSV download redirect did not include a Location header.", response.status);
152
152
  }
153
- const downloadResponse = await fetchResponse(new URL(location, runtime.baseUrl).toString(), {
153
+ const downloadResponse = await fetchResponse(new URL(location, runtime.apiBaseUrl).toString(), {
154
154
  method: "GET",
155
155
  headers: buildDownloadHeaders(runtime, "text/csv"),
156
156
  });
@@ -0,0 +1,82 @@
1
+ import { Command } from "commander";
2
+ import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
3
+ import { CliError, EXIT_CODES } from "../errors.js";
4
+ import { request } from "../http.js";
5
+ import { renderJson } from "../renderers.js";
6
+ import { buildRuntime } from "../runtime.js";
7
+ import { renderWorkflowList } from "./workflowsRendering.js";
8
+ const WORKFLOW_SCOPES = ["GLOBAL", "ENTITY"];
9
+ export function workflowsCommand() {
10
+ const workflows = new Command()
11
+ .name("workflows")
12
+ .description("Manage Self-service workflow definitions");
13
+ workflows
14
+ .command("list")
15
+ .description("List Self-service workflows for the account")
16
+ .option("--scope <scope>", "Filter by workflow scope: GLOBAL or ENTITY (omit for both)")
17
+ .option("--entity-identifier <id>", "Catalog entity identifier; must be used with --scope ENTITY")
18
+ .addHelpText("afterAll", createExampleText([
19
+ {
20
+ label: "List workflows the token can trigger",
21
+ command: "dx workflows list",
22
+ },
23
+ {
24
+ label: "Entity-scoped workflows for a catalog entity",
25
+ command: "dx workflows list --scope ENTITY --entity-identifier acme-app",
26
+ },
27
+ {
28
+ label: "Same request as JSON",
29
+ command: "dx --json workflows list --scope ENTITY --entity-identifier acme-app",
30
+ },
31
+ ]))
32
+ .action(wrapAction(async (options, command) => {
33
+ const scope = parseOptionalScope(options.scope);
34
+ const entityIdentifier = parseOptionalTrimmed(options.entityIdentifier);
35
+ if (entityIdentifier && scope !== "ENTITY") {
36
+ throw new CliError("--entity-identifier requires --scope ENTITY", EXIT_CODES.ARGUMENT_ERROR);
37
+ }
38
+ const runtime = buildRuntime(getContext(command));
39
+ const response = await listWorkflows(runtime, {
40
+ scope,
41
+ entity_identifier: entityIdentifier,
42
+ });
43
+ if (runtime.context.json) {
44
+ renderJson(response);
45
+ }
46
+ else {
47
+ renderWorkflowList(response.workflows);
48
+ }
49
+ }));
50
+ return workflows;
51
+ }
52
+ function parseOptionalTrimmed(value) {
53
+ if (typeof value !== "string") {
54
+ return undefined;
55
+ }
56
+ const t = value.trim();
57
+ return t.length > 0 ? t : undefined;
58
+ }
59
+ function parseOptionalScope(value) {
60
+ const raw = parseOptionalTrimmed(value);
61
+ if (raw === undefined) {
62
+ return undefined;
63
+ }
64
+ if (!WORKFLOW_SCOPES.includes(raw)) {
65
+ throw new CliError(`--scope must be one of: ${WORKFLOW_SCOPES.join(", ")}`, EXIT_CODES.ARGUMENT_ERROR);
66
+ }
67
+ return raw;
68
+ }
69
+ // --- API ---
70
+ async function listWorkflows(runtime, params) {
71
+ const query = {};
72
+ if (params.scope !== undefined)
73
+ query.scope = params.scope;
74
+ if (params.entity_identifier !== undefined) {
75
+ query.entity_identifier = params.entity_identifier;
76
+ }
77
+ const response = await request(runtime, "/workflows.list", {
78
+ method: "GET",
79
+ query,
80
+ });
81
+ return response.body;
82
+ }
@@ -0,0 +1,136 @@
1
+ import { renderRichText } from "../renderers.js";
2
+ import * as ui from "../ui.js";
3
+ const WORKFLOW_SCOPE_LABELS = {
4
+ GLOBAL: "Global",
5
+ ENTITY: "Catalog entity",
6
+ };
7
+ const WORKFLOW_EXECUTION_TYPE_LABELS = {
8
+ SIMPLE: "Simple",
9
+ EVENT_DRIVEN: "Event-driven",
10
+ };
11
+ const WORKFLOW_TRIGGER_TYPE_LABELS = {
12
+ ANY: "All users",
13
+ APPROVAL: "All users with approval",
14
+ RUNNERS: "Selected teams and users",
15
+ ENTITY: "Entity owners only",
16
+ };
17
+ const WORKFLOW_TRIGGER_TYPE_DESCRIPTIONS = {
18
+ ANY: "All users can trigger this workflow run without approval.",
19
+ APPROVAL: "All users can request a workflow run, but a designated approver must accept before it runs.",
20
+ RUNNERS: "Only selected teams or users can trigger a workflow run.",
21
+ ENTITY: "Only users who directly own the entity, or belong to an owning team, can trigger a run.",
22
+ };
23
+ function workflowScopeLabel(scope) {
24
+ return WORKFLOW_SCOPE_LABELS[scope] ?? scope;
25
+ }
26
+ function workflowExecutionTypeLabel(executionType) {
27
+ return WORKFLOW_EXECUTION_TYPE_LABELS[executionType] ?? executionType;
28
+ }
29
+ function workflowTriggerTypeLabel(triggerType) {
30
+ return WORKFLOW_TRIGGER_TYPE_LABELS[triggerType] ?? triggerType;
31
+ }
32
+ function workflowTriggerTypeDescription(triggerType) {
33
+ return WORKFLOW_TRIGGER_TYPE_DESCRIPTIONS[triggerType] ?? "";
34
+ }
35
+ export function renderWorkflowList(workflows) {
36
+ const blocks = [
37
+ ui.h1("Workflows"),
38
+ ui.p(`Displaying ${ui.bold(workflows.length.toString())} workflows.`),
39
+ ];
40
+ for (const workflow of workflows) {
41
+ blocks.push(ui.h2(`${workflow.name} (${ui.code(workflow.identifier)})`));
42
+ blocks.push(ui.h3("Core attributes"));
43
+ blocks.push(...coreContent(workflow));
44
+ if (workflow.scope === "ENTITY") {
45
+ blocks.push(ui.h3("Entity filter"));
46
+ blocks.push(...entityFilterContent(workflow));
47
+ }
48
+ blocks.push(ui.h3("Parameters"));
49
+ blocks.push(...parametersContent(workflow));
50
+ }
51
+ renderRichText(blocks);
52
+ }
53
+ function coreContent(workflow) {
54
+ const blocks = [];
55
+ const description = workflow.description?.trim()
56
+ ? workflow.description
57
+ : ui.dim("(None)");
58
+ blocks.push(ui.dl([
59
+ ui.dli("Description", description),
60
+ ui.dli("Scope", workflowScopeLabel(workflow.scope)),
61
+ ui.dli("Execution type", workflowExecutionTypeLabel(workflow.execution_type)),
62
+ ui.dli("Who can run", `${workflowTriggerTypeLabel(workflow.trigger_type)} ${ui.dim(workflowTriggerTypeDescription(workflow.trigger_type))}`),
63
+ ], { termWidth: 16 }));
64
+ return blocks;
65
+ }
66
+ function entityFilterContent(workflow) {
67
+ const blocks = [];
68
+ if (workflow.entity_filter_type === "ENTITY_TYPES") {
69
+ blocks.push(ui.p(`Filter type: ${ui.code("Entity types")}`));
70
+ if (workflow.entity_filter_type_identifiers &&
71
+ workflow.entity_filter_type_identifiers.length > 0) {
72
+ blocks.push(ui.ul(workflow.entity_filter_type_identifiers.map((identifier) => ui.li([ui.p(ui.code(identifier), false)]))));
73
+ }
74
+ }
75
+ else if (workflow.entity_filter_type === "SQL") {
76
+ blocks.push(ui.p(`Filter type: ${ui.code("SQL")}`));
77
+ if (workflow.entity_filter_sql) {
78
+ blocks.push(ui.truncatedSqlBlock(workflow.entity_filter_sql));
79
+ }
80
+ }
81
+ return blocks;
82
+ }
83
+ function parametersContent(workflow) {
84
+ if (workflow.parameters.length === 0) {
85
+ return [ui.p(ui.dim("(No parameters)"))];
86
+ }
87
+ return [
88
+ ui.ul(workflow.parameters.map((parameter) => ui.li(singleParameterContent(parameter)))),
89
+ ];
90
+ }
91
+ function singleParameterContent(parameter) {
92
+ const requiredPart = parameter.is_required ? ` ${ui.dim("required")}` : "";
93
+ const headline = `${ui.bold(parameter.name)} (${ui.code(parameter.identifier)}) ${ui.code(parameter.type)}${requiredPart}`;
94
+ const blocks = [ui.p(headline, false)];
95
+ const desc = parameter.description?.trim();
96
+ if (desc) {
97
+ blocks.push(ui.p(desc, false));
98
+ }
99
+ if (parameter.default_value !== null &&
100
+ parameter.default_value !== undefined &&
101
+ parameter.default_value !== "") {
102
+ blocks.push(ui.p(`Default: ${ui.code(formatParameterDefault(parameter.default_value))}`, false));
103
+ }
104
+ if (parameter.type === "SELECT") {
105
+ const options = selectParameterOptions(parameter.definition);
106
+ if (options.length > 0) {
107
+ blocks.push(ui.p("Options:", false));
108
+ blocks.push(ui.ul(options.map((value) => ui.li([ui.p(ui.code(value), false)]))));
109
+ }
110
+ }
111
+ return blocks;
112
+ }
113
+ function selectParameterOptions(definition) {
114
+ if (!definition || typeof definition !== "object") {
115
+ return [];
116
+ }
117
+ const raw = definition.options;
118
+ if (!Array.isArray(raw)) {
119
+ return [];
120
+ }
121
+ return raw.filter((item) => typeof item === "string");
122
+ }
123
+ function formatParameterDefault(value) {
124
+ if (typeof value === "string") {
125
+ return value;
126
+ }
127
+ if (typeof value === "number" || typeof value === "boolean") {
128
+ return String(value);
129
+ }
130
+ try {
131
+ return JSON.stringify(value);
132
+ }
133
+ catch {
134
+ return String(value);
135
+ }
136
+ }