@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
75 lines (74 loc) • 3.13 kB
JavaScript
import { mkdtemp, rm as rmrf } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { gitWithAuth, resolveToken, vcsApi } from "./common.js";
function normalize(p) {
return p.trim().replace(/^\.\//, "").replace(/\/+$/, "");
}
export function expandPathsToDeletions(repoFiles, requested) {
const out = new Set();
for (const raw of requested) {
const path = normalize(raw);
if (path.startsWith("/") || path.split("/").includes("..")) {
throw new Error(`"${raw}" is not a repo-relative path — vcs paths never start with "/" or traverse with "..".`);
}
const exact = repoFiles.filter((f) => f === path);
if (exact.length > 0) {
for (const f of exact)
out.add(f);
continue;
}
const under = repoFiles.filter((f) => f.startsWith(`${path}/`));
if (under.length === 0) {
throw new Error(`"${raw}" matched no files in the repo — nothing was proposed. ` +
`Check the path with: mesh vcs get <repo> ${path}`);
}
for (const f of under)
out.add(f);
}
return [...out].sort();
}
export async function rmCommand(repo, paths, opts) {
const baseUrl = (opts.url ?? process.env.VCS_URL)?.replace(/\/$/, "");
if (!baseUrl)
throw new Error("rm requires --url or VCS_URL");
const token = await resolveToken(opts);
const dir = await mkdtemp(join(tmpdir(), "mesh-vcs-rm-"));
try {
const git = (args, cwd) => gitWithAuth(args, token, cwd);
await git([
"clone",
"--filter=blob:none",
"--no-checkout",
"-c",
`http.extraHeader=Authorization: Bearer ${token}`,
`${baseUrl}/git/${repo}`,
dir,
]);
const { stdout: treeOut } = await git(["ls-tree", "-r", "--name-only", "HEAD"], dir);
const repoFiles = treeOut.split("\n").filter(Boolean);
const doomed = expandPathsToDeletions(repoFiles, paths);
if (opts.dryRun) {
console.log(`would delete ${doomed.length} file(s) from ${repo}:`);
for (const p of doomed)
console.log(` ${p}`);
console.log("(--dry-run: nothing proposed)");
return;
}
const { stdout: head } = await git(["rev-parse", "HEAD"], dir);
const body = {
baseCommit: head.trim(),
message: opts.message ?? `Delete ${doomed.length} file(s)`,
operations: doomed.map((path) => ({ op: "delete", path })),
};
const data = (await vcsApi(baseUrl, token, `/v1/repos/${repo}/proposals`, "POST", body));
console.log(`deleting ${doomed.length} file(s) from ${repo}:`);
for (const p of doomed)
console.log(` ${p}`);
console.log(`proposal ${data.proposalId} @ ${data.sha.slice(0, 8)} — a human must approve: ` +
`mesh vcs approve ${repo} ${data.proposalId} --url ${baseUrl}`);
}
finally {
await rmrf(dir, { recursive: true, force: true });
}
}