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