@tikoci/quickchr 0.2.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.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * CLI output formatting — tables, colors, status indicators.
3
+ */
4
+
5
+ import { listMachineNames } from "../lib/state.ts";
6
+
7
+ /** Format a "machine not found" error with helpful suggestions. */
8
+ export function machineNotFoundMessage(name: string): string {
9
+ const names = listMachineNames();
10
+ let msg = `Machine "${name}" not found.`;
11
+ if (names.length === 0) {
12
+ msg += `\n No machines exist. Create one: quickchr start`;
13
+ } else if (names.length <= 5) {
14
+ msg += `\n Available: ${names.join(", ")}`;
15
+ } else {
16
+ msg += `\n Run 'quickchr list' to see available machines.`;
17
+ }
18
+ return msg;
19
+ }
20
+
21
+ const COLORS = {
22
+ reset: "\x1b[0m",
23
+ bold: "\x1b[1m",
24
+ dim: "\x1b[2m",
25
+ red: "\x1b[31m",
26
+ green: "\x1b[32m",
27
+ yellow: "\x1b[33m",
28
+ blue: "\x1b[34m",
29
+ cyan: "\x1b[36m",
30
+ } as const;
31
+
32
+ const NO_COLORS = Object.fromEntries(
33
+ Object.keys(COLORS).map((k) => [k, ""]),
34
+ ) as unknown as typeof COLORS;
35
+
36
+ function c(): typeof COLORS {
37
+ // Respect NO_COLOR env and pipe detection
38
+ if (process.env.NO_COLOR || !process.stdout.isTTY) return NO_COLORS;
39
+ return COLORS;
40
+ }
41
+
42
+ export function statusIcon(status: string): string {
43
+ const col = c();
44
+ switch (status) {
45
+ case "running": return `${col.green}●${col.reset}`;
46
+ case "stopped": return `${col.dim}○${col.reset}`;
47
+ case "error": return `${col.red}✗${col.reset}`;
48
+ case "ok": return `${col.green}✓${col.reset}`;
49
+ case "warn": return `${col.yellow}⚠${col.reset}`;
50
+ default: return `${col.dim}?${col.reset}`;
51
+ }
52
+ }
53
+
54
+ export function bold(text: string): string {
55
+ const col = c();
56
+ return `${col.bold}${text}${col.reset}`;
57
+ }
58
+
59
+ export function dim(text: string): string {
60
+ const col = c();
61
+ return `${col.dim}${text}${col.reset}`;
62
+ }
63
+
64
+ export function cyan(text: string): string {
65
+ const col = c();
66
+ return `${col.cyan}${text}${col.reset}`;
67
+ }
68
+
69
+ /** Format a simple table with columns. */
70
+ export function table(headers: string[], rows: string[][]): string {
71
+ const widths = headers.map((h, i) =>
72
+ Math.max(h.length, ...rows.map((r) => stripAnsi(r[i] ?? "").length)),
73
+ );
74
+
75
+ const sep = widths.map((w) => "─".repeat(w + 2)).join("┼");
76
+ const headerLine = headers
77
+ .map((h, i) => ` ${bold(h.padEnd(widths[i] ?? 0))} `)
78
+ .join("│");
79
+ const dataLines = rows.map((row) =>
80
+ row.map((cell, i) => {
81
+ const stripped = stripAnsi(cell);
82
+ const padding = (widths[i] ?? 0) - stripped.length;
83
+ return ` ${cell}${" ".repeat(Math.max(0, padding))} `;
84
+ }).join("│"),
85
+ );
86
+
87
+ return [headerLine, sep, ...dataLines].join("\n");
88
+ }
89
+
90
+ /** Strip ANSI escape codes for width calculation. */
91
+ function stripAnsi(str: string): string {
92
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping requires matching ESC (0x1b)
93
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
94
+ }
95
+
96
+ /** Format port mappings as a compact string. */
97
+ export function formatPorts(ports: Record<string, { host: number; guest: number }>): string {
98
+ return Object.entries(ports)
99
+ .map(([name, p]) => `${name}:${p.host}`)
100
+ .join(" ");
101
+ }
102
+
103
+ /** Format a network config array for display. */
104
+ export function formatNetworks(networks: { specifier: unknown; id: string }[]): string {
105
+ if (!networks || networks.length === 0) return "none";
106
+ return networks.map((n) => {
107
+ const spec = n.specifier;
108
+ if (typeof spec === "string") return spec;
109
+ if (typeof spec === "object" && spec !== null) {
110
+ const s = spec as Record<string, unknown>;
111
+ if (s.type === "vmnet-bridged") return `vmnet-bridge (${s.iface})`;
112
+ if (s.type === "bridged") return `bridged (${s.iface})`;
113
+ if (s.type === "socket-listen") return `socket:listen:${s.port}`;
114
+ if (s.type === "socket-connect") return `socket:connect:${s.port}`;
115
+ if (s.type === "socket-mcast") return `socket:mcast:${s.group}:${s.port}`;
116
+ if (s.type === "socket-named") return `socket::${s.name}`;
117
+ if (s.type === "tap") return `tap:${s.ifname}`;
118
+ return JSON.stringify(spec);
119
+ }
120
+ return String(spec);
121
+ }).join(", ");
122
+ }
123
+
124
+ /** Format a clickable URL (some terminals support this). */
125
+ export function link(url: string, label?: string): string {
126
+ const col = c();
127
+ if (!process.stdout.isTTY) return label ?? url;
128
+ return `${col.cyan}${label ?? url}${col.reset}`;
129
+ }
130
+
131
+ /** Resolve display credentials for a machine (from secret store or state). */
132
+ export async function resolveDisplayCredentials(
133
+ state: { name: string; user?: { name: string; password: string } },
134
+ ): Promise<{ user: string; password: string }> {
135
+ const { getInstanceCredentials } = await import("../lib/credentials.ts");
136
+ const stored = getInstanceCredentials(state.name);
137
+ if (stored) return { user: stored.user, password: stored.password };
138
+ if (state.user && state.user.password !== "(stored in secrets)") {
139
+ return { user: state.user.name, password: state.user.password };
140
+ }
141
+ return { user: state.user?.name ?? "admin", password: "" };
142
+ }
143
+
144
+ /** Format SSH connection string with correct user. */
145
+ export function formatSshCommand(user: string, port: number): string {
146
+ return `ssh ${user}@127.0.0.1 -p ${port}`;
147
+ }
148
+
149
+ /** Format REST URL with credentials if available. */
150
+ export function formatRestUrl(httpPort: number, user: string, password: string): string {
151
+ const auth = password ? `${user}:${password}@` : `${user}@`;
152
+ return `http://${auth}127.0.0.1:${httpPort}`;
153
+ }
154
+