uplink-cli 0.2.8 → 0.2.9

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.9 — 2026-08-31
4
+
5
+ The menu's **My domains** now shows verified registration status (the same as `domains list --verify`) instead of "no expiry data" for zone/hosted entries. RDAP results are cached in `~/.uplink/rdap-cache.json` for 24 hours, so only the first open pays for the lookups.
6
+
3
7
  ## 0.2.8 — 2026-08-31
4
8
 
5
9
  `domains list` is now grouped by provider and honest about what it knows: cPanel entries show as **hosted** (a panel serving a site says nothing about ownership), registrar expiry dates in the past show as **EXPIRED**, and new **`--verify`** RDAP-checks every entry without registration data (DNS zones, hosted sites) to reveal lapsed domains. RDAP goes straight to each TLD registry via the IANA bootstrap, with a DNS-delegation fallback for registries that 404.
@@ -22,6 +22,7 @@ import {
22
22
  rdapRegistration,
23
23
  type RdapRegistration,
24
24
  } from "../utils/domain-availability";
25
+ import { cachedRegistrations, storeRegistrations } from "../utils/rdap-cache";
25
26
  import { searchDomains } from "../utils/domain-search";
26
27
  import {
27
28
  createNamecheapAddFundsRequest,
@@ -140,9 +141,14 @@ function formatDay(date: Date): string {
140
141
 
141
142
  const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
142
143
 
143
- /** RDAP-check registration for domains whose source can't attest ownership. */
144
+ /**
145
+ * RDAP-check registration for domains whose source can't attest ownership.
146
+ * Fresh results (24h) come from the local cache, so repeat runs are instant.
147
+ */
144
148
  async function verifyRegistrations(domains: string[]): Promise<Map<string, RdapRegistration>> {
145
- const out = new Map<string, RdapRegistration>();
149
+ const unique = [...new Set(domains)];
150
+ const out = cachedRegistrations(unique);
151
+ const missing = unique.filter((domain) => !out.has(domain));
146
152
  const run = async (targets: string[], batchSize: number, pauseMs: number) => {
147
153
  for (let i = 0; i < targets.length; i += batchSize) {
148
154
  if (i > 0) await sleep(pauseMs);
@@ -150,13 +156,14 @@ async function verifyRegistrations(domains: string[]): Promise<Map<string, RdapR
150
156
  for (const result of results) out.set(result.domain, result);
151
157
  }
152
158
  };
153
- await run([...new Set(domains)], 4, 400);
154
- // rdap.org rate-limits bursts; give inconclusive lookups one slower retry.
159
+ await run(missing, 4, 400);
160
+ // Registries rate-limit bursts; give inconclusive lookups one slower retry.
155
161
  const inconclusive = [...out.values()].filter((r) => r.registered === null).map((r) => r.domain);
156
162
  if (inconclusive.length > 0) {
157
163
  await sleep(2000);
158
164
  await run(inconclusive, 2, 1000);
159
165
  }
166
+ if (missing.length > 0) storeRegistrations(out.values());
160
167
  return out;
161
168
  }
162
169
 
@@ -302,8 +309,8 @@ domainsCommand
302
309
  console.log("");
303
310
  }
304
311
  if (!opts.verify && domains.some((item) => !parseExpiry(item.expiresAt))) {
305
- console.log("Entries without expiry come from DNS zones / hosted sites and may be lapsed.");
306
- console.log("Check actual registration: uplink domains list --verify");
312
+ console.log("Entries without expiry come from DNS zones / hosted sites, which don't include");
313
+ console.log("registration data. Confirm each one: uplink domains list --verify");
307
314
  }
308
315
  } catch (error) {
309
316
  handleError(error, { json: opts.json });
@@ -58,7 +58,9 @@ export function buildDomainsMenu(deps: Deps): MenuChoice {
58
58
  label: "My domains",
59
59
  action: async () => {
60
60
  try {
61
- const output = runCliCapture(["domains", "list"]);
61
+ // --verify resolves registration for zone/hosted entries; results
62
+ // are cached for a day, so only the first open is slow.
63
+ const output = runCliCapture(["domains", "list", "--verify"]);
62
64
  restoreRawMode();
63
65
  return output || "No domains. Connect a registrar first.";
64
66
  } catch (error) {
@@ -0,0 +1,75 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import type { RdapRegistration } from "./domain-availability";
5
+
6
+ /**
7
+ * Registration status changes rarely, and registry RDAP lookups are slow in
8
+ * bulk. Cache results for a day so `domains list --verify` (and the menu's
9
+ * My domains view) is instant after the first check.
10
+ */
11
+
12
+ const TTL_MS = 24 * 60 * 60 * 1000;
13
+
14
+ type CacheEntry = {
15
+ registered: boolean | null;
16
+ expiresAt?: string;
17
+ detail?: string;
18
+ checkedAt: number;
19
+ };
20
+
21
+ type CacheFile = Record<string, CacheEntry>;
22
+
23
+ function cachePath(): string {
24
+ return join(homedir(), ".uplink", "rdap-cache.json");
25
+ }
26
+
27
+ function loadCache(): CacheFile {
28
+ const path = cachePath();
29
+ if (!existsSync(path)) return {};
30
+ try {
31
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as CacheFile;
32
+ return parsed && typeof parsed === "object" ? parsed : {};
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ export function cachedRegistrations(domains: string[]): Map<string, RdapRegistration> {
39
+ const cache = loadCache();
40
+ const now = Date.now();
41
+ const out = new Map<string, RdapRegistration>();
42
+ for (const domain of domains) {
43
+ const entry = cache[domain];
44
+ // Inconclusive results (registered === null) are never served from cache.
45
+ if (!entry || entry.registered === null || now - entry.checkedAt > TTL_MS) continue;
46
+ out.set(domain, {
47
+ domain,
48
+ registered: entry.registered,
49
+ ...(entry.expiresAt ? { expiresAt: entry.expiresAt } : {}),
50
+ ...(entry.detail ? { detail: entry.detail } : {}),
51
+ });
52
+ }
53
+ return out;
54
+ }
55
+
56
+ export function storeRegistrations(results: Iterable<RdapRegistration>): void {
57
+ const cache = loadCache();
58
+ const now = Date.now();
59
+ for (const result of results) {
60
+ if (result.registered === null) continue;
61
+ cache[result.domain] = {
62
+ registered: result.registered,
63
+ ...(result.expiresAt ? { expiresAt: result.expiresAt } : {}),
64
+ ...(result.detail ? { detail: result.detail } : {}),
65
+ checkedAt: now,
66
+ };
67
+ }
68
+ mkdirSync(join(homedir(), ".uplink"), { recursive: true });
69
+ writeFileSync(cachePath(), JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 0o600 });
70
+ try {
71
+ chmodSync(cachePath(), 0o600);
72
+ } catch {
73
+ /* ignore */
74
+ }
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uplink-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
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",