uplink-cli 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Color palette and helpers for the interactive menu UI.
|
|
2
|
+
// Claude Code-inspired - clean, minimal aesthetic
|
|
2
3
|
export const c = {
|
|
3
4
|
reset: "\x1b[0m",
|
|
4
5
|
bold: "\x1b[1m",
|
|
5
6
|
dim: "\x1b[2m",
|
|
7
|
+
italic: "\x1b[3m",
|
|
6
8
|
// Colors
|
|
7
9
|
cyan: "\x1b[36m",
|
|
8
10
|
green: "\x1b[32m",
|
|
@@ -11,11 +13,17 @@ export const c = {
|
|
|
11
13
|
magenta: "\x1b[35m",
|
|
12
14
|
white: "\x1b[97m",
|
|
13
15
|
gray: "\x1b[90m",
|
|
16
|
+
blue: "\x1b[34m",
|
|
14
17
|
// Bright variants
|
|
15
18
|
brightCyan: "\x1b[96m",
|
|
16
19
|
brightGreen: "\x1b[92m",
|
|
17
20
|
brightYellow: "\x1b[93m",
|
|
18
21
|
brightWhite: "\x1b[97m",
|
|
22
|
+
brightBlue: "\x1b[94m",
|
|
23
|
+
// 256 color for subtle tones
|
|
24
|
+
softBlue: "\x1b[38;5;75m",
|
|
25
|
+
softGray: "\x1b[38;5;245m",
|
|
26
|
+
darkGray: "\x1b[38;5;240m",
|
|
19
27
|
};
|
|
20
28
|
|
|
21
29
|
export function colorCyan(text: string) {
|
|
@@ -46,11 +54,14 @@ export function colorWhite(text: string) {
|
|
|
46
54
|
return `${c.brightWhite}${text}${c.reset}`;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
export function colorBlue(text: string) {
|
|
58
|
+
return `${c.softBlue}${text}${c.reset}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function colorSoftGray(text: string) {
|
|
62
|
+
return `${c.softGray}${text}${c.reset}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function colorAccent(text: string) {
|
|
66
|
+
return `${c.brightBlue}${text}${c.reset}`;
|
|
67
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { colorAccent, colorBold, colorDim, colorSoftGray } from "./colors";
|
|
2
2
|
|
|
3
3
|
export type SelectOption = { label: string; value: string | number | null };
|
|
4
4
|
|
|
5
5
|
// Inline arrow-key selector (returns selected index, or null for "Back")
|
|
6
|
+
// Clean, minimal styling inspired by Claude Code
|
|
6
7
|
export async function inlineSelect(
|
|
7
8
|
title: string,
|
|
8
9
|
options: SelectOption[],
|
|
@@ -17,38 +18,34 @@ export async function inlineSelect(
|
|
|
17
18
|
process.stdout.write(`\x1b[${linesToClear}A\x1b[0J`);
|
|
18
19
|
|
|
19
20
|
console.log();
|
|
20
|
-
console.log(
|
|
21
|
+
console.log(" " + colorSoftGray(title));
|
|
21
22
|
console.log();
|
|
22
23
|
|
|
23
24
|
allOptions.forEach((opt, idx) => {
|
|
24
|
-
const isLast = idx === allOptions.length - 1;
|
|
25
25
|
const isSelected = idx === selected;
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const pointer = isSelected ? colorAccent("›") : " ";
|
|
27
|
+
|
|
28
28
|
let label: string;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (isSelected) {
|
|
32
|
-
|
|
33
|
-
label = opt.label === "Back" ? colorDim(opt.label) : colorCyan(opt.label);
|
|
29
|
+
if (opt.label === "Back") {
|
|
30
|
+
label = colorSoftGray(opt.label);
|
|
31
|
+
} else if (isSelected) {
|
|
32
|
+
label = colorBold(opt.label);
|
|
34
33
|
} else {
|
|
35
|
-
|
|
36
|
-
label = opt.label === "Back" ? colorDim(opt.label) : colorWhite(opt.label);
|
|
34
|
+
label = opt.label;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
console.log(
|
|
37
|
+
console.log(` ${pointer} ${label}`);
|
|
40
38
|
});
|
|
41
39
|
};
|
|
42
40
|
|
|
43
41
|
console.log();
|
|
44
|
-
console.log(
|
|
42
|
+
console.log(" " + colorSoftGray(title));
|
|
45
43
|
console.log();
|
|
46
44
|
allOptions.forEach((opt, idx) => {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
console.log(`${branchColor} ${label}`);
|
|
45
|
+
const isSelected = idx === 0;
|
|
46
|
+
const pointer = isSelected ? colorAccent("›") : " ";
|
|
47
|
+
const label = opt.label === "Back" ? colorSoftGray(opt.label) : (isSelected ? colorBold(opt.label) : opt.label);
|
|
48
|
+
console.log(` ${pointer} ${label}`);
|
|
52
49
|
});
|
|
53
50
|
|
|
54
51
|
try {
|
|
@@ -39,12 +39,14 @@ function clearScreen() {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// ─────────────────────────────────────────────────────────────
|
|
42
|
-
// Color palette (
|
|
42
|
+
// Color palette (Claude Code-inspired - clean, minimal)
|
|
43
43
|
// ─────────────────────────────────────────────────────────────
|
|
44
44
|
const c = {
|
|
45
45
|
reset: "\x1b[0m",
|
|
46
46
|
bold: "\x1b[1m",
|
|
47
47
|
dim: "\x1b[2m",
|
|
48
|
+
italic: "\x1b[3m",
|
|
49
|
+
underline: "\x1b[4m",
|
|
48
50
|
// Colors
|
|
49
51
|
cyan: "\x1b[36m",
|
|
50
52
|
green: "\x1b[32m",
|
|
@@ -53,11 +55,17 @@ const c = {
|
|
|
53
55
|
magenta: "\x1b[35m",
|
|
54
56
|
white: "\x1b[97m",
|
|
55
57
|
gray: "\x1b[90m",
|
|
58
|
+
blue: "\x1b[34m",
|
|
56
59
|
// Bright variants
|
|
57
60
|
brightCyan: "\x1b[96m",
|
|
58
61
|
brightGreen: "\x1b[92m",
|
|
59
62
|
brightYellow: "\x1b[93m",
|
|
60
63
|
brightWhite: "\x1b[97m",
|
|
64
|
+
brightBlue: "\x1b[94m",
|
|
65
|
+
// 256 color for subtle tones
|
|
66
|
+
softBlue: "\x1b[38;5;75m",
|
|
67
|
+
softGray: "\x1b[38;5;245m",
|
|
68
|
+
darkGray: "\x1b[38;5;240m",
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
function colorCyan(text: string) {
|
|
@@ -88,15 +96,45 @@ function colorMagenta(text: string) {
|
|
|
88
96
|
return `${c.magenta}${text}${c.reset}`;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
function colorBlue(text: string) {
|
|
100
|
+
return `${c.softBlue}${text}${c.reset}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function colorSoftGray(text: string) {
|
|
104
|
+
return `${c.softGray}${text}${c.reset}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function colorAccent(text: string) {
|
|
108
|
+
return `${c.brightBlue}${text}${c.reset}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ASCII banner with status indicator
|
|
112
|
+
const ASCII_BANNER = [
|
|
93
113
|
"██╗ ██╗██████╗ ██╗ ██╗███╗ ██╗██╗ ██╗",
|
|
94
114
|
"██║ ██║██╔══██╗██║ ██║████╗ ██║██║ ██╔╝",
|
|
95
115
|
"██║ ██║██████╔╝██║ ██║██╔██╗ ██║█████╔╝ ",
|
|
96
116
|
"██║ ██║██╔═══╝ ██║ ██║██║╚██╗██║██╔═██╗ ",
|
|
97
117
|
"╚██████╔╝██║ ███████╗██║██║ ╚████║██║ ██╗",
|
|
98
118
|
" ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝",
|
|
99
|
-
]
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
function renderHeader(role: string, connected: boolean) {
|
|
122
|
+
const status = connected
|
|
123
|
+
? colorGreen("●") + colorSoftGray(" connected")
|
|
124
|
+
: colorYellow("●") + colorSoftGray(" offline");
|
|
125
|
+
const roleTag = role === "admin"
|
|
126
|
+
? colorSoftGray(" • ") + colorYellow("admin")
|
|
127
|
+
: "";
|
|
128
|
+
|
|
129
|
+
// ASCII banner in accent color
|
|
130
|
+
console.log();
|
|
131
|
+
ASCII_BANNER.forEach(line => console.log(" " + colorAccent(line)));
|
|
132
|
+
|
|
133
|
+
// Status line below banner
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(" " + status + roleTag);
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
100
138
|
|
|
101
139
|
function truncate(text: string, max: number) {
|
|
102
140
|
if (text.length <= max) return text;
|
|
@@ -1077,79 +1115,58 @@ export const menuCommand = new Command("menu")
|
|
|
1077
1115
|
|
|
1078
1116
|
const render = () => {
|
|
1079
1117
|
clearScreen();
|
|
1080
|
-
console.log();
|
|
1081
|
-
console.log(ASCII_UPLINK);
|
|
1082
|
-
console.log();
|
|
1083
|
-
|
|
1084
|
-
// Status bar - relay and API status
|
|
1085
|
-
if (menuStack.length === 1 && cachedRelayStatus) {
|
|
1086
|
-
const statusColor = cachedRelayStatus.includes("ok") ? colorGreen : colorRed;
|
|
1087
|
-
console.log(colorDim("├─") + " Status " + statusColor(cachedRelayStatus.replace("Relay: ", "")));
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Show active tunnels if we're at the main menu (use cached value, no scanning)
|
|
1091
|
-
if (menuStack.length === 1 && cachedActiveTunnels) {
|
|
1092
|
-
console.log(cachedActiveTunnels);
|
|
1093
|
-
}
|
|
1094
1118
|
|
|
1095
|
-
|
|
1119
|
+
// Check if API is reachable based on cached relay status
|
|
1120
|
+
const isConnected = cachedRelayStatus?.includes("ok") ?? false;
|
|
1121
|
+
const role = isAdmin ? "admin" : "user";
|
|
1122
|
+
renderHeader(role, isConnected);
|
|
1096
1123
|
|
|
1097
1124
|
const currentMenu = getCurrentMenu();
|
|
1098
1125
|
|
|
1099
|
-
// Breadcrumb navigation
|
|
1126
|
+
// Breadcrumb navigation (for submenus)
|
|
1100
1127
|
if (menuPath.length > 0) {
|
|
1101
|
-
const breadcrumb = menuPath.map((p, i) =>
|
|
1102
|
-
i === menuPath.length - 1 ? colorCyan(p) : colorDim(p)
|
|
1103
|
-
).join(colorDim(" › "));
|
|
1104
|
-
console.log(breadcrumb);
|
|
1105
1128
|
console.log();
|
|
1129
|
+
const breadcrumb = menuPath.map((p, i) =>
|
|
1130
|
+
i === menuPath.length - 1 ? colorAccent(p) : colorSoftGray(p)
|
|
1131
|
+
).join(colorSoftGray(" › "));
|
|
1132
|
+
console.log(" " + breadcrumb);
|
|
1106
1133
|
}
|
|
1107
1134
|
|
|
1108
|
-
|
|
1135
|
+
console.log();
|
|
1136
|
+
|
|
1137
|
+
// Menu items - clean list style
|
|
1109
1138
|
currentMenu.forEach((choice, idx) => {
|
|
1110
|
-
const isLast = idx === currentMenu.length - 1;
|
|
1111
1139
|
const isSelected = idx === selected;
|
|
1112
|
-
const branch = isLast ? "└─" : "├─";
|
|
1113
1140
|
|
|
1114
|
-
// Clean up labels
|
|
1141
|
+
// Clean up labels
|
|
1115
1142
|
let cleanLabel = choice.label
|
|
1116
1143
|
.replace(/^🚀\s*/, "")
|
|
1117
1144
|
.replace(/^⚠️\s*/, "")
|
|
1118
1145
|
.replace(/^✅\s*/, "")
|
|
1119
1146
|
.replace(/^❌\s*/, "");
|
|
1120
1147
|
|
|
1148
|
+
// Selection indicator
|
|
1149
|
+
const pointer = isSelected ? colorAccent("›") : " ";
|
|
1150
|
+
|
|
1121
1151
|
// Style based on selection and type
|
|
1122
1152
|
let label: string;
|
|
1123
|
-
let branchColor: string;
|
|
1124
1153
|
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
} else {
|
|
1134
|
-
label = colorCyan(cleanLabel);
|
|
1135
|
-
}
|
|
1154
|
+
if (cleanLabel.toLowerCase().includes("exit")) {
|
|
1155
|
+
label = colorSoftGray(cleanLabel);
|
|
1156
|
+
} else if (cleanLabel.toLowerCase().includes("stop all") || cleanLabel.toLowerCase().includes("kill")) {
|
|
1157
|
+
label = isSelected ? colorRed(cleanLabel) : colorSoftGray(cleanLabel);
|
|
1158
|
+
} else if (cleanLabel.toLowerCase().includes("get started")) {
|
|
1159
|
+
label = isSelected ? colorGreen(cleanLabel) : colorGreen(cleanLabel);
|
|
1160
|
+
} else if (isSelected) {
|
|
1161
|
+
label = colorBold(cleanLabel);
|
|
1136
1162
|
} else {
|
|
1137
|
-
|
|
1138
|
-
if (cleanLabel.toLowerCase().includes("exit")) {
|
|
1139
|
-
label = colorDim(cleanLabel);
|
|
1140
|
-
} else if (cleanLabel.toLowerCase().includes("stop all") || cleanLabel.toLowerCase().includes("kill")) {
|
|
1141
|
-
label = colorRed(cleanLabel);
|
|
1142
|
-
} else if (cleanLabel.toLowerCase().includes("get started")) {
|
|
1143
|
-
label = colorGreen(cleanLabel);
|
|
1144
|
-
} else {
|
|
1145
|
-
label = cleanLabel;
|
|
1146
|
-
}
|
|
1163
|
+
label = cleanLabel;
|
|
1147
1164
|
}
|
|
1148
1165
|
|
|
1149
1166
|
// Submenu indicator
|
|
1150
|
-
const indicator = choice.subMenu ?
|
|
1167
|
+
const indicator = choice.subMenu ? colorSoftGray(" →") : "";
|
|
1151
1168
|
|
|
1152
|
-
console.log(
|
|
1169
|
+
console.log(` ${pointer} ${label}${indicator}`);
|
|
1153
1170
|
});
|
|
1154
1171
|
|
|
1155
1172
|
// Message area
|
|
@@ -1162,30 +1179,30 @@ export const menuCommand = new Command("menu")
|
|
|
1162
1179
|
// Format multi-line messages nicely
|
|
1163
1180
|
const lines = message.split("\n");
|
|
1164
1181
|
lines.forEach((line) => {
|
|
1165
|
-
// Color success/error indicators
|
|
1182
|
+
// Color success/error indicators - clean symbols
|
|
1166
1183
|
let styledLine = line
|
|
1167
1184
|
.replace(/^✅/, colorGreen("✓"))
|
|
1168
1185
|
.replace(/^❌/, colorRed("✗"))
|
|
1169
1186
|
.replace(/^⚠️/, colorYellow("!"))
|
|
1170
|
-
.replace(/^🔑/,
|
|
1171
|
-
.replace(/^🌐/,
|
|
1172
|
-
.replace(/^📡/,
|
|
1187
|
+
.replace(/^🔑/, colorAccent("→"))
|
|
1188
|
+
.replace(/^🌐/, colorAccent("→"))
|
|
1189
|
+
.replace(/^📡/, colorAccent("→"))
|
|
1173
1190
|
.replace(/^💡/, colorYellow("→"));
|
|
1174
|
-
console.log(
|
|
1191
|
+
console.log(" " + colorSoftGray("│") + " " + styledLine);
|
|
1175
1192
|
});
|
|
1176
1193
|
}
|
|
1177
1194
|
|
|
1178
|
-
// Footer hints
|
|
1195
|
+
// Footer hints - clean and subtle
|
|
1179
1196
|
console.log();
|
|
1180
1197
|
const hints = [
|
|
1181
|
-
|
|
1182
|
-
|
|
1198
|
+
"↑↓ navigate",
|
|
1199
|
+
"enter select",
|
|
1183
1200
|
];
|
|
1184
1201
|
if (menuStack.length > 1) {
|
|
1185
|
-
hints.push(
|
|
1202
|
+
hints.push("← back");
|
|
1186
1203
|
}
|
|
1187
|
-
hints.push(
|
|
1188
|
-
console.log(
|
|
1204
|
+
hints.push("ctrl+c quit");
|
|
1205
|
+
console.log(" " + colorSoftGray(hints.join(" • ")));
|
|
1189
1206
|
};
|
|
1190
1207
|
|
|
1191
1208
|
const cleanup = () => {
|
package/package.json
CHANGED