Skip to content

Commit ef13c2f

Browse files
authored
refactor(codex): unify protocol artifact staging (#104948)
1 parent 982efe5 commit ef13c2f

3 files changed

Lines changed: 107 additions & 68 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
4+
type CodexProtocolArtifactRoots = {
5+
jsonRoot: string;
6+
typescriptRoot: string;
7+
};
8+
9+
export async function stageCodexAppServerProtocolArtifacts(
10+
sourceRoot: string,
11+
roots: CodexProtocolArtifactRoots,
12+
): Promise<void> {
13+
await stageGeneratedArtifactDirectory(sourceRoot, sourceRoot, roots);
14+
}
15+
16+
async function stageGeneratedArtifactDirectory(
17+
sourceRoot: string,
18+
currentRoot: string,
19+
roots: CodexProtocolArtifactRoots,
20+
): Promise<void> {
21+
const entries = await fs.readdir(currentRoot, { withFileTypes: true });
22+
await Promise.all(
23+
entries.map(async (entry) => {
24+
const sourcePath = path.join(currentRoot, entry.name);
25+
if (entry.isDirectory()) {
26+
await stageGeneratedArtifactDirectory(sourceRoot, sourcePath, roots);
27+
return;
28+
}
29+
if (!entry.isFile()) {
30+
return;
31+
}
32+
33+
const kind = entry.name.endsWith(".ts")
34+
? "typescript"
35+
: entry.name.endsWith(".json")
36+
? "json"
37+
: undefined;
38+
if (kind === undefined) {
39+
return;
40+
}
41+
42+
const targetRoot = kind === "typescript" ? roots.typescriptRoot : roots.jsonRoot;
43+
const targetPath = path.join(targetRoot, path.relative(sourceRoot, sourcePath));
44+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
45+
if (kind === "json") {
46+
await fs.copyFile(sourcePath, targetPath);
47+
return;
48+
}
49+
50+
const source = await fs.readFile(sourcePath, "utf8");
51+
await fs.writeFile(targetPath, normalizeGeneratedTypeScript(source));
52+
}),
53+
);
54+
}
55+
56+
function normalizeGeneratedTypeScript(text: string): string {
57+
// Codex emits TS-oriented relative imports; OpenClaw consumes the staged tree as NodeNext.
58+
return text
59+
.replace(/(from\s+["'])(\.{1,2}\/[^"']+?)(\.js)?(["'])/g, "$1$2.js$4")
60+
.replace('export * as v2 from "./v2.js";', 'export * as v2 from "./v2/index.js";')
61+
.replaceAll("| null | null", "| null");
62+
}

scripts/lib/codex-app-server-protocol-source.ts

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
33
import fs from "node:fs/promises";
44
import path from "node:path";
55
import { resolvePnpmRunner } from "../pnpm-runner.mjs";
6+
import { stageCodexAppServerProtocolArtifacts } from "./codex-app-server-protocol-artifacts.js";
67

78
const PROTOCOL_SCHEMA_RELATIVE_PATH = "codex-rs/app-server-protocol/schema";
89
const DEFAULT_PROTOCOL_GENERATION_MIN_FREE_BYTES = 10 * 1024 * 1024 * 1024;
@@ -171,8 +172,7 @@ export async function generateExperimentalCodexAppServerProtocolSource(
171172
try {
172173
await assertCodexProtocolGenerationHeadroom({ codexRepo, repoRoot });
173174
runCargoProtocolGenerator(codexRepo, buildCodexProtocolExportArgs(manifestPath, generatedRoot));
174-
await splitGeneratedProtocolOutput(generatedRoot, { jsonRoot, typescriptRoot });
175-
await rewriteTypeScriptImports(typescriptRoot);
175+
await stageCodexAppServerProtocolArtifacts(generatedRoot, { jsonRoot, typescriptRoot });
176176
formatGeneratedTypeScript(repoRoot, typescriptRoot);
177177
} catch (error) {
178178
await cleanup();
@@ -316,47 +316,6 @@ async function resolveExistingStatfsPath(targetPath: string): Promise<string> {
316316
}
317317
}
318318

319-
async function splitGeneratedProtocolOutput(
320-
sourceRoot: string,
321-
roots: { jsonRoot: string; typescriptRoot: string },
322-
): Promise<void> {
323-
await copyGeneratedProtocolFiles(sourceRoot, sourceRoot, roots);
324-
}
325-
326-
async function copyGeneratedProtocolFiles(
327-
sourceRoot: string,
328-
currentRoot: string,
329-
roots: { jsonRoot: string; typescriptRoot: string },
330-
): Promise<void> {
331-
const entries = await fs.readdir(currentRoot, { withFileTypes: true });
332-
await Promise.all(
333-
entries.map(async (entry) => {
334-
const sourcePath = path.join(currentRoot, entry.name);
335-
if (entry.isDirectory()) {
336-
await copyGeneratedProtocolFiles(sourceRoot, sourcePath, roots);
337-
return;
338-
}
339-
if (!entry.isFile()) {
340-
return;
341-
}
342-
343-
const relativePath = path.relative(sourceRoot, sourcePath);
344-
const targetRoot = entry.name.endsWith(".ts")
345-
? roots.typescriptRoot
346-
: entry.name.endsWith(".json")
347-
? roots.jsonRoot
348-
: null;
349-
if (targetRoot === null) {
350-
return;
351-
}
352-
353-
const targetPath = path.join(targetRoot, relativePath);
354-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
355-
await fs.copyFile(sourcePath, targetPath);
356-
}),
357-
);
358-
}
359-
360319
function runCargoProtocolGenerator(codexRepo: string, args: string[]): void {
361320
const result = spawnSync("cargo", args, {
362321
cwd: codexRepo,
@@ -399,31 +358,6 @@ function formatGeneratedTypeScript(repoRoot: string, root: string): void {
399358
}
400359
}
401360

402-
async function rewriteTypeScriptImports(root: string): Promise<void> {
403-
const entries = await fs.readdir(root, { withFileTypes: true });
404-
await Promise.all(
405-
entries.map(async (entry) => {
406-
const fullPath = path.join(root, entry.name);
407-
if (entry.isDirectory()) {
408-
await rewriteTypeScriptImports(fullPath);
409-
return;
410-
}
411-
if (!entry.isFile() || !entry.name.endsWith(".ts")) {
412-
return;
413-
}
414-
const text = await fs.readFile(fullPath, "utf8");
415-
await fs.writeFile(fullPath, normalizeGeneratedTypeScript(text));
416-
}),
417-
);
418-
}
419-
420-
function normalizeGeneratedTypeScript(text: string): string {
421-
return text
422-
.replace(/(from\s+["'])(\.{1,2}\/[^"']+?)(\.js)?(["'])/g, "$1$2.js$4")
423-
.replace('export * as v2 from "./v2.js";', 'export * as v2 from "./v2/index.js";')
424-
.replaceAll("| null | null", "| null");
425-
}
426-
427361
// Sort typed-object arrays for schema keywords whose item order does not affect
428362
// payload validity; preserve order everywhere else, especially prefixItems.
429363
const typeSortedSchemaArrayKeys = new Set(["anyOf", "enum", "oneOf", "required"]);

test/scripts/codex-app-server-protocol-source.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import fs from "node:fs";
33
import path from "node:path";
44
import { afterEach, describe, expect, it } from "vitest";
5+
import { stageCodexAppServerProtocolArtifacts } from "../../scripts/lib/codex-app-server-protocol-artifacts.js";
56
import {
67
buildCodexProtocolExportArgs,
78
canonicalizeCodexAppServerProtocolJson,
@@ -27,6 +28,48 @@ afterEach(() => {
2728
}
2829
});
2930

31+
describe("Codex app-server generated artifact staging", () => {
32+
it("copies JSON bytes and normalizes nested TypeScript files in one pass", async () => {
33+
const sourceRoot = createTempDir("openclaw-protocol-artifacts-source-");
34+
const targetRoot = createTempDir("openclaw-protocol-artifacts-target-");
35+
const typescriptRoot = path.join(targetRoot, "typescript");
36+
const jsonRoot = path.join(targetRoot, "json");
37+
const rootTypeScript = [
38+
'import type { Root } from "./Root";',
39+
"export type { Parent } from '../Parent.js';",
40+
'export * as v2 from "./v2.js";',
41+
"export type Nullable = string | null | null;",
42+
"",
43+
].join("\n");
44+
const nestedTypeScript = 'export type { Shared } from "../Shared";\n';
45+
const json = '{\n "z": 1,\n "a": 2\n}\n';
46+
fs.mkdirSync(path.join(sourceRoot, "v2"), { recursive: true });
47+
fs.writeFileSync(path.join(sourceRoot, "index.ts"), rootTypeScript);
48+
fs.writeFileSync(path.join(sourceRoot, "v2/Thing.ts"), nestedTypeScript);
49+
fs.writeFileSync(path.join(sourceRoot, "v2/Thing.json"), json);
50+
fs.writeFileSync(path.join(sourceRoot, "README.md"), "ignored\n");
51+
52+
await stageCodexAppServerProtocolArtifacts(sourceRoot, { jsonRoot, typescriptRoot });
53+
54+
expect(fs.readFileSync(path.join(typescriptRoot, "index.ts"), "utf8")).toBe(
55+
[
56+
'import type { Root } from "./Root.js";',
57+
"export type { Parent } from '../Parent.js';",
58+
'export * as v2 from "./v2/index.js";',
59+
"export type Nullable = string | null;",
60+
"",
61+
].join("\n"),
62+
);
63+
expect(fs.readFileSync(path.join(typescriptRoot, "v2/Thing.ts"), "utf8")).toBe(
64+
'export type { Shared } from "../Shared.js";\n',
65+
);
66+
expect(fs.readFileSync(path.join(jsonRoot, "v2/Thing.json"), "utf8")).toBe(json);
67+
expect(fs.existsSync(path.join(typescriptRoot, "README.md"))).toBe(false);
68+
expect(fs.existsSync(path.join(jsonRoot, "README.md"))).toBe(false);
69+
expect(fs.readFileSync(path.join(sourceRoot, "index.ts"), "utf8")).toBe(rootTypeScript);
70+
});
71+
});
72+
3073
describe("codex app-server protocol source resolver", () => {
3174
it("reads the Cargo workspace package version without matching sibling sections", () => {
3275
expect(

0 commit comments

Comments
 (0)