uplink-cli 0.2.2 → 0.2.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.3 — 2026-08-30
4
+
5
+ New **`uplink upgrade`** (Uplink Pro subscription — $9/mo or `--yearly` $90/yr via Stripe Checkout) and **`uplink billing`** (Stripe billing portal for cancel/card changes). Pro unlocks unlimited hosted apps within 1 GB of storage, always-on for your 5 most-active apps, custom domains, and permanent aliases.
6
+
7
+ Fix global CLI crash on every command (`host list`, `login`, …): `domains.ts` was statically importing Ink, so tsx tried to load `yoga-layout` as CommonJS (`Top-level await is currently not supported with the "cjs" output format`). Interactive Find a domain now runs as a child ESM process, same as the menu.
8
+
3
9
  ## 0.2.2 — 2026-08-29
4
10
 
5
11
  Built-in **Find a domain** in the CLI (no Domainking install). Type a label to check common TLDs via DNS/RDAP. `uplink domains` opens it; agents use `uplink domains search acme --json`. Domainking remains a separate app.
package/cli/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import { signupCommand } from "./subcommands/signup";
10
10
  import { systemCommand } from "./subcommands/system";
11
11
  import { hostCommand } from "./subcommands/host";
12
12
  import { domainsCommand } from "./subcommands/domains";
13
+ import { upgradeCommand, billingCommand } from "./subcommands/upgrade";
13
14
  import { readFileSync } from "fs";
14
15
  import { join } from "path";
15
16
  import { ensureApiBase, parseTokenEnv } from "./utils/api-base";
@@ -39,6 +40,8 @@ program.addCommand(systemCommand);
39
40
  program.addCommand(menuCommand);
40
41
  program.addCommand(hostCommand);
41
42
  program.addCommand(domainsCommand);
43
+ program.addCommand(upgradeCommand);
44
+ program.addCommand(billingCommand);
42
45
 
43
46
  // Global pre-action hook to apply shared options
44
47
  let cachedTokenStdin: string | null = null;
@@ -1,5 +1,7 @@
1
1
  import { Command } from "commander";
2
+ import { join } from "path";
2
3
  import { handleError, printJson } from "../utils/machine";
4
+ import { runEsmEntry } from "../utils/run-esm";
3
5
  import {
4
6
  adapters,
5
7
  getAdapter,
@@ -18,7 +20,14 @@ import {
18
20
  formatPublicAvailability,
19
21
  } from "../utils/domain-availability";
20
22
  import { searchDomains } from "../utils/domain-search";
21
- import { runDomainSearch } from "../tui/DomainSearch";
23
+
24
+ function runDomainSearchTui(): void {
25
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
26
+ console.log("Domain search needs a terminal. Agents: uplink domains search myapp --json");
27
+ return;
28
+ }
29
+ runEsmEntry(join(__dirname, "../tui/domain-search.mts"));
30
+ }
22
31
 
23
32
  // DreamHost last: it can only confirm ownership, not quote availability.
24
33
  const CHECK_ORDER: ProviderId[] = ["godaddy", "cloudflare", "hostinger", "namecheap", "dreamhost"];
@@ -137,9 +146,8 @@ domainsCommand.addHelpText(
137
146
  "\nWith no subcommand, opens Find a domain (type a name; common TLDs are checked via DNS/RDAP).\n"
138
147
  );
139
148
 
140
- domainsCommand.action(async () => {
141
- const message = await runDomainSearch();
142
- if (message) console.log(message);
149
+ domainsCommand.action(() => {
150
+ runDomainSearchTui();
143
151
  });
144
152
 
145
153
  domainsCommand
@@ -223,8 +231,7 @@ domainsCommand
223
231
  try {
224
232
  if (!name) {
225
233
  if (opts.json) throw new Error("Pass a name: uplink domains search acme --json");
226
- const message = await runDomainSearch();
227
- if (message) console.log(message);
234
+ runDomainSearchTui();
228
235
  return;
229
236
  }
230
237
  const results = await searchDomains(name);
@@ -1,23 +1,6 @@
1
1
  import { Command } from "commander";
2
- import { spawnSync } from "child_process";
3
2
  import { join } from "path";
4
-
5
- function projectRoot(): string {
6
- return join(__dirname, "../../..");
7
- }
8
-
9
- function resolveTsx(): string {
10
- const root = projectRoot();
11
- try {
12
- return require.resolve("tsx/dist/cli.cjs", { paths: [root] });
13
- } catch {
14
- try {
15
- return require.resolve("tsx/cli", { paths: [root] });
16
- } catch {
17
- return "tsx";
18
- }
19
- }
20
- }
3
+ import { runEsmEntry } from "../utils/run-esm";
21
4
 
22
5
  /**
23
6
  * Ink 6 is ESM-only (yoga-layout uses top-level await). The CLI package is
@@ -30,12 +13,5 @@ export const menuCommand = new Command("menu")
30
13
  console.error("Uplink menu needs an interactive terminal. Use `uplink --help` for commands.");
31
14
  process.exit(1);
32
15
  }
33
- const entry = join(__dirname, "../tui/index.mts");
34
- const result = spawnSync(resolveTsx(), [entry], {
35
- stdio: "inherit",
36
- cwd: projectRoot(),
37
- env: process.env,
38
- });
39
- if (result.error) throw result.error;
40
- process.exit(result.status ?? 0);
16
+ runEsmEntry(join(__dirname, "../tui/index.mts"));
41
17
  });
@@ -0,0 +1,62 @@
1
+ import { Command } from "commander";
2
+ import { spawn } from "child_process";
3
+ import { apiRequest } from "../http";
4
+ import { handleError, printJson } from "../utils/machine";
5
+
6
+ function openInBrowser(url: string): void {
7
+ const [cmd, args] =
8
+ process.platform === "darwin"
9
+ ? ["open", [url]]
10
+ : process.platform === "win32"
11
+ ? ["cmd", ["/c", "start", "", url]]
12
+ : ["xdg-open", [url]];
13
+ try {
14
+ spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
15
+ } catch {
16
+ // Non-fatal: the URL is printed either way.
17
+ }
18
+ }
19
+
20
+ export const upgradeCommand = new Command("upgrade")
21
+ .description("Upgrade to Uplink Pro — unlimited apps in 1 GB, 5 always-on, custom domains, aliases")
22
+ .option("--yearly", "Yearly billing (2 months free)", false)
23
+ .option("--json", "Output JSON (prints the checkout URL, does not open a browser)", false)
24
+ .action(async (opts) => {
25
+ try {
26
+ const interval = opts.yearly ? "year" : "month";
27
+ const result = await apiRequest("POST", "/v1/billing/checkout", { interval });
28
+ if (opts.json) {
29
+ printJson({ url: result.url, interval: result.interval });
30
+ return;
31
+ }
32
+ console.log("");
33
+ console.log("Uplink Pro — complete your upgrade in the browser:");
34
+ console.log("");
35
+ console.log(` ${result.url}`);
36
+ console.log("");
37
+ openInBrowser(result.url);
38
+ } catch (error) {
39
+ handleError(error, { json: opts.json });
40
+ }
41
+ });
42
+
43
+ export const billingCommand = new Command("billing")
44
+ .description("Manage your subscription (opens the Stripe billing portal)")
45
+ .option("--json", "Output JSON (prints the portal URL, does not open a browser)", false)
46
+ .action(async (opts) => {
47
+ try {
48
+ const result = await apiRequest("POST", "/v1/billing/portal");
49
+ if (opts.json) {
50
+ printJson({ url: result.url });
51
+ return;
52
+ }
53
+ console.log("");
54
+ console.log("Manage your subscription here:");
55
+ console.log("");
56
+ console.log(` ${result.url}`);
57
+ console.log("");
58
+ openInBrowser(result.url);
59
+ } catch (error) {
60
+ handleError(error, { json: opts.json });
61
+ }
62
+ });
@@ -0,0 +1,10 @@
1
+ import { runDomainSearch } from "./DomainSearch.tsx";
2
+
3
+ runDomainSearch()
4
+ .then((message) => {
5
+ if (message) console.log(message);
6
+ })
7
+ .catch((error) => {
8
+ console.error(error instanceof Error ? error.message : error);
9
+ process.exit(1);
10
+ });
@@ -0,0 +1,38 @@
1
+ import { spawnSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ export function cliPackageRoot(): string {
6
+ return join(__dirname, "../../..");
7
+ }
8
+
9
+ export function resolveTsx(): string {
10
+ const root = cliPackageRoot();
11
+ try {
12
+ return require.resolve("tsx/dist/cli.cjs", { paths: [root] });
13
+ } catch {
14
+ try {
15
+ return require.resolve("tsx/cli", { paths: [root] });
16
+ } catch {
17
+ return "tsx";
18
+ }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Ink 6 is ESM-only (yoga-layout uses top-level await). The CLI package is
24
+ * CommonJS, so Ink screens must run as a child ESM process rather than being
25
+ * imported from commander commands.
26
+ */
27
+ export function runEsmEntry(entry: string): never {
28
+ const root = cliPackageRoot();
29
+ const tsconfigPath = join(root, "tsconfig.json");
30
+ const tsxArgs = existsSync(tsconfigPath) ? ["--tsconfig", tsconfigPath, entry] : [entry];
31
+ const result = spawnSync(resolveTsx(), tsxArgs, {
32
+ stdio: "inherit",
33
+ cwd: root,
34
+ env: process.env,
35
+ });
36
+ if (result.error) throw result.error;
37
+ process.exit(result.status ?? 0);
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplink-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Software for agents — share localhost, host apps, and attach domains from the terminal. JSON-first CLI for Cursor, Claude, Codex, and Windsurf.",
5
5
  "keywords": [
6
6
  "ai-agents",