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.
- package/AGENTS.md +177 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +72 -52
- package/cli/src/index.ts +16 -3
- package/cli/src/registrars/cloudflare.ts +148 -0
- package/cli/src/registrars/dreamhost.ts +129 -0
- package/cli/src/registrars/godaddy.ts +105 -0
- package/cli/src/registrars/hostinger.ts +106 -0
- package/cli/src/registrars/http.ts +18 -0
- package/cli/src/registrars/index.ts +32 -0
- package/cli/src/registrars/namecheap.ts +163 -0
- package/cli/src/registrars/secret.ts +66 -0
- package/cli/src/registrars/store.ts +55 -0
- package/cli/src/registrars/types.ts +42 -0
- package/cli/src/subcommands/admin.ts +17 -30
- package/cli/src/subcommands/db.ts +63 -57
- package/cli/src/subcommands/dev.ts +23 -25
- package/cli/src/subcommands/domains.ts +295 -0
- package/cli/src/subcommands/host-domains.ts +148 -0
- package/cli/src/subcommands/host.ts +3 -0
- package/cli/src/subcommands/login.ts +85 -0
- package/cli/src/subcommands/menu/colors.ts +1 -1
- package/cli/src/subcommands/menu/effects/tunnel-clients.ts +87 -14
- package/cli/src/subcommands/menu/inline-tree-select.ts +6 -5
- package/cli/src/subcommands/menu/io.ts +27 -5
- package/cli/src/subcommands/menu/menus/domain-check.ts +34 -0
- package/cli/src/subcommands/menu/menus/domains.ts +197 -0
- package/cli/src/subcommands/menu/menus/hosting.ts +14 -46
- package/cli/src/subcommands/menu/menus/index.ts +1 -0
- package/cli/src/subcommands/menu/menus/tunnels.ts +25 -67
- package/cli/src/subcommands/menu/render.ts +2 -2
- package/cli/src/subcommands/menu/requests.ts +9 -2
- package/cli/src/subcommands/menu/tests.ts +1 -1
- package/cli/src/subcommands/menu/tunnels.ts +10 -99
- package/cli/src/subcommands/menu/types.ts +8 -0
- package/cli/src/subcommands/menu.ts +32 -524
- package/cli/src/subcommands/signup.ts +2 -2
- package/cli/src/subcommands/system.ts +58 -36
- package/cli/src/subcommands/tunnel.ts +126 -33
- package/cli/src/templates/index.ts +3 -3
- package/cli/src/tui/App.tsx +202 -0
- package/cli/src/tui/AppInspector.tsx +114 -0
- package/cli/src/tui/HomeStatus.tsx +92 -0
- package/cli/src/tui/brand.tsx +20 -0
- package/cli/src/tui/format.ts +22 -0
- package/cli/src/tui/index.mts +6 -0
- package/cli/src/tui/liveTree.ts +40 -0
- package/cli/src/tui/package.json +3 -0
- package/cli/src/tui/runMenu.tsx +57 -0
- package/cli/src/tui/session.mts +267 -0
- package/cli/src/tui/snapshot.ts +175 -0
- package/cli/src/utils/api-base.ts +11 -0
- package/cli/src/utils/credentials.ts +58 -0
- package/cli/src/utils/domain-availability.ts +56 -0
- package/cli/src/utils/guest-access.ts +38 -0
- package/cli/src/utils/launchDomainking.ts +64 -0
- package/cli/src/utils/login-flow.ts +57 -0
- package/docs/AGENTS.md +130 -148
- package/docs/HOSTING.md +55 -0
- package/docs/MENU_STRUCTURE.md +60 -288
- package/docs/PRODUCT.md +64 -0
- package/docs/README.md +11 -7
- package/package.json +22 -36
- package/scripts/tunnel/client-improved.js +127 -38
- package/scripts/tunnel/client.js +118 -0
- 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
|
|
67
|
-
|
|
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
|
|
122
|
-
|
|
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
|
|
180
|
-
|
|
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
|
|
226
|
-
|
|
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
|
|
291
|
-
|
|
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(
|
|
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
|
|
323
|
-
|
|
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(
|
|
336
|
+
process.exit(2);
|
|
348
337
|
}
|
|
349
|
-
} catch (error
|
|
350
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
console.log(
|
|
27
|
-
}
|
|
28
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
try {
|
|
106
|
+
const body = {
|
|
107
|
+
service: opts.service,
|
|
108
|
+
envVar: opts.envVar,
|
|
109
|
+
};
|
|
98
110
|
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
|
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 (
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
82
|
-
|
|
81
|
+
} catch (error) {
|
|
82
|
+
handleError(error, { json: opts.json });
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
|
-
|
|
86
|
-
|