Add AgentOS daemon control plane and maintenance tooling
This commit is contained in:
321
scripts/subtree.ts
Normal file
321
scripts/subtree.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import manifest from "./subtrees.json";
|
||||
|
||||
interface Subtree {
|
||||
readonly name: string;
|
||||
readonly prefix: string;
|
||||
readonly upstream: string;
|
||||
readonly ref: string;
|
||||
readonly squash: true;
|
||||
readonly role: string;
|
||||
readonly refreshPolicy: "allowed" | "pinned";
|
||||
}
|
||||
|
||||
interface CommandResult {
|
||||
readonly exitCode: number;
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
const subtrees = manifest.subtrees as readonly Subtree[];
|
||||
|
||||
const run = async (
|
||||
command: readonly string[],
|
||||
options: { readonly inherit?: boolean } = {}
|
||||
): Promise<CommandResult> => {
|
||||
const process = Bun.spawn([...command], {
|
||||
cwd,
|
||||
stdin: options.inherit ? "inherit" : "ignore",
|
||||
stdout: options.inherit ? "inherit" : "pipe",
|
||||
stderr: options.inherit ? "inherit" : "pipe",
|
||||
});
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
process.exited,
|
||||
options.inherit ? Promise.resolve("") : new Response(process.stdout).text(),
|
||||
options.inherit ? Promise.resolve("") : new Response(process.stderr).text(),
|
||||
]);
|
||||
return { exitCode, stdout, stderr };
|
||||
};
|
||||
|
||||
const shellQuote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`;
|
||||
const formatCommand = (command: readonly string[]) =>
|
||||
command.map(shellQuote).join(" ");
|
||||
|
||||
const requireRepositoryRoot = async () => {
|
||||
const result = await run(["git", "rev-parse", "--show-toplevel"]);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error("The current directory is not inside a Git checkout.");
|
||||
}
|
||||
if (result.stdout.trim() !== cwd) {
|
||||
throw new Error(
|
||||
`Run this script from the repository root: ${result.stdout.trim()}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const requireCleanWorktree = async () => {
|
||||
const result = await run(["git", "status", "--porcelain"]);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(result.stderr.trim());
|
||||
}
|
||||
if (result.stdout.trim() !== "") {
|
||||
throw new Error(
|
||||
"Subtree mutations require a clean worktree. Commit or stash changes first."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getSubtree = (name: string | undefined) => {
|
||||
if (name === undefined) {
|
||||
throw new Error(
|
||||
`A subtree name is required. Available: ${subtrees.map((entry) => entry.name).join(", ")}`
|
||||
);
|
||||
}
|
||||
const subtree = subtrees.find((entry) => entry.name === name);
|
||||
if (subtree === undefined) {
|
||||
throw new Error(
|
||||
`Unknown subtree ${shellQuote(name)}. Available: ${subtrees.map((entry) => entry.name).join(", ")}`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!subtree.prefix.startsWith("repos/") ||
|
||||
!subtree.upstream.startsWith("https://")
|
||||
) {
|
||||
throw new Error(
|
||||
`Unsafe subtree manifest entry ${shellQuote(subtree.name)}.`
|
||||
);
|
||||
}
|
||||
return subtree;
|
||||
};
|
||||
|
||||
const readSubtreeMetadata = async (subtree: Subtree) => {
|
||||
const merge = await run([
|
||||
"git",
|
||||
"log",
|
||||
"--all",
|
||||
"-n",
|
||||
"1",
|
||||
"--format=%H%x09%ci%x09%s",
|
||||
"--",
|
||||
subtree.prefix,
|
||||
]);
|
||||
if (merge.exitCode !== 0) {
|
||||
throw new Error(merge.stderr.trim());
|
||||
}
|
||||
const mergeLine = merge.stdout.trim();
|
||||
if (mergeLine === "") {
|
||||
return { mergeLine: "No subtree merge found", split: undefined };
|
||||
}
|
||||
const mergeSha = mergeLine.split("\t")[0];
|
||||
if (mergeSha === undefined) {
|
||||
return { mergeLine, split: undefined };
|
||||
}
|
||||
const squash = await run([
|
||||
"git",
|
||||
"show",
|
||||
"--no-patch",
|
||||
"--format=%b",
|
||||
`${mergeSha}^2`,
|
||||
]);
|
||||
const split = squash.stdout
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("git-subtree-split: "))
|
||||
?.slice("git-subtree-split: ".length);
|
||||
return { mergeLine, split };
|
||||
};
|
||||
|
||||
const readStatus = async (subtree: Subtree) => {
|
||||
const [dirty, metadata] = await Promise.all([
|
||||
run(["git", "status", "--porcelain", "--", subtree.prefix]),
|
||||
readSubtreeMetadata(subtree),
|
||||
]);
|
||||
if (dirty.exitCode !== 0) {
|
||||
throw new Error(dirty.stderr.trim());
|
||||
}
|
||||
return {
|
||||
dirty: dirty.stdout.trim() !== "",
|
||||
latest: metadata.mergeLine,
|
||||
};
|
||||
};
|
||||
|
||||
const updateCommand = (subtree: Subtree) => [
|
||||
"git",
|
||||
"subtree",
|
||||
"pull",
|
||||
`--prefix=${subtree.prefix}`,
|
||||
subtree.upstream,
|
||||
subtree.ref,
|
||||
"--squash",
|
||||
];
|
||||
|
||||
const addCommand = (subtree: Subtree) => [
|
||||
"git",
|
||||
"subtree",
|
||||
"add",
|
||||
`--prefix=${subtree.prefix}`,
|
||||
subtree.upstream,
|
||||
subtree.ref,
|
||||
"--squash",
|
||||
];
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
"force-pinned": { type: "boolean", default: false },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
yes: { type: "boolean", short: "y", default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const [operation = "list", name, ...extra] = positionals;
|
||||
if (values.help || extra.length > 0) {
|
||||
console.log(`Usage: bun run subtree [list|status|preview|add|update] [name] [options]
|
||||
|
||||
Commands:
|
||||
list List configured vendored subtrees
|
||||
status [name] Show local subtree metadata and dirtiness
|
||||
preview <name> Fetch the configured upstream and show incoming commits
|
||||
add <name> Add an absent manifest entry with git subtree add --squash
|
||||
update <name> Pull a configured subtree with git subtree pull --squash
|
||||
|
||||
Options:
|
||||
--dry-run Print mutating/fetch commands without executing them
|
||||
--yes, -y Required for add and update
|
||||
--force-pinned Allow updating a pinned historical subtree together with --yes`);
|
||||
process.exit(extra.length > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
try {
|
||||
await requireRepositoryRoot();
|
||||
|
||||
switch (operation) {
|
||||
case "list": {
|
||||
for (const subtree of subtrees) {
|
||||
console.log(
|
||||
`${subtree.name}\t${subtree.prefix}\t${subtree.upstream}#${subtree.ref}\t${subtree.refreshPolicy}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "status": {
|
||||
const selected = name === undefined ? subtrees : [getSubtree(name)];
|
||||
for (const subtree of selected) {
|
||||
const status = await readStatus(subtree);
|
||||
console.log(`${subtree.name}:`);
|
||||
console.log(` prefix: ${subtree.prefix}`);
|
||||
console.log(` upstream: ${subtree.upstream}#${subtree.ref}`);
|
||||
console.log(` policy: ${subtree.refreshPolicy}`);
|
||||
console.log(` prefix dirty: ${status.dirty ? "yes" : "no"}`);
|
||||
console.log(` latest metadata: ${status.latest}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "preview": {
|
||||
const subtree = getSubtree(name);
|
||||
const fetch = [
|
||||
"git",
|
||||
"fetch",
|
||||
"--no-tags",
|
||||
subtree.upstream,
|
||||
subtree.ref,
|
||||
];
|
||||
if (values["dry-run"]) {
|
||||
console.log(formatCommand(fetch));
|
||||
console.log(
|
||||
formatCommand([
|
||||
"git",
|
||||
"log",
|
||||
"--oneline",
|
||||
"--decorate",
|
||||
"<last-subtree-split>..FETCH_HEAD",
|
||||
])
|
||||
);
|
||||
break;
|
||||
}
|
||||
const fetched = await run(fetch, { inherit: true });
|
||||
if (fetched.exitCode !== 0) {
|
||||
process.exit(fetched.exitCode);
|
||||
}
|
||||
const metadata = await readSubtreeMetadata(subtree);
|
||||
if (metadata.split === undefined) {
|
||||
throw new Error(
|
||||
`No git-subtree-split metadata found for ${subtree.prefix}.`
|
||||
);
|
||||
}
|
||||
const log = await run([
|
||||
"git",
|
||||
"log",
|
||||
"--oneline",
|
||||
"--decorate",
|
||||
`${metadata.split}..FETCH_HEAD`,
|
||||
]);
|
||||
if (log.exitCode !== 0) {
|
||||
throw new Error(log.stderr.trim());
|
||||
}
|
||||
console.log(log.stdout.trim() || `${subtree.name} is up to date.`);
|
||||
break;
|
||||
}
|
||||
case "add":
|
||||
case "update": {
|
||||
const subtree = getSubtree(name);
|
||||
const prefixType = await run([
|
||||
"git",
|
||||
"cat-file",
|
||||
"-t",
|
||||
`HEAD:${subtree.prefix}`,
|
||||
]);
|
||||
const prefixExists =
|
||||
prefixType.exitCode === 0 && prefixType.stdout.trim() === "tree";
|
||||
if (
|
||||
operation === "update" &&
|
||||
subtree.refreshPolicy === "pinned" &&
|
||||
!values["force-pinned"]
|
||||
) {
|
||||
throw new Error(
|
||||
`${subtree.name} is pinned historical reference material. Pass --force-pinned --yes only when historical synchronization is explicitly required.`
|
||||
);
|
||||
}
|
||||
if (!values["dry-run"]) {
|
||||
await requireCleanWorktree();
|
||||
}
|
||||
if (operation === "add" && prefixExists) {
|
||||
throw new Error(
|
||||
`Cannot add ${subtree.name}: ${subtree.prefix} already exists.`
|
||||
);
|
||||
}
|
||||
if (operation === "update" && !prefixExists) {
|
||||
throw new Error(
|
||||
`Cannot update ${subtree.name}: ${subtree.prefix} does not exist.`
|
||||
);
|
||||
}
|
||||
const command =
|
||||
operation === "add" ? addCommand(subtree) : updateCommand(subtree);
|
||||
console.log(formatCommand(command));
|
||||
if (values["dry-run"]) {
|
||||
break;
|
||||
}
|
||||
if (!values.yes) {
|
||||
throw new Error(
|
||||
`Refusing to ${operation} without --yes. Re-run after reviewing the command above.`
|
||||
);
|
||||
}
|
||||
const result = await run(command, { inherit: true });
|
||||
if (result.exitCode !== 0) {
|
||||
process.exit(result.exitCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown command ${shellQuote(operation)}. Use list, status, preview, add, or update.`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user