uplink-cli 0.1.38 → 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.
Files changed (67) hide show
  1. package/AGENTS.md +177 -0
  2. package/CHANGELOG.md +15 -0
  3. package/LICENSE +21 -0
  4. package/README.md +72 -52
  5. package/cli/src/index.ts +16 -3
  6. package/cli/src/registrars/cloudflare.ts +148 -0
  7. package/cli/src/registrars/dreamhost.ts +129 -0
  8. package/cli/src/registrars/godaddy.ts +105 -0
  9. package/cli/src/registrars/hostinger.ts +106 -0
  10. package/cli/src/registrars/http.ts +18 -0
  11. package/cli/src/registrars/index.ts +32 -0
  12. package/cli/src/registrars/namecheap.ts +163 -0
  13. package/cli/src/registrars/secret.ts +66 -0
  14. package/cli/src/registrars/store.ts +55 -0
  15. package/cli/src/registrars/types.ts +42 -0
  16. package/cli/src/subcommands/admin.ts +17 -30
  17. package/cli/src/subcommands/db.ts +63 -57
  18. package/cli/src/subcommands/dev.ts +23 -25
  19. package/cli/src/subcommands/domains.ts +295 -0
  20. package/cli/src/subcommands/host-domains.ts +148 -0
  21. package/cli/src/subcommands/host.ts +3 -0
  22. package/cli/src/subcommands/login.ts +85 -0
  23. package/cli/src/subcommands/menu/colors.ts +1 -1
  24. package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
  25. package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
  26. package/cli/src/subcommands/menu/io.ts +27 -5
  27. package/cli/src/subcommands/menu/menus/domain-check.ts +34 -0
  28. package/cli/src/subcommands/menu/menus/domains.ts +197 -0
  29. package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
  30. package/cli/src/subcommands/menu/menus/index.ts +1 -0
  31. package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
  32. package/cli/src/subcommands/menu/render.ts +2 -2
  33. package/cli/src/subcommands/menu/requests.ts +9 -2
  34. package/cli/src/subcommands/menu/tests.ts +1 -1
  35. package/cli/src/subcommands/menu/tunnels.ts +10 -99
  36. package/cli/src/subcommands/menu/types.ts +8 -0
  37. package/cli/src/subcommands/menu.ts +32 -524
  38. package/cli/src/subcommands/signup.ts +2 -2
  39. package/cli/src/subcommands/system.ts +58 -36
  40. package/cli/src/subcommands/tunnel.ts +126 -33
  41. package/cli/src/templates/index.ts +3 -3
  42. package/cli/src/tui/App.tsx +202 -0
  43. package/cli/src/tui/AppInspector.tsx +114 -0
  44. package/cli/src/tui/HomeStatus.tsx +92 -0
  45. package/cli/src/tui/brand.tsx +20 -0
  46. package/cli/src/tui/format.ts +22 -0
  47. package/cli/src/tui/index.mts +6 -0
  48. package/cli/src/tui/liveTree.ts +40 -0
  49. package/cli/src/tui/package.json +3 -0
  50. package/cli/src/tui/runMenu.tsx +57 -0
  51. package/cli/src/tui/session.mts +267 -0
  52. package/cli/src/tui/snapshot.ts +175 -0
  53. package/cli/src/utils/api-base.ts +11 -0
  54. package/cli/src/utils/credentials.ts +58 -0
  55. package/cli/src/utils/domain-availability.ts +56 -0
  56. package/cli/src/utils/guest-access.ts +38 -0
  57. package/cli/src/utils/launchDomainking.ts +64 -0
  58. package/cli/src/utils/login-flow.ts +57 -0
  59. package/docs/AGENTS.md +130 -148
  60. package/docs/HOSTING.md +55 -0
  61. package/docs/MENU_STRUCTURE.md +60 -288
  62. package/docs/PRODUCT.md +64 -0
  63. package/docs/README.md +11 -7
  64. package/package.json +22 -36
  65. package/scripts/tunnel/client-improved.js +127 -38
  66. package/scripts/tunnel/client.js +118 -0
  67. package/assets/cli-screenshot.png +0 -0
@@ -0,0 +1,42 @@
1
+ export const PROVIDER_IDS = ["godaddy", "cloudflare", "hostinger", "namecheap", "dreamhost"] as const;
2
+ export type ProviderId = (typeof PROVIDER_IDS)[number];
3
+
4
+ export function isProviderId(value: string): value is ProviderId {
5
+ return (PROVIDER_IDS as readonly string[]).includes(value);
6
+ }
7
+
8
+ export type RegistrarCredentials = {
9
+ token?: string;
10
+ apiUser?: string;
11
+ apiKey?: string;
12
+ accountId?: string;
13
+ /** Additional API keys for providers with multiple accounts (e.g. DreamHost). */
14
+ extraTokens?: string[];
15
+ };
16
+
17
+ export type InventoryDomain = {
18
+ domain: string;
19
+ provider: ProviderId;
20
+ status: "owned";
21
+ expiresAt?: string;
22
+ };
23
+
24
+ export type DomainQuote = {
25
+ domain: string;
26
+ provider: ProviderId;
27
+ status: "owned" | "available" | "taken" | "not_for_sale" | "unknown";
28
+ buyable?: boolean;
29
+ priceUsd?: number;
30
+ premium?: boolean;
31
+ error?: string;
32
+ };
33
+
34
+ export type RegistrarAdapter = {
35
+ id: ProviderId;
36
+ label: string;
37
+ /** What an agent must pass to connect. */
38
+ connectHelp: string;
39
+ verify(creds: RegistrarCredentials): Promise<RegistrarCredentials>;
40
+ listDomains(creds: RegistrarCredentials): Promise<InventoryDomain[]>;
41
+ check(creds: RegistrarCredentials, domain: string): Promise<DomainQuote>;
42
+ };
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { apiRequest } from "../http";
3
3
  import { getResolvedApiBase } from "../utils/api-base";
4
+ import { handleError } from "../utils/machine";
4
5
 
5
6
  export const adminCommand = new Command("admin")
6
7
  .description("Admin commands for system management");
@@ -63,10 +64,8 @@ adminCommand
63
64
  console.log(` Created 24h: ${stats.databases.createdLast24h}`);
64
65
  console.log();
65
66
  }
66
- } catch (error: any) {
67
- const errorMsg = error.message || error.toString() || JSON.stringify(error);
68
- console.error("Error getting status:", errorMsg);
69
- process.exit(1);
67
+ } catch (error) {
68
+ handleError(error, { json: opts.json });
70
69
  }
71
70
  });
72
71
 
@@ -118,10 +117,8 @@ adminCommand
118
117
  }
119
118
  console.log();
120
119
  }
121
- } catch (error: any) {
122
- const errorMsg = error.message || error.toString() || JSON.stringify(error);
123
- console.error("Error listing tunnels:", errorMsg);
124
- process.exit(1);
120
+ } catch (error) {
121
+ handleError(error, { json: opts.json });
125
122
  }
126
123
  });
127
124
 
@@ -176,10 +173,8 @@ adminCommand
176
173
  }
177
174
  console.log();
178
175
  }
179
- } catch (error: any) {
180
- const errorMsg = error.message || error.toString() || JSON.stringify(error);
181
- console.error("Error listing databases:", errorMsg);
182
- process.exit(1);
176
+ } catch (error) {
177
+ handleError(error, { json: opts.json });
183
178
  }
184
179
  });
185
180
 
@@ -222,10 +217,8 @@ tokensCommand
222
217
  console.log("\nIMPORTANT: This token is shown only once. Store it securely.\n");
223
218
  console.log(result.token);
224
219
  console.log();
225
- } catch (error: any) {
226
- const errorMsg = error.message || error.toString() || JSON.stringify(error);
227
- console.error("Error creating token:", errorMsg);
228
- process.exit(1);
220
+ } catch (error) {
221
+ handleError(error, { json: opts.json });
229
222
  }
230
223
  });
231
224
 
@@ -287,10 +280,8 @@ tokensCommand
287
280
  );
288
281
  }
289
282
  console.log();
290
- } catch (error: any) {
291
- const errorMsg = error.message || error.toString() || JSON.stringify(error);
292
- console.error("Error listing tokens:", errorMsg);
293
- process.exit(1);
283
+ } catch (error) {
284
+ handleError(error, { json: opts.json });
294
285
  }
295
286
  });
296
287
 
@@ -306,7 +297,7 @@ tokensCommand
306
297
  const token = opts.token ? String(opts.token) : "";
307
298
  if (!id && !token) {
308
299
  console.error("Provide --id or --token");
309
- process.exit(1);
300
+ process.exit(2);
310
301
  }
311
302
 
312
303
  const result = await apiRequest("POST", "/v1/admin/tokens/revoke", {
@@ -319,10 +310,8 @@ tokensCommand
319
310
  } else {
320
311
  console.log(`✅ Revoked token${id ? ` ${id}` : ""} at ${result.revokedAt || ""}`);
321
312
  }
322
- } catch (error: any) {
323
- const errorMsg = error.message || error.toString() || JSON.stringify(error);
324
- console.error("Error revoking token:", errorMsg);
325
- process.exit(1);
313
+ } catch (error) {
314
+ handleError(error, { json: opts.json });
326
315
  }
327
316
  });
328
317
 
@@ -344,12 +333,10 @@ adminCommand
344
333
  }
345
334
  } else {
346
335
  console.error("Specify what to cleanup: --dev-user-tunnels");
347
- process.exit(1);
336
+ process.exit(2);
348
337
  }
349
- } catch (error: any) {
350
- const errorMsg = error.message || error.toString() || JSON.stringify(error);
351
- console.error("Error during cleanup:", errorMsg);
352
- process.exit(1);
338
+ } catch (error) {
339
+ handleError(error, { json: opts.json });
353
340
  }
354
341
  });
355
342
 
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { apiRequest } from "../http";
3
+ import { handleError, printJson } from "../utils/machine";
3
4
 
4
5
  export const dbCommand = new Command("db").description("Manage databases");
5
6
 
@@ -13,19 +14,20 @@ dbCommand
13
14
  .option("--plan <plan>", "Plan", "dev")
14
15
  .option("--json", "Output JSON", false)
15
16
  .action(async (opts) => {
16
- const body = {
17
- name: opts.name,
18
- project: opts.project,
19
- provider: opts.provider,
20
- region: opts.region,
21
- plan: opts.plan,
22
- };
17
+ try {
18
+ const body = {
19
+ name: opts.name,
20
+ project: opts.project,
21
+ provider: opts.provider,
22
+ region: opts.region,
23
+ plan: opts.plan,
24
+ };
23
25
 
24
- const result = await apiRequest("POST", "/v1/dbs", body);
25
- if (opts.json) {
26
- console.log(JSON.stringify(result, null, 2));
27
- } else {
28
- console.log(`Created DB ${result.name} (${result.id}) in ${result.region}`);
26
+ const result = await apiRequest("POST", "/v1/dbs", body);
27
+ if (opts.json) printJson(result);
28
+ else console.log(`Created DB ${result.name} (${result.id}) in ${result.region}`);
29
+ } catch (error) {
30
+ handleError(error, { json: opts.json });
29
31
  }
30
32
  });
31
33
 
@@ -35,16 +37,20 @@ dbCommand
35
37
  .option("--project <project>", "Project name")
36
38
  .option("--json", "Output JSON", false)
37
39
  .action(async (opts) => {
38
- const query = opts.project ? `?project=${encodeURIComponent(opts.project)}` : "";
39
- const result = await apiRequest("GET", `/v1/dbs${query}`);
40
- if (opts.json) {
41
- console.log(JSON.stringify(result, null, 2));
42
- } else {
43
- for (const db of result.items) {
44
- console.log(
45
- `${db.id} ${db.name} ${db.region} ${db.status} ready=${db.ready}`
46
- );
40
+ try {
41
+ const query = opts.project ? `?project=${encodeURIComponent(opts.project)}` : "";
42
+ const result = await apiRequest("GET", `/v1/dbs${query}`);
43
+ if (opts.json) {
44
+ printJson(result);
45
+ } else {
46
+ for (const db of result.items || []) {
47
+ console.log(
48
+ `${db.id} ${db.name} ${db.region} ${db.status} ready=${db.ready}`
49
+ );
50
+ }
47
51
  }
52
+ } catch (error) {
53
+ handleError(error, { json: opts.json });
48
54
  }
49
55
  });
50
56
 
@@ -54,14 +60,18 @@ dbCommand
54
60
  .requiredOption("--id <id>", "Database id")
55
61
  .option("--json", "Output JSON", false)
56
62
  .action(async (opts) => {
57
- const result = await apiRequest("GET", `/v1/dbs/${opts.id}`);
58
- if (opts.json) {
59
- console.log(JSON.stringify(result, null, 2));
60
- } else {
61
- console.log(`DB ${result.name} (${result.id})`);
62
- console.log(` region: ${result.region}`);
63
- console.log(` status: ${result.status}`);
64
- console.log(` engine: ${result.engine} ${result.version}`);
63
+ try {
64
+ const result = await apiRequest("GET", `/v1/dbs/${opts.id}`);
65
+ if (opts.json) {
66
+ printJson(result);
67
+ } else {
68
+ console.log(`DB ${result.name} (${result.id})`);
69
+ console.log(` region: ${result.region}`);
70
+ console.log(` status: ${result.status}`);
71
+ console.log(` engine: ${result.engine} ${result.version}`);
72
+ }
73
+ } catch (error) {
74
+ handleError(error, { json: opts.json });
65
75
  }
66
76
  });
67
77
 
@@ -72,14 +82,15 @@ dbCommand
72
82
  .option("--yes", "Confirm deletion", false)
73
83
  .option("--json", "Output JSON", false)
74
84
  .action(async (opts) => {
75
- if (!opts.yes) {
76
- throw new Error("Refusing to delete without --yes");
77
- }
78
- const result = await apiRequest("DELETE", `/v1/dbs/${opts.id}`);
79
- if (opts.json) {
80
- console.log(JSON.stringify(result, null, 2));
81
- } else {
82
- console.log(`Deleted DB ${result.id}`);
85
+ try {
86
+ if (!opts.yes) {
87
+ throw new Error("Refusing to delete without --yes");
88
+ }
89
+ const result = await apiRequest("DELETE", `/v1/dbs/${opts.id}`);
90
+ if (opts.json) printJson(result);
91
+ else console.log(`Deleted DB ${result.id}`);
92
+ } catch (error) {
93
+ handleError(error, { json: opts.json });
83
94
  }
84
95
  });
85
96
 
@@ -91,27 +102,22 @@ dbCommand
91
102
  .requiredOption("--env-var <envVar>", "Environment variable name")
92
103
  .option("--json", "Output JSON", false)
93
104
  .action(async (opts) => {
94
- const body = {
95
- service: opts.service,
96
- envVar: opts["env-var"],
97
- };
105
+ try {
106
+ const body = {
107
+ service: opts.service,
108
+ envVar: opts.envVar,
109
+ };
98
110
 
99
- const result = await apiRequest(
100
- "POST",
101
- `/v1/dbs/${opts["db-id"]}/link-service`,
102
- body
103
- );
111
+ const result = await apiRequest("POST", `/v1/dbs/${opts.dbId}/link-service`, body);
104
112
 
105
- if (opts.json) {
106
- console.log(JSON.stringify(result, null, 2));
107
- } else {
108
- console.log(
109
- `Linked DB ${result.dbId} to service ${result.service} as ${result.envVar}`
110
- );
113
+ if (opts.json) {
114
+ printJson(result);
115
+ } else {
116
+ console.log(
117
+ `Linked DB ${result.dbId} to service ${result.service} as ${result.envVar}`
118
+ );
119
+ }
120
+ } catch (error) {
121
+ handleError(error, { json: opts.json });
111
122
  }
112
123
  });
113
-
114
-
115
-
116
-
117
-
@@ -1,26 +1,33 @@
1
1
  import { Command } from "commander";
2
2
  import { spawn } from "child_process";
3
3
  import { apiRequest } from "../http";
4
- import path from "path";
4
+ import { handleError, printJson } from "../utils/machine";
5
+ import { resolveTunnelClientPath } from "./menu/effects/tunnel-clients";
5
6
 
6
7
  export const devCommand = new Command("dev")
7
- .description("Run local dev with optional tunnel")
8
- .option("--tunnel", "Enable tunnel")
8
+ .description("Run local tunnel client against a new tunnel (foreground)")
9
+ .option("--tunnel", "Enable tunnel", false)
9
10
  .option("--port <port>", "Local port to expose", "3000")
10
11
  .option("--json", "Output JSON", false)
11
- .option("--improved", "Use improved client with auto-reconnect and better error handling", false)
12
12
  .action(async (opts) => {
13
13
  const port = Number(opts.port);
14
- if (opts.tunnel) {
15
- // Request tunnel from control plane
14
+ if (!Number.isFinite(port) || port <= 0) {
15
+ console.error("Invalid port. Provide a positive integer.");
16
+ process.exit(2);
17
+ }
18
+
19
+ if (!opts.tunnel) {
20
+ console.log("Tunnel not enabled. Provide --tunnel to expose localhost.");
21
+ console.log("Prefer: uplink tunnel create --port <port> --json");
22
+ return;
23
+ }
24
+
25
+ try {
16
26
  const result = await apiRequest("POST", "/v1/tunnels", { port });
17
27
  if (opts.json) {
18
- console.log(JSON.stringify(result, null, 2));
28
+ printJson(result);
19
29
  } else {
20
30
  console.log(`Tunnel URL: ${result.url}`);
21
- // If the control-plane returns an https URL for the dev.uplink.spot domain,
22
- // it may not actually be reachable unless TLS is configured on the relay.
23
- // Print an HTTP fallback to reduce confusion during development.
24
31
  if (
25
32
  typeof result.url === "string" &&
26
33
  result.url.startsWith("https://") &&
@@ -30,14 +37,7 @@ export const devCommand = new Command("dev")
30
37
  }
31
38
  }
32
39
 
33
- // Spawn tunnel client (use improved version if requested)
34
- const clientFile = opts.improved ? "client-improved.js" : "client.js";
35
- const clientPath = path.join(
36
- process.cwd(),
37
- "scripts",
38
- "tunnel",
39
- clientFile
40
- );
40
+ const clientPath = resolveTunnelClientPath();
41
41
  const ctrlHost = process.env.TUNNEL_CTRL ?? "tunnel.uplink.spot:7071";
42
42
  const args = [
43
43
  clientPath,
@@ -48,10 +48,11 @@ export const devCommand = new Command("dev")
48
48
  "--ctrl",
49
49
  ctrlHost,
50
50
  ];
51
- console.log(`Starting tunnel client: node ${args.join(" ")}`);
51
+ if (!opts.json) {
52
+ console.log(`Starting tunnel client: node ${args.join(" ")}`);
53
+ }
52
54
  const child = spawn("node", args, { stdio: "inherit" });
53
55
 
54
- // Forward Ctrl+C / termination to the child so the tunnel shuts down cleanly.
55
56
  const shutdown = () => {
56
57
  try {
57
58
  child.kill("SIGINT");
@@ -62,7 +63,6 @@ export const devCommand = new Command("dev")
62
63
  process.on("SIGINT", shutdown);
63
64
  process.on("SIGTERM", shutdown);
64
65
 
65
- // Keep the CLI process alive while the tunnel client is running.
66
66
  await new Promise<void>((resolve) => {
67
67
  child.on("exit", (code, signal) => {
68
68
  process.off("SIGINT", shutdown);
@@ -78,9 +78,7 @@ export const devCommand = new Command("dev")
78
78
  resolve();
79
79
  });
80
80
  });
81
- } else {
82
- console.log("Tunnel not enabled. Provide --tunnel to expose localhost.");
81
+ } catch (error) {
82
+ handleError(error, { json: opts.json });
83
83
  }
84
84
  });
85
-
86
-