@lizard-build/cli 4.0.64 → 4.0.66
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/dist/commands/domain-owned.d.ts +28 -0
- package/dist/commands/domain-owned.js +173 -0
- package/dist/commands/domain-owned.js.map +1 -0
- package/dist/commands/domain.d.ts +4 -0
- package/dist/commands/domain.js +12 -0
- package/dist/commands/domain.js.map +1 -1
- package/dist/commands/git-hosting.js +14 -12
- package/dist/commands/git-hosting.js.map +1 -1
- package/dist/lib/lizard-git.d.ts +7 -3
- package/dist/lib/lizard-git.js +19 -8
- package/dist/lib/lizard-git.js.map +1 -1
- package/dist/lib/skills-data.generated.js +2 -2
- package/dist/lib/skills-data.generated.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -1
- package/skill-data/core/SKILL.md +5 -1
- package/skill-data/core/references/sdk.md +1 -1
- package/src/commands/domain-owned.ts +202 -0
- package/src/commands/domain.ts +14 -0
- package/src/commands/git-hosting.ts +13 -11
- package/src/lib/lizard-git.ts +19 -7
- package/src/lib/updater.ts +1 -1
- package/test/unit/domain-owned.test.ts +17 -0
- package/test/unit/lizard-git.test.ts +11 -3
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { api, APIError } from "../lib/api.js";
|
|
5
|
+
import { success, isJSONMode, printJSON, table, isTTY, info } from "../lib/format.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Domains bought on Lizard (lizard.build/domains): `lizard domain owned` lists them,
|
|
9
|
+
* `lizard domain dns <domain>` reads and edits their DNS records, and `lizard domain
|
|
10
|
+
* <hostname>` on one of them (or a subdomain) sets the DNS itself instead of printing
|
|
11
|
+
* records to add. These endpoints take a login or an unscoped API key, never a scoped one.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface OwnedDomain {
|
|
15
|
+
id: string;
|
|
16
|
+
domain: string;
|
|
17
|
+
status: string;
|
|
18
|
+
appId: string | null;
|
|
19
|
+
expiresAt: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type DnsType = "A" | "AAAA" | "CNAME" | "ALIAS" | "MX" | "TXT" | "URL301";
|
|
23
|
+
interface DnsRecord { name: string; type: DnsType; value: string; ttl: number; priority?: number }
|
|
24
|
+
const DNS_TYPES: DnsType[] = ["A", "AAAA", "CNAME", "ALIAS", "MX", "TXT", "URL301"];
|
|
25
|
+
|
|
26
|
+
const SESSION_HINT = "Domains bought on Lizard need `lizard login` or an API key without scopes (a scoped key cannot reach them).";
|
|
27
|
+
|
|
28
|
+
async function listOwned(): Promise<OwnedDomain[]> {
|
|
29
|
+
const r = await api.get<{ orders: OwnedDomain[] }>("/api/domains/orders").catch((err: unknown) => {
|
|
30
|
+
if (err instanceof APIError && err.status === 401) throw new Error(SESSION_HINT);
|
|
31
|
+
throw err;
|
|
32
|
+
});
|
|
33
|
+
return r.orders ?? [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The bought domain `hostname` is, or is a subdomain of; null when none (or the
|
|
37
|
+
* token cannot see bought domains, e.g. an API key). */
|
|
38
|
+
export async function findOwnedDomain(hostname: string): Promise<OwnedDomain | null> {
|
|
39
|
+
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
40
|
+
const orders = await api.get<{ orders: OwnedDomain[] }>("/api/domains/orders").then((r) => r.orders ?? []).catch(() => []);
|
|
41
|
+
return orders
|
|
42
|
+
.filter((o) => o.status === "registered" && (host === o.domain || host.endsWith(`.${o.domain}`)))
|
|
43
|
+
.sort((a, b) => b.domain.length - a.domain.length)[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Points `hostname` (the bought domain, or a subdomain of it) at a service: DNS and attach in one call. */
|
|
47
|
+
export async function connectOwned(order: OwnedDomain, hostname: string, service: { id: string; name: string }) {
|
|
48
|
+
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
49
|
+
const r = await api.post<{ ok: boolean; attached: string[]; note?: string }>(
|
|
50
|
+
`/api/domains/orders/${encodeURIComponent(order.id)}/connect`,
|
|
51
|
+
{ appId: service.id, hostname: host },
|
|
52
|
+
);
|
|
53
|
+
if (isJSONMode()) {
|
|
54
|
+
printJSON({ ...r, domain: order.domain, hostname: host, service: service.name });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
success(`${r.attached.map((h) => chalk.cyan(h)).join(" and ")} now point at ${service.name}`);
|
|
58
|
+
console.log(chalk.dim(` DNS records on ${order.domain} were set for you. ${r.note ?? "HTTPS follows on its own."}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function ownedByName(domain: string): Promise<OwnedDomain> {
|
|
62
|
+
const d = domain.toLowerCase().replace(/\.$/, "");
|
|
63
|
+
const all = await listOwned();
|
|
64
|
+
const o = all.find((x) => x.domain === d);
|
|
65
|
+
if (!o) {
|
|
66
|
+
const names = all.filter((x) => x.status === "registered").map((x) => x.domain);
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${d} is not a domain you bought on Lizard.` +
|
|
69
|
+
(names.length ? ` Yours: ${names.join(", ")}` : " Buy one at https://lizard.build/domains"),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (o.status !== "registered") throw new Error(`${d} is not registered yet (status: ${o.status})`);
|
|
73
|
+
return o;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** "@", "api", or "api.example.com" -> the name relative to the domain. */
|
|
77
|
+
export function relativeName(name: string, domain: string): string {
|
|
78
|
+
const n = name.toLowerCase().replace(/\.$/, "");
|
|
79
|
+
if (n === "@" || n === domain) return "@";
|
|
80
|
+
if (n.endsWith(`.${domain}`)) return n.slice(0, -(domain.length + 1));
|
|
81
|
+
return n;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseType(raw: string): DnsType {
|
|
85
|
+
const t = raw.toUpperCase();
|
|
86
|
+
if (t === "REDIRECT") return "URL301";
|
|
87
|
+
if (!(DNS_TYPES as string[]).includes(t)) {
|
|
88
|
+
throw new Error(`Unknown record type "${raw}". Use one of: ${DNS_TYPES.join(", ")} (redirect = URL301)`);
|
|
89
|
+
}
|
|
90
|
+
return t as DnsType;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function printRecords(domain: string, records: DnsRecord[]) {
|
|
94
|
+
if (!records.length) {
|
|
95
|
+
info(`${domain} has no DNS records. Add one: lizard domain dns ${domain} set <name> <type> <value>`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
table(
|
|
99
|
+
["NAME", "TYPE", "VALUE", "TTL"],
|
|
100
|
+
records.map((r) => [r.name, r.type === "URL301" ? "REDIRECT" : r.type, r.priority != null ? `${r.priority} ${r.value}` : r.value, String(r.ttl)]),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function registerOwnedDomainCommands(dom: Command) {
|
|
105
|
+
dom
|
|
106
|
+
.command("owned")
|
|
107
|
+
.alias("bought")
|
|
108
|
+
.description("List the domains you bought on Lizard")
|
|
109
|
+
.action(async () => {
|
|
110
|
+
const orders = await listOwned();
|
|
111
|
+
if (isJSONMode()) return printJSON({ domains: orders });
|
|
112
|
+
if (!orders.length) {
|
|
113
|
+
info("You have not bought a domain on Lizard yet: https://lizard.build/domains");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
table(
|
|
117
|
+
["DOMAIN", "STATUS", "EXPIRES", "APP"],
|
|
118
|
+
orders.map((o) => [
|
|
119
|
+
o.domain,
|
|
120
|
+
o.status,
|
|
121
|
+
o.expiresAt ? new Date(o.expiresAt).toISOString().slice(0, 10) : "-",
|
|
122
|
+
o.appId ? "connected" : "-",
|
|
123
|
+
]),
|
|
124
|
+
);
|
|
125
|
+
console.log(chalk.dim(" Point one at a service: lizard domain <domain or sub.domain> --service <svc>"));
|
|
126
|
+
console.log(chalk.dim(" Edit its DNS records: lizard domain dns <domain>"));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
dom
|
|
130
|
+
.command("dns")
|
|
131
|
+
.argument("<domain>", "A domain you bought on Lizard")
|
|
132
|
+
.argument("[action]", "set | add | rm (omit to list the records)")
|
|
133
|
+
.argument("[args...]", "set/add: <name> <type> <value>; rm: <name> [type] [value]")
|
|
134
|
+
.description("Show or change the DNS records of a domain you bought on Lizard")
|
|
135
|
+
.option("--ttl <seconds>", "TTL for set/add (default 1800)", (v) => parseInt(v, 10))
|
|
136
|
+
.option("--priority <n>", "MX priority (default 10)", (v) => parseInt(v, 10))
|
|
137
|
+
.option("-y, --yes", "Skip the confirmation for rm")
|
|
138
|
+
.addHelpText(
|
|
139
|
+
"after",
|
|
140
|
+
`
|
|
141
|
+
Examples:
|
|
142
|
+
lizard domain dns example.com list the records
|
|
143
|
+
lizard domain dns example.com set api CNAME myapp.onlizard.com replace api's CNAME
|
|
144
|
+
lizard domain dns example.com set @ A 203.0.113.10
|
|
145
|
+
lizard domain dns example.com add @ MX mail.example.com --priority 10
|
|
146
|
+
lizard domain dns example.com add @ TXT "v=spf1 include:_spf.google.com ~all"
|
|
147
|
+
lizard domain dns example.com set old redirect https://example.com
|
|
148
|
+
lizard domain dns example.com rm api CNAME
|
|
149
|
+
|
|
150
|
+
set replaces every record of that name and type; add keeps the existing ones (several MX or TXT).
|
|
151
|
+
To point a name at a Lizard service, use: lizard domain api.example.com --service <svc>`,
|
|
152
|
+
)
|
|
153
|
+
.action(async (domain: string, action: string | undefined, args: string[], opts: { ttl?: number; priority?: number; yes?: boolean }) => {
|
|
154
|
+
const order = await ownedByName(domain);
|
|
155
|
+
const path = `/api/domains/orders/${encodeURIComponent(order.id)}/dns`;
|
|
156
|
+
const current = await api.get<{ domain: string; records: DnsRecord[]; version: string }>(path);
|
|
157
|
+
|
|
158
|
+
if (!action || action === "list" || action === "ls") {
|
|
159
|
+
if (isJSONMode()) return printJSON(current);
|
|
160
|
+
printRecords(order.domain, current.records);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let records: DnsRecord[];
|
|
165
|
+
let summary: string;
|
|
166
|
+
if (action === "set" || action === "add") {
|
|
167
|
+
if (args.length < 3) throw new Error(`Usage: lizard domain dns ${order.domain} ${action} <name> <type> <value>`);
|
|
168
|
+
const name = relativeName(args[0], order.domain);
|
|
169
|
+
const type = parseType(args[1]);
|
|
170
|
+
const value = args.slice(2).join(" ");
|
|
171
|
+
const rec: DnsRecord = { name, type, value, ttl: opts.ttl ?? 1800 };
|
|
172
|
+
if (type === "MX") rec.priority = opts.priority ?? 10;
|
|
173
|
+
const kept = action === "set" ? current.records.filter((r) => !(r.name === name && r.type === type)) : current.records;
|
|
174
|
+
records = [...kept, rec];
|
|
175
|
+
summary = `${action === "set" ? "Set" : "Added"} ${type === "URL301" ? "REDIRECT" : type} ${name} -> ${value}`;
|
|
176
|
+
} else if (action === "rm" || action === "remove" || action === "delete") {
|
|
177
|
+
if (args.length < 1) throw new Error(`Usage: lizard domain dns ${order.domain} rm <name> [type] [value]`);
|
|
178
|
+
const name = relativeName(args[0], order.domain);
|
|
179
|
+
const type = args[1] ? parseType(args[1]) : null;
|
|
180
|
+
const value = args.length > 2 ? args.slice(2).join(" ") : null;
|
|
181
|
+
const match = (r: DnsRecord) => r.name === name && (!type || r.type === type) && (value === null || r.value === value);
|
|
182
|
+
const gone = current.records.filter(match);
|
|
183
|
+
if (!gone.length) throw new Error(`No record matches ${[name, type, value].filter(Boolean).join(" ")} on ${order.domain}`);
|
|
184
|
+
if (!opts.yes) {
|
|
185
|
+
if (!isTTY()) throw new Error("Use -y to confirm in non-interactive mode");
|
|
186
|
+
printRecords(order.domain, gone);
|
|
187
|
+
const ok = await p.confirm({ message: `Remove ${gone.length === 1 ? "this record" : `these ${gone.length} records`} from ${order.domain}?` });
|
|
188
|
+
if (p.isCancel(ok) || !ok) process.exit(5);
|
|
189
|
+
}
|
|
190
|
+
records = current.records.filter((r) => !match(r));
|
|
191
|
+
summary = `Removed ${gone.length} record${gone.length === 1 ? "" : "s"}`;
|
|
192
|
+
} else {
|
|
193
|
+
throw new Error(`Unknown action "${action}". Use set, add or rm (or nothing to list).`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const saved = await api.put<{ records: DnsRecord[]; version: string }>(path, { version: current.version, records });
|
|
197
|
+
if (isJSONMode()) return printJSON({ domain: order.domain, ...saved });
|
|
198
|
+
success(`${summary} on ${order.domain}`);
|
|
199
|
+
printRecords(order.domain, saved.records);
|
|
200
|
+
console.log(chalk.dim(" Changes reach most resolvers within minutes (up to the old record's TTL)."));
|
|
201
|
+
});
|
|
202
|
+
}
|
package/src/commands/domain.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { api } from "../lib/api.js";
|
|
|
5
5
|
import { resolveProjectId } from "../lib/config.js";
|
|
6
6
|
import { getActiveService } from "../lib/resolve.js";
|
|
7
7
|
import { success, isJSONMode, printJSON, table, isTTY, info } from "../lib/format.js";
|
|
8
|
+
import { connectOwned, findOwnedDomain, registerOwnedDomainCommands } from "./domain-owned.js";
|
|
8
9
|
|
|
9
10
|
interface DomainResponse {
|
|
10
11
|
ok?: boolean;
|
|
@@ -31,6 +32,10 @@ interface AppLite {
|
|
|
31
32
|
* <hostname> → attach a custom domain
|
|
32
33
|
* delete <h> → remove a domain
|
|
33
34
|
* generate → force-generate a fresh *.onlizard.com subdomain
|
|
35
|
+
* owned → domains bought on Lizard; dns <domain> → their DNS records
|
|
36
|
+
*
|
|
37
|
+
* A hostname on a domain bought on Lizard (or a subdomain of one) is pointed and attached
|
|
38
|
+
* in one step: the platform writes the DNS record itself, no records to copy.
|
|
34
39
|
*/
|
|
35
40
|
export function registerDomain(program: Command) {
|
|
36
41
|
const dom = program
|
|
@@ -100,6 +105,13 @@ export function registerDomain(program: Command) {
|
|
|
100
105
|
"`lizard domain list` shows the service's domains.",
|
|
101
106
|
);
|
|
102
107
|
}
|
|
108
|
+
// Bought on Lizard: set the DNS record and attach, nothing for the user to copy.
|
|
109
|
+
const owned = await findOwnedDomain(hostname);
|
|
110
|
+
if (owned) {
|
|
111
|
+
await connectOwned(owned, hostname, service);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
103
115
|
const result = await api
|
|
104
116
|
.post<DomainResponse>(`/api/apps/${service.id}/domains`, {
|
|
105
117
|
hostname,
|
|
@@ -284,6 +296,8 @@ export function registerDomain(program: Command) {
|
|
|
284
296
|
}
|
|
285
297
|
});
|
|
286
298
|
|
|
299
|
+
registerOwnedDomainCommands(dom);
|
|
300
|
+
|
|
287
301
|
dom
|
|
288
302
|
.command("verify")
|
|
289
303
|
.argument("<hostname>", "Custom domain to verify")
|
|
@@ -7,11 +7,11 @@ import { APIError } from "../lib/api.js";
|
|
|
7
7
|
import { validToken } from "../lib/auth.js";
|
|
8
8
|
import { isJSONMode, printJSON, table, success, info, warn, link } from "../lib/format.js";
|
|
9
9
|
import {
|
|
10
|
-
lg, qs, seg, segs, resolveRepo, repoPath, myLogin, cloneURL, gitURL, helperCommand, ago, ACTIVE_RUN,
|
|
10
|
+
lg, qs, seg, segs, resolveRepo, repoPath, myLogin, cloneURL, gitURL, gitURLs, helperCommand, ago, ACTIVE_RUN,
|
|
11
11
|
} from "../lib/lizard-git.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Lizard Git (
|
|
14
|
+
* Lizard Git (lizard.build/git) from the terminal: repositories, issues, pull requests,
|
|
15
15
|
* reviews, CI runs, releases, secrets, collaborators, webhooks, branch rules, labels,
|
|
16
16
|
* notifications, tokens, and the credential helper that lets plain `git` push and pull.
|
|
17
17
|
*
|
|
@@ -60,7 +60,7 @@ function repoOpt(c: Command): Command {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function registerLizardGit(git: Command) {
|
|
63
|
-
git.description("Lizard Git (
|
|
63
|
+
git.description("Lizard Git (lizard.build/git) repositories, pull requests, CI; and GitHub integration");
|
|
64
64
|
|
|
65
65
|
// ── Sign-in for plain git ────────────────────────────────────────────
|
|
66
66
|
git
|
|
@@ -68,14 +68,17 @@ export function registerLizardGit(git: Command) {
|
|
|
68
68
|
.description("Let git push and pull Lizard Git repositories with your Lizard sign-in (credential helper)")
|
|
69
69
|
.option("--local", "Only for the repository in this directory")
|
|
70
70
|
.action(async (opts: { local?: boolean }) => {
|
|
71
|
-
const key = `credential.${gitURL()}.helper`;
|
|
72
71
|
const scope = opts.local ? "--local" : "--global";
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
// For lizard.build/git only (git matches the path: the rest of lizard.build never gets
|
|
73
|
+
// the helper), and the old git.lizard.build for clones made before.
|
|
74
|
+
for (const url of gitURLs()) {
|
|
75
|
+
// An empty value first resets helpers inherited for this address; then ours.
|
|
76
|
+
for (const value of ["", helperCommand()]) {
|
|
77
|
+
const r = spawnSync("git", ["config", scope, value ? "--add" : "--replace-all", `credential.${url}.helper`, value], { encoding: "utf8" });
|
|
78
|
+
if (r.status !== 0) throw new APIError(500, `git config failed: ${r.stderr || r.error?.message}`);
|
|
79
|
+
}
|
|
80
|
+
spawnSync("git", ["config", scope, `credential.${url}.username`, "lizard"]);
|
|
77
81
|
}
|
|
78
|
-
spawnSync("git", ["config", scope, `credential.${gitURL()}.username`, "lizard"]);
|
|
79
82
|
const who = await myLogin();
|
|
80
83
|
out({ helper: helperCommand(), scope: opts.local ? "local" : "global", host: gitURL(), login: who }, () => {
|
|
81
84
|
success(`git now signs in to ${gitURL()} as ${chalk.bold(who)} (${opts.local ? "this repository" : "every repository"})`);
|
|
@@ -91,9 +94,8 @@ export function registerLizardGit(git: Command) {
|
|
|
91
94
|
if (op !== "get") return; // store/erase: nothing kept, nothing to forget
|
|
92
95
|
const input = readFileSync(0, "utf8");
|
|
93
96
|
const fields = Object.fromEntries(input.split("\n").filter(Boolean).map((l) => l.split(/=(.*)/s).slice(0, 2)));
|
|
94
|
-
const want = new URL(gitURL());
|
|
95
97
|
// Only for Lizard Git itself, over the same protocol (git always says both).
|
|
96
|
-
if (fields.host
|
|
98
|
+
if (!gitURLs().some((u) => { const w = new URL(u); return fields.host === w.host && fields.protocol === w.protocol.replace(":", ""); })) return;
|
|
97
99
|
const token = process.env.LIZARD_GIT_TOKEN || validToken();
|
|
98
100
|
if (!token) {
|
|
99
101
|
process.stderr.write("lizard: not signed in; run `lizard login`\n");
|
package/src/lib/lizard-git.ts
CHANGED
|
@@ -3,14 +3,16 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { apiErrorFrom, clientHeaders, getRequestToken, APIError } from "./api.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Lizard Git (
|
|
6
|
+
* Lizard Git (lizard.build/git) with the CLI's own sign-in: the session from `lizard login`,
|
|
7
7
|
* or LIZARD_TOKEN / LIZARD_API_KEY. Lizard Git asks the platform whose it is and acts as that
|
|
8
8
|
* account's user. Scoped API keys are refused there (they don't say which repositories they
|
|
9
9
|
* may reach); a Lizard Git token (lzg_..., `lizard git token create`) in LIZARD_GIT_TOKEN
|
|
10
10
|
* is used instead when set.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
export const DEFAULT_GIT_URL = "https://
|
|
13
|
+
export const DEFAULT_GIT_URL = "https://lizard.build/git";
|
|
14
|
+
/** The address before lizard.build/git: git and the API still answer there, for older clones. */
|
|
15
|
+
export const OLD_GIT_URL = "https://git.lizard.build";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Where Lizard Git is. LIZARD_GIT_URL must be https (or http on this machine): the Lizard
|
|
@@ -92,11 +94,21 @@ export async function myLogin(): Promise<string> {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
/** owner/name from a Lizard Git remote URL (https or ssh-style), or null. */
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
/** Every address a Lizard Git remote may use: LIZARD_GIT_URL, or lizard.build/git and the old one. */
|
|
98
|
+
export function gitURLs(): string[] {
|
|
99
|
+
const u = gitURL();
|
|
100
|
+
return u === DEFAULT_GIT_URL ? [u, OLD_GIT_URL] : [u];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function repoFromRemote(url: string, bases = gitURLs()): string | null {
|
|
104
|
+
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
105
|
+
for (const base of bases) {
|
|
106
|
+
const b = new URL(base);
|
|
107
|
+
const re = new RegExp(`^https?://(?:[^@/]+@)?${esc(b.host)}${esc(b.pathname.replace(/\/+$/, ""))}/([^/]+)/([^/]+?)(?:\\.git)?/?$`);
|
|
108
|
+
const m = url.trim().match(re);
|
|
109
|
+
if (m && ![m[1], m[2]].some((x) => x === "." || x === "..")) return `${m[1]}/${m[2]}`;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
function originRepo(): string | null {
|
package/src/lib/updater.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
export const CURRENT_VERSION = "4.0.
|
|
8
|
+
export const CURRENT_VERSION = "4.0.66";
|
|
9
9
|
const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
|
|
10
10
|
const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
|
|
11
11
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseType, relativeName } from "../../src/commands/domain-owned.js";
|
|
3
|
+
|
|
4
|
+
describe("lizard domain dns: names and types", () => {
|
|
5
|
+
it("makes names relative to the domain", () => {
|
|
6
|
+
expect(relativeName("@", "example.com")).toBe("@");
|
|
7
|
+
expect(relativeName("example.com", "example.com")).toBe("@");
|
|
8
|
+
expect(relativeName("api.example.com.", "example.com")).toBe("api");
|
|
9
|
+
expect(relativeName("API", "example.com")).toBe("api");
|
|
10
|
+
expect(relativeName("a.b", "example.com")).toBe("a.b");
|
|
11
|
+
});
|
|
12
|
+
it("accepts record types in any case, redirect as URL301", () => {
|
|
13
|
+
expect(parseType("cname")).toBe("CNAME");
|
|
14
|
+
expect(parseType("redirect")).toBe("URL301");
|
|
15
|
+
expect(() => parseType("SRV")).toThrow(/Unknown record type/);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -8,10 +8,15 @@ afterEach(() => {
|
|
|
8
8
|
|
|
9
9
|
describe("lizard git helpers", () => {
|
|
10
10
|
it("reads owner/name from Lizard Git remotes only", () => {
|
|
11
|
-
expect(repoFromRemote("https://
|
|
11
|
+
expect(repoFromRemote("https://lizard.build/git/alice/app.git")).toBe("alice/app");
|
|
12
|
+
expect(repoFromRemote("https://lizard:liz_x@lizard.build/git/alice/app")).toBe("alice/app");
|
|
13
|
+
expect(repoFromRemote("https://git.lizard.build/alice/app.git")).toBe("alice/app"); // older clones
|
|
12
14
|
expect(repoFromRemote("https://lizard:liz_x@git.lizard.build/alice/app")).toBe("alice/app");
|
|
13
15
|
expect(repoFromRemote("https://github.com/alice/app.git")).toBeNull();
|
|
14
16
|
expect(repoFromRemote("git@github.com:alice/app.git")).toBeNull();
|
|
17
|
+
expect(repoFromRemote("https://lizard.build/alice/app.git")).toBeNull();
|
|
18
|
+
expect(repoFromRemote("https://lizard.build/gitx/alice/app.git")).toBeNull();
|
|
19
|
+
expect(repoFromRemote("https://lizard.build/git/alice/app/extra.git")).toBeNull();
|
|
15
20
|
});
|
|
16
21
|
|
|
17
22
|
it("drops empty query values", () => {
|
|
@@ -21,7 +26,8 @@ describe("lizard git helpers", () => {
|
|
|
21
26
|
it("honours LIZARD_GIT_URL", () => {
|
|
22
27
|
vi.stubEnv("LIZARD_GIT_URL", "http://localhost:8080/");
|
|
23
28
|
expect(gitURL()).toBe("http://localhost:8080");
|
|
24
|
-
expect(repoFromRemote("http://localhost:8080/a/b.git"
|
|
29
|
+
expect(repoFromRemote("http://localhost:8080/a/b.git")).toBe("a/b");
|
|
30
|
+
expect(repoFromRemote("https://git.lizard.build/a/b.git")).toBeNull(); // another server: only it
|
|
25
31
|
});
|
|
26
32
|
|
|
27
33
|
it("runs itself as git's credential helper", () => {
|
|
@@ -37,7 +43,7 @@ describe("lizard git helpers", () => {
|
|
|
37
43
|
}));
|
|
38
44
|
expect(await resolveRepo("alice/app")).toBe("alice/app");
|
|
39
45
|
expect(await resolveRepo("app")).toBe("me/app");
|
|
40
|
-
expect(calls).toEqual(["GET https://
|
|
46
|
+
expect(calls).toEqual(["GET https://lizard.build/git/api/session Bearer liz_test"]);
|
|
41
47
|
await expect(resolveRepo("a/b/c")).rejects.toThrow(/owner\/name/);
|
|
42
48
|
});
|
|
43
49
|
|
|
@@ -61,5 +67,7 @@ describe("lizard git safety", () => {
|
|
|
61
67
|
expect(() => segs("../../api/user/tokens")).toThrow(/bad path/);
|
|
62
68
|
expect(segs("src/a b.js")).toBe("src/a%20b.js");
|
|
63
69
|
expect(repoFromRemote("https://git.lizard.build/../x.git")).toBeNull();
|
|
70
|
+
expect(repoFromRemote("https://lizard.build/git/../x.git")).toBeNull();
|
|
71
|
+
expect(repoFromRemote("https://lizard.build/git/a/...git")).toBeNull();
|
|
64
72
|
});
|
|
65
73
|
});
|