uplink-cli 0.1.0-alpha.1
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/README.md +205 -0
- package/cli/bin/uplink.js +55 -0
- package/cli/src/http.ts +60 -0
- package/cli/src/index.ts +32 -0
- package/cli/src/subcommands/admin.ts +351 -0
- package/cli/src/subcommands/db.ts +117 -0
- package/cli/src/subcommands/dev.ts +86 -0
- package/cli/src/subcommands/menu.ts +1222 -0
- package/cli/src/utils/port-scanner.ts +98 -0
- package/package.json +71 -0
- package/scripts/tunnel/client-improved.js +404 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiRequest } from "../http";
|
|
3
|
+
|
|
4
|
+
export const dbCommand = new Command("db").description("Manage databases");
|
|
5
|
+
|
|
6
|
+
dbCommand
|
|
7
|
+
.command("create")
|
|
8
|
+
.description("Create a new database")
|
|
9
|
+
.requiredOption("--name <name>", "Database name")
|
|
10
|
+
.requiredOption("--project <project>", "Project name")
|
|
11
|
+
.option("--provider <provider>", "Provider", "neon")
|
|
12
|
+
.option("--region <region>", "Region", "eu-central-1")
|
|
13
|
+
.option("--plan <plan>", "Plan", "dev")
|
|
14
|
+
.option("--json", "Output JSON", false)
|
|
15
|
+
.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
|
+
};
|
|
23
|
+
|
|
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}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
dbCommand
|
|
33
|
+
.command("list")
|
|
34
|
+
.description("List databases")
|
|
35
|
+
.option("--project <project>", "Project name")
|
|
36
|
+
.option("--json", "Output JSON", false)
|
|
37
|
+
.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
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
dbCommand
|
|
52
|
+
.command("info")
|
|
53
|
+
.description("Get details for a database")
|
|
54
|
+
.requiredOption("--id <id>", "Database id")
|
|
55
|
+
.option("--json", "Output JSON", false)
|
|
56
|
+
.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}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
dbCommand
|
|
69
|
+
.command("delete")
|
|
70
|
+
.description("Delete a database")
|
|
71
|
+
.requiredOption("--id <id>", "Database id")
|
|
72
|
+
.option("--yes", "Confirm deletion", false)
|
|
73
|
+
.option("--json", "Output JSON", false)
|
|
74
|
+
.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}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
dbCommand
|
|
87
|
+
.command("link")
|
|
88
|
+
.description("Link a DB to a service via env var")
|
|
89
|
+
.requiredOption("--db-id <id>", "Database id")
|
|
90
|
+
.requiredOption("--service <service>", "Service name")
|
|
91
|
+
.requiredOption("--env-var <envVar>", "Environment variable name")
|
|
92
|
+
.option("--json", "Output JSON", false)
|
|
93
|
+
.action(async (opts) => {
|
|
94
|
+
const body = {
|
|
95
|
+
service: opts.service,
|
|
96
|
+
envVar: opts["env-var"],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = await apiRequest(
|
|
100
|
+
"POST",
|
|
101
|
+
`/v1/dbs/${opts["db-id"]}/link-service`,
|
|
102
|
+
body
|
|
103
|
+
);
|
|
104
|
+
|
|
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
|
+
);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { apiRequest } from "../http";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
export const devCommand = new Command("dev")
|
|
7
|
+
.description("Run local dev with optional tunnel")
|
|
8
|
+
.option("--tunnel", "Enable tunnel")
|
|
9
|
+
.option("--port <port>", "Local port to expose", "3000")
|
|
10
|
+
.option("--json", "Output JSON", false)
|
|
11
|
+
.option("--improved", "Use improved client with auto-reconnect and better error handling", false)
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const port = Number(opts.port);
|
|
14
|
+
if (opts.tunnel) {
|
|
15
|
+
// Request tunnel from control plane
|
|
16
|
+
const result = await apiRequest("POST", "/v1/tunnels", { port });
|
|
17
|
+
if (opts.json) {
|
|
18
|
+
console.log(JSON.stringify(result, null, 2));
|
|
19
|
+
} else {
|
|
20
|
+
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
|
+
if (
|
|
25
|
+
typeof result.url === "string" &&
|
|
26
|
+
result.url.startsWith("https://") &&
|
|
27
|
+
result.url.includes(".dev.uplink.spot")
|
|
28
|
+
) {
|
|
29
|
+
console.log(`HTTP URL (if HTTPS not enabled): ${result.url.replace(/^https:\/\//, "http://")}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
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
|
+
);
|
|
41
|
+
const ctrlHost = process.env.TUNNEL_CTRL ?? "127.0.0.1:7071";
|
|
42
|
+
const args = [
|
|
43
|
+
clientPath,
|
|
44
|
+
"--token",
|
|
45
|
+
result.token,
|
|
46
|
+
"--port",
|
|
47
|
+
String(port),
|
|
48
|
+
"--ctrl",
|
|
49
|
+
ctrlHost,
|
|
50
|
+
];
|
|
51
|
+
console.log(`Starting tunnel client: node ${args.join(" ")}`);
|
|
52
|
+
const child = spawn("node", args, { stdio: "inherit" });
|
|
53
|
+
|
|
54
|
+
// Forward Ctrl+C / termination to the child so the tunnel shuts down cleanly.
|
|
55
|
+
const shutdown = () => {
|
|
56
|
+
try {
|
|
57
|
+
child.kill("SIGINT");
|
|
58
|
+
} catch {
|
|
59
|
+
/* ignore */
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
process.on("SIGINT", shutdown);
|
|
63
|
+
process.on("SIGTERM", shutdown);
|
|
64
|
+
|
|
65
|
+
// Keep the CLI process alive while the tunnel client is running.
|
|
66
|
+
await new Promise<void>((resolve) => {
|
|
67
|
+
child.on("exit", (code, signal) => {
|
|
68
|
+
process.off("SIGINT", shutdown);
|
|
69
|
+
process.off("SIGTERM", shutdown);
|
|
70
|
+
|
|
71
|
+
if (signal) {
|
|
72
|
+
console.error(`Tunnel client exited due to signal ${signal}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
} else if (code && code !== 0) {
|
|
75
|
+
console.error(`Tunnel client exited with code ${code}`);
|
|
76
|
+
process.exitCode = code;
|
|
77
|
+
}
|
|
78
|
+
resolve();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
console.log("Tunnel not enabled. Provide --tunnel to expose localhost.");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
|