@get-dx/cli 0.3.2 → 0.3.4
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/assets.js +15 -0
- package/dist/cli.js +2 -0
- package/dist/commands/auth.js +38 -2
- package/dist/commands/catalog/entities.js +1 -1
- package/dist/commands/init.js +18 -17
- package/dist/commands/teams.js +1 -1
- package/dist/commands/workflowRuns/addLink.js +46 -0
- package/dist/commands/workflowRuns/changeStatus.js +53 -0
- package/dist/commands/workflowRuns/events.js +58 -0
- package/dist/commands/workflowRuns/info.js +82 -0
- package/dist/commands/workflowRuns/parameters.js +163 -0
- package/dist/commands/workflowRuns/postMessage.js +46 -0
- package/dist/commands/workflowRuns/trigger.js +286 -0
- package/dist/commands/workflowRuns.js +17 -0
- package/dist/commands/workflows.js +1 -1
- package/dist/ui/blocks.js +5 -3
- package/dist/ui.js +10 -0
- package/dist/welcomeAnimation.js +115 -0
- package/package.json +1 -1
package/dist/assets.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
let assets = null;
|
|
3
|
+
export function getAssets() {
|
|
4
|
+
if (assets === null) {
|
|
5
|
+
assets = {
|
|
6
|
+
logoD: loadAsset("logo-d.txt"),
|
|
7
|
+
logoX: loadAsset("logo-x.txt"),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
return assets;
|
|
11
|
+
}
|
|
12
|
+
function loadAsset(filename) {
|
|
13
|
+
const filePath = new URL(`../assets/${filename}`, import.meta.url);
|
|
14
|
+
return fs.readFileSync(filePath, "utf8");
|
|
15
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { scorecardsCommand } from "./commands/scorecards.js";
|
|
|
6
6
|
import { snapshotsCommand } from "./commands/snapshots.js";
|
|
7
7
|
import { studioCommand } from "./commands/studio.js";
|
|
8
8
|
import { teamsCommand } from "./commands/teams.js";
|
|
9
|
+
import { workflowRunsCommand } from "./commands/workflowRuns.js";
|
|
9
10
|
import { workflowsCommand } from "./commands/workflows.js";
|
|
10
11
|
import { handleError } from "./commandHelpers.js";
|
|
11
12
|
import cliPackage from "../package.json" with { type: "json" };
|
|
@@ -40,6 +41,7 @@ function createProgram() {
|
|
|
40
41
|
program.addCommand(snapshotsCommand());
|
|
41
42
|
program.addCommand(studioCommand());
|
|
42
43
|
program.addCommand(teamsCommand());
|
|
44
|
+
program.addCommand(workflowRunsCommand());
|
|
43
45
|
program.addCommand(workflowsCommand());
|
|
44
46
|
applyExitOverride(program);
|
|
45
47
|
return program;
|
package/dist/commands/auth.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from "commander";
|
|
|
3
3
|
import { deleteToken, setToken } from "../secrets.js";
|
|
4
4
|
import { renderJson } from "../renderers.js";
|
|
5
5
|
import { renderAuthInfo, renderLoggedOut } from "./authRendering.js";
|
|
6
|
-
import { getContext, wrapAction } from "../commandHelpers.js";
|
|
6
|
+
import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
|
|
7
7
|
import { persistBaseUrls, resolveApiBaseUrl, deriveBaseUrlsFromEnv, } from "../config.js";
|
|
8
8
|
import { CliError } from "../errors.js";
|
|
9
9
|
import { request } from "../http.js";
|
|
@@ -17,6 +17,20 @@ export function authCommand() {
|
|
|
17
17
|
auth
|
|
18
18
|
.command("login")
|
|
19
19
|
.option("--token <token>", "Account web API token or personal access token. Omit this option to login interactively by web browser or pasting.")
|
|
20
|
+
.addHelpText("afterAll", createExampleText([
|
|
21
|
+
{
|
|
22
|
+
label: "Log in interactively via browser or by pasting a token",
|
|
23
|
+
command: "dx auth login",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: "Log in non-interactively with a token",
|
|
27
|
+
command: "dx auth login --token <token>",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: "Log in to a managed deployment with custom base URLs",
|
|
31
|
+
command: "DX_API_BASE_URL=https://api.dx.example.com DX_WEB_BASE_URL=https://dx.example.com dx auth login --token <token>",
|
|
32
|
+
},
|
|
33
|
+
]))
|
|
20
34
|
.action(wrapAction(async (commandOptions, command) => {
|
|
21
35
|
const context = getContext(command);
|
|
22
36
|
const { apiBaseUrl, webBaseUrl } = deriveBaseUrlsFromEnv();
|
|
@@ -63,7 +77,15 @@ export function authCommand() {
|
|
|
63
77
|
}
|
|
64
78
|
renderAuthInfo(response, token, apiBaseUrl);
|
|
65
79
|
}));
|
|
66
|
-
auth
|
|
80
|
+
auth
|
|
81
|
+
.command("logout")
|
|
82
|
+
.addHelpText("afterAll", createExampleText([
|
|
83
|
+
{
|
|
84
|
+
label: "Log out and remove the stored token",
|
|
85
|
+
command: "dx auth logout",
|
|
86
|
+
},
|
|
87
|
+
]))
|
|
88
|
+
.action(wrapAction(async (_options, command) => {
|
|
67
89
|
const context = getContext(command);
|
|
68
90
|
const apiBaseUrl = resolveApiBaseUrl();
|
|
69
91
|
deleteToken(apiBaseUrl);
|
|
@@ -77,6 +99,20 @@ export function authCommand() {
|
|
|
77
99
|
auth
|
|
78
100
|
.command("status")
|
|
79
101
|
.alias("info")
|
|
102
|
+
.addHelpText("afterAll", createExampleText([
|
|
103
|
+
{
|
|
104
|
+
label: "Show the currently authenticated user and token details",
|
|
105
|
+
command: "dx auth status",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
label: "Return auth details as JSON",
|
|
109
|
+
command: "dx auth status --json",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
label: "Pipe auth details into `jq` and list scopes",
|
|
113
|
+
command: "dx auth status --json | jq --raw-output '.auth.scopes[]'",
|
|
114
|
+
},
|
|
115
|
+
]))
|
|
80
116
|
.action(wrapAction(async (_options, command) => {
|
|
81
117
|
const runtime = buildRuntime(getContext(command));
|
|
82
118
|
const response = await getAuthInfo(runtime);
|
|
@@ -497,7 +497,7 @@ function parseEntityAliases(entityTypeIdentifier, rawAliases, knownAliases) {
|
|
|
497
497
|
result[key] = [];
|
|
498
498
|
continue;
|
|
499
499
|
}
|
|
500
|
-
result[key] = [...(result[key] ?? []), {
|
|
500
|
+
result[key] = [...(result[key] ?? []), { lookup: rawValue }];
|
|
501
501
|
}
|
|
502
502
|
return result;
|
|
503
503
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -8,34 +8,35 @@ 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";
|
|
11
|
-
import { getContext } from "../commandHelpers.js";
|
|
12
|
-
import { wrapAction } from "../commandHelpers.js";
|
|
11
|
+
import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
|
|
13
12
|
import { renderRichText } from "../renderers.js";
|
|
14
13
|
import * as ui from "../ui.js";
|
|
15
14
|
import { CliError } from "../errors.js";
|
|
16
15
|
import { deriveBaseUrlsFromEnv, persistBaseUrls } from "../config.js";
|
|
17
16
|
import { setToken } from "../secrets.js";
|
|
18
|
-
|
|
19
|
-
// Choose a more interesting ASCII art font from https://patorjk.com/software/taag/
|
|
20
|
-
const WELCOME_BANNER = ui.indent(`\
|
|
21
|
-
▄▄▄▄▄ ▄▄▄ ▄▄▄
|
|
22
|
-
██▀▀▀██ ██▄▄██
|
|
23
|
-
██ ██ ████
|
|
24
|
-
██ ██ ██
|
|
25
|
-
██ ██ ████
|
|
26
|
-
██▄▄▄██ ██ ██
|
|
27
|
-
▀▀▀▀▀ ▀▀▀ ▀▀▀
|
|
28
|
-
`, 2);
|
|
17
|
+
import { renderWelcomeAnimation } from "../welcomeAnimation.js";
|
|
29
18
|
export function initCommand() {
|
|
30
19
|
const init = new Command()
|
|
31
20
|
.name("init")
|
|
32
21
|
.description("Initialize the DX CLI")
|
|
22
|
+
.addHelpText("afterAll", createExampleText([
|
|
23
|
+
{
|
|
24
|
+
label: "Initialize the CLI",
|
|
25
|
+
command: "dx init",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "Initialize the CLI for a managed deployment with custom base URLs",
|
|
29
|
+
command: "DX_API_BASE_URL=https://api.dx.example.com DX_WEB_BASE_URL=https://dx.example.com dx init",
|
|
30
|
+
},
|
|
31
|
+
]))
|
|
33
32
|
.action(wrapAction(async (commandOptions, command) => {
|
|
34
33
|
const context = getContext(command);
|
|
35
34
|
let runtime = buildRuntimeSafe(context);
|
|
36
35
|
ensureInteractive();
|
|
37
|
-
|
|
36
|
+
await renderWelcomeAnimation();
|
|
38
37
|
runtime = await ensureLoggedIn(context, runtime);
|
|
38
|
+
// Sleep for a bit to help with visual chunking
|
|
39
|
+
await sleep(500);
|
|
39
40
|
await optionallySetupSkill(runtime);
|
|
40
41
|
renderRichText([
|
|
41
42
|
ui.blankLine(),
|
|
@@ -49,9 +50,6 @@ function ensureInteractive() {
|
|
|
49
50
|
throw new CliError("`dx init` must be run interactively");
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
function showWelcomeBanner() {
|
|
53
|
-
renderRichText([ui.p(ui.success(WELCOME_BANNER))]);
|
|
54
|
-
}
|
|
55
53
|
async function ensureLoggedIn(context, runtime) {
|
|
56
54
|
renderRichText([ui.h1(`Checking if you are logged in...`), ui.blankLine()]);
|
|
57
55
|
if (runtime) {
|
|
@@ -155,3 +153,6 @@ async function optionallySetupSkill(runtime) {
|
|
|
155
153
|
throw new CliError(`Failed to setup the DX skill: ${result.stderr}`);
|
|
156
154
|
}
|
|
157
155
|
}
|
|
156
|
+
async function sleep(ms) {
|
|
157
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
158
|
+
}
|
package/dist/commands/teams.js
CHANGED
|
@@ -109,7 +109,7 @@ async function getTeamInfo(runtime, lookup) {
|
|
|
109
109
|
});
|
|
110
110
|
return response.body;
|
|
111
111
|
}
|
|
112
|
-
async function listTeams(runtime) {
|
|
112
|
+
export async function listTeams(runtime) {
|
|
113
113
|
const response = await request(runtime, "/teams.list", {
|
|
114
114
|
method: "GET",
|
|
115
115
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, wrapAction, } from "../../commandHelpers.js";
|
|
3
|
+
import { buildRuntime } from "../../runtime.js";
|
|
4
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
5
|
+
import * as ui from "../../ui.js";
|
|
6
|
+
import { request } from "../../http.js";
|
|
7
|
+
export function addLinkCommand() {
|
|
8
|
+
return new Command()
|
|
9
|
+
.name("addLink")
|
|
10
|
+
.description("Add a link to a workflow run")
|
|
11
|
+
.argument("<workflow-run-id>", "The ID of the workflow run")
|
|
12
|
+
.requiredOption("--url <url>", "The URL of the link")
|
|
13
|
+
.requiredOption("--label <label>", "The label of the link")
|
|
14
|
+
.addHelpText("afterAll", createExampleText([
|
|
15
|
+
{
|
|
16
|
+
label: "Add a link to a workflow run",
|
|
17
|
+
command: 'dx workflowRuns addLink hvserjgz5lo7 --url https://www.example.com --label "Example Website"',
|
|
18
|
+
},
|
|
19
|
+
]))
|
|
20
|
+
.action(wrapAction(async (workflowRunId, options, command) => {
|
|
21
|
+
const context = getContext(command);
|
|
22
|
+
const runtime = buildRuntime(context);
|
|
23
|
+
const response = await addLink(runtime, workflowRunId, options.url, options.label);
|
|
24
|
+
if (runtime.context.json) {
|
|
25
|
+
renderJson(response);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
renderRichText([
|
|
29
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Added link to workflow run ${ui.code(workflowRunId)}.`),
|
|
30
|
+
ui.p(`Web link: ${ui.link(ui.webLink(`/self-service/workflow-runs/${workflowRunId}`, runtime))}`),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
async function addLink(runtime, workflowRunId, url, label) {
|
|
36
|
+
return request(runtime, "/workflowRuns.addLink", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
body: {
|
|
39
|
+
workflow_run_id: workflowRunId,
|
|
40
|
+
link: {
|
|
41
|
+
url,
|
|
42
|
+
label,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, wrapAction, } from "../../commandHelpers.js";
|
|
3
|
+
import { buildRuntime } from "../../runtime.js";
|
|
4
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
5
|
+
import * as ui from "../../ui.js";
|
|
6
|
+
import { request } from "../../http.js";
|
|
7
|
+
import { CliError, EXIT_CODES } from "../../errors.js";
|
|
8
|
+
export function changeStatusCommand() {
|
|
9
|
+
return new Command()
|
|
10
|
+
.name("changeStatus")
|
|
11
|
+
.description("Change the status of a workflow run")
|
|
12
|
+
.argument("<workflow-run-id>", "The ID of the workflow run")
|
|
13
|
+
.requiredOption("--status <updated-status>", "The status to change to")
|
|
14
|
+
.addHelpText("afterAll", createExampleText([
|
|
15
|
+
{
|
|
16
|
+
label: "Mark a workflow run as succeeded",
|
|
17
|
+
command: "dx workflowRuns changeStatus hvserjgz5lo7 --status SUCCEEDED",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "Mark a workflow run as failed",
|
|
21
|
+
command: "dx workflowRuns changeStatus hvserjgz5lo7 --status FAILED",
|
|
22
|
+
},
|
|
23
|
+
]))
|
|
24
|
+
.action(wrapAction(async (workflowRunId, options, command) => {
|
|
25
|
+
const context = getContext(command);
|
|
26
|
+
const runtime = buildRuntime(context);
|
|
27
|
+
if (!isValidStatus(options.status)) {
|
|
28
|
+
throw new CliError(`Invalid status: ${options.status}. Must be one of: SUCCEEDED, FAILED.`, EXIT_CODES.ARGUMENT_ERROR);
|
|
29
|
+
}
|
|
30
|
+
const response = await changeStatus(runtime, workflowRunId, options.status);
|
|
31
|
+
if (runtime.context.json) {
|
|
32
|
+
renderJson(response);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
renderRichText([
|
|
36
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Changed status of workflow run ${ui.code(workflowRunId)} to ${ui.code(options.status)}.`),
|
|
37
|
+
ui.p(`Web link: ${ui.link(ui.webLink(`/self-service/workflow-runs/${workflowRunId}`, runtime))}`),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
function isValidStatus(updatedStatus) {
|
|
43
|
+
return updatedStatus === "SUCCEEDED" || updatedStatus === "FAILED";
|
|
44
|
+
}
|
|
45
|
+
async function changeStatus(runtime, workflowRunId, status) {
|
|
46
|
+
return request(runtime, "/workflowRuns.changeStatus", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: {
|
|
49
|
+
workflow_run_id: workflowRunId,
|
|
50
|
+
status,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as ui from "../../ui.js";
|
|
2
|
+
export function workflowRunEventContent(event) {
|
|
3
|
+
const ts = ui.dim(event.occurred_at);
|
|
4
|
+
const message = formatEventMessage(event);
|
|
5
|
+
return ui.p(`${ts} ${message}`, false);
|
|
6
|
+
}
|
|
7
|
+
function formatEventMessage(event) {
|
|
8
|
+
const userName = event.user?.name ?? event.user?.email ?? "API";
|
|
9
|
+
switch (event.type) {
|
|
10
|
+
case "WORKFLOW_TRIGGERED":
|
|
11
|
+
return `Workflow run triggered by ${ui.bold(userName)}`;
|
|
12
|
+
case "WORKFLOW_RUN_REQUESTED":
|
|
13
|
+
return `Workflow run requested by ${ui.bold(userName)}`;
|
|
14
|
+
case "APPROVERS_NOTIFIED":
|
|
15
|
+
return "Approvers notified";
|
|
16
|
+
case "APPROVAL_PERFORMED":
|
|
17
|
+
return `Approved by ${ui.bold(userName)}`;
|
|
18
|
+
case "REJECTION_PERFORMED":
|
|
19
|
+
return `Rejected by ${ui.bold(userName)}`;
|
|
20
|
+
case "HTTP_REQUEST_COMPLETED": {
|
|
21
|
+
const method = event.data?.request?.method?.toUpperCase();
|
|
22
|
+
const url = event.data?.request?.url;
|
|
23
|
+
const status = event.data?.response?.status;
|
|
24
|
+
const reqSummary = method && url ? ` ${ui.code(`${method} ${url}`)}` : "";
|
|
25
|
+
const statusSummary = status ? ` → ${ui.code(String(status))}` : "";
|
|
26
|
+
return `Sent HTTP request${reqSummary}${statusSummary}`;
|
|
27
|
+
}
|
|
28
|
+
case "POST_MESSAGE": {
|
|
29
|
+
const text = event.message ?? "";
|
|
30
|
+
const truncated = text.length > 120 ? text.slice(0, 120) + "…" : text;
|
|
31
|
+
return truncated;
|
|
32
|
+
}
|
|
33
|
+
case "ADD_LINK": {
|
|
34
|
+
const link = event.data?.link;
|
|
35
|
+
if (link) {
|
|
36
|
+
const label = link.label ? `${link.label}: ` : "";
|
|
37
|
+
return `Added link ${label}${ui.link(link.url)}`;
|
|
38
|
+
}
|
|
39
|
+
return "Added a link";
|
|
40
|
+
}
|
|
41
|
+
case "CHANGE_STATUS": {
|
|
42
|
+
const status = event.data?.status;
|
|
43
|
+
return status
|
|
44
|
+
? `Status changed to ${ui.bold(status.toLowerCase())}`
|
|
45
|
+
: "Status changed";
|
|
46
|
+
}
|
|
47
|
+
case "WORKFLOW_SUCCEEDED":
|
|
48
|
+
return `${ui.success(ui.GLYPHS.CHECK)} Workflow run succeeded`;
|
|
49
|
+
case "WORKFLOW_FAILED":
|
|
50
|
+
return `${ui.error(ui.GLYPHS.ERROR)} Workflow run failed`;
|
|
51
|
+
case "WORKFLOW_TIMEOUT":
|
|
52
|
+
return `${ui.error(ui.GLYPHS.ERROR)} Workflow run timed out`;
|
|
53
|
+
case "WORKFLOW_CANCELLED":
|
|
54
|
+
return `${ui.warning(ui.GLYPHS.WARNING)} Workflow run cancelled by ${ui.bold(userName)}`;
|
|
55
|
+
default:
|
|
56
|
+
return `Event: ${event.type}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, wrapAction, } from "../../commandHelpers.js";
|
|
3
|
+
import { buildRuntime } from "../../runtime.js";
|
|
4
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
5
|
+
import * as ui from "../../ui.js";
|
|
6
|
+
import { request } from "../../http.js";
|
|
7
|
+
import { workflowRunEventContent } from "./events.js";
|
|
8
|
+
export function infoCommand() {
|
|
9
|
+
return new Command()
|
|
10
|
+
.name("info")
|
|
11
|
+
.description("Get info for a workflow run")
|
|
12
|
+
.argument("<workflow-run-id>", "The ID of the workflow run")
|
|
13
|
+
.addHelpText("afterAll", createExampleText([
|
|
14
|
+
{
|
|
15
|
+
label: "Get info for a workflow run",
|
|
16
|
+
command: "dx workflowRuns info hvserjgz5lo7",
|
|
17
|
+
},
|
|
18
|
+
]))
|
|
19
|
+
.action(wrapAction(async (workflowRunId, options, command) => {
|
|
20
|
+
const context = getContext(command);
|
|
21
|
+
const runtime = buildRuntime(context);
|
|
22
|
+
const infoResponse = await getWorkflowRun(runtime, workflowRunId);
|
|
23
|
+
const workflowRun = infoResponse.body.workflow_run;
|
|
24
|
+
if (runtime.context.json) {
|
|
25
|
+
renderJson({ ok: true, workflow_run: workflowRun });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
renderWorkflowRunInfo(workflowRun, runtime);
|
|
29
|
+
}
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function renderWorkflowRunInfo(run, runtime) {
|
|
33
|
+
renderRichText([
|
|
34
|
+
ui.h2("Workflow run"),
|
|
35
|
+
ui.h3("Summary"),
|
|
36
|
+
...summaryContent(run, runtime),
|
|
37
|
+
ui.h3("Events"),
|
|
38
|
+
...eventsContent(run),
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
export function renderWorkflowRunSummary(run, runtime) {
|
|
42
|
+
renderRichText([ui.h3("Workflow run"), ...summaryContent(run, runtime)]);
|
|
43
|
+
}
|
|
44
|
+
function summaryContent(run, runtime) {
|
|
45
|
+
const items = [
|
|
46
|
+
ui.dli("Run ID", ui.code(run.id)),
|
|
47
|
+
ui.dli("Status", run.status),
|
|
48
|
+
];
|
|
49
|
+
if (run.workflow) {
|
|
50
|
+
items.push(ui.dli("Workflow", `${run.workflow.name} (${ui.code(run.workflow.identifier)})`));
|
|
51
|
+
items.push(ui.dli("Web link", ui.link(ui.webLink(`/self-service/workflow-runs/${run.id}`, runtime))));
|
|
52
|
+
}
|
|
53
|
+
if (run.entity) {
|
|
54
|
+
items.push(ui.dli("Entity", `${run.entity.name} (${run.entity.identifier})`));
|
|
55
|
+
}
|
|
56
|
+
if (run.started_at) {
|
|
57
|
+
items.push(ui.dli("Started", ui.timestampSummary(run.started_at)));
|
|
58
|
+
}
|
|
59
|
+
if (run.started_at && run.completed_at) {
|
|
60
|
+
items.push(ui.dli("Completed", ui.timestampSummary(run.completed_at)));
|
|
61
|
+
items.push(ui.dli("Duration", ui.durationSummary(run.started_at, run.completed_at)));
|
|
62
|
+
}
|
|
63
|
+
if (run.links?.length) {
|
|
64
|
+
const linkSummaries = run.links.map((l) => ui.p(l.label ? `${ui.bold(l.label)} ${ui.link(l.url)}` : ui.link(l.url), false));
|
|
65
|
+
items.push(ui.dli("Links", linkSummaries));
|
|
66
|
+
}
|
|
67
|
+
return [ui.dl(items, { termWidth: 14 })];
|
|
68
|
+
}
|
|
69
|
+
function eventsContent(run) {
|
|
70
|
+
const items = [];
|
|
71
|
+
for (const event of run.events ?? []) {
|
|
72
|
+
items.push(workflowRunEventContent(event));
|
|
73
|
+
}
|
|
74
|
+
items.push(ui.blankLine());
|
|
75
|
+
return items;
|
|
76
|
+
}
|
|
77
|
+
export async function getWorkflowRun(runtime, id) {
|
|
78
|
+
return request(runtime, "/workflowRuns.info", {
|
|
79
|
+
method: "GET",
|
|
80
|
+
query: { id },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { input, select, confirm, search } from "@inquirer/prompts";
|
|
2
|
+
import { listTeams } from "../teams.js";
|
|
3
|
+
import { CliError, EXIT_CODES } from "../../errors.js";
|
|
4
|
+
import { renderRichText } from "../../renderers.js";
|
|
5
|
+
import * as ui from "../../ui.js";
|
|
6
|
+
export async function promptForParameterValue(runtime, param) {
|
|
7
|
+
const message = param.description
|
|
8
|
+
? `${param.name}: ${param.description}`
|
|
9
|
+
: param.name;
|
|
10
|
+
switch (param.type) {
|
|
11
|
+
case "STRING": {
|
|
12
|
+
const rawValue = await input({
|
|
13
|
+
message,
|
|
14
|
+
default: param.default_value
|
|
15
|
+
? param.default_value
|
|
16
|
+
: undefined,
|
|
17
|
+
validate: (value) => {
|
|
18
|
+
if (value === "" && !param.is_required) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
else if (value === "") {
|
|
22
|
+
return "Value is required";
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return rawValue === "" ? undefined : rawValue;
|
|
28
|
+
}
|
|
29
|
+
case "INTEGER": {
|
|
30
|
+
const rawValue = await input({
|
|
31
|
+
message,
|
|
32
|
+
default: param.default_value
|
|
33
|
+
? param.default_value
|
|
34
|
+
: undefined,
|
|
35
|
+
validate: (value) => {
|
|
36
|
+
if (value === "" && !param.is_required) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
else if (value === "") {
|
|
40
|
+
return "Value is required";
|
|
41
|
+
}
|
|
42
|
+
const num = Number(value);
|
|
43
|
+
if (isNaN(num)) {
|
|
44
|
+
return "Value must be a number";
|
|
45
|
+
}
|
|
46
|
+
else if (num % 1 !== 0) {
|
|
47
|
+
return "Value must be an integer";
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return rawValue === "" ? undefined : Number(rawValue);
|
|
55
|
+
}
|
|
56
|
+
case "FLOAT": {
|
|
57
|
+
const rawValue = await input({
|
|
58
|
+
message,
|
|
59
|
+
default: param.default_value
|
|
60
|
+
? param.default_value
|
|
61
|
+
: undefined,
|
|
62
|
+
validate: (value) => {
|
|
63
|
+
if (value === "" && !param.is_required) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
else if (value === "") {
|
|
67
|
+
return "Value is required";
|
|
68
|
+
}
|
|
69
|
+
const num = Number(value);
|
|
70
|
+
if (isNaN(num)) {
|
|
71
|
+
return "Value must be a number";
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return rawValue === "" ? undefined : Number(rawValue);
|
|
79
|
+
}
|
|
80
|
+
case "BOOLEAN":
|
|
81
|
+
return await confirm({
|
|
82
|
+
message,
|
|
83
|
+
default: false,
|
|
84
|
+
});
|
|
85
|
+
case "SELECT": {
|
|
86
|
+
return await select({
|
|
87
|
+
message,
|
|
88
|
+
default: param.default_value
|
|
89
|
+
? param.default_value
|
|
90
|
+
: undefined,
|
|
91
|
+
choices: param.definition.options.map((option) => ({
|
|
92
|
+
name: option,
|
|
93
|
+
value: option,
|
|
94
|
+
})),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
case "USER":
|
|
98
|
+
throw new CliError("User-based parameters are not yet supported in the CLI", EXIT_CODES.ARGUMENT_ERROR);
|
|
99
|
+
case "TEAM": {
|
|
100
|
+
renderRichText([ui.p("Fetching teams...")]);
|
|
101
|
+
const teamsResponse = await listTeams(runtime);
|
|
102
|
+
const teams = teamsResponse.teams;
|
|
103
|
+
const teamId = await search({
|
|
104
|
+
message,
|
|
105
|
+
source: (term) => teams
|
|
106
|
+
.filter((team) => term === undefined ||
|
|
107
|
+
team.name.toLowerCase().includes(term.toLowerCase()))
|
|
108
|
+
.map((team) => ({
|
|
109
|
+
name: team.name,
|
|
110
|
+
value: team.id,
|
|
111
|
+
})),
|
|
112
|
+
});
|
|
113
|
+
// TODO: change the trigger endpoint to accept encoded IDs too
|
|
114
|
+
const decodedTeamId = decodeBase64(teamId);
|
|
115
|
+
return decodedTeamId;
|
|
116
|
+
}
|
|
117
|
+
case "EMAIL": {
|
|
118
|
+
const rawValue = await input({
|
|
119
|
+
message,
|
|
120
|
+
default: param.default_value
|
|
121
|
+
? param.default_value
|
|
122
|
+
: undefined,
|
|
123
|
+
validate: (value) => {
|
|
124
|
+
if (value === "" && !param.is_required) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
else if (value === "") {
|
|
128
|
+
return "Value is required";
|
|
129
|
+
}
|
|
130
|
+
else if (!isValidEmail(value)) {
|
|
131
|
+
return "Value must be a valid email address";
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
return rawValue === "" ? undefined : rawValue;
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
throw new CliError(`Unknown parameter type: ${param.type}`, EXIT_CODES.ARGUMENT_ERROR);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export function renderParameterData(parameters, data) {
|
|
143
|
+
const items = [];
|
|
144
|
+
for (const parameter of parameters) {
|
|
145
|
+
items.push(ui.p(parameter.description
|
|
146
|
+
? `${ui.bold(parameter.name)} ${ui.dim(parameter.description)}`
|
|
147
|
+
: ui.bold(parameter.name), false));
|
|
148
|
+
if (data[parameter.identifier] !== undefined) {
|
|
149
|
+
items.push(ui.p(` ${ui.code(String(data[parameter.identifier]))}`));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
items.push(ui.p(` ${ui.dim("(none)")}`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
items.push(ui.blankLine());
|
|
156
|
+
renderRichText(items, { useStderr: true });
|
|
157
|
+
}
|
|
158
|
+
function decodeBase64(value) {
|
|
159
|
+
return Buffer.from(value, "base64").toString("utf-8");
|
|
160
|
+
}
|
|
161
|
+
function isValidEmail(value) {
|
|
162
|
+
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
|
|
163
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, wrapAction, } from "../../commandHelpers.js";
|
|
3
|
+
import { buildRuntime } from "../../runtime.js";
|
|
4
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
5
|
+
import * as ui from "../../ui.js";
|
|
6
|
+
import { request } from "../../http.js";
|
|
7
|
+
export function postMessageCommand() {
|
|
8
|
+
return new Command()
|
|
9
|
+
.name("postMessage")
|
|
10
|
+
.description("Post a message to a workflow run")
|
|
11
|
+
.argument("<workflow-run-id>", "The ID of the workflow run")
|
|
12
|
+
.requiredOption("--message <message>", "The message to post")
|
|
13
|
+
.addHelpText("afterAll", createExampleText([
|
|
14
|
+
{
|
|
15
|
+
label: "Post a message to a workflow run",
|
|
16
|
+
command: 'dx workflowRuns postMessage hvserjgz5lo7 --message "Hello from the CLI"',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: "Post the contents of a markdown file to a workflow run",
|
|
20
|
+
command: 'dx workflowRuns postMessage hvserjgz5lo7 --message "$(cat message.md)"',
|
|
21
|
+
},
|
|
22
|
+
]))
|
|
23
|
+
.action(wrapAction(async (workflowRunId, options, command) => {
|
|
24
|
+
const context = getContext(command);
|
|
25
|
+
const runtime = buildRuntime(context);
|
|
26
|
+
const response = await postMessage(runtime, workflowRunId, options.message);
|
|
27
|
+
if (runtime.context.json) {
|
|
28
|
+
renderJson(response);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
renderRichText([
|
|
32
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Posted message to workflow run ${ui.code(workflowRunId)}.`),
|
|
33
|
+
ui.p(`Web link: ${ui.link(ui.webLink(`/self-service/workflow-runs/${workflowRunId}`, runtime))}`),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
async function postMessage(runtime, workflowRunId, message) {
|
|
39
|
+
return request(runtime, "/workflowRuns.postMessage", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: {
|
|
42
|
+
workflow_run_id: workflowRunId,
|
|
43
|
+
message,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { confirm } from "@inquirer/prompts";
|
|
3
|
+
import { createExampleText, getContext, wrapAction, } from "../../commandHelpers.js";
|
|
4
|
+
import { CliError, EXIT_CODES, HttpError } from "../../errors.js";
|
|
5
|
+
import { request } from "../../http.js";
|
|
6
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
7
|
+
import { buildRuntime } from "../../runtime.js";
|
|
8
|
+
import * as ui from "../../ui.js";
|
|
9
|
+
import { listWorkflows } from "../workflows.js";
|
|
10
|
+
import { promptForParameterValue, renderParameterData } from "./parameters.js";
|
|
11
|
+
import { getWorkflowRun, renderWorkflowRunSummary, } from "./info.js";
|
|
12
|
+
import { workflowRunEventContent } from "./events.js";
|
|
13
|
+
const POLL_INTERVAL_MS = 1000;
|
|
14
|
+
const PENDING_WORKFLOW_RUN_STATUSES = new Set([
|
|
15
|
+
"PENDING_RUN",
|
|
16
|
+
"IN_PROGRESS",
|
|
17
|
+
"WAITING_FOR_APPROVAL",
|
|
18
|
+
"WAITING_FOR_EVENTS",
|
|
19
|
+
]);
|
|
20
|
+
const TERMINAL_SUCCESS = new Set(["SUCCEEDED"]);
|
|
21
|
+
const TERMINAL_FAILURE = new Set([
|
|
22
|
+
"FAILED",
|
|
23
|
+
"REJECTED",
|
|
24
|
+
"TIMEOUT",
|
|
25
|
+
"CANCELLED",
|
|
26
|
+
]);
|
|
27
|
+
export function triggerCommand() {
|
|
28
|
+
return new Command()
|
|
29
|
+
.name("trigger")
|
|
30
|
+
.description("Trigger a Self-service workflow run and wait until it succeeds or fails. If the workflow requires approval, this command will submit a request and optionally notify approvers.")
|
|
31
|
+
.argument("<workflow-identifier>", "Workflow identifier (from dx workflows list)")
|
|
32
|
+
.option("--entity <identifier>", "Catalog entity identifier (required for entity-scoped workflows)")
|
|
33
|
+
.option("--param <key=value>", "Workflow parameter value (repeatable). Value after the first '=' is kept verbatim", (val, prev) => [...prev, val], [])
|
|
34
|
+
.option("--notify-approvers <boolean>", "For workflows that require approval, send a message to each approver requesting review")
|
|
35
|
+
.addHelpText("afterAll", createExampleText([
|
|
36
|
+
{
|
|
37
|
+
label: "Trigger a global workflow and wait for completion",
|
|
38
|
+
command: "dx workflowRuns trigger my-global-workflow",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: "Trigger with an entity and parameters",
|
|
42
|
+
command: 'dx workflowRuns trigger provision-db --entity acme-api --param size=small --param "greeting=hello world"',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "Trigger with machine-readable output",
|
|
46
|
+
command: "dx workflowRuns trigger my-workflow --json --entity svc-1 --param count=3",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "Request approval to trigger a workflow but do not notify approvers",
|
|
50
|
+
command: "dx workflowRuns trigger workflow-requiring-approval --notify-approvers false",
|
|
51
|
+
},
|
|
52
|
+
]))
|
|
53
|
+
.action(wrapAction(async (workflowIdentifier, options, command) => {
|
|
54
|
+
const context = getContext(command);
|
|
55
|
+
const runtime = buildRuntime(context);
|
|
56
|
+
// Fetch list of all workflows, find the one that applies
|
|
57
|
+
const workflowsResponse = await listWorkflows(runtime, {});
|
|
58
|
+
const workflow = workflowsResponse.workflows.find((w) => w.identifier === workflowIdentifier);
|
|
59
|
+
if (!workflow) {
|
|
60
|
+
throw new CliError(`Workflow \`${workflowIdentifier}\` not found`, EXIT_CODES.ARGUMENT_ERROR);
|
|
61
|
+
}
|
|
62
|
+
const requiresApproval = workflow.trigger_type === "APPROVAL";
|
|
63
|
+
const entityIdentifier = parseOptionalTrimmed(options.entity);
|
|
64
|
+
if (entityIdentifier === undefined && workflow.scope === "ENTITY") {
|
|
65
|
+
throw new CliError("--entity is required for entity-scoped workflows", EXIT_CODES.ARGUMENT_ERROR);
|
|
66
|
+
}
|
|
67
|
+
// Collect parameter data
|
|
68
|
+
const parameterData = parseParameterData(options.param);
|
|
69
|
+
if (isInteractive()) {
|
|
70
|
+
for (const param of workflow.parameters) {
|
|
71
|
+
if (parameterData[param.identifier] !== undefined) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
parameterData[param.identifier] = await promptForParameterValue(runtime, param);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let notifyApprovers = true;
|
|
78
|
+
if (requiresApproval && options.notifyApprovers !== undefined) {
|
|
79
|
+
notifyApprovers = options.notifyApprovers === "true";
|
|
80
|
+
}
|
|
81
|
+
else if (requiresApproval && isInteractive()) {
|
|
82
|
+
notifyApprovers = await confirm({
|
|
83
|
+
message: "Notify approvers?",
|
|
84
|
+
default: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (isInteractive()) {
|
|
88
|
+
// Confirm everything looks right
|
|
89
|
+
renderRichText([ui.blankLine(), ui.h3("Confirm"), ui.blankLine()], {
|
|
90
|
+
useStderr: true,
|
|
91
|
+
});
|
|
92
|
+
if (entityIdentifier !== undefined) {
|
|
93
|
+
renderRichText([
|
|
94
|
+
ui.p(`${ui.bold("Entity")}`, false),
|
|
95
|
+
ui.p(` ${ui.code(entityIdentifier)}`),
|
|
96
|
+
ui.blankLine(),
|
|
97
|
+
], {
|
|
98
|
+
useStderr: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (workflow.parameters.length > 0) {
|
|
102
|
+
renderParameterData(workflow.parameters, parameterData);
|
|
103
|
+
}
|
|
104
|
+
const confirmationMessage = requiresApproval
|
|
105
|
+
? "Request approval to trigger this workflow run with these settings?"
|
|
106
|
+
: "Trigger this workflow run with these settings?";
|
|
107
|
+
const isConfirmed = await confirm({
|
|
108
|
+
message: confirmationMessage,
|
|
109
|
+
default: true,
|
|
110
|
+
});
|
|
111
|
+
if (!isConfirmed) {
|
|
112
|
+
throw new CliError("Workflow run not confirmed", EXIT_CODES.RETRY_RECOMMENDED);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Create the request body
|
|
116
|
+
const triggerRequestBody = {
|
|
117
|
+
workflow_identifier: workflowIdentifier.trim(),
|
|
118
|
+
};
|
|
119
|
+
if (entityIdentifier !== undefined) {
|
|
120
|
+
triggerRequestBody.entity_identifier = entityIdentifier;
|
|
121
|
+
}
|
|
122
|
+
if (Object.keys(parameterData).length > 0) {
|
|
123
|
+
triggerRequestBody.data = parameterData;
|
|
124
|
+
}
|
|
125
|
+
if (requiresApproval) {
|
|
126
|
+
triggerRequestBody.notify_approvers = notifyApprovers;
|
|
127
|
+
}
|
|
128
|
+
// Trigger the workflow
|
|
129
|
+
let runId;
|
|
130
|
+
try {
|
|
131
|
+
const triggerResponse = await triggerWorkflowRun(runtime, triggerRequestBody);
|
|
132
|
+
runId = triggerResponse.body.workflow_run.id;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
renderRichText([
|
|
136
|
+
ui.p(`${ui.error(ui.GLYPHS.ERROR)} Failed to trigger workflow run.`),
|
|
137
|
+
], { useStderr: true });
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
renderRichText([
|
|
141
|
+
ui.blankLine(),
|
|
142
|
+
ui.p(`Workflow run ${ui.code(runId)} triggered.`, false),
|
|
143
|
+
ui.p(`Web link: ${ui.link(ui.webLink(`/self-service/workflow-runs/${runId}`, runtime))}`),
|
|
144
|
+
ui.blankLine(),
|
|
145
|
+
], { useStderr: true });
|
|
146
|
+
// Poll for updates
|
|
147
|
+
try {
|
|
148
|
+
await sleep(POLL_INTERVAL_MS);
|
|
149
|
+
renderRichText([ui.h3("Events"), ui.blankLine()], {
|
|
150
|
+
useStderr: true,
|
|
151
|
+
});
|
|
152
|
+
const finalDetail = await pollForWorkflowRunInfo(runtime, runId);
|
|
153
|
+
if (runtime.context.json) {
|
|
154
|
+
renderJson({ ok: true, workflow_run: finalDetail });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
renderRichText([
|
|
158
|
+
ui.blankLine(),
|
|
159
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Workflow run ${ui.code(finalDetail.id)} completed with status ${ui.bold(finalDetail.status)}.`),
|
|
160
|
+
ui.blankLine(),
|
|
161
|
+
]);
|
|
162
|
+
renderWorkflowRunSummary(finalDetail, runtime);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
renderRichText([
|
|
167
|
+
ui.blankLine(),
|
|
168
|
+
ui.p(`${ui.error(ui.GLYPHS.ERROR)} Workflow run failed.`),
|
|
169
|
+
], { useStderr: true });
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
// TODO: move somewhere central
|
|
175
|
+
function isInteractive() {
|
|
176
|
+
return process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY;
|
|
177
|
+
}
|
|
178
|
+
function parseOptionalTrimmed(value) {
|
|
179
|
+
if (typeof value !== "string") {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const t = value.trim();
|
|
183
|
+
return t.length > 0 ? t : undefined;
|
|
184
|
+
}
|
|
185
|
+
function parseParameterData(pairs) {
|
|
186
|
+
if (pairs.length === 0) {
|
|
187
|
+
return {};
|
|
188
|
+
}
|
|
189
|
+
const result = {};
|
|
190
|
+
for (const pair of pairs) {
|
|
191
|
+
const eqIdx = pair.indexOf("=");
|
|
192
|
+
if (eqIdx === -1) {
|
|
193
|
+
throw new CliError(`Invalid --param format: "${pair}". Expected key=value.`, EXIT_CODES.ARGUMENT_ERROR);
|
|
194
|
+
}
|
|
195
|
+
const key = pair.slice(0, eqIdx).trim();
|
|
196
|
+
if (!key) {
|
|
197
|
+
throw new CliError(`Invalid --param format: "${pair}". Parameter name cannot be empty.`, EXIT_CODES.ARGUMENT_ERROR);
|
|
198
|
+
}
|
|
199
|
+
const raw = pair.slice(eqIdx + 1);
|
|
200
|
+
result[key] = coerceParamValue(raw);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
function coerceParamValue(raw) {
|
|
205
|
+
const trimmed = raw.trim();
|
|
206
|
+
if (trimmed === "true") {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (trimmed === "false") {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
213
|
+
return Number(trimmed);
|
|
214
|
+
}
|
|
215
|
+
if (/^-?\d+\.\d+$/.test(trimmed)) {
|
|
216
|
+
return Number(trimmed);
|
|
217
|
+
}
|
|
218
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(trimmed);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new CliError(`Invalid JSON in --param value: ${trimmed.slice(0, 80)}${trimmed.length > 80 ? "…" : ""}`, EXIT_CODES.ARGUMENT_ERROR);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return raw;
|
|
227
|
+
}
|
|
228
|
+
async function triggerWorkflowRun(runtime, body) {
|
|
229
|
+
return request(runtime, "/workflowRuns.trigger", {
|
|
230
|
+
method: "POST",
|
|
231
|
+
body,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function pollForWorkflowRunInfo(runtime, workflowRunId) {
|
|
235
|
+
const seenEventIds = new Set();
|
|
236
|
+
while (true) {
|
|
237
|
+
try {
|
|
238
|
+
const infoResponse = await getWorkflowRun(runtime, workflowRunId);
|
|
239
|
+
const workflowRun = infoResponse.body.workflow_run;
|
|
240
|
+
if (workflowRun.events) {
|
|
241
|
+
for (const event of workflowRun.events) {
|
|
242
|
+
if (!seenEventIds.has(event.id)) {
|
|
243
|
+
renderRichText([workflowRunEventContent(event), ui.blankLine()], {
|
|
244
|
+
useStderr: true,
|
|
245
|
+
});
|
|
246
|
+
seenEventIds.add(event.id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (TERMINAL_SUCCESS.has(workflowRun.status)) {
|
|
251
|
+
return workflowRun;
|
|
252
|
+
}
|
|
253
|
+
if (TERMINAL_FAILURE.has(workflowRun.status)) {
|
|
254
|
+
throw buildTerminalStatusError(workflowRun);
|
|
255
|
+
}
|
|
256
|
+
if (!PENDING_WORKFLOW_RUN_STATUSES.has(workflowRun.status)) {
|
|
257
|
+
throw new CliError(`Unexpected workflow run status: ${workflowRun.status}`, 1);
|
|
258
|
+
}
|
|
259
|
+
// progress.update(
|
|
260
|
+
// `${ui.bold("Workflow running")} ${ui.dim(`(${workflowRunId})`)} — ${workflowRun.status}`,
|
|
261
|
+
// );
|
|
262
|
+
await sleep(POLL_INTERVAL_MS);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (error instanceof HttpError && error.status === 429) {
|
|
266
|
+
// progress.update(
|
|
267
|
+
// `${ui.warning(ui.GLYPHS.WARNING)} Rate limited while polling ${ui.dim(`(${workflowRunId})`)}; retrying`,
|
|
268
|
+
// );
|
|
269
|
+
await sleep(POLL_INTERVAL_MS * 5);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function buildTerminalStatusError(run) {
|
|
277
|
+
if (run.status === "FAILED") {
|
|
278
|
+
return new CliError(`Workflow run ${run.id} failed.`, 1);
|
|
279
|
+
}
|
|
280
|
+
return new CliError(`Workflow run ${run.id} ended with status ${run.status}.`, 1);
|
|
281
|
+
}
|
|
282
|
+
async function sleep(ms) {
|
|
283
|
+
await new Promise((resolve) => {
|
|
284
|
+
setTimeout(resolve, ms);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { addLinkCommand } from "./workflowRuns/addLink.js";
|
|
3
|
+
import { changeStatusCommand } from "./workflowRuns/changeStatus.js";
|
|
4
|
+
import { infoCommand } from "./workflowRuns/info.js";
|
|
5
|
+
import { postMessageCommand } from "./workflowRuns/postMessage.js";
|
|
6
|
+
import { triggerCommand } from "./workflowRuns/trigger.js";
|
|
7
|
+
export function workflowRunsCommand() {
|
|
8
|
+
const workflowRuns = new Command()
|
|
9
|
+
.name("workflowRuns")
|
|
10
|
+
.description("Trigger and monitor Self-service workflow runs");
|
|
11
|
+
workflowRuns.addCommand(addLinkCommand());
|
|
12
|
+
workflowRuns.addCommand(changeStatusCommand());
|
|
13
|
+
workflowRuns.addCommand(infoCommand());
|
|
14
|
+
workflowRuns.addCommand(postMessageCommand());
|
|
15
|
+
workflowRuns.addCommand(triggerCommand());
|
|
16
|
+
return workflowRuns;
|
|
17
|
+
}
|
|
@@ -67,7 +67,7 @@ function parseOptionalScope(value) {
|
|
|
67
67
|
return raw;
|
|
68
68
|
}
|
|
69
69
|
// --- API ---
|
|
70
|
-
async function listWorkflows(runtime, params) {
|
|
70
|
+
export async function listWorkflows(runtime, params) {
|
|
71
71
|
const query = {};
|
|
72
72
|
if (params.scope !== undefined)
|
|
73
73
|
query.scope = params.scope;
|
package/dist/ui/blocks.js
CHANGED
|
@@ -73,10 +73,12 @@ export class DescriptionListItemContainer extends AbstractBlock {
|
|
|
73
73
|
}
|
|
74
74
|
render() {
|
|
75
75
|
if (!this.options) {
|
|
76
|
-
throw new Error("Options are not set");
|
|
76
|
+
throw new Error("DescriptionListItemContainer: Options are not set");
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const termPart = pc.bold(padEnd(`${this.term}:`, this.options.termWidth));
|
|
79
|
+
const detailText = this.detailItems.map((item) => item.render()).join("\n");
|
|
80
|
+
const padding = " ".repeat(this.options.termWidth);
|
|
81
|
+
return termPart + detailText.replace(/\n/g, `\n${padding}`);
|
|
80
82
|
}
|
|
81
83
|
setOptions(options) {
|
|
82
84
|
this.options = options;
|
package/dist/ui.js
CHANGED
|
@@ -105,6 +105,16 @@ export function timestampSummary(timestamp) {
|
|
|
105
105
|
const ts = dayjs(timestamp);
|
|
106
106
|
return `${ts.fromNow()} ${dim(`(${timestamp})`)}`;
|
|
107
107
|
}
|
|
108
|
+
export function durationSummary(start, end) {
|
|
109
|
+
const pluralizeWithCount = (word, n) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
110
|
+
const duration = dayjs(end).diff(dayjs(start), "seconds");
|
|
111
|
+
if (duration < 60) {
|
|
112
|
+
return pluralizeWithCount("second", duration);
|
|
113
|
+
}
|
|
114
|
+
const minutes = Math.floor(duration / 60);
|
|
115
|
+
const seconds = duration % 60;
|
|
116
|
+
return `${pluralizeWithCount("minute", minutes)}, ${pluralizeWithCount("second", seconds)}`;
|
|
117
|
+
}
|
|
108
118
|
// Other helpers
|
|
109
119
|
export function indent(text, level) {
|
|
110
120
|
return text
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getAssets } from "./assets.js";
|
|
2
|
+
const FRAMES = 20;
|
|
3
|
+
const FRAME_DELAY_MS = 100;
|
|
4
|
+
const COLOR_START = [20, 20, 20];
|
|
5
|
+
const COLOR_HALFWAY = [95, 89, 237]; // Indigo
|
|
6
|
+
const COLOR_END = [255, 255, 255];
|
|
7
|
+
const COLOR_BACKGROUND = [0, 0, 0];
|
|
8
|
+
const SEQUENCE_CLEAR_SCREEN = "\x1b[2J\x1b[3J\x1b[H";
|
|
9
|
+
const SEQUENCE_CURSOR_HOME = "\x1b[H";
|
|
10
|
+
export async function renderWelcomeAnimation() {
|
|
11
|
+
if (process.env.DX_DISABLE_WELCOME_ANIMATION) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const assets = getAssets();
|
|
15
|
+
const LOGO_D_CONTENT = assets["logoD"];
|
|
16
|
+
const LOGO_X_CONTENT = assets["logoX"];
|
|
17
|
+
const canvasColumns = process.stdout.columns;
|
|
18
|
+
process.stderr.write(SEQUENCE_CLEAR_SCREEN);
|
|
19
|
+
await sleep(100);
|
|
20
|
+
const compositeWidth = getMaxColumns(LOGO_X_CONTENT);
|
|
21
|
+
const centeredLogoD = center(LOGO_D_CONTENT, Math.min(canvasColumns, 80), compositeWidth);
|
|
22
|
+
const centeredLogoX = center(LOGO_X_CONTENT, Math.min(canvasColumns, 80), compositeWidth);
|
|
23
|
+
for (let frame = 0; frame < FRAMES; frame += 1) {
|
|
24
|
+
const progress = frame / (FRAMES - 1);
|
|
25
|
+
const progressD = clamp(progress * 2, 0, 1);
|
|
26
|
+
const progressX = clamp((progress - 0.5) * 2, 0, 1);
|
|
27
|
+
const colorD = getThreeStageColor(progressD, COLOR_START, COLOR_HALFWAY, COLOR_END);
|
|
28
|
+
const colorX = getThreeStageColor(progressX, COLOR_START, COLOR_HALFWAY, COLOR_END);
|
|
29
|
+
const showX = progress >= 0.5;
|
|
30
|
+
process.stderr.write(SEQUENCE_CURSOR_HOME +
|
|
31
|
+
"\n\n" +
|
|
32
|
+
colorizeComposite(centeredLogoD, centeredLogoX, colorD, colorX, COLOR_BACKGROUND, showX) +
|
|
33
|
+
"\n\n\n");
|
|
34
|
+
await sleep(FRAME_DELAY_MS);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function center(text, canvasWidth, anchorWidth = getMaxColumns(text)) {
|
|
38
|
+
const maxTextWidth = Math.max(...text.split("\n").map((row) => row.length));
|
|
39
|
+
const safeCanvasWidth = Math.max(canvasWidth, anchorWidth);
|
|
40
|
+
const leftPaddingSize = Math.max(0, Math.floor((safeCanvasWidth - anchorWidth) / 2));
|
|
41
|
+
const lines = text.split("\n");
|
|
42
|
+
const centeredLines = [];
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const leftPaddedLine = " ".repeat(leftPaddingSize) + line.padEnd(maxTextWidth);
|
|
45
|
+
const rightPadding = " ".repeat(Math.max(0, safeCanvasWidth - leftPaddedLine.length));
|
|
46
|
+
centeredLines.push(leftPaddedLine + rightPadding);
|
|
47
|
+
}
|
|
48
|
+
return centeredLines.join("\n") + "\n";
|
|
49
|
+
}
|
|
50
|
+
function colorizeComposite(textD, textX, colorD, colorX, colorBackground, showX) {
|
|
51
|
+
const linesD = textD.split("\n");
|
|
52
|
+
const linesX = textX.split("\n");
|
|
53
|
+
const rowCount = Math.max(linesD.length, linesX.length);
|
|
54
|
+
const [br, bg, bb] = colorBackground;
|
|
55
|
+
const background = `\x1b[48;2;${br};${bg};${bb}m`;
|
|
56
|
+
const reset = "\x1b[0m";
|
|
57
|
+
const outputLines = [];
|
|
58
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
59
|
+
const rowD = linesD[rowIndex] ?? "";
|
|
60
|
+
const rowX = linesX[rowIndex] ?? "";
|
|
61
|
+
const columnCount = Math.max(rowD.length, rowX.length);
|
|
62
|
+
let currentColor = null;
|
|
63
|
+
let rowOutput = background;
|
|
64
|
+
for (let col = 0; col < columnCount; col += 1) {
|
|
65
|
+
const charD = rowD[col] ?? " ";
|
|
66
|
+
const charX = showX ? (rowX[col] ?? " ") : " ";
|
|
67
|
+
const useX = charX !== " ";
|
|
68
|
+
const useD = !useX && charD !== " ";
|
|
69
|
+
const nextColor = useX ? colorX : useD ? colorD : null;
|
|
70
|
+
const char = useX ? charX : useD ? charD : " ";
|
|
71
|
+
if (nextColor === null) {
|
|
72
|
+
if (currentColor !== null) {
|
|
73
|
+
rowOutput += background;
|
|
74
|
+
currentColor = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (currentColor === null ||
|
|
78
|
+
currentColor[0] !== nextColor[0] ||
|
|
79
|
+
currentColor[1] !== nextColor[1] ||
|
|
80
|
+
currentColor[2] !== nextColor[2]) {
|
|
81
|
+
rowOutput += colorToAnsi(nextColor);
|
|
82
|
+
currentColor = nextColor;
|
|
83
|
+
}
|
|
84
|
+
rowOutput += char;
|
|
85
|
+
}
|
|
86
|
+
rowOutput += reset;
|
|
87
|
+
outputLines.push(rowOutput);
|
|
88
|
+
}
|
|
89
|
+
return outputLines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
function colorToAnsi(colorForeground) {
|
|
92
|
+
const [fr, fg, fb] = colorForeground;
|
|
93
|
+
return `\x1b[38;2;${fr};${fg};${fb}m`;
|
|
94
|
+
}
|
|
95
|
+
function getMaxColumns(...texts) {
|
|
96
|
+
return Math.max(...texts.map((text) => Math.max(...text.split("\n").map((row) => row.length))));
|
|
97
|
+
}
|
|
98
|
+
function clamp(value, min, max) {
|
|
99
|
+
return Math.min(max, Math.max(min, value));
|
|
100
|
+
}
|
|
101
|
+
function sleep(ms) {
|
|
102
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
103
|
+
}
|
|
104
|
+
function tweenLinear(start, end, percent) {
|
|
105
|
+
return start + (end - start) * percent;
|
|
106
|
+
}
|
|
107
|
+
function tweenColor(colorStart, colorEnd, percent) {
|
|
108
|
+
return colorStart.map((start, index) => Math.round(tweenLinear(start, colorEnd[index], percent)));
|
|
109
|
+
}
|
|
110
|
+
function getThreeStageColor(progress, colorStart, colorHalfway, colorEnd) {
|
|
111
|
+
if (progress <= 0.5) {
|
|
112
|
+
return tweenColor(colorStart, colorHalfway, progress * 2);
|
|
113
|
+
}
|
|
114
|
+
return tweenColor(colorHalfway, colorEnd, (progress - 0.5) * 2);
|
|
115
|
+
}
|