@lizard-build/cli 4.0.66 → 4.0.68
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 +14 -2
- package/dist/commands/domain-owned.js +46 -4
- package/dist/commands/domain-owned.js.map +1 -1
- package/dist/commands/domain.js +2 -2
- package/dist/commands/domain.js.map +1 -1
- package/dist/commands/git-hosting.d.ts +2 -0
- package/dist/commands/git-hosting.js +166 -81
- package/dist/commands/git-hosting.js.map +1 -1
- package/dist/commands/git.js +4 -2
- package/dist/commands/git.js.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/checkout.d.ts +104 -0
- package/dist/lib/checkout.js +545 -0
- package/dist/lib/checkout.js.map +1 -0
- package/dist/lib/lizard-git.d.ts +3 -6
- package/dist/lib/lizard-git.js +19 -46
- package/dist/lib/lizard-git.js.map +1 -1
- package/dist/lib/skills-data.generated.js +1 -1
- 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 +2 -0
- package/src/commands/domain-owned.ts +45 -5
- package/src/commands/domain.ts +2 -2
- package/src/commands/git-hosting.ts +164 -75
- package/src/commands/git.ts +3 -2
- package/src/index.ts +2 -3
- package/src/lib/checkout.ts +564 -0
- package/src/lib/lizard-git.ts +18 -43
- package/src/lib/updater.ts +1 -1
- package/test/unit/domain-owned.test.ts +18 -1
- package/test/unit/lizard-git.test.ts +63 -22
|
@@ -24,6 +24,22 @@ interface DnsRecord { name: string; type: DnsType; value: string; ttl: number; p
|
|
|
24
24
|
const DNS_TYPES: DnsType[] = ["A", "AAAA", "CNAME", "ALIAS", "MX", "TXT", "URL301"];
|
|
25
25
|
|
|
26
26
|
const SESSION_HINT = "Domains bought on Lizard need `lizard login` or an API key without scopes (a scoped key cannot reach them).";
|
|
27
|
+
/** Changing a bought domain needs a login: an API key can read it but not repoint it, so a
|
|
28
|
+
* leaked key cannot take the domain or its email. */
|
|
29
|
+
const WRITE_HINT = "Changing a domain bought on Lizard needs `lizard login` (an API key can read its records but not change them).";
|
|
30
|
+
|
|
31
|
+
function writeError(err: unknown): never {
|
|
32
|
+
if (err instanceof APIError && err.status === 401) throw new Error(WRITE_HINT);
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const POINTING: DnsType[] = ["A", "AAAA", "CNAME", "ALIAS", "URL301"];
|
|
37
|
+
/** Records connecting `names` would replace, leaving out ones nobody would miss: the
|
|
38
|
+
* registrar's parking page and a previous Lizard target. */
|
|
39
|
+
export function recordsToReplace(records: DnsRecord[], names: string[]): DnsRecord[] {
|
|
40
|
+
return records.filter((r) => names.includes(r.name) && POINTING.includes(r.type)
|
|
41
|
+
&& !/(^|\.)parkingpage\.namecheap\.com\.?$/i.test(r.value) && !/(^|\.)onlizard\.com\.?$/i.test(r.value));
|
|
42
|
+
}
|
|
27
43
|
|
|
28
44
|
async function listOwned(): Promise<OwnedDomain[]> {
|
|
29
45
|
const r = await api.get<{ orders: OwnedDomain[] }>("/api/domains/orders").catch((err: unknown) => {
|
|
@@ -43,13 +59,37 @@ export async function findOwnedDomain(hostname: string): Promise<OwnedDomain | n
|
|
|
43
59
|
.sort((a, b) => b.domain.length - a.domain.length)[0] ?? null;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
/** Points `hostname` (the bought domain, or a subdomain of it) at a service: DNS and attach
|
|
47
|
-
|
|
62
|
+
/** Points `hostname` (the bought domain, or a subdomain of it) at a service: DNS and attach
|
|
63
|
+
* in one call. Records it would replace, and a hostname attached to another service, need
|
|
64
|
+
* a yes at the terminal or --force, so one command cannot quietly take a working site down. */
|
|
65
|
+
export async function connectOwned(order: OwnedDomain, hostname: string, service: { id: string; name: string }, force: boolean) {
|
|
48
66
|
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
49
|
-
const
|
|
67
|
+
const names = host === order.domain ? ["@", "www"] : [host.slice(0, -(order.domain.length + 1))];
|
|
68
|
+
let move = force;
|
|
69
|
+
if (!force) {
|
|
70
|
+
const dns = await api.get<{ records: DnsRecord[] }>(`/api/domains/orders/${encodeURIComponent(order.id)}/dns`);
|
|
71
|
+
const replaced = recordsToReplace(dns.records, names);
|
|
72
|
+
if (replaced.length) {
|
|
73
|
+
const list = replaced.map((r) => `${r.name} ${r.type} ${r.value}`).join(", ");
|
|
74
|
+
if (!isTTY() || isJSONMode()) throw new Error(`This replaces existing records on ${order.domain}: ${list}. Run again with --force to replace them.`);
|
|
75
|
+
printRecords(order.domain, replaced);
|
|
76
|
+
const ok = await p.confirm({ message: `Replace ${replaced.length === 1 ? "this record" : "these records"} so ${host} points at ${service.name}?` });
|
|
77
|
+
if (p.isCancel(ok) || !ok) process.exit(5);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const connect = (forceMove: boolean) => api.post<{ ok: boolean; attached: string[]; note?: string }>(
|
|
50
81
|
`/api/domains/orders/${encodeURIComponent(order.id)}/connect`,
|
|
51
|
-
{ appId: service.id, hostname: host },
|
|
82
|
+
{ appId: service.id, hostname: host, force: forceMove },
|
|
52
83
|
);
|
|
84
|
+
const r = await connect(move).catch(async (err: unknown) => {
|
|
85
|
+
if (err instanceof APIError && err.status === 409 && !move && isTTY() && !isJSONMode()) {
|
|
86
|
+
const ok = await p.confirm({ message: `${err.message} Move it to ${service.name}?` });
|
|
87
|
+
if (p.isCancel(ok) || !ok) process.exit(5);
|
|
88
|
+
move = true;
|
|
89
|
+
return connect(true).catch(writeError);
|
|
90
|
+
}
|
|
91
|
+
return writeError(err);
|
|
92
|
+
});
|
|
53
93
|
if (isJSONMode()) {
|
|
54
94
|
printJSON({ ...r, domain: order.domain, hostname: host, service: service.name });
|
|
55
95
|
return;
|
|
@@ -193,7 +233,7 @@ To point a name at a Lizard service, use: lizard domain api.example.com --servic
|
|
|
193
233
|
throw new Error(`Unknown action "${action}". Use set, add or rm (or nothing to list).`);
|
|
194
234
|
}
|
|
195
235
|
|
|
196
|
-
const saved = await api.put<{ records: DnsRecord[]; version: string }>(path, { version: current.version, records });
|
|
236
|
+
const saved = await api.put<{ records: DnsRecord[]; version: string }>(path, { version: current.version, records }).catch(writeError);
|
|
197
237
|
if (isJSONMode()) return printJSON({ domain: order.domain, ...saved });
|
|
198
238
|
success(`${summary} on ${order.domain}`);
|
|
199
239
|
printRecords(order.domain, saved.records);
|
package/src/commands/domain.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function registerDomain(program: Command) {
|
|
|
46
46
|
.option("-s, --service <name>", "Service name or ID")
|
|
47
47
|
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
48
48
|
.option("--port <n>", "Port to expose", parseIntOption)
|
|
49
|
-
.option("-f, --force", "Move the domain here if it's attached to another of your services")
|
|
49
|
+
.option("-f, --force", "Move the domain here if it's attached to another of your services; on a domain bought on Lizard, also replace its existing records without asking")
|
|
50
50
|
.action(async (hostname: string | undefined, opts, _cmd) => {
|
|
51
51
|
const projectId = await resolveProjectId(opts.project);
|
|
52
52
|
const service = await getActiveService(opts.service, projectId);
|
|
@@ -108,7 +108,7 @@ export function registerDomain(program: Command) {
|
|
|
108
108
|
// Bought on Lizard: set the DNS record and attach, nothing for the user to copy.
|
|
109
109
|
const owned = await findOwnedDomain(hostname);
|
|
110
110
|
if (owned) {
|
|
111
|
-
await connectOwned(owned, hostname, service);
|
|
111
|
+
await connectOwned(owned, hostname, service, !!opts.force);
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -4,16 +4,18 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
5
5
|
import { basename } from "node:path";
|
|
6
6
|
import { APIError } from "../lib/api.js";
|
|
7
|
-
import { validToken } from "../lib/auth.js";
|
|
8
7
|
import { isJSONMode, printJSON, table, success, info, warn, link } from "../lib/format.js";
|
|
9
8
|
import {
|
|
10
|
-
lg, qs, seg, segs, resolveRepo, repoPath, myLogin,
|
|
9
|
+
lg, qs, seg, segs, resolveRepo, repoPath, myLogin, gitURL, ago, ACTIVE_RUN,
|
|
11
10
|
} from "../lib/lizard-git.js";
|
|
11
|
+
import * as wc from "../lib/checkout.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
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
|
-
* notifications, tokens
|
|
16
|
+
* notifications, tokens; and working copies: clone, status, diff, commit, sync, switch. Lizard
|
|
17
|
+
* Git is Scion-style (no git protocol): a checkout holds files and .lizard-git/checkout.json, and
|
|
18
|
+
* every commit is made on the server (lib/checkout.ts).
|
|
17
19
|
*
|
|
18
20
|
* Registered under `lizard git`, next to the GitHub integration commands.
|
|
19
21
|
*/
|
|
@@ -55,69 +57,163 @@ function bodyFrom(opts: { body?: string; bodyFile?: string }): string | undefine
|
|
|
55
57
|
|
|
56
58
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
57
59
|
|
|
60
|
+
/** The checkout this directory is in, or a clear error. */
|
|
61
|
+
function needCheckout(): string {
|
|
62
|
+
const root = wc.findCheckout();
|
|
63
|
+
if (!root) throw new APIError(400, "not in a Lizard Git checkout (lizard git clone owner/name)");
|
|
64
|
+
return root;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `lizard git status` inside a checkout: your changes. */
|
|
68
|
+
export function checkoutStatus(): boolean {
|
|
69
|
+
const root = wc.findCheckout();
|
|
70
|
+
if (!root) return false;
|
|
71
|
+
const c = wc.loadCheckout(root);
|
|
72
|
+
const ch = wc.changesOf(c, wc.scanWorking(root, c));
|
|
73
|
+
out({ repo: c.repo, branch: c.branch, commit: c.base, sparse: c.sparse, ...ch }, () => {
|
|
74
|
+
info(`${chalk.bold(c.repo)} on ${chalk.cyan(c.branch)} at ${c.base.slice(0, 12)}${c.sparse ? chalk.dim(` (sparse: ${c.sparse.join(", ")})`) : ""}`);
|
|
75
|
+
const n = ch.added.length + ch.modified.length + ch.deleted.length;
|
|
76
|
+
if (!n) return info(chalk.dim("no changes"));
|
|
77
|
+
for (const p of ch.modified) info(` ${chalk.yellow("modified")} ${p}`);
|
|
78
|
+
for (const p of ch.added) info(` ${chalk.green("added")} ${p}`);
|
|
79
|
+
for (const p of ch.deleted) info(` ${chalk.red("deleted")} ${p}`);
|
|
80
|
+
info(chalk.dim(` commit them: lizard git commit -m "..."`));
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
function repoOpt(c: Command): Command {
|
|
59
|
-
return c.option("-R, --repo <owner/name>", "Repository (default: the
|
|
86
|
+
return c.option("-R, --repo <owner/name>", "Repository (default: the one this checkout is of)");
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
export function registerLizardGit(git: Command) {
|
|
63
90
|
git.description("Lizard Git (lizard.build/git) repositories, pull requests, CI; and GitHub integration");
|
|
64
91
|
|
|
65
|
-
// ──
|
|
92
|
+
// ── Working copies ───────────────────────────────────────────────────
|
|
66
93
|
git
|
|
67
|
-
.command("
|
|
68
|
-
.description("
|
|
69
|
-
.
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
const who = await myLogin();
|
|
83
|
-
out({ helper: helperCommand(), scope: opts.local ? "local" : "global", host: gitURL(), login: who }, () => {
|
|
84
|
-
success(`git now signs in to ${gitURL()} as ${chalk.bold(who)} (${opts.local ? "this repository" : "every repository"})`);
|
|
85
|
-
info(chalk.dim(` Try: lizard git clone ${who}/<repo>`));
|
|
86
|
-
});
|
|
94
|
+
.command("clone")
|
|
95
|
+
.description("Check out a Lizard Git repository: only files, fetched on demand by hash (no history to download)")
|
|
96
|
+
.argument("<repo>", "owner/name, or name for one of yours")
|
|
97
|
+
.argument("[dir]", "Directory (default: the repository name)")
|
|
98
|
+
.option("-b, --branch <name>", "Branch (default: the repository's default branch)")
|
|
99
|
+
.option("--sparse <paths>", "Only these folders or files, comma-separated (a sparse checkout fetches nothing else)")
|
|
100
|
+
.action(async (repoArg: string, dir: string | undefined, opts: { branch?: string; sparse?: string }) => {
|
|
101
|
+
const repo = await resolveRepo(repoArg);
|
|
102
|
+
const target = dir ?? repo.split("/")[1];
|
|
103
|
+
const sparse = list(opts.sparse)?.map((x) => x.replace(/^\/+|\/+$/g, "")) ?? null;
|
|
104
|
+
const c = await wc.clone(repo, target, opts.branch, sparse, (m) => !isJSONMode() && process.stderr.write(chalk.dim(` ${m}\r`)));
|
|
105
|
+
if (!isJSONMode()) process.stderr.write("\x1b[2K");
|
|
106
|
+
done({ repo, dir: target, branch: c.branch, commit: c.base, files: Object.keys(c.files).length, sparse },
|
|
107
|
+
`Checked out ${repo} (${c.branch} at ${c.base.slice(0, 12)}, ${Object.keys(c.files).length} files) into ${target}`);
|
|
87
108
|
});
|
|
88
109
|
|
|
89
110
|
git
|
|
90
|
-
.command("
|
|
91
|
-
.description("
|
|
92
|
-
.argument("
|
|
93
|
-
.action(async (
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
.command("init")
|
|
112
|
+
.description("Make this folder the checkout of an empty repository (then lizard git commit)")
|
|
113
|
+
.argument("<repo>", "owner/name, or name for one of yours")
|
|
114
|
+
.action(async (repoArg: string) => {
|
|
115
|
+
const repo = await resolveRepo(repoArg);
|
|
116
|
+
const c = await wc.initHere(repo, process.cwd());
|
|
117
|
+
done({ repo, branch: c.branch }, `This folder is now ${repo} (${c.branch}); commit with lizard git commit -m "..."`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
git
|
|
121
|
+
.command("diff")
|
|
122
|
+
.description("Your changes in this checkout against the commit it's at")
|
|
123
|
+
.argument("[paths...]", "Only these files or folders")
|
|
124
|
+
.option("--name-only", "Only the changed paths")
|
|
125
|
+
.action(async (paths: string[], opts: { nameOnly?: boolean }) => {
|
|
126
|
+
const root = needCheckout();
|
|
127
|
+
const c = wc.loadCheckout(root);
|
|
128
|
+
const local = wc.scanWorking(root, c);
|
|
129
|
+
const ch = wc.changesOf(c, local);
|
|
130
|
+
const pick = (p: string) => !paths.length || paths.some((x) => p === x || p.startsWith(x.replace(/\/+$/, "") + "/"));
|
|
131
|
+
const all = [...ch.added.map((p) => ["added", p]), ...ch.modified.map((p) => ["modified", p]), ...ch.deleted.map((p) => ["deleted", p])].filter(([, p]) => pick(p));
|
|
132
|
+
if (isJSONMode() || opts.nameOnly) {
|
|
133
|
+
out(all.map(([status, path]) => ({ status, path })), () => all.forEach(([, p]) => console.log(p)));
|
|
102
134
|
return;
|
|
103
135
|
}
|
|
104
|
-
|
|
136
|
+
const before = await wc.getBlobs(c.repo, all.filter(([k]) => k !== "added").map(([, p]) => c.files[p].h));
|
|
137
|
+
for (const [kind, p] of all) {
|
|
138
|
+
const old = kind === "added" ? "" : before.get(c.files[p].h)!.toString();
|
|
139
|
+
const cur = kind === "deleted" ? "" : readFileSync(`${root}/${p}`, "utf8");
|
|
140
|
+
if (old.includes("\0") || cur.includes("\0")) {
|
|
141
|
+
console.log(chalk.bold(`Binary ${p} ${kind}`));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (old.length + cur.length > 4 << 20) {
|
|
145
|
+
console.log(chalk.bold(`${p}: ${kind} (too large to show)`));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
for (const line of wc.unifiedDiff(old, cur, p).split("\n")) {
|
|
149
|
+
console.log(line.startsWith("+") && !line.startsWith("+++") ? chalk.green(line) : line.startsWith("-") && !line.startsWith("---") ? chalk.red(line) : line.startsWith("@@") ? chalk.cyan(line) : line);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
105
152
|
});
|
|
106
153
|
|
|
107
154
|
git
|
|
108
|
-
.command("
|
|
109
|
-
.description("
|
|
110
|
-
.argument("
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
.command("commit")
|
|
156
|
+
.description("Commit your changes on the server (nothing to push): if the branch moved, the server puts them on top")
|
|
157
|
+
.argument("[paths...]", "Only these files or folders (default: every change)")
|
|
158
|
+
.requiredOption("-m, --message <text>", "Commit message")
|
|
159
|
+
.option("-b, --branch <name>", "Commit to a new branch, starting at this checkout's commit (and switch to it)")
|
|
160
|
+
.action(async (paths: string[], opts: { message: string; branch?: string }) => {
|
|
161
|
+
const root = needCheckout();
|
|
162
|
+
const c = wc.loadCheckout(root);
|
|
163
|
+
const r = await wc.commit(root, c, opts.message.endsWith("\n") ? opts.message : opts.message + "\n", paths, opts.branch);
|
|
164
|
+
const extra = r.sync.updated.length + r.sync.removed.length;
|
|
165
|
+
done(r, `Committed ${r.files} file(s) to ${r.branch}: ${r.sha.slice(0, 12)}${r.uploaded ? chalk.dim(` (${r.uploaded} uploaded)`) : ""}${extra ? chalk.dim(` · ${extra} file(s) from others' commits updated`) : ""}`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
git
|
|
169
|
+
.command("sync")
|
|
170
|
+
.alias("pull")
|
|
171
|
+
.description("Bring this checkout to the branch's latest commit (keeps your changes)")
|
|
172
|
+
.action(async () => {
|
|
173
|
+
const root = needCheckout();
|
|
174
|
+
const c = wc.loadCheckout(root);
|
|
175
|
+
const r = await wc.sync(root, c);
|
|
176
|
+
done(r, r.from === r.to ? `Already at ${c.branch}'s latest (${r.to.slice(0, 12)})` :
|
|
177
|
+
`${c.branch}: ${r.from.slice(0, 12)} -> ${r.to.slice(0, 12)} (${r.updated.length} updated, ${r.removed.length} removed)`);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
git
|
|
181
|
+
.command("switch")
|
|
182
|
+
.description("Switch this checkout to another branch (keeps your changes)")
|
|
183
|
+
.argument("<branch>")
|
|
184
|
+
.option("-c, --create", "Create the branch at this checkout's commit first")
|
|
185
|
+
.action(async (branch: string, opts: { create?: boolean }) => {
|
|
186
|
+
const root = needCheckout();
|
|
187
|
+
const c = wc.loadCheckout(root);
|
|
188
|
+
if (opts.create) await lg.post(`${repoPath(c.repo)}/branches`, { name: branch, from: c.base });
|
|
189
|
+
const r = await wc.sync(root, c, branch);
|
|
190
|
+
done({ ...r, branch }, `On ${branch} at ${r.to.slice(0, 12)} (${r.updated.length} updated, ${r.removed.length} removed)`);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
git
|
|
194
|
+
.command("sparse")
|
|
195
|
+
.description("Change which folders this checkout has (comma-separated; \"all\" for everything)")
|
|
196
|
+
.argument("<paths>")
|
|
197
|
+
.action(async (paths: string) => {
|
|
198
|
+
const root = needCheckout();
|
|
199
|
+
const c = wc.loadCheckout(root);
|
|
200
|
+
const local = wc.scanWorking(root, c);
|
|
201
|
+
const ch = wc.changesOf(c, local);
|
|
202
|
+
if (ch.added.length + ch.modified.length + ch.deleted.length) throw new APIError(409, "commit your changes first (lizard git commit)");
|
|
203
|
+
const next = paths === "all" ? null : list(paths)!.map((x) => x.replace(/^\/+|\/+$/g, ""));
|
|
204
|
+
// Drop what leaves the set, then sync with the new set (fetches what joins it).
|
|
205
|
+
const keep = (p: string) => !next || next.some((x) => p === x || p.startsWith(x + "/"));
|
|
206
|
+
for (const p of Object.keys(c.files)) {
|
|
207
|
+
if (!keep(p)) {
|
|
208
|
+
wc.removeFileAt(root, p);
|
|
209
|
+
delete c.files[p];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
c.sparse = next;
|
|
213
|
+
const before = c.base;
|
|
214
|
+
c.base = ""; // make sync list and fetch everything in the new set
|
|
215
|
+
const r = await wc.sync(root, c);
|
|
216
|
+
done({ sparse: next, files: Object.keys(c.files).length, commit: r.to }, `Sparse: ${next ? next.join(", ") : "everything"} (${Object.keys(c.files).length} files at ${(r.to || before).slice(0, 12)})`);
|
|
121
217
|
});
|
|
122
218
|
|
|
123
219
|
// ── Repositories ─────────────────────────────────────────────────────
|
|
@@ -147,16 +243,16 @@ export function registerLizardGit(git: Command) {
|
|
|
147
243
|
.argument("<name>")
|
|
148
244
|
.option("--private", "Only you and collaborators can see it")
|
|
149
245
|
.option("-d, --description <text>")
|
|
150
|
-
.option("--readme", "Start with a README (otherwise empty
|
|
246
|
+
.option("--readme", "Start with a README (otherwise empty)")
|
|
151
247
|
.option("--default-branch <name>", "Default branch (main)")
|
|
152
248
|
.action(async (name: string, opts) => {
|
|
153
249
|
const r = await lg.post<{ owner: string; name: string }>("/api/repos", {
|
|
154
250
|
name, private: !!opts.private, description: opts.description, readme: !!opts.readme, defaultBranch: opts.defaultBranch,
|
|
155
251
|
});
|
|
156
252
|
const full = `${r.owner}/${r.name}`;
|
|
157
|
-
out({ ...r, url: `${gitURL()}/${full}
|
|
253
|
+
out({ ...r, url: `${gitURL()}/${full}` }, () => {
|
|
158
254
|
success(`Created ${chalk.bold(full)} ${link(`${gitURL()}/${full}`)}`);
|
|
159
|
-
info(chalk.dim(`
|
|
255
|
+
info(chalk.dim(` Check out: lizard git clone ${full} then: lizard git commit -m "..."`));
|
|
160
256
|
});
|
|
161
257
|
});
|
|
162
258
|
repoOpt(repo.command("view").description("A repository's details").argument("[repo]"))
|
|
@@ -168,7 +264,7 @@ export function registerLizardGit(git: Command) {
|
|
|
168
264
|
if (m.repo.description) info(m.repo.description);
|
|
169
265
|
if (m.forkOf) info(chalk.dim(`forked from ${m.forkOf}`));
|
|
170
266
|
info(`default branch ${m.repo.defaultBranch} · ${m.branches} branches · ${m.tags} tags · ${m.openIssues} open issues · ${m.openPulls} open pull requests · ${m.repo.stars} stars`);
|
|
171
|
-
info(`${link(`${gitURL()}/${r}`)}\
|
|
267
|
+
info(`${link(`${gitURL()}/${r}`)}\ncheck out: lizard git clone ${r}`);
|
|
172
268
|
});
|
|
173
269
|
});
|
|
174
270
|
repoOpt(repo.command("edit").description("Change name, description, visibility or default branch").argument("[repo]"))
|
|
@@ -176,13 +272,11 @@ export function registerLizardGit(git: Command) {
|
|
|
176
272
|
.option("-d, --description <text>")
|
|
177
273
|
.option("--private").option("--public")
|
|
178
274
|
.option("--default-branch <name>")
|
|
179
|
-
.option("--api-commits", "Also accept commits written through the API by agents without a checkout")
|
|
180
|
-
.option("--no-api-commits", "Changes only by git push and merged pull requests (the default for new repositories)")
|
|
181
275
|
.action(async (arg: string | undefined, opts) => {
|
|
182
276
|
const r = await resolveRepo(arg ?? opts.repo);
|
|
183
277
|
await lg.patch(repoPath(r), {
|
|
184
278
|
name: opts.name, description: opts.description, defaultBranch: opts.defaultBranch,
|
|
185
|
-
private: opts.private ? true : opts.public ? false : undefined,
|
|
279
|
+
private: opts.private ? true : opts.public ? false : undefined,
|
|
186
280
|
});
|
|
187
281
|
done({ ok: true, repo: opts.name ? `${r.split("/")[0]}/${opts.name}` : r }, `Updated ${r}`);
|
|
188
282
|
});
|
|
@@ -201,7 +295,7 @@ export function registerLizardGit(git: Command) {
|
|
|
201
295
|
const r = await resolveRepo(arg ?? opts.repo);
|
|
202
296
|
const f = await lg.post<{ owner: string; name: string }>(`${repoPath(r)}/forks`, { name: opts.name });
|
|
203
297
|
const full = `${f.owner}/${f.name}`;
|
|
204
|
-
if (opts.clone)
|
|
298
|
+
if (opts.clone) await wc.clone(full, f.name, undefined, null);
|
|
205
299
|
done({ ...f, url: `${gitURL()}/${full}` }, `Forked ${r} to ${chalk.bold(full)}`);
|
|
206
300
|
});
|
|
207
301
|
repoOpt(repo.command("forks").description("Forks of a repository").argument("[repo]"))
|
|
@@ -339,9 +433,9 @@ export function registerLizardGit(git: Command) {
|
|
|
339
433
|
if (pulls) {
|
|
340
434
|
let head = opts.head;
|
|
341
435
|
if (!head) {
|
|
342
|
-
const
|
|
343
|
-
head =
|
|
344
|
-
if (!head
|
|
436
|
+
const root = wc.findCheckout();
|
|
437
|
+
head = root ? wc.loadCheckout(root).branch : "";
|
|
438
|
+
if (!head) throw new APIError(400, "pass --head <branch>");
|
|
345
439
|
}
|
|
346
440
|
Object.assign(body, { head, base: opts.base, headRepo: opts.headRepo, draft: !!opts.draft, reviewers: opts.reviewer.length ? opts.reviewer : undefined });
|
|
347
441
|
}
|
|
@@ -530,20 +624,15 @@ export function registerLizardGit(git: Command) {
|
|
|
530
624
|
});
|
|
531
625
|
done(m, `Merged #${n} (${String(m.sha).slice(0, 7)})`);
|
|
532
626
|
});
|
|
533
|
-
repoOpt(grp.command("checkout").description("
|
|
627
|
+
repoOpt(grp.command("checkout").description("Switch this checkout to a pull request's branch").argument("<number>", "", int))
|
|
534
628
|
.action(async (n: number, opts) => {
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
};
|
|
543
|
-
run(["fetch", "origin", `+${remoteRef}:refs/remotes/origin/${local}`]);
|
|
544
|
-
const exists = spawnSync("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${local}`]).status === 0;
|
|
545
|
-
run(exists ? ["checkout", local] : ["checkout", "-b", local, "--track", `origin/${local}`]);
|
|
546
|
-
done({ branch: local }, `On branch ${local} (#${n})`);
|
|
629
|
+
const root = needCheckout();
|
|
630
|
+
const c = wc.loadCheckout(root);
|
|
631
|
+
const d = await lg.get<any>(`${repoPath(c.repo)}/issues/${n}`);
|
|
632
|
+
// A fork's branch is read here under refs/pull/<n>/head: look, don't commit.
|
|
633
|
+
const target = d.issue.headRepo && d.issue.headRepo !== c.repo ? `pull/${n}/head` : d.issue.headRef;
|
|
634
|
+
const r = await wc.sync(root, c, target);
|
|
635
|
+
done({ branch: target, commit: r.to }, `On ${target} (#${n}) at ${r.to.slice(0, 12)}`);
|
|
547
636
|
});
|
|
548
637
|
}
|
|
549
638
|
|
package/src/commands/git.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { api, getBaseURL, streamSSE, withScope } from "../lib/api.js";
|
|
|
7
7
|
import { openURL } from "../lib/auth.js";
|
|
8
8
|
import { resolveProjectScope, resolveService } from "../lib/resolve.js";
|
|
9
9
|
import { success, error, info, fail, isJSONMode, printJSON, isTTY, link } from "../lib/format.js";
|
|
10
|
-
import { registerLizardGit } from "./git-hosting.js";
|
|
10
|
+
import { registerLizardGit, checkoutStatus } from "./git-hosting.js";
|
|
11
11
|
|
|
12
12
|
interface GitHubInstallation {
|
|
13
13
|
id: number;
|
|
@@ -171,9 +171,10 @@ export function registerGit(program: Command) {
|
|
|
171
171
|
// lizard git status
|
|
172
172
|
git
|
|
173
173
|
.command("status")
|
|
174
|
-
.description("
|
|
174
|
+
.description("In a Lizard Git checkout: your changes. Elsewhere: GitHub connection and the repositories this project's services deploy from")
|
|
175
175
|
.option("-p, --project <id>", "Project name, slug, or ID")
|
|
176
176
|
.action(async (opts) => {
|
|
177
|
+
if (!opts.project && checkoutStatus()) return;
|
|
177
178
|
const { projectId, scope } = await resolveProjectScope(opts.project);
|
|
178
179
|
|
|
179
180
|
const [githubStatus, services] = await Promise.all([
|
package/src/index.ts
CHANGED
|
@@ -146,9 +146,8 @@ program
|
|
|
146
146
|
topLevel = topLevel.parent;
|
|
147
147
|
}
|
|
148
148
|
if (noAuth.has(topLevel.name())) return;
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
if (topLevel.name() === "git" && actionCommand.name() === "credential") return;
|
|
149
|
+
// Lizard Git with its own token (LIZARD_GIT_TOKEN: CI, agents): no Lizard sign-in needed.
|
|
150
|
+
if (topLevel.name() === "git" && process.env.LIZARD_GIT_TOKEN) return;
|
|
152
151
|
|
|
153
152
|
// Require auth — auto-triggers login flow if not logged in
|
|
154
153
|
const creds = await requireAuth();
|