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);
|
||||
}
|
||||
22
scripts/subtrees.json
Normal file
22
scripts/subtrees.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"subtrees": [
|
||||
{
|
||||
"name": "effect",
|
||||
"prefix": "repos/effect",
|
||||
"upstream": "https://github.com/Effect-TS/effect.git",
|
||||
"ref": "main",
|
||||
"squash": true,
|
||||
"role": "Current Effect v4 source of truth",
|
||||
"refreshPolicy": "allowed"
|
||||
},
|
||||
{
|
||||
"name": "effect-smol",
|
||||
"prefix": "repos/effect-smol",
|
||||
"upstream": "https://github.com/Effect-TS/effect-smol.git",
|
||||
"ref": "main",
|
||||
"squash": true,
|
||||
"role": "Archived historical Effect v4 reference",
|
||||
"refreshPolicy": "pinned"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
scripts/tsconfig.json
Normal file
7
scripts/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../packages/config/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
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