fix: restore lightweight root help startup

This commit is contained in:
Peter Steinberger
2026-03-30 03:08:33 +01:00
parent 2481c0a9b6
commit 25074de838
2 changed files with 138 additions and 37 deletions

View File

@@ -1,7 +1,6 @@
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { pathToFileURL } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
function dedupe(values: string[]): string[] {
const seen = new Set<string>();
@@ -16,7 +15,8 @@ function dedupe(values: string[]): string[] {
return out;
}
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const scriptPath = fileURLToPath(import.meta.url);
const scriptDir = path.dirname(scriptPath);
const rootDir = path.resolve(scriptDir, "..");
const distDir = path.join(rootDir, "dist");
const outputPath = path.join(distDir, "cli-startup-metadata.json");
@@ -38,13 +38,15 @@ type ExtensionChannelEntry = {
label: string;
};
function readBundledChannelCatalogIds(): string[] {
export function readBundledChannelCatalogIds(
extensionsDirOverride: string = extensionsDir,
): string[] {
const entries: ExtensionChannelEntry[] = [];
for (const dirEntry of readdirSync(extensionsDir, { withFileTypes: true })) {
for (const dirEntry of readdirSync(extensionsDirOverride, { withFileTypes: true })) {
if (!dirEntry.isDirectory()) {
continue;
}
const packageJsonPath = path.join(extensionsDir, dirEntry.name, "package.json");
const packageJsonPath = path.join(extensionsDirOverride, dirEntry.name, "package.json");
try {
const raw = readFileSync(packageJsonPath, "utf8");
const parsed = JSON.parse(raw) as {
@@ -76,19 +78,7 @@ function readBundledChannelCatalogIds(): string[] {
.map((entry) => entry.id);
}
async function renderBundledRootHelpText(): Promise<string> {
const bundleName = readdirSync(distDir).find(
(entry) => entry.startsWith("root-help-") && entry.endsWith(".js"),
);
if (!bundleName) {
throw new Error("No root-help bundle found in dist; cannot write CLI startup metadata.");
}
const moduleUrl = pathToFileURL(path.join(distDir, bundleName)).href;
const mod = (await import(moduleUrl)) as { outputRootHelp?: () => void };
if (typeof mod.outputRootHelp !== "function") {
throw new Error(`Bundle ${bundleName} does not export outputRootHelp.`);
}
async function captureStdout(action: () => void | Promise<void>): Promise<string> {
let output = "";
const originalWrite = process.stdout.write.bind(process.stdout);
const captureWrite: typeof process.stdout.write = ((chunk: string | Uint8Array) => {
@@ -97,28 +87,61 @@ async function renderBundledRootHelpText(): Promise<string> {
}) as typeof process.stdout.write;
process.stdout.write = captureWrite;
try {
mod.outputRootHelp();
await action();
} finally {
process.stdout.write = originalWrite;
}
return output;
}
const catalog = readBundledChannelCatalogIds();
const channelOptions = dedupe([...CORE_CHANNEL_ORDER, ...catalog]);
const rootHelpText = await renderBundledRootHelpText();
export async function renderBundledRootHelpText(
distDirOverride: string = distDir,
): Promise<string> {
const bundleName = readdirSync(distDirOverride).find(
(entry) => entry.startsWith("root-help-") && entry.endsWith(".js"),
);
if (!bundleName) {
throw new Error("No root-help bundle found in dist; cannot write CLI startup metadata.");
}
const moduleUrl = pathToFileURL(path.join(distDirOverride, bundleName)).href;
const mod = (await import(moduleUrl)) as { outputRootHelp?: () => void | Promise<void> };
if (typeof mod.outputRootHelp !== "function") {
throw new Error(`Bundle ${bundleName} does not export outputRootHelp.`);
}
mkdirSync(distDir, { recursive: true });
writeFileSync(
outputPath,
`${JSON.stringify(
{
generatedBy: "scripts/write-cli-startup-metadata.ts",
channelOptions,
rootHelpText,
},
null,
2,
)}\n`,
"utf8",
);
return captureStdout(async () => {
await mod.outputRootHelp?.();
});
}
export async function writeCliStartupMetadata(options?: {
distDir?: string;
outputPath?: string;
extensionsDir?: string;
}): Promise<void> {
const resolvedDistDir = options?.distDir ?? distDir;
const resolvedOutputPath = options?.outputPath ?? outputPath;
const resolvedExtensionsDir = options?.extensionsDir ?? extensionsDir;
const catalog = readBundledChannelCatalogIds(resolvedExtensionsDir);
const channelOptions = dedupe([...CORE_CHANNEL_ORDER, ...catalog]);
const rootHelpText = await renderBundledRootHelpText(resolvedDistDir);
mkdirSync(resolvedDistDir, { recursive: true });
writeFileSync(
resolvedOutputPath,
`${JSON.stringify(
{
generatedBy: "scripts/write-cli-startup-metadata.ts",
channelOptions,
rootHelpText,
},
null,
2,
)}\n`,
"utf8",
);
}
if (process.argv[1] && path.resolve(process.argv[1]) === scriptPath) {
await writeCliStartupMetadata();
}

View File

@@ -0,0 +1,78 @@
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
renderBundledRootHelpText,
writeCliStartupMetadata,
} from "../../scripts/write-cli-startup-metadata.ts";
function createTempDir(prefix: string): string {
return mkdtempSync(path.join(os.tmpdir(), prefix));
}
describe("write-cli-startup-metadata", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
it("captures async root help bundle output", async () => {
const distDir = createTempDir("openclaw-root-help-");
tempDirs.push(distDir);
writeFileSync(
path.join(distDir, "root-help-async.js"),
[
"export async function outputRootHelp() {",
" await Promise.resolve();",
" process.stdout.write('OpenClaw help\\n');",
"}",
"",
].join("\n"),
"utf8",
);
await expect(renderBundledRootHelpText(distDir)).resolves.toBe("OpenClaw help\n");
});
it("writes startup metadata with populated root help text", async () => {
const tempRoot = createTempDir("openclaw-startup-metadata-");
tempDirs.push(tempRoot);
const distDir = path.join(tempRoot, "dist");
const extensionsDir = path.join(tempRoot, "extensions");
const outputPath = path.join(distDir, "cli-startup-metadata.json");
mkdirSync(distDir, { recursive: true });
mkdirSync(path.join(extensionsDir, "matrix"), { recursive: true });
writeFileSync(
path.join(distDir, "root-help-fixture.js"),
"export async function outputRootHelp() { process.stdout.write('Usage: openclaw\\n'); }\n",
"utf8",
);
writeFileSync(
path.join(extensionsDir, "matrix", "package.json"),
JSON.stringify({
openclaw: {
channel: {
id: "matrix",
order: 120,
label: "Matrix",
},
},
}),
"utf8",
);
await writeCliStartupMetadata({ distDir, outputPath, extensionsDir });
const written = JSON.parse(readFileSync(outputPath, "utf8")) as {
channelOptions: string[];
rootHelpText: string;
};
expect(written.channelOptions).toContain("matrix");
expect(written.rootHelpText).toBe("Usage: openclaw\n");
});
});