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 const ASCII_UPLINK = colorWhite([
50
- "██╗ ██╗██████╗ ██╗ ██╗███╗ ██╗██╗ ██╗",
51
- "██║ ██║██╔══██╗██║ ██║████╗ ██║██║ ██╔╝",
52
- "██║ ██║██████╔╝██║ ██║██╔██╗ ██║█████╔╝ ",
53
- "██║ ██║██╔═══╝ ██║ ██║██║╚██╗██║██╔═██╗ ",
54
- "╚██████╔╝██║ ███████╗██║██║ ╚████║██║ ██╗",
55
- " ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝",
56
- ].join("\n"));
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 { colorCyan, colorDim, colorWhite } from "./colors";
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(colorDim(title));
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 branch = isLast ? "└─" : "├─";
27
-
26
+ const pointer = isSelected ? colorAccent("") : " ";
27
+
28
28
  let label: string;
29
- let branchColor: string;
30
-
31
- if (isSelected) {
32
- branchColor = colorCyan(branch);
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
- branchColor = colorWhite(branch);
36
- label = opt.label === "Back" ? colorDim(opt.label) : colorWhite(opt.label);
34
+ label = opt.label;
37
35
  }
38
36
 
39
- console.log(`${branchColor} ${label}`);
37
+ console.log(` ${pointer} ${label}`);
40
38
  });
41
39
  };
42
40
 
43
41
  console.log();
44
- console.log(colorDim(title));
42
+ console.log(" " + colorSoftGray(title));
45
43
  console.log();
46
44
  allOptions.forEach((opt, idx) => {
47
- const isLast = idx === allOptions.length - 1;
48
- const branch = isLast ? "└─" : "├─";
49
- const branchColor = idx === 0 ? colorCyan(branch) : colorWhite(branch);
50
- const label = idx === 0 ? colorCyan(opt.label) : opt.label === "Back" ? colorDim(opt.label) : colorWhite(opt.label);
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 (Oxide-inspired)
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
- // ASCII banner with color styling
92
- const ASCII_UPLINK = colorCyan([
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
- ].join("\n"));
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
- console.log();
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
- // Menu items with tree-style rendering
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 - remove emojis for cleaner look
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 (isSelected) {
1126
- branchColor = colorCyan(branch);
1127
- if (cleanLabel.toLowerCase().includes("exit")) {
1128
- label = colorDim(cleanLabel);
1129
- } else if (cleanLabel.toLowerCase().includes("stop all") || cleanLabel.toLowerCase().includes("kill")) {
1130
- label = colorRed(cleanLabel);
1131
- } else if (cleanLabel.toLowerCase().includes("get started")) {
1132
- label = colorGreen(cleanLabel);
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
- branchColor = colorDim(branch);
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 ? colorDim(" ") : "";
1167
+ const indicator = choice.subMenu ? colorSoftGray(" ") : "";
1151
1168
 
1152
- console.log(`${branchColor} ${label}${indicator}`);
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(/^🔑/, colorCyan("→"))
1171
- .replace(/^🌐/, colorCyan("→"))
1172
- .replace(/^📡/, colorCyan("→"))
1187
+ .replace(/^🔑/, colorAccent("→"))
1188
+ .replace(/^🌐/, colorAccent("→"))
1189
+ .replace(/^📡/, colorAccent("→"))
1173
1190
  .replace(/^💡/, colorYellow("→"));
1174
- console.log(colorDim("│ ") + styledLine);
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
- colorDim("↑↓") + " navigate",
1182
- colorDim("↵") + " select",
1198
+ "↑↓ navigate",
1199
+ "enter select",
1183
1200
  ];
1184
1201
  if (menuStack.length > 1) {
1185
- hints.push(colorDim("←") + " back");
1202
+ hints.push("← back");
1186
1203
  }
1187
- hints.push(colorDim("^C") + " exit");
1188
- console.log(colorDim(hints.join(" ")));
1204
+ hints.push("ctrl+c quit");
1205
+ console.log(" " + colorSoftGray(hints.join(" ")));
1189
1206
  };
1190
1207
 
1191
1208
  const cleanup = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplink-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Expose localhost to the internet in seconds. Interactive terminal UI, permanent custom domains, zero config. A modern ngrok alternative.",
5
5
  "keywords": [
6
6
  "tunnel",