feat(omo-claude): copy telemetry component sync script
Product-agnostic sync-telemetry-component.mjs (verbatim from omo-codex) + test; generates the telemetry component's atomic-write/data-path/env-flags/ posthog-activity-state from src/telemetry. product-identity is authored per-package, not synced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export const TELEMETRY_SYNC_FILES = [
|
||||
"atomic-write.ts",
|
||||
"data-path.ts",
|
||||
"env-flags.ts",
|
||||
"posthog-activity-state.ts",
|
||||
];
|
||||
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const PACKAGE_ROOT = dirname(SCRIPT_DIR);
|
||||
const DEFAULT_SOURCE_DIR = join(PACKAGE_ROOT, "src", "telemetry");
|
||||
const DEFAULT_COMPONENT_DIR = join(PACKAGE_ROOT, "plugin", "components", "telemetry", "src");
|
||||
|
||||
export async function syncTelemetryComponent(options = {}) {
|
||||
const sourceDir = resolve(options.sourceDir ?? DEFAULT_SOURCE_DIR);
|
||||
const componentDir = resolve(options.componentDir ?? DEFAULT_COMPONENT_DIR);
|
||||
const files = options.files ?? TELEMETRY_SYNC_FILES;
|
||||
const check = options.check ?? false;
|
||||
const changed = [];
|
||||
|
||||
for (const fileName of files) {
|
||||
const sourcePath = join(sourceDir, fileName);
|
||||
const componentPath = join(componentDir, fileName);
|
||||
const sourceText = await readFile(sourcePath, "utf8");
|
||||
const componentText = await readOptionalText(componentPath);
|
||||
const nextText = toComponentSource(sourceText);
|
||||
if (componentText === nextText) continue;
|
||||
changed.push(fileName);
|
||||
if (!check) {
|
||||
await mkdir(dirname(componentPath), { recursive: true });
|
||||
await writeFile(componentPath, nextText);
|
||||
}
|
||||
}
|
||||
|
||||
if (check && changed.length > 0) {
|
||||
throw new Error(`telemetry component out of sync: ${changed.join(", ")}`);
|
||||
}
|
||||
|
||||
return { checked: check, changed };
|
||||
}
|
||||
|
||||
function toComponentSource(sourceText) {
|
||||
return sourceText
|
||||
.replaceAll(/\bprocess\.env\.([A-Z0-9_]+)/g, 'process.env["$1"]')
|
||||
.replaceAll(/from "(\.\/[^"]+)"/g, 'from "$1.js"');
|
||||
}
|
||||
|
||||
async function readOptionalText(path) {
|
||||
try {
|
||||
return await readFile(path, "utf8");
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === "ENOENT") return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function isNodeError(error) {
|
||||
return error instanceof Error && "code" in error;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const parsed = {
|
||||
check: false,
|
||||
sourceDir: DEFAULT_SOURCE_DIR,
|
||||
componentDir: DEFAULT_COMPONENT_DIR,
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--check") {
|
||||
parsed.check = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--source-dir") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined) throw new Error("--source-dir requires a value");
|
||||
parsed.sourceDir = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--component-dir") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined) throw new Error("--component-dir requires a value");
|
||||
parsed.componentDir = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await syncTelemetryComponent(parseArgs(process.argv.slice(2)));
|
||||
if (result.changed.length === 0) {
|
||||
console.log("telemetry component in sync");
|
||||
return;
|
||||
}
|
||||
console.log(`synced telemetry component: ${result.changed.join(", ")}`);
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
|
||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const SCRIPT_PATH = new URL("./sync-telemetry-component.mjs", import.meta.url);
|
||||
|
||||
async function makeTempDir() {
|
||||
return mkdtemp(join(tmpdir(), "omo-claude-telemetry-sync-"));
|
||||
}
|
||||
|
||||
async function runSync(args) {
|
||||
const { syncTelemetryComponent } = await import(SCRIPT_PATH);
|
||||
return syncTelemetryComponent(args);
|
||||
}
|
||||
|
||||
test("#given stale telemetry component files #when sync runs #then pure package telemetry source rewrites the component copy", async () => {
|
||||
// given
|
||||
const root = await makeTempDir();
|
||||
const sourceDir = join(root, "source");
|
||||
const componentDir = join(root, "component");
|
||||
await mkdir(sourceDir);
|
||||
await mkdir(componentDir);
|
||||
await writeFile(join(sourceDir, "atomic-write.ts"), "export const source = true\n", { flush: true });
|
||||
await writeFile(join(componentDir, "atomic-write.ts"), "export const stale = true\n", { flush: true });
|
||||
|
||||
try {
|
||||
// when
|
||||
const result = await runSync({
|
||||
sourceDir,
|
||||
componentDir,
|
||||
files: ["atomic-write.ts"],
|
||||
check: false,
|
||||
});
|
||||
|
||||
// then
|
||||
assert.deepEqual(result, {
|
||||
checked: false,
|
||||
changed: ["atomic-write.ts"],
|
||||
});
|
||||
assert.equal(await readFile(join(componentDir, "atomic-write.ts"), "utf8"), "export const source = true\n");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("#given a missing pure telemetry source file #when sync runs #then it fails with the missing source path", async () => {
|
||||
// given
|
||||
const root = await makeTempDir();
|
||||
const sourceDir = join(root, "source");
|
||||
const componentDir = join(root, "component");
|
||||
await mkdir(componentDir);
|
||||
await writeFile(join(componentDir, "atomic-write.ts"), "export const stale = true\n", { flush: true });
|
||||
|
||||
try {
|
||||
// when / then
|
||||
await assert.rejects(
|
||||
runSync({
|
||||
sourceDir,
|
||||
componentDir,
|
||||
files: ["atomic-write.ts"],
|
||||
check: false,
|
||||
}),
|
||||
(error) => error instanceof Error && error.message.includes(join(sourceDir, "atomic-write.ts")),
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user