@get-dx/cli 0.3.1 → 0.3.3
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/README.md +11 -4
- package/dist/assets.js +15 -0
- package/dist/authCodeCallbackServer.js +50 -16
- 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/postMessage.js +46 -0
- package/dist/commands/workflowRuns.js +15 -0
- package/dist/commands/workflows.js +1 -1
- package/dist/secrets.js +91 -11
- 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/README.md
CHANGED
|
@@ -18,6 +18,13 @@ Interactively login and install the AI agent skill:
|
|
|
18
18
|
dx init
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
For CI, Docker/dev containers, or other headless environments, pass the token
|
|
22
|
+
through the environment instead of storing it in the OS credential store:
|
|
23
|
+
|
|
24
|
+
```shell
|
|
25
|
+
DX_API_TOKEN="$DX_TOKEN" dx auth status
|
|
26
|
+
```
|
|
27
|
+
|
|
21
28
|
Show help text:
|
|
22
29
|
|
|
23
30
|
```shell
|
|
@@ -42,8 +49,8 @@ Set the env vars once when initializing:
|
|
|
42
49
|
# Interactive login
|
|
43
50
|
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx init
|
|
44
51
|
|
|
45
|
-
# Non-interactive
|
|
46
|
-
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx auth
|
|
52
|
+
# Non-interactive use for CI, containers, or remote agents
|
|
53
|
+
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" DX_API_TOKEN="$DX_TOKEN" dx auth status
|
|
47
54
|
```
|
|
48
55
|
|
|
49
56
|
#### For managed deployments
|
|
@@ -54,8 +61,8 @@ Set the env vars once when initializing:
|
|
|
54
61
|
# Interactive login
|
|
55
62
|
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx init
|
|
56
63
|
|
|
57
|
-
# Non-interactive
|
|
58
|
-
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx auth
|
|
64
|
+
# Non-interactive use for CI, containers, or remote agents
|
|
65
|
+
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" DX_API_TOKEN="$DX_TOKEN" dx auth status
|
|
59
66
|
```
|
|
60
67
|
|
|
61
68
|
## Logging
|
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
|
+
}
|
|
@@ -3,6 +3,7 @@ import { CliError } from "./errors.js";
|
|
|
3
3
|
export class AuthCodeCallbackServer {
|
|
4
4
|
server = null;
|
|
5
5
|
address = null;
|
|
6
|
+
requestReceived = false;
|
|
6
7
|
async start() {
|
|
7
8
|
await new Promise((resolve, reject) => {
|
|
8
9
|
const srv = createServer();
|
|
@@ -23,19 +24,34 @@ export class AuthCodeCallbackServer {
|
|
|
23
24
|
}
|
|
24
25
|
async listenForCode(onCodeReceipt) {
|
|
25
26
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
+
const srv = this.server;
|
|
28
|
+
if (!srv) {
|
|
27
29
|
reject(new Error("Unreachable: server not started"));
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
+
srv.on("request", async (req, res) => {
|
|
33
|
+
if (this.requestReceived) {
|
|
34
|
+
res.writeHead(409, { Connection: "close" }).end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.requestReceived = true;
|
|
32
38
|
const url = new URL(req.url ?? "/", this.address);
|
|
33
39
|
const code = url.searchParams.get("code");
|
|
34
40
|
const state = url.searchParams.get("state");
|
|
41
|
+
const stopServerAndResolve = (result) => {
|
|
42
|
+
res.once("close", () => {
|
|
43
|
+
this.stop();
|
|
44
|
+
resolve(result);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
35
47
|
if (!code) {
|
|
36
48
|
const message = "Authentication failed: no code received";
|
|
37
|
-
res.writeHead(500, {
|
|
38
|
-
|
|
49
|
+
res.writeHead(500, {
|
|
50
|
+
Connection: "close",
|
|
51
|
+
"Content-Type": "text/plain",
|
|
52
|
+
});
|
|
53
|
+
stopServerAndResolve({ type: "ERROR", error: new CliError(message) });
|
|
54
|
+
res.end(message);
|
|
39
55
|
return;
|
|
40
56
|
}
|
|
41
57
|
let result;
|
|
@@ -46,34 +62,52 @@ export class AuthCodeCallbackServer {
|
|
|
46
62
|
const message = err instanceof Error
|
|
47
63
|
? err.message
|
|
48
64
|
: "Authentication failed: unknown error";
|
|
49
|
-
res.writeHead(500, {
|
|
50
|
-
|
|
65
|
+
res.writeHead(500, {
|
|
66
|
+
Connection: "close",
|
|
67
|
+
"Content-Type": "text/plain",
|
|
68
|
+
});
|
|
69
|
+
stopServerAndResolve({
|
|
51
70
|
type: "ERROR",
|
|
52
71
|
error: err instanceof Error ? err : new CliError(message),
|
|
53
72
|
});
|
|
73
|
+
res.end(message);
|
|
54
74
|
return;
|
|
55
75
|
}
|
|
56
76
|
if (result.type === "SUCCESS") {
|
|
57
77
|
if (result.redirectUri) {
|
|
58
|
-
res.writeHead(302, {
|
|
78
|
+
res.writeHead(302, {
|
|
79
|
+
Connection: "close",
|
|
80
|
+
Location: result.redirectUri,
|
|
81
|
+
});
|
|
82
|
+
stopServerAndResolve(result);
|
|
83
|
+
res.end();
|
|
59
84
|
}
|
|
60
85
|
else {
|
|
61
|
-
res
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
res.writeHead(200, {
|
|
87
|
+
Connection: "close",
|
|
88
|
+
"Content-Type": "text/plain",
|
|
89
|
+
});
|
|
90
|
+
stopServerAndResolve(result);
|
|
91
|
+
res.end("Authentication successful");
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
94
|
else {
|
|
67
|
-
res
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
res.writeHead(500, {
|
|
96
|
+
Connection: "close",
|
|
97
|
+
"Content-Type": "text/plain",
|
|
98
|
+
});
|
|
99
|
+
stopServerAndResolve(result);
|
|
100
|
+
res.end(result.error.message);
|
|
70
101
|
}
|
|
71
|
-
resolve(result);
|
|
72
102
|
});
|
|
73
103
|
});
|
|
74
104
|
}
|
|
75
105
|
stop() {
|
|
76
|
-
this.server
|
|
106
|
+
const srv = this.server;
|
|
107
|
+
if (!srv)
|
|
108
|
+
return;
|
|
109
|
+
srv.closeAllConnections();
|
|
110
|
+
srv.close();
|
|
77
111
|
this.server = null;
|
|
78
112
|
}
|
|
79
113
|
}
|
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,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,15 @@
|
|
|
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
|
+
export function workflowRunsCommand() {
|
|
7
|
+
const workflowRuns = new Command()
|
|
8
|
+
.name("workflowRuns")
|
|
9
|
+
.description("Trigger and monitor Self-service workflow runs");
|
|
10
|
+
workflowRuns.addCommand(addLinkCommand());
|
|
11
|
+
workflowRuns.addCommand(changeStatusCommand());
|
|
12
|
+
workflowRuns.addCommand(infoCommand());
|
|
13
|
+
workflowRuns.addCommand(postMessageCommand());
|
|
14
|
+
return workflowRuns;
|
|
15
|
+
}
|
|
@@ -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/secrets.js
CHANGED
|
@@ -40,16 +40,28 @@ function macosSecretStore() {
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
set(baseUrl, token) {
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
try {
|
|
44
|
+
execaSync("security", [
|
|
45
|
+
"add-generic-password",
|
|
46
|
+
"-U",
|
|
47
|
+
"-s",
|
|
48
|
+
SERVICE,
|
|
49
|
+
"-a",
|
|
50
|
+
baseUrl,
|
|
51
|
+
"-w",
|
|
52
|
+
token,
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new CliError([
|
|
57
|
+
"Unable to save the DX API token securely using macOS Keychain.",
|
|
58
|
+
"",
|
|
59
|
+
"Set DX_API_TOKEN in the environment, or try again from a macOS session with Keychain access.",
|
|
60
|
+
formatErrorDetails(error),
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join("\n"));
|
|
64
|
+
}
|
|
53
65
|
},
|
|
54
66
|
delete(baseUrl) {
|
|
55
67
|
try {
|
|
@@ -84,10 +96,78 @@ function linuxSecretStore() {
|
|
|
84
96
|
}
|
|
85
97
|
},
|
|
86
98
|
set(baseUrl, token) {
|
|
87
|
-
|
|
99
|
+
try {
|
|
100
|
+
execaSync("secret-tool", ["store", "--label=dx-cli", "service", SERVICE, "account", baseUrl], { input: token });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw buildLinuxTokenStorageError(error);
|
|
104
|
+
}
|
|
88
105
|
},
|
|
89
106
|
delete(_baseUrl) {
|
|
90
107
|
throw new CliError("Linux token deletion is not supported by secret-tool in this implementation");
|
|
91
108
|
},
|
|
92
109
|
};
|
|
93
110
|
}
|
|
111
|
+
function buildLinuxTokenStorageError(error) {
|
|
112
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
113
|
+
return new CliError([
|
|
114
|
+
"Unable to save the DX API token securely because `secret-tool` is not installed.",
|
|
115
|
+
"",
|
|
116
|
+
"Install `libsecret-tools`, or set DX_API_TOKEN in the environment instead of using `dx auth login` or `dx init` to store credentials.",
|
|
117
|
+
].join("\n"));
|
|
118
|
+
}
|
|
119
|
+
if (isLinuxSecretServiceUnavailable(error)) {
|
|
120
|
+
return new CliError([
|
|
121
|
+
"Unable to save the DX API token securely because the Linux Secret Service/keyring is not available.",
|
|
122
|
+
"",
|
|
123
|
+
"This often happens in Docker or dev containers, even when `secret-tool` is installed. Set DX_API_TOKEN in the container environment instead of using `dx auth login` or `dx init` to store credentials, or run the CLI in an environment with a working Secret Service provider such as GNOME Keyring.",
|
|
124
|
+
formatErrorDetails(error),
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join("\n"));
|
|
128
|
+
}
|
|
129
|
+
return new CliError([
|
|
130
|
+
"Unable to save the DX API token securely using `secret-tool`.",
|
|
131
|
+
"",
|
|
132
|
+
"Set DX_API_TOKEN in the environment, or configure a working Linux Secret Service provider such as GNOME Keyring.",
|
|
133
|
+
formatErrorDetails(error),
|
|
134
|
+
]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join("\n"));
|
|
137
|
+
}
|
|
138
|
+
function hasErrorCode(error, code) {
|
|
139
|
+
if (!isRecord(error)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return error.code === code;
|
|
143
|
+
}
|
|
144
|
+
function isLinuxSecretServiceUnavailable(error) {
|
|
145
|
+
const text = errorText(error);
|
|
146
|
+
return [
|
|
147
|
+
"org.freedesktop.secrets",
|
|
148
|
+
"Cannot autolaunch D-Bus",
|
|
149
|
+
"No such secret service",
|
|
150
|
+
].some((message) => text.includes(message));
|
|
151
|
+
}
|
|
152
|
+
function formatErrorDetails(error) {
|
|
153
|
+
if (!isRecord(error) || typeof error.stderr !== "string") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const stderr = error.stderr.trim();
|
|
157
|
+
return stderr ? `Details: ${stderr}` : null;
|
|
158
|
+
}
|
|
159
|
+
function errorText(error) {
|
|
160
|
+
if (!isRecord(error)) {
|
|
161
|
+
return String(error);
|
|
162
|
+
}
|
|
163
|
+
return [
|
|
164
|
+
typeof error.stderr === "string" ? error.stderr : null,
|
|
165
|
+
typeof error.shortMessage === "string" ? error.shortMessage : null,
|
|
166
|
+
typeof error.message === "string" ? error.message : null,
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join("\n");
|
|
170
|
+
}
|
|
171
|
+
function isRecord(value) {
|
|
172
|
+
return typeof value === "object" && value !== null;
|
|
173
|
+
}
|
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
|
+
}
|