lovieme 0.0.0-stage → 0.1.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 +101 -3
- package/bin/lovieme.js +9 -0
- package/dist/client.d.ts +104 -0
- package/dist/client.js +127 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +250 -0
- package/dist/ui.d.ts +17 -0
- package/dist/ui.js +38 -0
- package/package.json +42 -6
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# 🚀 LovieMe Developer CLI (`lovieme`)
|
|
2
|
+
|
|
3
|
+
The official command-line interface for [LovieMe](https://www.lovie.me) — claim free `*.lovie.me` subdomains, manage global Anycast DNS records, and wire up 1-click cloud hosting presets directly from your terminal or CI/CD pipelines.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚡ Zero Installation (Run with `npx`)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Check subdomain availability
|
|
11
|
+
npx lovieme subdomains check my-app
|
|
12
|
+
|
|
13
|
+
# Run live multi-resolver DoH propagation check
|
|
14
|
+
npx lovieme verify my-app
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 📦 Global Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g lovieme
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🔑 Authentication
|
|
28
|
+
|
|
29
|
+
Log in with your LovieMe API key (generate in [Dashboard > API Keys](https://www.lovie.me/dashboard/api-keys)):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Save token locally to ~/.lovieme/config.json
|
|
33
|
+
lovieme login lovie_live_your_token_here
|
|
34
|
+
|
|
35
|
+
# Inspect your account and quotas
|
|
36
|
+
lovieme whoami
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or set the environment variable:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export LOVIE_API_KEY="lovie_live_your_token_here"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 📖 Command Reference
|
|
48
|
+
|
|
49
|
+
### Subdomains
|
|
50
|
+
```bash
|
|
51
|
+
# Check availability
|
|
52
|
+
lovieme subdomains check <name>
|
|
53
|
+
|
|
54
|
+
# Claim a subdomain
|
|
55
|
+
lovieme subdomains claim <name>
|
|
56
|
+
|
|
57
|
+
# List your claimed subdomains
|
|
58
|
+
lovieme subdomains list
|
|
59
|
+
|
|
60
|
+
# Delete/release a subdomain
|
|
61
|
+
lovieme subdomains delete <name>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Anycast DNS Records
|
|
65
|
+
```bash
|
|
66
|
+
# List records for a subdomain
|
|
67
|
+
lovieme dns list <subdomain>
|
|
68
|
+
|
|
69
|
+
# Add a CNAME record (points @ to Vercel)
|
|
70
|
+
lovieme dns add <subdomain> --type CNAME --name @ --content cname.vercel-dns.com
|
|
71
|
+
|
|
72
|
+
# Add an A record (points api to your VPS)
|
|
73
|
+
lovieme dns add <subdomain> --type A --name api --content 192.0.2.1
|
|
74
|
+
|
|
75
|
+
# Delete a record by ID
|
|
76
|
+
lovieme dns rm <id>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 1-Click Hosting Presets
|
|
80
|
+
```bash
|
|
81
|
+
# List all verified presets
|
|
82
|
+
lovieme presets list
|
|
83
|
+
|
|
84
|
+
# Apply Vercel preset
|
|
85
|
+
lovieme presets apply <subdomain> vercel
|
|
86
|
+
|
|
87
|
+
# Apply GitHub Pages preset
|
|
88
|
+
lovieme presets apply <subdomain> github-pages --target username.github.io
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Global DoH Radar Verification
|
|
92
|
+
```bash
|
|
93
|
+
# Verify propagation worldwide (Google, Cloudflare, Quad9)
|
|
94
|
+
lovieme verify <subdomain> --type CNAME
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 📄 License
|
|
100
|
+
MIT © Sagar Kewat (Sagarithm)
|
|
101
|
+
|
package/bin/lovieme.js
ADDED
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export declare class CliClient {
|
|
2
|
+
private baseUrl;
|
|
3
|
+
private apiKey;
|
|
4
|
+
constructor();
|
|
5
|
+
private request;
|
|
6
|
+
checkSubdomain(name: string): Promise<{
|
|
7
|
+
available: boolean;
|
|
8
|
+
name: string;
|
|
9
|
+
fqdn: string;
|
|
10
|
+
reason?: string;
|
|
11
|
+
}>;
|
|
12
|
+
getProfile(): Promise<{
|
|
13
|
+
profile: {
|
|
14
|
+
id: string;
|
|
15
|
+
email?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
plan: string;
|
|
18
|
+
quota: {
|
|
19
|
+
subdomains_max: number;
|
|
20
|
+
subdomains_used: number;
|
|
21
|
+
subdomains_remaining: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
listSubdomains(): Promise<{
|
|
26
|
+
subdomains: Array<{
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
fqdn: string;
|
|
30
|
+
created_at: string;
|
|
31
|
+
dns_records_count: number;
|
|
32
|
+
email_forwarder_active: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
}>;
|
|
35
|
+
claimSubdomain(name: string): Promise<{
|
|
36
|
+
success: boolean;
|
|
37
|
+
message: string;
|
|
38
|
+
subdomain: {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
fqdn: string;
|
|
42
|
+
};
|
|
43
|
+
}>;
|
|
44
|
+
deleteSubdomain(name: string): Promise<{
|
|
45
|
+
success: boolean;
|
|
46
|
+
message: string;
|
|
47
|
+
}>;
|
|
48
|
+
listDnsRecords(subdomain: string): Promise<{
|
|
49
|
+
dns_records: Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
type: string;
|
|
52
|
+
name: string;
|
|
53
|
+
content: string;
|
|
54
|
+
ttl: number;
|
|
55
|
+
priority?: number;
|
|
56
|
+
}>;
|
|
57
|
+
}>;
|
|
58
|
+
addDnsRecord(data: {
|
|
59
|
+
subdomain: string;
|
|
60
|
+
type: string;
|
|
61
|
+
name: string;
|
|
62
|
+
content: string;
|
|
63
|
+
ttl?: number;
|
|
64
|
+
priority?: number;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
success: boolean;
|
|
67
|
+
message: string;
|
|
68
|
+
record: {
|
|
69
|
+
id: string;
|
|
70
|
+
};
|
|
71
|
+
}>;
|
|
72
|
+
removeDnsRecord(id: string): Promise<{
|
|
73
|
+
success: boolean;
|
|
74
|
+
message: string;
|
|
75
|
+
}>;
|
|
76
|
+
listPresets(): Promise<{
|
|
77
|
+
presets: Array<{
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
category?: string;
|
|
81
|
+
description: string;
|
|
82
|
+
requiresCustomInput?: boolean;
|
|
83
|
+
inputLabel?: string;
|
|
84
|
+
documentationUrl?: string;
|
|
85
|
+
}>;
|
|
86
|
+
}>;
|
|
87
|
+
applyPreset(subdomain: string, preset: string, customTarget?: string): Promise<{
|
|
88
|
+
success: boolean;
|
|
89
|
+
message: string;
|
|
90
|
+
created_records: Array<{
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
type: string;
|
|
94
|
+
content: string;
|
|
95
|
+
}>;
|
|
96
|
+
}>;
|
|
97
|
+
verifyDoH(domain: string, type?: string): Promise<{
|
|
98
|
+
resolver: string;
|
|
99
|
+
status: string;
|
|
100
|
+
latency: string;
|
|
101
|
+
targets: string;
|
|
102
|
+
}[]>;
|
|
103
|
+
private ensureAuth;
|
|
104
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import dns from "node:dns";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
try {
|
|
4
|
+
dns.setDefaultResultOrder?.("ipv4first");
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
// Ignore in environments where setDefaultResultOrder is unsupported
|
|
8
|
+
}
|
|
9
|
+
export class CliClient {
|
|
10
|
+
baseUrl;
|
|
11
|
+
apiKey;
|
|
12
|
+
constructor() {
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
this.apiKey = config.apiKey || "";
|
|
15
|
+
this.baseUrl = (config.apiUrl || "https://www.lovie.me").replace(/\/+$/, "");
|
|
16
|
+
}
|
|
17
|
+
async request(path, options = {}) {
|
|
18
|
+
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
19
|
+
const headers = {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
"User-Agent": "LovieMe-CLI/0.1.1",
|
|
22
|
+
...(options.headers || {}),
|
|
23
|
+
};
|
|
24
|
+
if (this.apiKey) {
|
|
25
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
26
|
+
}
|
|
27
|
+
const res = await fetch(url, { ...options, headers });
|
|
28
|
+
const isJson = (res.headers.get("content-type") || "").includes("application/json");
|
|
29
|
+
const payload = isJson ? await res.json() : await res.text();
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const msg = (typeof payload === "object" && payload !== null && (payload.error || payload.message)) ||
|
|
32
|
+
`HTTP ${res.status}: ${res.statusText}`;
|
|
33
|
+
throw new Error(String(msg));
|
|
34
|
+
}
|
|
35
|
+
return payload;
|
|
36
|
+
}
|
|
37
|
+
async checkSubdomain(name) {
|
|
38
|
+
const clean = name.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
39
|
+
return this.request(`/api/v1/subdomains/check?name=${encodeURIComponent(clean)}`);
|
|
40
|
+
}
|
|
41
|
+
async getProfile() {
|
|
42
|
+
this.ensureAuth();
|
|
43
|
+
return this.request("/api/v1/me");
|
|
44
|
+
}
|
|
45
|
+
async listSubdomains() {
|
|
46
|
+
this.ensureAuth();
|
|
47
|
+
return this.request("/api/v1/subdomains");
|
|
48
|
+
}
|
|
49
|
+
async claimSubdomain(name) {
|
|
50
|
+
this.ensureAuth();
|
|
51
|
+
const clean = name.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
52
|
+
return this.request("/api/v1/subdomains", { method: "POST", body: JSON.stringify({ name: clean }) });
|
|
53
|
+
}
|
|
54
|
+
async deleteSubdomain(name) {
|
|
55
|
+
this.ensureAuth();
|
|
56
|
+
const clean = name.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
57
|
+
return this.request(`/api/v1/subdomains/${encodeURIComponent(clean)}`, {
|
|
58
|
+
method: "DELETE",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async listDnsRecords(subdomain) {
|
|
62
|
+
this.ensureAuth();
|
|
63
|
+
const clean = subdomain.replace(/\.lovie\.me$/i, "").trim().toLowerCase();
|
|
64
|
+
return this.request(`/api/v1/dns/records?subdomain=${encodeURIComponent(clean)}`);
|
|
65
|
+
}
|
|
66
|
+
async addDnsRecord(data) {
|
|
67
|
+
this.ensureAuth();
|
|
68
|
+
return this.request("/api/v1/dns/records", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify(data),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async removeDnsRecord(id) {
|
|
74
|
+
this.ensureAuth();
|
|
75
|
+
return this.request(`/api/v1/dns/records/${encodeURIComponent(id)}`, {
|
|
76
|
+
method: "DELETE",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async listPresets() {
|
|
80
|
+
return this.request("/api/v1/dns/presets");
|
|
81
|
+
}
|
|
82
|
+
async applyPreset(subdomain, preset, customTarget) {
|
|
83
|
+
this.ensureAuth();
|
|
84
|
+
return this.request("/api/v1/dns/presets", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: JSON.stringify({ subdomain, preset, customTarget }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async verifyDoH(domain, type = "A") {
|
|
90
|
+
const clean = domain.trim().toLowerCase();
|
|
91
|
+
const fqdn = clean === "lovie.me" || clean.endsWith(".lovie.me") ? clean : `${clean}.lovie.me`;
|
|
92
|
+
const resolvers = [
|
|
93
|
+
{ name: "Google Public DNS", url: `https://dns.google/resolve?name=${encodeURIComponent(fqdn)}&type=${type}` },
|
|
94
|
+
{ name: "Cloudflare (1.1.1.1)", url: `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(fqdn)}&type=${type}` },
|
|
95
|
+
{ name: "AliDNS Global", url: `https://dns.alidns.com/resolve?name=${encodeURIComponent(fqdn)}&type=${type}` },
|
|
96
|
+
];
|
|
97
|
+
const settled = await Promise.allSettled(resolvers.map(async (r) => {
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
const res = await fetch(r.url, { headers: { Accept: "application/dns-json" } });
|
|
100
|
+
const latency = Date.now() - start;
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
throw new Error(`HTTP ${res.status}`);
|
|
103
|
+
const data = (await res.json());
|
|
104
|
+
return {
|
|
105
|
+
resolver: r.name,
|
|
106
|
+
status: data.Answer && data.Answer.length > 0 ? "RESOLVED" : "NOT FOUND",
|
|
107
|
+
latency: `${latency}ms`,
|
|
108
|
+
targets: (data.Answer || []).map((a) => a.data).join(", ") || "-",
|
|
109
|
+
};
|
|
110
|
+
}));
|
|
111
|
+
return settled.map((s, i) => {
|
|
112
|
+
if (s.status === "fulfilled")
|
|
113
|
+
return s.value;
|
|
114
|
+
return {
|
|
115
|
+
resolver: resolvers[i].name,
|
|
116
|
+
status: "ERROR",
|
|
117
|
+
latency: "-",
|
|
118
|
+
targets: "-",
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
ensureAuth() {
|
|
123
|
+
if (!this.apiKey) {
|
|
124
|
+
throw new Error("Authentication required. Run `lovieme login <token>` or set LOVIE_API_KEY environment variable.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".lovieme");
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function getConfig() {
|
|
7
|
+
const envKey = process.env.LOVIE_API_KEY;
|
|
8
|
+
const envUrl = process.env.LOVIE_API_URL;
|
|
9
|
+
let fileConfig = {};
|
|
10
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
13
|
+
fileConfig = JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Ignore corrupted config
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
apiKey: envKey || fileConfig.apiKey || "",
|
|
21
|
+
apiUrl: envUrl || fileConfig.apiUrl || "https://www.lovie.me",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function saveApiKey(key, url) {
|
|
25
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
26
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
const existing = getConfig();
|
|
29
|
+
const nextConfig = {
|
|
30
|
+
...existing,
|
|
31
|
+
apiKey: key.trim(),
|
|
32
|
+
apiUrl: url || existing.apiUrl || "https://www.lovie.me",
|
|
33
|
+
};
|
|
34
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(nextConfig, null, 2), "utf-8");
|
|
35
|
+
}
|
|
36
|
+
export function clearConfig() {
|
|
37
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
38
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { CliClient } from "./client.js";
|
|
3
|
+
import { saveApiKey, clearConfig } from "./config.js";
|
|
4
|
+
import { logSuccess, logInfo, logError, formatTable, colors } from "./ui.js";
|
|
5
|
+
export function createProgram(client) {
|
|
6
|
+
const cli = client || new CliClient();
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("lovieme")
|
|
10
|
+
.description("Official CLI for LovieMe — Developer & AI-Agent Custom Subdomains & Anycast DNS Cloud")
|
|
11
|
+
.version("0.1.1");
|
|
12
|
+
// 1. Auth: login / logout / whoami
|
|
13
|
+
program
|
|
14
|
+
.command("login")
|
|
15
|
+
.description("Log in to LovieMe using your personal or AI agent API key (lovie_live_...)")
|
|
16
|
+
.argument("<key>", "API key secret")
|
|
17
|
+
.action((key) => {
|
|
18
|
+
saveApiKey(key);
|
|
19
|
+
logSuccess("Saved API key to ~/.lovieme/config.json");
|
|
20
|
+
});
|
|
21
|
+
program
|
|
22
|
+
.command("logout")
|
|
23
|
+
.description("Remove stored credentials")
|
|
24
|
+
.action(() => {
|
|
25
|
+
clearConfig();
|
|
26
|
+
logSuccess("Logged out and purged ~/.lovieme/config.json");
|
|
27
|
+
});
|
|
28
|
+
program
|
|
29
|
+
.command("whoami")
|
|
30
|
+
.description("Show authenticated profile, plan tier, and subdomain quotas")
|
|
31
|
+
.action(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const { profile } = await cli.getProfile();
|
|
34
|
+
console.log(`\n${colors.bold}LovieMe Account Profile${colors.reset}`);
|
|
35
|
+
console.log(` User: ${profile.name || profile.email || profile.id}`);
|
|
36
|
+
console.log(` Plan: ${colors.green}${profile.plan.toUpperCase()}${colors.reset}`);
|
|
37
|
+
console.log(` Subdomains Used: ${profile.quota.subdomains_used} / ${profile.quota.subdomains_max}`);
|
|
38
|
+
console.log(` Remaining: ${profile.quota.subdomains_remaining}\n`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// 2. Subdomains: check / claim / list / rm
|
|
46
|
+
const subdomains = program.command("subdomains").alias("sub").description("Manage custom *.lovie.me subdomains");
|
|
47
|
+
subdomains
|
|
48
|
+
.command("check <name>")
|
|
49
|
+
.description("Check availability of a subdomain prefix")
|
|
50
|
+
.action(async (name) => {
|
|
51
|
+
try {
|
|
52
|
+
const res = await cli.checkSubdomain(name);
|
|
53
|
+
if (res.available) {
|
|
54
|
+
logSuccess(`${colors.bold}${res.fqdn}${colors.reset} is AVAILABLE!`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
logError(`${colors.bold}${res.fqdn}${colors.reset} is NOT available: ${res.reason || "already registered"}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
subdomains
|
|
66
|
+
.command("claim <name>")
|
|
67
|
+
.description("Claim an available subdomain prefix under your account")
|
|
68
|
+
.action(async (name) => {
|
|
69
|
+
try {
|
|
70
|
+
const res = await cli.claimSubdomain(name);
|
|
71
|
+
logSuccess(`Subdomain claimed: ${colors.bold}${res.subdomain.fqdn}${colors.reset}`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
subdomains
|
|
79
|
+
.command("list")
|
|
80
|
+
.description("List all subdomains owned by your account")
|
|
81
|
+
.action(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const { subdomains: list } = await cli.listSubdomains();
|
|
84
|
+
if (list.length === 0) {
|
|
85
|
+
logInfo("You have no claimed subdomains yet. Use `lovieme subdomains claim <name>`.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const headers = ["NAME", "FQDN", "DNS RECORDS", "FORWARDER", "CREATED AT"];
|
|
89
|
+
const rows = list.map((s) => [
|
|
90
|
+
s.name,
|
|
91
|
+
s.fqdn,
|
|
92
|
+
String(s.dns_records_count),
|
|
93
|
+
s.email_forwarder_active ? "Active" : "None",
|
|
94
|
+
new Date(s.created_at).toISOString().slice(0, 10),
|
|
95
|
+
]);
|
|
96
|
+
console.log(`\n${formatTable(headers, rows)}\n`);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
subdomains
|
|
104
|
+
.command("delete <name>")
|
|
105
|
+
.alias("rm")
|
|
106
|
+
.description("Release and permanently delete a subdomain")
|
|
107
|
+
.action(async (name) => {
|
|
108
|
+
try {
|
|
109
|
+
await cli.deleteSubdomain(name);
|
|
110
|
+
logSuccess(`Deleted subdomain: ${name}.lovie.me`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// 3. DNS: list / add / rm
|
|
118
|
+
const dns = program.command("dns").description("Manage Gcore Anycast DNS records for your subdomains");
|
|
119
|
+
dns
|
|
120
|
+
.command("list <subdomain>")
|
|
121
|
+
.description("List all active DNS records for a subdomain")
|
|
122
|
+
.action(async (subdomain) => {
|
|
123
|
+
try {
|
|
124
|
+
const { dns_records: records } = await cli.listDnsRecords(subdomain);
|
|
125
|
+
if (records.length === 0) {
|
|
126
|
+
logInfo(`No DNS records found for ${subdomain}.lovie.me.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const headers = ["ID", "TYPE", "NAME", "CONTENT", "TTL", "PRIORITY"];
|
|
130
|
+
const rows = records.map((r) => [
|
|
131
|
+
r.id.slice(0, 8),
|
|
132
|
+
r.type,
|
|
133
|
+
r.name,
|
|
134
|
+
r.content,
|
|
135
|
+
String(r.ttl),
|
|
136
|
+
r.priority !== undefined && r.priority !== null ? String(r.priority) : "-",
|
|
137
|
+
]);
|
|
138
|
+
console.log(`\n${formatTable(headers, rows)}\n`);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
dns
|
|
146
|
+
.command("add <subdomain>")
|
|
147
|
+
.description("Create a new DNS record on Anycast nameservers")
|
|
148
|
+
.requiredOption("-t, --type <type>", "Record type (A, AAAA, CNAME, TXT, MX)")
|
|
149
|
+
.requiredOption("-n, --name <name>", "Host label (@, www, api, etc.)")
|
|
150
|
+
.requiredOption("-c, --content <content>", "Target IP or host")
|
|
151
|
+
.option("--ttl <ttl>", "TTL in seconds", "300")
|
|
152
|
+
.option("-p, --priority <priority>", "Priority (required for MX)")
|
|
153
|
+
.action(async (subdomain, opts) => {
|
|
154
|
+
try {
|
|
155
|
+
await cli.addDnsRecord({
|
|
156
|
+
subdomain,
|
|
157
|
+
type: opts.type.toUpperCase(),
|
|
158
|
+
name: opts.name,
|
|
159
|
+
content: opts.content,
|
|
160
|
+
ttl: parseInt(opts.ttl, 10),
|
|
161
|
+
priority: opts.priority ? parseInt(opts.priority, 10) : undefined,
|
|
162
|
+
});
|
|
163
|
+
logSuccess(`Created ${opts.type.toUpperCase()} record on ${opts.name}.${subdomain}.lovie.me`);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
dns
|
|
171
|
+
.command("rm <id>")
|
|
172
|
+
.description("Delete a DNS record by ID")
|
|
173
|
+
.action(async (id) => {
|
|
174
|
+
try {
|
|
175
|
+
await cli.removeDnsRecord(id);
|
|
176
|
+
logSuccess(`Deleted DNS record: ${id}`);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// 4. Presets
|
|
184
|
+
const presets = program.command("presets").description("1-Click Cloud Hosting Presets (Vercel, Railway, Render, etc.)");
|
|
185
|
+
presets
|
|
186
|
+
.command("list")
|
|
187
|
+
.description("List all verified cloud hosting presets")
|
|
188
|
+
.action(async () => {
|
|
189
|
+
try {
|
|
190
|
+
const { presets: list } = await cli.listPresets();
|
|
191
|
+
const headers = ["ID", "PLATFORM", "CATEGORY", "DESCRIPTION"];
|
|
192
|
+
const rows = list.map((p) => [
|
|
193
|
+
p.id,
|
|
194
|
+
p.name,
|
|
195
|
+
p.category || "cloud",
|
|
196
|
+
p.description,
|
|
197
|
+
]);
|
|
198
|
+
console.log(`\n${formatTable(headers, rows)}\n`);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
202
|
+
process.exitCode = 1;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
presets
|
|
206
|
+
.command("apply <subdomain> <preset>")
|
|
207
|
+
.description("Apply a cloud hosting preset to a subdomain")
|
|
208
|
+
.option("-t, --target <customTarget>", "Custom target value (e.g. GitHub username or Railway target)")
|
|
209
|
+
.action(async (subdomain, preset, opts) => {
|
|
210
|
+
try {
|
|
211
|
+
const res = await cli.applyPreset(subdomain, preset, opts.target);
|
|
212
|
+
logSuccess(res.message);
|
|
213
|
+
for (const r of res.created_records) {
|
|
214
|
+
console.log(` + ${r.type} ${r.name} -> ${r.content}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// 5. Verify DoH
|
|
223
|
+
program
|
|
224
|
+
.command("verify <domain>")
|
|
225
|
+
.description("Verify live Anycast propagation across Google (8.8.8.8), Cloudflare (1.1.1.1), and Quad9 (9.9.9.9)")
|
|
226
|
+
.option("-t, --type <type>", "Record type to verify (A, CNAME, etc.)", "A")
|
|
227
|
+
.action(async (domain, opts) => {
|
|
228
|
+
try {
|
|
229
|
+
logInfo(`Verifying DoH propagation for ${domain} (${opts.type})...`);
|
|
230
|
+
const results = await cli.verifyDoH(domain, opts.type);
|
|
231
|
+
const headers = ["RESOLVER", "STATUS", "LATENCY", "RESOLVED TARGETS"];
|
|
232
|
+
const rows = results.map((r) => [
|
|
233
|
+
r.resolver,
|
|
234
|
+
r.status === "RESOLVED" ? `${colors.green}RESOLVED${colors.reset}` : `${colors.red}${r.status}${colors.reset}`,
|
|
235
|
+
r.latency,
|
|
236
|
+
r.targets,
|
|
237
|
+
]);
|
|
238
|
+
console.log(`\n${formatTable(headers, rows)}\n`);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
logError(err instanceof Error ? err.message : String(err));
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
return program;
|
|
246
|
+
}
|
|
247
|
+
export async function main() {
|
|
248
|
+
const program = createProgram();
|
|
249
|
+
await program.parseAsync(process.argv);
|
|
250
|
+
}
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const colors: {
|
|
2
|
+
reset: string;
|
|
3
|
+
bold: string;
|
|
4
|
+
dim: string;
|
|
5
|
+
green: string;
|
|
6
|
+
yellow: string;
|
|
7
|
+
blue: string;
|
|
8
|
+
magenta: string;
|
|
9
|
+
cyan: string;
|
|
10
|
+
red: string;
|
|
11
|
+
white: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function logSuccess(msg: string): void;
|
|
14
|
+
export declare function logInfo(msg: string): void;
|
|
15
|
+
export declare function logWarn(msg: string): void;
|
|
16
|
+
export declare function logError(msg: string): void;
|
|
17
|
+
export declare function formatTable(headers: string[], rows: string[][]): string;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const colors = {
|
|
2
|
+
reset: "\x1b[0m",
|
|
3
|
+
bold: "\x1b[1m",
|
|
4
|
+
dim: "\x1b[2m",
|
|
5
|
+
green: "\x1b[32m",
|
|
6
|
+
yellow: "\x1b[33m",
|
|
7
|
+
blue: "\x1b[34m",
|
|
8
|
+
magenta: "\x1b[35m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
red: "\x1b[31m",
|
|
11
|
+
white: "\x1b[37m",
|
|
12
|
+
};
|
|
13
|
+
export function logSuccess(msg) {
|
|
14
|
+
console.log(`${colors.green}✔${colors.reset} ${msg}`);
|
|
15
|
+
}
|
|
16
|
+
export function logInfo(msg) {
|
|
17
|
+
console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
export function logWarn(msg) {
|
|
20
|
+
console.log(`${colors.yellow}⚠${colors.reset} ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
export function logError(msg) {
|
|
23
|
+
console.error(`${colors.red}✖${colors.reset} ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
export function formatTable(headers, rows) {
|
|
26
|
+
const colWidths = headers.map((h, i) => {
|
|
27
|
+
const maxRowLen = rows.reduce((max, row) => Math.max(max, (row[i] || "").length), 0);
|
|
28
|
+
return Math.max(h.length, maxRowLen);
|
|
29
|
+
});
|
|
30
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(" ");
|
|
31
|
+
const separator = colWidths.map((w) => "─".repeat(w)).join(" ");
|
|
32
|
+
const rowLines = rows.map((row) => row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" "));
|
|
33
|
+
return [
|
|
34
|
+
`${colors.bold}${headerLine}${colors.reset}`,
|
|
35
|
+
`${colors.dim}${separator}${colors.reset}`,
|
|
36
|
+
...rowLines,
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "lovieme",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "lovieme",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Official CLI for LovieMe — claim free *.lovie.me subdomains, configure Anycast DNS, and wire up cloud presets in seconds.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"lovieme": "bin/lovieme.js",
|
|
10
|
+
"lovie": "bin/lovieme.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"bin",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"test": "vitest run"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"lovieme",
|
|
24
|
+
"cli",
|
|
25
|
+
"subdomain",
|
|
26
|
+
"dns",
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"anycast",
|
|
29
|
+
"vercel",
|
|
30
|
+
"github-pages"
|
|
31
|
+
],
|
|
32
|
+
"author": "Sagar Kewat (Sagarithm)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^13.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.17.0",
|
|
39
|
+
"typescript": "^5.8.0",
|
|
40
|
+
"vitest": "^4.1.7"
|
|
41
|
+
}
|
|
42
|
+
}
|