Add AgentOS daemon control plane and maintenance tooling
This commit is contained in:
282
scripts/update-docs.ts
Normal file
282
scripts/update-docs.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const excludedPrefixes = [
|
||||
".agents/",
|
||||
".claude/skills/",
|
||||
"node_modules/",
|
||||
"repos/",
|
||||
"dist/",
|
||||
"build/",
|
||||
"apps/web/build/",
|
||||
"apps/web/.react-router/",
|
||||
"apps/native/.expo/",
|
||||
"apps/native/dist/",
|
||||
"apps/native/web-build/",
|
||||
"packages/backend/convex/_generated/",
|
||||
];
|
||||
|
||||
const documentationNames = new Set([
|
||||
"AGENTS.md",
|
||||
"README.md",
|
||||
"README.mdx",
|
||||
"LLMS.md",
|
||||
]);
|
||||
|
||||
const supportedAgents = ["codex", "opencode", "claude"] as const;
|
||||
type AgentName = (typeof supportedAgents)[number];
|
||||
|
||||
interface CommandResult {
|
||||
readonly exitCode: number;
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
}
|
||||
|
||||
const run = async (
|
||||
command: readonly string[],
|
||||
options: { readonly stdin?: string; readonly inherit?: boolean } = {}
|
||||
): Promise<CommandResult> => {
|
||||
const process = Bun.spawn([...command], {
|
||||
cwd: processCwd,
|
||||
stdin: options.stdin === undefined ? "ignore" : new Blob([options.stdin]),
|
||||
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 isExcluded = (path: string) =>
|
||||
excludedPrefixes.some((prefix) => {
|
||||
const directory = prefix.slice(0, -1);
|
||||
return (
|
||||
path === directory ||
|
||||
path.startsWith(prefix) ||
|
||||
path.includes(`/${directory}/`) ||
|
||||
path.endsWith(`/${directory}`)
|
||||
);
|
||||
});
|
||||
|
||||
const isDocumentation = (path: string) => {
|
||||
const name = path.split("/").at(-1) ?? path;
|
||||
return (
|
||||
documentationNames.has(name) ||
|
||||
name.endsWith(".md") ||
|
||||
name.endsWith(".mdx")
|
||||
);
|
||||
};
|
||||
|
||||
const resolveRange = async (revision: string | undefined) => {
|
||||
const requested = revision ?? "HEAD";
|
||||
if (requested.includes("..")) {
|
||||
const result = await run(["git", "rev-list", "--max-count=1", requested]);
|
||||
if (result.exitCode !== 0 || result.stdout.trim() === "") {
|
||||
throw new Error(
|
||||
`Invalid revision range ${shellQuote(requested)}: ${result.stderr.trim()}`
|
||||
);
|
||||
}
|
||||
return requested;
|
||||
}
|
||||
const verify = await run([
|
||||
"git",
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
`${requested}^{commit}`,
|
||||
]);
|
||||
if (verify.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Invalid commit ${shellQuote(requested)}: ${verify.stderr.trim()}`
|
||||
);
|
||||
}
|
||||
return `${requested}^!`;
|
||||
};
|
||||
|
||||
const listChangedFiles = async (range: string) => {
|
||||
const result = await run([
|
||||
"git",
|
||||
"diff",
|
||||
"--name-only",
|
||||
"--diff-filter=ACDMRTUXB",
|
||||
range,
|
||||
"--",
|
||||
]);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Unable to inspect ${shellQuote(range)}: ${result.stderr.trim()}`
|
||||
);
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((path) => path.trim())
|
||||
.filter((path) => path !== "" && !isExcluded(path));
|
||||
};
|
||||
|
||||
const listDocumentationFiles = async () => {
|
||||
const result = await run(["git", "ls-files", "*.md", "*.mdx"]);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Unable to list documentation files: ${result.stderr.trim()}`
|
||||
);
|
||||
}
|
||||
return result.stdout
|
||||
.split("\n")
|
||||
.map((path) => path.trim())
|
||||
.filter(
|
||||
(path) => path !== "" && !isExcluded(path) && isDocumentation(path)
|
||||
);
|
||||
};
|
||||
|
||||
const resolveAgent = (requested: string | undefined): AgentName => {
|
||||
const configured = requested ?? process.env.DOCS_AGENT;
|
||||
if (configured !== undefined) {
|
||||
if (!supportedAgents.includes(configured as AgentName)) {
|
||||
throw new Error(
|
||||
`Unsupported agent ${shellQuote(configured)}. Use codex, opencode, or claude.`
|
||||
);
|
||||
}
|
||||
if (Bun.which(configured) === null) {
|
||||
throw new Error(
|
||||
`Agent executable ${shellQuote(configured)} was not found on PATH.`
|
||||
);
|
||||
}
|
||||
return configured as AgentName;
|
||||
}
|
||||
const detected = supportedAgents.find((agent) => Bun.which(agent) !== null);
|
||||
if (detected === undefined) {
|
||||
throw new Error(
|
||||
"No supported coding agent found. Install codex, opencode, or claude, or set DOCS_AGENT."
|
||||
);
|
||||
}
|
||||
return detected;
|
||||
};
|
||||
|
||||
const agentCommand = (agent: AgentName) => {
|
||||
switch (agent) {
|
||||
case "codex":
|
||||
return [
|
||||
"codex",
|
||||
"exec",
|
||||
"-",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--cd",
|
||||
processCwd,
|
||||
"--ephemeral",
|
||||
];
|
||||
case "opencode":
|
||||
return ["opencode", "run", "--auto"];
|
||||
case "claude":
|
||||
return ["claude", "--print", "--permission-mode", "acceptEdits"];
|
||||
}
|
||||
};
|
||||
|
||||
const buildPrompt = (input: {
|
||||
readonly range: string;
|
||||
readonly changedFiles: readonly string[];
|
||||
readonly documentationFiles: readonly string[];
|
||||
readonly diff: string;
|
||||
}) => `Update repository documentation for revision ${input.range} in the current checkout.
|
||||
|
||||
Requirements:
|
||||
- Read and follow the root AGENTS.md and any more specific AGENTS.md governing files you edit.
|
||||
- Inspect the supplied diff and the current code before editing; do not document behavior that is not implemented.
|
||||
- Update affected README.md, AGENTS.md, LLMS.md, or other first-party Markdown/MDX files when the revision made them stale.
|
||||
- Keep documentation concise and practical. Preserve generated markers and package-specific instructions.
|
||||
- Never edit repos/**, .agents/**, .claude/skills/**, generated files, or dependency/build output.
|
||||
- Do not modify source code, run broad formatting, or commit changes.
|
||||
- If documentation is already accurate, make no edits and say so.
|
||||
|
||||
Changed first-party files:
|
||||
${input.changedFiles.map((path) => `- ${path}`).join("\n") || "- None"}
|
||||
|
||||
First-party documentation candidates:
|
||||
${input.documentationFiles.map((path) => `- ${path}`).join("\n") || "- None"}
|
||||
|
||||
Revision diff:
|
||||
\`\`\`diff
|
||||
${input.diff}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const processCwd = process.cwd();
|
||||
const { values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
agent: { type: "string" },
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
if (values.help || positionals.length > 1) {
|
||||
console.log(`Usage: bun run docs:update [<commit-or-range>] [--agent codex|opencode|claude] [--dry-run]
|
||||
|
||||
A single commit defaults to HEAD and is converted to <commit>^!. Revision ranges such as main..HEAD and main...HEAD are accepted directly. Set DOCS_AGENT to choose the default coding-agent CLI.`);
|
||||
process.exit(positionals.length > 1 ? 1 : 0);
|
||||
}
|
||||
|
||||
try {
|
||||
const range = await resolveRange(positionals[0]);
|
||||
const [changedFiles, documentationFiles, diffResult] = await Promise.all([
|
||||
listChangedFiles(range),
|
||||
listDocumentationFiles(),
|
||||
run([
|
||||
"git",
|
||||
"diff",
|
||||
"--binary",
|
||||
"--find-renames",
|
||||
range,
|
||||
"--",
|
||||
".",
|
||||
":(exclude)repos/**",
|
||||
]),
|
||||
]);
|
||||
if (diffResult.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Unable to read revision diff: ${diffResult.stderr.trim()}`
|
||||
);
|
||||
}
|
||||
const agent = resolveAgent(values.agent);
|
||||
const command = agentCommand(agent);
|
||||
const prompt = buildPrompt({
|
||||
range,
|
||||
changedFiles,
|
||||
documentationFiles,
|
||||
diff: diffResult.stdout,
|
||||
});
|
||||
|
||||
console.log(`Revision: ${range}`);
|
||||
console.log(`Agent: ${agent}`);
|
||||
console.log(`Changed first-party files: ${changedFiles.length}`);
|
||||
console.log(`Command: ${formatCommand(command)} < generated-prompt`);
|
||||
|
||||
if (values["dry-run"]) {
|
||||
console.log("\n--- Generated prompt ---\n");
|
||||
console.log(prompt);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = await run(command, { stdin: prompt, inherit: true });
|
||||
if (result.exitCode !== 0) {
|
||||
process.exit(result.exitCode);
|
||||
}
|
||||
const status = await run(["git", "status", "--short", "--", "*.md", "*.mdx"]);
|
||||
if (status.stdout.trim() !== "") {
|
||||
console.log("\nDocumentation changes:");
|
||||
console.log(status.stdout.trimEnd());
|
||||
} else {
|
||||
console.log("\nNo documentation files changed.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user