@higherdev/cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/dist/api.js +31 -22
- package/dist/config.js +62 -8
- package/dist/index.js +15 -1
- package/dist/out/format.js +96 -0
- package/dist/out/theme.js +73 -0
- package/dist/tui/App.js +418 -0
- package/dist/tui/Banner.js +301 -0
- package/dist/tui/Bubble.js +12 -0
- package/dist/tui/Dashboard.js +206 -0
- package/dist/tui/Decision.js +48 -0
- package/dist/tui/Help.js +30 -0
- package/dist/tui/Panels.js +195 -0
- package/dist/tui/Settings.js +45 -0
- package/dist/tui/Splash.js +15 -0
- package/dist/tui/TextInput.js +137 -0
- package/dist/tui/alert.js +19 -0
- package/dist/tui/bounded.js +37 -0
- package/dist/tui/capability.js +4 -0
- package/dist/tui/data.js +165 -0
- package/dist/tui/height.js +58 -0
- package/dist/tui/launch.js +20 -0
- package/dist/tui/layout.js +54 -0
- package/dist/tui/parse.js +48 -0
- package/dist/tui/settings-model.js +76 -0
- package/dist/tui/stream.js +122 -0
- package/dist/tui/theme.js +52 -0
- package/dist/tui/workspace-load.js +18 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -12,17 +12,24 @@ Create `~/.config/hd/config.json` with an existing workspace API key:
|
|
|
12
12
|
|
|
13
13
|
```json
|
|
14
14
|
{
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
15
|
+
"current": "workspace",
|
|
16
|
+
"workspaces": {
|
|
17
|
+
"workspace": {
|
|
18
|
+
"url": "https://hdx-higher-ops.vercel.app",
|
|
19
|
+
"api_key": "hdx_..."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
18
22
|
}
|
|
19
23
|
```
|
|
20
24
|
|
|
25
|
+
The previous single-workspace config shape is migrated automatically when it is read.
|
|
26
|
+
|
|
21
27
|
## Commands
|
|
22
28
|
|
|
23
29
|
| Command | Purpose |
|
|
24
30
|
| --- | --- |
|
|
25
|
-
| `hd` |
|
|
31
|
+
| `hd` | Open the live TUI when attached to a terminal |
|
|
32
|
+
| `hd --help` | Show the HigherDEV banner and usage |
|
|
26
33
|
| `hd status` | Show workspace, ticket, run, and decision status |
|
|
27
34
|
| `hd ticket list` | List tickets |
|
|
28
35
|
| `hd ticket show KEY` | Show one ticket |
|
|
@@ -39,4 +46,6 @@ Create `~/.config/hd/config.json` with an existing workspace API key:
|
|
|
39
46
|
| `hd init [options]` | Configure this host and runner service |
|
|
40
47
|
| `hd upgrade [options]` | Refresh this host configuration |
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
Inside the TUI, use `/board`, `/inbox`, `/ticket`, `/decide`, `/agents`,
|
|
50
|
+
`/settings`, `/workspace`, `/feed`, `/orchestrator`, `/refresh`, `/help`, or
|
|
51
|
+
`/exit`. The display refreshes from the HDX API every five seconds.
|
package/dist/api.js
CHANGED
|
@@ -22,24 +22,19 @@ async function request(config, method, path, body) {
|
|
|
22
22
|
}
|
|
23
23
|
return parsed;
|
|
24
24
|
}
|
|
25
|
-
export async function getStatus() {
|
|
26
|
-
const config = loadConfig();
|
|
25
|
+
export async function getStatus(config = loadConfig()) {
|
|
27
26
|
return request(config, "GET", `/api/w/${config.slug}/status`);
|
|
28
27
|
}
|
|
29
|
-
export async function listTickets() {
|
|
30
|
-
const config = loadConfig();
|
|
28
|
+
export async function listTickets(config = loadConfig()) {
|
|
31
29
|
return request(config, "GET", `/api/w/${config.slug}/tickets`);
|
|
32
30
|
}
|
|
33
|
-
export async function showTicket(key) {
|
|
34
|
-
const config = loadConfig();
|
|
31
|
+
export async function showTicket(key, config = loadConfig()) {
|
|
35
32
|
return request(config, "GET", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}`);
|
|
36
33
|
}
|
|
37
|
-
export async function createTicket(fields) {
|
|
38
|
-
const config = loadConfig();
|
|
34
|
+
export async function createTicket(fields, config = loadConfig()) {
|
|
39
35
|
return request(config, "POST", `/api/w/${config.slug}/tickets`, fields);
|
|
40
36
|
}
|
|
41
|
-
export async function listTicketEvents(key, afterAt, afterId) {
|
|
42
|
-
const config = loadConfig();
|
|
37
|
+
export async function listTicketEvents(key, afterAt, afterId, config = loadConfig()) {
|
|
43
38
|
const query = new URLSearchParams();
|
|
44
39
|
if (afterAt)
|
|
45
40
|
query.set("after_at", afterAt);
|
|
@@ -48,29 +43,43 @@ export async function listTicketEvents(key, afterAt, afterId) {
|
|
|
48
43
|
const suffix = query.size ? `?${query.toString()}` : "";
|
|
49
44
|
return request(config, "GET", `/api/w/${config.slug}/tickets/${encodeURIComponent(key)}/events${suffix}`);
|
|
50
45
|
}
|
|
51
|
-
export async function postMessage(fields) {
|
|
52
|
-
const config = loadConfig();
|
|
46
|
+
export async function postMessage(fields, config = loadConfig()) {
|
|
53
47
|
return request(config, "POST", `/api/w/${config.slug}/messages`, fields);
|
|
54
48
|
}
|
|
55
|
-
export async function answerDecision(id, answer_md) {
|
|
56
|
-
const config = loadConfig();
|
|
49
|
+
export async function answerDecision(id, answer_md, config = loadConfig()) {
|
|
57
50
|
return request(config, "POST", `/api/w/${config.slug}/decisions/${encodeURIComponent(id)}/answer`, { answer_md });
|
|
58
51
|
}
|
|
59
|
-
export async function setPaused(paused) {
|
|
60
|
-
const config = loadConfig();
|
|
52
|
+
export async function setPaused(paused, config = loadConfig()) {
|
|
61
53
|
const path = paused ? `/api/w/${config.slug}/pause` : `/api/w/${config.slug}/resume`;
|
|
62
54
|
return request(config, "POST", path);
|
|
63
55
|
}
|
|
64
|
-
export async function createWorkspace(fields) {
|
|
65
|
-
const config = loadConfig();
|
|
56
|
+
export async function createWorkspace(fields, config = loadConfig()) {
|
|
66
57
|
const result = await request(config, "POST", "/api/workspaces", fields);
|
|
67
58
|
return { ...result, url: config.url };
|
|
68
59
|
}
|
|
69
|
-
export async function listAgents() {
|
|
70
|
-
const config = loadConfig();
|
|
60
|
+
export async function listAgents(config = loadConfig()) {
|
|
71
61
|
return request(config, "GET", `/api/w/${config.slug}/agents`);
|
|
72
62
|
}
|
|
73
|
-
export async function updateAgent(id, fields) {
|
|
74
|
-
const config = loadConfig();
|
|
63
|
+
export async function updateAgent(id, fields, config = loadConfig()) {
|
|
75
64
|
return request(config, "PATCH", `/api/w/${config.slug}/agents`, { id, ...fields });
|
|
76
65
|
}
|
|
66
|
+
export async function listEpics(config = loadConfig()) {
|
|
67
|
+
return request(config, "GET", `/api/w/${config.slug}/epics`);
|
|
68
|
+
}
|
|
69
|
+
export async function listMessages(options = {}, config = loadConfig()) {
|
|
70
|
+
const query = new URLSearchParams();
|
|
71
|
+
if (options.ticketId)
|
|
72
|
+
query.set("ticket_id", options.ticketId);
|
|
73
|
+
if (options.since)
|
|
74
|
+
query.set("since", options.since);
|
|
75
|
+
if (options.limit)
|
|
76
|
+
query.set("limit", String(options.limit));
|
|
77
|
+
const suffix = query.size ? `?${query.toString()}` : "";
|
|
78
|
+
return request(config, "GET", `/api/w/${config.slug}/messages${suffix}`);
|
|
79
|
+
}
|
|
80
|
+
export async function updateCaps(provider_caps, config = loadConfig()) {
|
|
81
|
+
return request(config, "PATCH", `/api/w/${config.slug}/caps`, { provider_caps });
|
|
82
|
+
}
|
|
83
|
+
export async function listFeed(config = loadConfig()) {
|
|
84
|
+
return request(config, "GET", `/api/w/${config.slug}/feed`);
|
|
85
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,26 +1,80 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
export function configPath() {
|
|
5
5
|
return process.env.HD_CONFIG ?? join(homedir(), ".config/hd/config.json");
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function normalizeWorkspace(value) {
|
|
8
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
9
|
+
return null;
|
|
10
|
+
const row = value;
|
|
11
|
+
if (typeof row.url !== "string" || typeof row.api_key !== "string" || !row.url || !row.api_key) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return { url: row.url.replace(/\/$/, ""), api_key: row.api_key };
|
|
15
|
+
}
|
|
16
|
+
function missing(path) {
|
|
17
|
+
return new Error(`missing ${path}\nWrite { "current": "workspace", "workspaces": { "workspace": { "url": "https://hdx-higher-ops.vercel.app", "api_key": "hdx_..." } } }`);
|
|
18
|
+
}
|
|
19
|
+
export function loadStoredConfig(path = configPath()) {
|
|
9
20
|
let raw;
|
|
10
21
|
try {
|
|
11
22
|
raw = readFileSync(path, "utf8");
|
|
12
23
|
}
|
|
13
24
|
catch {
|
|
14
|
-
throw
|
|
25
|
+
throw missing(path);
|
|
15
26
|
}
|
|
16
27
|
const parsed = JSON.parse(raw);
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
// 0.4 and earlier stored one workspace at the top level. Rewrite it once so
|
|
29
|
+
// every later read sees the multi-workspace format.
|
|
30
|
+
const legacy = normalizeWorkspace(parsed);
|
|
31
|
+
if (legacy && typeof parsed.slug === "string" && parsed.slug) {
|
|
32
|
+
const migrated = { current: parsed.slug, workspaces: { [parsed.slug]: legacy } };
|
|
33
|
+
writeStoredConfig(migrated, path);
|
|
34
|
+
return migrated;
|
|
35
|
+
}
|
|
36
|
+
const current = typeof parsed.current === "string" ? parsed.current : "";
|
|
37
|
+
const source = parsed.workspaces;
|
|
38
|
+
if (!current || !source || typeof source !== "object" || Array.isArray(source)) {
|
|
39
|
+
throw new Error(`${path} needs current and workspaces`);
|
|
40
|
+
}
|
|
41
|
+
const workspaces = {};
|
|
42
|
+
for (const [slug, value] of Object.entries(source)) {
|
|
43
|
+
const workspace = normalizeWorkspace(value);
|
|
44
|
+
if (workspace)
|
|
45
|
+
workspaces[slug] = workspace;
|
|
19
46
|
}
|
|
20
|
-
|
|
47
|
+
if (!workspaces[current])
|
|
48
|
+
throw new Error(`${path} current workspace ${current} is not configured`);
|
|
49
|
+
return { current, workspaces };
|
|
21
50
|
}
|
|
22
|
-
export function
|
|
51
|
+
export function loadConfig(slug) {
|
|
52
|
+
const stored = loadStoredConfig();
|
|
53
|
+
const selected = slug ?? stored.current;
|
|
54
|
+
const workspace = stored.workspaces[selected];
|
|
55
|
+
if (!workspace)
|
|
56
|
+
throw new Error(`No configured workspace ${selected}.`);
|
|
57
|
+
return { ...workspace, slug: selected };
|
|
58
|
+
}
|
|
59
|
+
export function writeStoredConfig(config, path = configPath()) {
|
|
23
60
|
mkdirSync(dirname(path), { recursive: true });
|
|
24
61
|
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
25
62
|
chmodSync(path, 0o600);
|
|
26
63
|
}
|
|
64
|
+
/** Store credentials for a workspace and make it current. */
|
|
65
|
+
export function writeHdConfig(config, path = configPath()) {
|
|
66
|
+
const stored = existsSync(path)
|
|
67
|
+
? loadStoredConfig(path)
|
|
68
|
+
: { current: config.slug, workspaces: {} };
|
|
69
|
+
stored.workspaces[config.slug] = { url: config.url.replace(/\/$/, ""), api_key: config.api_key };
|
|
70
|
+
stored.current = config.slug;
|
|
71
|
+
writeStoredConfig(stored, path);
|
|
72
|
+
}
|
|
73
|
+
export function switchWorkspace(slug, path = configPath()) {
|
|
74
|
+
const stored = loadStoredConfig(path);
|
|
75
|
+
const workspace = stored.workspaces[slug];
|
|
76
|
+
if (!workspace)
|
|
77
|
+
throw new Error(`No configured workspace ${slug}.`);
|
|
78
|
+
writeStoredConfig({ ...stored, current: slug }, path);
|
|
79
|
+
return { ...workspace, slug };
|
|
80
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { answerDecision, createTicket, createWorkspace, getStatus, listAgents, listTicketEvents, listTickets, postMessage, setPaused, showTicket, updateAgent, } from "./api.js";
|
|
5
5
|
import { initHost, parseHostFlags } from "./host.js";
|
|
6
|
+
import { writeHdConfig } from "./config.js";
|
|
6
7
|
import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
7
8
|
function fail(message) {
|
|
8
9
|
console.error(message);
|
|
@@ -184,7 +185,8 @@ async function cmdWorkspace(argv) {
|
|
|
184
185
|
const result = await createWorkspace({ name: opts.name, repo: opts.repo, slug: opts.slug,
|
|
185
186
|
default_branch: opts.branch ?? "main", default_host: opts.host ?? "box" });
|
|
186
187
|
console.log(`workspace: ${result.workspace.slug}`);
|
|
187
|
-
|
|
188
|
+
writeHdConfig({ url: result.url, api_key: result.api_key, slug: result.workspace.slug });
|
|
189
|
+
console.log("saved and switched; configure another machine with:");
|
|
188
190
|
console.log(`hd init --url ${result.url} --api-key ${result.api_key} --slug ${result.workspace.slug}`);
|
|
189
191
|
}
|
|
190
192
|
function selectAgent(agents, role, provider) {
|
|
@@ -225,6 +227,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
225
227
|
const [cmd, ...rest] = argv;
|
|
226
228
|
try {
|
|
227
229
|
if (!cmd) {
|
|
230
|
+
const { canLaunchApp } = await import("./tui/capability.js");
|
|
231
|
+
if (canLaunchApp()) {
|
|
232
|
+
const { launchApp } = await import("./tui/launch.js");
|
|
233
|
+
await launchApp();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
console.log(banner());
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log(usage());
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
228
242
|
console.log(banner());
|
|
229
243
|
console.log("");
|
|
230
244
|
console.log(usage());
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { statusTone } from "../tui/data.js";
|
|
2
|
+
import { c, DOT, ELLIPSIS, tone } from "./theme.js";
|
|
3
|
+
export function truncate(text, max) {
|
|
4
|
+
const flat = String(text ?? "")
|
|
5
|
+
.replace(/\s+/g, " ")
|
|
6
|
+
.trim();
|
|
7
|
+
if (flat.length <= max)
|
|
8
|
+
return flat;
|
|
9
|
+
return `${flat.slice(0, Math.max(0, max - 1))}${ELLIPSIS}`;
|
|
10
|
+
}
|
|
11
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
12
|
+
export function displayWidth(text) {
|
|
13
|
+
return text.replace(ANSI, "").length;
|
|
14
|
+
}
|
|
15
|
+
export function pad(text, width) {
|
|
16
|
+
const gap = width - displayWidth(text);
|
|
17
|
+
return gap > 0 ? text + " ".repeat(gap) : text;
|
|
18
|
+
}
|
|
19
|
+
export function padStart(text, width) {
|
|
20
|
+
const gap = width - displayWidth(text);
|
|
21
|
+
return gap > 0 ? " ".repeat(gap) + text : text;
|
|
22
|
+
}
|
|
23
|
+
/** Fixed-width table. Widths measure printable characters, not escape codes. */
|
|
24
|
+
export function table(columns, rows) {
|
|
25
|
+
if (rows.length === 0)
|
|
26
|
+
return c.dim("none");
|
|
27
|
+
const widths = columns.map((column, i) => Math.max(displayWidth(column.header), ...rows.map((row) => displayWidth(row[i] ?? ""))));
|
|
28
|
+
const line = (cells) => cells
|
|
29
|
+
.map((cell, i) => {
|
|
30
|
+
if (i === cells.length - 1 && columns[i]?.align !== "right")
|
|
31
|
+
return cell;
|
|
32
|
+
return columns[i]?.align === "right" ? padStart(cell, widths[i]) : pad(cell, widths[i]);
|
|
33
|
+
})
|
|
34
|
+
.join(" ")
|
|
35
|
+
.trimEnd();
|
|
36
|
+
return [c.dim(line(columns.map((column) => column.header))), ...rows.map(line)].join("\n");
|
|
37
|
+
}
|
|
38
|
+
export function statusChip(status) {
|
|
39
|
+
return tone(statusTone(status), status.replaceAll("_", " "));
|
|
40
|
+
}
|
|
41
|
+
export function stateDot(state) {
|
|
42
|
+
return tone(state, DOT);
|
|
43
|
+
}
|
|
44
|
+
export function relativeTime(iso, now = Date.now()) {
|
|
45
|
+
if (!iso)
|
|
46
|
+
return "never";
|
|
47
|
+
const at = Date.parse(iso);
|
|
48
|
+
if (Number.isNaN(at))
|
|
49
|
+
return "never";
|
|
50
|
+
const seconds = Math.round((now - at) / 1000);
|
|
51
|
+
if (seconds < 0)
|
|
52
|
+
return "just now";
|
|
53
|
+
if (seconds < 60)
|
|
54
|
+
return `${seconds}s ago`;
|
|
55
|
+
const minutes = Math.round(seconds / 60);
|
|
56
|
+
if (minutes < 60)
|
|
57
|
+
return `${minutes}m ago`;
|
|
58
|
+
const hours = Math.round(minutes / 60);
|
|
59
|
+
if (hours < 48)
|
|
60
|
+
return `${hours}h ago`;
|
|
61
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
62
|
+
}
|
|
63
|
+
export function elapsed(from, to, now = Date.now()) {
|
|
64
|
+
if (!from)
|
|
65
|
+
return "";
|
|
66
|
+
const start = Date.parse(from);
|
|
67
|
+
if (Number.isNaN(start))
|
|
68
|
+
return "";
|
|
69
|
+
const parsedEnd = to ? Date.parse(to) : now;
|
|
70
|
+
const end = Number.isNaN(parsedEnd) ? now : parsedEnd;
|
|
71
|
+
const seconds = Math.max(0, Math.round((end - start) / 1000));
|
|
72
|
+
if (seconds < 60)
|
|
73
|
+
return `${seconds}s`;
|
|
74
|
+
const minutes = Math.floor(seconds / 60);
|
|
75
|
+
if (minutes < 60)
|
|
76
|
+
return `${minutes}m${seconds % 60 ? ` ${seconds % 60}s` : ""}`;
|
|
77
|
+
const hours = Math.floor(minutes / 60);
|
|
78
|
+
return `${hours}h${minutes % 60 ? ` ${minutes % 60}m` : ""}`;
|
|
79
|
+
}
|
|
80
|
+
export function heading(text) {
|
|
81
|
+
return c.bold(text);
|
|
82
|
+
}
|
|
83
|
+
export function bullet(text) {
|
|
84
|
+
return `${c.dim("-")} ${text}`;
|
|
85
|
+
}
|
|
86
|
+
export function section(title, body) {
|
|
87
|
+
return `${heading(title)}\n${body}`;
|
|
88
|
+
}
|
|
89
|
+
/** A one-line horizontal bar for spend and progress. */
|
|
90
|
+
export function bar(value, max, width = 20) {
|
|
91
|
+
if (!(max > 0))
|
|
92
|
+
return c.dim("-".repeat(width));
|
|
93
|
+
const filled = Math.max(0, Math.min(width, Math.round((value / max) * width)));
|
|
94
|
+
return c.blue("█".repeat(filled)) + c.dim("░".repeat(width - filled));
|
|
95
|
+
}
|
|
96
|
+
export { DOT };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal palette. The web app is light only by decision; a terminal is the
|
|
3
|
+
* user's own surface, so nothing here paints a background. Foreground and dim
|
|
4
|
+
* only, blue as the single accent, status colour confined to state.
|
|
5
|
+
*/
|
|
6
|
+
const ESC = `${String.fromCharCode(27)}[`;
|
|
7
|
+
export function colorEnabled(stream = process.stdout) {
|
|
8
|
+
if (process.env.HIGHERDEV_FORCE_COLOR === "1")
|
|
9
|
+
return true;
|
|
10
|
+
if (process.env.NO_COLOR)
|
|
11
|
+
return false;
|
|
12
|
+
return Boolean(stream.isTTY);
|
|
13
|
+
}
|
|
14
|
+
const CODES = {
|
|
15
|
+
bold: 1,
|
|
16
|
+
dim: 2,
|
|
17
|
+
red: 31,
|
|
18
|
+
green: 32,
|
|
19
|
+
yellow: 33,
|
|
20
|
+
magenta: 35,
|
|
21
|
+
cyan: 36,
|
|
22
|
+
grey: 90,
|
|
23
|
+
blue: 94,
|
|
24
|
+
};
|
|
25
|
+
function wrap(name, text) {
|
|
26
|
+
if (!colorEnabled())
|
|
27
|
+
return text;
|
|
28
|
+
return `${ESC}${CODES[name]}m${text}${ESC}0m`;
|
|
29
|
+
}
|
|
30
|
+
export const c = {
|
|
31
|
+
bold: (t) => wrap("bold", t),
|
|
32
|
+
dim: (t) => wrap("dim", t),
|
|
33
|
+
blue: (t) => wrap("blue", t),
|
|
34
|
+
green: (t) => wrap("green", t),
|
|
35
|
+
yellow: (t) => wrap("yellow", t),
|
|
36
|
+
red: (t) => wrap("red", t),
|
|
37
|
+
grey: (t) => wrap("grey", t),
|
|
38
|
+
cyan: (t) => wrap("cyan", t),
|
|
39
|
+
magenta: (t) => wrap("magenta", t),
|
|
40
|
+
};
|
|
41
|
+
export function tone(value, text) {
|
|
42
|
+
switch (value) {
|
|
43
|
+
case "blue":
|
|
44
|
+
return c.blue(text);
|
|
45
|
+
case "success":
|
|
46
|
+
return c.green(text);
|
|
47
|
+
case "warning":
|
|
48
|
+
return c.yellow(text);
|
|
49
|
+
case "danger":
|
|
50
|
+
return c.red(text);
|
|
51
|
+
default:
|
|
52
|
+
return c.dim(text);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Ink colour name for the same tone, so plain and TUI output agree. */
|
|
56
|
+
export function inkColor(value) {
|
|
57
|
+
switch (value) {
|
|
58
|
+
case "blue":
|
|
59
|
+
return "blueBright";
|
|
60
|
+
case "success":
|
|
61
|
+
return "green";
|
|
62
|
+
case "warning":
|
|
63
|
+
return "yellow";
|
|
64
|
+
case "danger":
|
|
65
|
+
return "red";
|
|
66
|
+
default:
|
|
67
|
+
return "gray";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export const DOT = "●";
|
|
71
|
+
export const ARROW = "→";
|
|
72
|
+
export const BAR = "│";
|
|
73
|
+
export const ELLIPSIS = "…";
|