From c39d16c3784033f586696c613ab362b55f4c6249 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 29 May 2026 13:01:15 +0900 Subject: [PATCH] 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) --- packages/omo-claude/scripts/.gitkeep | 0 .../scripts/sync-telemetry-component.mjs | 111 ++++++++++++++++++ .../scripts/sync-telemetry-component.test.mjs | 70 +++++++++++ 3 files changed, 181 insertions(+) delete mode 100644 packages/omo-claude/scripts/.gitkeep create mode 100644 packages/omo-claude/scripts/sync-telemetry-component.mjs create mode 100644 packages/omo-claude/scripts/sync-telemetry-component.test.mjs diff --git a/packages/omo-claude/scripts/.gitkeep b/packages/omo-claude/scripts/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/omo-claude/scripts/sync-telemetry-component.mjs b/packages/omo-claude/scripts/sync-telemetry-component.mjs new file mode 100644 index 000000000..5f4cf0140 --- /dev/null +++ b/packages/omo-claude/scripts/sync-telemetry-component.mjs @@ -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; + }); +} diff --git a/packages/omo-claude/scripts/sync-telemetry-component.test.mjs b/packages/omo-claude/scripts/sync-telemetry-component.test.mjs new file mode 100644 index 000000000..5814af64c --- /dev/null +++ b/packages/omo-claude/scripts/sync-telemetry-component.test.mjs @@ -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 }); + } +});